Download testing/experiments/q2_0_packed.rs from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 9.6 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/testing/experiments/q2_0_packed.rs
- Command line
-
hf download hf://spaces/PYTHAI/bankml/testing/experiments/q2_0_packed.rs
-
curl -L -o q2_0_packed.rs https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/testing/experiments/q2_0_packed.rs
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. | |
| 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); | |
| 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| { | |
| 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 | |
| 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. | |
| 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); | |
| } | |
| } | |
| } | |