1#[cfg(target_arch = "aarch64")]
17use std::arch::aarch64::*;
18
19#[cfg(target_arch = "aarch64")]
20use super::simd_config;
21
22#[derive(Debug, Clone, Copy)]
29pub struct Int4Params {
30 pub scale: f32,
32 pub max_abs: f32,
34}
35
36impl Int4Params {
37 pub fn from_vector(vector: &[f32]) -> Self {
39 let mut max_abs: f32 = 0.0;
40 for &v in vector {
41 if v.is_finite() {
42 max_abs = max_abs.max(v.abs());
43 }
44 }
45
46 let scale = if max_abs > 1e-10 {
48 15.0 / (2.0 * max_abs)
49 } else {
50 1.0
51 };
52
53 Self { scale, max_abs }
54 }
55}
56
57#[derive(Debug, Clone)]
61pub struct Int4Vector {
62 pub data: Vec<u8>,
64 pub dims: usize,
66 pub params: Int4Params,
68 pub norm: f32,
70}
71
72impl Int4Vector {
73 pub fn from_f32(vector: &[f32]) -> Self {
79 let params = Int4Params::from_vector(vector);
80 let dims = vector.len();
81
82 let mut norm_sq = 0.0f32;
84 for &v in vector {
85 if v.is_finite() {
86 norm_sq += v * v;
87 }
88 }
89 let norm = norm_sq.sqrt();
90
91 let packed_len = dims.div_ceil(2);
93 let mut data = vec![0u8; packed_len];
94
95 for (i, &elem) in vector[..dims].iter().enumerate() {
96 let v = if elem.is_finite() { elem } else { 0.0 };
97 let q = ((v + params.max_abs) * params.scale)
99 .round()
100 .clamp(0.0, 15.0) as u8;
101
102 let byte_idx = i / 2;
103 if i % 2 == 0 {
104 data[byte_idx] |= q << 4;
106 } else {
107 data[byte_idx] |= q;
109 }
110 }
111
112 Self {
113 data,
114 dims,
115 params,
116 norm,
117 }
118 }
119
120 pub fn to_f32(&self) -> Vec<f32> {
138 let required_bytes = self.dims.div_ceil(2);
139 if self.data.len() < required_bytes {
140 return Vec::new();
141 }
142
143 let scale = if self.params.scale.is_finite() && self.params.scale != 0.0 {
144 self.params.scale
145 } else {
146 1.0
147 };
148
149 let mut result = Vec::with_capacity(self.dims);
150 for i in 0..self.dims {
151 let byte_idx = i / 2;
152 let q = if i % 2 == 0 {
153 (self.data[byte_idx] >> 4) & 0x0F
154 } else {
155 self.data[byte_idx] & 0x0F
156 };
157 result.push(q as f32 / scale - self.params.max_abs);
158 }
159 result
160 }
161
162 #[inline]
166 pub fn dot_product(&self, other: &Int4Vector) -> f32 {
167 dot_product_int4(self, other)
168 }
169
170 #[inline]
172 pub fn cosine_similarity(&self, other: &Int4Vector) -> f32 {
173 let denom = self.norm * other.norm;
174 if denom == 0.0 || !denom.is_finite() {
175 return 0.0;
176 }
177 self.dot_product(other) / denom
178 }
179
180 #[inline]
182 pub fn cosine_distance(&self, other: &Int4Vector) -> f32 {
183 1.0 - self.cosine_similarity(other)
184 }
185}
186
187#[inline]
194pub fn dot_product_int4(a: &Int4Vector, b: &Int4Vector) -> f32 {
195 if a.dims != b.dims {
196 return 0.0;
197 }
198
199 let scale_a = a.params.scale;
200 let scale_b = b.params.scale;
201 if scale_a == 0.0 || scale_b == 0.0 || !scale_a.is_finite() || !scale_b.is_finite() {
202 return 0.0;
203 }
204
205 let packed_len = a.dims.div_ceil(2);
206 if a.data.len() < packed_len || b.data.len() < packed_len {
207 return 0.0;
208 }
209
210 #[cfg(target_arch = "aarch64")]
211 {
212 let config = simd_config();
213 if config.neon_enabled {
214 let (raw_dot, sum_a, sum_b) =
218 unsafe { dot_product_int4_neon_unrolled(&a.data, &b.data, a.dims) };
219 return finish_int4_dot(raw_dot, sum_a, sum_b, a, b);
220 }
221 }
222
223 let (raw_dot, sum_a, sum_b) = dot_product_int4_packed_scalar(&a.data, &b.data, a.dims);
224 finish_int4_dot(raw_dot, sum_a, sum_b, a, b)
225}
226
227#[inline]
228fn finish_int4_dot(raw_dot: i32, sum_a: i32, sum_b: i32, a: &Int4Vector, b: &Int4Vector) -> f32 {
229 let raw_dot = raw_dot as f32;
230 let sum_a = sum_a as f32;
231 let sum_b = sum_b as f32;
232 let scale_a = a.params.scale;
233 let scale_b = b.params.scale;
234
235 raw_dot / (scale_a * scale_b)
236 - (b.params.max_abs * sum_a / scale_a)
237 - (a.params.max_abs * sum_b / scale_b)
238 + (a.dims as f32 * a.params.max_abs * b.params.max_abs)
239}
240
241#[inline]
242fn dot_product_int4_packed_scalar(a: &[u8], b: &[u8], dims: usize) -> (i32, i32, i32) {
243 let full_bytes = dims / 2;
244 let mut raw_dot = 0i32;
245 let mut sum_a = 0i32;
246 let mut sum_b = 0i32;
247
248 for i in 0..full_bytes {
249 let av = a[i];
250 let bv = b[i];
251 let ah = ((av >> 4) & 0x0f) as i32;
252 let al = (av & 0x0f) as i32;
253 let bh = ((bv >> 4) & 0x0f) as i32;
254 let bl = (bv & 0x0f) as i32;
255 raw_dot += ah * bh + al * bl;
256 sum_a += ah + al;
257 sum_b += bh + bl;
258 }
259
260 if dims % 2 == 1 {
261 let av = a[full_bytes];
262 let bv = b[full_bytes];
263 let ah = ((av >> 4) & 0x0f) as i32;
264 let bh = ((bv >> 4) & 0x0f) as i32;
265 raw_dot += ah * bh;
266 sum_a += ah;
267 sum_b += bh;
268 }
269
270 (raw_dot, sum_a, sum_b)
271}
272
273#[cfg(target_arch = "aarch64")]
274#[target_feature(enable = "neon")]
275#[inline]
276unsafe fn dot_product_int4_neon_unrolled(a: &[u8], b: &[u8], dims: usize) -> (i32, i32, i32) {
277 debug_assert!(a.len() >= dims.div_ceil(2));
278 debug_assert!(b.len() >= dims.div_ceil(2));
279
280 const BLOCK_BYTES: usize = 16;
281 const UNROLL: usize = 4;
282 const CHUNK_BYTES: usize = BLOCK_BYTES * UNROLL;
283
284 let full_bytes = dims / 2;
288 let chunks = full_bytes / CHUNK_BYTES;
289
290 let mut raw0 = vdupq_n_u32(0);
291 let mut raw1 = vdupq_n_u32(0);
292 let mut raw2 = vdupq_n_u32(0);
293 let mut raw3 = vdupq_n_u32(0);
294 let mut sum_a = vdupq_n_u32(0);
295 let mut sum_b = vdupq_n_u32(0);
296 let mask = vdupq_n_u8(0x0f);
297
298 macro_rules! accumulate_block {
299 ($base:expr, $raw:ident) => {{
300 let a_bytes = vld1q_u8(a.as_ptr().add($base));
301 let b_bytes = vld1q_u8(b.as_ptr().add($base));
302
303 let a_hi = vshrq_n_u8::<4>(a_bytes);
304 let b_hi = vshrq_n_u8::<4>(b_bytes);
305 let a_lo = vandq_u8(a_bytes, mask);
306 let b_lo = vandq_u8(b_bytes, mask);
307
308 $raw = vpadalq_u16($raw, vmull_u8(vget_low_u8(a_hi), vget_low_u8(b_hi)));
309 $raw = vpadalq_u16($raw, vmull_u8(vget_high_u8(a_hi), vget_high_u8(b_hi)));
310 $raw = vpadalq_u16($raw, vmull_u8(vget_low_u8(a_lo), vget_low_u8(b_lo)));
311 $raw = vpadalq_u16($raw, vmull_u8(vget_high_u8(a_lo), vget_high_u8(b_lo)));
312
313 sum_a = vpadalq_u16(sum_a, vpaddlq_u8(a_hi));
314 sum_a = vpadalq_u16(sum_a, vpaddlq_u8(a_lo));
315 sum_b = vpadalq_u16(sum_b, vpaddlq_u8(b_hi));
316 sum_b = vpadalq_u16(sum_b, vpaddlq_u8(b_lo));
317 }};
318 }
319
320 for i in 0..chunks {
321 let base = i * CHUNK_BYTES;
322 accumulate_block!(base, raw0);
323 accumulate_block!(base + BLOCK_BYTES, raw1);
324 accumulate_block!(base + BLOCK_BYTES * 2, raw2);
325 accumulate_block!(base + BLOCK_BYTES * 3, raw3);
326 }
327
328 let raw_vec = vaddq_u32(vaddq_u32(raw0, raw1), vaddq_u32(raw2, raw3));
329 let mut raw_total = (vgetq_lane_u32::<0>(raw_vec)
330 + vgetq_lane_u32::<1>(raw_vec)
331 + vgetq_lane_u32::<2>(raw_vec)
332 + vgetq_lane_u32::<3>(raw_vec)) as i32;
333 let mut sum_a_total = (vgetq_lane_u32::<0>(sum_a)
334 + vgetq_lane_u32::<1>(sum_a)
335 + vgetq_lane_u32::<2>(sum_a)
336 + vgetq_lane_u32::<3>(sum_a)) as i32;
337 let mut sum_b_total = (vgetq_lane_u32::<0>(sum_b)
338 + vgetq_lane_u32::<1>(sum_b)
339 + vgetq_lane_u32::<2>(sum_b)
340 + vgetq_lane_u32::<3>(sum_b)) as i32;
341
342 let remainder_start = chunks * CHUNK_BYTES;
343 for byte_idx in remainder_start..full_bytes {
344 let av = *a.get_unchecked(byte_idx);
345 let bv = *b.get_unchecked(byte_idx);
346 let ah = ((av >> 4) & 0x0f) as i32;
347 let al = (av & 0x0f) as i32;
348 let bh = ((bv >> 4) & 0x0f) as i32;
349 let bl = (bv & 0x0f) as i32;
350
351 raw_total += ah * bh + al * bl;
352 sum_a_total += ah + al;
353 sum_b_total += bh + bl;
354 }
355
356 if dims % 2 == 1 {
357 let av = *a.get_unchecked(full_bytes);
358 let bv = *b.get_unchecked(full_bytes);
359 let ah = ((av >> 4) & 0x0f) as i32;
360 let bh = ((bv >> 4) & 0x0f) as i32;
361
362 raw_total += ah * bh;
363 sum_a_total += ah;
364 sum_b_total += bh;
365 }
366
367 (raw_total, sum_a_total, sum_b_total)
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 fn generate_vector(dim: usize, seed: u64) -> Vec<f32> {
375 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
376 (0..dim)
377 .map(|i| {
378 state = state
379 .wrapping_mul(6364136223846793005)
380 .wrapping_add(1442695040888963407)
381 .wrapping_add(i as u64);
382 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
383 unit * 2.0 - 1.0
384 })
385 .collect()
386 }
387
388 #[test]
389 fn test_int4_roundtrip_accuracy() {
390 let original = generate_vector(384, 42);
391 let quantized = Int4Vector::from_f32(&original);
392 let dequantized = quantized.to_f32();
393
394 assert_eq!(dequantized.len(), original.len());
395
396 let max_abs = original
399 .iter()
400 .filter(|v| v.is_finite())
401 .map(|v| v.abs())
402 .fold(0.0f32, f32::max);
403 let expected_max_error = 2.0 * max_abs / 15.0;
404
405 for (i, (orig, deq)) in original.iter().zip(dequantized.iter()).enumerate() {
406 let error = (orig - deq).abs();
407 assert!(
408 error <= expected_max_error + 1e-5,
409 "INT4 roundtrip error too large at index {i}: orig={orig}, deq={deq}, error={error}, max_allowed={expected_max_error}"
410 );
411 }
412 }
413
414 #[test]
415 fn test_int4_packing_correctness() {
416 let v = vec![0.5, -0.5, 0.0, 1.0]; let q = Int4Vector::from_f32(&v);
419 assert_eq!(q.data.len(), 2);
420 assert_eq!(q.dims, 4);
421
422 let deq = q.to_f32();
424 assert_eq!(deq.len(), 4);
425 assert!((deq[0] - 0.5).abs() < 0.15, "deq[0]={}", deq[0]);
427 assert!((deq[1] - (-0.5)).abs() < 0.15, "deq[1]={}", deq[1]);
428 }
429
430 #[test]
431 fn test_int4_odd_dimensions() {
432 let v = generate_vector(383, 77);
434 let q = Int4Vector::from_f32(&v);
435 assert_eq!(q.data.len(), 192); assert_eq!(q.dims, 383);
437
438 let deq = q.to_f32();
439 assert_eq!(deq.len(), 383);
440 }
441
442 #[test]
443 fn test_int4_zero_vector() {
444 let v = vec![0.0; 384];
445 let q = Int4Vector::from_f32(&v);
446 let deq = q.to_f32();
447 for &val in &deq {
448 assert!(
449 val.abs() < 1e-5,
450 "Zero vector should dequantize to near-zero"
451 );
452 }
453 }
454
455 #[test]
456 fn test_int4_dot_product_vs_f32() {
457 let a = generate_vector(384, 101);
461 let b: Vec<f32> = a
462 .iter()
463 .enumerate()
464 .map(|(i, &x)| x + 0.2 * (i as f32 * 0.3).sin())
465 .collect();
466
467 let f32_dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
469
470 let qa = Int4Vector::from_f32(&a);
471 let qb = Int4Vector::from_f32(&b);
472 let int4_dot = qa.dot_product(&qb);
473
474 let rel_error = (f32_dot - int4_dot).abs() / f32_dot.abs().max(1.0);
477 assert!(
478 rel_error < 0.15,
479 "INT4 dot product relative error too large: f32={f32_dot}, int4={int4_dot}, rel_error={rel_error}"
480 );
481 }
482
483 #[cfg(target_arch = "aarch64")]
484 #[test]
485 fn test_packed_scalar_matches_neon_exact() {
486 for dim in [1usize, 3, 31, 127, 383, 384] {
491 let a_f32 = generate_vector(dim, 500 + dim as u64);
492 let b_f32 = generate_vector(dim, 600 + dim as u64);
493 let qa = Int4Vector::from_f32(&a_f32);
494 let qb = Int4Vector::from_f32(&b_f32);
495
496 let scalar_result = dot_product_int4_packed_scalar(&qa.data, &qb.data, dim);
497 let neon_result = unsafe { dot_product_int4_neon_unrolled(&qa.data, &qb.data, dim) };
500
501 assert_eq!(
502 scalar_result, neon_result,
503 "packed_scalar vs NEON integer mismatch at dim={dim}: scalar={scalar_result:?}, neon={neon_result:?}"
504 );
505 }
506 }
507
508 #[cfg(target_arch = "aarch64")]
509 #[test]
510 fn test_int4_neon_matches_dequantized_scalar() {
511 for dim in [1, 2, 31, 64, 127, 384, 768] {
512 let a = generate_vector(dim, 501);
513 let b = generate_vector(dim, 777);
514 let qa = Int4Vector::from_f32(&a);
515 let qb = Int4Vector::from_f32(&b);
516
517 let a_deq = qa.to_f32();
518 let b_deq = qb.to_f32();
519 let expected: f32 = a_deq.iter().zip(b_deq.iter()).map(|(&x, &y)| x * y).sum();
520 let got = qa.dot_product(&qb);
521
522 assert!(
523 (expected - got).abs() < 1e-4,
524 "INT4 NEON mismatch for dim={dim}: expected={expected}, got={got}"
525 );
526 }
527 }
528
529 #[test]
530 fn test_int4_cosine_similarity() {
531 let a = generate_vector(384, 301);
532 let b = generate_vector(384, 302);
533
534 let qa = Int4Vector::from_f32(&a);
535 let qb = Int4Vector::from_f32(&b);
536 let int4_cos = qa.cosine_similarity(&qb);
537
538 let dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
540 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
541 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
542 let f32_cos = dot / (norm_a * norm_b);
543
544 assert!(
545 (f32_cos - int4_cos).abs() < 0.1,
546 "INT4 cosine too far from f32: f32={f32_cos}, int4={int4_cos}"
547 );
548 }
549
550 #[test]
551 fn test_int4_memory_savings() {
552 let v = generate_vector(384, 999);
553 let q = Int4Vector::from_f32(&v);
554
555 assert_eq!(q.data.len(), 192);
558 assert_eq!(v.len() * 4, 1536);
559 }
560
561 #[test]
562 fn test_int4_nan_inf_handling() {
563 let v = vec![
564 1.0,
565 f32::NAN,
566 f32::INFINITY,
567 f32::NEG_INFINITY,
568 -1.0,
569 0.5,
570 0.0,
571 -0.3,
572 ];
573 let q = Int4Vector::from_f32(&v);
574 let deq = q.to_f32();
575 assert_eq!(deq.len(), 8);
576 for &val in &deq {
580 assert!(val.is_finite(), "Dequantized value should be finite");
581 }
582 }
583
584 #[test]
587 fn test_int4_to_f32_short_data_returns_empty() {
588 let q = Int4Vector {
590 dims: 128,
591 data: vec![0xFFu8; 4],
592 params: Int4Params {
593 scale: 7.5,
594 max_abs: 1.0,
595 },
596 norm: 1.0,
597 };
598 let result = q.to_f32();
599 assert!(
600 result.is_empty(),
601 "to_f32 on malformed Int4Vector must return empty Vec"
602 );
603 }
604
605 #[test]
606 fn test_int4_to_f32_exact_length_works() {
607 let v: Vec<f32> = (0..128).map(|i| (i as f32) / 64.0 - 1.0).collect();
609 let q = Int4Vector::from_f32(&v);
610 let deq = q.to_f32();
611 assert_eq!(deq.len(), 128);
612 for &val in &deq {
613 assert!(val.is_finite());
614 }
615 }
616}