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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user