Skip to content

Nightly 2026-08-09: f16 becomes a verified path - and immediately kills flash-attention prefill - #18

Merged
physics515 merged 7 commits into
mainfrom
mummu-nightly-2026-08-09
Aug 9, 2026
Merged

Nightly 2026-08-09: f16 becomes a verified path - and immediately kills flash-attention prefill#18
physics515 merged 7 commits into
mainfrom
mummu-nightly-2026-08-09

Conversation

@physics515

Copy link
Copy Markdown
Owner

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 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, 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 (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] cache defaults
to target; this repo's live cache is crates/mummu-bench/target/autotune/), so a global/file
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 (read from source), so
[cubecl.compilation] cache is inert on our path.

New mummu-bench/tests/warmup_f16.rs measures the whole curve in one process — 8 bursts × 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):

cumulative tokens 32 64 96 128 160 192 224 256
run A, tok/s 12.5 37.4 38.8 36.7 38.1 37.4 36.8 37.6
run B, tok/s 16.3 41.1 39.7 39.3 40.3 40.6 41.3 40.3

The first 32 tokens cost 2.5–3.0×, and the curve is flat from token 33 on. So 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.02× where un-warmed it is 0.33×.

Two findings folded into bench/BASELINE.md:

  1. A stale autotune cache silently cost 21–27 % of f16 decode. The day's first budget run
    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_tokens
    1.0279 s → 0.8109 / 0.8371 s, while f32 was unmoved (1.9646 → 1.9797 s).
  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: +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.rs closes that with the honest shape — llama.cpp on the SAME Q4_K_M file, our
side on GpuF16 — and both legs passed on the first run:

leg top-5 ids greedy max |Δlogprob| f32 twin
qwen2-f16 exact in order byte-identical 2.5284926197284596e-1 2.66e-1
qwen3-f16 exact in order byte-identical (incl. <think>) 3.938617118639698e-1 4.02e-1

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.rs into a shared tests/gguf_compare/ module with explicit
port + tolerance parameters; f32 legs re-ran unchanged (qwen3 bit-identical at
4.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 on
t > 1 && f16 && masked, picking between a fused attention(.., is_causal: true) and the existing
chain, 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:

metric explicit (control) fused f16 prefill delta
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 868.7 / 792.8 ms 822.3 / 791.1 ms noise (path not taken)
f32 TTFT / prefill / decode 98.2 ms / 597.3 ms / 1.9787 s 98.0 ms / 598.5 ms / 1.9877 s control, unmoved

Then the parity gate killed it. Qwen2.5-1.5B on GpuF16 returns non-finite logits through the
fused 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 motivated
that 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 cache

Route (a) of the hazard measured in increment 2. autotune_cache_dir() reports 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) 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 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). 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 through
burn-cubecl (feature-unified, no compile-time cost; the burn-store/wgpu precedent). REAL-GPU proof
(tests/real_autotune_cache.rs): clears the cache, runs four 512² matmuls with readbacks, finds
3 files / 8 303 bytes written to exactly the reported directory, clears them, confirms empty. It
touches crates/mummu/target/autotune and never the bench crate's, so recorded benchmark numbers keep
their own tuning.

6. Research folded

  • Pin watch: burn 0.22 still pre-release, cubecl still 0.11.0-pre.1, tokenizers 0.23.1 current.
  • cubecl 0.11 adds CUBECL_AUTOTUNE_CACHE (PR #1423, "Disable persistent tune cache option") — an
    env 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-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 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.
  • 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, and the route that would remove the warm-up cost rather than relocate it.
  • P9: 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.

7. mummu::plan — pick the highest float precision that fits

The 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_decoder takes the config.json numbers and derives KV geometry;
DeviceBudget::from_adapter reads off backend::inventory() and returns None when vram_bytes is
unknown rather than guessing; Fit carries projected/usable bytes and headroom_bytes() — already
the shape the plan/doctor introspection item will render. OVERHEAD_BYTES (1 GiB) and
USABLE_VRAM_FRACTION (0.75) are calibrated against bench/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 a
12 GiB card down to f16; and OLMoE-1B-7B on a 16 GiB card returns None, matching what BASELINE.md
records as "out of reach until keep-quantized VRAM".

Verification — everything green on the reference GPU

cargo fmt clean · cargo clippy --all-targets no warnings (no #![allow] added) · 216 unit
tests
(14 new) · cargo build green.

Parity gatesparity_qwen2 both legs (Candle max |Δlogit| 1.907e-5, Ollama greedy
byte-identical) · parity_gguf qwen2 2.6614442413586614e-1 + qwen3 4.015608155114805e-1
(bit-identical to the recorded value) · parity_f16 qwen2 2.5284926197284596e-1 + qwen3
3.938617118639698e-1 · template_gate 10/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_up API 40.1 vs 40.3 tok/s (1.00×).

Real-model suitesreal_qwen3 2/2 · real_inference 4/4 · real_mixed_dtype 1/1 ·
real_toolcall_qwen3 1/1 · real_autotune_cache 1/1.

Not run (fixtures absent locally): parity_lfm2 and the parity_gguf lfm2/olmoe legs need the LFM2
GGUFs and ~60 GB RAM respectively.

What's next

  • burn 0.22 when it stabilizes — the single highest-value pending item: graph capture (the
    dispatch-bound decode lever), LoRA/QLoRA in-framework, the remote multi-device backend, plus
    CUBECL_AUTOTUNE_CACHE and possibly a wgpu compilation cache. Re-run every gate; expect the backend
    aliases and dtype helpers to change shape.
  • Autotune-cache hazard routes (b) pin-the-location and (c) automatic detection — both likely satisfied
    upstream by 0.22, so re-scope rather than hand-roll.
  • P6 placement plan (the multi-device half), P9 keep-quantized VRAM, P2 OLMoE-from-safetensors.

🤖 Generated with Claude Code

Justin Icenhour and others added 7 commits August 9, 2026 06:34
`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>
Copilot AI lite review requested due to automatic review settings August 9, 2026 13:47
@physics515
physics515 merged commit 36e1f4f into main Aug 9, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_up plus real-GPU and benchmark harnesses to quantify and amortize the f16 cold-start cost.
  • Added mummu::tune (report/clear autotune cache) and mummu::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.

Comment thread crates/mummu/src/tune.rs

use std::path::{Path, PathBuf};

use cubecl_runtime::config::{CubeClRuntimeConfig, RuntimeConfig};
Comment thread crates/mummu/src/tune.rs
Comment on lines +163 to +166
} else {
files += 1;
bytes += entry.metadata().map(|m| m.len()).unwrap_or(0);
}
Comment thread crates/mummu/src/tune.rs
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)
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants