int3-g64 (fmt=5): per-group-scale 3-bit weights, the size/quality point your own ablation (#132) says the next container should sit at#168
Conversation
rajpratham1
left a comment
There was a problem hiding this comment.
This is a substantial feature that introduces a new int3-g64 quantization format across the quantization pipeline, runtime kernels, loader logic, conversion tools, and test suite.
The implementation appears well thought out and includes comprehensive tests covering packing, loading, conversion, and numerical correctness, which is great to see.
Before merging, I'd appreciate review from maintainers on a few aspects:
- The long-term compatibility of introducing
fmt=4into the existing serialization/loading logic. - Whether the new format-detection heuristics in
qt_fmt_from_bytes()remain robust for all supported tensor shapes and future formats. - The maintenance implications of adding a separate int3 execution path (especially the scalar/x86 fallback versus NEON implementation).
Overall the change looks promising, but given its scope and impact on core quantization/inference paths, I think it would benefit from additional review before approval.
b810888 to
8ffd929
Compare
|
Rebased onto current main (62419af, post today's merge wave — #111/#195/#199/#201/#202/#204/#205/#206/#212). One conflict, in On @rajpratham1's three review questions — all three are design decisions the PR already commits to, so stating them here explicitly:
🤖 Generated with Claude Code |
|
Heads-up: #242 (group-scaled int4) just merged to |
|
New independent evidence for this PR's size/quality claim, from a per-projection ablation run last night. I extended
Two takeaways relevant here:
Caveats: OLMoE as proxy model, n=200/task, 0-shot acc_norm; MMLU didn't respond to grouping in this run (recovery showed on hellaswag/arc). Happy to fold the per-projection scheme support into this PR or send it separately — it's ~15 lines in the scheme parser. |
Routed experts (gate/up/down) can now take different bit widths via PROJ_BITS. Motivating config: --xbits 4 --up-bits 3 puts up_proj at int3-g64 (fmt=4, this PR's format) while gate/down stay int4 — ~8% fewer expert bytes on disk and per token, at ~zero quality cost. Backed by the OLMoE per-projection ablation posted to JustVugg#168: up@int3 matches int4-g64 (56.2 vs 55.8), up@int2 craters (-16pp). Validated: synthetic GLM fixture with --up-bits 3 yields int3-g64 up_proj (O*(I/64)*24B weight + group scales) and int4-per-row gate/down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Could you rewrite this against current It's 121 commits behind Your timing turned out to be better than anyone realised. #225 and #307 are converging on per-row int4 scales being the root cause of incoherent output — three users report a repetition loop, and @woolcoxm says gs=64 makes it disappear. Grouped scales (fmt=4) are now in the tree for CPU, and #298 is adding the CUDA half. int3-g64 sits exactly on that thread, so a rebased version would be reviewed against a live question rather than a hypothetical one. What changed under you, which makes a rewrite cheaper than it sounds:
What would help it land fast: the smallest version that does one thing. A 300–700 line PR touching If you'd rather not, say so and I'll close it with thanks — no hard feelings either way. And if you think I've misjudged and it should go in as-is, push back: I've been wrong twice today already and both times a contributor caught it. |
482745a to
1d49224
Compare
Routed experts (gate/up/down) can now take different bit widths via PROJ_BITS. Motivating config: --xbits 4 --up-bits 3 puts up_proj at int3-g64 (fmt=5, this PR's format) while gate/down stay int4 — ~8% fewer expert bytes on disk and per token, at ~zero quality cost. Backed by the OLMoE per-projection ablation posted to JustVugg#168: up@int3 matches int4-g64 (56.2 vs 55.8), up@int2 craters (-16pp). Validated: synthetic GLM fixture with --up-bits 3 yields int3-g64 up_proj (O*(I/64)*24B weight + group scales) and int4-per-row gate/down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Routed experts (gate/up/down) can now take different bit widths via PROJ_BITS. Motivating config: --xbits 4 --up-bits 3 puts up_proj at int3-g64 (fmt=5, this PR's format) while gate/down stay int4 — ~8% fewer expert bytes on disk and per token, at ~zero quality cost. Backed by the OLMoE per-projection ablation posted to JustVugg#168: up@int3 matches int4-g64 (56.2 vs 55.8), up@int2 craters (-16pp). Validated: synthetic GLM fixture with --up-bits 3 yields int3-g64 up_proj (O*(I/64)*24B weight + group scales) and int4-per-row gate/down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1d49224 to
5044f8e
Compare
…rter, tests New weight format: int3 with ONE f32 scale per 64-input group (3.5 bits/weight effective). Per group: 16B low plane (2 bits/val, int2 layout) + 8B high plane (1 bit/val), values [-4,3] stored v+4. Same quantization math as tools/quant_ablation.py _quant_last_dim(bits=3, group=64) from JustVugg#132, whose OLMoE ablation measured int3-g64 BEATING the shipped per-row int4 on quality (-7.5 vs -9.3pp) at ~14% fewer bits. Engine: pack_int3_g64 + matmul_i3 (NEON low-plane unpack reuses matmul_i2's pattern, high plane via vtst bit-expansion; scalar reference everywhere; exact f32 path only - no IDOT in v1), fmt=5 branches in qt_bytes/qt_alloc/qt_fill/ matmul_qt/embed_row/qt_addrow/qt_matvec_rows. Format detection: fmt is not stored on disk and row formats are inferred from byte counts, so the .qs scale tensor SIZE is the tag (O floats = row formats, O*ceil(I/64) = fmt5); qt_fmt_from_bytes centralizes the inference for the dense loader and both expert_load paths. Metal/CUDA fall back to CPU for fmt5 via existing gates. Converter: quant_int3_g64 in convert_fp8_to_int4.py; --ebits 3/--xbits 3 now emit it (previously bits=3 silently produced int4). Tests: tests/test_int3.c (bit-exact pack/unpack vs reference, matmul_i3 vs dequant-matmul incl. short tail groups and the real GLM I=7168, QT plumbing, format tag, outlier-rows RMS: int3-g64 3.3x lower error than per-row int4), tests/test_int3_load.c (hand-rolled .safetensors fixture through st_init + qt_from_disk: fmt5 detected and loaded next to an int4 control tensor), tests/test_int3_convert.py (NumPy pack round-trip vs independent decoder). Rebased onto current `dev`: renumbered int3-g64 to fmt=5 because JustVugg#242 (grouped int4) took fmt=4 in the meantime; on-disk detection is by the distinct int3 weight-byte count, so it coexists with grouped-int4's scale-size detection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Routed experts (gate/up/down) can now take different bit widths via PROJ_BITS. Motivating config: --xbits 4 --up-bits 3 puts up_proj at int3-g64 (fmt=5, this PR's format) while gate/down stay int4 — ~8% fewer expert bytes on disk and per token, at ~zero quality cost. Backed by the OLMoE per-projection ablation posted to JustVugg#168: up@int3 matches int4-g64 (56.2 vs 55.8), up@int2 craters (-16pp). Validated: synthetic GLM fixture with --up-bits 3 yields int3-g64 up_proj (O*(I/64)*24B weight + group scales) and int4-per-row gate/down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5044f8e to
838404b
Compare
|
Rebased onto current One detection subtlety worth flagging: int3-g64 and grouped-int4 at gs=64 carry the same scale count ( The converter now carries two orthogonal axes: your per-tensor-type Kept to the two original commits, 6 files, +503/-18. All 8 CI jobs pass. The first run earned its keep: it caught two real bugs in my own tests (an unconditional On the live question: if #225/#307 converge on per-row int4 scales being the repetition-loop cause, int3-g64 is the 3.5-bit point on that same grouped-scale thread. |
This adds a fourth weight format end-to-end — int3 with per-64-group f32 scales (3.5 bits/weight) — engine, converter, and tests, so producing an int3-g64 GLM container becomes a one-flag job (
--ebits 3 --xbits 3) for anyone holding the FP8 source.Why this format, with data
Three measured inputs converge on it:
int3-g64beats the shipped per-row int4 (−7.5pp vs −9.3pp) at ~14% fewer bits — grouped scales matter more than the 4th bit. This PR ships that exact quantization math (mirrorsquant_ablation._quant_last_dim(bits=3, group=64)bit-for-bit; our test suite reproduces the effect: on outlier rows int3-g64's reconstruction RMS is 3.3× lower than per-row int4).Design
matmul_i2's packing) + 8B high plane (1 bit/val); values in [-4,3] stored v+4; one f32 scale per group inNAME.qs..qsscale-tensor size is the tag:Ofloats → row-scale formats,O*ceil(I/64)→ fmt=4 (qt_fmt_from_bytescentralizes this for the dense loader and bothexpert_loadpaths, with a byte-count fallback for the degenerate I≤64 case).matmul_i3, exact-f32 path with per-group scale accumulation. NEON: low plane reusesmatmul_i2's unpack, high plane expanded viavtston bit masks. Scalar reference everywhere (x86 SIMD is a follow-up — the target regime is fetch-bound). No IDOT path in v1: per-group accumulation doesn't compose with the current int8-activation kernels without restructuring, and the quality economics of IDOT are under separate scrutiny anyway (prefill: batch the attention input projections (bit-identical, -4.5% prefill) #152 and the ARM datapoint we posted there: +0.169 nats/token)../glm 64 3 3on an unquantized checkpoint now produces fmt=4 (previously bits=3 silently became int4).Tests (all green on Apple Silicon,
make test-c+ python suite)tests/test_int3.c— bit-exact pack/unpack vs a plain-C reference;matmul_i3(NEON + scalar tail) vs dequant-matmul across shapes including short tail groups and the real GLMI=7168;qt_alloc/qt_fill/qt_bytes/matmul_qt/qt_addrow/qt_matvec_rowsplumbing; the format tag; the outlier-rows RMS assertion (int3-g64 < per-row int4, ratio 3.34).tests/test_int3_load.c— writes a real single-shard.safetensorsfixture (fmt=4 tensor + int4 control), indexes it withst_init, loads throughqt_from_disk, asserts both formats are detected and dequantize bit-identically to the packer's output.tests/test_int3_convert.py— NumPy converter round-trip against an independent decoder, plus the outlier-vs-int4 property.What this PR does NOT claim
GLM-native quality validation requires quantizing from the FP8 source (int4→int3 requantization stacks errors and is not the honest test). We don't have the 1.51 TB checkpoint; @MateoGrgic (or anyone holding it) can produce the container with
--ebits 3 --xbits 3unchanged otherwise, and #108's harness plusTF=1are the right acceptance gates. Happy to coordinate and to run the full decode/quality ladder on Apple Silicon once a container exists.🤖 Generated with Claude Code