Operator: "replace that a4b moe over pfish-6 -- remove the pfish-6 alias and create an alias for char-rp-fast." G4-MeroMero-26B-A4B-it-uncensored-heretic-NVFP4A16 is live on ana-ml2 :8021 under its own served name, behind the new gateway alias char-rp-fast. Pfish-6 is gone from the gateway and now returns an explicit 400 rather than a substitution; 0 of 17 LiteLLM keys scoped it, so nothing was orphaned. The compose project name stays erp-seat because asset-engine derives seat liveness from it. The first quant of that A4B served NaN and passed its healthcheck doing it. It was built with the dense v2-31B recipe, whose ignore list has no router regex, so all 30 MoE routers were quantized to 4 bits -- and a 4-bit router changes which experts run rather than degrading them. Quant rc=0, healthcheck green, correct KV pool, correct served name, and every completion returned finish_reason=length with the full token count and content: null. The model was emitting a full budget of tokens that decoded to the empty string. Raw /v1/completions was empty too, ruling out the chat template and the reasoning parser. The signal that named it was logprobs: vLLM refused to serialize the response, "Out of range float values are not JSON compliant: nan". The lesson is about the control rather than the router. That tree had already been structurally diffed and passed -- against a verified-good DENSE quant of the same Gemma-4 family. A dense model has no routers, so the single thing that was wrong was the single thing the control could not distinguish. Diffing instead against Pfish-6, a known-good quant of the same 26B-A4B MoE, gave it in one line: 222 ignore entries against 252, the 30 missing being layers.N.router.proj. A positive control is only worth what it can distinguish, and "same family" is not "same architecture class". Re-quantized with the MoE recipe, whose dry-run asserts 11,520 expert Linears and refuses a router in the quantize set before any GPU time. The live seat then passed prose with no channel-prefix leak, a solid-colour image read correctly, an auto tool call parsed, finite logprobs, and KV 534,649 tokens / 2.04x carried over from Pfish-6 unchanged. The broken tree is parked on ana-ml2 as ...-NVFP4A16.BROKEN-routers-quantized-20260910. Section 4.4's temp port was not reachable: 15.9 GiB of weights plus KV plus multimodal encoder-cache profiling does not fit in the ~19 GiB free beside GPU1's six other tenants -- 0.20 utilization refused admission, 0.185 OOM'd in encoder profiling. The substitute was reversibility and ordering: named .env backup, prove the seat on its real port while no alias points at it, move the alias last. That is why a NaN-serving seat never reached a consumer. The seat was down about 16 minutes across two attempts; no consumer saw a broken alias. Playbook gains the router-quant failure signature and the control-class rule in 3.15, and a logprobs check in 4.4. seat_verify.py carries that check as check 6. Quality is NOT established: no RP eval, no long-context check, no A/B against Pfish-6 or char-rp. Samplers are the author's card values, untuned here.
10 KiB
[2026-09-10] MeroMero — both quants landed; the v2 dense took five attempts
Operator wanted a MeroMero seat. Getting there cost five quant attempts and corrected three wrong hypotheses (two of the previous session's, one of mine), so the failure chain is the durable part. Both outputs now exist and are verified against a known-good tree. Neither is serving yet.
The family, because I got it wrong first
I called the unsuffixed 31B "v1" because v2 is v2-31B. The operator caught it. Creation dates from
the upstream author settle it:
2026-04-15 zerofata/G4-MeroMero-26B-A4B 30 layers, kv 8 MoE 128 experts ← the ORIGINAL
2026-04-30 zerofata/G4-MeroMero-31B 60 layers, kv 16 DENSE ← a separate line
2026-07-31 zerofata/G4-MeroMero-v2-31B 60 layers, kv 16 DENSE
Not a v1→v2 sequence at one size: an A4B MoE first, then a dense 31B line that later got a v2.
⚠ The -v1- repo 401s, and I read that as "v1 must be the unsuffixed one" instead of checking dates.
The geometry is why it matters. The A4B is 30 layers / kv 8 — identical to Pfish-6 (a gemma4 26B-A4B) — so it drops into the existing KV budget at 262k. The dense 31B is 60 layers / kv 16, ~4x the KV per token, and did NOT fit 262k on GPU1 alongside the other seats.
Published quants: none of them are our scheme
pekkAi/G4-MeroMero-31B-NVFP4 true v1 dense, W4A4
llmfan46/G4-MeroMero-31B-uncensored-heretic-NVFP4 abliterated dense, W4A16_NVFP4 (pulled, 19 GB)
pekkAi/G4-MeroMero-26B-A4B-it-...-heretic-NVFP4 abliterated A4B, W4A4 + FP8 KV baked in
NVFP4 of v2-31B DOES NOT EXIST, from anyone (0 of 27 v2 repos)
W4A4 carries our own measured long-context collapse (Granite, 30k ctx), which is why every seat we run is A16. So the operator ruled: pull both ablits bf16 and quantize in-house at W4A16.
MTP — checked on the SOURCES, not inferred
Gemma-4 ships no MTP head at all: 0 mtp tensors and no mtp config keys in the A4B bf16, the v2 bf16, Pfish-6, or the published quants. The "mtp if applicable" clause is a no-op here; the MTP work in our history is Qwen-family.
A4B — SUCCEEDED
G4-MeroMero-26B-A4B-it-uncensored-heretic-NVFP4A16, 16 G, 7 files, 2m08s. That time confirms
playbook §3.16: weight-only A16 runs a DataFreePipeline and never touches the dataset.
⚠ Output is missing preprocessor_config.json — §4.3 post-step 2 still owed before serving.
v2 dense — FOUR failures, each a different layer
num_key_value_headsis None atGemma4TextAttention.__init__. I hypothesised a transformers version deadlock (config authored by 5.16.1, container on 5.12.1) — wrong: all three configs construct fine on 5.12.1. Then I hypothesised the staletorch_dtypekwarg reshaping the config — also wrong: every kwarg combination returns 16. ⚠ A 2-layer truncation PASSED and hid it. The failing local is chosen per layer type; onlyfull_attentionlayers take the branch. Testing each layer type individually found it in seconds. Real cause: DogOnKeyboard's config setsattention_k_eq_v: truebut omitsnum_global_key_value_headsandglobal_head_dim— a malformed upload, not a toolchain issue. Patched from zerofata's canonical values (4 / 512) after shape-verifying the checkpoint: full-attn k_proj[2048,5376]= 4×512, sliding[4096,5376]= 16×256, identical to canonical. Original atconfig.json.bak-pre-kvfields-20260910.initialize model processor ... required when a dataset is provided— DogOnKeyboard ships noprocessor_config.json(the A4B does; that is why it succeeded). Copied from canonical, AND dropped the dataset entirely, since §3.16 says A16 ignores it and §3.14 says building it bakes a truncation cap into the shipped tokenizer. Removing it kills both for zero loss.- My own bug: the reference module runs argparse with
required=Trueat IMPORT, so blankingsys.argvstill exited 2. Placeholder args, real argv restored after. AmbiguousGlobalPerLayerAttributeError: 'head_dim' is a per-layer attribute— RESOLVED, and not what it looked like. See "The attempt-4 trap" below.- Clean.
rc=0, 19 G, 3m07s.
⚠ My wrapper reported rc=0 on a failed run because it read $? after an echo. A wrapper that
reports success on failure is the false-reassurance class; fixed to capture $? immediately.
The attempt-4 trap — the toolchain moved, the config was fine
The standing hypothesis was that DogOnKeyboard's per_layer_config key was the defect and
allow_global_per_layer_attribute_access=True was the fix. Both halves were wrong, and the second
half would have shipped a risk for no reason.
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 out of global_head_dim +
layer_types; 5.14.1 has the heterogeneity guard but not the gemma4 resolver, so it refuses the
global read. Under the image's own 5.16.1 the identical config loads fine. On top of that,
vllm/vllm-openai:latest was re-pulled during attempt 4 and in no earlier run — the pull line is
in that block alone — so the error changed for reasons unrelated to anything under test.
Two things made this findable, and neither was inspection:
- Reproducing the real call. A bare
AutoConfig.from_pretraineddoes not reproduce it; the trigger is reached throughAutoTokenizer. Testing the config alone would have cleared it. - A known-good positive control. zerofata's canonical v2, quantized cleanly on 2026-08-21, is what separated "this upload is malformed" from "this toolchain moved". Four green variants and one red one are noise without it.
Fix: drop per_layer_config, don't force global access. It is exactly redundant — keys are
precisely the ten full_attention layer indices, sole value (512, 4), verbatim the global fields.
Forcing instead leaves the config heterogeneous and makes config.head_dim answer 256 to every
caller including the ones building the 512-wide layers; geometry survived it in a meta-device check,
but llmcompressor's onloading is an unaudited caller and that is what the warning is about. The
patch re-proves the redundancy at apply time and refuses if it ever stops holding.
What landed, and what is verified
G4-MeroMero-v2-31B-heretic-NVFP4A16— 19 G, and its tensor table is identical family for family and count for count to the 2026-08-21 canonical quant: 410 U8 packed + 410 F8_E4M3 + 410 F32 scales on the LM Linears, 356 BF16 vision-tower tensors preserved,input_activations=None(genuinely A16). Shard sizes match that tree byte for byte.- CPU load-and-generate: 0 tensors left on meta, decompresses, emits coherent prose. n=1, greedy, 24 tokens — an "is it wired up" check and nothing more.
- ⚠ The A4B output had the §3.14 truncation cap baked in (
max_length: 8192), because it was quantized with the corpus. Caught and fixed; backup attokenizer.json.bak-pre-truncfix. The v2, run data-free, came outtruncation: null.
Still owed
The §4.4 serve test has NOT run. GPU1 has 19.9 GB free against 19.5 GB of weights, so it cannot happen without displacing a live seat — operator's call. Until it does, "vllm servable" is unverified for this tree, and the dense 31B's 60-layer / kv-16 geometry still does not fit 262k on GPU1 beside the current tenants regardless.
Instruments and the full write-up: services/meromero-quant/. General lessons:
docs/pfi/model-quantization-playbook.md §3.16, §3.17 (new), §4.3.
The A4B reached a live seat while broken — and looked healthy doing it
Operator, later the same day: "replace that a4b moe over pfish-6 — remove the pfish-6 alias and
create an alias for char-rp-fast." The A4B went onto the erp-seat seat (ana-ml2 :8021) and
served NaN.
Cause: the morning's batch used the dense recipe for a MoE model. Its IGNORE list has no
re:.*router.*, so all 30 MoE routers were quantized to NVFP4, and a 4-bit router does not degrade
expert selection — it changes which experts run.
Nothing in the normal startup path showed it. Quant rc=0. Healthcheck green in 210 s. Engine
log reported the correct KV pool. /v1/models correct. Every completion came back
finish_reason=length with the full token count and content: null — the model was generating
a full budget of tokens that decoded to the empty string. Raw /v1/completions was empty too, which
ruled out the chat template and the reasoning parser. The one signal that named it: logprobs: 1
→ HTTP 400 Out of range float values are not JSON compliant: nan.
⚠⚠ The durable lesson is about the CONTROL, not the router. That broken tree HAD been
structurally diffed before it shipped — and passed — against a verified-good dense 31B quant of
the same Gemma-4 family. A dense model has no routers, so the single thing that was wrong was the
single thing that control could not distinguish. A positive control is only worth what it can
distinguish; "same family" is not "same architecture class." Diffing instead against Pfish-6
— a known-good NVFP4A16 quant of the same 26B-A4B MoE — gave the answer in one line: 222 ignore
entries against 252, the 30 missing being exactly model.language_model.layers.N.router.proj.
Re-quantized with services/erp-seat-quant/quant_nvfp4a16_gemma4_moe.py, whose --dry-run asserts
11,520 expert Linears and refuses a router in the quantize set, both before any GPU time. 90 s.
Live seat then passed prose / vision / tool-call / logprobs. Broken tree parked as
...-NVFP4A16.BROKEN-routers-quantized-20260910.
§4.4's temp port was not reachable, and the fallback mattered. 15.9 GiB of weights + KV +
multimodal encoder-cache profiling does not fit in ~19 GiB free beside GPU1's six other tenants:
gpu-memory-utilization 0.20 refused admission (18.26 free vs 18.99 wanted) and 0.185 OOM'd in
encoder profiling. Substitute: named .env backup, prove the seat on its real port while no
gateway alias points at it, move the alias last. That ordering is the only reason a NaN-serving
seat never reached a consumer. Cost: ~16 min of seat downtime, twice; zero broken aliases.
Runbook: services/erp-seat-quant/RUNBOOK-char-rp-fast-swap.md. Playbook §3.15 (failure signature +
the control-class rule), §4.4 (ask for logprobs once).
Related: 2026-09-10-r49-babybronte-d1-d3-and-the-1-epoch-pilot