Skip to content

avifenesh/bw24

Repository files navigation

bw24

License: MIT Rust CUDA arch

bw24 vs llama.cpp perf board

From-scratch LLM inference engine in Rust + CUDA, built for one machine: an RTX 5090 Laptop (Blackwell sm_120a, 24 GB, 175 W with dynamic boost). No frameworks, no ggml — every kernel written and tuned against measured hardware limits, with llama.cpp as the benchmark to beat on the same rig.

The Performance section and card above show same-session measurements against llama.cpp's best config. It also loads HF safetensors checkpoints directly (no GGUF conversion) and runs a 121 GB MoE on this 24 GB card.

Why this project

  • Real, running inference on sm_120a (consumer Blackwell), with every shipped optimization backed by its measured win/loss record.
  • Gated on bit-exactness — argmax match and speculative self-consistency, verified on every kernel change.
  • Runs Qwen3.5/3.6 dense and MoE checkpoints on 24 GB — from GGUF or straight from HF safetensors, no conversion — including models far larger than VRAM+RAM.

Requirements

  • NVIDIA Blackwell consumer GPU (sm_120a). Primary target: RTX 5090 Laptop.
  • CUDA toolkit 13.1 (the build default — crates/bw24-engine/build.rs uses /usr/local/cuda-13.1/bin/nvcc; override with BW24_NVCC=/path/to/nvcc).
  • Rust (edition 2024), cudarc 0.19 with dynamic loading.
  • A model — GGUF or an HF safetensors directory (pass either path). Tested: Qwen3.5-9B, Qwen3.6-27B, Qwen3.6-35B-A3B MoE (common quants), nvidia/Qwen3.6-27B-NVFP4, MiniMax-M3 REAP50, Hy3-REAP50.

Quick start

# build
cargo build --release

# verify all kernels against the CPU reference
./target/release/kernel-check

# generate text (the fast path is the default — no flags needed)
BW24_CHAT=1 ./target/release/run-gen /path/to/model.gguf --prompt "Explain KV caches in one paragraph."

# speculative decoding with the embedded MTP draft head (Qwen3.6)
BW24_SPEC_K=3 ./target/release/run-spec /path/to/qwen36-27b.gguf

# OpenAI-compatible server
./target/release/bw24-server

Every tuned kernel path is the default; environment flags exist only for runtime parameters (prompt, draft depth, trims), machine-specific configuration, and rollback seams (BW24_FAST=0 drops to the f32 oracle path). The catalog lives in docs/FLAGS.md.

run-gen prints a prefill/decode correctness gate (prefill argmax must match decode argmax) before timing anything — if that line says MISMATCH, the numbers after it don't count.

Workspace layout

Crate What it does
bw24-engine Core: CUDA kernels (cu/), forward passes, speculative decoding, MoE cache, CUDA-graph decode
bw24-gguf GGUF parser + tensor loading (memory-mapped)
bw24-tokenizer BPE tokenizer + chat templates from GGUF metadata
bw24-runtime CUDA device/stream/memory primitives over cudarc
bw24-server HTTP server (axum), OpenAI-compatible /v1 endpoints
bw24-probe Standalone hardware microbenches (probe/*.cu: bandwidth, tensor-core peaks, layout experiments)

What's inside

  • NVFP4 (W4) decode path — block-scaled FP4 matvec with split-plane repack, warp-level dp4a, and an int8 W4A8 tensor-core GEMM for prefill. Auto-dispatches per matrix shape.
  • MTP speculative decoding — draft with the model's embedded multi-token-prediction head, verify K+1 tokens in one batched target forward. The whole draft chain runs inside a captured CUDA graph; exactness is enforced by a K=1..8 self-consistency gate (all K must emit identical tokens).
  • MoE on 24 GB — expert-major CSR batching, decode-once dequant kernels, int8 tensor-core expert GEMM, and an SLRU expert-residency cache with VRAM → host → disk spill.
  • FlashAttention-style kernels — fused prefill and decode attention with quantized KV (q8_0 K / q5_1 V default; FP8 and q4_0 arms exist behind flags — q4_0 V measured quality-taxed and stays off), register-resident dequant, split-K for long context.
  • CUDA-graph decode — the full per-token decode is one graph replay; per-step host round-trip is 4 bytes.
  • Hybrid architectures — full-attention + gated-delta-net (SSM) layer mixes, as in Qwen3.6.
  • Safetensors loader — HF checkpoints load directly (no GGUF conversion): modelopt NVFP4 repacks byte-exact into the GGUF block layout, FP8-E4M3 and large-BF16 tensors re-encode to Q8_0/NVFP4 at load, V-head permutations apply on packed bytes, MoE experts stream through a disk-tier repack cache for models far bigger than VRAM+RAM.
  • Sigmoid-router MoE (MiniMax/DeepSeek-style) — e_score_correction_bias selection, swigluoai activation, gate-optional attention; with the measured law that cross-kernel-family FP-order differences are architectural on discontinuous top-k routing (exactness binds within a config).

Correctness discipline

Every kernel change must pass, in order:

  1. kernel-check — every quant kernel vs a CPU reference.
  2. run-gen argmax gate — prefill and decode paths must agree on the next token.
  3. run-spec self-consistency — speculative output at K=1..8 must be token-identical to plain decode.

Floating-point summation order is part of the contract: two mathematically equal kernels that reduce in different orders can flip an argmax at tight logit margins. Several "faster" kernels were rejected for exactly this (research/tune-data/).

Performance

Measured 2026-07-09 on the target rig (RTX 5090 Laptop, N≥2 medians, both engines measured in the SAME session and thermal regime, no flags (tuned paths are defaults)) against llama.cpp built on the same machine, same exact prompts, both engines re-baselined the same day. Boards move with the tuning campaign — research/tune-data/rig5090.jsonl is the running record; the README is refreshed with every board-moving merge.

Plain decode first (no speculation, tg128 at 512-token context — the honest floor comparison):

Model bw24 plain llama.cpp plain Ratio
Qwen3.6-27B NVFP4 44.8 40.3 1.11x
Qwen3.5-9B NVFP4 128.1 113.9 1.12x
Qwen3.6-35B-A3B MoE 170.7 159.6 1.07x
Qwen3.5-9B NVFP4 ST (safetensors, modelopt) 129.1 123.7 1.04x
Qwen3.6-27B NVFP4 ST (NVIDIA official) 45.2 41.2 1.10x

Depth is part of the contract: at 6.3k-token context the leads hold (1.04-1.07x) except the 35B at 0.99x. Every attention/split change validates across the depth axis, not just one point.

Speculative decoding (MTP head, both engines at their measured best config) as the bonus layer on top:

Model bw24 spec llama.cpp spec-best Ratio
Qwen3.5-9B (K=3 + native trim) 246 / 206 / 172 186 / 158 / 155 1.32x / 1.30x / 1.11x
Qwen3.6-27B (K=3 + generic trim) 109 / 100 / 78.8 86.4 / 89.9 / 73.2 1.26x / 1.11x / 1.08x
Qwen3.6-35B-A3B (K=3 + trim + zero-draft) 251 / 232 / 193 215 / 208.5 / 201.7 1.17x / 1.11x / 0.96x
Qwen3.5-9B ST (K=2-3 per-content + native trim) 203.9 / 192.5 / 256.0 122.9 / 122.2 / 118.2 1.66x / 1.58x / 2.17x
Qwen3.6-27B ST (K=3 + HPOST + own-head trim, per-content pmin) 92.9 / 88.1 / 84.6 79.7 / 84.7 / 71.3 1.17x / 1.04x / 1.19x

The three columns are short-code / medium-code / long-agentic prompts, raw-continuation protocol, llama.cpp at serve-best config. Optimal config is content-class dependent. Caveat on the long-agentic column: a text audit found the greedy continuations there partially degenerate into repetition on both engines symmetrically — ratios stand, absolutes overstate real agentic throughput (rig5090.jsonl tag p3-degeneration-audit; prompt replacement pending). The speculative edge comes from three mechanisms — FR-Spec vocabulary trims, whole-round confidence gating, and per-content-class draft depth — detailed in HANDOVER.md.

Reproducing: every artifact is public — trimmed draft-head GGUFs, exact prompts, and full configs at huggingface.co/Avifenesh/bw24-bench. llama.cpp build/serve flags are in docs/COMPETITOR-SETUP.md; the harness is research/e2e/run-e2e.sh.

Known gaps (tracked and diagnosed in research/tune-data/): prefill trails llama.cpp at 0.55-0.76x (decomposed — the residual is int8-vs-native-FP4 tensor-core ceiling under the exactness contract), the 35B loses two cells narrowly, and vLLM's batched MTP wins on big-VRAM boxes.

Beyond llama.cpp: the safetensors path runs checkpoints llama.cpp cannot — NVIDIA's official Qwen3.6-27B-NVFP4 (2.3x the tuned local vLLM reference) and giant spilled MoEs: MiniMax-M3 REAP50 (121 GB) and Hy3-REAP50 (82 GB) both generate correct text on this 24 GB / 60 GB machine through VRAM→RAM→NVMe expert tiers. Details in HANDOVER.md.

Limitations

  • Built for sm_120a only; tuning choices assume this exact memory/compute ratio.
  • Model coverage: Qwen3.5/3.6 dense and MoE, MiniMax-M3, Hy3 — not a general GGUF runner.
  • Single GPU, single stream. No tensor parallelism, no continuous batching.
  • APIs and flags change without notice; moving research codebase.

Docs

  • ARCHITECTURE.md — tech stack + sm_120a feasibility ledger (what the silicon can and cannot do, measured).
  • HANDOVER.md — the living state-of-work doc (current standings, laws, open lanes); internal but readable.
  • docs/decisions/ — design decision records: internal weight format, quant/GEMM policy, safetensors import, hybrid-architecture plan.
  • docs/COMPETITOR-SETUP.md — how each competitor engine is built and tuned to its peak on this box (the "beat them at their best" contract).
  • research/tune-data/current-board.json — the numbers behind the Performance section and docs/perf-card.svg, both regenerated by tools/update-perf-board.py — edit the JSON, never the generated regions directly.
  • research/sm120-empirical-capabilities.md — microbenched silicon peaks for this GPU.
  • research/benchmarks.md — the A/B measurement protocol.
  • research/tune-data/ — every tuning experiment as JSONL: config → measured result, wins and losses both. ~215 records and counting; treat it as a labeled corpus of what sm_120a actually rewards.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md.

License

MIT — see LICENSE.

About

From-scratch Rust+CUDA inference engine, bit-exact by construction — NVFP4, MoE, MTP speculative decoding, tuned against measured limits of one RTX 5090 Laptop (sm_120a).

Topics

Resources

License

Contributing

Security policy

Stars

258 stars

Watchers

23 watching

Forks

Packages

 
 
 

Contributors