Fused CUDA kernels for the GNN encoder and loss computation, targeting NVIDIA Ampere (sm_80) and Ada Lovelace (sm_89) architectures.
| Kernel | Replaces | Key optimisation |
|---|---|---|
fused_edge_features |
2x gather + add + sub+abs + concat (5 launches => 1) | Template-dispatched float4 vectorised loads |
fused_norm_residual |
LayerNorm + Dropout + residual add (3 launches => 1) | 2-pass fused stats (sum+sum_sq), shared gamma/beta cache, template dropout elimination |
graph_norm_bce |
BCE + scatter_add + normalize (3 launches => 2) | Warp-cooperative segmented prefix sum (64× fewer atomicAdd vs naive scatter) |
- CUDA Toolkit ≥ 12.0
- PyTorch ≥ 2.5 with CUDA support
- A GPU with compute capability ≥ 8.0 (Ampere or newer)
From the project root:
uv run scripts/build_kernels.py build_ext --inplaceVerify:
uv run python -c "import kernels; print(kernels.AVAILABLE)" # Truefrom kernels.build import build
module = build() # compiles on first call, cached afterwardsJIT is slower on first run but doesn't require the build step.
Important
The "cuda" backend is inference and benchmarking only. The custom kernels do not implement autograd backward passes. For training, use "pytorch" or "compiled".
Kernels activate automatically when the compute backend is set to "cuda":
from model.ops import set_backend
set_backend("cuda") # inference only - no autograd backwardOr via environment variable:
QECDEC_BACKEND=cuda uv run scripts/eval_gnn.py \
--checkpoint outputs/runs/direct/best.ptOr in benchmarking:
uv run scripts/export_trt.py \
--checkpoint outputs/runs/direct/best.pt --backends cudaIf kernels are not built, set_backend("cuda") falls back to PyTorch
with a warning.
Equivalence tests verify CUDA kernels match PyTorch output within atol=1e-5:
uv run scripts/build_kernels.py build_ext --inplace
uv run pytest tests/test_ops_equivalence.py -k "CUDA" -vTests are automatically skipped without a GPU or without built kernels.
One thread-block per edge, each block processes one edge's hidden dimension.
- Template dispatch:
<bool UseVec4>selected at compile time - float4 vectorised path (whenhidden_dim % 4 == 0) or scalar fallback, with zero branch cost in the hot loop. __launch_bounds__(256)for register allocation hints.- High parallelism from massive block count (batch×edges ≈ 100k+ blocks), not from wide blocks.
One thread-block per row (node or edge embedding).
- 2-pass instead of 3: mean and variance computed together via
sum + sum_sqin a single read of the input row, thenvar = E[x²] - E[x]². Saves ~14% bandwidth vs the naive mean-then-variance approach. Numerically sufficient for float32 athidden_dim ≤ 512(verified: max error 2.4e-7 vs Welford gold standard). - Shared memory cache for gamma/beta vectors - loaded cooperatively once per block, eliminating repeated global reads in the normalise+scale pass.
- Template
<bool Training>: inference path compiled without curand state allocation (~40 registers freed), dropout branch, or scale computation. - Reproducible seed from PyTorch's default CUDA generator (
at::cuda::detail::getDefaultCUDAGenerator()) instead of non-deterministicsteady_clock::now().
Two-kernel pipeline: scatter (BCE => per-graph accumulators) then reduce (mean-of-means).
- Warp-cooperative segmented prefix sum: exploits the fact that PyG
Batchproduces sorted (monotonic)edge_graphindices. Within each warp of 32 threads, edges from the same graph form contiguous segments.__shfl_up_syncwith graph-ID equality check accumulates BCE values within each segment; only the segment-end lane emits a singleatomicAdd. Result: ~32× fewer atomics than per-thread scatter. - Precomputed graph counts:
torch::bincount(edge_graph)replaces the second set ofatomicAddfor counting edges. Combined: ~64× fewer atomics than the original (verified: d=7 batch=128 => 797k => 12k atomicAdd). - Boundary handling: warps that straddle graph boundaries correctly emit separate atomics per segment (at most 2 per warp).
All kernels use:
at::cuda::getCurrentCUDAStream()- correct behaviour with AMP, DataParallel, and multi-stream pipelines.C10_CUDA_KERNEL_LAUNCH_CHECK()- catches asynchronous kernel launch failures.__restrict__pointer hints on all kernel arguments.- Anonymous namespaces for internal symbols.
src/kernels/
├── README.md
├── __init__.py # AVAILABLE flag
├── ops.py # Python wrappers (fallback to PyTorch on CPU)
├── build.py # JIT build config
└── cpp/
├── fused_edge_features.cu # Kernel #1
├── fused_norm_residual.cu # Kernel #2
├── graph_norm_bce.cu # Kernel #3
└── bindings.cpp # pybind11 => kernels._C