1use scirs2_core::parallel_ops::*;
15use torsh_core::error::{Result as TorshResult, TorshError};
16
17pub fn quantize_per_tensor_affine_simd(
22 input: &[f32],
23 scale: f32,
24 zero_point: i32,
25 output: &mut [f32],
26) -> TorshResult<()> {
27 quantize_per_tensor_affine_simd_range(input, scale, zero_point, -128, 127, output)
28}
29
30pub fn quantize_per_tensor_affine_simd_range(
32 input: &[f32],
33 scale: f32,
34 zero_point: i32,
35 qmin: i32,
36 qmax: i32,
37 output: &mut [f32],
38) -> TorshResult<()> {
39 if input.len() != output.len() {
40 return Err(TorshError::InvalidArgument(
41 "Input and output length mismatch".to_string(),
42 ));
43 }
44
45 if scale <= 0.0 {
46 return Err(TorshError::InvalidArgument(
47 "Scale must be positive".to_string(),
48 ));
49 }
50
51 if qmin >= qmax {
52 return Err(TorshError::InvalidArgument(
53 "qmin must be less than qmax".to_string(),
54 ));
55 }
56
57 let inv_scale = 1.0 / scale;
58 let zero_point_f32 = zero_point as f32;
59 let (qmin_f, qmax_f) = (qmin as f32, qmax as f32);
60
61 input
63 .par_iter()
64 .zip(output.par_iter_mut())
65 .for_each(|(&x, out)| {
66 let quantized = (x * inv_scale).round() + zero_point_f32;
67 *out = quantized.clamp(qmin_f, qmax_f);
68 });
69
70 Ok(())
71}
72
73pub fn dequantize_per_tensor_affine_simd(
75 input: &[f32],
76 scale: f32,
77 zero_point: i32,
78 output: &mut [f32],
79) -> TorshResult<()> {
80 if input.len() != output.len() {
81 return Err(TorshError::InvalidArgument(
82 "Input and output length mismatch".to_string(),
83 ));
84 }
85
86 if scale <= 0.0 {
87 return Err(TorshError::InvalidArgument(
88 "Scale must be positive".to_string(),
89 ));
90 }
91
92 let zero_point_f32 = zero_point as f32;
93
94 input
96 .par_iter()
97 .zip(output.par_iter_mut())
98 .for_each(|(&x, out)| {
99 *out = (x - zero_point_f32) * scale;
100 });
101
102 Ok(())
103}
104
105pub fn find_min_max_simd(data: &[f32]) -> TorshResult<(f32, f32)> {
107 if data.is_empty() {
108 return Err(TorshError::InvalidArgument(
109 "Cannot find min/max of empty array".to_string(),
110 ));
111 }
112
113 const CHUNK_SIZE: usize = 1024; let (min_val, max_val) = if data.len() > CHUNK_SIZE {
116 data.par_chunks(CHUNK_SIZE)
118 .map(|chunk| {
119 let mut local_min = f32::INFINITY;
120 let mut local_max = f32::NEG_INFINITY;
121 for &val in chunk {
122 local_min = local_min.min(val);
123 local_max = local_max.max(val);
124 }
125 (local_min, local_max)
126 })
127 .reduce(
128 || (f32::INFINITY, f32::NEG_INFINITY),
129 |(min1, max1), (min2, max2)| (min1.min(min2), max1.max(max2)),
130 )
131 } else {
132 let mut min_val = f32::INFINITY;
134 let mut max_val = f32::NEG_INFINITY;
135 for &val in data {
136 min_val = min_val.min(val);
137 max_val = max_val.max(val);
138 }
139 (min_val, max_val)
140 };
141
142 Ok((min_val, max_val))
143}
144
145pub fn quantize_per_channel_simd(
147 input: &[f32],
148 scales: &[f32],
149 zero_points: &[i32],
150 channel_size: usize,
151 output: &mut [f32],
152) -> TorshResult<()> {
153 if input.len() != output.len() {
154 return Err(TorshError::InvalidArgument(
155 "Input and output length mismatch".to_string(),
156 ));
157 }
158
159 let num_channels = scales.len();
160 if num_channels != zero_points.len() {
161 return Err(TorshError::InvalidArgument(
162 "Scales and zero_points length mismatch".to_string(),
163 ));
164 }
165
166 if input.len() != num_channels * channel_size {
167 return Err(TorshError::InvalidArgument(
168 "Input size does not match channel configuration".to_string(),
169 ));
170 }
171
172 for (ch, (&scale, &zero_point)) in scales.iter().zip(zero_points.iter()).enumerate() {
174 if scale <= 0.0 {
175 return Err(TorshError::InvalidArgument(format!(
176 "Scale for channel {} must be positive",
177 ch
178 )));
179 }
180
181 let channel_start = ch * channel_size;
182 let channel_end = channel_start + channel_size;
183
184 let input_slice = &input[channel_start..channel_end];
185 let output_slice = &mut output[channel_start..channel_end];
186
187 quantize_per_tensor_affine_simd(input_slice, scale, zero_point, output_slice)?;
188 }
189
190 Ok(())
191}
192
193pub fn quantize_batch_consistent_simd(
195 tensors: &[&[f32]],
196 scale: f32,
197 zero_point: i32,
198 outputs: &mut [&mut [f32]],
199) -> TorshResult<()> {
200 if tensors.len() != outputs.len() {
201 return Err(TorshError::InvalidArgument(
202 "Number of input tensors must match output tensors".to_string(),
203 ));
204 }
205
206 tensors
208 .par_iter()
209 .zip(outputs.par_iter_mut())
210 .try_for_each(|(input, output)| {
211 quantize_per_tensor_affine_simd(input, scale, zero_point, output)
212 })?;
213
214 Ok(())
215}
216
217pub fn quantize_to_int8_simd(
219 input: &[f32],
220 scale: f32,
221 zero_point: i32,
222 output: &mut [i8],
223) -> TorshResult<()> {
224 if input.len() != output.len() {
225 return Err(TorshError::InvalidArgument(
226 "Input and output length mismatch".to_string(),
227 ));
228 }
229
230 if scale <= 0.0 {
231 return Err(TorshError::InvalidArgument(
232 "Scale must be positive".to_string(),
233 ));
234 }
235
236 let inv_scale = 1.0 / scale;
237 let zero_point_f32 = zero_point as f32;
238
239 input
241 .par_iter()
242 .zip(output.par_iter_mut())
243 .for_each(|(&x, out)| {
244 let quantized = (x * inv_scale).round() + zero_point_f32;
245 *out = quantized.clamp(-128.0, 127.0) as i8;
246 });
247
248 Ok(())
249}
250
251pub fn calculate_tensor_stats_simd(data: &[f32]) -> TorshResult<TensorStats> {
253 if data.is_empty() {
254 return Err(TorshError::InvalidArgument(
255 "Cannot calculate stats of empty tensor".to_string(),
256 ));
257 }
258
259 let (min_val, max_val) = find_min_max_simd(data)?;
260
261 let sum: f64 = data.par_iter().map(|&x| x as f64).sum();
263 let mean = sum / data.len() as f64;
264
265 let variance_sum: f64 = data
267 .par_iter()
268 .map(|&x| {
269 let diff = x as f64 - mean;
270 diff * diff
271 })
272 .sum();
273 let variance = variance_sum / data.len() as f64;
274 let std_dev = variance.sqrt();
275
276 Ok(TensorStats {
277 min: min_val,
278 max: max_val,
279 mean: mean as f32,
280 std_dev: std_dev as f32,
281 variance: variance as f32,
282 })
283}
284
285#[derive(Debug, Clone)]
287pub struct TensorStats {
288 pub min: f32,
289 pub max: f32,
290 pub mean: f32,
291 pub std_dev: f32,
292 pub variance: f32,
293}
294
295pub fn is_simd_available() -> bool {
297 cfg!(any(
299 target_feature = "avx512f",
300 target_feature = "avx2",
301 target_feature = "avx",
302 target_feature = "sse2",
303 target_feature = "neon" ))
305}
306
307pub fn get_simd_width() -> usize {
309 if cfg!(target_feature = "avx512f") {
312 16 } else if cfg!(target_feature = "avx2") {
314 8 } else if cfg!(any(target_feature = "sse2", target_feature = "neon")) {
316 4 } else {
318 1 }
320}
321
322#[cfg(target_arch = "aarch64")]
324pub fn quantize_neon_optimized(
325 input: &[f32],
326 scale: f32,
327 zero_point: i32,
328 output: &mut [f32],
329) -> TorshResult<()> {
330 if input.len() != output.len() {
331 return Err(TorshError::InvalidArgument(
332 "Input and output length mismatch".to_string(),
333 ));
334 }
335
336 if scale <= 0.0 {
337 return Err(TorshError::InvalidArgument(
338 "Scale must be positive".to_string(),
339 ));
340 }
341
342 let inv_scale = 1.0 / scale;
343 let zero_point_f32 = zero_point as f32;
344
345 const NEON_WIDTH: usize = 4;
347 let chunks = input.len() / NEON_WIDTH;
348
349 for i in 0..chunks {
351 let start = i * NEON_WIDTH;
352 let end = start + NEON_WIDTH;
353
354 for (&inp, out) in input[start..end].iter().zip(output[start..end].iter_mut()) {
356 let quantized = (inp * inv_scale).round() + zero_point_f32;
357 *out = quantized.clamp(-128.0, 127.0);
358 }
359 }
360
361 let remainder_start = chunks * NEON_WIDTH;
363 for (&inp, out) in input[remainder_start..]
364 .iter()
365 .zip(output[remainder_start..].iter_mut())
366 {
367 let quantized = (inp * inv_scale).round() + zero_point_f32;
368 *out = quantized.clamp(-128.0, 127.0);
369 }
370
371 Ok(())
372}
373
374#[cfg(target_arch = "aarch64")]
376pub fn find_min_max_neon(data: &[f32]) -> TorshResult<(f32, f32)> {
377 if data.is_empty() {
378 return Err(TorshError::InvalidArgument(
379 "Cannot find min/max of empty array".to_string(),
380 ));
381 }
382
383 const NEON_WIDTH: usize = 4;
384 let chunks = data.len() / NEON_WIDTH;
385
386 let mut min_val = f32::INFINITY;
387 let mut max_val = f32::NEG_INFINITY;
388
389 for i in 0..chunks {
391 let start = i * NEON_WIDTH;
392 let end = start + NEON_WIDTH;
393
394 for &val in &data[start..end] {
396 min_val = min_val.min(val);
397 max_val = max_val.max(val);
398 }
399 }
400
401 let remainder_start = chunks * NEON_WIDTH;
403 for &val in &data[remainder_start..] {
404 min_val = min_val.min(val);
405 max_val = max_val.max(val);
406 }
407
408 Ok((min_val, max_val))
409}
410
411pub fn quantize_mobile_optimized(
413 input: &[f32],
414 scale: f32,
415 zero_point: i32,
416 output: &mut [i8],
417 use_reduced_precision: bool,
418) -> TorshResult<()> {
419 if input.len() != output.len() {
420 return Err(TorshError::InvalidArgument(
421 "Input and output length mismatch".to_string(),
422 ));
423 }
424
425 if scale <= 0.0 {
426 return Err(TorshError::InvalidArgument(
427 "Scale must be positive".to_string(),
428 ));
429 }
430
431 let inv_scale = if use_reduced_precision {
432 1.0 / scale
434 } else {
435 1.0 / scale
436 };
437
438 let zero_point_f32 = zero_point as f32;
439
440 const MOBILE_CHUNK_SIZE: usize = 256;
442
443 if input.len() > MOBILE_CHUNK_SIZE {
444 input
446 .chunks(MOBILE_CHUNK_SIZE)
447 .zip(output.chunks_mut(MOBILE_CHUNK_SIZE))
448 .for_each(|(input_chunk, output_chunk)| {
449 for (&x, out) in input_chunk.iter().zip(output_chunk.iter_mut()) {
450 let quantized = if use_reduced_precision {
451 (x * inv_scale + 0.5).floor() + zero_point_f32
453 } else {
454 (x * inv_scale).round() + zero_point_f32
455 };
456 *out = quantized.clamp(-128.0, 127.0) as i8;
457 }
458 });
459 } else {
460 for (&x, out) in input.iter().zip(output.iter_mut()) {
462 let quantized = (x * inv_scale).round() + zero_point_f32;
463 *out = quantized.clamp(-128.0, 127.0) as i8;
464 }
465 }
466
467 Ok(())
468}
469
470pub fn get_mobile_optimization_hints() -> MobileOptimizationHints {
472 MobileOptimizationHints {
473 prefer_int8: true,
474 use_reduced_precision: cfg!(target_os = "android") || cfg!(target_os = "ios"),
475 optimal_chunk_size: if cfg!(target_arch = "aarch64") {
476 256
477 } else {
478 512
479 },
480 enable_fast_math: true,
481 prefer_sequential: false, }
483}
484
485#[derive(Debug, Clone)]
487pub struct MobileOptimizationHints {
488 pub prefer_int8: bool,
489 pub use_reduced_precision: bool,
490 pub optimal_chunk_size: usize,
491 pub enable_fast_math: bool,
492 pub prefer_sequential: bool,
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use approx::assert_relative_eq;
499
500 #[test]
501 fn test_quantize_per_tensor_affine_simd() {
502 let input = vec![1.0, 2.0, 3.0, 4.0];
503 let mut output = vec![0.0; 4];
504
505 quantize_per_tensor_affine_simd(&input, 0.1, 0, &mut output).unwrap();
506
507 assert_relative_eq!(output[0], 10.0, epsilon = 1e-6);
508 assert_relative_eq!(output[1], 20.0, epsilon = 1e-6);
509 assert_relative_eq!(output[2], 30.0, epsilon = 1e-6);
510 assert_relative_eq!(output[3], 40.0, epsilon = 1e-6);
511 }
512
513 #[test]
514 fn test_dequantize_per_tensor_affine_simd() {
515 let input = vec![10.0, 20.0, 30.0, 40.0];
516 let mut output = vec![0.0; 4];
517
518 dequantize_per_tensor_affine_simd(&input, 0.1, 0, &mut output).unwrap();
519
520 assert_relative_eq!(output[0], 1.0, epsilon = 1e-6);
521 assert_relative_eq!(output[1], 2.0, epsilon = 1e-6);
522 assert_relative_eq!(output[2], 3.0, epsilon = 1e-6);
523 assert_relative_eq!(output[3], 4.0, epsilon = 1e-6);
524 }
525
526 #[test]
527 fn test_find_min_max_simd() {
528 let data = vec![-1.5, 0.0, 2.3, -0.8, 4.7, 1.2];
529 let (min_val, max_val) = find_min_max_simd(&data).unwrap();
530
531 assert_relative_eq!(min_val, -1.5, epsilon = 1e-6);
532 assert_relative_eq!(max_val, 4.7, epsilon = 1e-6);
533 }
534
535 #[test]
536 fn test_calculate_tensor_stats_simd() {
537 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
538 let stats = calculate_tensor_stats_simd(&data).unwrap();
539
540 assert_relative_eq!(stats.min, 1.0, epsilon = 1e-6);
541 assert_relative_eq!(stats.max, 5.0, epsilon = 1e-6);
542 assert_relative_eq!(stats.mean, 3.0, epsilon = 1e-6);
543 assert_relative_eq!(stats.std_dev, (2.0f64).sqrt() as f32, epsilon = 1e-4);
544 }
545
546 #[test]
547 fn test_quantize_to_int8_simd() {
548 let input = vec![1.0, 2.0, 3.0, 4.0];
549 let mut output = vec![0i8; 4];
550
551 quantize_to_int8_simd(&input, 0.1, 0, &mut output).unwrap();
552
553 assert_eq!(output[0], 10i8);
554 assert_eq!(output[1], 20i8);
555 assert_eq!(output[2], 30i8);
556 assert_eq!(output[3], 40i8);
557 }
558
559 #[test]
560 fn test_error_cases() {
561 let input = vec![1.0, 2.0];
562 let mut output = vec![0.0; 3]; let result = quantize_per_tensor_affine_simd(&input, 0.1, 0, &mut output);
565 assert!(result.is_err());
566
567 let mut output_correct = vec![0.0; 2];
568 let result = quantize_per_tensor_affine_simd(&input, -0.1, 0, &mut output_correct);
569 assert!(result.is_err());
570
571 let empty_data: Vec<f32> = vec![];
572 let result = find_min_max_simd(&empty_data);
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn test_simd_availability() {
578 let available = is_simd_available();
579 let width = get_simd_width();
580
581 assert!(width >= 1); if available {
587 assert!(width > 1); }
589 }
590
591 #[test]
592 fn test_mobile_optimized_quantization() {
593 let input = vec![1.0, 2.0, 3.0, 4.0, -1.0, -2.0];
594 let mut output = vec![0i8; 6];
595
596 quantize_mobile_optimized(&input, 0.1, 0, &mut output, false).unwrap();
597
598 assert_eq!(output[0], 10i8);
599 assert_eq!(output[1], 20i8);
600 assert_eq!(output[2], 30i8);
601 assert_eq!(output[3], 40i8);
602 assert_eq!(output[4], -10i8);
603 assert_eq!(output[5], -20i8);
604 }
605
606 #[test]
607 fn test_mobile_optimized_quantization_reduced_precision() {
608 let input = vec![1.0, 2.0, 3.0, 4.0];
609 let mut output = vec![0i8; 4];
610
611 quantize_mobile_optimized(&input, 0.1, 0, &mut output, true).unwrap();
613
614 assert!((output[0] as f32 - 10.0).abs() <= 1.0);
616 assert!((output[1] as f32 - 20.0).abs() <= 1.0);
617 }
618
619 #[cfg(target_arch = "aarch64")]
620 #[test]
621 fn test_neon_quantization() {
622 let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
623 let mut output = vec![0.0; 8];
624
625 quantize_neon_optimized(&input, 0.1, 0, &mut output).unwrap();
626
627 assert_relative_eq!(output[0], 10.0, epsilon = 1e-6);
628 assert_relative_eq!(output[1], 20.0, epsilon = 1e-6);
629 assert_relative_eq!(output[7], 80.0, epsilon = 1e-6);
630 }
631
632 #[cfg(target_arch = "aarch64")]
633 #[test]
634 fn test_neon_min_max() {
635 let data = vec![-1.5, 0.0, 2.3, -0.8, 4.7, 1.2, 9.5, -2.1];
636 let (min_val, max_val) = find_min_max_neon(&data).unwrap();
637
638 assert_relative_eq!(min_val, -2.1, epsilon = 1e-6);
639 assert_relative_eq!(max_val, 9.5, epsilon = 1e-6);
640 }
641
642 #[test]
643 fn test_mobile_optimization_hints() {
644 let hints = get_mobile_optimization_hints();
645
646 assert!(hints.prefer_int8); assert!(hints.optimal_chunk_size > 0); assert_eq!(hints.prefer_sequential, false); }
650}