|
| 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 | +} |
0 commit comments