diff --git a/docs/audits/impl-PMAT-3430-plan.md b/docs/audits/impl-PMAT-3430-plan.md new file mode 100644 index 0000000000..916fe7ad5c --- /dev/null +++ b/docs/audits/impl-PMAT-3430-plan.md @@ -0,0 +1,168 @@ +# PMAT-3430 plan — PP-QUANT-001 M1: one `GgmlType` + `TRAITS[43]` + +Status: **PLAN v4 — grilled 0/3, 1/3, 1/3 (§6); this revision answers every finding of all three rounds; not yet re-reviewed.** Operator rule (#3421, 2026-09-17): no code until the plan quorum is 3/3 on Q1 and X1. Inputs: Tables S / E / B / G of `docs/audits/impl-PMAT-3427-receipt.md` on draft PR #3447 (`d76b4a417`), measured on `origin/main` @ `7eb81a531`. Everything marked `[V]` below was re-measured for this plan on `origin/main` @ `ee684e94c`; `[U]` is unverified and says so. + +## 1. What is measured `[V]` + +### 1.1 Home crate — derived from `cargo metadata --locked`, normal-dependency closure, workspace members only + +| consumer | workspace crates in its closure | +|---|---| +| aprender-core | apr-format, aprender-common, aprender-compute, aprender-contracts-macros, aprender-gemm-codegen, aprender-quant | +| aprender-compute | aprender-contracts-macros, aprender-gemm-codegen, aprender-quant | +| aprender-serve | aprender-compute, aprender-contracts-macros, aprender-gemm-codegen, aprender-present-core, aprender-present-terminal, aprender-profile-core, aprender-quant | + +Intersection (self included): aprender-compute, aprender-contracts-macros, aprender-gemm-codegen, aprender-quant. Of those, three are leaves (no workspace deps): a proc-macro crate, a codegen crate, and **aprender-quant** (`[lib] name = "trueno_quant"`, one external dep `half`, 1641 lines, "K-quantization formats for GGUF/APR model weights", already exports `Q4_K_BLOCK_SIZE`/`Q4_K_BLOCK_BYTES`-style constants). + +**Proposed home: `aprender-quant`, new module `ggml_type`.** No new crate, so clean-room membership and publish order do not change. aprender-serve does not depend on aprender-core (normal deps), so core cannot be the home; compute could, but it is not the lowest and pulls 162 packages. + +### 1.2 The three enums disagree in more than their id sets + +| | compute `GgmlType` | core `GgmlType` | serve `GgmlQuantType` | +|---|---|---|---| +| file | crates/aprender-compute/src/inference/gguf.rs:27 | crates/aprender-core/src/format/gguf/types.rs:55 | crates/aprender-serve/src/gguf/types.rs:101 | +| ids | 15 | 12 | 16 | +| k-quant variant spelling | `Q4K` | `Q4K` | `Q4K` | +| bf16 spelling | `Bf16` | — | `BF16` | +| only here | `Q8K` | `I8 I16 I32 I64 F64` | `IQ2XXS IQ2XS` | +| id → enum | `fn from_u32` (private) | — | `pub const fn from_id` | +| methods | `block_bytes block_size tensor_bytes` | none | `as_id as_byte as_str from_str_lossy Display` | + +**A size disagreement already exists:** compute's `block_bytes` gives `Q8_1 => 36`; Table S (gguf-py `GGML_QUANT_SIZES`) gives 40. Both upstream sources were read at the pinned sha — see Q1-d. + +## 2. Design (Q1) + +The block below is the API **contract** — names, signatures, semantics — not literal source. The implementation additionally carries what `crates/aprender-quant/Cargo.toml`'s lint table demands (`missing_docs`, `pedantic`): a doc comment on every public item and variant, `#[must_use]` on every value-returning method (serve's enum already does this), backticked identifiers in docs. `cargo clippy -p aprender-quant --lib -- -D warnings` is part of Phase 2's acceptance, so none of that is left to taste. `non_camel_case_types` does not fire on variants (measured by lanes 1–3; the three existing enums carry no allow for it). + +```rust +// crates/aprender-quant/src/ggml_type.rs (lib name: trueno_quant) +#[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GgmlType { F32 = 0, F16 = 1, Q4_0 = 2, /* … 35 live variants, upstream ids … */ Q2_0 = 42 } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GgmlFamily { Float, Int, Affine, KQuant, Codebook, Ternary, MicroscaleFp, Removed } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct QuantTraits { pub name: &'static str, pub blck_size: u32, pub type_size: u32, pub family: GgmlFamily } +pub const TRAITS: [QuantTraits; 43] = [ /* indexed BY ID; the 8 removed ids are rows with family = Removed, sizes 0 */ ]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] // + impl Display + impl std::error::Error +pub enum GgmlTypeError { Removed { id: u32, name: &'static str }, Unknown { id: u32 } } +impl GgmlType { // THE COMPLETE leaf API. Every name and signature that exists on any of the three enums today is here, unchanged. + // from serve's GgmlQuantType (crates/aprender-serve/src/gguf/types.rs:136-221): + pub const fn from_id(id: u32) -> Option; // match, never transmute; all 35 live ids + pub const fn as_id(self) -> u32; + pub const fn as_byte(self) -> u8; // every id <= 42 + pub const fn as_str(self) -> &'static str; // == TRAITS[id].name; measured equal to serve's 16 strings today ("Q4_K", "IQ2_XXS", "BF16", …) + pub fn from_str_lossy(s: &str) -> Option; // exact upstream name or its all-lowercase form, as today + // from compute's GgmlType (crates/aprender-compute/src/inference/gguf.rs:68-101): + pub const fn block_bytes(&self) -> usize; // == TRAITS[id].type_size; `&self` kept so compute's call sites are untouched + pub const fn block_size(&self) -> usize; // == TRAITS[id].blck_size + pub const fn tensor_bytes(&self, n_elements: usize) -> usize; // compute's rounding-up formula, unchanged + // new: + pub const fn try_from_id(id: u32) -> Result; // names WHY: Removed vs Unknown + pub const fn traits(self) -> &'static QuantTraits; + pub const fn checked_tensor_bytes(self, n_elements: usize) -> Option; // reads TRAITS directly (const-callable); None on overflow or n % blck_size != 0; M4 (#3432) adopts it +} +impl std::fmt::Display for GgmlType { /* as_str, as serve has today — the only trait impl on any of the three (measured) */ } +// `blck_size` / `type_size` are TRAITS FIELD names only (upstream's spelling); there is no second method spelling. +``` + +Decisions put to the quorum: + +- **Q1-a. 35 variants, 43 table rows.** A removed id is not constructible; `try_from_id(4)` is `Err(Removed{..})`, distinct from `Err(Unknown{..})`; `from_id(4)` is `None`. The table stays dense so `TRAITS[id]` is the lookup and a `const` assertion proves `TRAITS[v.as_id()].name == v.name()` for every variant. +- **Q1-b. Variant spelling — one spelling, NO aliases.** Keep the tree's `Q4K`-style (all three enums agree on it) and `BF16`. v1 proposed an associated-const alias `Bf16`; that is withdrawn: measured with rustc, `match self { Self::Bf16 => … }` on `&self` is **E0308** (a const pattern gets no default-binding-mode deref, unlike a variant pattern), it needs `#[allow(non_upper_case_globals)]`, and a glob import does not carry it. Instead compute's `GgmlType::Bf16` sites are renamed to `BF16` in Phase 3 (2 files: `crates/aprender-compute/src/inference/gguf.rs`, `…/inference/model.rs`; the `Bf16` in `brick/tracing/quant_type.rs` belongs to a different enum and is not touched). +- **Q1-c. M1 is behaviour-preserving: no crate accepts an id or a name it refused before — enforced at every id→enum and name→enum BOUNDARY, not only at the GGUF parser.** v1 gated the parser only; the grill found boundaries outside it. The measured list — serve: `gguf/dtype.rs:17` and `:54`, `infer/mod.rs:39`, `apr/mod.rs:324` (APR dtype byte), `apr/special_tokens.rs:210`, `apr/dequant.rs:235`, test `gguf/tests/loader_tests_apr.rs:311`. compute: private `from_u32` in `inference/gguf.rs`. **core has NO boundary** (v2 was wrong): `crates/aprender-core/src/format/gguf/reader.rs:325` keeps `dtype: u32` raw and nothing in core turns an integer or a string into `GgmlType` — core builds the enum from literals only, so core gets no admission function and no `u32` field is retyped. Mechanism: each crate gets ONE admission function (`admitted_from_id(u32) -> Option`, and in serve `admitted_from_name(&str)`) that is `GgmlType::from_id` filtered by a `const ADMITTED: [GgmlType; N]` reproducing the former set exactly (compute 15, serve 16), and every boundary above calls it instead of `from_id`/`from_str_lossy`. **Boundary function names survive:** compute's private `fn from_u32(v: u32) -> Option` stays under that name as a free function in `inference/gguf.rs` and IS compute's admission function (it cannot remain an inherent method of a re-exported type); its one caller changes from `GgmlType::from_u32(x)` to `from_u32(x)`. serve's boundary functions (`gguf/dtype.rs`, `infer/mod.rs:39`, `apr/mod.rs:324`, `apr/special_tokens.rs:210`, `apr/dequant.rs:235`) keep their names and signatures. So the Phase 1 characterization tests call the same symbols before and after; the single permitted edit to a Phase 1 test in Phase 3 is that `GgmlType::from_u32` → `from_u32` path change, named here in advance. **Proof is a characterization test written in Phase 1, before any change,** placed as a `#[cfg(test)]` child module of each boundary's own module (a child module sees its ancestors' private items, so no visibility is widened; the only non-test edit in Phase 1 is the `mod …;` line)**:** for each boundary function, its output for every id 0..=255 (and every name in `TRAITS` plus case variants) is snapshotted on `origin/main`, and the same test must pass unchanged after Phases 3–4. Wildcard arms (`_ =>`; ~22 across 8 files that name these enums, e.g. `crates/aprender-compute/src/inference/model.rs:734` "quantized norms … return zeros") are therefore not widened in practice: they are already reached today by admitted quantized ids, and a non-admitted variant can only arrive by a new literal in new code. Phase 3/4 receipts still list every wildcard site with its disposition. **Exhaustive matches (no wildcard) stop compiling — E0004 — and that is the compiler enumerating them.** Rule: such a match is replaced by delegation to the leaf (`TRAITS`-backed method), never patched with a bare `_ =>`. One is known: `GgufTensor::byte_size`, `crates/aprender-core/src/format/gguf/types.rs:361`, becomes `self.dtype.tensor_bytes(elements as usize)`. The hand-written arm sizes `Q4_1` at 18 bytes/block (`Q4_0 | Q4_1 => … * 18`, `:365`); upstream and `TRAITS` say 20 — a latent export-size defect. **It is NOT fixed inside M1** (round 3, lane 1: a fix riding a refactor breaks M1's own behaviour-preserving claim; adopted). It lands first as its own issue and its own small PR — Phase 0b in §4: split the arm, `Q4_1 => … * 20`, one test. After that, delegation in Phase 3 is value-identical for all 12 of core's variants, and Phase 1 asserts exactly that: `byte_size` for each of the 12, on a block-aligned and a non-aligned element count, snapshotted before and unchanged after. Any OTHER exhaustive match the compiler reports whose delegated value differs from its hand-written arm is a stop-the-line finding, handled the same way — never a silent fix. +- **Q1-d. Q8_1 `type_size` = 36. Measured at llama.cpp `3173a56471c1753650cd806694145ffd6dcace67`:** `ggml/src/ggml-common.h:269` is `static_assert(sizeof(block_q8_1) == 2*sizeof(ggml_half) + QK8_1, "wrong q8_1 block size/padding")` = 36 — the assert rules out padding, so `sizeof` IS the on-disk block — while `gguf-py/gguf/constants.py:5678` says `(32, 4 + 4 + 32)` = 40 (the pre-f16 layout). Truth source for `type_size` is the `static_assert`ed C struct; gguf-py is the cross-check. The falsifier becomes: `TRAITS` equals gguf-py `GGML_QUANT_SIZES` for every live id **except a named exception list, today exactly `[Q8_1]`**, each exception carrying both upstream file:line citations in `contracts/ggml-type-v1.yaml`. The test fails if an exception stops being a disagreement (so the list cannot rot). This amends the ticket's falsifier wording; the amendment goes on #3430 with the verdict grid. (Grill: lanes 2, 3 ruled 36; lane 1 ruled 40 on a padding argument the `static_assert` refutes.) +- **Q1-e. Re-exports.** compute: `pub use trueno_quant::GgmlType;`. core: same. serve: `pub use trueno_quant::GgmlType as GgmlQuantType;` plus `GgmlType`. serve's `from_id`/`as_id`/`as_byte`/`as_str`/`from_str_lossy`/`Display` and compute's `block_bytes`/`block_size`/`tensor_bytes` move into the leaf with **unchanged names and signatures** (all three lanes: no body depends on a crate-local type; `TruenoError` is used by the parser, not the enum). `from_id` stays `Option` so `apr/mod.rs:325`'s zero-arg `map_or_else` and `loader_tests_apr.rs:311`'s `.is_some()` compile untouched; `block_bytes` stays as a name alongside `type_size`. +- **Q1-f. #3405's private `QTYPE_LABELS` (13 rows, `crates/aprender-serve/src/gguf/qwen3_moe_load.rs:90`)** is replaced in this ticket — it is a fourth table of the same fact. `qtype_label(qtype: u32)` (`:107`) takes an arbitrary `u32`, so the replacement is `GgmlType::from_id(qtype).map_or_else(|| format!("qtype {qtype}"), |t| format!("{}({qtype})", t.as_str()))` — never a raw `TRAITS[id]` index, which would panic for id ≥ 43. The 13 existing labels are measured equal to upstream names. Second named delta, diagnostics only: ids the 13-row table lacked (e.g. 9, 15–29) now render their name instead of `qtype N` in that error message. + +### Table E — the 24 "keep?" rows resolved + +Rule applied: `merge` iff the enum's variants are ggml tensor types and nothing else (a subset of Table S by meaning, not by spelling); `keep(concept)` names the different concept. **Execution of every `merge` below is Q2 (#3423 Phase 2), not M1** — M1 lands the target they merge into. + +| enum | file | verdict | +|---|---|---| +| `QuantKernel` | crates/aprender-cgp/src/profilers/quant.rs:9 | keep(kernel identity — `Nf4Gemv` is not a ggml type) | +| `QuantType` | crates/aprender-core/src/format/quantize.rs:39 | keep(APR's own on-disk id space: `0x01/0x02/0x10/0xFF`, has `Q8Tensor`, `Custom`) | +| `GgufValue` | crates/aprender-cbtop/src/quantize/gguf.rs:24 | keep(metadata value, not tensor type) | +| `AprQuantizationType` | crates/aprender-serve/src/apr_transformer/loader.rs:303 | merge (F32/Q4_K/Q8_0 — a ggml subset used as a loader selector) | +| `Quantization` | crates/aprender-train-inspect/src/convert.rs:113 | merge (Q4_0/Q8_0/F16) | +| `KvQuantType` | crates/aprender-serve/src/paged_kv/mod_compute_prefix.rs:254 | keep(KV-cache precision: FP32/Q8/Q4 are cache layouts, not ggml block formats) — **dissent:** lane 1 would merge, lanes 2 and 3 keep; Q2 re-rules after reading the cache's block layout | +| `QuantizedKvData` | crates/aprender-serve/src/paged_kv/mod_compute_prefix.rs:410 | keep(data carrier, not a type id) | +| `GgufQuantization` | crates/aprender-train/src/hf_pipeline/export/gguf_writer.rs:13 | merge → `Option` (`None` variant is "don't quantize") | +| `GGUFQuantType` | crates/aprender-train/src/quant/gguf_quant/quant_type.rs:5 | merge (Q4_0/Q8_0) | +| `QuantScheme` | crates/apr-cli/src/commands/quantize.rs:27 | keep(user-facing scheme: Int8/Int4/Fp16/Q4K — maps onto types, is not one) | +| `QuantizationType` | crates/aprender-core/src/format/converter_types_expectations.rs:149 | keep(same scheme concept as the row above; these two duplicate EACH OTHER — file for Q2) | +| `QuantizationType` | crates/aprender-registry/src/lineage/mod.rs:53 | keep(lineage metadata: has `Dynamic`) | +| `AutoQuantError` · `QuantizationError` · `QuantPublishError` | crates/apr-cli/src/commands/auto_quant.rs:100 · crates/aprender-orchestrate/src/oracle/rag/quantization/error.rs:7 · crates/aprender-train/src/hf_pipeline/export/publish_pipeline.rs:66 | keep(error types) ×3 | +| `QuantizeArgvVerdict` | crates/apr-cli/src/commands/quantize_flag_parity.rs:49 | keep(verdict) | +| `QuantScheme` | crates/aprender-cbtop/src/grammar/transform.rs:7 | keep(grammar) | +| `QuantizationType` | crates/aprender-core/src/demo/mod.rs:344 · crates/aprender-core/src/stack/mod.rs:223 | keep(demo / stack descriptor) ×2 | +| `GgufValue` | crates/aprender-core/src/format/gguf/types.rs:84 | keep(metadata value) | +| `QuantFamily` | crates/aprender-serve/src/quantize/format_trait.rs:31 | keep(kernel-format family, 2 variants) — which is why §2 names the new enum `GgmlFamily`, not `QuantFamily`; revisit in Q2 | +| `QuantMethod` · `QuantGranularity` · `QuantMode` | crates/aprender-train/src/config/cli/quant_merge.rs:76 · crates/aprender-train/src/quant/granularity/types.rs:7, :19 | keep(training-time quantization config) ×3 | + +Count: 4 merge · 20 keep(concept). + +## 3. Ordering (X1) + +Q1 (this ticket) → T1/T2 (#3433/#3434) → Q2 → Q3. Evidence: 14 of 34 serve raw-integer qtype sites (41 %, file-level) sit in a file that uses a tensor carrier; the falsifier for running T1 ∥ Q2 was < 20 %. Nothing in §1–§2 changes that: M1 retypes no `qtype: u32` field (Table B's 53 rows are untouched), so it does not collide with T1. + +## 4. Phases and acceptance commands + +| # | phase | scope_paths | `A_i` | +|---|---|---|---| +| 0b | **separate PR, lands first:** core `GgufTensor::byte_size` sizes `Q4_1` at 20, not 18 (own issue) | crates/aprender-core/src/format/gguf/ | `cargo test -p aprender-core --lib -- format::gguf` incl. a new `Q4_1` test that is RED before the one-line fix | +| 1 | RED: falsifier guard, upstream size fixture, boundary characterization tests (snapshot taken on unchanged code; `#[cfg(test)]` child modules + their `mod` lines only) | scripts/check_one_ggml_type_enum.sh, contracts/ggml-type-v1.yaml, crates/aprender-quant/tests/, crates/aprender-serve/src/gguf/, crates/aprender-serve/src/apr/, crates/aprender-serve/src/infer/, crates/aprender-compute/src/inference/, crates/aprender-core/src/format/gguf/ | `bash scripts/check_one_ggml_type_enum.sh` exits 1 naming exactly 3 definitions; its case table passes (must-match: the 3; must-not-match: `GgufValueType`, `GgufValue`, `QuantType`, a commented-out enum); characterization tests GREEN on unchanged code | +| 2 | `GgmlType` + `TRAITS[43]` in aprender-quant | crates/aprender-quant/ | `cargo test -p aprender-quant && cargo clippy -p aprender-quant --lib -- -D warnings` (lib AND tests/): every live id round-trips, 8 removed ids are `None` / `Err(Removed)`, `TRAITS` vs the pinned fixture with the exception list exactly `[Q8_1]` | +| 3 | compute + core re-export, compute admission, `Bf16`→`BF16`, `GgufTensor::byte_size` delegates (value-identical for all 12 after Phase 0b) | crates/aprender-compute/src/inference/, crates/aprender-core/src/format/ | `cargo test -p aprender-compute --lib -- inference:: && cargo test -p aprender-core --lib -- format::` — includes the Phase 1 characterization tests, unchanged | +| 4 | serve re-export, admission at all 7 boundaries, `QTYPE_LABELS` removed via `from_id` (no raw index) | crates/aprender-serve/src/gguf/, crates/aprender-serve/src/apr/, crates/aprender-serve/src/infer/, crates/aprender-serve/tests/apr_coverage.rs | `cargo test -p aprender-serve --lib -- gguf:: apr:: infer:: && cargo test -p aprender-serve --test apr_coverage` + `bash scripts/check_one_ggml_type_enum.sh` exits **0** + `cargo check --workspace --all-targets` (apr-cli's `tests_trace_dispatch.rs` uses core's re-export) | +| 5 | DoD: `pv validate`, mutation (re-add a second enum → guard RED; change one `ADMITTED` set → characterization RED; set `TRAITS[9].type_size = 40` → fixture RED; `TRAITS[3].type_size = 18` → core byte_size characterization RED), diff quorum, receipt, PR | docs/audits/, .github CI selection line (needs the operator's web-UI merge click) | `make gate` | + +Phases 3 and 4 have disjoint scopes and may run together. The guard is wired into the same PR's CI selection line or it is dark. + +## 5. Not in this ticket + +Retyping `qtype: u32` fields (Table B) — T1/Q2. The 558 bare-integer lines — #3431. `tensor_byte_size` single refusal — #3432. Executing the 4 Table E merges and the 10 merge-candidates — #3423 Phase 2. Widening any crate's admitted id set. + +## 6. Grill record — v1, 2026-09-17, 3 lanes (gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high; author fable-5-1): **0/3 PASS** + +Reducer: `agreed=false`, `partial=true` (each lane's `.err` had 2 lines beyond workspace narration). Every finding below was re-executed by the orchestrator before being accepted. + +| finding | lanes | re-run | disposition in v2 | +|---|---|---|---| +| assoc-const alias `Bf16` fails in `match self` on `&self` | 2 (E0308), 3 (lint, glob) ; 1 said it works | rustc: **E0308 reproduced** — lane 1 was wrong | Q1-b: aliases withdrawn, rename 2 files | +| admission at the GGUF parser only; `apr/mod.rs:324` and 5 more boundaries bypass it | 1, 2, 3 | grep: 7 serve boundaries confirmed | Q1-c rewritten; characterization tests | +| `from_id → Result` and `tensor_bytes → Option` break callers | 2, 3 | `apr/mod.rs:325`, `loader_tests_apr.rs:311` confirmed | signatures kept; `try_from_id` / `checked_tensor_bytes` added | +| Q8_1: 36 vs 40 | 2, 3 → 36 ; 1 → 40 | upstream read at the full sha: C `static_assert` = 36, gguf-py = 40 | Q1-d: 36, named exception | +| `KvQuantType` should merge | 1 ; 3 agrees with keep | not re-run (Q2 executes it) | recorded as dissent in Table E | +| Phase 2 `--lib` filter skips `tests/`; Phase 4 `gguf::types` exercises 0 `GgmlQuantType` sites | 2, 3 | — | §4 acceptance commands widened | +| scope_paths miss `serve/src/apr/`, `serve/src/infer/`, core `format/` siblings, the `QTYPE_LABELS` file; Table E paths not repo-relative | 1, 2, 3 | usage footprint measured: core 22 files under `format/`, serve 9 files in 4 dirs + `tests/` | §4 and Table E corrected | +| home crate `aprender-quant`: sound, no cycle | 1, 2, 3 | cargo metadata (§1.1) | unchanged | +| X1 ordering | no lane contradicted it | — | unchanged | + +### Round 2 — v2, 3 lanes, same models: **1/3 PASS** (lane 3 PASS with notes; lanes 1, 2 FAIL) + +Reducer: `agreed=false`, `partial=true` — lane 2 exited 3, `LANE ISOLATION VIOLATED - a shared ref changed (refs/remotes/origin/HEAD, refs/remotes/origin/main)`. repo_root is a linked worktree sharing the main checkout's git dir; another session fetched `origin` during the lane window. Lane 2's own clone came back byte-identical. Its findings were kept because each was re-run below, not because its verdict was counted. + +| finding | lanes | re-run | disposition in v3 | +|---|---|---|---| +| exhaustive match `GgufTensor::byte_size`, core `format/gguf/types.rs:361` → E0004 | 1, 2, 3 | confirmed; **and its `Q4_1` arm is 18 where upstream is 20** (found on the re-run, no lane saw it) | Q1-c: delegation rule + the named Q4_1 delta | +| core `format/gguf/reader.rs` is not a boundary (stores raw `u32`) | 1, 2 ; lane 3 agreed core has none | grep: no `u32 → GgmlType` anywhere in core | Q1-c corrected; core gets no admission function | +| §2 API block omitted `as_byte as_str from_str_lossy block_bytes block_size Display` | 2 ; 3 (`block_size` vs `blck_size`) | only trait impl on the three is serve's `Display` (grep) | §2 block is now the complete API; one method spelling | +| `TRAITS[id]` raw index panics for `qtype ≥ 43` in `qtype_label` | 2 | `qwen3_moe_load.rs:107` takes arbitrary `u32` — confirmed | Q1-f: via `from_id` | +| cargo test filters `inference:: format:: gguf:: apr:: infer::` select tests | 2, 3 verified | — | unchanged | + +### Round 3 — v3, 3 lanes, same models: **1/3 PASS** (lane 3 PASS; lanes 1, 2 FAIL). Reducer `partial=false`; no isolation violation this round. Lanes 1 and 2 compiled probe crates inside their sandboxed clones (KEPT, nothing escaped). + +No lane disputed the architecture any more: home crate, 35/43, Q8_1 = 36, boundaries (compute 1, serve 7, core 0), `byte_size` as the only exhaustive match, Table E and X1 were each called sound by all three. What failed is below. + +| finding | lanes | re-run | disposition in v4 | +|---|---|---|---| +| Q4_1 18→20 must not ride the refactor — land it first, separately | 1 blocking ; 2, 3 accepted it in-ticket | the 18 is confirmed (`types.rs:365`) | adopted: Phase 0b, own issue + PR | +| §2 block fails `clippy -D warnings` in aprender-quant (`missing_docs`, `must_use_candidate`, doc backticks) | 1, 2 | lint table read: `missing_docs = warn`, `pedantic = warn` | §2 preamble: block is the contract; clippy is in Phase 2's `A_i` | +| aux types lack derives; `GgmlTypeError` lacks `Display`/`Error` | 2 | — | added | +| `const fn checked_tensor_bytes` cannot call non-const `block_size` | 2 | — | all size methods `const fn`; `&self` kept | +| Phase 1 tests on compute's private `from_u32` break in Phase 3 (E0599) | 2 ; lane 1 said reachable | both are right: reachable from a child module, and the inherent method cannot survive a re-export | Q1-c: `from_u32` survives as a free fn; the one path edit is named in advance | +| Phase 1 scope omits serve `gguf/dtype.rs` | 2 | `gguf/tests/` was too narrow — confirmed | Phase 1 scope widened to the boundary modules' directories | + diff --git a/docs/audits/impl-PMAT-3430-receipt.md b/docs/audits/impl-PMAT-3430-receipt.md new file mode 100644 index 0000000000..8f89dcfaf0 --- /dev/null +++ b/docs/audits/impl-PMAT-3430-receipt.md @@ -0,0 +1,104 @@ +# IMPL receipt — PMAT-3430 — PP-QUANT-001 M1 (plan phase only) + +Verdict: **PARTIAL(escalate)** — plan quorum is not 3/3 after four rounds; the operator rules. No code was written. No v5. + +## Identity +- ticket PMAT-3430 (#3430), kind=code, branch `PMAT-3430-one-ggml-type`, orchestrator model fable-5-1 admitted by `model-gate.sh` (`orch:fable`, `orch-basis:M>=3` — from main's fragment, landed by #3448; not written by this session). +- First launch of this session was refused at Phase 0 (`kind-gate` exit 2: ticket not filed on main; `model-gate` exit 1 against the then-unlabelled entry). Work started only after the entry carrying the basis existed on `origin/PMAT-3427-roadmap-entries`; the workaround commit that pulled it was dropped by rebase once #3448 merged. `git diff origin/main...HEAD -- docs/roadmaps` is empty. +- Harness findings: `pmat hooks install --strict --force` fails in a linked worktree (`Error: Not a directory (os error 20)`); the edit hook keys `active-ticket` on the session cwd's repo key, not the edited file's, so one was written by hand at the path the hook named; `estimate.sh aprender 5` exits 2 (47 rows, none pooled) so `K̂=24` is the issue's borrowed `[A]` figure entered as `first-run[U]`, the only basis `goal.sh` admits; `receipt-lint.sh` has no `--kind delegate`. + +## Dispatch ledger +| round | description prefix | lane | width | route | agy conversations | delegate turns hit? | +|---|---|---|---|---|---|---| +| v1 | PMAT-3430/ph0.delegate | quorum/grillme | 3 | route=agy-plan w=1.00 basis=absent effort=1[U] | (receipt not emitted — delegate hit 30 turns; lanes + reduce-artifact read from disk) | yes | +| v2 | PMAT-3430/ph0.delegate2 | quorum/grillme | 3 | same | 5588db06…, 909d9ee2…, 99f05b71… | no | +| v3 | PMAT-3430/ph0.delegate3 | quorum/grillme | 3 | same | d0d7bffd…, 9ec33c0a…, 8ba37edd… | no | +| v4 | PMAT-3430/ph0.delegate4 | quorum/grillme | 3 | same | 2efab8bf…, 6efcbd2f…, f42ccf0d… | no | + +Slots: 1 Claude subagent live at any instant (slots=3), 0 resumes, 0 hook denials, 0 stalls. Review lanes wrote rustc probes into their sandboxed clones in v1, v3, v4 (KEPT; nothing reached the checkout). + +I-3 (`transcript-gate.sh`, run from the session cwd; run from the worktree it looks in the wrong project directory and passes vacuously with `attempted=0` — a harness finding): +``` +PASS transcript-gate: attempted=4 denied=0 stalled=0 running_peak=1 slots=3 (agent_calls=4 resumes=0 workflow_started=0) +``` + +## PMAT-3430 plan quorum — history, v1–v4. **Not 3/3. No v5 written; no code written.** + +Plan: `docs/audits/impl-PMAT-3430-plan.md` on branch `PMAT-3430-one-ggml-type` (draft PR linked below). Lanes: agy `grillme`, review-only, none in the author's model family (author `fable-5-1`). Every objection below was re-executed by the orchestrator before it was answered; two lane claims were refuted on re-run (lane 1 v1: "assoc-const alias works in patterns" — rustc E0308; lane 1 v1: "Q8_1 = 40, C struct has padding" — upstream `static_assert` says 36). + +### 1. Grid + +| lane (model) | v1 | v2 | v3 | v4 | +|---|---|---|---|---| +| 1 gemini-3.1-pro-high | FAIL | FAIL | FAIL | FAIL † | +| 2 gemini-3.8-flash-high | FAIL | FAIL † | FAIL | FAIL † | +| 3 gemini-3.7-flash-high | FAIL | PASS | PASS | PASS † | +| **PASS count** | **0/3** | **1/3** | **1/3** | **1/3** | +| reducer | partial | partial | clean | partial | + +† lane exited 3, `LANE ISOLATION VIOLATED — a shared ref changed`: the plan worktree shares a git dir with the main checkout, and other sessions moved refs during the lane window (v2: `refs/remotes/origin/main`; v4: `refs/heads/PMAT-3445-t0-milestone-gate`, `refs/heads/PMAT-3451-ont-6-verdict-lattice`, a merge-queue ref). Each lane's own clone was unaffected, but under the rail those verdicts are unclean — **v3 is the only round with three clean lanes, and it was 1/3.** v1's `partial` was 2 stderr lines per lane beyond workspace narration. + +### 2. Every REJECT / AMEND reason, verbatim from the lane's `findings[].claim`, one tag each + +| round | lane | tag | at | reason (verbatim) | +|---|---|---|---|---| +| v1 | 1 | Q1 | `crates/aprender-compute/src/inference/model.rs:712` | Q1-c: Admission function fails to protect cross-crate passing. If serve admits IQ2XS and passes a TensorInfo to compute, it silently hits the wildcard and widens to returning zeros. | +| v1 | 1 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:58` | Q1-d: gguf-py is authoritative for bytes-on-disk in a GGUF file. The TRAITS falsifier must compare against gguf-py (40), not the C struct sizeof (36). | +| v1 | 1 | Table E resolutions | `crates/aprender-serve/src/paged_kv/mod_compute_prefix.rs:254` | TABLE E: KvQuantType variants (FP32, Q8, Q4) are exactly ggml block formats. It meets the merge rule. | +| v1 | 1 | plan mechanics | `crates/aprender-core/src/format/tensors_tests_workflow.rs:82` | PHASES (Phase 3): Scope path misses core files using GgmlType. | +| v1 | 1 | plan mechanics | `crates/aprender-serve/src/apr/mod.rs:324` | PHASES (Phase 4): Scope path misses serve files using GgmlQuantType. | +| v1 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:56` | compute's Bf16 becomes an associated const alias pub const Bf16: Self = Self::BF16 inside the quant crate — usable in expressions and patterns — so M1 touches no dispatch arm. | +| v1 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:57` | M1 is behaviour-preserving: no crate accepts an id it refused before. Going from 12/15/16 to 35 variants would silently widen every match that ends in _ =>. So each crate keeps a thin, named admission function (supported(GgmlType) -> bool, one each in compute, core and serve) that reproduces its former id set exactly, and its parser calls it. | +| v1 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:46` | pub const fn from_id(id: u32) -> Result; ... serve's as_str/from_str_lossy/as_byte/Display move into the quant crate | +| v1 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:49` | pub const fn tensor_bytes(self, n_elements: usize) -> Option; // checked: None on overflow or n % blck_size != 0 | +| v1 | 2 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:101` | Phase 4 A_i: cargo test -p aprender-serve --lib gguf::types + bash scripts/check_one_ggml_type_enum.sh exits 0 | +| v1 | 2 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:101` | Phase 4 scope_paths: crates/aprender-serve/src/gguf/, the #3405 file | +| v1 | 2 | Table E resolutions | `docs/audits/impl-PMAT-3430-plan.md:76` | Table E file paths: apr-cli quantize.rs:27, apr-cli quantize_flag_parity.rs:49, aprender-train quant_type.rs:5, aprender-core converter_types_expectations.rs:149, aprender-cgp profilers/quant.rs:9, aprender-serve paged_kv:254 | +| v1 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:31` | The ticket's falsifier says 'equals gguf-py for every shared id', which would bake in 40. Phase 2 must read both upstream files at 3173a5647 and the quorum is asked to rule (§2, Q1-d). | +| v1 | 3 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:56` | Associated const alias pub const Bf16 triggers non_upper_case_globals compiler warning/error and does not work with glob imports (use GgmlType::*). | +| v1 | 3 | Q1 | `crates/aprender-serve/src/gguf/tests/loader_tests_apr.rs:311` | GgmlQuantType::from_id changing from Option to Result breaks existing call sites such as .is_some() in loader_tests_apr.rs:311. | +| v1 | 3 | Q1 | `crates/aprender-serve/src/apr/mod.rs:324` | Non-GGUF callers of from_id, specifically TensorEntry::from_binary in apr/mod.rs:324, will silently admit new variants when from_id expands to 35 variants without going through the GGUF parser admission function. | +| v1 | 3 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:99` | Phase 2 acceptance command `cargo test -p aprender-quant --lib ggml_type` does not run integration tests in crates/aprender-quant/tests/ (declared in Phase 1 scope_paths). | +| v1 | 3 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:101` | Phase 4 acceptance command `cargo test -p aprender-serve --lib gguf::types` runs only 10 unit tests in types.rs and does not execute qwen3_moe_load.rs (#3405 QTYPE_LABELS removal) or loader_tests_apr.rs. | +| v2 | 1 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:59` | The plan claims `core: format/gguf/reader.rs` is an id-to-enum boundary and will get an admission function. | +| v2 | 1 | Q1 | `crates/aprender-core/src/format/gguf/types.rs:361` | The plan fails to account for an existing exhaustive match on `GgmlType` in core that will become non-exhaustive. | +| v2 | 2 | Q1 | `crates/aprender-core/src/format/gguf/types.rs:361` | The plan assumes all matches on GgmlType across the tree have wildcard arms (_ =>) and only accounts for wildcard disposition. In reality, crates/aprender-core/src/format/gguf/types.rs:361 (GgufTensor::byte_size) is an EXHAUSTIVE match over core's 12 variants with NO wildcard arm. Re-exporting trueno_quant::GgmlType (35 variants) causes rustc compilation error E0004 (non-exhaustive patterns). | +| v2 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:45` | The Section 2 leaf API code definition (lines 45-52) omits existing inherent methods (as_byte, as_str, from_str_lossy, block_bytes, block_size) and trait impl Display, causing existing callers across aprender-serve and aprender-compute to fail compilation if implemented from the code block as written. | +| v2 | 2 | Q1 | `crates/aprender-core/src/format/gguf/reader.rs:325` | Section 2 Q1-c claims 'core: format/gguf/reader.rs' is an id-to-enum boundary where u32 becomes GgmlType and prescribes an admission function admitted_from_id there. In reality, format/gguf/reader.rs and reader_parsing.rs store raw dtype: u32 in GgufTensorMeta and never convert u32 to GgmlType. Core has no id-to-enum boundary. | +| v2 | 2 | Q1 | `crates/aprender-serve/src/gguf/qwen3_moe_load.rs:107` | Section 2 Q1-f states '#3405\'s private QTYPE_LABELS (13 rows, crates/aprender-serve/src/gguf/qwen3_moe_load.rs:90) is replaced by TRAITS[id].name in this ticket'. In qwen3_moe_load.rs:107, qtype_label(qtype: u32) accepts arbitrary u32. Direct array indexing TRAITS[id] will panic with out-of-bounds on any qtype >= 43. | +| v3 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:45` | Section 2 code block claims to be the complete leaf API, but omits #[must_use] on methods returning values. Under aprender-quant's Cargo.toml lints (pedantic = warn), cargo clippy -p aprender-quant --lib -- -D warnings fails with clippy::must_use_candidate on every method. | +| v3 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:40` | Auxiliary types GgmlFamily, QuantTraits, and GgmlTypeError in Section 2 lack derive attributes. Without #[derive(Debug, Clone, Copy, PartialEq, Eq)], Phase 2 test assertions like assert_eq!(GgmlType::try_from_id(4), Err(GgmlTypeError::Removed { .. })) fail compilation, and GgmlTypeError lacks Display/Error trait implementations. | +| v3 | 2 | Q1 | `docs/audits/impl-PMAT-3430-plan.md:59` | checked_tensor_bytes is declared as pub const fn, but block_size(&self) and block_bytes(&self) are declared as non-const pub fn. In Rust, a const fn cannot call non-const methods, causing E0015 if checked_tensor_bytes delegates to them. | +| v3 | 2 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:69` | Phase 1 characterization tests cannot call compute's private from_u32 and pass unchanged in Phase 3, because in Phase 3 compute re-exports trueno_quant::GgmlType (which lacks from_u32) and replaces from_u32 with admitted_from_id, causing E0599 in Phase 3. | +| v3 | 2 | plan mechanics | `docs/audits/impl-PMAT-3430-plan.md:110` | Phase 1 scope_paths specifies crates/aprender-serve/src/gguf/tests/ and crates/aprender-compute/src/inference/, but compute's from_u32 in gguf.rs and serve's apr_qtype_to_dtype / apr_dtype_to_byte in dtype.rs are private (fn). External test files in those directories cannot call them without editing gguf.rs and dtype.rs to make them pub(crate), which is not explicit in Phase 1 scope. | +| v3 | 1 | Q1 | (summary; this lane's findings carried no fix) | First, fixing the Q4_1 byte_size calculation (18->20) within this refactoring violates the behavior-preserving invariant of M1 and must be landed as a separate PR first. | +| v3 | 1 | Q1 | (summary) | Second, moving the enum to `aprender-quant` without doc comments will fail the crate's existing `#![warn(missing_docs)]` lint, though it does pass `non_camel_case_types`. | +| v4 | 1 | plan mechanics | `crates/aprender-serve/src/apr/mod.rs:324` | serve's boundary functions (gguf/dtype.rs, infer/mod.rs:39, apr/mod.rs:324, apr/special_tokens.rs:210, apr/dequant.rs:235) keep their names and signatures | +| v4 | 1 | plan mechanics | `Makefile:1129` | Phase 1 scope_paths cover the creation and wiring of contracts/ggml-type-v1.yaml | +| v4 | 2 | plan mechanics | `crates/aprender-serve/src/apr/mod.rs:324` | apr/mod.rs:324 is a boundary function that keeps its name and signature and can be called directly by Phase 1 characterization tests | +| v4 | 2 | plan mechanics | `crates/aprender-serve/src/infer/mod.rs:38` | infer/mod.rs:39 is a boundary function that keeps its name and signature | +| v4 | 2 | plan mechanics | `contracts/census.json:5` | Adding contracts/ggml-type-v1.yaml requires regenerating contracts/census.json and synchronizing README.md, but both are omitted from scope_paths | +| v4 | 2 | plan mechanics | `README.md:44` | README.md contract count is checked for exact equality by scripts/readme_sync.sh --check during make contracts, which fails upon adding a new contract unless updated | +| v4 | 2 | plan mechanics | `ci/explicit-test-commands.d:1` | crates/aprender-quant/tests/ integration tests are not executed in CI unless an explicit command fragment is added under ci/explicit-test-commands.d/ | +| v4 | 2 | plan mechanics | `.github/workflows/ci.yml:938` | The plan asserts in Phase 5 and line 122 that the guard check_one_ggml_type_enum.sh must be wired into a CI selection line in .github, but no such selection line exists (ci.yml dispatches via scripts/guard_tree.sh --no-cargo) | + +Counts by tag: Q1 (enum + TRAITS design) 21 · plan mechanics 16 · Table E resolutions 2 · **home crate 0 · X1 ordering 0**. `aprender-quant` as home was called sound by every lane in every round; no lane contradicted X1; Table E drew one flip (`KvQuantType`, lane 1, v1 only) and one path-format note. + +### 3. Objections that repeated across rounds + +No objection came back in the same words — each round's specific was re-run, answered, and not raised again. But three objection **classes** survived two or more rounds, each time with a new instance the previous revision had not found: + +| class | tag | rounds | instances | +|---|---|---|---| +| **A. The §2 leaf API does not match the surface existing call sites use** | Q1 | v1, v2, v3 | v1: `from_id → Result` and `tensor_bytes → Option` break callers · v2: block omits `as_byte as_str from_str_lossy block_bytes block_size Display` · v3: aux types lack derives, `const fn` calls non-const, `must_use`/`missing_docs` under the crate's lint table | +| **B. The list of id→enum / name→enum admission boundaries is wrong** | Q1 | v1, v2, v4 | v1: gated the GGUF parser only, `apr/mod.rs:324` + 5 more bypass it · v2: listed core `reader.rs`, which keeps `u32` raw — core has no boundary · v4: `apr/mod.rs:324` is an inline match inside `TensorEntry::from_binary`, not a callable "boundary function that keeps its name" | +| **C. scope_paths / acceptance commands miss files or cannot fail** | plan mechanics | v1, v3, v4 | v1: Phase 3/4 scopes miss core `format/` siblings, serve `apr/` `infer/`; `gguf::types` filter exercises 0 sites · v3: Phase 1 scope omits serve `gguf/dtype.rs`; tests on private `from_u32` break in Phase 3 · v4: `contracts/census.json`, `README.md:44` (1791→1792), `ci/explicit-test-commands.d/`, Makefile `CONTRACTS` are outside every phase's scope; "CI selection line" does not exist (guards auto-dispatch via `guard_tree.sh --no-cargo`) | + +Read against the operator's rule: **A and B are tagged Q1 and each survived three rounds** — by that rule they are design findings, not wording. What they have in common: the plan keeps trying to make M1 *source-compatible and behaviour-preserving at every existing call site* while replacing three enums with one 35-variant enum, and each round finds one more site where that promise needs another special case (an alias, an `Option`-returning twin, a surviving free function, a per-crate `ADMITTED` set, an inline match that is not a function). The lanes never disputed the target (one enum, `TRAITS[43]`, in `aprender-quant`); they dispute that the compatibility shim can be specified completely in advance. C is the same thing seen from the phase table. + +Found on the orchestrator's re-runs, not by any lane — both stand regardless of the ruling: +- core `GgufTensor::byte_size` (`crates/aprender-core/src/format/gguf/types.rs:365`) sizes `Q4_1` at 18 bytes/block; upstream is 20. Latent export-size defect, to be filed and fixed on its own. +- Q8_1: ggml C `static_assert` = 36, gguf-py `GGML_QUANT_SIZES` = 40 at llama.cpp `3173a56471c1753650cd806694145ffd6dcace67`. The ticket's falsifier ("equals gguf-py for every shared id") cannot be met as worded. + +Decision requested: rule on A/B — e.g. accept a compatibility shim specified by characterization tests rather than by enumeration, or drop source-compatibility from M1 and let the compiler enumerate the sites. Andon stands: 3/3 by 2026-09-18T12:00Z or the ticket is pulled. No code before 3/3. +