Skip to content

perf(qdp): remove mid-stream D2H norm validation from batch GPU-pointer encoding paths #1370

Description

@ryankert01

Update (2026-06-26): Two corrections after re-checking main (full detail in the comment below).

  1. The single-sample "prior work" cited below is not on main. The launch_amplitude_encode_f32_device_norm kernel exists only on an unmerged perf branch (commit 4ebe417b3); PR feat(qdp): hoist encode_from_gpu_ptr_f32 onto QuantumEncoder trait #1310 is the trait-dispatch refactor, not the device-norm kernel. The single-sample path on main still does the blocking D2H round-trip too.
  2. The batch path needs neither a policy decision nor a new kernel. The batch encode kernel already reads inv_norms from device memory (amplitude.rs:660), so the fix is to defer validation: launch norm kernel → encode kernel on the same stream → single sync at the end → D2H-copy norms and validate after. This removes the mid-stream stall while keeping norm validation a hard guarantee — no strict flag, no new CUDA kernel, and no remove-vs-optional-vs-keep policy call needed.

Summary

The batch amplitude encoding paths (encode_batch_from_gpu_ptr_f32, encode_batch_from_gpu_ptr) perform a blocking CPU round-trip for norm validation between the norm kernel and the encode kernel. This serializes two GPU kernel launches unnecessarily and is the primary reason Mahout's encode-only throughput falls behind PyTorch's reference implementation.

Current pipeline (batch path)

launch_l2_norm_batch_f32   (GPU, queued)
↓
cudaStreamSynchronize      ← STALL #1
dtoh_sync_copy(norms)      ← PCIe transfer: N floats CPU-bound
CPU: check norms[i] for zero/NaN
↓
launch_amplitude_encode_batch_f32  (GPU, queued)
↓
cudaStreamSynchronize      ← STALL #2

Two syncs + one D2H copy per batch, even when all norms are valid (which is almost always).

Benchmark evidence

Running benchmark_pytorch_ref.py --qubits 16 --batches 200 --batch-size 64 (encode-only mode — both frameworks start with data already on GPU):

Framework Throughput
PyTorch GPU 228,825 vec/s
Mahout 66,615 vec/s
Ratio 0.3x (Mahout 3.4x slower)

PyTorch's amplitude_encode uses torch.linalg.vector_norm + data / norms.clamp(min=1e-10) — everything stays on GPU, no CPU validation.

In end-to-end mode (data gen + H2D + encode), Mahout is 7.8x faster than PyTorch because the full pipeline cost dominates. The gap only appears when isolating kernel work.

Related prior work

⚠️ Correction (see Update at top): this was measured on an unmerged perf branch, not main.

The idea: a launch_amplitude_encode_f32_device_norm kernel reads inv_norm from device memory, letting the norm kernel and encode kernel chain on the same stream with a single sync at the end. On the unmerged branch (commit 4ebe417b3) this yielded 1.22x on the single-sample path (39.7 → 32.6 µs/sample at 16 qubits). It is not present on main.

The batch path has the same structure and benefits from the same idea — and, per the Update at top, it already passes inv_norms as a device pointer, so it gets there by deferring validation with no new kernel.

Related issues / PRs

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions