int3-g64 (fmt=5): per-group-scale 3-bit weight format — engine, converter, 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 #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 (placed per the #391 split): matmul_i3 + pack_int3_g64 + I3_* layout
helpers in quant.h next to their kernel family; fmt=5 branches in colibri.c's
qt_bytes/qt_alloc/qt_fill/matmul_qt/embed_row/qt_addrow/qt_matvec_rows.

Format detection now goes through #413's qt_resolve_fmt: fmt=5 registers its
distinct weight-byte layout O*ceil(I/64)*24 and its scale cardinality
O*ceil(I/64) there, validated against [O,I] like every other format. int3-g64
and grouped-int4-at-gs=64 carry the SAME scale count, so the weight bytes are
the int3 tag; row formats keep precedence for the small-I shapes where byte
counts coincide. The io_uring expert path still used the raw ?1:?2:3 byte
inference (it missed fmt=4 grouping entirely and never set gs) — converted to
qt_resolve_fmt like the other two expert paths.

Backends: qt_cuda_upload returns 0 for fmt=5 (tensor stays CPU-side, the
documented fallback), the dense CUDA matmul gate excludes fmt=5, and Metal's
existing fmt gates (gemm fmt<=3, moe fmt 1/2) already reject it.

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,
qt_resolve_fmt disambiguation incl. the same-scale-count fmt=4/fmt=5 pair,
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: fmt=5 detected and loaded next to an int4 control tensor),
tests/test_int3_convert.py (NumPy pack round-trip vs independent decoder).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
FABIOTESS
2026-07-20 17:31:05 +01:00
parent ebd85a781b
commit 5e42e70ec3
7 changed files with 503 additions and 12 deletions
+27 -1
View File
@@ -83,6 +83,29 @@ def quant_int4_grouped(w, bits, gs=128):
s_flat = s[:, :, 0].astype(np.float32).reshape(-1)
return out.reshape(-1), s_flat
def quant_int3_g64(w, bits=3, group=64): # -> (qbytes U8 [O*ceil(I/64)*24], scales f32 [O*ceil(I/64)])
"""int3 with PER-GROUP scales (fmt=5 in colibri.c): per 64-input group, symmetric absmax
(qmax=3, clamp [-4,3], stored v+4), packed as 16B low plane (2 bits/val, int2 layout)
+ 8B high plane (1 bit/val). Same math as quant_ablation._quant_last_dim(bits=3,
group=64) (#132), here with real packing. 3.5 bits/weight effective."""
O, I = w.shape
ng = (I + group - 1) // group
pad = ng * group - I
wp = np.pad(w, ((0, 0), (0, pad))) if pad else w
g = wp.reshape(O, ng, group)
amax = np.abs(g).max(axis=2, keepdims=True)
s = np.maximum(amax / 3.0, 1e-8)
q = (np.clip(np.rint(g / s), -4, 3).astype(np.int32) + 4).astype(np.uint8) # 0..7
if pad: q[:, -1, group - pad:] = 4 # pad packs as 0 after -4
lo = np.zeros((O, ng, 16), np.uint8)
for k in range(4):
lo |= ((q[:, :, k::4] & 3) << (k * 2)).astype(np.uint8)
hi = np.zeros((O, ng, 8), np.uint8)
for b in range(8):
hi |= (((q[:, :, b::8] >> 2) & 1) << b).astype(np.uint8)
out = np.concatenate([lo, hi], axis=2) # [O, ng, 24]
return out.reshape(-1), s[:, :, 0].astype(np.float32).reshape(-1)
def quant_int2(w, bits): # -> (qbytes U8 [O*ceil(I/4)], scale f32 [O]); 4/byte
O, I = w.shape
qmax = (1 << (bits - 1)) - 1 # bits=2 -> qmax=1, valori [-2,1]
@@ -237,7 +260,10 @@ def convert_shard(path, out_dict, n_layers, ebits, io_bits, xbits,
bits = ebits
if w.ndim != 2: # es. bias 1D non previsto come 'q' -> tienilo f32
out_dict[name] = w.astype(np.float32); continue
if group_size > 0 and bits <= 4:
if bits == 3:
# int3-g64 (fmt=5): inherently group-64, distinct from grouped-int4.
q, s = quant_int3_g64(w)
elif group_size > 0 and bits <= 4:
q, s = quant_int4_grouped(w, bits, group_size)
else:
q, s = (quant_int2(w, bits) if bits <= 2 else