You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 loading — build.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.
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
.cufile, 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:
torch_ref.py(which now covers phase);benchmark/baseline.pycaptures and compares throughput/latency with a threshold.build.rscompiles each.cuto a device-only fatbin and embeds it;qdp_kernels::registryloads through the CUDA driver API on first use and exposes one genericlaunch+kernel_args!. A test proves every embedded symbol resolves.Kerneltrait with a singlelaunchon device-resident data,Input(host slice or device pointer on a stream), andQdpEngine::encode(input, shape, num_qubits, encoding)replacing 24 named methods (old names kept as one-line shorthands incompat.rs). Bindings becomePyInput::from_py+ one call.ccdependency are gone.make check-kernels,make parity.extern "C"decls / no-CUDA stubsqdp-kernels/src/lib.rsencode*methods onQdpEngineNet: 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.pyandbenchmark_e2e.pyat their default arguments, 5 alternating rounds on an idle RTX 6000 Ada. Medians, with new/old ratio: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
Follow-ups (separate issues once this lands)
compat.rsshorthands once callers useencode..cufiles.