Skip to content

Commit c8ffd21

Browse files
committed
feat(quant/cpu): add NEON fused dequant+dot kernels for aarch64
Implement fused dequantization and dot product kernels for Q4_K, Q5_K, and Q6_K formats using ARM NEON intrinsics. Adds a shared horizontal sum helper (dot_f32.rs) used across all three kernels. These kernels are the aarch64 counterpart to the existing x86 AVX2 implementations and are wired into the dispatch logic added in the previous commit.
1 parent 009b75f commit c8ffd21

5 files changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! NEON f32 dot product with FMA for quantized matmul accumulation
2+
3+
#[cfg(target_arch = "aarch64")]
4+
use std::arch::aarch64::*;
5+
6+
const F32_LANES: usize = 4;
7+
8+
/// Horizontal sum of 4 f32 values in a NEON register.
9+
///
10+
/// # Safety
11+
/// - CPU must support NEON (always true on AArch64)
12+
#[cfg(target_arch = "aarch64")]
13+
#[target_feature(enable = "neon")]
14+
#[inline]
15+
pub unsafe fn hsum_f32_neon(v: float32x4_t) -> f32 {
16+
let pair = vadd_f32(vget_low_f32(v), vget_high_f32(v));
17+
vget_lane_f32::<0>(vpadd_f32(pair, pair))
18+
}
19+
20+
/// NEON f32 dot product of two slices using FMA.
21+
///
22+
/// # Safety
23+
/// - CPU must support NEON (always true on AArch64)
24+
/// - `a` and `b` must be valid for reads of `len` f32 elements each
25+
#[cfg(target_arch = "aarch64")]
26+
#[target_feature(enable = "neon")]
27+
pub unsafe fn dot_f32_neon(a: *const f32, b: *const f32, len: usize) -> f32 {
28+
let chunks = len / F32_LANES;
29+
let remainder = len % F32_LANES;
30+
31+
let mut acc = vdupq_n_f32(0.0);
32+
33+
for i in 0..chunks {
34+
let offset = i * F32_LANES;
35+
let va = vld1q_f32(a.add(offset));
36+
let vb = vld1q_f32(b.add(offset));
37+
acc = vfmaq_f32(acc, va, vb);
38+
}
39+
40+
let mut result = hsum_f32_neon(acc);
41+
42+
for i in 0..remainder {
43+
let offset = chunks * F32_LANES + i;
44+
result += *a.add(offset) * *b.add(offset);
45+
}
46+
47+
result
48+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! NEON fused dequant+dot for Q4_K format
2+
//!
3+
//! Q4_K block layout (256 elements, 144 bytes):
4+
//! [0..2] d (f16 scale)
5+
//! [2..4] dmin (f16 minimum)
6+
//! [4..16] sc (12-byte packed 6-bit scales+mins for 8 sub-blocks)
7+
//! [16..144] qs (128 bytes of 4-bit quantized values, 2 per byte)
8+
9+
#[cfg(target_arch = "aarch64")]
10+
use std::arch::aarch64::*;
11+
12+
use half::f16;
13+
14+
use super::super::super::dequant_k_quants::unpack_q4k_q5k_scales;
15+
use super::dot_f32::hsum_f32_neon;
16+
17+
const F32_LANES: usize = 4;
18+
19+
/// Fused dequant+dot for Q4_K using NEON.
20+
///
21+
/// # Safety
22+
/// Requires NEON. Caller must ensure act.len() >= k and blocks covers k/256 blocks.
23+
#[cfg(target_arch = "aarch64")]
24+
#[target_feature(enable = "neon")]
25+
pub unsafe fn fused_dot_q4k_neon(act: &[f32], blocks: &[u8], k: usize) -> f32 {
26+
const BLOCK_SIZE: usize = 256;
27+
const BLOCK_BYTES: usize = 144;
28+
let num_blocks = k / BLOCK_SIZE;
29+
30+
debug_assert!(act.len() >= k, "act.len() {} < k {}", act.len(), k);
31+
debug_assert!(
32+
blocks.len() >= num_blocks * BLOCK_BYTES,
33+
"blocks.len() {} < required {}",
34+
blocks.len(),
35+
num_blocks * BLOCK_BYTES
36+
);
37+
38+
let mut total_acc = vdupq_n_f32(0.0);
39+
40+
for b in 0..num_blocks {
41+
let block = &blocks[b * BLOCK_BYTES..];
42+
let d = f16::from_le_bytes([block[0], block[1]]).to_f32();
43+
let dmin = f16::from_le_bytes([block[2], block[3]]).to_f32();
44+
let sc = &block[4..16];
45+
let qs = &block[16..144];
46+
let act_block = &act[b * BLOCK_SIZE..];
47+
48+
let (scales, mins) = unpack_q4k_q5k_scales(sc);
49+
50+
for j in 0..8 {
51+
let dl = d * scales[j] as f32;
52+
let ml = dmin * mins[j] as f32;
53+
54+
let chunk = j / 2;
55+
let is_high = j % 2 == 1;
56+
let qs_base = chunk * 32;
57+
58+
let act_sub = &act_block[j * 32..];
59+
let dl_vec = vdupq_n_f32(dl);
60+
let ml_vec = vdupq_n_f32(ml);
61+
let mask_0f = vdupq_n_u32(0x0F);
62+
63+
// Process 32 elements in 8 groups of 4
64+
for g in 0..8 {
65+
let l_base = g * 4;
66+
67+
// Load 4 bytes of quantized data, zero-extend to u32
68+
let q0 = qs[qs_base + l_base] as u32;
69+
let q1 = qs[qs_base + l_base + 1] as u32;
70+
let q2 = qs[qs_base + l_base + 2] as u32;
71+
let q3 = qs[qs_base + l_base + 3] as u32;
72+
73+
let raw = vcreate_u32(q0 as u64 | (q1 as u64) << 32);
74+
let raw_hi = vcreate_u32(q2 as u64 | (q3 as u64) << 32);
75+
let raw256 = vcombine_u32(raw, raw_hi);
76+
77+
// Extract nibbles
78+
let nibbles = if is_high {
79+
vandq_u32(vshrq_n_u32::<4>(raw256), mask_0f)
80+
} else {
81+
vandq_u32(raw256, mask_0f)
82+
};
83+
84+
// Convert to f32
85+
let q_f32 = vcvtq_f32_u32(nibbles);
86+
87+
// Load 4 activation values
88+
let a = vld1q_f32(act_sub.as_ptr().add(l_base));
89+
90+
// Accumulate: dl * (a * q) - ml * a
91+
let aq = vmulq_f32(a, q_f32);
92+
total_acc = vfmaq_f32(total_acc, dl_vec, aq);
93+
total_acc = vsubq_f32(total_acc, vmulq_f32(ml_vec, a));
94+
}
95+
}
96+
}
97+
98+
hsum_f32_neon(total_acc)
99+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! NEON fused dequant+dot for Q5_K format
2+
//!
3+
//! Q5_K block layout (256 elements, 176 bytes):
4+
//! [0..2] d (f16 scale)
5+
//! [2..4] dmin (f16 minimum)
6+
//! [4..16] sc (12-byte packed 6-bit scales+mins for 8 sub-blocks)
7+
//! [16..48] qh (32 bytes, 1 high bit per element)
8+
//! [48..176] qs (128 bytes of 4-bit low nibbles, 2 per byte)
9+
10+
#[cfg(target_arch = "aarch64")]
11+
use std::arch::aarch64::*;
12+
13+
use half::f16;
14+
15+
use super::super::super::dequant_k_quants::unpack_q4k_q5k_scales;
16+
use super::dot_f32::hsum_f32_neon;
17+
18+
/// Fused dequant+dot for Q5_K using NEON.
19+
///
20+
/// # Safety
21+
/// Requires NEON. Caller must ensure act.len() >= k and blocks covers k/256 blocks.
22+
#[cfg(target_arch = "aarch64")]
23+
#[target_feature(enable = "neon")]
24+
pub unsafe fn fused_dot_q5k_neon(act: &[f32], blocks: &[u8], k: usize) -> f32 {
25+
const BLOCK_SIZE: usize = 256;
26+
const BLOCK_BYTES: usize = 176;
27+
let num_blocks = k / BLOCK_SIZE;
28+
29+
debug_assert!(act.len() >= k, "act.len() {} < k {}", act.len(), k);
30+
debug_assert!(
31+
blocks.len() >= num_blocks * BLOCK_BYTES,
32+
"blocks.len() {} < required {}",
33+
blocks.len(),
34+
num_blocks * BLOCK_BYTES
35+
);
36+
37+
let mut total_acc = vdupq_n_f32(0.0);
38+
39+
for b in 0..num_blocks {
40+
let block = &blocks[b * BLOCK_BYTES..];
41+
let d = f16::from_le_bytes([block[0], block[1]]).to_f32();
42+
let dmin = f16::from_le_bytes([block[2], block[3]]).to_f32();
43+
let sc = &block[4..16];
44+
let qh = &block[16..48];
45+
let qs = &block[48..176];
46+
let act_block = &act[b * BLOCK_SIZE..];
47+
48+
let (scales, mins) = unpack_q4k_q5k_scales(sc);
49+
50+
for j in 0..8 {
51+
let dl = d * scales[j] as f32;
52+
let ml = dmin * mins[j] as f32;
53+
54+
let act_sub = &act_block[j * 32..];
55+
let dl_vec = vdupq_n_f32(dl);
56+
let ml_vec = vdupq_n_f32(ml);
57+
58+
// Process 32 elements in 8 groups of 4
59+
for g in 0..8 {
60+
let l_base = g * 4;
61+
62+
// Build 5-bit values: low4 from qs + high1 from qh
63+
let mut q_vals = [0u32; 4];
64+
for l in 0..4 {
65+
let idx = j * 32 + l_base + l;
66+
let qs_idx = j * 16 + (l_base + l) / 2;
67+
let low4 = if (l_base + l) % 2 == 0 {
68+
qs[qs_idx] & 0x0F
69+
} else {
70+
(qs[qs_idx] >> 4) & 0x0F
71+
};
72+
let qh_byte = idx / 8;
73+
let qh_bit = idx % 8;
74+
let high1 = (qh[qh_byte] >> qh_bit) & 0x01;
75+
q_vals[l] = (low4 | (high1 << 4)) as u32;
76+
}
77+
78+
let q_u32 = vcombine_u32(
79+
vcreate_u32(q_vals[0] as u64 | (q_vals[1] as u64) << 32),
80+
vcreate_u32(q_vals[2] as u64 | (q_vals[3] as u64) << 32),
81+
);
82+
let q_f32 = vcvtq_f32_u32(q_u32);
83+
84+
let a = vld1q_f32(act_sub.as_ptr().add(l_base));
85+
86+
// Accumulate: dl * (a * q) - ml * a
87+
let aq = vmulq_f32(a, q_f32);
88+
total_acc = vfmaq_f32(total_acc, dl_vec, aq);
89+
total_acc = vsubq_f32(total_acc, vmulq_f32(ml_vec, a));
90+
}
91+
}
92+
}
93+
94+
hsum_f32_neon(total_acc)
95+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! NEON fused dequant+dot for Q6_K format
2+
//!
3+
//! Q6_K block layout (256 elements, 210 bytes):
4+
//! [0..128] ql (low 4 bits, packed 2 per byte for 256 elements)
5+
//! [128..192] qh (high 2 bits, packed 4 per byte)
6+
//! [192..208] sc (16 x i8 scales)
7+
//! [208..210] d (f16 scale)
8+
9+
#[cfg(target_arch = "aarch64")]
10+
use std::arch::aarch64::*;
11+
12+
use half::f16;
13+
14+
use super::dot_f32::hsum_f32_neon;
15+
16+
/// Fused dequant+dot for Q6_K using NEON.
17+
///
18+
/// # Safety
19+
/// Requires NEON. Caller must ensure act.len() >= k and blocks covers k/256 blocks.
20+
#[cfg(target_arch = "aarch64")]
21+
#[target_feature(enable = "neon")]
22+
pub unsafe fn fused_dot_q6k_neon(act: &[f32], blocks: &[u8], k: usize) -> f32 {
23+
const BLOCK_SIZE: usize = 256;
24+
const BLOCK_BYTES: usize = 210;
25+
let num_blocks = k / BLOCK_SIZE;
26+
27+
debug_assert!(act.len() >= k, "act.len() {} < k {}", act.len(), k);
28+
debug_assert!(
29+
blocks.len() >= num_blocks * BLOCK_BYTES,
30+
"blocks.len() {} < required {}",
31+
blocks.len(),
32+
num_blocks * BLOCK_BYTES
33+
);
34+
35+
let mut total_acc = vdupq_n_f32(0.0);
36+
37+
for b in 0..num_blocks {
38+
let block = &blocks[b * BLOCK_BYTES..];
39+
let ql = &block[0..128];
40+
let qh = &block[128..192];
41+
let sc: &[i8] = std::slice::from_raw_parts(block[192..208].as_ptr() as *const i8, 16);
42+
let d = f16::from_le_bytes([block[208], block[209]]).to_f32();
43+
let act_block = &act[b * BLOCK_SIZE..];
44+
45+
// Process in two halves of 128 elements
46+
for n in 0..2 {
47+
let y_base = n * 128;
48+
let ql_base = n * 64;
49+
let qh_base = n * 32;
50+
let sc_base = n * 8;
51+
52+
// Process 32 elements per sub-iteration, 4 outputs each
53+
for l in 0..32 {
54+
let is = l / 16;
55+
56+
// Reconstruct 6-bit values
57+
let q1 = ((ql[ql_base + l] & 0x0F) | ((qh[qh_base + l] & 0x03) << 4)) as i8 - 32;
58+
let q2 = ((ql[ql_base + l + 32] & 0x0F) | (((qh[qh_base + l] >> 2) & 0x03) << 4))
59+
as i8
60+
- 32;
61+
let q3 =
62+
((ql[ql_base + l] >> 4) | (((qh[qh_base + l] >> 4) & 0x03) << 4)) as i8 - 32;
63+
let q4 = ((ql[ql_base + l + 32] >> 4) | (((qh[qh_base + l] >> 6) & 0x03) << 4))
64+
as i8
65+
- 32;
66+
67+
// Build NEON vector of 4 dequantized values
68+
let s1 = d * sc[sc_base + is] as f32;
69+
let s2 = d * sc[sc_base + is + 2] as f32;
70+
let s3 = d * sc[sc_base + is + 4] as f32;
71+
let s4 = d * sc[sc_base + is + 6] as f32;
72+
73+
let dq = vcombine_f32(
74+
vcreate_f32(
75+
(s1 * q1 as f32).to_bits() as u64
76+
| ((s2 * q2 as f32).to_bits() as u64) << 32,
77+
),
78+
vcreate_f32(
79+
(s3 * q3 as f32).to_bits() as u64
80+
| ((s4 * q4 as f32).to_bits() as u64) << 32,
81+
),
82+
);
83+
84+
// Load 4 activation values at positions [y_base+l, y_base+l+32, y_base+l+64, y_base+l+96]
85+
let a = vcombine_f32(
86+
vcreate_f32(
87+
(*act_block.as_ptr().add(y_base + l)).to_bits() as u64
88+
| ((*act_block.as_ptr().add(y_base + l + 32)).to_bits() as u64) << 32,
89+
),
90+
vcreate_f32(
91+
(*act_block.as_ptr().add(y_base + l + 64)).to_bits() as u64
92+
| ((*act_block.as_ptr().add(y_base + l + 96)).to_bits() as u64) << 32,
93+
),
94+
);
95+
96+
total_acc = vfmaq_f32(total_acc, dq, a);
97+
}
98+
}
99+
}
100+
101+
hsum_f32_neon(total_acc)
102+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod dot_f32;
2+
pub mod fused_q4k;
3+
pub mod fused_q5k;
4+
pub mod fused_q6k;

0 commit comments

Comments
 (0)