Proposal: opt-in HQQ 5-bit AXIS=0 MoE weights — −25% bytes, +36% E2E on disk-bound hardware, quality within +2.4% ppl of int8
TL;DR. A converter-side, opt-in 5-bit weight format for MoE expert FFNs (attention stays int8), using HQQ AXIS=0 grouping so scales/zeros are per input channel. On a Core i5-7300U with weights streamed from disk: snapshot 7.0 GB → 5.4 GB (−25%), end-to-end 2.42 → 3.28 tok/s (+36%, medians of 3; up to 2.46× in more disk-bound conditions), HF-corpus perplexity 14.2420 vs 13.9144 int8 control (+2.36%), greedy token streams bit-identical to the int8 engine in all 8 test runs. Everything sits behind bits==5 in the converter + a snapshot field; default builds and int8 snapshots are untouched. Related: the axis finding in [issue draft: HQQ axis asymmetry].
Motivation. On disk-bound CPUs the expert pipe, not matmul, is the wall (profiled: pipe_wait ≈ 45% of decode time at Q8_0). The cheapest FLOP is the one you never load: cutting weight bytes 25% cuts pipe traffic 25%. But 5-bit only works if the grouping axis is chosen correctly — HQQ's AXIS=1 mixes 32 heterogeneous input channels under one scale and costs +9% ppl (separate issue); AXIS=0 keeps scales per input channel and lands at +2.4%.
Design (mirrors the existing i4 pattern)
- Converter (
convert_olmoe_hqq5.py): explicit --bits 5 choice at conversion time, per the repo's "quantization choice lives in the converter" policy. Writes merged_weight5_ax0 tensors + a snapshot header field. Weight layout is the natural flat order of the HQQ q tensor (no repacking needed — AXIS=0 groups are strided slabs, so the packed stream is identical in form to the axis=1 variant: 20 B per 32 weights = 16 B nibbles + 4 B hi-plane). Meta (scales|zeros) is contiguous per row, one 64 KB slab per 32 rows.
- Kernel (
dotrow_i5ax0_avx2 / matmul_i5ax0_avx2 in c/fused_simd.h): float FMA with per-element dequant from a hoisted per-group scale/zero. Microbench: 0.145 ms/row-equivalent vs 0.137 for int8 IDOT — parity (±10% run-to-run noise).
- Dispatch (
olmoe.c): on snapshot load, presence of merged_weight5_ax0 sets m->i5=2; runtime prints a MODE I5AX0 ... banner; missing AVX2 → hard refusal; int8 snapshots → today's path, untouched. Fallback to int8 tensors if both present.
Measured (i5-7300U, production flags; medians of 3 E2E reps)
| Metric |
int8 (i5=0) |
i5ax0 |
Δ |
| Snapshot size |
7.0 GB |
5.4 GB |
−25% |
| E2E tok/s |
2.42 |
3.28 |
+36% (up to 2.46× when more disk-bound) |
| pipe_wait (decode) |
10.55 s |
5.77 s |
−45% |
| pipe_io / sync_load |
— |
— |
−34% / −35% |
| time in matmul |
— |
— |
+14% (float vs IDOT — the trade) |
| HF-corpus ppl (harness-verified) |
13.9144 |
14.2420 |
+2.36% |
| Engine TF-NLL on same corpus |
7.7141 |
7.5187 |
−2.5% (consistent) |
| task_acc |
0.8889 |
0.8889 |
0 |
| Greedy stream vs int8 |
ref |
bit-identical, 8/8 runs |
— |
How this answers the review gates from #906
- (a) off by default, loud when on: nothing changes unless the converter is run with
--bits 5 and the resulting snapshot is loaded. Loading such a snapshot prints a MODE I5AX0 banner naming the format, axis, and group size; per-layer [PROF] counters show which layers took the i5 path.
- (b) explicit conversion-time choice: the int8 path is byte-for-byte untouched; 5-bit is a snapshot property, not a runtime flag. A snapshot declares what it is; the engine never silently approximates.
- (c) token-exact when off: int8 snapshots produce bit-identical streams to master (verified by md5 across runs). Determinism within the 5-bit path also holds (8/8 identical greedy streams).
Caveats (honest list)
- Quality cost is real: +2.36% ppl on the HF corpus. That's the price of −25% bytes; the axis=1 alternative (+9%) is strictly worse. Per the warmup-thread policy this belongs to the user at conversion time, and the proposal keeps it there.
- Float-FMA dequant kernel is ~14% slower per FLOP than IDOT; net win exists only when disk-bound (which is the regime this repo targets). On NVMe the gain shrinks.
- AXIS=0 quality depends on the flattening convention of the source checkpoint: per-input-channel scales are only meaningful if axis 0 of the HQQ tensor is the output dimension (true for these safetensors shards; the converter asserts it).
- One machine tested (i5-7300U). No NEON kernel yet.
Happy to open this as a PR (converter + kernels + dispatch + harness, ~1k lines) or to split it (converter RFC first, kernels second). Also happy to run any eval corpus you point us at before anyone spends review time.
Proposal: opt-in HQQ 5-bit AXIS=0 MoE weights — −25% bytes, +36% E2E on disk-bound hardware, quality within +2.4% ppl of int8
TL;DR. A converter-side, opt-in 5-bit weight format for MoE expert FFNs (attention stays int8), using HQQ AXIS=0 grouping so scales/zeros are per input channel. On a Core i5-7300U with weights streamed from disk: snapshot 7.0 GB → 5.4 GB (−25%), end-to-end 2.42 → 3.28 tok/s (+36%, medians of 3; up to 2.46× in more disk-bound conditions), HF-corpus perplexity 14.2420 vs 13.9144 int8 control (+2.36%), greedy token streams bit-identical to the int8 engine in all 8 test runs. Everything sits behind
bits==5in the converter + a snapshot field; default builds and int8 snapshots are untouched. Related: the axis finding in [issue draft: HQQ axis asymmetry].Motivation. On disk-bound CPUs the expert pipe, not matmul, is the wall (profiled:
pipe_wait≈ 45% of decode time at Q8_0). The cheapest FLOP is the one you never load: cutting weight bytes 25% cuts pipe traffic 25%. But 5-bit only works if the grouping axis is chosen correctly — HQQ's AXIS=1 mixes 32 heterogeneous input channels under one scale and costs +9% ppl (separate issue); AXIS=0 keeps scales per input channel and lands at +2.4%.Design (mirrors the existing i4 pattern)
convert_olmoe_hqq5.py): explicit--bits 5choice at conversion time, per the repo's "quantization choice lives in the converter" policy. Writesmerged_weight5_ax0tensors + a snapshot header field. Weight layout is the natural flat order of the HQQqtensor (no repacking needed — AXIS=0 groups are strided slabs, so the packed stream is identical in form to the axis=1 variant: 20 B per 32 weights = 16 B nibbles + 4 B hi-plane). Meta (scales|zeros) is contiguous per row, one 64 KB slab per 32 rows.dotrow_i5ax0_avx2/matmul_i5ax0_avx2inc/fused_simd.h): float FMA with per-element dequant from a hoisted per-group scale/zero. Microbench: 0.145 ms/row-equivalent vs 0.137 for int8 IDOT — parity (±10% run-to-run noise).olmoe.c): on snapshot load, presence ofmerged_weight5_ax0setsm->i5=2; runtime prints aMODE I5AX0 ...banner; missing AVX2 → hard refusal; int8 snapshots → today's path, untouched. Fallback to int8 tensors if both present.Measured (i5-7300U, production flags; medians of 3 E2E reps)
How this answers the review gates from #906
--bits 5and the resulting snapshot is loaded. Loading such a snapshot prints aMODE I5AX0banner naming the format, axis, and group size; per-layer[PROF]counters show which layers took the i5 path.Caveats (honest list)
Happy to open this as a PR (converter + kernels + dispatch + harness, ~1k lines) or to split it (converter RFC first, kernels second). Also happy to run any eval corpus you point us at before anyone spends review time.