TL;DR
The test-model generators (make_glm_oracle.py, make_glm_bench_model.py, glm_fp8_emit.py) now:
- Emit weights as FP8 e4m3 + 128×128 block scale_inv in the exact layout of the real GLM-5.2-FP8 checkpoint, so the converter's FP8→int4 dequant path can be exercised locally without the 379 GB download.
- Unfuse HF's 3-D
gate_up_proj expert weights into the per-expert 2-D layout (gate_proj/up_proj/down_proj) that the real checkpoint, the converter, and the C engine all expect.
Two bugs were found and fixed in the process.
Branch: windows-dev (commits 4788685, 91b1035, d9b6a2d)
Why
The converter (convert_fp8_to_int4.py) has a dequant path for FP8 e4m3 + 128×128 block scales. Before this change, the only way to exercise that path was to download the 379 GB GLM-5.2-FP8 checkpoint. There was no local fixture that could validate the FP8→int4 pipeline, meaning bugs in the dequant path would only surface during the multi-hour production conversion.
The FP8 emission gap
glm_fp8_emit.py is a new shared helper that:
- Block-quantizes 2-D tensors to FP8 e4m3 with 128×128 block scales (FBGEMM/TransformerEngine convention:
scale = amax(block)/448, stored as a multiplier)
- Keeps norms, router, 1-D biases, and 3-D tensors at f32 (matching the converter's
classify() + ndim != 2 guard)
- Provides
fp8_block_dequantize (exact inverse) for the oracle's reference round-trip
Both generators gained a --fp8 flag. The oracle additionally round-trips weights through FP8 before computing ref_glm.json, so the reference reflects exactly the FP8 model the converter ingests.
Bug 1: Fused expert weights (found during validation)
HF's GlmMoeDsaForCausalLM fuses gate_proj + up_proj into a single 3-D gate_up_proj tensor [E, 2*M, I] for compute efficiency. But the real GLM-5.2-FP8 checkpoint stores experts unfused as per-expert 2-D tensors:
model.layers.10.mlp.experts.0.gate_proj.weight (2048, 6144) FP8 e4m3
model.layers.10.mlp.experts.0.gate_proj.weight_scale_inv (16, 48) F32
model.layers.10.mlp.experts.0.up_proj.weight (2048, 6144) FP8 e4m3
model.layers.10.mlp.experts.0.up_proj.weight_scale_inv (16, 48) F32
model.layers.10.mlp.experts.0.down_proj.weight (6144, 2048) FP8 e4m3
model.layers.10.mlp.experts.0.down_proj.weight_scale_inv (48, 16) F32
The converter's classify() matches ".mlp.experts." in name, but its ndim != 2 guard silently skipped the 3-D fused tensors. The C engine then crashed with missing tensor: model.layers.3.mlp.experts.5.gate_proj.weight because it expects per-expert 2-D tensors.
Fix: unfuse_experts() in glm_fp8_emit.py splits gate_up_proj [E, 2M, I] → per-expert gate_proj [M, I] + up_proj [M, I], and down_proj [E, I, M] → per-expert down_proj [M_out, I]. Called after reference generation (the model needs fused weights for forward()/generate()) but before saving, in both generators, for both FP8 and bf16 paths.
Bug 2: Oracle FP8 round-trip guard
make_glm_oracle.py's FP8 round-trip loop used p.dim() < 2 to skip non-quantizable tensors. But 3-D fused expert tensors have dim() == 3, which passed the guard and crashed fp8_block_quantize (which expects exactly 2-D). Fixed to p.dim() != 2, matching the converter's guard.
What changed
| File |
Change |
c/tools/glm_fp8_emit.py |
New: fp8_block_quantize, fp8_block_dequantize, keep_f32, state_dict_to_fp8, save_fp8_safetensors, unfuse_experts |
c/tools/make_glm_oracle.py |
--fp8 flag; round-trips weights through FP8 before computing reference; unfuses experts before saving; fixed dim guard |
c/tools/make_glm_bench_model.py |
--fp8 flag; unfuses experts before saving; writes config.json manually in FP8 path (bypasses save_pretrained) |
Impact on the program
- Enables local testing of the full conversion chain: generator
--fp8 → converter --indir → engine load, all without a network download.
- Fixes a silent data-loss bug: without unfusing, the converter would skip all expert weights and the engine would crash. This would have manifested as a broken production conversion once the FP8 download completed.
- Backward compatible: without
--fp8, both generators behave exactly as before (bf16 output, save_pretrained path).
- Validated end-to-end: 570 FP8 tensors + 629 scale_inv sidecars → converter produces per-row int4 (fmt=2) and grouped int4 (fmt=4) → engine loads both clean, fmt=4 auto-detected.
Verification
generator --fp8 → 570 e4m3 tensors + 629 scale_inv (was 90 when fused)
converter --group-size 0 → per-row int4 (fmt=2), engine loads clean, 0 missing-tensor errors
converter --group-size 128 → grouped int4 (fmt=4), 8-16x more scales per tensor, engine loads clean
dequant error: grouped 1.14-1.22x lower than per-row vs FP8 source
TL;DR
The test-model generators (
make_glm_oracle.py,make_glm_bench_model.py,glm_fp8_emit.py) now:gate_up_projexpert weights into the per-expert 2-D layout (gate_proj/up_proj/down_proj) that the real checkpoint, the converter, and the C engine all expect.Two bugs were found and fixed in the process.
Branch:
windows-dev(commits4788685,91b1035,d9b6a2d)Why
The converter (
convert_fp8_to_int4.py) has a dequant path for FP8 e4m3 + 128×128 block scales. Before this change, the only way to exercise that path was to download the 379 GB GLM-5.2-FP8 checkpoint. There was no local fixture that could validate the FP8→int4 pipeline, meaning bugs in the dequant path would only surface during the multi-hour production conversion.The FP8 emission gap
glm_fp8_emit.pyis a new shared helper that:scale = amax(block)/448, stored as a multiplier)classify()+ndim != 2guard)fp8_block_dequantize(exact inverse) for the oracle's reference round-tripBoth generators gained a
--fp8flag. The oracle additionally round-trips weights through FP8 before computingref_glm.json, so the reference reflects exactly the FP8 model the converter ingests.Bug 1: Fused expert weights (found during validation)
HF's
GlmMoeDsaForCausalLMfusesgate_proj+up_projinto a single 3-Dgate_up_projtensor[E, 2*M, I]for compute efficiency. But the real GLM-5.2-FP8 checkpoint stores experts unfused as per-expert 2-D tensors:The converter's
classify()matches".mlp.experts." in name, but itsndim != 2guard silently skipped the 3-D fused tensors. The C engine then crashed withmissing tensor: model.layers.3.mlp.experts.5.gate_proj.weightbecause it expects per-expert 2-D tensors.Fix:
unfuse_experts()inglm_fp8_emit.pysplitsgate_up_proj [E, 2M, I]→ per-expertgate_proj [M, I]+up_proj [M, I], anddown_proj [E, I, M]→ per-expertdown_proj [M_out, I]. Called after reference generation (the model needs fused weights forforward()/generate()) but before saving, in both generators, for both FP8 and bf16 paths.Bug 2: Oracle FP8 round-trip guard
make_glm_oracle.py's FP8 round-trip loop usedp.dim() < 2to skip non-quantizable tensors. But 3-D fused expert tensors havedim() == 3, which passed the guard and crashedfp8_block_quantize(which expects exactly 2-D). Fixed top.dim() != 2, matching the converter's guard.What changed
c/tools/glm_fp8_emit.pyfp8_block_quantize,fp8_block_dequantize,keep_f32,state_dict_to_fp8,save_fp8_safetensors,unfuse_expertsc/tools/make_glm_oracle.py--fp8flag; round-trips weights through FP8 before computing reference; unfuses experts before saving; fixed dim guardc/tools/make_glm_bench_model.py--fp8flag; unfuses experts before saving; writes config.json manually in FP8 path (bypassessave_pretrained)Impact on the program
--fp8→ converter--indir→ engine load, all without a network download.--fp8, both generators behave exactly as before (bf16 output,save_pretrainedpath).Verification