Files
esh-pfi-infrastructure/services/meromero-quant/perlayer_test.py
T
vh 1a5bc2ddf1 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.
2026-09-10 10:56:16 -07:00

101 lines
3.9 KiB
Python

"""Attempt-4 blocker: is dropping `per_layer_config` the correct fix, or must we
force `allow_global_per_layer_attribute_access`?
The two candidates are NOT equivalent:
* DROP -> config becomes homogeneous in transformers' eyes and the global
`global_head_dim` / `num_global_key_value_heads` fields describe the
full-attention layers, exactly as zerofata's canonical config does.
* FORCE -> config stays heterogeneous; `config.head_dim` starts answering 256 to
every caller, including the ones building the 512-wide full-attention
layers. That is the hazard transformers' own warning names.
So this is not a "did the traceback go away" test. It builds the model on the meta
device from each candidate and reads the ACTUAL k_proj widths back, against the
checkpoint's measured 2048 (full) / 4096 (sliding). A candidate that constructs but
mis-shapes a layer is a worse outcome than the crash, because it would ship.
Positive control: zerofata's canonical config, which we already quantized
successfully on 2026-08-21, MUST pass every check here. If it doesn't, the
instrument is broken and none of the negatives mean anything.
"""
import json, shutil, tempfile, traceback
from pathlib import Path
import torch
import transformers
from transformers import AutoConfig
print(f"transformers {transformers.__version__} torch {torch.__version__}", flush=True)
HERETIC = Path("/tank/aimodels/G4-MeroMero-v2-31B-heretic-bf16")
CANON = Path("/tank/aimodels/meromero-v2-nvfp4-work/src")
# Measured off the checkpoints by shape_verify.py; both trees agree.
EXPECT = {"full_attention": 2048, "sliding_attention": 4096}
def variant(name, cfg_dict):
d = Path(tempfile.mkdtemp(prefix=f"cfg-{name}-"))
(d / "config.json").write_text(json.dumps(cfg_dict))
return name, d
heretic = json.loads((HERETIC / "config.json").read_text())
canon = json.loads((CANON / "config.json").read_text())
dropped = json.loads(json.dumps(heretic))
dropped["text_config"].pop("per_layer_config")
forced = json.loads(json.dumps(heretic))
forced["text_config"]["allow_global_per_layer_attribute_access"] = True
variants = [
variant("A-canonical-POSITIVE-CONTROL", canon),
variant("B-heretic-asis", heretic),
variant("C-heretic-drop-per_layer_config", dropped),
variant("D-heretic-force-global-access", forced),
]
for name, d in variants:
print(f"\n=== {name} ===", flush=True)
try:
cfg = AutoConfig.from_pretrained(d)
except Exception as e:
print(f" CONFIG FAILED: {type(e).__name__}: {str(e)[:160]}")
continue
t = cfg.text_config
fields = {}
for k in ("head_dim", "num_key_value_heads", "global_head_dim",
"num_global_key_value_heads"):
try:
fields[k] = getattr(t, k, "<absent>")
except Exception as e:
fields[k] = f"<{type(e).__name__}>"
print(f" config OK: {fields}")
try:
from transformers import Gemma4ForConditionalGeneration as M
with torch.device("meta"):
model = M(cfg)
except Exception:
print(" MODEL BUILD FAILED:")
print(" " + traceback.format_exc().strip().replace("\n", "\n ")[-1200:])
continue
layer_types = t.layer_types
probes = [next(i for i, x in enumerate(layer_types) if x == "full_attention"),
next(i for i, x in enumerate(layer_types) if x == "sliding_attention")]
layers = model.model.language_model.layers
verdict = []
for li in probes:
got = tuple(layers[li].self_attn.k_proj.weight.shape)
want = EXPECT[layer_types[li]]
ok = got[0] == want
verdict.append(ok)
print(f" L{li:>2} {layer_types[li]:<18} k_proj {got} "
f"want out={want} {'OK' if ok else '*** MISMATCH ***'}")
print(f" => {'GEOMETRY MATCHES CHECKPOINT' if all(verdict) else 'GEOMETRY WRONG'}")
for _, d in variants:
shutil.rmtree(d, ignore_errors=True)