A fixed standard for gradient-free post-training by random optimization, in the spirit of the nanoGPT speedrun: fix the hardware, fix the model, fix the protocol, and race the wall clock — then report quality on a held-out metric everyone shares.
Given pretrained weights W₀, draw perturbations W(s,σ) = W₀ + σ·ε_s for seeds
s and scales σ, score each on a small train set (greedy decode + task
reward), keep the top-k, and majority-vote ensemble them on the test set.
No gradients. The cost is dominated by evaluating perturbations, so the figure
of merit is seeds evaluated per second.
The upstream switch (utils/worker_extn.py) did, per seed:
- loop every parameter, allocate a full model-sized
torch.randnnoise tensor, add it; - restore by regenerating the same noise and subtracting it;
torch.cuda.synchronize()+torch.cuda.empty_cache()every call.
That is two full-model RNG materializations per seed plus cache churn. For a 72B model (144 GB bf16) it is ~288 GB of noise traffic per seed, ×population.
The fast runtime (core/perturb.py) switches to dense Rademacher noise
(ε ∈ {−1,+1}) and reconstructs absolutely from a resident base copy:
W = W₀ + σ · R(seed, global_index)
Ris a counter-based hash (murmur3fmix32over a folded 64-bit position), evaluated inline in a Triton kernel — no noise tensor is materialized.- Reconstruction is absolute from base, so it is drift-free regardless of history and identical across engines/TP layouts.
- The restore pass disappears: the next seed's reconstruct overwrites the live
weights from the base. Per-call
synchronize/empty_cacheare removed (one sync per seed).
Net per seed: 2 full-model RNG materializations → 0, 2 passes → 1.
The RNG is defined by us (not Triton's Philox) so the Triton kernel and a
pure-torch reference produce bit-identical signs — seeds reproduce across
backends, and the CPU reference doubles as the no-Triton fallback. This is
asserted bit-for-bit in tests/test_kernel_gpu.py.
Why Rademacher is principled: it is zero-mean, unit-variance, symmetric, and the
maximum-entropy ±1 distribution; E‖σε‖₂ = σ·√d matches the Gaussian case, so a
given σ is comparable (Gaussian remains available via noise: gaussian, on the
slower materializing path).
configs/standard_8xh100_qwen72b.yaml:
| Hardware | 8× H100-80GB, single node |
| Model | Qwen/Qwen2.5-72B-Instruct (dense, bf16) |
| Topology | TP=8, 1 engine (see below) |
| Selection task | GSM8K, 200 train / 200 test, greedy, max_tokens=1024 |
| Population | 512 seeds, σ ∈ {5e-4, 1e-3, 2e-3} |
| Ensemble | top 4% and 10% by train reward, majority vote |
| Held-out metric | FineWeb-edu sample-10BT bits-per-byte (fixed slice + sha256) |
The resident base copy gives drift-free reconstruction but doubles weight
memory: 72B → 288 GB. Across 8×H100-80GB that is ~36 GB/GPU at TP=8, leaving
~44 GB/GPU for KV — comfortable, but only one engine, so seeds are evaluated
serially (each seed's inference still uses all 8 GPUs, and the switch is one
cheap fused pass). To get data-parallel-over-seeds you scale nodes (16 GPUs →
TP=8 × 2 engines), not the single node. gpu_memory_utilization must leave room
for the base copy: (1−util)·80GB ≥ per-GPU base shard (≈18 GB at TP=8 → util ≤ ~0.7).
Memory-constrained alternative: drop the base copy and use the in-place delta
switch (switch_to_seed, W += σ(R_to − R_from)) for 1× memory and more
engines — at the cost of bf16 drift over long runs (documented and tested).
nanoGPT reports raw token cross-entropy because its tokenizer is fixed (GPT-2 BPE). Profile models have different tokenizers, so we report bits-per-byte, which is tokenizer-invariant:
bpb = (Σ token NLL in nats) / (ln2 · total UTF-8 bytes)
Teacher-forced, obtained from vLLM prompt_logprobs (the logprob of each actual
token given its prefix) — reflecting the live (perturbed) weights with no custom
forward code. We report bpb for the base model and the ensemble, where the
ensemble distribution is the probability average of the top-k models'
next-token distributions (p̄ = mean_m p_m, nll = −log p̄), computed exactly
from per-model realized-token probabilities via a streaming accumulator.
FineWeb is never used for selection (selection is task reward), so any fixed
slice is a valid held-out language-modeling probe; the slice + its sha256 are
pinned in eval/fineweb_heldout.jsonl(.manifest.json).
A σ≈1e-3 perturbation on an O(1) weight is below bf16 resolution (ULP = 2⁻⁸ ≈ 0.0039 at |w|≈1), so it rounds to ±1 ULP; perturbations survive mainly on smaller-magnitude weights. Inherent to bf16 (upstream has it too) — choose σ relative to the weight magnitudes and the dtype. Ensemble averaging is in fp32.
This is also the real reason for the 2× resident base copy (absolute
reconstruction W = W₀ + σ·R) over the 1× in-place delta switch
(W += σ(R_to − R_from)). Measured drift (CPU, |w|≈1; tests/test_perturb.py:: test_inplace_switch_drift):
| dtype | σ | mean |Δ| vs fresh reconstruct | Δ/σ | grows with #hops? | |-------|-----|---------------------------|------|--------| | bf16 | 1e-3 | 7.7e-4 | 0.77 | no — flat 16→2048 hops | | bf16 | 1e-2 | 2.0e-3 | 0.20 | no | | fp16 | 1e-3 | 4.6e-5 | 0.046 | no |
Two findings: (1) the error does not accumulate over hops — R∈{−1,+1} is
exact in bf16, so each += rounds at the local ULP and re-randomizes rather than
compounding. (2) But the per-switch rounding is a large fraction of σ at
production σ (0.77σ for bf16/σ=1e-3), which makes delta-switching path-dependent:
two different seed-chains reaching the same final seed differ by up to one ULP
(≈4σ), so a seed's weights aren't reproducible. Absolute reconstruction is
path-independent and bit-identical every run — that reproducibility (not
"preventing accumulation") is what the 2× copy buys. The 1× path
(switch_model) remains available when memory forces it and bit-exactness can be
traded away.
You cannot cheaply co-batch many dense perturbations into one GEMM: a full sign
matrix S makes Sx a full matvec, so k profiles in one forward = k× the FLOPs,
and KV can't be shared across seeds (different weights → different KV). The real,
principled throughput levers — all used here — are:
- the fused single-pass switch (no materialization);
- deleting the restore pass + per-call cache churn;
- CUDA graphs (
enforce_eager: false) for inference throughput — compatible with in-place weight reconstruction (graphs reference weight storage addresses); - data-parallel over seeds across engines/GPUs (the existing structure);
- maximizing per-engine tokens/sec (large continuous batch, high
gpu_memory_utilization).
# CPU: verify the perturbation + bpb + harness math (no GPU needed)
.venv/bin/python -m pytest tests/test_perturb.py tests/test_worker.py \
tests/test_fineweb.py tests/test_speedrun.py -q
# GPU pod: verify Triton == reference, then the standard / a smoke
python -m pytest tests/test_kernel_gpu.py -q -s
python speedrun.py --config configs/smoke_1gpu_small.yaml # tiny end-to-end
python speedrun.py --config configs/standard_8xh100_qwen72b.yaml # THE standard
# build/refresh the fixed held-out FineWeb slice (records a sha256)
python speedrun.py --config configs/standard_8xh100_qwen72b.yaml --build-finewebscripts/prime_smoke.sh provisions the cheapest viable single-GPU Prime Intellect
pod, runs the smoke end-to-end, copies the record back, and tears the pod down.
Each run writes speedrun-runs/<name>/record.json (full repro blob: commit,
config sha, top-k seeds, timings, throughput, accuracies, bpb) and appends a row
to RECORDS.md. A run is reproducible from its commit + config +
FineWeb manifest sha + the recorded top-k seeds.