Skip to content

[Feature][QDP] One encode path across Python, Rust and CUDA (tracking) #1477

Description

@ryankert01

TL;DR

Adding one encoding to QDP today means writing it three times: a CUDA host launcher plus extern "C" declaration plus no-CUDA stub, an 8-method Rust encoder (f32/f64 × single/batch × host/device), and a hand-matched PyO3 branch. #1268 added one f32 variant of angle encoding and touched 9 files across 2 languages for 1,135 lines.

I refactored this so an encoding is one device-only .cu file, one ~80-line Rust descriptor, and one PyTorch reference function. The data-movement engine (dual-stream pinned upload, prefetch, buffer pool, DLPack) is untouched, the public Python API is unchanged, and the repo's standard benchmarks show no measurable throughput or latency change.

What

Six commits on a branch, each independently green (Rust tests, clippy, no-CUDA build, Python suites including a new parity grid). I'll open them as PRs against this issue:

  • Parity grid + baseline tool — one test over encoding × precision × shape × input location against torch_ref.py (which now covers phase); benchmark/baseline.py captures and compares throughput/latency with a threshold.
  • Runtime kernel loadingbuild.rs compiles each .cu to a device-only fatbin and embeds it; qdp_kernels::registry loads through the CUDA driver API on first use and exposes one generic launch + kernel_args!. A test proves every embedded symbol resolves.
  • Encoder collapse — a Kernel trait with a single launch on device-resident data, Input (host slice or device pointer on a stream), and QdpEngine::encode(input, shape, num_qubits, encoding) replacing 24 named methods (old names kept as one-line shorthands in compat.rs). Bindings become PyInput::from_py + one call.
  • Host-path fixes found while benchmarking (amplitude CPU finite pass, basis device cast).
  • Remove launchers — 26 host launchers, 26 declarations, 26 stubs, the static nvcc compile and the cc dependency are gone.
  • Docs — DEVELOPMENT.md architecture section, a "which files do I touch" table, the three-file recipe, make check-kernels, make parity.
Surface Before After
CUDA host launchers / extern "C" decls / no-CUDA stubs 26 / 26 / 26 0
qdp-kernels/src/lib.rs 726 lines 57 lines
CUDA sources (device code only after) 2,633 lines 1,434 lines
Encoder trait methods per encoding 8 1
Angle encoder 868 lines 89 lines
Public encode* methods on QdpEngine 24 1 (+ shorthands)
PyO3 engine methods 11 6
Encodings that stream from Parquet 3 6
Encodings that read a CUDA tensor in place 5 6

Net: 62 files, +4,334 / −9,974 lines. Bonus behaviour: phase now encodes from CUDA tensors, basis accepts float64 CUDA tensors, streaming Parquet works for all six encodings, NumPy float32 input is accepted.

Why

Contributors should be able to add or tune an encoding knowing CUDA and a little Rust, add a reader knowing Rust, and add a loader feature knowing Python. Today every encoder change needs all three, and the FFI signatures are matched by eye (a mismatch is UB, not a compile error).

How

Performance. Old (pre-refactor commit) vs new, built into separate venvs, run with benchmark_throughput.py, benchmark_latency.py and benchmark_e2e.py at their default arguments, 5 alternating rounds on an idle RTX 6000 Ada. Medians, with new/old ratio:

Encoding Throughput old → new (vec/s) Latency old → new (ms/vec)
amplitude 12,660 → 5,667 (0.45, see below) 0.096 → 0.097 (1.01)
angle 23,110 → 22,993 (0.99) 0.043 → 0.044 (1.02)
basis 68,651 → 77,569 (1.13) 0.013 → 0.015 (1.15)
iqp 22,140 → 22,045 (1.00) 0.046 → 0.045 (0.98)
iqp-z 49,597 → 51,432 (1.04) 0.020 → 0.020 (1.00)

The amplitude throughput row is benchmark noise, not code: that benchmark alternates between ~11k and ~5.5k vec/s on consecutive runs of the same build (old: 10,563 5,665 11,367 5,694 11,434 5,606; new: 5,973 12,592 5,761 10,385 5,569 5,390). Amplitude latency on the same workload is identical, and an nsys trace shows the three amplitude kernels at the same grid and the same time on both builds (69.0 / 29.7 / 2.5 µs per batch), with the batch dominated by the pageable 16 MB host copy. Per-call amplitude encode, interleaved: 3.73 vs 3.76 ms and 3.60 vs 3.71 ms.

Behaviour changes to note in review

  • Parquet streaming now honours engine precision (previously always complex128).
  • Error message wording was preserved where tests asserted it.

Follow-ups (separate issues once this lands)

  • Delete the compat.rs shorthands once callers use encode.
  • Delete the now-unused single-sample kernels from the .cu files.
  • Consider pinned producer buffers: the amplitude pipeline is bound by a pageable host copy per batch, not by kernels.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions