Nightly 2026-08-09: f16 becomes a verified path - and immediately kills flash-attention prefill - #18
Merged
Merged
Conversation
`cargo update` moved 24 transitive packages to their newest Rust-1.99-nightly-compatible versions (notably tracel-ash 0.39.3 -> 0.39.5+sdk1.4.357 on the Vulkan path, ureq 3.3 -> 3.4, minijinja 2.22 -> 2.23 under the template gate, thiserror 2.0.19 -> 2.0.20, zerocopy 0.8.55 -> 0.8.56). No manifest edit was needed: `cargo upgrade --incompatible` offers only wgpu 29 -> 30, which is the standing intentional pin (burn 0.21 resolves wgpu 29 transitively, so 30 unblocks with a burn bump, not a `cargo upgrade`), and every other direct dep is already at its newest stable. burn stays 0.21.0 - 0.22.0-pre.1 is still the only 0.22 tag, so the P0 migration item stays gated. Verified: fmt clean, clippy --all-targets no warnings, 202 unit tests green, cargo build green, and the budget gates hold - GPU 110.4 ms TTFT / 13.1 tok/s / 597 ms prefill@2048, f16 30.5 ms / 13.9 tok/s, CPU 14.2 tok/s. Both GPU gates read low on their FIRST post-bump run (9.1 and 6.2 tok/s) and recovered on re-run, the documented autotune-invalidation transient. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aid off the critical path Closes the ROADMAP item "close the f16 autotune warm-up gap", and the route it proposed turned out to rest on a wrong premise. CubeCL ALREADY persists autotune across processes by default (`[cubecl.autotune] cache` defaults to `target`; this repo's live cache is crates/mummu-bench/target/autotune/), so a `global`/file-backed location changes where the cache lives, never whether tuning carries. What a cold process still pays is kernel compilation + pipeline creation, which the wgpu runtime caches nowhere - cubecl 0.10 wires `CompilationCache` for the CUDA and HIP runtimes only, so `[cubecl.compilation] cache` is inert on our path. No configuration can carry it; only spending it earlier can. New `mummu-bench/tests/warmup_f16.rs` measures the whole curve in one process: 8 bursts x 32 decode steps, each burst a fresh cache + untimed prefill so KV length stays constant (decoding 256 CONSECUTIVE tokens instead confounds warm-up with attention length - measured, it reads ~20% low by the last burst). The first 32 tokens run at 12.5-16.3 tok/s and the curve is FLAT from token 33 on at 37-42. So the cold tax is 2.5-3.0x, exactly one burst deep, and `budget_f16.rs`'s 16.9 tok/s is not a mystery - it IS the first burst. Shipped `CausalLm::warm_up(probe_ids, steps, device)`: one prefill plus `steps` greedy decode steps on a throwaway cache, every step's argmax read back (an unsynchronized warm-up returns before the GPU runs anything), bounded by `MAX_WARM_UP_STEPS = 256`, default-implemented so every zoo model inherits it beside `sanity_check`. REAL-GPU proof in its own binary (`warmup_api_f16.rs` - warm-up is a once-per-process effect, so a second test in one binary would prove nothing): after a 4.21 s `warm_up(&ids, 32)` a cold process's FIRST burst runs at 41.9 tok/s against the next burst's 41.0, ratio 1.02x where un-warmed it is 0.33x. Two findings folded into bench/BASELINE.md rather than acted on here: 1. A STALE AUTOTUNE CACHE silently cost 21-27% of f16 decode. This run's first budget run happened on a contended machine (9.1 tok/s f32, failing its own gate before recovering to 13.1); autotune tuned under that contention, wrote its picks, and every later process loaded them and never re-tuned. Deleting the cache: f16 decode_32_tokens 1.0279 s -> 0.8109/0.8371 s, f32 unmoved (1.9646 -> 1.9797). New ROADMAP item. 2. The gap's other half - "48.8 tok/s steady" - is not reproducible. Criterion measures 36.8 today, and the SAME numbers come back on the pre-`cargo update` lockfile, so it is machine state, not a regression. Measured the mechanism directly: +22pp of host CPU load costs f32 +3.2% and f16 +9.8%, because f16 does ~3x less GPU work per dispatch. README's retracted f16 claims are corrected while here: "identical speed" (an f32 run mislabelled f16, retracted 2026-08-06 in BASELINE.md but never fixed in the README) becomes the measured ~2.3x, and the benchmark bullet carries today's full criterion set. Verified: fmt clean, clippy --all-targets no warnings, 204 unit tests (2 new), cargo build green, warmup_f16 15.4 -> 41.4 tok/s and the f16 budget gate 25.2 ms / 15.5 tok/s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ass first run Every strict gate in the repo ran f32 (or GGUF-dequantized-to-f32): the Candle logits fixture, the llama.cpp GGUF legs, the Ollama greedy leg. The f16 path had liveness only - `real_f16.rs` (no NaN, right VRAM, coherent text) and a one-token f16-vs-f32 agreement in `real_mixed_dtype.rs`. That gap is why the 2026-08-06 flash-attention evaluation could not adopt its one winning quadrant (f16 prefill, -22% @2048): an f16-only numeric fork would have shipped unverified. New `tests/parity_f16.rs` closes it with the honest shape the ROADMAP asked for - llama.cpp on the SAME Q4_K_M file, our side loaded onto `GpuF16` - and both legs passed on the first run, on the 4070 Ti SUPER: qwen2-f16 top-5 ids exact in order [785, 32, 16, 1249, 8420], 24-token greedy byte-identical, max |dlogprob| 2.5284926197284596e-1 qwen3-f16 top-5 ids exact in order [151667, 151644, 151645, 99966, 131545], greedy byte-identical incl. the <think> tokens, max |dlogprob| 3.938617118639698e-1 Both f16 numbers come in BELOW their f32 twins (2.66e-1 / 4.02e-1), so f16 adds nothing measurable on top of the reference's own Q8_K activation-quantization noise - the f32-softmax attention island is doing exactly what it was added for. Own test binary, deliberately: `GpuF16` locks Burn's per-device default dtype policy, so one alias per process is what keeps the numbers real. The only production-adjacent change is a test refactor: the comparator moved out of `parity_gguf.rs` into a shared `tests/gguf_compare/` module and gained explicit `port` + `tolerance` parameters. It sits beside `llama_ref` rather than inside it because `parity_lfm2.rs` uses only the transport half, and an unused pub fn in its binary would be a dead_code warning (no `#![allow]`). f32 legs re-ran unchanged after the move - qwen3 bit-identical at 4.015608155114805e-1, qwen2 at 2.66e-1 with top-5 exact and greedy byte-identical. Fixture note: the Qwen2.5-1.5B Q4_K_M GGUF (1.07 GB) was not in the local cache and was fetched for this run. Verified: fmt clean, clippy --all-targets no warnings, 204 unit tests, cargo build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e new parity leg caught it Closes the ROADMAP item "adopt flash attention for f16 prefill only, once f16 has parity coverage". Prerequisite (a) shipped two commits ago; this run then did (b) and the answer changed from "wait" to "no". The conditional was built exactly as scoped - `use_fused_attention(t, ambient, masked)` gating on `t > 1 && f16 && masked` (the measured quadrant, nothing wider), picking between a fused `tensor::module::attention(.., is_causal: true)` and the existing chain, with a CPU-backend unit test holding the two formulations to each other at four (past, t) pairs. THE WIN REPRODUCED, two runs per arm, f32 as an untouched control: f16 TTFT (36 tok) 24.9 / 24.6 ms -> 21.0 / 20.8 ms -15.6% f16 prefill @ 2048 240.6 / 239.5 ms -> 224.2 / 221.3 ms -7.2% f16 decode 830.8 ms -> 806.7 ms (noise; path not taken) f32 (control) 98.2 -> 98.0 ms, 597.3 -> 598.5 ms, 1.979 -> 1.988 s Then `tests/parity_f16.rs` failed: Qwen2.5-1.5B Q4_K_M on GpuF16 returns NON-FINITE logits through the fused kernel, while the identical weights through the explicit chain are byte-identical to llama.cpp. The 2026-08-06 evaluation assumed the f32 score island survives inside the kernel (`AccumulatorPrecision::Strict(F32)`); it does not, for the very model whose q.k^T overflow motivated that island - the fused path reproduces the pre-island 2026-07-11 NaN. It is also model-dependent: Qwen3-0.6B PASSED the same fused path (top-3 exact, greedy byte-identical, max |dlogprob| 3.9386e-1 against the explicit 3.9386e-1, only the 5th tail id reshuffling). That is exactly what makes it unshippable in a shared leaf function - correct for narrow models, silently NaN for wide ones. Reverted; the tree keeps the explicit chain, and the f16 parity legs re-passed green after the revert (2.5284926197284596e-1 / 3.938617118639698e-1, both greedy byte-identical). What would reopen it: burn 0.22 / wgpu 30, or an upstream fix that makes the kernel's score accumulation genuinely f32 for f16 inputs. Standing lesson recorded in bench/BASELINE.md: run the parity leg BEFORE the A/B. The measurement was never the hard part - and an evaluation without a parity gate behind it would have shipped this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route (a) of the hazard this run measured: CubeCL persists its kernel picks to disk and reloads them forever, with no invalidation and no re-tune trigger, so a pick made while the machine was busy is indistinguishable from a good one. Measured today (bench/BASELINE.md): a tune taken during a contended moment cost 21-27% of f16 decode throughput in EVERY subsequent process, while the f32 picks from the same moment were fine - silent, partial, permanent. New `mummu::tune`: - `autotune_cache_dir()` - where CubeCL will persist picks, read out of the very config CubeCL itself discovers (`CubeClRuntimeConfig::get().autotune.cache.root()` + the `autotune` segment `CacheOption::name` adds), so the path is right by construction instead of a hardcoded copy of the discovery rule that would drift the first time cubecl changes it. - `autotune_cache_report()` - files + bytes; an absent cache is empty, not an error. - `clear_autotune_cache()` - removes it and returns what it removed: the "re-tune GPU kernels" action a settings UI needs. Bounded and fail-loud: a tree deeper than 8 levels or wider than 65 536 files is `TuneError::Implausible` rather than a long walk or a wide delete, and every path the module touches ends in the `autotune` segment by construction (asserted before any removal), so a misconfigured root cannot widen the delete. Documented honestly - it takes effect on the NEXT process, since a running one has already loaded the cache into memory. One new direct dependency, `cubecl-runtime 0.10` - the same version burn 0.21 already resolves through burn-cubecl, so it is feature-unified and costs no compile time. Same precedent as the direct `burn-store` and `wgpu` handles. Proof: 5 unit tests plus a REAL-GPU test (`tests/real_autotune_cache.rs`) that clears the cache, runs four 512-square matmuls with readbacks, finds 3 files / 8 303 bytes written to exactly the reported directory, then clears them and confirms empty. It touches `crates/mummu/target/autotune` and never the bench crate's - CubeCL's root is the walk-up from the process CWD, so the recorded benchmark numbers keep their own tuning undisturbed. Still open on the ROADMAP item: (b) pinning the cache to a Mummu-owned location via `RuntimeConfig::set`, and (c) detecting a bad tune automatically rather than only exposing the repair. Verified: fmt clean, clippy --all-targets no warnings, 209 unit tests (5 new), cargo build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…totune-cache hazard
Pin watch: burn 0.22 is STILL pre-release (0.22.0-pre.1 the newest tag,
0.21.0 the latest stable), cubecl still 0.11.0-pre.1, tokenizers 0.23.1
current - the P0 migration stays gated and `cargo upgrade --incompatible`
offers only the standing wgpu 29->30 pin.
Three concrete folds, all tied to items this run touched:
1. Autotune-cache hazard (the item this run opened and half-shipped):
cubecl 0.11 adds CUBECL_AUTOTUNE_CACHE (PR #1423, "Disable persistent
tune cache option") - an env var that bypasses persistent read AND
write and keeps tuning in-memory per process. That is a cleaner
route (c) than anything we would build, and it is worth adopting for
mummu-bench the moment it lands: this run's 21-27% f16 swing came
from exactly the inheritance it disables. cubecl 0.11 also moves
autotune scoring from latency to throughput (#1422, #1408), so
re-measure the hazard's magnitude after the bump instead of assuming
it survives. And CubeCL documents the persisted caches as a SHIPPING
artifact ("ship a warm cache with your binary when you know the
deployment target"), which is the mirror image of the hazard and the
right shape for a consumer with a fixed build target.
2. Warm-up gap: "no configuration can carry it" is a cubecl-0.10-on-wgpu
statement, not a permanent one. wgpu ships
Device::create_pipeline_cache (gfx-rs/wgpu #5293) explicitly for
startup time, so a cubecl-wgpu compilation cache is buildable
upstream - the thing to look for on the 0.22 / wgpu-30 bump. Recorded
with the verification that produced today's finding: at cubecl 0.10
CompilationCache is constructed in the cuda and hip runtimes only.
3. P9 keep-quantized kernels: a third independent corroboration of the
fused-dequant-into-shared-memory/registers split, this time from a
shipped WebGPU product (PrismML's 1-bit 27B) rather than a paper. The
design question is settled; only the substrate choice is open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The float half of P6's "precision selection". Everything it needs already existed on both sides - a checkpoint's config.json shape, and `backend::inventory()`'s per-adapter VRAM + SHADER_F16 - but a consumer still had to guess between `Gpu` and `GpuF16` by hand. `mummu::plan::pick_precision(&ModelShape, &DeviceBudget) -> Option<Fit>` returns the HIGHEST precision that fits, or `None`, which is the honest "no float tier fits; this needs quantization or several devices" rather than a silently-worse tier. Supporting types keep callers out of the arithmetic: `ModelShape::from_decoder` takes the config.json numbers and derives KV geometry itself; `DeviceBudget::from_adapter` reads straight off an enumerated adapter and returns `None` when `vram_bytes` is unknown (every non-Windows adapter today) rather than guessing a budget; `Fit` carries projected/usable bytes and `headroom_bytes()` - already the shape the `plan`/`doctor` introspection item will render. Two constants carry the judgement, calibrated against bench/BASELINE.md rather than first principles: OVERHEAD_BYTES (1 GiB for activations/workspaces/CubeCL pools - the residual between measured runner VRAM and weights+KV) and USABLE_VRAM_FRACTION (0.75, because the reference box runs 3.5-6.5 GiB of desktop ambient on the same card and a plan that ignores it fails at load, not slowly). 7 unit tests pin the decisions to real hardware and real models rather than to the formula: - Qwen2.5-1.5B projects 7.0 GiB f32 / 3.9 GiB f16 against the measured 8.0 / 3.6. - The 15.7 GiB reference card gets f32; an 8 GiB card gets f16. - The dev box's own DX12 rows (same card, no SHADER_F16) never get an f16 plan - and do get f32 once the card is big enough. - A 64k context pushes a 12 GiB card from f32 down to f16. - OLMoE-1B-7B on a 16 GiB card returns None, matching what bench/BASELINE.md records as "out of reach until keep-quantized VRAM". Stays [ ] on the ROADMAP for the int8/int4 tiers, which extend `Precision` downward when P9 lands. Verified: fmt clean, clippy --all-targets no warnings, 216 unit tests (7 new), cargo build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This nightly PR strengthens the f16 execution path by adding parity coverage and a warm-up API, introduces public utilities for managing CubeCL’s autotune cache, and adds an initial VRAM-based precision planner. It also updates documentation/roadmap to reflect the new verification story and measured performance behavior.
Changes:
- Added f16 parity gates and shared GGUF-vs-llama.cpp comparison harness, plus updated docs/roadmap to reflect verified f16 behavior and the rejected fused-attention attempt.
- Introduced
CausalLm::warm_upplus real-GPU and benchmark harnesses to quantify and amortize the f16 cold-start cost. - Added
mummu::tune(report/clear autotune cache) andmummu::plan(pick highest float precision that fits VRAM), along with tests.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ROADMAP.md | Records nightly findings (f16 parity, warm-up, autotune hazard, planning) and updates item statuses. |
| README.md | Updates public claims with measured f16 throughput, parity verification, warm-up API, tuning and planning utilities. |
| crates/mummu/tests/real_autotune_cache.rs | Adds a real-GPU ignored test validating autotune cache persistence and clearing behavior. |
| crates/mummu/tests/parity_gguf.rs | Refactors GGUF parity tests to use a shared comparator and explicit port allocation. |
| crates/mummu/tests/parity_f16.rs | Adds a new f16 parity test binary comparing GpuF16 against llama.cpp on the same GGUF. |
| crates/mummu/tests/gguf_compare/mod.rs | Introduces shared GGUF-vs-llama.cpp comparator and port allocator. |
| crates/mummu/src/tune.rs | Adds public API to locate, measure, and clear CubeCL autotune cache with safety bounds and tests. |
| crates/mummu/src/plan.rs | Adds VRAM-based float precision selection (F32 vs F16) with sizing model and unit tests. |
| crates/mummu/src/models/qwen2.rs | Adds unit tests covering the new warm_up behavior and step bound enforcement. |
| crates/mummu/src/models/mod.rs | Adds MAX_WARM_UP_STEPS and default CausalLm::warm_up implementation. |
| crates/mummu/src/lib.rs | Exposes new plan and tune modules. |
| crates/mummu/Cargo.toml | Adds cubecl-runtime as a direct dependency for reading CubeCL runtime config. |
| crates/mummu-bench/tests/warmup_f16.rs | Adds a benchmark-style ignored test measuring f16 warm-up curve over multiple bursts. |
| crates/mummu-bench/tests/warmup_api_f16.rs | Adds a benchmark-style ignored test proving warm_up eliminates the cold first-burst penalty. |
| Cargo.toml | Adds workspace dependency on cubecl-runtime = "0.10". |
| Cargo.lock | Updates transitive dependencies from cargo update and adds cubecl-runtime to mummu’s resolved deps. |
| bench/BASELINE.md | Documents warm-up curve measurements, stale autotune-cache hazard, and the fused-attention rejection. |
Suppressed comments (1)
crates/mummu/src/tune.rs:210
- This test uses a fixed directory name under the global temp directory, which can collide with parallel test runs or stale data from prior runs. Using a unique temp directory (and cleaning it up via a guard) will make the test deterministic and avoid cross-run interference.
#[test]
fn measuring_counts_files_and_bytes_across_nested_dirs() {
let root = std::env::temp_dir().join("mummu-tune-measure-a41c");
let nested = root.join("0.10.0").join("device-4-0");
std::fs::create_dir_all(&nested).expect("fixture dirs");
std::fs::write(nested.join("matmul.json.log"), b"12345").expect("fixture file");
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use cubecl_runtime::config::{CubeClRuntimeConfig, RuntimeConfig}; |
Comment on lines
+163
to
+166
| } else { | ||
| files += 1; | ||
| bytes += entry.metadata().map(|m| m.len()).unwrap_or(0); | ||
| } |
Comment on lines
+195
to
+203
| #[test] | ||
| fn measuring_an_absent_directory_reports_empty() { | ||
| let missing = std::env::temp_dir().join("mummu-no-such-autotune-dir-9e3f"); | ||
| assert!(!missing.exists(), "fixture path must not exist"); | ||
| assert_eq!( | ||
| measure(&missing, 0).expect("absent is not an error"), | ||
| (0, 0) | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nightly run 2026-08-09. Seven increments, all green, all landed on this branch.
The through-line: the f16 path went from live to verified, and that verification immediately
earned its keep by killing a change the previous run had wanted to ship.
1. Dependency freshness
cargo updatemoved 24 transitive packages to their newest Rust-1.99-nightly-compatible versions(notably tracel-ash 0.39.3 → 0.39.5+sdk1.4.357 on the Vulkan path, ureq 3.3 → 3.4, minijinja
2.22 → 2.23 under the template gate, thiserror 2.0.19 → 2.0.20, zerocopy 0.8.55 → 0.8.56). No
manifest edit was needed:
cargo upgrade --incompatibleoffers only wgpu 29 → 30, the standingintentional pin (burn 0.21 resolves wgpu 29 transitively, so 30 unblocks with a burn bump, not a
cargo upgrade), and every other direct dep is already at its newest stable.burn stays 0.21.0 — 0.22.0-pre.1 (2026-07-29) is still the only 0.22 tag, so the P0 migration
item stays gated. cubecl likewise still 0.11.0-pre.1.
Both GPU budget gates read low on their first post-bump run (9.1 and 6.2 tok/s) and recovered on
re-run — the documented autotune-invalidation transient, and, as increment 2 discovered, not
actually a transient at all.
2. The f16 cold-start tax is exactly one 32-token burst
Closes "close the f16 autotune warm-up gap: 16.9 tok/s cold vs 48.8 steady" — and the route the
item proposed rested on a wrong premise.
CubeCL already persists autotune across processes by default (
[cubecl.autotune] cachedefaultsto
target; this repo's live cache iscrates/mummu-bench/target/autotune/), so aglobal/filelocation changes where the cache lives, never whether tuning carries. What a cold process still
pays is kernel compilation + pipeline creation, which the wgpu runtime caches nowhere — cubecl
0.10 wires
CompilationCachefor the CUDA and HIP runtimes only (read from source), so[cubecl.compilation] cacheis inert on our path.New
mummu-bench/tests/warmup_f16.rsmeasures the whole curve in one process — 8 bursts × 32 decodesteps, each burst a fresh cache + untimed prefill so KV length stays constant (decoding 256
consecutive tokens instead confounds warm-up with attention length; measured, it reads ~20 % low by
the last burst):
The first 32 tokens cost 2.5–3.0×, and the curve is flat from token 33 on. So
budget_f16.rs's16.9 tok/s is not a mystery — it is the first burst.
Shipped
CausalLm::warm_up(probe_ids, steps, device): one prefill plusstepsgreedy decodesteps on a throwaway cache, every step's argmax read back (an unsynchronized warm-up returns before
the GPU runs anything), bounded by
MAX_WARM_UP_STEPS = 256, default-implemented so every zoo modelinherits it beside
sanity_check. REAL-GPU proof in its own binary (warmup_api_f16.rs— warm-up isa once-per-process effect, so a second test in one binary would prove nothing): after a 4.21 s
warm_up(&ids, 32), a cold process's first burst runs at 41.9 tok/s against the next burst's41.0 — ratio 1.02× where un-warmed it is 0.33×.
Two findings folded into
bench/BASELINE.md:happened on a contended machine; autotune tuned under that contention, wrote its picks, and every
later process loaded them and never re-tuned. Deleting the cache: f16
decode_32_tokens1.0279 s → 0.8109 / 0.8371 s, while f32 was unmoved (1.9646 → 1.9797 s).
today, and the same numbers come back on the pre-
cargo updatelockfile, so it is machine state,not a regression. Measured the mechanism directly: +22 pp of host CPU load costs f32 +3.2 % and
f16 +9.8 %, because f16 does ~3× less GPU work per dispatch.
The README's retracted f16 claims are corrected while here: "identical speed" (an f32 run mislabelled
f16, retracted 2026-08-06 in BASELINE.md but never fixed in the README) becomes the measured ~2.3×.
3. f16 is now a parity-verified path
Every strict gate in the repo ran f32 (or GGUF-dequantized-to-f32). The f16 path had liveness only.
New
tests/parity_f16.rscloses that with the honest shape — llama.cpp on the SAME Q4_K_M file, ourside on
GpuF16— and both legs passed on the first run:<think>)Both f16 numbers come in below their f32 twins, so f16 adds nothing measurable on top of the
reference's own Q8_K activation-quantization noise — the f32-softmax attention island is doing exactly
what it was added for. Own test binary (one dtype alias per process). The only refactor: the
comparator moved from
parity_gguf.rsinto a sharedtests/gguf_compare/module with explicitport+toleranceparameters; f32 legs re-ran unchanged (qwen3 bit-identical at4.015608155114805e-1).
Fixture note: the Qwen2.5-1.5B Q4_K_M GGUF (1.07 GB) was not cached locally and was fetched.
4. Flash attention for f16 prefill — rejected on correctness
Closes "adopt flash attention for f16 prefill only, once f16 has parity coverage". With (a) in hand,
(b) was implemented exactly as scoped:
use_fused_attention(t, ambient, masked)gating ont > 1 && f16 && masked, picking between a fusedattention(.., is_causal: true)and the existingchain, plus a CPU-backend unit test holding the two formulations to each other at four
(past, t)pairs. The win reproduced — two runs per arm, f32 as an untouched control:
Then the parity gate killed it. Qwen2.5-1.5B on
GpuF16returns non-finite logits through thefused kernel, while the identical weights through the explicit chain are llama.cpp-identical. The
2026-08-06 evaluation assumed the f32 score island survives inside the kernel
(
AccumulatorPrecision::Strict(F32)); it does not, for the very model whose q·kᵀ overflow motivatedthat island. It is also model-dependent — Qwen3-0.6B passed the same fused path — which is
precisely what makes it unshippable in a shared leaf function: correct for narrow models, silently
NaN for wide ones. Reverted; f16 parity re-passed green after the revert.
Standing lesson recorded: run the parity leg before the A/B. An evaluation without a parity gate
behind it would have shipped this.
5.
mummu::tune— throw away a bad autotune cacheRoute (a) of the hazard measured in increment 2.
autotune_cache_dir()reports where CubeCL willpersist picks, read out of the very config CubeCL itself discovers
(
CubeClRuntimeConfig::get().autotune.cache.root()+ theautotunesegmentCacheOption::nameadds) rather than a hardcoded copy of the discovery rule;
autotune_cache_report()measures it;clear_autotune_cache()removes it and returns what it removed — the "re-tune GPU kernels" action asettings UI needs.
Bounded and fail-loud: a tree deeper than 8 levels or wider than 65 536 files is
TuneError::Implausiblerather than a long walk or a wide delete, and every path the module touchesends in the
autotunesegment by construction (asserted before any removal). Documented honestly —it takes effect on the next process.
One new direct dependency,
cubecl-runtime 0.10, the same version burn 0.21 already resolves throughburn-cubecl (feature-unified, no compile-time cost; the
burn-store/wgpuprecedent). REAL-GPU proof(
tests/real_autotune_cache.rs): clears the cache, runs four 512² matmuls with readbacks, finds3 files / 8 303 bytes written to exactly the reported directory, clears them, confirms empty. It
touches
crates/mummu/target/autotuneand never the bench crate's, so recorded benchmark numbers keeptheir own tuning.
6. Research folded
CUBECL_AUTOTUNE_CACHE(PR #1423, "Disable persistent tune cache option") — anenv var that bypasses persistent read and write. That is a cleaner route (c) for the hazard above
than anything we would build, and worth adopting for
mummu-benchthe moment it lands: this run's21–27 % f16 swing came from exactly the inheritance it disables. cubecl 0.11 also moves autotune
scoring from latency to throughput (#1422, #1408), so re-measure the hazard's magnitude after the
bump rather than assuming it survives. CubeCL also documents the persisted caches as a shipping
artifact ("ship a warm cache with your binary when you know the deployment target") — the mirror
image of the hazard, and the right shape for a consumer with a fixed build target.
Device::create_pipeline_cache(gfx-rs/wgpu #5293) explicitly for startup time, so acubecl-wgpu compilation cache is buildable upstream — the thing to look for on the 0.22 / wgpu-30
bump, and the route that would remove the warm-up cost rather than relocate it.
this time from a shipped WebGPU product (PrismML's 1-bit 27B) rather than a paper.
7.
mummu::plan— pick the highest float precision that fitsThe float half of P6's precision selection.
pick_precision(&ModelShape, &DeviceBudget) -> Option<Fit>returns the highest precision that fits one adapter, or
None— the honest "no float tier fits;this needs quantization or several devices", never a silently-worse tier.
ModelShape::from_decodertakes theconfig.jsonnumbers and derives KV geometry;DeviceBudget::from_adapterreads offbackend::inventory()and returnsNonewhenvram_bytesisunknown rather than guessing;
Fitcarries projected/usable bytes andheadroom_bytes()— alreadythe shape the
plan/doctorintrospection item will render.OVERHEAD_BYTES(1 GiB) andUSABLE_VRAM_FRACTION(0.75) are calibrated againstbench/BASELINE.md, not first principles.7 unit tests pin the decisions to real hardware and real models: Qwen2.5-1.5B projects 7.0 GiB f32 /
3.9 GiB f16 against the measured 8.0 / 3.6; the 15.7 GiB reference card takes f32 and an 8 GiB card
f16; the dev box's own DX12 rows (no
SHADER_F16) never get an f16 plan; a 64k context pushes a12 GiB card down to f16; and OLMoE-1B-7B on a 16 GiB card returns
None, matching what BASELINE.mdrecords as "out of reach until keep-quantized VRAM".
Verification — everything green on the reference GPU
cargo fmtclean ·cargo clippy --all-targetsno warnings (no#![allow]added) · 216 unittests (14 new) ·
cargo buildgreen.Parity gates —
parity_qwen2both legs (Candle max |Δlogit| 1.907e-5, Ollama greedybyte-identical) ·
parity_ggufqwen2 2.6614442413586614e-1 + qwen3 4.015608155114805e-1(bit-identical to the recorded value) ·
parity_f16qwen2 2.5284926197284596e-1 + qwen33.938617118639698e-1 ·
template_gate10/10 byte-identical.Budgets — GPU 106.5 ms TTFT / 11.8 tok/s / 594 ms prefill@2048 · f16 26.1 ms / 15.2 tok/s · CPU
15.5 tok/s · warm-up curve 15.5 → 40.0 tok/s ·
warm_upAPI 40.1 vs 40.3 tok/s (1.00×).Real-model suites —
real_qwen32/2 ·real_inference4/4 ·real_mixed_dtype1/1 ·real_toolcall_qwen31/1 ·real_autotune_cache1/1.Not run (fixtures absent locally):
parity_lfm2and theparity_gguflfm2/olmoe legs need the LFM2GGUFs and ~60 GB RAM respectively.
What's next
dispatch-bound decode lever), LoRA/QLoRA in-framework, the remote multi-device backend, plus
CUBECL_AUTOTUNE_CACHEand possibly a wgpu compilation cache. Re-run every gate; expect the backendaliases and dtype helpers to change shape.
upstream by 0.22, so re-scope rather than hand-roll.
🤖 Generated with Claude Code