ferrox_quant/repack/common.rs
1//! Pieces more than one interleaved kind family needs: the f16 read,
2//! the 6-bit K-quant scale/min decode, the `×4` kernel choice, and the
3//! pre-interleaved activation quads the batch GEMMs consume.
4
5use crate::{Q8Activations, Q8KActivations, Q4_K_BLOCK_ELEMS, Q8_0_BLOCK_ELEMS};
6use half::f16;
7
8pub(crate) const KMASK1: u32 = 0x3f3f_3f3f;
9pub(crate) const KMASK2: u32 = 0x0f0f_0f0f;
10pub(crate) const KMASK3: u32 = 0x0303_0303;
11
12#[inline]
13pub(crate) fn f16_from_bytes(b: &[u8]) -> f32 {
14 f16::from_le_bytes([b[0], b[1]]).to_f32()
15}
16
17/// Which `×4` GEMM kernel this host runs, resolved once instead of once
18/// per call.
19///
20/// The `gemm_*_group_x4` entry points are called once per (row-group ×
21/// activation-quad) pair, which on a `pp512` projection is 10^4 to 10^5
22/// calls per GEMM. Each one used to re-run `is_aarch64_feature_detected!`,
23/// whose relaxed atomic load LLVM cannot hoist out of the caller's loop.
24/// Callers now probe once per matmul and pass the answer down through the
25/// `_on` variants; [`gemm_q4_kx8_group_x4`] and its siblings stay as
26/// probe-per-call wrappers so existing callers and tests are unchanged.
27///
28/// This is a dispatch decision only. Every arm computes the same values,
29/// to within f32 rounding order; the portable arm is bit-identical to the
30/// per-activation GEMV, which is what the
31/// `*_x4_portable_is_bit_exact_vs_scalar_gemv` tests assert. Forcing
32/// [`AccelX4::Portable`] on an accelerated host is therefore a valid
33/// (slow) way to run, and the tests use it that way.
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub enum AccelX4 {
36 /// ARM i8mm `SMMLA` kernels in `super::neon`.
37 NeonI8mm,
38 /// x86_64 AVX2 + FMA kernels in `super::avx2`.
39 Avx2,
40 /// The portable scalar reference.
41 Portable,
42}
43
44impl AccelX4 {
45 /// The fastest kernel available on this host.
46 #[inline]
47 pub fn detect() -> Self {
48 #[cfg(target_arch = "aarch64")]
49 {
50 if std::arch::is_aarch64_feature_detected!("i8mm") {
51 return AccelX4::NeonI8mm;
52 }
53 }
54 #[cfg(target_arch = "x86_64")]
55 {
56 if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
57 return AccelX4::Avx2;
58 }
59 }
60 AccelX4::Portable
61 }
62
63 /// Whether this is a real SIMD kernel rather than the scalar
64 /// reference.
65 #[inline]
66 pub fn is_simd(self) -> bool {
67 // Exhaustive on purpose, with no `_` arm: a new kernel family
68 // must answer this question rather than inherit an answer.
69 match self {
70 AccelX4::NeonI8mm | AccelX4::Avx2 => true,
71 AccelX4::Portable => false,
72 }
73 }
74}
75
76/// Whether the `×4` batch GEMMs run a SIMD kernel on this host for
77/// `interleave`-packed weights.
78///
79/// **One predicate.** The five `q*_gemm_uses_acts_x4` entry points are
80/// the same question asked with a different name, and they used to be
81/// five separately written `cfg` blocks — the exact shape (two or more
82/// structures that must agree, with nothing enforcing it) that this repo
83/// keeps paying for. They now all delegate here, so a host either has
84/// the whole `×4` tier or none of it, and no kind can be told "yes" while
85/// its kernel is missing.
86///
87/// The `×4` GEMMs exist only for the interleave-8 layout, on either
88/// architecture, which is why the width is part of the question.
89#[inline]
90pub fn interleaved_gemm_is_accelerated(interleave: usize) -> bool {
91 interleave == 8 && AccelX4::detect().is_simd()
92}
93
94/// The qs interleave width a matrix should be packed with on this host:
95/// 8 wherever the `×4` GEMMs have a SIMD kernel, 4 otherwise.
96///
97/// **One width, derived from the kernel choice.** The five
98/// `q*_interleave` entry points below used to answer this separately,
99/// and they had already drifted: the K-quants returned 8 on x86
100/// unconditionally while Q8_0 and Q4_0 returned 4 there, so packing an
101/// x86 host for the AVX2 GEMM would have given three kinds the layout
102/// its kernel reads and two kinds a layout with none. Asking
103/// [`AccelX4::detect`] once removes that possibility instead of fixing
104/// its instance.
105///
106/// The width is a pure function of the host, so it is constant for the
107/// life of the process — which is what lets the repack cache in
108/// `ferrox_core::weight_matrix::repack_cache` key on `(mapping, format,
109/// rows, cols)` without the width in the key. If this ever became a
110/// per-matrix choice, the width would have to join that key: two
111/// packings of one matrix at different widths are different bytes, and
112/// serving one where the other was asked for is a wrong answer rather
113/// than a panic.
114#[inline]
115pub fn preferred_interleave() -> usize {
116 if AccelX4::detect().is_simd() {
117 8
118 } else {
119 4
120 }
121}
122
123/// Decode one 12-byte packed scale/min group into 8 scales + 8 mins (u8).
124#[inline]
125pub(crate) fn decode_scales_mins(
126 scales12: &[u8],
127 scales_out: &mut [u8; 8],
128 mins_out: &mut [u8; 8],
129) {
130 debug_assert!(scales12.len() >= 12);
131 let mut utmp = [0u32; 4];
132 utmp[0] = u32::from_le_bytes(scales12[0..4].try_into().unwrap());
133 utmp[1] = u32::from_le_bytes(scales12[4..8].try_into().unwrap());
134 utmp[2] = u32::from_le_bytes(scales12[8..12].try_into().unwrap());
135 utmp[3] = ((utmp[2] >> 4) & KMASK2) | (((utmp[1] >> 6) & KMASK3) << 4);
136 let uaux_0 = utmp[1] & KMASK1;
137 utmp[1] = (utmp[2] & KMASK2) | (((utmp[0] >> 6) & KMASK3) << 4);
138 utmp[2] = uaux_0;
139 utmp[0] &= KMASK1;
140 let bytes = unsafe { std::slice::from_raw_parts(utmp.as_ptr() as *const u8, 16) };
141 scales_out.copy_from_slice(&bytes[0..8]);
142 mins_out.copy_from_slice(&bytes[8..16]);
143}
144
145/// A quad of up to [`Q8K_ACTS_X4_NC`] Q8_0 activations, pre-interleaved
146/// into the layout llama.cpp's `ggml_quantize_mat_q8_0_4x8` writes into
147/// `block_q8_0x4` (`arch/arm/repack.cpp`): every 32-element block's qs in
148/// 8-byte runs, plus the per-block per-row scales. Consumed by the i8mm
149/// `4x8` GEMMs; prepared once per matmul, same hoist as [`Q8KActsX4`].
150pub struct Q8ActsX4 {
151 /// Real activations in the quad (≤ 4); rows `na..4` are zero padding.
152 pub na: usize,
153 /// Q8_0 blocks per activation (`n_cols / 32`).
154 pub n_blocks: usize,
155 /// Interleaved quants, `n_blocks * 128` long. Block `b`, 8-element run
156 /// `c`, quad row `a`, lane `k` ↦
157 /// `qs[b*128 + c*32 + a*8 + k] = acts[a].q[b*32 + c*8 + k]`.
158 pub qs: Vec<i8>,
159 /// Activation scales, `n_blocks * 4` long: `d[b*4 + a] = acts[a].d[b]`.
160 pub d: Vec<f32>,
161}
162
163/// Interleave a quad of Q8_0 activations for the `4x8` i8mm GEMMs
164/// (llama.cpp `ggml_quantize_mat_q8_0_4x8`, minus the quantization we
165/// already did). Zero-pads when `acts.len() < 4`. Available on every
166/// target so the portable GEMMs — and the tests pinning the NEON kernels
167/// to them — run anywhere.
168pub fn prepare_q8_acts_x4(acts: &[Q8Activations], n_cols: usize) -> Q8ActsX4 {
169 assert!(acts.len() <= Q8K_ACTS_X4_NC);
170 assert!(n_cols.is_multiple_of(Q8_0_BLOCK_ELEMS));
171 let na = acts.len();
172 let nb = n_cols / Q8_0_BLOCK_ELEMS;
173 let mut qs = vec![0i8; nb * Q8_0_BLOCK_ELEMS * 4];
174 let mut d = vec![0f32; nb * 4];
175 for (a, act) in acts.iter().enumerate() {
176 debug_assert_eq!(act.d.len(), nb);
177 debug_assert!(
178 !act.q.contains(&i8::MIN),
179 "the AVX2 Q8_0 GEMM negates the activation with `_mm256_sign_epi8`, \
180 and -128 negates to itself; every ggml quantizer clamps to +-127"
181 );
182 for b in 0..nb {
183 let src = &act.q[b * Q8_0_BLOCK_ELEMS..(b + 1) * Q8_0_BLOCK_ELEMS];
184 let dst = &mut qs[b * Q8_0_BLOCK_ELEMS * 4..(b + 1) * Q8_0_BLOCK_ELEMS * 4];
185 for (c, run) in src.as_chunks::<8>().0.iter().enumerate() {
186 dst[c * 32 + a * 8..c * 32 + a * 8 + 8].copy_from_slice(run);
187 }
188 d[b * 4 + a] = act.d[b];
189 }
190 }
191 Q8ActsX4 {
192 na,
193 n_blocks: nb,
194 qs,
195 d,
196 }
197}
198
199/// A quad of up to [`Q8K_ACTS_X4_NC`] Q8_K activations, pre-interleaved into
200/// the layout llama.cpp's `ggml_quantize_mat_q8_K_4x8` writes into
201/// `block_q8_Kx4` (`ggml-cpu/repack.cpp`): every super-block's qs, the folded
202/// `bsums` pairs, and the per-block per-row scales.
203///
204/// The i8mm GEMM consumes activations in this shape. Interleaving them once
205/// per matmul — instead of once per (row-group, block) inside the kernel —
206/// is the point: the old in-kernel repack was a scalar pass over
207/// `rows/8 · batch · cols` bytes with a div and a mod per element, roughly
208/// 4× the instruction count of the `vmmlaq_s32` math it fed.
209/// Activations per [`Q8KActsX4`] quad (llama.cpp's `q8_k_blocklen`).
210pub const Q8K_ACTS_X4_NC: usize = 4;
211
212pub struct Q8KActsX4 {
213 /// Real activations in the quad (≤ 4); rows `na..4` are zero padding.
214 pub na: usize,
215 /// Q8_K super-blocks per activation (`n_cols / 256`).
216 pub n_blocks: usize,
217 /// Interleaved quants, `n_blocks * 1024` long. Block `b`, 8-element run
218 /// `c`, quad row `a`, lane `k` ↦
219 /// `qs[b*1024 + c*32 + a*8 + k] = acts[a].q[b*256 + c*8 + k]`.
220 pub qs: Vec<i8>,
221 /// Folded `bsums` pairs, `n_blocks * 4 * 8` long:
222 /// `bsums[(b*4 + a)*8 + i] = acts[a].bsums[b*16 + 2i] + acts[a].bsums[b*16 + 2i + 1]`.
223 pub bsums: Vec<i16>,
224 /// Activation scales, `n_blocks * 4` long: `d[b*4 + a] = acts[a].d[b]`.
225 pub d: Vec<f32>,
226}
227
228/// Interleave a quad of activations for [`gemm_q4_kx8_group_x4`]
229/// (llama.cpp `ggml_quantize_mat_q8_K_4x8`, minus the quantization we
230/// already did). Zero-pads when `acts.len() < 4`, matching what the kernel's
231/// in-loop repack used to emit. Available on every target so the portable
232/// GEMM below — and the tests pinning the NEON kernel to it — run anywhere.
233pub fn prepare_q8_k_acts_x4(acts: &[Q8KActivations], n_cols: usize) -> Q8KActsX4 {
234 assert!(acts.len() <= Q8K_ACTS_X4_NC);
235 assert!(n_cols.is_multiple_of(Q4_K_BLOCK_ELEMS));
236 let na = acts.len();
237 let nb = n_cols / Q4_K_BLOCK_ELEMS;
238 let mut qs = vec![0i8; nb * Q4_K_BLOCK_ELEMS * 4];
239 let mut bsums = vec![0i16; nb * 4 * 8];
240 let mut d = vec![0f32; nb * 4];
241 for (a, act) in acts.iter().enumerate() {
242 debug_assert_eq!(act.n_blocks(), nb);
243 for b in 0..nb {
244 let src = &act.q[b * Q4_K_BLOCK_ELEMS..(b + 1) * Q4_K_BLOCK_ELEMS];
245 let dst = &mut qs[b * Q4_K_BLOCK_ELEMS * 4..(b + 1) * Q4_K_BLOCK_ELEMS * 4];
246 for (c, run) in src.as_chunks::<8>().0.iter().enumerate() {
247 dst[c * 32 + a * 8..c * 32 + a * 8 + 8].copy_from_slice(run);
248 }
249 let src_bs = &act.bsums[b * 16..(b + 1) * 16];
250 let dst_bs = &mut bsums[(b * 4 + a) * 8..(b * 4 + a) * 8 + 8];
251 for (slot, pair) in dst_bs.iter_mut().zip(src_bs.as_chunks::<2>().0) {
252 *slot = pair[0] + pair[1];
253 }
254 d[b * 4 + a] = act.d[b];
255 }
256 }
257 Q8KActsX4 {
258 na,
259 n_blocks: nb,
260 qs,
261 bsums,
262 d,
263 }
264}