Download docs/modules/q2_0.md from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 9.38 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/q2_0.md
- Command line
-
hf download hf://spaces/PYTHAI/bankml/docs/modules/q2_0.md
-
curl -L -o q2_0.md https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/q2_0.md
bankML/q2_0.rs — the ternary Q2_0_g64 kernel
Summary
q2_0.rs multiplies Q2_0 weights (ggml type 42, group 64) exactly as mainline ggml does at b11192, read from
the tag's source. A block is 18 bytes: an f16 scale d first, then 16 bytes of 2-bit codes, least significant bits
first. Code c ∈ {0,1,2,3} decodes to (c − 1)·d, that is {−1, 0, +1, +2}: 2.25 bits per weight. The Bonsai
files are ternary (in Ternary-Bonsai-8B the split is 31 % / 38 % / 31 % / 0 %), but the format is not, so every kernel
here accepts code 3.
As with Q1_0, the activation is quantized to q8_0 first (the same AVX2 quantizer, q1_0::quantize_row_q8_0), and
one Q2_0 block meets two q8_0 blocks.
On x86, llama.cpp b11192 has no Q2_0 kernel. arch-fallback.h renames the generic C to
ggml_vec_dot_q2_0_q8_0; there is no repack or sgemm case. The shipped haswell symbol is scalar code with 64 imul
per block, used one (row, column) pair at a time for decode and prefill. bankml's AVX2 kernel gives the same bits much
faster. The native forward pass (forward.rs) calls mat_vec_par for decode and mat_mul_par for prompt
micro-batches.
Technical usage
Constants: QK2_0 = 64, Q2_0_BYTES = 18.
pub fn dequantize_row(blocks: &[u8], out: &mut [f32]) // bit-exact with dequantize_row_q2_0
pub fn vec_dot_ref(n: usize, x: &[u8], y: &[u8]) -> f32 // scalar model of the shipped haswell symbol (FMA)
pub fn vec_dot_ref_nofma(n: usize, x: &[u8], y: &[u8]) -> f32 // the same C as the baseline libggml-cpu-x64.so (no FMA)
vec_dot_ref is the reference for the node's kernel. vec_dot_ref_nofma is not used for inference; it is kept so
that both shipped builds of ggml are proven.
Prepared activations
pub struct Q8Act2 { /* private */ }
impl Q8Act2 {
pub fn from_q8_0(n: usize, q8: &[u8]) -> Self
pub fn quantize(x: &[f32]) -> Self
pub fn n(&self) -> usize
pub fn qs(&self) -> &[i8]
}
pub fn vec_dot_act(x: &[u8], a: &Q8Act2) -> f32
pub fn vec_dot_act_scalar(x: &[u8], a: &Q8Act2) -> f32
pub unsafe fn vec_dot_act_avx2(x: &[u8], a: &Q8Act2) -> f32
Q8Act2 lays out a q8_0 activation once per token so that no shuffle touches the weights. Blocks are grouped in
quads (A, B, C, D); a 256-bit register holds the code bytes of A and B, and (v >> 2r) & 3 gives code r of every
byte. The activation stores, per pair and per r, the matching quants. Scales (f32) and Σq per q8 block are stored in
the lane order hadd leaves. Tail blocks (nb % 4) keep a plain layout. Storage is 64-byte aligned.
vec_dot_act picks the AVX2 kernel when q1_0::has_avx2(), else vec_dot_act_scalar. Both have the bits of
vec_dot_ref on the q8_0 bytes the activation came from.
Matrix products
pub fn mat_vec(w: &[u8], rows: usize, a: &Q8Act2, out: &mut [f32])
pub fn mat_vec_par(pool: &crate::par::Pool, w: &[u8], rows: usize, a: &Q8Act2, out: &mut [f32])
pub fn mat_mul(w: &[u8], rows: usize, cols: &[Q8Act2], out: &mut [f32])
pub fn mat_mul_par(pool: &crate::par::Pool, w: &[u8], rows: usize, cols: &[Q8Act2], out: &mut [f32])
pub unsafe fn vec_dot_1x4_avx2(x: &[u8], a: [&Q8Act2; 4]) -> [f32; 4]
mat_mul* writes out[c·rows + r], using 1×4 tiles (one row, four columns, codes expanded once) where the CPU has
AVX2. Every element has the bits of ggml's per-pair vec_dot, at any thread count.
How it is verified
- Unit tests:
dequantize_layout_scale_first_2bit_lsb_first(all four codes),ref_matches_dequantized_math(against f64),act_paths_bit_exact_with_ref_randomized(3,300 cases: 4-block bodies and tails, q = −128, code 3, and activations through the quantizer),par_bit_exact_with_single_thread,mat_vec_and_mat_mul_bit_exact_with_ref. - The ggml oracle
oracle_ggml_b11192_real_ternary_bonsai_8b(#[ignore], needs the model and.models/oracle-ternaryfromtesting/ggml_oracle.py), weights read throughgguf::Mmap. Recorded (oracles.md §1, PERFORMANCE.md): all 254 Q2_0 tensors, 8,188,239,872 weights, dequantized bit-exact; 762/762 q8_0 rows byte-exact; 762/762 dot products bit-exact against the haswell build (scalar model, AVX2 and portable paths); the no-FMA model equals the baseline x64 build in 762/762. The two ggml builds differ from each other in 19 of 762, so the oracle does distinguish float orders. - A/B against the live library (
BANKML_GGML_LIB):ab_vs_ggml_q2_0(real layer-0 weights, every Qwen3-8B projection shape andoutput) anddecode_budget_q2_0(all 253 matmuls of one token,BANKML_THREADS, default1,3). Both check agreement before timing.
BANKML_GGML_LIB=/path/to/llama-b11192 cargo test --release -- --ignored ab_vs_ggml_q2_0 --nocapture --test-threads=1
Advantages and efficiency
- A missing kernel, supplied with the same bits. The ternary gap in llama.cpp b11192 is not a property of ternary
weights but of the scalar fallback. bankml keeps ggml's float chain
(
sumi = fma(d1₁, s₁, fma(d1₀, s₀, 0)),sumf = fma(d0, sumi, sumf), block after block) and reorders only the integer sums, which are exact in any order. - The activation moves, not the weights. Weights are read straight from the file layout. Two blocks share one
256-bit register; codes come out with a shift and a mask, and
maddubs(c, q) − ΣqgivesΣ(c−1)·qwith unsigned codes. The activation is permuted once per token (11 µs at n = 4096, 29 µs at n = 12288, about 0.5 % of the matmul time). - Measured (laptop, Ryzen 3 3200U, PERFORMANCE.md): decode GEMV 9.1–10.6× ggml over four runs
(
output151669×4096: 492 → 51 ms); prefill 1×4 tile 12.3–13.1×. One whole token's 253 matmuls with the model resident: 0.231 s at three threads against ggml's 2.185 s (9.45×), 0.435 s against 4.145 s at one thread. At three threads bankml's ternary matmuls take less time than ggml's 1-bit ones (0.23–0.25 s against 0.34–0.35 s), although the ternary weights are twice the bytes. - Where it sits. About 2× above the measured memory floor (15–17 GB/s on the laptop); compute-bound per core and
at its instruction-throughput limit on Zen+. Three further experiments (repacked quads, a two-row tile, software
prefetch) were bit-exact and not reliably faster, and are kept in
testing/experiments/(0.0.5). - Upstream. The same plain-AVX2 approach was prepared as a C drop-in for llama.cpp (
upstream/): bit-exact on 200,000 of 200,000 cases and 3.4× ggml's shipped scalar kernel (0.2.0). - Practice. Run-time feature detection;
#[target_feature(enable = "avx2,fma,f16c")]functions with documented safety contracts behind safe wrappers;Q8Act2fields private since 0.0.2 because the kernels trustn; alignedLinestorage so no 32-byte activation load straddles a cache line (not measured to matter; kept because it cannot cost).
Design notes
- Source and binary. The format and kernels were read from the b11192 tag's source on 2026-09-26. The shipped
haswell symbol was read from the disassembly: the generic C, unvectorised (64
imulper block), with GCC's default FP contraction turningsumi += d1·s(twice per block) andsumf += d0·sumiintovfmadd231ss. Decode and prefill both run through it one (row, column) pair at a time. This scalar fallback is where bankml's ternary speed advantage comes from (an earlier source note put it at 5×; the current measurements are under Advantages). - Two shipped builds.
vec_dot_refmodels the haswell build (FMA).vec_dot_ref_nofmamodels the baselinelibggml-cpu-x64.so(SSE only, no FMA), where each multiply and add rounds separately. - No saturation. In
dot2,maddubs(c, q)withc ≤ 3andq ≥ −128stays within ±768 per i16 lane, and the sum of the four code planes within ±4·768, so the integer sums are exact. Q8Act2fields. 0.0.1 maden,dandsumpublic, so safe code could send the AVX2 kernels past their buffers; they are private since 0.0.2 and set only byfrom_q8_0/quantize.Linealignment. The 64-byte alignment was not measured to matter on the dev box, whose run-to-run swings are the boost clock (both kernels move together); it is kept because it cannot cost.
Limitations
- x86_64 AVX2 is the only SIMD path; elsewhere the portable scalar path runs with the same bits. NEON, AVX-512 / VNNI and a Zen 3 row are open (TODO.md).
- The oracle uses real weights with synthetic activations; bit-exactness on dumped real activations is open.
- The whole-token figures need the 2.31 GB file resident in the page cache. When memory is short, every token re-reads weights from disk and the gates measured 4.5–5.2 s per token for both runtimes (PERFORMANCE.md).
- Legacy group-128 Q2_0 files, PrismML's
PQ2_0and Bonsai 2's rotated basis are refused by the guard (gguf.md); there is no kernel for them. The ternary GPU kernel is open (TODO 0.5.0).
See also
- q1_0.md (the quantizer, f16 helpers and
has_avx2), par.md, gguf.md - ../oracles.md §1–2 and §6, ../PERFORMANCE.md, ../TECHNICAL.md §III.5, §III.8, ../TODO.md