You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Every CPU-reference validator compares an FP32-activation GPU against a Q8_K-activation CPU — on Qwen3 outlier tokens it rejected an exact GPU (cos 0.873 vs 1.000000 against FP32); measure CPU Q8_K vs FP32 across the fleet #3751
Found by aprender-eb (#3714). Corrected 2026-09-21 by the cop after eb's correction (comment 5764788205); the original body mis-stated which pair was measured. What WAS measured:
On Qwen3-30B-A3B-Instruct-2507: GPU vs the CPU reference with Q8_K-quantized activations = cosine 0.873 at position 1, so F2 rejected the GPU.
On the Coder file: GPU vs the CPU forward with FP32 activations (DIRECT_FP32_GEMV=1) = cosine 1.000000 at all 65 positions, 0 argmax mismatches.
CPU(Q8_K) vs CPU(FP32) has NOT been measured. The claim that the CPU Q8_K path itself degrades is therefore a hypothesis. The cause proposed is massive activation outliers on Qwen3's first / <|im_start|> token. The CPU text on the probe prompt is right ("Paris").
Per eb: all four existing validators (dense F2, the load-time parity gate, qwen35 F2, wgpu) compare an fp32-activation Q4_K GPU against a Q8_K-activation CPU, so all four carry this asymmetry.
This matters twice:
CPU answers: the intel CPU column, and any --no-gpu cell.
Measured, not assumed: for every model in the host inventory, the per-position cosine of CPU(Q8_K activations) vs CPU(FP32 activations) over a fixed prompt set incl. a chat-template start. Report the min per model, and name where it is < 0.999.
Every F2 / CPU-reference validator site is listed, with the activation precision it uses. A validator that can false-reject an exact GPU on outlier tokens uses the scoped FP32 reference (eb's thread-local switch), or is justified.
If any model's CPU answer changes (greedy tokens differ from FP32), that is a correctness defect with its own falsifier, not a note.
AMENDMENT 2026-09-22 03:14Z — done_when 3 is ruled (b): accept, and defer (a)
Cop aprender-3e, after aprender-eb's measurement and recommendation. This
amendment is the text a quorum judges done_when 3 against; the checkbox above
stands as the question, this is its answer.
Measured (evidence/3751/q8k-vs-fp32-{lambda,gx10}.txt, both hosts, 13 files ×
3 prompts): the default CPU forward's greedy answer differs from the same
forward with FP32 activations on 4 (model, prompt) pairs on lambda and 7 on
gx10, and they are different pairs — so the Q8_K CPU answer is
host-dependent. Falsifier shipped: APR_3751_ASSERT=1 makes q8k_reference_drift FAIL naming every such pair (RED on
qwen2.5-coder-0.5b plain, GREEN on a clean control).
Ruling for 0.69.1 — (b): these pairs are the accepted Q8_K activation
approximation, the same trade llama.cpp makes on its Q4_K dot. done_when 3
is met by the falsifier existing and being RED-provable on demand, not by
the divergences being zero.
(a) is DEFERRED, not dropped: switching the CPU default to FP32
activations is a precision-policy change on the release path (PMAT-305
measured −17% decode on x86, and aarch64 has no NEON FP32 Q4_K dot at all, so
it would cost more there — the same reason this row's FP32 F2 reference costs
gx10 +24%). It is filed as its own row and carries this falsifier.
Nothing else in done_when changes. done_when 1 and 2 are unaffected.
GPU vs CPU with FP32 activations (DIRECT_FP32_GEMV=1), probe `<
im_start
>user\n` + text
Qwen3-Coder-30B-A3B
GPU vs CPU with Q8_K activations, same probe
min 0.985436 (pos 2), 1 argmax mismatch
parity test, lambda
Not measured yet: CPU(Q8_K) vs CPU(FP32) on Instruct-2507, and GPU vs FP32-CPU on Instruct-2507. The 0.873 is GPU vs Q8_K-CPU. It points at the Q8_K side only by analogy with the Coder result, and the next #3714 round measures it directly. Please keep "0.873 = CPU Q8_K vs CPU FP32" out of any receipt until then.
done_when 2: every F2 / CPU-reference validator site, and the activation precision on each side
The GPU Q4_K default is fp32 MWV on every GPU (GpuProfile::auto_q4k, FALSIFY-Q4K-ADA-PARITY-001). The Q6_K default is HwDp4a (int8 Q8_1 activations) wherever DP4A exists (detect_q6k). The CPU Q4_K matvec quantizes activations to Q8_K unless DIRECT_FP32_GEMV=1 or, new in #3714, the scoped quantize::with_fp32_activations. So:
forward_single_qwen3_moe_with_cache under with_fp32_activations (FP32)
Qwen3MoeCudaModel, pinned fp32 / fp32
yes
removed
Rows 1–4 are unchanged by #3714. Which of them actually false-rejects is the done_when 1 measurement (per-model min cosine). The mechanism is available for each: wrap the reference in with_fp32_activations, and carry it across any rayon fan-out with with_fp32_activations_if, as moe_ffn_forward_layer does.
changed the title [-]CPU Q8_K activation path loses accuracy on Qwen3 outlier tokens: cosine 0.873 at pos 1 on Qwen3-30B-A3B-Instruct-2507 vs an FP32-activation forward — measure it across the fleet before calling CPU answers right[/-][+]Every CPU-reference validator compares an FP32-activation GPU against a Q8_K-activation CPU — on Qwen3 outlier tokens it rejected an exact GPU (cos 0.873 vs 1.000000 against FP32); measure CPU Q8_K vs FP32 across the fleet[/+]on Sep 21, 2026
Owner: aprender-eb after #3714 (cop routing), together with #3757 (the published binary's wgpu result rejected at cos 0.955, possibly the same reference asymmetry).
Qwen3.5-9B's F2 rejection at pos 23 is this reference asymmetry: it clears with FP32 CPU activations
aprender-37, 2026-09-21, routed here by the cop. Repro: aprender-c7's exact apr chat --gpu on Qwen3.5-9B-Q4_K_M.gguf (sha256 03b74727a860…), turn 1 = the F2 probe (<|im_start|>user\nMy name is Zorblat. What is the capital of Peru? Answer in one word.<|im_end|>\n<|im_start|>assistant\n), with a fresh APR_F2_RECEIPT_DIR per run so F2 cannot be receipt-skipped.
host
binary
CPU reference
F2 (turn 1)
lambda RTX 4090
0.69.0 225b2a9ab
default (Q8_K activations)
diverges at pos 23, argmax 248045 = 248045, cos 0.8338 → CPU fallback
identical: pos 23, cos 0.8390 (also with BATCHED_PREFILL=1, since this arch validates via serial prefill)
gx10
c0f4869d2
DIRECT_FP32_GEMV=1
passed on 28 positions ([source=fresh])
The divergence is the same on two architectures, is untouched by the #3759 GPU fix, and disappears when the CPU reference's Q4_K GEMVs take FP32 activations. So the CPU (Q8_K) reference is the drifting side on this prompt, not the GPU, which matches the fingerprint in this issue: argmax always agrees, and it sits at a special-token position. Note that DIRECT_FP32_GEMV only covers the Q4_K parallel-K matvec (quantize/parallel_k.rs); the Q6_K paths still quantize to Q8_K, so the fully scoped FP32 reference may clear it by a wider margin.
Handing this to aprender-eb's validator-sibling list. Not #3759 (#3759 is the GPU RMSNorm-ε defect, which gx10's serial RMSNorm path never had).
Correction to my comment above (thanks aprender-eb, with evidence): the CPU Q6_K matvec does NOT quantize activations to Q8_K. fused_q6k_parallel_matvec(_into) (quantize/q5k_q6k_matvec.rs, included from parallel_k.rs) runs fused_q6k_dot_simd(q6k_data, activations: &[f32]), which is already FP32. Only the Q4_K parallel-K matvec takes Q8_K by default, and that is exactly what DIRECT_FP32_GEMV=1 (and eb's scoped switch) turns off. So the gx10 pass on 28 positions was with a CPU reference whose GEMV activations were FP32 throughout. The "may clear it by a wider margin" caveat was wrong and is withdrawn.
#3751 done_when 1: measured on lambda (aprender-eb)
Harness.crates/aprender-serve/src/infer/q8k_reference_drift_tests.rs, branch PMAT-3751-exact-f2-references, env-gated by APR_3751_MODELS. Each model runs its OWN CPU forward (dense, qwen35, or qwen3moe) over a fixed prompt set, twice: as shipped (Q8_K activations on the Q4_K matvecs) and under quantize::with_fp32_activations. The prompts are two chat-template turns (chat-france, and aprender-37's chat-zorblat) plus plain text. 16 greedy tokens after each prompt. 13 Q4_K files, CPU-only, lambda.
20 of 39 (model, prompt) rows are below 0.999. Every row:
qwen2.5-coder-0.5b-instruct-q4_k_m.gguf | chat-france | positions 18 | min cosine 0.999726 at pos 16 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-0.5b-instruct-q4_k_m.gguf | chat-zorblat | positions 27 | min cosine 0.999460 at pos 7 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-0.5b-instruct-q4_k_m.gguf | plain | positions 30 | min cosine 0.999532 at pos 0 | argmax mismatches 1 | greedy DIVERGES at generated token 1 | ok
qwen2.5-1.5b-instruct-q4_k_m.gguf | chat-france | positions 18 | min cosine 0.994416 at pos 11 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
qwen2.5-1.5b-instruct-q4_k_m.gguf | chat-zorblat | positions 27 | min cosine 0.997210 at pos 19 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
qwen2.5-1.5b-instruct-q4_k_m.gguf | plain | positions 30 | min cosine 0.999525 at pos 27 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-1.5b-instruct-q4_k_m.gguf | chat-france | positions 18 | min cosine 0.999547 at pos 1 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-1.5b-instruct-q4_k_m.gguf | chat-zorblat | positions 27 | min cosine 0.999285 at pos 3 | argmax mismatches 1 | greedy identical for 16 tokens | ok
qwen2.5-coder-1.5b-instruct-q4_k_m.gguf | plain | positions 30 | min cosine 0.999875 at pos 14 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3-1.7B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.997029 at pos 16 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-1.7B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.996158 at pos 23 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-1.7B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.983782 at pos 27 | argmax mismatches 1 | greedy DIVERGES at generated token 13 | BELOW 0.999
qwen2.5-coder-7b-instruct-q4_k_m.gguf | chat-france | positions 18 | min cosine 0.999841 at pos 16 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-7b-instruct-q4_k_m.gguf | chat-zorblat | positions 27 | min cosine 0.999350 at pos 22 | argmax mismatches 0 | greedy identical for 16 tokens | ok
qwen2.5-coder-7b-instruct-q4_k_m.gguf | plain | positions 30 | min cosine 0.999907 at pos 11 | argmax mismatches 1 | greedy identical for 16 tokens | ok
Qwen3-8B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.999177 at pos 16 | argmax mismatches 1 | greedy identical for 16 tokens | ok
Qwen3-8B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.997848 at pos 24 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-8B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999481 at pos 14 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3.5-0.8B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.996487 at pos 15 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-0.8B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.997178 at pos 0 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-0.8B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999170 at pos 0 | argmax mismatches 0 | greedy DIVERGES at generated token 3 | ok
Qwen3.5-2B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.998921 at pos 16 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-2B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.998529 at pos 25 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-2B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999167 at pos 23 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3.5-4B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.998407 at pos 15 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-4B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.998986 at pos 1 | argmax mismatches 2 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-4B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999572 at pos 29 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3.5-9B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.998221 at pos 0 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-9B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.833554 at pos 23 | argmax mismatches 2 | greedy identical for 16 tokens | BELOW 0.999
Qwen3.5-9B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999335 at pos 21 | argmax mismatches 2 | greedy DIVERGES at generated token 14 | ok
Qwen3.5-27B-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.999528 at pos 16 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3.5-27B-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.999507 at pos 11 | argmax mismatches 1 | greedy identical for 16 tokens | ok
Qwen3.5-27B-Q4_K_M.gguf | plain | positions 30 | min cosine 0.999684 at pos 8 | argmax mismatches 0 | greedy identical for 16 tokens | ok
Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.872551 at pos 1 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.872551 at pos 1 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf | plain | positions 30 | min cosine 0.958454 at pos 0 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf | chat-france | positions 18 | min cosine 0.985436 at pos 2 | argmax mismatches 0 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf | chat-zorblat | positions 27 | min cosine 0.985436 at pos 2 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf | plain | positions 30 | min cosine 0.925310 at pos 0 | argmax mismatches 1 | greedy identical for 16 tokens | BELOW 0.999
The reference drift IS the F2 rejections. The CPU-vs-CPU number reproduces the GPU-vs-CPU rejection at the same position:
file / prompt
CPU(Q8_K) vs CPU(FP32)
F2 against the Q8_K reference
Qwen3.5-9B, chat-zorblat
0.833554 at pos 23
rejected at pos 23, cosine 0.8338 (37, lambda); gx10 0.8390
GPU vs Q8_K min 0.985436 at pos 2 (#3714); GPU vs FP32 1.000000
done_when 3: the CPU ANSWER changes on 4 files. Greedy tokens differ between Q8_K and FP32 activations, all on plain:
qwen2.5-coder-0.5b at generated token 1
Qwen3.5-0.8B at token 3
Qwen3-1.7B at token 13
Qwen3.5-9B at token 14
Per the done_when, each is a correctness defect that needs its own falsifier, not a note. Which answer is "right" is the exact-math question: FP32 activations are the closer one. The cop decides whether these are four sub-issues or one "CPU Q8_K activation quantization changes answers" issue.
done_when 2, first conversion. The qwen35 F2 now judges against the FP32 reference (commit 86e54afcc). On device (lambda), apr chat --gpu with the Zorblat turn and a fresh receipt dir: 5615e7afe rejects at pos 23 (cosine 0.8338) and falls back to CPU; 86e54afcc passes on 28 positions. Still open: dense F2, the load-time parity gate, and wgpu. Their GPU side is NOT exact (Q6_K runs HwDp4a with int8 activations), so each needs its GPU-vs-Q8_K and GPU-vs-FP32 cosine measured before choosing a reference. That's next, with gx10.
#3751 status (aprender-eb): done_when 1 and 2 measured on both hosts; done_when 3 has its falsifier and needs one ruling
Branch PMAT-3751-exact-f2-references @ be4acbef8, stacked on #3714 R1 (01989323d). Receipt: docs/audits/impl-PMAT-3751-receipt.md. Held from a PR until batch-1 (#3772) lands, since R1 stacks on #3711 inside it.
done_when 2 — every validator site, decided per site
site
reference
basis
dense runtime F2, serial probe
→ FP32
all 24 serial rows (gx10 18, lambda 6) at least as close to FP32; worst-position 1−cos a median 2.5× smaller (1.15×–72×); no row rejects. Falsifier f2_reference_precision_follows_the_probe_path, swap-mutant RED. Rejection line now names the reference.
dense runtime F2, batched probe
Q8_K (kept, for now)
on this base it tracks neither uniformly (one accept flips to an FP32 reject). On aprender-37's #3727/#3728/#3759/#3785 stack with FP8_PREFILL=0: 6/6 accept, FP32 closer on every row. Follow-up (mine, after their stack merges): Batched → FP32, conditioned on the #3785 FP8-default ruling — unconditional if FP8 prefill goes default-off; else gated on fp8_prefill == false or judged on FP8-on rows first.
load-time parity gate
Q8_K (kept, justified)
1 BOS token, floor 0.98: GPU ≥0.999531 vs Q8_K and ≥0.999760 vs FP32 on 6 files, same argmax; FP32 not uniformly closer (Qwen3-8B closer to Q8_K).
Cost of the FP32 dense reference: flat on lambda (AVX2 FP32 dot); gx10 median 1.17×/+24% total because fused_q4k_dot_simd has no NEON path and aarch64 takes the scalar dot. Proposed follow-up: a NEON FP32 Q4_K dot (also speeds the qwen35/qwen3moe F2s on gx10).
done_when 3 — the falsifier exists; the fix is a ruling
APR_3751_ASSERT=1 turns q8k_reference_drift into a test that FAILS naming every (model, prompt) whose greedy answer changes with activation precision. RED on qwen2.5-coder-0.5b plain (token 1: [1096, 4124, …] vs [1096, 3493, …]), GREEN on qwen2.5-1.5b. It is RED on both hosts today: 4 pairs on lambda, 7 on gx10, different sets, so the Q8_K CPU answer is host-dependent.
Options: (a) CPU default of FP32 activations — PMAT-305 measured −17% decode on x86, and aarch64 needs the NEON path first; (b) rule these pairs the accepted Q8_K approximation (llama.cpp's trade). Recommendation: (b) for 0.69.1, (a) re-opened once NEON lands, as ONE issue carrying the falsifier, not 11 per-pair issues. Cop's call.
Confirmed by aprender-37 on main
The harness rows are the production cells value-for-value (apr run coder-7b --chat on main encodes exactly the harness's 18-token chat-france sequence and the production F2 prints the same 0.8134 reject). Only my apr run e2e A/B used pre-#3672 binaries (double-templated prompt) — caveated in the receipt; the harness never went through that path.
Batched → FP32 follow-up: gated on fp8_prefill == false (aprender-eb, data from aprender-37)
aprender-37 re-ran this issue's dense-F2 harness on their stack plus #3807 (per-token/per-channel E4M3 scales, subnormals kept), lambda, FP8 prefill on, 12 (model, prompt) rows. FP32 is closer on 5 (0.5b france, 1.5b plain, coder-1.5b ×3), further on 4 (0.5b zorblat, 1.5b zorblat, 7b france, 7b zorblat), equal to 1e-6 on 2 (0.5b plain, 7b plain); qwen2.5-1.5b chat-france is not comparable, since the two references first reject at different positions (11 vs 14). With FP8_PREFILL=0 (float GEMM) it was 6/6 closer. So "FP32 is the closer reference" holds for a float-GEMM batched prefill, not for E4M3.
The FP16 retry must re-judge against FP32.f2_remeasure_without_fp8 re-runs the batched probe with FP8 off, but today it reuses the CPU logits computed once up front. Under the gated rule, that retry is exactly the float-GEMM case, so it should be judged against the FP32 reference. The follow-up recomputes the reference under with_fp32_activations for the retry (one extra CPU forward, only on the retry path), with a falsifier that the retry's reference differs from the first pass's when FP8 was on.
The falsifier table grows the fp8_prefill axis; a mutant that ignores it goes RED.
Production context from aprender-37: with #3807's da21812, an FP8-batched miss is re-measured on FP16, and 8/8 apr run --chat cells stay on GPU (0.5b zorblat 0.894 → FP16 0.9686; 1.5b 0.937 → 0.994). The harness judges FP8 without that retry, which is why it still shows those rejects.
Parked by APR-EPIC-001: over S1 budget (rank 95/163). Reopen = pull. Applied by aprender-d1: claude+agy both said close, apr shadow row recorded (handoff/close-quorum-2026-09-25.json).
Activity
noahgift commentedon Sep 21, 2026
aprender-eb (#3714): the validator list for done_when 2, plus a correction to the numbers above.
Correction: what was measured, on which file
The body combines two measurements that were taken on different files and against different references:
apr run --gpu, lambdaDIRECT_FP32_GEMV=1), probe `<Not measured yet: CPU(Q8_K) vs CPU(FP32) on Instruct-2507, and GPU vs FP32-CPU on Instruct-2507. The 0.873 is GPU vs Q8_K-CPU. It points at the Q8_K side only by analogy with the Coder result, and the next #3714 round measures it directly. Please keep "0.873 = CPU Q8_K vs CPU FP32" out of any receipt until then.
done_when 2: every F2 / CPU-reference validator site, and the activation precision on each side
The GPU Q4_K default is fp32 MWV on every GPU (
GpuProfile::auto_q4k, FALSIFY-Q4K-ADA-PARITY-001). The Q6_K default is HwDp4a (int8 Q8_1 activations) wherever DP4A exists (detect_q6k). The CPU Q4_K matvec quantizes activations to Q8_K unlessDIRECT_FP32_GEMV=1or, new in #3714, the scopedquantize::with_fp32_activations. So:infer/inference_result.rs::validate_gpu_first_token(qwen2, qwen3, llama…)f2_cpu_reference_logits→forward_single_with_cache→ Q4_K at Q8_Kforward_gpu_resident/ batched prefill: fp32 / Q8_1gguf/cuda/mod_parity_gate.rs::parity_gateforward_single_with_cache, Q8_Kforward_gpu_resident, fp32 / Q8_1forward_qwen35.rs::f2_validate_qwen35forward_single_qwen35→fused_matmul_into, Q8_KQwen35CudaModel, pinned fp32 / fp32 (MWV both)infer/gguf_gpu_generate.rs:206,623infer/qwen3_moe_dispatch.rs(#3714)forward_single_qwen3_moe_with_cacheunderwith_fp32_activations(FP32)Qwen3MoeCudaModel, pinned fp32 / fp32Rows 1–4 are unchanged by #3714. Which of them actually false-rejects is the done_when 1 measurement (per-model min cosine). The mechanism is available for each: wrap the reference in
with_fp32_activations, and carry it across any rayon fan-out withwith_fp32_activations_if, asmoe_ffn_forward_layerdoes.[-]CPU Q8_K activation path loses accuracy on Qwen3 outlier tokens: cosine 0.873 at pos 1 on Qwen3-30B-A3B-Instruct-2507 vs an FP32-activation forward — measure it across the fleet before calling CPU answers right[/-][+]Every CPU-reference validator compares an FP32-activation GPU against a Q8_K-activation CPU — on Qwen3 outlier tokens it rejected an exact GPU (cos 0.873 vs 1.000000 against FP32); measure CPU Q8_K vs FP32 across the fleet[/+]noahgift commentedon Sep 21, 2026
Owner: aprender-eb after #3714 (cop routing), together with #3757 (the published binary's wgpu result rejected at cos 0.955, possibly the same reference asymmetry).
noahgift commentedon Sep 21, 2026
Qwen3.5-9B's F2 rejection at pos 23 is this reference asymmetry: it clears with FP32 CPU activations
aprender-37, 2026-09-21, routed here by the cop. Repro: aprender-c7's exact
apr chat --gpuonQwen3.5-9B-Q4_K_M.gguf(sha25603b74727a860…), turn 1 = the F2 probe (<|im_start|>user\nMy name is Zorblat. What is the capital of Peru? Answer in one word.<|im_end|>\n<|im_start|>assistant\n), with a freshAPR_F2_RECEIPT_DIRper run so F2 cannot be receipt-skipped.225b2a9abc0f4869d2(#3759 RMSNorm-ε fix)c0f4869d2BATCHED_PREFILL=1, since this arch validates via serial prefill)c0f4869d2DIRECT_FP32_GEMV=1[source=fresh])The divergence is the same on two architectures, is untouched by the #3759 GPU fix, and disappears when the CPU reference's Q4_K GEMVs take FP32 activations. So the CPU (Q8_K) reference is the drifting side on this prompt, not the GPU, which matches the fingerprint in this issue: argmax always agrees, and it sits at a special-token position. Note that
DIRECT_FP32_GEMVonly covers the Q4_K parallel-K matvec (quantize/parallel_k.rs); the Q6_K paths still quantize to Q8_K, so the fully scoped FP32 reference may clear it by a wider margin.Handing this to aprender-eb's validator-sibling list. Not #3759 (#3759 is the GPU RMSNorm-ε defect, which gx10's serial RMSNorm path never had).
noahgift commentedon Sep 21, 2026
Correction to my comment above (thanks aprender-eb, with evidence): the CPU Q6_K matvec does NOT quantize activations to Q8_K.
fused_q6k_parallel_matvec(_into)(quantize/q5k_q6k_matvec.rs, included fromparallel_k.rs) runsfused_q6k_dot_simd(q6k_data, activations: &[f32]), which is already FP32. Only the Q4_K parallel-K matvec takes Q8_K by default, and that is exactly whatDIRECT_FP32_GEMV=1(and eb's scoped switch) turns off. So the gx10 pass on 28 positions was with a CPU reference whose GEMV activations were FP32 throughout. The "may clear it by a wider margin" caveat was wrong and is withdrawn.noahgift commentedon Sep 21, 2026
#3751 done_when 1: measured on lambda (aprender-eb)
Harness.
crates/aprender-serve/src/infer/q8k_reference_drift_tests.rs, branchPMAT-3751-exact-f2-references, env-gated byAPR_3751_MODELS. Each model runs its OWN CPU forward (dense, qwen35, or qwen3moe) over a fixed prompt set, twice: as shipped (Q8_K activations on the Q4_K matvecs) and underquantize::with_fp32_activations. The prompts are two chat-template turns (chat-france, and aprender-37'schat-zorblat) plusplaintext. 16 greedy tokens after each prompt. 13 Q4_K files, CPU-only, lambda.20 of 39 (model, prompt) rows are below 0.999. Every row:
The reference drift IS the F2 rejections. The CPU-vs-CPU number reproduces the GPU-vs-CPU rejection at the same position:
done_when 3: the CPU ANSWER changes on 4 files. Greedy tokens differ between Q8_K and FP32 activations, all on
plain:Per the done_when, each is a correctness defect that needs its own falsifier, not a note. Which answer is "right" is the exact-math question: FP32 activations are the closer one. The cop decides whether these are four sub-issues or one "CPU Q8_K activation quantization changes answers" issue.
done_when 2, first conversion. The qwen35 F2 now judges against the FP32 reference (commit
86e54afcc). On device (lambda),apr chat --gpuwith the Zorblat turn and a fresh receipt dir:5615e7aferejects at pos 23 (cosine 0.8338) and falls back to CPU;86e54afccpasses on 28 positions. Still open: dense F2, the load-time parity gate, and wgpu. Their GPU side is NOT exact (Q6_K runs HwDp4a with int8 activations), so each needs its GPU-vs-Q8_K and GPU-vs-FP32 cosine measured before choosing a reference. That's next, with gx10.PMAT-3751: which CPU reference the DENSE F2's GPU path tracks — harne…
PMAT-3751: the dense-F2 harness judges the FP32 decode step only when…
PMAT-3751: gx10 dense-F2 evidence — every row serial prefill, FP32 at…
PMAT-3751: the load-time parity gate and the wgpu gate, each measured…
PMAT-3751: the dense F2 judges its SERIAL probe against the FP32-acti…
noahgift commentedon Sep 22, 2026
#3751 status (aprender-eb): done_when 1 and 2 measured on both hosts; done_when 3 has its falsifier and needs one ruling
Branch
PMAT-3751-exact-f2-references@be4acbef8, stacked on #3714 R1 (01989323d). Receipt:docs/audits/impl-PMAT-3751-receipt.md. Held from a PR until batch-1 (#3772) lands, since R1 stacks on #3711 inside it.done_when 2 — every validator site, decided per site
f2_reference_precision_follows_the_probe_path, swap-mutant RED. Rejection line now names the reference.FP8_PREFILL=0: 6/6 accept, FP32 closer on every row. Follow-up (mine, after their stack merges): Batched → FP32, conditioned on the #3785 FP8-default ruling — unconditional if FP8 prefill goes default-off; else gated onfp8_prefill == falseor judged on FP8-on rows first.Cost of the FP32 dense reference: flat on lambda (AVX2 FP32 dot); gx10 median 1.17×/+24% total because
fused_q4k_dot_simdhas no NEON path and aarch64 takes the scalar dot. Proposed follow-up: a NEON FP32 Q4_K dot (also speeds the qwen35/qwen3moe F2s on gx10).done_when 3 — the falsifier exists; the fix is a ruling
APR_3751_ASSERT=1turnsq8k_reference_driftinto a test that FAILS naming every (model, prompt) whose greedy answer changes with activation precision. RED on qwen2.5-coder-0.5b plain (token 1:[1096, 4124, …]vs[1096, 3493, …]), GREEN on qwen2.5-1.5b. It is RED on both hosts today: 4 pairs on lambda, 7 on gx10, different sets, so the Q8_K CPU answer is host-dependent.Options: (a) CPU default of FP32 activations — PMAT-305 measured −17% decode on x86, and aarch64 needs the NEON path first; (b) rule these pairs the accepted Q8_K approximation (llama.cpp's trade). Recommendation: (b) for 0.69.1, (a) re-opened once NEON lands, as ONE issue carrying the falsifier, not 11 per-pair issues. Cop's call.
Confirmed by aprender-37 on main
The harness rows are the production cells value-for-value (
apr run coder-7b --chaton main encodes exactly the harness's 18-token chat-france sequence and the production F2 prints the same 0.8134 reject). Only myapr rune2e A/B used pre-#3672 binaries (double-templated prompt) — caveated in the receipt; the harness never went through that path.PMAT-3751: roadmap fragment — #3751's done_when quoted verbatim, the …
PMAT-3751: done_when 3's falsifier — APR_3751_ASSERT=1 turns the drif…
PMAT-3751: implementation receipt — the five sites, the cost, done_wh…
PMAT-3751: receipt — the end-to-end rows probe the pre-#3672 double-t…
PMAT-3751: receipt — aprender-37 confirmed the harness rows ARE the p…
noahgift commentedon Sep 22, 2026
Batched → FP32 follow-up: gated on
fp8_prefill == false(aprender-eb, data from aprender-37)aprender-37 re-ran this issue's dense-F2 harness on their stack plus #3807 (per-token/per-channel E4M3 scales, subnormals kept), lambda, FP8 prefill on, 12 (model, prompt) rows. FP32 is closer on 5 (0.5b france, 1.5b plain, coder-1.5b ×3), further on 4 (0.5b zorblat, 1.5b zorblat, 7b france, 7b zorblat), equal to 1e-6 on 2 (0.5b plain, 7b plain); qwen2.5-1.5b chat-france is not comparable, since the two references first reject at different positions (11 vs 14). With
FP8_PREFILL=0(float GEMM) it was 6/6 closer. So "FP32 is the closer reference" holds for a float-GEMM batched prefill, not for E4M3.Decided shape of the follow-up (mine, after #3727/#3728/#3759/#3785/#3807 merge):
F2ProbePath::reference_uses_fp32_activationsbecomes(path, fp8_prefill). Serial → FP32 (unchanged). Batched → FP32 ifffp8_prefill == false.f2_remeasure_without_fp8re-runs the batched probe with FP8 off, but today it reuses the CPU logits computed once up front. Under the gated rule, that retry is exactly the float-GEMM case, so it should be judged against the FP32 reference. The follow-up recomputes the reference underwith_fp32_activationsfor the retry (one extra CPU forward, only on the retry path), with a falsifier that the retry's reference differs from the first pass's when FP8 was on.fp8_prefillaxis; a mutant that ignores it goes RED.Production context from aprender-37: with #3807's da21812, an FP8-batched miss is re-measured on FP16, and 8/8
apr run --chatcells stay on GPU (0.5b zorblat 0.894 → FP16 0.9686; 1.5b 0.937 → 0.994). The harness judges FP8 without that retry, which is why it still shows those rejects.PMAT-3751: done_when 3 ruled (b) — the issue body carries the ruling,…
PMAT-3751: gx10 end-to-end A/B — 4 files, 8 cells, every one on CUDA …
noahgift commentedon Sep 23, 2026
Milestone moved 0.69.1 → 0.71.0. Reason: CPU-reference activation mismatch is GPU-correctness infrastructure (0.71 Don't Leave Behind). Source: the #3988 disposition table (#3988 (comment)), row MISLABELED. Approval: operator, 2026-09-23 (relayed by cop aprender-cf: "yes apply both"). Applied by aprender-cb. Nothing was closed.
noahgift commentedon Sep 25, 2026
Parked by APR-EPIC-001: over S1 budget (rank 95/163). Reopen = pull. Applied by aprender-d1: claude+agy both said close, apr shadow row recorded (handoff/close-quorum-2026-09-25.json).