Land the MeroMero v2-31B NVFP4A16 quant and record the pinned-transformers trap

The v2 dense quant had failed four times. Attempt 5 lands it at 19 G.

The blocker was not what it looked like. `AmbiguousGlobalPerLayerAttributeError`
on `head_dim` read as a malformed upload -- DogOnKeyboard's config carries a
`per_layer_config` key zerofata's canonical one lacks -- and the standing fix was
to force `allow_global_per_layer_attribute_access=True`. Both halves were wrong.

`pip install llmcompressor==0.13.0` downgrades transformers 5.16.1 -> 5.14.1. The
config was serialized by 5.16.1, which materializes `per_layer_config` from
`global_head_dim` + `layer_types`; 5.14.1 carries the heterogeneity guard but not
the gemma4 resolver. Under the image's own transformers the same config loads
fine. `:latest` was also re-pulled during attempt 4 and no earlier run, so the
toolchain moved mid-diagnosis. Two things separated "malformed upload" from
"moved toolchain": reproducing the real failing call (a bare AutoConfig load does
not reproduce it; the trigger is reached through AutoTokenizer) and keeping
zerofata's canonical tree, quantized cleanly on 2026-08-21, as a positive control.

The fix drops `per_layer_config` rather than forcing global access. It is exactly
redundant -- keys are precisely the ten full_attention layer indices, sole value
(512, 4), verbatim the global fields -- and forcing instead would make
`config.head_dim` answer 256 to the callers building the 512-wide layers.
patch_perlayer.py re-proves that redundancy at apply time and refuses if it ever
stops holding.

Verified on the tensor table rather than the exit code: the output is identical
family-for-family and count-for-count to the August canonical quant, with 356
BF16 vision-tower tensors preserved and input_activations=None. A GPU-free load
leaves 0 tensors on meta and generates coherent prose. The section 4.4 serve test
has NOT run -- GPU1 has 19.9 GB free against 19.5 GB of weights, so it needs a
live seat displaced.

Also fixes the A4B output, which had a truncation cap baked into its tokenizer
(max_length 8192) from being quantized with the calibration corpus.

Playbook gains section 3.17 for the pinned-transformers class and sharpens 3.16
to say drop the dataset outright for any A16 scheme.
This commit is contained in:
vh
2026-09-10 10:56:16 -07:00
parent b8dbe71a1c
commit 1a5bc2ddf1
19 changed files with 1393 additions and 17 deletions
+138
View File
@@ -0,0 +1,138 @@
# MeroMero NVFP4A16 quants (Gemma-4) — instruments and the failure chain
Two in-house W4A16 quants of the abliterated MeroMero models, 2026-09-10. The
operator's brief was three clauses long: *"w4a16 vllm servable, vision towers
intact, mtp if applicable."* Every published quant of these models is W4A4 (our own
measured long-context collapse) or, for v2, does not exist at all — 0 of 27 v2 repos.
| output | source | result |
|---|---|---|
| `G4-MeroMero-26B-A4B-it-uncensored-heretic-NVFP4A16` | `DogOnKeyboard` A4B ablit | **16 G**, first try, 2m08s |
| `G4-MeroMero-v2-31B-heretic-NVFP4A16` | `DogOnKeyboard` v2-31B ablit | **19 G**, attempt **5** |
Both live on ana-ml2 under `/tank/aimodels/`. Neither is serving yet — see *Owed*.
## What the v2 dense cost, and why each layer mattered
Five attempts, five different causes. The order matters because each one masked the
next.
1. **`num_key_value_heads` is None** at `Gemma4TextAttention.__init__`. The uploader
set `attention_k_eq_v: true` but omitted `num_global_key_value_heads` and
`global_head_dim` — a malformed upload, not a toolchain problem. Patched from
zerofata's canonical values (4 / 512) after shape-verifying the checkpoint
(`shape_verify.py`): full-attn `k_proj [2048,5376]` = 4×512, sliding
`[4096,5376]` = 16×256, identical to canonical.
⚠ **A 2-layer truncation test PASSED and hid this.** The failing branch is chosen
per layer type and only `full_attention` layers take it. Testing each layer type
individually found it in seconds.
2. **`initialize model processor ... required when a dataset is provided`.** This
upload ships no `processor_config.json`. Rather than supply one, the dataset was
dropped entirely — NVFP4A16 is weight-only and runs a `DataFreePipeline`, so the
corpus was never read anyway (playbook §3.16), and passing one also bakes a
truncation cap into the shipped tokenizer (§3.14). Removing it kills both for
zero loss. `quant_a16_datafree.py`.
3. **Our own bug**: the reference module argparses at import with `required=True`,
so blanking `sys.argv` still exited 2. Placeholder argv, real one restored after.
4. **`AmbiguousGlobalPerLayerAttributeError: 'head_dim' is a per-layer attribute`.**
See below — this one was not what it looked like.
5. Clean. `rc=0`, 19 G, 3m07s.
## The attempt-4 trap: the transformers you measured is not the one that ran
The obvious reading was "DogOnKeyboard's config carries a `per_layer_config` key
zerofata's lacks, so that key is the defect." Two measurements said otherwise.
`tok_repro.py` reproduces the **actual failing call** rather than a paraphrase of it
— a bare `AutoConfig.from_pretrained` does not reproduce it, and testing that
instead would have sent us patching a file that was never the problem — with
zerofata's canonical tree, which quantized cleanly on 2026-08-21, as the positive
control. Against the container's shipped transformers **5.16.1**, every variant
passes, the unmodified heretic config included. Run the same script *after*
`pip install llmcompressor==0.13.0` and transformers is **5.14.1**: canonical
passes, heretic fails. **llmcompressor pins transformers and silently downgrades
it**, so the version printed before the install is not the version that runs.
Compounding it, `vllm/vllm-openai:latest` was re-pulled during attempt 4 and not
before it — the pull line appears in that run's log block and in no earlier one — so
the toolchain moved mid-campaign (the previous session recorded 5.12.1 in-container
while diagnosing attempt 1). That is why attempt 4's error read as a *new config
problem* and was not one. **`run_v2_quant.sh` now pins the image by digest.**
`per_layer_config` was in fact a 5.16.1 serialization artifact, and an exactly
redundant one: its keys are precisely the ten `full_attention` layer indices
[5,11,…,59] and its sole distinct value is `(head_dim 512, num_key_value_heads 4)` —
verbatim what `global_head_dim: 512` / `num_global_key_value_heads: 4` already say.
`patch_perlayer.py` re-proves that redundancy at patch time and refuses to drop the
key if it ever stops holding.
**Why drop it rather than set `allow_global_per_layer_attribute_access=True`.** The
forcing flag leaves the config heterogeneous and makes `config.head_dim` answer 256
to every caller — including the ones building the 512-wide full-attention layers.
`perlayer_test.py` builds all four variants on the meta device and reads the k_proj
widths back: geometry survived the flag, so it was not wrong, but llmcompressor's
own onloading is a caller nobody here has audited and the flag's warning names
exactly that hazard. The lossless option verified identically, so there was no
reason to take the risk.
## Verification — the tensor table, not the exit code
`rc=0` and a plausible file size prove neither of the operator's two checkable
requirements. `verify_quant.py` parses the safetensors headers directly (no torch,
no GPU, no 20 GB load) and reports dtypes by module family. The new quant is
**identical, family for family and count for count, to the 2026-08-21 known-good
canonical quant** — 410 U8 packed + 410 F8_E4M3 scales + 410 F32 global scales on the
LM Linears, **356 BF16 vision-tower tensors preserved**, `input_activations=None`
(genuinely A16, not A4). Shard sizes match that tree byte for byte. Full transcript
in `raw/verification.txt`.
MTP is N/A and was checked on the sources, not assumed: Gemma-4 ships no MTP head at
all — 0 mtp tensors and no mtp config keys in either bf16 source or in any published
quant. The "mtp if applicable" clause is a no-op for this family.
`cpu_smoke.py` then loads the finished tree with no GPU at all, confirms **0 tensors
left on the meta device** (a hole `from_pretrained` will not always raise on),
decompresses, and generates:
> *"A lighthouse is a tower with a bright light used to guide ships at sea and warn
> them of dangerous coastlines."*
24 greedy tokens, 3.4 s/tok on CPU. That is an "is it wired up" check and is offered
as nothing more — n=1 says nothing about quality, and it says nothing about whether
vLLM's sm_120 NVFP4 kernels serve the thing.
## Owed
- **§4.4 serve test on a temp port.** Not run. GPU1 has 19.9 GB free against 19.5 GB
of weights, so it cannot happen without displacing a live seat, which is the
operator's call. Until it runs, *"vllm servable"* is unverified for this tree.
- The A4B output **had the §3.14 truncation cap baked in** (`max_length: 8192`) — it
was quantized *with* the calibration corpus, before the data-free path existed.
Fixed 2026-09-10 by `post_quant_gemma4.py`; backup at
`tokenizer.json.bak-pre-truncfix`. Both trees now pass `--check` clean, as does the
August tree the checker is calibrated against.
## Files
| file | what it does |
|---|---|
| `run_v2_quant.sh` | attempt-5 runner, image pinned by digest |
| `run_quant_batch.sh` | the original two-model batch (A4B succeeded here, v2 did not) |
| `quant_a16_datafree.py` | NVFP4A16 driver with no dataset, wrapping the reference recipe |
| `patch_perlayer.py` | drops `per_layer_config`, re-proving its redundancy first |
| `shape_verify.py` | do the checkpoint's k/v shapes agree with the patched config? |
| `perlayer_test.py` | meta-device geometry across all four config variants |
| `tok_repro.py` | reproduces the real failing call; canonical tree as positive control |
| `post_quant_gemma4.py` | playbook §4.3 post-steps, idempotent, `--check` mode |
| `verify_quant.py` | dtypes by module family, straight from safetensors headers |
| `cpu_smoke.py` | GPU-free load + generate |
| `raw/` | run logs and the verification transcript, so the claims can be re-derived |
`raw/` holds `batch-quant-run-2026-09-10.txt` (attempt 1, and the A4B success),
`v2-quant-run-2026-09-10.txt` (attempts 2-5), `cpu-smoke-2026-09-10.txt`, and
`verification.txt`. Progress-bar redraws are collapsed to one line per bar, final
state; nothing else is edited. (`.txt` rather than `.log` because the repo ignores
`*.log` — same convention as `scripts/training-probes/`.)
General lessons live in `docs/pfi/model-quantization-playbook.md` (§3.4, §3.14,
§3.16, §3.17, §4.3) — read that first, and where it disagrees with this file, it wins.