Skip to content

fix(quantize): Q5_K readers decoded an invented layout — every Q5_K tensor on the CPU path and in apr import was wrong (PMAT-1101) - #3110

Closed
noahgift wants to merge 6 commits into
mainfrom
PMAT-1101-q5k-ggml-layout
Closed

fix(quantize): Q5_K readers decoded an invented layout — every Q5_K tensor on the CPU path and in apr import was wrong (PMAT-1101)#3110
noahgift wants to merge 6 commits into
mainfrom
PMAT-1101-q5k-ggml-layout

Conversation

@noahgift

@noahgift noahgift commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Q5_K readers decoded an invented layout (PMAT-1101). P0 correctness, blocks #3091

What was wrong. ggml's block_q5_K stores 256 values in 176 bytes: d, dmin, 12 bytes of 6-bit scales and mins, qh[32], then qs[128]. Sub-blocks 2c and 2c+1 share the same 32 qs bytes, low nibbles first and then high nibbles. The fifth bit of value l in sub-block s is bit s of qh[l]. Three aprender readers each did something different:

Reader Used by What it did
realizar dequantize_q5_k + fused_q5k_dot every CPU Q5_K matmul (apr run / serve / chat) read the two nibbles of one byte as neighbouring values, and took the fifth bit from qh[4s + l/8]
aprender-quant dequantize_q5_k_to_f32 apr import (GGUF → APR) gave each sub-block its own 16 qs bytes
aprender-quant quantize_q5_k quantizing to Q5_K packed the same wrong way

Measured on Qwen3.5-0.8B-Q4_K_M, where attn_qkv and ssm_out are Q5_K:

Why nothing caught it. Every Q5_K test compared one aprender reader against another. realizar's dequantizer and fused dot shared one invented layout; aprender-quant's quantizer and dequantizer shared another.

Fix

  • realizar: a single block walker, for_each_q5k_value, emits values in ggml order. dequantize_q5_k and fused_q5k_dot both go through it, so the two can no longer disagree.
  • aprender-quant: the reader and the packer now use ggml's layout.

FALSIFY-QDOT-007 (contracts/quantized-dot-product-v1.yaml, both copies) pins the readers to the 256 values gguf-py produced from a real super-block that llama.cpp quantized. The fixture is embedded in the tests.

  • realizar quantize::tests::q5k_ggml::*: dequant is bit-exact; the fused dot with a one-hot activation returns each value exactly; the parallel matvec matches the gguf-py dot.
  • aprender-quant tests::test_q5k_ggml_*: dequant is bit-exact; a quantize round-trip on data whose sub-blocks all differ stays within 0.15.

Mutation check. Each change below was reverted locally, and the tests went red every time:

  • realizar nibble shift: RED
  • realizar qh bit: RED
  • aprender-quant reader: RED
  • aprender-quant packer (sub-block halves swapped): RED

The packer mutant survived the existing ramp round-trip test, because a ramp gives every sub-block the same 5-bit codes. That is why the distinct-sub-block test exists.

Local gates

  • Formatting and tests:
    • cargo fmt --check
    • cargo test -p aprender-quant --lib
    • cargo test -p aprender-serve --lib -- quantize::: 2223 passed
    • the four Q5_K integration targets ✓ (quantize_coverage, property_quantize, gguf_quantize_coverage, quantize_property_tests)
  • Contracts and repo checks:
    • pv validate, both contract copies ✓
    • cargo test -p aprender-contracts --lib
    • check_baseline_ratchets.sh
    • cargo deny check advisories
    • guard_tree.sh --no-cargo ✓, except check_complexity_ratchet.sh: the local pmat is not the pinned 3.39.0, so that failure is the instrument, not the code.
  • Clippy:
    • cargo clippy -p aprender-quant --all-targets -- -D warnings -A unused-variables
    • cargo clippy -p aprender-serve --lib --tests reports 5 errors that already exist on main, none in files this PR touches.

Not in this PR: the CUDA Q5_K kernels in aprender-gpu (q5k/gemv.rs, q5k/gemm.rs) carry two more wrong layouts. They are tracked in a separate issue and will get a GPU-side golden test.

Refs #3091

Pmat-Ticket: PMAT-1101

🤖 Generated with Claude Code

noahgift and others added 3 commits September 11, 2026 00:11
…n the checkout is too shallow to hold it — intel passed on leftover history, every fresh yoga checkout died 'invalid object name 42be156'

42be156 is on main (the v1.5 spec's squash-merge, 2026-09-05) but deeper than CI's checkout; the long-lived intel workspaces carried it from earlier fetches, the ephemeral yoga runners never do, so guard-tree's 'PP-066 spec v1.6 defect table and its v1.5 RED proof' step exited 128 there (#3097 on yoga-build3). One object fetched by SHA when absent; the table and its RED proof are unchanged.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… abbreviated one, so the shallow-checkout fetch fell through

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ensor on the CPU path and in apr import was wrong

ggml's block_q5_K gives sub-blocks 2c and 2c+1 the same 32 qs bytes (low
nibbles, then high nibbles) and takes the fifth bit of value l in
sub-block s from bit s of qh[l]. realizar's dequantize_q5_k and
fused_q5k_dot instead read the two nibbles of one byte as neighbouring
values and took the fifth bit from qh[4s + l/8]. aprender-quant, the
reader behind apr import, gave each sub-block its own 16 qs bytes, and
its quantizer packed that way too.

Measured on Qwen3.5-0.8B-Q4_K_M, whose attn_qkv and ssm_out are Q5_K:
realizar disagreed with gguf-py on 5,959,751 of 6,291,456 values of
blk.0.attn_qkv, and the layer-0 QKV sum was +26.64 against llama.cpp's
-17.06. With this change it is -17.0967, and layers 0-2 of the #3091
trace match llama.cpp.

Nothing caught it because every Q5_K test compared one aprender reader
with another. realizar's dequantizer and fused dot now share a single
block walker, so they cannot drift apart again. FALSIFY-QDOT-009 pins
both readers to the 256 values that gguf-py (llama.cpp's own reader)
produced from a block llama.cpp quantized. A round-trip on data whose
sub-blocks all differ pins the packer to that reader; the old ramp
round-trip gives every sub-block the same codes, and a packer that
swapped sub-blocks survived it. Reverting either reader or the packer
turns the tests red.

The CUDA Q5_K gemv and gemm kernels carry two more wrong layouts. They
get a separate PR.

Refs #3091

Pmat-Ticket: PMAT-1101
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3110 head=634436f33548f5736b55d02215a8e1b2695e3ef1 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

…ckout) — guard-tree fails on every ephemeral runner without it

Pmat-Ticket: PMAT-1101
…cts gaps in falsification ids

guard-cargo's "Contract corpus integrity" step (`cargo test -p
aprender-contracts --test validate_contracts`) failed #3110 with
"quantized-dot-product-v1: test ID gap: expected FALSIFY-QDOT-007, found
FALSIFY-QDOT-009". I had skipped to 009 because
crates/aprender-serve/src/quantize/contract_tests.rs already uses
"FALSIFY-007" and "FALSIFY-QDOT-008". Those labels exist only in that
file's panic strings. The corpus gate enforces the id sequence of the
contract YAML itself, so the entry now takes the next id in the file,
007, everywhere it is cited.

Pmat-Ticket: PMAT-1101
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift added a commit that referenced this pull request Sep 11, 2026
noahgift added a commit that referenced this pull request Sep 11, 2026
…ation ids

Now that #3110's entry is FALSIFY-QDOT-007 (validate_contracts rejects gaps
in the ids), the CUDA GEMV falsifier takes the next id, 008, and the shared
fixture's header names 007/008.

Pmat-Ticket: PMAT-1104
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift
noahgift added this pull request to the merge queue Sep 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 11, 2026
@noahgift
noahgift added this pull request to the merge queue Sep 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 11, 2026
noahgift added a commit that referenced this pull request Sep 11, 2026
…ark row, impossible since the type landed); pr-review-receipt pinned to X64 until reject-76-drop dies on arm64 (#3132)

Train run 34648948737: workspace-test (intel-6) test_chat_completion_request_n_multiple set ChoiceCount::ONE and asserted '"n":5' — the deserializer refuses n!=1 by design, so the row could never pass; it was dark because aprender-serve's integration targets run only when the crate is touched (#3130) and the train touches it (#3110/#3113/#3099). The row now asserts the contract both ways (ONE serialises as 1; n=5 is refused with the client-visible message); 269/270 locally, the one local-only red (test_completion_request_with_all_params, f32-narrowed temperature vs Some(0.7)) does not reproduce in CI's feature-unified build and is not touched here. pr-review-receipt (gx10-pool1): mutate-guard.sh 232/233 — reject-76-drop survived on arm64 exactly as memory recorded for gx10-pool2; the job goes back to X64 with the issue named in the comment. Not a required check (ruleset: gate, workspace-test).

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift

Copy link
Copy Markdown
Contributor Author

Landed on main via train #3127 (cb829fc) — this branch's head is an ancestor of the train head a6148ab (PMAT-1098, 0.67.0).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P0 Critical priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant