CUDA/C++20 benchmark and correctness suite for post-quantum and proof-system-adjacent cryptographic kernels.
This repository is intentionally focused: CPU references, CUDA kernels, deterministic correctness tests, CMake, CI, and benchmark output for a small set of workloads that matter for PQC and proof systems.
This is research and benchmark code, not production cryptography.
The implementations are not audited, not constant-time, and not suitable for protecting real assets. The Poseidon2 path is a Poseidon2-style Goldilocks permutation with deterministic benchmark constants; it is not a standardized Poseidon2 parameter set.
- CUDA kernels for field arithmetic, NTTs, SHA-256 Merkle trees, ML-KEM-style polynomial operations, and Poseidon2-style Merkle forests.
- Straightforward CPU reference implementations used for correctness checks.
- Deterministic vectors for field math, SHA-256, ML-KEM-style polynomials, and Poseidon2-style leaf/pair/root hashes.
- Batching for small PQC polynomials, because one Kyber-sized polynomial is too small to be a useful GPU workload.
- Device-resident Poseidon2-style Merkle forest levels, with roots copied back at the end.
- Benchmark reporting that separates host-transfer-included timing from Poseidon2 device-only CUDA event timing.
- Separate resident-buffer benchmark rows for ML-KEM batches and Poseidon2 forests, so wrapper overhead and GPU-resident throughput are not conflated.
| Module | Status | Current Limitations |
|---|---|---|
| Goldilocks field + radix-2 NTT | Implemented | Readable stage kernels, not a fully fused production NTT |
| SHA-256 Merkle tree | Implemented | Root builder only; no GPU auth-path extraction |
| Batched ML-KEM/Kyber primitive path | Implemented benchmark primitive | Not full ML-KEM keygen/encap/decap |
| ML-KEM resident-buffer benchmark | Implemented benchmark path | Device-resident timing helper, not a reusable production API |
| Poseidon2-style Merkle forest | Experimental benchmark path | Benchmark constants, not standardized Poseidon2 parameters |
| Poseidon2 resident-buffer benchmark | Implemented benchmark path | Benchmarks repeated resident builds, not standardized Poseidon2 |
- field:
p = 2^64 - 2^32 + 1 - CPU field add/mul reference
- CUDA field add/mul kernels
- radix-2 NTT and inverse NTT
- CPU-vs-CUDA correctness tests
- local SHA-256 implementation
- CPU Merkle root builder
- CUDA leaf and parent hashing kernels
- deterministic CPU-vs-CUDA root checks
- modulus:
q = 3329 - degree:
n = 256 - primitive 256th root:
17 - CPU schoolbook polynomial multiplication
- CPU NTT-based polynomial multiplication
- CUDA batched NTT
- CUDA batched NTT-based polynomial multiplication
- CUDA resident-buffer benchmark helpers for batched NTT and poly mul
- benchmark batches in full mode:
1,1024,10000,100000
The point is batching. A single Kyber-sized polynomial is too small to make CUDA look good; batched polynomial operations are the relevant GPU story.
- Poseidon2-style hash over the Goldilocks field
- CPU reference hash, forest root builder, and proof path generation
- CUDA leaf and parent hash kernels
- intermediate Merkle levels kept GPU-resident
- host-transfer-included timing and CUDA-event device-only timing
- resident-buffer benchmark helper for repeated GPU-resident builds
- CPU verification of generated authentication paths
The exact benchmark permutation shape, round constants, and domain tags are documented in docs/poseidon2-parameters.md.
Full-mode benchmark forest shapes:
- one tree with
2^20leaves 1024trees with2^10leaves65536small authentication trees with16leaves each
NVIDIA cuPQC directly targets ML-KEM and ML-DSA and highlights SHA-2, SHA-3, SHAKE, Poseidon2, and Merkle trees. Its public material reports H100 throughput for ML-KEM-768 operations in millions of ops/sec and shows the expected CUDA pattern for Merkle workloads: larger trees expose more parallelism.
ZKProphet identifies MSM and NTT as the dominant GPU prover kernels and calls out CPU-GPU transfer and kernel-launch overheads as important bottlenecks. This repo emphasizes batching, residency, NTTs, and Merkle forests for that reason.
The Cambridge/OpenTitan NTT acceleration paper frames NTT as a core bottleneck for Kyber/ML-KEM and Dilithium/ML-DSA style systems.
Requirements:
- CMake 3.24+
- C++20 compiler
- NVIDIA CUDA Toolkit for GPU builds
CPU-only:
./scripts/build.sh -DCPB_ENABLE_CUDA=OFFCUDA:
./scripts/build.sh -DCPB_ENABLE_CUDA=ONPin an architecture when building on a known GPU:
./scripts/build.sh -DCPB_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75CPU-only CI path:
./scripts/test.sh -DCPB_ENABLE_CUDA=OFFCUDA correctness path:
./scripts/test.sh -DCPB_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75The correctness suite checks deterministic vectors, CPU roundtrips, edge-case validation, and CPU-vs-CUDA parity when CUDA is available. Mismatch reports include the first mismatching index, coefficient, byte, or digest word.
Quick mode is the default:
./scripts/bench.sh -DCPB_ENABLE_CUDA=OFF
./scripts/bench.sh -DCPB_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75Full CUDA mode runs the large batched ML-KEM and Poseidon2 forest rows:
CPB_BENCH_MODE=full ./scripts/bench.sh -DCPB_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75Full scalar CPU Poseidon2 rows are opt-in because they are intentionally large:
CPB_POSEIDON2_FULL_CPU=1 CPB_BENCH_MODE=full ./scripts/bench.sh -DCPB_ENABLE_CUDA=OFFBenchmark output prints the mode, GPU name, CUDA runtime/driver versions, architecture, elapsed time, throughput, and whether Poseidon2 timings include host transfers or device-only CUDA event timing.
Current public CUDA wrappers allocate and copy per call. Those wrapper numbers are honest API timings. Resident rows are printed separately and keep inputs, scratch buffers, and intermediate levels on the GPU across repeated timed iterations.
See docs/benchmark-methodology.md for the exact recording checklist.
Latest quick CUDA validation was run on Brev cuda-p2-test, machine type
g4dn.xlarge.
- GPU: NVIDIA Tesla T4,
sm_75, 14.56 GiB visible memory - CUDA container:
nvidia/cuda:12.4.1-devel-ubuntu22.04 - CUDA compiler: NVIDIA 12.4.131
- CMake: 3.29.6
- Command shape:
./scripts/test.sh -DCPB_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75 - CUDA build passed
- correctness tests passed
- quick benchmark passed
Latest quick-mode T4 results:
| Primitive | Workload | Time ms | Throughput |
|---|---|---|---|
| Field mul | n=65536 |
0.805 | 81.40 Mops/s |
| Radix-2 NTT | n=16384 |
0.514 | 31.87 Mops/s |
| SHA-256 Merkle | n=16384 |
0.490 | 33.40 Mops/s |
| ML-KEM NTT | batch=1024 |
0.790 | 1296.837 Kpoly/s |
| ML-KEM poly mul | batch=1024 |
1.258 | 814.126 Kpoly/s |
Latest quick-mode resident-buffer rows:
| Primitive | Workload | Device ms | Throughput |
|---|---|---|---|
| ML-KEM NTT resident | batch=1024 |
0.129 | 7962.377 Kpoly/s |
| ML-KEM poly mul resident | batch=1024 |
0.394 | 2602.028 Kpoly/s |
| Poseidon2 resident | 1 x 2^10 leaves |
1.510 | 1.36 Mhash/s |
| Poseidon2 resident | 32 x 2^10 |
2.161 | 30.31 Mhash/s |
Latest quick-mode Poseidon2-style Merkle forest rows:
| Workload | Host ms | Device ms | Host Mhash/s | Host GB/s | Device Mhash/s | Device GB/s |
|---|---|---|---|---|---|---|
1 x 2^10 leaves |
1.801 | 1.639 | 1.14 | 0.07 | 1.25 | 0.07 |
32 x 2^10 |
2.999 | 2.174 | 21.84 | 1.30 | 30.12 | 1.80 |
The quick-mode T4 numbers include current wrapper allocation/copy overhead, and the Poseidon2 rows also report device-only CUDA event timing. They are intended as a cheap smoke benchmark, not the full throughput headline.
Previously recorded full-mode T4 reference on Brev g4dn.xlarge:
| Workload | Host ms | Device ms | Host Mhash/s | Host GB/s | Device Mhash/s | Device GB/s |
|---|---|---|---|---|---|---|
1 x 2^20 leaves |
46.208 | 31.920 | 45.39 | 2.71 | 65.70 | 3.92 |
1024 x 2^10 |
47.932 | 31.493 | 43.73 | 2.61 | 66.56 | 3.97 |
65536 x 16 |
44.943 | 29.841 | 45.20 | 2.69 | 68.08 | 4.06 |
Previously recorded full-mode batched ML-KEM/Kyber-style rows on T4:
| Primitive | Workload | Time ms | Throughput Kpoly/s |
|---|---|---|---|
| ML-KEM NTT | batch=1 |
0.244 | 4.094 |
| ML-KEM NTT | batch=1024 |
0.786 | 1303.056 |
| ML-KEM NTT | batch=10000 |
4.394 | 2276.023 |
| ML-KEM NTT | batch=100000 |
59.778 | 1672.846 |
| ML-KEM poly mul | batch=1 |
0.583 | 1.715 |
| ML-KEM poly mul | batch=1024 |
1.198 | 854.695 |
| ML-KEM poly mul | batch=10000 |
8.330 | 1200.443 |
| ML-KEM poly mul | batch=100000 |
82.597 | 1210.699 |
Historical A6000 validation for the Goldilocks/SHA-256 path only:
| Primitive | Size | Baseline | Current | Change |
|---|---|---|---|---|
| NTT | 4096 | 4.32 Mops/s | 10.90 Mops/s | 2.52x |
| NTT | 16384 | 12.57 Mops/s | 31.25 Mops/s | 2.49x |
| Field mul | 65536 | 84.91 Mops/s | 94.82 Mops/s | 1.12x |
| Merkle SHA-256 | 16384 | 39.97 Mops/s | 41.70 Mops/s | 1.04x |
The A6000 table predates the ML-KEM and Poseidon2-style forest additions. Do not compare it against the T4 tables as if they were from the same machine or code path.
- CPU references are deliberately direct and easy to audit.
- CUDA vector kernels use one thread per field element.
- CUDA NTTs use explicit stage kernels and device twiddle generation.
- Batched ML-KEM kernels map each polynomial to a CUDA block.
- Poseidon2-style Merkle forests copy leaves once, alternate resident level buffers, and copy only roots back.
- Resident benchmark helpers allocate/copy once, then report CUDA-event device-only timing for repeated ML-KEM or Poseidon2 runs.
- Goldilocks CUDA multiplication uses
__umul64hito recover the high half of a 64x64 product and folds with2^64 = 2^32 - 1 mod p.
./scripts/format.sh --check
./scripts/format.sh
cmake --build build --target format-checkThe format targets require clang-format.
include/ public headers
src/ CPU and CUDA implementations
tests/ correctness tests
bench/ benchmark executable
scripts/ build, test, benchmark, and formatting helpers
docs/ design, parameter, and benchmark notes
- Standardized Poseidon2 parameters and published vectors.
- CUDA auth-path extraction from resident tree levels.
- Fused NTT stages and shared-memory tiling after benchmark baselines are stable.
- A reproducible benchmark matrix by GPU architecture.