bankml / testing /experiments /q2_0_packed.rs
Gregory-L's picture
bankML: the whole source (github.com/cryptoAGI/bankml @ 12ae409) and its page, with the bankML persona; the live engine (Dockerfile, hf/start.sh) ready for Docker hardware
28c70af verified
Raw History Blame Contribute Delete
9.6 kB
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 0.0.5 — measured and not adopted: `Q2Packed`, the ternary weights repacked once (per quad: 64 contiguous
//! code bytes, then the four f16 scales; the same 72 bytes, arithmetic unchanged). Bit-exact against the scalar
//! model of ggml and against the file-layout kernel on the real Ternary-Bonsai-8B tensors — and not reliably
//! faster: 1.00–1.075× in the first run, 0.90–1.05× in the release gate (`testing/results/0.0.5.txt`), on the
//! laptop (Ryzen 3 3200U). Within noise, so by the gate's rule it does not ship. Paste into q2_0.rs (code above
//! `mat_vec`, tests into `mod tests`) to re-run on another core.
/// A Q2_0 matrix repacked once for the AVX2 kernel (0.0.5): each quad of blocks (A, B, C, D) becomes
/// `codes A | codes B | codes C | codes D | d_A d_B d_C d_D` — the 64 code bytes contiguous, so A|B and C|D are
/// one 256-bit load each, and the four f16 scales adjacent, so one 64-bit load and one `vcvtph2ps` give all four.
/// Same 72 bytes per quad as the file (18 per block), only moved; tail blocks (nb % 4) keep the file layout.
/// No arithmetic changes, so the bits are the file's (checked against `vec_dot_ref` on the unpacked rows).
pub struct Q2Packed {
pub rows: usize,
pub n: usize,
data: Vec<u8>,
}
impl Q2Packed {
/// Repack `rows` rows of `n` elements from the GGUF layout.
pub fn pack(raw: &[u8], rows: usize, n: usize) -> Self {
assert_eq!(n % QK2_0, 0);
let nb = n / QK2_0;
let rb = nb * Q2_0_BYTES;
assert!(raw.len() >= rows * rb);
let mut data = vec![0u8; rows * rb];
for (src, dst) in raw[..rows * rb].chunks_exact(rb).zip(data.chunks_exact_mut(rb)) {
for g in 0..nb / 4 {
let (s, d) = (&src[g * 72..g * 72 + 72], &mut dst[g * 72..g * 72 + 72]);
for b in 0..4 {
d[16 * b..16 * b + 16].copy_from_slice(&s[b * 18 + 2..b * 18 + 18]);
d[64 + 2 * b..66 + 2 * b].copy_from_slice(&s[b * 18..b * 18 + 2]);
}
}
let t = nb / 4 * 72;
dst[t..].copy_from_slice(&src[t..]);
}
Q2Packed { rows, n, data }
}
fn row(&self, r: usize) -> &[u8] {
let rb = self.n / QK2_0 * Q2_0_BYTES;
&self.data[r * rb..(r + 1) * rb]
}
}
/// One packed row · prepared activation; the float ops of `vec_dot_act_avx2`, in the same order.
///
/// # Safety
/// AVX2+FMA+F16C present; `x` is a row of a `Q2Packed` with `a.n` elements.
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma,f16c")]
pub unsafe fn vec_dot_packed_avx2(x: &[u8], a: &Q8Act2) -> f32 {
use std::arch::x86_64::*;
let nb = a.n / QK2_0;
let (xp, qp) = (x.as_ptr(), a.qs().as_ptr());
let m3 = _mm256_set1_epi8(3);
let codes = |v: __m256i| {
[
_mm256_and_si256(v, m3),
_mm256_and_si256(_mm256_srli_epi16(v, 2), m3),
_mm256_and_si256(_mm256_srli_epi16(v, 4), m3),
_mm256_and_si256(_mm256_srli_epi16(v, 6), m3),
]
};
let mut sumf = _mm_setzero_ps();
for g in 0..nb / 4 {
let xq = xp.add(g * 72);
let ab = avx2::dot2(&codes(_mm256_loadu_si256(xq as *const __m256i)), qp.add(g * 256));
let cd = avx2::dot2(&codes(_mm256_loadu_si256(xq.add(32) as *const __m256i)), qp.add(g * 256 + 128));
let s = _mm256_sub_epi32(_mm256_hadd_epi32(ab, cd), _mm256_loadu_si256(a.sum.as_ptr().add(8 * g) as *const __m256i));
let (s, d1) = (_mm256_cvtepi32_ps(s), _mm256_loadu_ps(a.d.as_ptr().add(8 * g)));
let u = _mm256_fmadd_ps(d1, s, _mm256_moveldup_ps(_mm256_fmadd_ps(d1, s, _mm256_setzero_ps())));
let (lo, hi) = (_mm256_castps256_ps128(u), _mm256_extractf128_ps(u, 1));
let d0 = _mm_cvtph_ps(_mm_loadl_epi64(xq.add(64) as *const __m128i)); // [dA dB dC dD]
sumf = _mm_fmadd_ss(d0, _mm_movehdup_ps(lo), sumf);
sumf = _mm_fmadd_ss(_mm_movehdup_ps(d0), _mm_movehdup_ps(hi), sumf);
sumf = _mm_fmadd_ss(_mm_movehl_ps(d0, d0), _mm_shuffle_ps(lo, lo, 3), sumf);
sumf = _mm_fmadd_ss(_mm_shuffle_ps(d0, d0, 3), _mm_shuffle_ps(hi, hi, 3), sumf);
}
for i in nb / 4 * 4..nb {
sumf = avx2::block1(xp.add(i * Q2_0_BYTES), a, i, sumf);
}
_mm_cvtss_f32(sumf)
}
/// out[r] = packed row r · `a`; the bits of `vec_dot_ref` on the unpacked row.
pub fn mat_vec_packed(w: &Q2Packed, a: &Q8Act2, out: &mut [f32]) {
assert!(w.n == a.n && out.len() >= w.rows);
#[cfg(target_arch = "x86_64")]
if crate::q1_0::has_avx2() {
for (r, o) in out[..w.rows].iter_mut().enumerate() {
*o = unsafe { vec_dot_packed_avx2(w.row(r), a) };
}
return;
}
let rb = w.n / QK2_0 * Q2_0_BYTES;
let mut raw = vec![0u8; rb];
for (r, o) in out[..w.rows].iter_mut().enumerate() {
unpack_row(w.row(r), &mut raw);
*o = vec_dot_act_scalar(&raw, a);
}
}
/// `mat_vec_packed` on every thread of `pool`; the same bits at any thread count.
pub fn mat_vec_packed_par(pool: &crate::par::Pool, w: &Q2Packed, a: &Q8Act2, out: &mut [f32]) {
assert!(w.n == a.n && out.len() >= w.rows);
pool.rows(w.rows, out, &|r0, o| {
#[cfg(target_arch = "x86_64")]
if crate::q1_0::has_avx2() {
for (i, v) in o.iter_mut().enumerate() {
*v = unsafe { vec_dot_packed_avx2(w.row(r0 + i), a) };
}
return;
}
let mut raw = vec![0u8; w.n / QK2_0 * Q2_0_BYTES];
for (i, v) in o.iter_mut().enumerate() {
unpack_row(w.row(r0 + i), &mut raw);
*v = vec_dot_act_scalar(&raw, a);
}
});
}
/// The file layout of one packed row (the portable path, and tests).
pub fn unpack_row(x: &[u8], out: &mut [u8]) {
let nb = out.len() / Q2_0_BYTES;
for g in 0..nb / 4 {
let (s, d) = (&x[g * 72..g * 72 + 72], &mut out[g * 72..g * 72 + 72]);
for b in 0..4 {
d[b * 18..b * 18 + 2].copy_from_slice(&s[64 + 2 * b..66 + 2 * b]);
d[b * 18 + 2..b * 18 + 18].copy_from_slice(&s[16 * b..16 * b + 16]);
}
}
let t = nb / 4 * 72;
let len = out.len();
out[t..].copy_from_slice(&x[t..len]);
}
// ---- tests
#[test]
fn packed_bit_exact_with_ref() {
let mut r = Rng(7070);
for round in 0..400 {
let (rows, nb) = (1 + (r.next() % 9) as usize, 1 + (r.next() % 70) as usize);
let w = random_q2(&mut r, rows * nb);
let y = random_q8(&mut r, nb, round % 2 == 0);
let a = Q8Act2::from_q8_0(nb * 64, &y);
let p = Q2Packed::pack(&w, rows, nb * 64);
let mut raw = vec![0u8; nb * Q2_0_BYTES];
let (mut o1, mut o3) = (vec![0f32; rows], vec![0f32; rows]);
mat_vec_packed(&p, &a, &mut o1);
mat_vec_packed_par(&crate::par::Pool::new(3), &p, &a, &mut o3);
for i in 0..rows {
unpack_row(p.row(i), &mut raw);
assert_eq!(raw, w[i * nb * Q2_0_BYTES..(i + 1) * nb * Q2_0_BYTES], "unpack round {round}");
let want = vec_dot_ref(nb * 64, &w[i * nb * Q2_0_BYTES..], &y).to_bits();
assert_eq!((o1[i].to_bits(), o3[i].to_bits()), (want, want), "round {round} rows {rows} nb {nb} row {i}");
}
}
}
/// Packed vs file layout on the real ffn_gate and output tensors, 1 and 3 threads, alternating, bits compared.
#[test]
#[ignore = "needs .models/Ternary-Bonsai-8B-Q2_0_g64.gguf; benchmark"]
fn bench_q2_0_packed() {
use crate::gguf::{guard_file, Engine, Mmap};
use crate::par::Pool;
use std::time::Instant;
let model = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".models/Ternary-Bonsai-8B-Q2_0_g64.gguf");
let rep = guard_file(&model, Engine::Mainline).unwrap();
let h = rep.header.as_ref().unwrap();
let mm = Mmap::open(&model).unwrap();
let mut r = Rng(7071);
for name in ["blk.0.ffn_gate.weight", "blk.0.ffn_down.weight", "output.weight"] {
let t = h.tensors.iter().find(|t| t.name == name).unwrap();
let (n, rows) = (t.dims[0] as usize, t.dims[1] as usize);
let w = mm.tensor(h, t).unwrap();
let t0 = Instant::now();
let p = Q2Packed::pack(w, rows, n);
let pack_ms = t0.elapsed().as_secs_f64() * 1e3;
let (_, a) = column(&mut r, n);
let blocks = (rows * n / QK2_0) as f64;
for th in [1, 3] {
let pool = Pool::new(th);
let (mut o1, mut o2) = (vec![0f32; rows], vec![0f32; rows]);
let (mut t1, mut t2) = (vec![], vec![]);
for _ in 0..9 {
let t0 = Instant::now();
mat_vec_par(&pool, w, rows, &a, &mut o1);
t1.push(t0.elapsed().as_nanos() as f64 / blocks);
let t0 = Instant::now();
mat_vec_packed_par(&pool, &p, &a, &mut o2);
t2.push(t0.elapsed().as_nanos() as f64 / blocks);
}
assert!(o1.iter().zip(&o2).all(|(x, y)| x.to_bits() == y.to_bits()), "{name}: bits");
let (a1, a2) = (stat(&mut t1), stat(&mut t2));
eprintln!("{name:22} {rows:6}x{n:<5} {th} thread(s) ns/block min/median: file layout {:.2}/{:.2} | packed {:.2}/{:.2} ({:.3}x) · repack {pack_ms:.0} ms, bits equal",
a1.0, a1.1, a2.0, a2.1, a1.1 / a2.1);
}
}
}