mirror of
https://github.com/lux-apps/papers.git
synced 2026-10-30 18:15:21 +00:00
First measured scaling laws for post-quantum threshold consensus on heterogeneous hardware. Real numbers from M1 Max with wgpu-native 29.0.0 (real WebGPU, not polyfill). Main findings: * CPU per-NTT cost flat at ~1.4 us across n=1..65536. * Metal: 1.5 ms per dispatch, crosses CPU at n~3260, peaks 1.48x at n=65536. * WGSL: 2.7 ms per dispatch, asymptotic per-op (1.58 us) HIGHER than CPU (1.38 us) — never crosses on M1 Max. Constraint: uint32-emulated u64 vs Metal's native u64. * Pulsar production hot path n=2688 sits below both crossovers. Per-validator GPU acceleration unprofitable; aggregator-side multi-validator batching wins. Three empirical scaling laws derived: * L1 batch-size law: T_GPU/poly(n) = D/n + tau, crossover n* = D/(tau_CPU - tau_GPU). * L2 validator-parallelism law: aggregator amortizes dispatch over k validators. * L3 epoch-amortization law: workloads that batch at epoch boundary cross n* far earlier. Sections (660 LoC of LaTeX): 01-introduction — claim summary + sharpest finding 02-methodology — bench harness, hardware, threats to validity 03-measurements — TSV-grade tables of NTT sweep + workload-level (ZAP, FHE, reshare) 04-scaling-laws — three theorems with fitted constants 05-implications — concrete posture for backend.Policy defaults 06-honesty — what was measured vs not (NVIDIA, Block-STM, DEX, e2e) 99-conclusion — production posture + queued workstreams Data/ directory carries the raw TSV (ntt-sweep-m1max-2026-05-04.tsv) emitted by the new corona_lattice_ring_sweep_bench. Honest scope: NVIDIA H100, Block-STM parallel EVM throughput, DEX matching engine, and end-to-end consensus+EVM+DEX+FHE pipeline are all queued for v0.3.0; not in this paper. Section 06 documents the boundary.
118 lines
5.0 KiB
TeX
118 lines
5.0 KiB
TeX
\section{Methodology}
|
|
\label{sec:methodology}
|
|
|
|
\subsection{Measurement target}
|
|
|
|
The benchmark target is the canonical Lux Pulsar NTT primitive:
|
|
forward Number-Theoretic Transform over
|
|
$R_q = \mathbb{Z}_q[X]/(X^N+1)$ with $N = 256$ and
|
|
$q = 0x1000000004A01$. Roots are bit-reversed Montgomery-form
|
|
constants generated once at SubRing construction (the same
|
|
$\mathsf{GenSubRingRoots}$ used by every Lux protocol).
|
|
|
|
The reference CPU body is
|
|
\texttt{lux::crypto::corona::lattice\_ring::NTTStandard} in
|
|
\texttt{corona/cpp/lattice\_ring.cpp}. It is byte-equal to
|
|
the upstream Lattigo \texttt{ring.SubRing.NTT} (validated by
|
|
\texttt{corona\_canonical\_lattice\_ring\_kat} which replays a
|
|
6-op $\times$ 64-entry corpus). The Metal kernel is at
|
|
\texttt{corona/gpu/metal/canonical\_lattice\_ring.metal}; the
|
|
WGSL kernel is at \texttt{corona/gpu/wgsl/canonical\_lattice\_ring.wgsl}.
|
|
All three produce byte-identical output by KAT
|
|
(\texttt{corona\_canonical\_lattice\_ring\_metal\_kat\_test} and
|
|
\texttt{corona\_canonical\_lattice\_ring\_wgsl\_kat\_test}).
|
|
|
|
\subsection{Measurement host}
|
|
|
|
Hardware: Apple M1 Max, 10-core (8P + 2E), 64 GB unified memory,
|
|
32-core integrated GPU. macOS 14.5, clang 17, CMake 4.3.2.
|
|
|
|
GPU runtime: Apple Metal 3 native; wgpu-native 29.0.0 from Homebrew
|
|
(\texttt{libwgpu\_native.dylib}, dispatching to Metal under the hood
|
|
on Darwin).
|
|
|
|
CPU runtime: Apple's loop-unrolled-by-16 NTT
|
|
(\texttt{nttUnrolled16Lazy}) with Montgomery reduction. No SIMD
|
|
GOEXPERIMENT path on arm64; the relevant Apple silicon hand-tuning
|
|
in the Lattigo port has been validated to match the Go reference
|
|
output bit-for-bit.
|
|
|
|
\subsection{Bench harness}
|
|
|
|
Source: \texttt{luxcpp/crypto/corona/test/lattice\_ring\_sweep\_bench.cpp}
|
|
(this paper's primary artefact).
|
|
|
|
For each batch size $n \in \{1, 4, 16, 64, 256, 1024, 2688, 4096,
|
|
16384, 65536\}$:
|
|
\begin{enumerate}[nosep,leftmargin=2em]
|
|
\item Construct $n$ random polynomials with deterministic seed
|
|
(\texttt{coeff[i] = (sample\_input[i] + k * 0x9e3779b1) mod q}).
|
|
\item For each lane $\in \{$CPU, Metal, WGSL$\}$:
|
|
\begin{enumerate}[nosep,leftmargin=2em]
|
|
\item Warmup: 4 calls.
|
|
\item Timed loop: $\mathrm{iters}$ calls, where
|
|
$\mathrm{iters} = \min(\max(\lceil 2^{18}/n \rceil, 4), 2000)$.
|
|
The aggregate target is $\sim 256K$ polys per timed run, capped
|
|
at 2{,}000 iterations to keep wall time bounded.
|
|
\item Wall-clock measured by \texttt{std::chrono::steady\_clock}.
|
|
The output buffer's first uint64 is XORed back into the input to
|
|
defeat the optimizer.
|
|
\end{enumerate}
|
|
\item Report \texttt{per-batch} latency (one full $n$-poly NTT
|
|
batch wall-clock) and \texttt{per-poly} latency
|
|
(\texttt{per-batch / n}).
|
|
\end{enumerate}
|
|
|
|
\subsection{What is being timed}
|
|
|
|
For Metal: one
|
|
\texttt{canonical\_lattice\_ring\_metal\_ntt\_batch} call per
|
|
iteration. Internally this loads the metallib (cached after first
|
|
call), creates one MTLCommandBuffer with $n$ threadgroups, dispatches,
|
|
and waits until completion. The per-call fixed cost is
|
|
$\sim 1.5$ ms.
|
|
|
|
For WGSL via wgpu-native: one
|
|
\texttt{lux\_corona\_lattice\_ring\_wgpu\_ntt} call per iteration.
|
|
Internally this allocates 11 buffers (3 storage + 6 uniform + 2
|
|
copy targets), encodes a compute pass with one bind group, submits
|
|
to the device queue, and reads back via a mapped staging buffer.
|
|
The per-call fixed cost is $\sim 2.7$ ms (slightly higher than Metal
|
|
because of the staging-buffer readback).
|
|
|
|
For CPU: $n$ serial calls to the scalar
|
|
\texttt{NTTStandard}. There is no batched CPU primitive — the CPU
|
|
is fully synchronous, scalar Montgomery arithmetic.
|
|
|
|
\subsection{Threats to validity}
|
|
|
|
\paragraph{Cold dispatch dominates at small batches.}
|
|
Both GPU backends pay a fixed per-call cost. At $n=1$ this
|
|
swamps any per-poly throughput advantage. We deliberately include
|
|
$n=1$ in the sweep to ground the fixed-cost constant, but the
|
|
production-relevant sizes are $n \geq 256$.
|
|
|
|
\paragraph{Buffer reuse not amortized.}
|
|
The bench allocates and destroys all GPU buffers per iteration (it
|
|
does not pool them across iterations). A production driver that
|
|
caches the bind group + command encoder for steady-state batches
|
|
would reduce the per-call fixed cost. Since the bench measures every
|
|
iteration with full alloc/free, the reported numbers are a
|
|
\emph{conservative} lower bound on GPU performance — a tuned
|
|
production driver would do at least this well, plausibly $1.5\times$
|
|
to $2\times$ better at small batches.
|
|
|
|
\paragraph{No multi-stream concurrency.}
|
|
Both Metal and WGSL paths submit one command buffer per call and
|
|
wait for completion before returning. A real production deployment
|
|
would pipeline command buffers (submit batch $k+1$ before batch $k$
|
|
finishes) and bury the dispatch cost. We measured the single-stream
|
|
synchronous case to bound the worst-case latency.
|
|
|
|
\paragraph{Single hardware target.}
|
|
This paper measures Apple M1 Max only. NVIDIA H100 + AMD MI300X
|
|
characterizations are out of scope (no hardware on the measurement
|
|
host); the CUDA driver compiles cleanly and the KAT contract
|
|
holds, but absolute GPU/CPU ratios on those targets will differ
|
|
in both magnitude and sign of the curve.
|