/home/noah/src/realizar/src/quantize/activation.rs
Line | Count | Source |
1 | | //! Fused activation functions for quantized inference (PMAT-802) |
2 | | //! |
3 | | //! Implements fused operations combining normalization with quantization: |
4 | | //! - `quantize_rmsnorm_q8_0` - RMSNorm with Q8_0 quantization |
5 | | //! - `quantize_rmsnorm_q8_0_into` - Zero-allocation variant |
6 | | //! - `fused_rmsnorm_q4_0_matmul` - RMSNorm + matmul fusion |
7 | | //! - `fused_rmsnorm_ffn_up_gate` - RMSNorm + FFN up/gate fusion |
8 | | //! - `fused_swiglu_simd` - SIMD-accelerated SwiGLU activation |
9 | | //! - `softmax_simd` - SIMD-accelerated softmax |
10 | | |
11 | | use crate::error::{RealizarError, Result}; |
12 | | use super::fused_q4_0_q8_0_dot_simd; |
13 | | |
14 | | // ============================================================================ |
15 | | // Key insight: llama.cpp quantizes activations to Q8_0 and uses integer |
16 | | // multiply-accumulate (maddubs_epi16), which is 4-5x faster than f32 FMA. |
17 | | |
18 | | /// Fused RMSNorm + Q8_0 quantization |
19 | | /// |
20 | | /// Computes RMSNorm and quantizes in a single pass: |
21 | | /// normalized[i] = x[i] / sqrt(mean(x^2) + eps) * weight[i] |
22 | | /// Then quantizes to Q8_0 format. |
23 | | /// |
24 | | /// This avoids allocating an intermediate normalized vector. |
25 | | #[inline] |
26 | 22 | pub fn quantize_rmsnorm_q8_0(input: &[f32], norm_weight: &[f32], eps: f32) -> (Vec<f32>, Vec<i8>) { |
27 | | #[cfg(target_arch = "x86_64")] |
28 | | { |
29 | 22 | if is_x86_feature_detected!("avx2") { |
30 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
31 | 22 | return unsafe { quantize_rmsnorm_q8_0_avx2(input, norm_weight, eps) }; |
32 | 0 | } |
33 | | } |
34 | 0 | quantize_rmsnorm_q8_0_scalar(input, norm_weight, eps) |
35 | 22 | } |
36 | | |
37 | | /// Scalar implementation of fused RMSNorm + Q8_0 quantization |
38 | | /// |
39 | | /// This is exposed as `pub(crate)` for direct testing. The production code |
40 | | /// uses the public `quantize_rmsnorm_q8_0` wrapper which dispatches to AVX2 |
41 | | /// when available. |
42 | 0 | pub(crate) fn quantize_rmsnorm_q8_0_scalar( |
43 | 0 | input: &[f32], |
44 | 0 | norm_weight: &[f32], |
45 | 0 | eps: f32, |
46 | 0 | ) -> (Vec<f32>, Vec<i8>) { |
47 | 0 | let hidden_dim = input.len(); |
48 | 0 | debug_assert_eq!(hidden_dim, norm_weight.len()); |
49 | | |
50 | | // Compute sum of squares for RMSNorm |
51 | 0 | let sum_sq: f32 = input.iter().map(|x| x * x).sum(); |
52 | 0 | let mean_sq = sum_sq / hidden_dim as f32; |
53 | 0 | let inv_rms = 1.0 / (mean_sq + eps).sqrt(); |
54 | | |
55 | | // Now quantize the normalized values directly |
56 | 0 | let num_blocks = hidden_dim.div_ceil(32); |
57 | 0 | let mut scales = Vec::with_capacity(num_blocks); |
58 | 0 | let mut quants = Vec::with_capacity(num_blocks * 32); |
59 | | |
60 | 0 | for block_idx in 0..num_blocks { |
61 | 0 | let start = block_idx * 32; |
62 | 0 | let end = (start + 32).min(hidden_dim); |
63 | | |
64 | | // Find max absolute value of normalized values for this block |
65 | 0 | let mut max_abs = 0.0f32; |
66 | 0 | for i in start..end { |
67 | | // Fused: x[i] * inv_rms * weight[i] |
68 | 0 | let normalized = input[i] * inv_rms * norm_weight[i]; |
69 | 0 | let abs = normalized.abs(); |
70 | 0 | if abs > max_abs { |
71 | 0 | max_abs = abs; |
72 | 0 | } |
73 | | } |
74 | | |
75 | | // Compute scale |
76 | 0 | let scale = if max_abs > 1e-10 { |
77 | 0 | max_abs / 127.0 |
78 | | } else { |
79 | 0 | 1.0 / 127.0 |
80 | | }; |
81 | 0 | let inv_scale = 1.0 / scale; |
82 | 0 | scales.push(scale); |
83 | | |
84 | | // Quantize normalized values |
85 | 0 | for i in start..end { |
86 | 0 | let normalized = input[i] * inv_rms * norm_weight[i]; |
87 | 0 | let q = (normalized * inv_scale).round(); |
88 | 0 | quants.push(q.clamp(-128.0, 127.0) as i8); |
89 | 0 | } |
90 | | // Pad to 32 if partial block |
91 | 0 | for _ in end..(start + 32) { |
92 | 0 | quants.push(0i8); |
93 | 0 | } |
94 | | } |
95 | | |
96 | 0 | (scales, quants) |
97 | 0 | } |
98 | | |
99 | | /// AVX2-accelerated fused RMSNorm + Q8_0 quantization |
100 | | /// |
101 | | /// Processes 8 floats at a time using SIMD for: |
102 | | /// - Sum of squares computation |
103 | | /// - Max abs finding per block |
104 | | /// - Normalization and quantization |
105 | | #[cfg(target_arch = "x86_64")] |
106 | | #[target_feature(enable = "avx2")] |
107 | | #[inline] |
108 | 22 | unsafe fn quantize_rmsnorm_q8_0_avx2( |
109 | 22 | input: &[f32], |
110 | 22 | norm_weight: &[f32], |
111 | 22 | eps: f32, |
112 | 22 | ) -> (Vec<f32>, Vec<i8>) { |
113 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
114 | | unsafe { |
115 | | use std::arch::x86_64::{ |
116 | | _mm256_add_ps, _mm256_and_ps, _mm256_andnot_ps, _mm256_castps256_ps128, |
117 | | _mm256_castsi256_ps, _mm256_castsi256_si128, _mm256_cvtps_epi32, _mm256_extractf128_ps, |
118 | | _mm256_extracti128_si256, _mm256_floor_ps, _mm256_fmadd_ps, _mm256_loadu_ps, |
119 | | _mm256_max_ps, _mm256_min_ps, _mm256_mul_ps, _mm256_or_ps, _mm256_set1_epi32, |
120 | | _mm256_set1_ps, _mm256_setzero_ps, _mm_add_ps, _mm_cvtss_f32, _mm_hadd_ps, _mm_max_ps, |
121 | | _mm_movehl_ps, _mm_packs_epi16, _mm_packs_epi32, _mm_shuffle_ps, _mm_storel_epi64, |
122 | | }; |
123 | | |
124 | 22 | let hidden_dim = input.len(); |
125 | 22 | debug_assert_eq!(hidden_dim, norm_weight.len()); |
126 | | |
127 | | // SIMD sum of squares |
128 | 22 | let mut sum_sq_vec = _mm256_setzero_ps(); |
129 | 22 | let mut i = 0; |
130 | | |
131 | | // Process 8 floats at a time |
132 | 354 | while i + 8 <= hidden_dim { |
133 | 332 | let v = _mm256_loadu_ps(input.as_ptr().add(i)); |
134 | 332 | sum_sq_vec = _mm256_fmadd_ps(v, v, sum_sq_vec); |
135 | 332 | i += 8; |
136 | 332 | } |
137 | | |
138 | | // Horizontal sum |
139 | 22 | let hi = _mm256_extractf128_ps(sum_sq_vec, 1); |
140 | 22 | let lo = _mm256_castps256_ps128(sum_sq_vec); |
141 | 22 | let sum128 = _mm_add_ps(lo, hi); |
142 | 22 | let sum64 = _mm_hadd_ps(sum128, sum128); |
143 | 22 | let sum32 = _mm_hadd_ps(sum64, sum64); |
144 | 22 | let mut sum_sq = _mm_cvtss_f32(sum32); |
145 | | |
146 | | // Handle remaining elements |
147 | 22 | while i < hidden_dim { |
148 | 0 | sum_sq += input[i] * input[i]; |
149 | 0 | i += 1; |
150 | 0 | } |
151 | | |
152 | 22 | let mean_sq = sum_sq / hidden_dim as f32; |
153 | 22 | let inv_rms = 1.0 / (mean_sq + eps).sqrt(); |
154 | 22 | let inv_rms_vec = _mm256_set1_ps(inv_rms); |
155 | | |
156 | | // Quantize with SIMD |
157 | 22 | let num_blocks = hidden_dim.div_ceil(32); |
158 | 22 | let mut scales = Vec::with_capacity(num_blocks); |
159 | 22 | let mut quants = vec![0i8; num_blocks * 32]; |
160 | | |
161 | 22 | let abs_mask = _mm256_castsi256_ps(_mm256_set1_epi32(0x7FFFFFFF_u32 as i32)); |
162 | 22 | let round_const = _mm256_set1_ps(0.5); |
163 | 22 | let clamp_min = _mm256_set1_ps(-128.0); |
164 | 22 | let clamp_max = _mm256_set1_ps(127.0); |
165 | | |
166 | 84 | for block_idx in 0..num_blocks22 { |
167 | 84 | let start = block_idx * 32; |
168 | 84 | let block_end = (start + 32).min(hidden_dim); |
169 | 84 | let valid_len = block_end - start; |
170 | | |
171 | | // Find max abs in this block using SIMD |
172 | 84 | let mut max_vec = _mm256_setzero_ps(); |
173 | 84 | let mut j = 0; |
174 | 416 | while j + 8 <= valid_len { |
175 | 332 | let idx = start + j; |
176 | 332 | let inp = _mm256_loadu_ps(input.as_ptr().add(idx)); |
177 | 332 | let wgt = _mm256_loadu_ps(norm_weight.as_ptr().add(idx)); |
178 | 332 | let normalized = _mm256_mul_ps(_mm256_mul_ps(inp, inv_rms_vec), wgt); |
179 | 332 | let abs_val = _mm256_and_ps(normalized, abs_mask); |
180 | 332 | max_vec = _mm256_max_ps(max_vec, abs_val); |
181 | 332 | j += 8; |
182 | 332 | } |
183 | | |
184 | | // Horizontal max |
185 | 84 | let max_hi = _mm256_extractf128_ps(max_vec, 1); |
186 | 84 | let max_lo = _mm256_castps256_ps128(max_vec); |
187 | 84 | let max_128 = _mm_max_ps(max_lo, max_hi); |
188 | 84 | let max_64 = _mm_max_ps(max_128, _mm_movehl_ps(max_128, max_128)); |
189 | 84 | let max_32 = _mm_max_ps(max_64, _mm_shuffle_ps(max_64, max_64, 1)); |
190 | 84 | let mut max_abs = _mm_cvtss_f32(max_32); |
191 | | |
192 | | // Handle remaining elements in block |
193 | 84 | while j < valid_len { |
194 | 0 | let normalized = input[start + j] * inv_rms * norm_weight[start + j]; |
195 | 0 | let abs = normalized.abs(); |
196 | 0 | if abs > max_abs { |
197 | 0 | max_abs = abs; |
198 | 0 | } |
199 | 0 | j += 1; |
200 | | } |
201 | | |
202 | | // Compute scale |
203 | 84 | let scale = if max_abs > 1e-10 { |
204 | 84 | max_abs / 127.0 |
205 | | } else { |
206 | 0 | 1.0 / 127.0 |
207 | | }; |
208 | 84 | let inv_scale = 1.0 / scale; |
209 | 84 | let inv_scale_vec = _mm256_set1_ps(inv_scale); |
210 | 84 | scales.push(scale); |
211 | | |
212 | | // Quantize with SIMD |
213 | 84 | let quant_ptr = quants.as_mut_ptr().add(block_idx * 32); |
214 | 84 | let mut k = 0; |
215 | 416 | while k + 8 <= valid_len { |
216 | 332 | let idx = start + k; |
217 | 332 | let inp = _mm256_loadu_ps(input.as_ptr().add(idx)); |
218 | 332 | let wgt = _mm256_loadu_ps(norm_weight.as_ptr().add(idx)); |
219 | 332 | let normalized = _mm256_mul_ps(_mm256_mul_ps(inp, inv_rms_vec), wgt); |
220 | 332 | let scaled = _mm256_mul_ps(normalized, inv_scale_vec); |
221 | 332 | |
222 | 332 | // Round to nearest (add 0.5 and truncate, handle sign) |
223 | 332 | let sign = _mm256_and_ps( |
224 | 332 | scaled, |
225 | 332 | _mm256_castsi256_ps(_mm256_set1_epi32(0x80000000_u32 as i32)), |
226 | 332 | ); |
227 | 332 | let abs_scaled = _mm256_andnot_ps(sign, scaled); |
228 | 332 | let rounded = _mm256_or_ps( |
229 | 332 | _mm256_floor_ps(_mm256_add_ps(abs_scaled, round_const)), |
230 | 332 | sign, |
231 | 332 | ); |
232 | 332 | |
233 | 332 | // Clamp to [-128, 127] |
234 | 332 | let clamped = _mm256_max_ps(clamp_min, _mm256_min_ps(clamp_max, rounded)); |
235 | 332 | |
236 | 332 | // Convert to int32 and extract to i8 |
237 | 332 | let int32 = _mm256_cvtps_epi32(clamped); |
238 | 332 | |
239 | 332 | // Pack i32 -> i16 -> i8 (only need lower 8 values) |
240 | 332 | let lo128 = _mm256_castsi256_si128(int32); |
241 | 332 | let hi128 = _mm256_extracti128_si256(int32, 1); |
242 | 332 | let packed16 = _mm_packs_epi32(lo128, hi128); |
243 | 332 | let packed8 = _mm_packs_epi16(packed16, packed16); |
244 | 332 | |
245 | 332 | // Store 8 i8 values |
246 | 332 | _mm_storel_epi64(quant_ptr.add(k).cast(), packed8); |
247 | 332 | k += 8; |
248 | 332 | } |
249 | | |
250 | | // Handle remaining elements |
251 | 84 | while k < valid_len { |
252 | 0 | let normalized = input[start + k] * inv_rms * norm_weight[start + k]; |
253 | 0 | let q = (normalized * inv_scale).round(); |
254 | 0 | *quant_ptr.add(k) = q.clamp(-128.0, 127.0) as i8; |
255 | 0 | k += 1; |
256 | 0 | } |
257 | | } |
258 | | |
259 | 22 | (scales, quants) |
260 | | } |
261 | 22 | } |
262 | | |
263 | | /// Fused RMSNorm + Q4_0 matmul |
264 | | /// |
265 | | /// Combines RMSNorm normalization with quantized matmul in one operation: |
266 | | /// 1. Computes inv_rms = 1 / sqrt(mean(x^2) + eps) |
267 | | /// 2. Quantizes (x * inv_rms * norm_weight) to Q8_0 |
268 | | /// 3. Performs Q4_0 × Q8_0 integer matmul |
269 | | /// |
270 | | /// This eliminates the intermediate normalized vector allocation. |
271 | | #[allow(clippy::similar_names)] |
272 | 4 | pub fn fused_rmsnorm_q4_0_matmul( |
273 | 4 | input: &[f32], |
274 | 4 | norm_weight: &[f32], |
275 | 4 | eps: f32, |
276 | 4 | weight_data: &[u8], |
277 | 4 | in_dim: usize, |
278 | 4 | out_dim: usize, |
279 | 4 | ) -> Result<Vec<f32>> { |
280 | | use rayon::prelude::*; |
281 | | |
282 | | const Q4_0_BLOCK_BYTES: usize = 18; |
283 | | const Q4_0_BLOCK_SIZE: usize = 32; |
284 | | |
285 | 4 | if input.len() != in_dim { |
286 | 1 | return Err(RealizarError::InvalidShape { |
287 | 1 | reason: format!( |
288 | 1 | "Input length {} doesn't match in_dim {}", |
289 | 1 | input.len(), |
290 | 1 | in_dim |
291 | 1 | ), |
292 | 1 | }); |
293 | 3 | } |
294 | | |
295 | 3 | let blocks_per_row = in_dim.div_ceil(Q4_0_BLOCK_SIZE); |
296 | 3 | let bytes_per_row = blocks_per_row * Q4_0_BLOCK_BYTES; |
297 | | |
298 | 3 | let expected_weight_bytes = out_dim * bytes_per_row; |
299 | 3 | if weight_data.len() < expected_weight_bytes { |
300 | 1 | return Err(RealizarError::InvalidShape { |
301 | 1 | reason: format!( |
302 | 1 | "Q4_0 weight data too small: need {} bytes for {}x{}, have {}", |
303 | 1 | expected_weight_bytes, |
304 | 1 | out_dim, |
305 | 1 | in_dim, |
306 | 1 | weight_data.len() |
307 | 1 | ), |
308 | 1 | }); |
309 | 2 | } |
310 | | |
311 | | // Fused RMSNorm + Q8_0 quantization (single pass, no intermediate allocation) |
312 | 2 | let (q8_scales, q8_quants) = quantize_rmsnorm_q8_0(input, norm_weight, eps); |
313 | | |
314 | | // Parallel matmul with chunking |
315 | | const CHUNK_SIZE: usize = 64; |
316 | 2 | let output: Vec<f32> = (0..out_dim) |
317 | 2 | .into_par_iter() |
318 | 2 | .with_min_len(CHUNK_SIZE) |
319 | 4 | .map2 (|o| { |
320 | 4 | let row_start = o * bytes_per_row; |
321 | 4 | let row_end = row_start + bytes_per_row; |
322 | 4 | let row_data = &weight_data[row_start..row_end]; |
323 | 4 | fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim) |
324 | 4 | }) |
325 | 2 | .collect(); |
326 | | |
327 | 2 | Ok(output) |
328 | 4 | } |
329 | | |
330 | | /// Fused RMSNorm + parallel FFN up/gate projections |
331 | | /// |
332 | | /// For SwiGLU models, FFN has two parallel matmuls (up and gate) that share |
333 | | /// the same normalized input. This function: |
334 | | /// 1. Computes inv_rms once |
335 | | /// 2. Quantizes normalized input to Q8_0 once |
336 | | /// 3. Runs both up and gate matmuls in parallel |
337 | | /// |
338 | | /// Eliminates: 1 RMSNorm pass, 1 intermediate allocation, 1 Q8_0 quantization |
339 | | #[allow(clippy::similar_names)] |
340 | | #[allow(clippy::too_many_arguments)] |
341 | 6 | pub fn fused_rmsnorm_ffn_up_gate( |
342 | 6 | input: &[f32], |
343 | 6 | norm_weight: &[f32], |
344 | 6 | eps: f32, |
345 | 6 | up_weight_data: &[u8], |
346 | 6 | gate_weight_data: &[u8], |
347 | 6 | in_dim: usize, |
348 | 6 | out_dim: usize, |
349 | 6 | ) -> Result<(Vec<f32>, Vec<f32>)> { |
350 | | use rayon::prelude::*; |
351 | | |
352 | | const Q4_0_BLOCK_BYTES: usize = 18; |
353 | | const Q4_0_BLOCK_SIZE: usize = 32; |
354 | | |
355 | 6 | if input.len() != in_dim { |
356 | 1 | return Err(RealizarError::InvalidShape { |
357 | 1 | reason: format!( |
358 | 1 | "Input length {} doesn't match in_dim {}", |
359 | 1 | input.len(), |
360 | 1 | in_dim |
361 | 1 | ), |
362 | 1 | }); |
363 | 5 | } |
364 | | |
365 | 5 | let blocks_per_row = in_dim.div_ceil(Q4_0_BLOCK_SIZE); |
366 | 5 | let bytes_per_row = blocks_per_row * Q4_0_BLOCK_BYTES; |
367 | 5 | let expected_weight_bytes = out_dim * bytes_per_row; |
368 | | |
369 | 5 | if up_weight_data.len() < expected_weight_bytes { |
370 | 2 | return Err(RealizarError::InvalidShape { |
371 | 2 | reason: format!( |
372 | 2 | "FFN up weight data too small: need {} bytes, have {}", |
373 | 2 | expected_weight_bytes, |
374 | 2 | up_weight_data.len() |
375 | 2 | ), |
376 | 2 | }); |
377 | 3 | } |
378 | 3 | if gate_weight_data.len() < expected_weight_bytes { |
379 | 1 | return Err(RealizarError::InvalidShape { |
380 | 1 | reason: format!( |
381 | 1 | "FFN gate weight data too small: need {} bytes, have {}", |
382 | 1 | expected_weight_bytes, |
383 | 1 | gate_weight_data.len() |
384 | 1 | ), |
385 | 1 | }); |
386 | 2 | } |
387 | | |
388 | | // Fused RMSNorm + Q8_0 quantization - computed ONCE for both matmuls |
389 | 2 | let (q8_scales, q8_quants) = quantize_rmsnorm_q8_0(input, norm_weight, eps); |
390 | | |
391 | | // Run both matmuls in parallel using rayon::join |
392 | | // Each matmul uses parallel iteration with chunking to reduce overhead |
393 | 2 | let (up_output, gate_output) = rayon::join( |
394 | 2 | || { |
395 | | const CHUNK_SIZE: usize = 64; |
396 | 2 | (0..out_dim) |
397 | 2 | .into_par_iter() |
398 | 2 | .with_min_len(CHUNK_SIZE) |
399 | 65 | .map2 (|o| { |
400 | 65 | let row_start = o * bytes_per_row; |
401 | 65 | let row_end = row_start + bytes_per_row; |
402 | 65 | let row_data = &up_weight_data[row_start..row_end]; |
403 | 65 | fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim) |
404 | 65 | }) |
405 | 2 | .collect::<Vec<f32>>() |
406 | 2 | }, |
407 | 2 | || { |
408 | | const CHUNK_SIZE: usize = 64; |
409 | 2 | (0..out_dim) |
410 | 2 | .into_par_iter() |
411 | 2 | .with_min_len(CHUNK_SIZE) |
412 | 65 | .map2 (|o| { |
413 | 65 | let row_start = o * bytes_per_row; |
414 | 65 | let row_end = row_start + bytes_per_row; |
415 | 65 | let row_data = &gate_weight_data[row_start..row_end]; |
416 | 65 | fused_q4_0_q8_0_dot_simd(row_data, &q8_scales, &q8_quants, in_dim) |
417 | 65 | }) |
418 | 2 | .collect::<Vec<f32>>() |
419 | 2 | }, |
420 | | ); |
421 | | |
422 | 2 | Ok((up_output, gate_output)) |
423 | 6 | } |
424 | | |
425 | | /// Zero-allocation variant of quantize_rmsnorm_q8_0 |
426 | | /// |
427 | | /// Writes results directly into pre-allocated output buffers. |
428 | 3 | pub fn quantize_rmsnorm_q8_0_into( |
429 | 3 | input: &[f32], |
430 | 3 | norm_weight: &[f32], |
431 | 3 | eps: f32, |
432 | 3 | scales: &mut [f32], |
433 | 3 | quants: &mut [i8], |
434 | 3 | ) { |
435 | 3 | let hidden_dim = input.len(); |
436 | 3 | debug_assert_eq!(hidden_dim, norm_weight.len()); |
437 | | |
438 | | // Compute sum of squares for RMSNorm |
439 | 320 | let sum_sq3 : f323 = input3 .iter3 ().map3 (|x| x * x).sum3 (); |
440 | 3 | let mean_sq = sum_sq / hidden_dim as f32; |
441 | 3 | let inv_rms = 1.0 / (mean_sq + eps).sqrt(); |
442 | | |
443 | 3 | let num_blocks = hidden_dim.div_ceil(32); |
444 | | |
445 | 10 | for block_idx in 0..num_blocks3 { |
446 | 10 | let start = block_idx * 32; |
447 | 10 | let end = (start + 32).min(hidden_dim); |
448 | | |
449 | | // Find max absolute value of normalized values for this block |
450 | 10 | let mut max_abs = 0.0f32; |
451 | 320 | for i in start10 ..end10 { |
452 | 320 | let normalized = input[i] * inv_rms * norm_weight[i]; |
453 | 320 | let abs = normalized.abs(); |
454 | 320 | if abs > max_abs { |
455 | 40 | max_abs = abs; |
456 | 280 | } |
457 | | } |
458 | | |
459 | | // Compute scale |
460 | 10 | let scale = if max_abs > 1e-10 { |
461 | 10 | max_abs / 127.0 |
462 | | } else { |
463 | 0 | 1.0 / 127.0 |
464 | | }; |
465 | 10 | let inv_scale = 1.0 / scale; |
466 | 10 | scales[block_idx] = scale; |
467 | | |
468 | | // Quantize normalized values |
469 | 10 | let quant_start = block_idx * 32; |
470 | 320 | for i in start10 ..end10 { |
471 | 320 | let normalized = input[i] * inv_rms * norm_weight[i]; |
472 | 320 | let q = (normalized * inv_scale).round(); |
473 | 320 | quants[quant_start + (i - start)] = q.clamp(-128.0, 127.0) as i8; |
474 | 320 | } |
475 | | // Pad to 32 if partial block |
476 | 10 | for j0 in (end - start)..32 { |
477 | 0 | quants[quant_start + j] = 0i8; |
478 | 0 | } |
479 | | } |
480 | 3 | } |
481 | | |
482 | | /// SIMD-accelerated fused SwiGLU activation: silu(gate) * up |
483 | | /// |
484 | | /// Combines silu activation and element-wise multiply in a single pass |
485 | | /// for better cache locality. Uses AVX2/AVX-512 SIMD where available. |
486 | | /// |
487 | | /// # Arguments |
488 | | /// * `gate` - Gate values, modified in-place to contain result |
489 | | /// * `up` - Up projection values |
490 | 15 | pub fn fused_swiglu_simd(gate: &mut [f32], up: &[f32]) { |
491 | 15 | debug_assert_eq!(gate.len(), up.len()); |
492 | | |
493 | | #[cfg(target_arch = "x86_64")] |
494 | | { |
495 | 15 | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
496 | | // SAFETY: AVX2 and FMA verified at runtime |
497 | 15 | unsafe { |
498 | 15 | fused_swiglu_avx2(gate, up); |
499 | 15 | } |
500 | 15 | return; |
501 | 0 | } |
502 | | } |
503 | | |
504 | | // Scalar fallback |
505 | 0 | fused_swiglu_scalar(gate, up); |
506 | 15 | } |
507 | | |
508 | | /// Scalar fused SwiGLU: silu(gate) * up |
509 | | /// |
510 | | /// Exposed as `pub(crate)` for direct testing on AVX2 machines. |
511 | | #[inline] |
512 | 0 | pub(crate) fn fused_swiglu_scalar(gate: &mut [f32], up: &[f32]) { |
513 | 0 | for (g, &u) in gate.iter_mut().zip(up.iter()) { |
514 | 0 | // silu(x) = x * sigmoid(x) = x / (1 + exp(-x)) |
515 | 0 | let silu_g = *g / (1.0 + (-*g).exp()); |
516 | 0 | *g = silu_g * u; |
517 | 0 | } |
518 | 0 | } |
519 | | |
520 | | /// AVX2 SIMD fused SwiGLU with FMA |
521 | | /// |
522 | | /// Computes silu(gate) * up using: |
523 | | /// - Polynomial approximation for exp(-x) |
524 | | /// - FMA for efficient multiply-add |
525 | | /// - 8-wide AVX2 vectors |
526 | | #[cfg(target_arch = "x86_64")] |
527 | | #[target_feature(enable = "avx2", enable = "fma")] |
528 | | #[inline] |
529 | | #[allow(clippy::many_single_char_names)] |
530 | 15 | unsafe fn fused_swiglu_avx2(gate: &mut [f32], up: &[f32]) { |
531 | | use std::arch::x86_64::{ |
532 | | _mm256_add_epi32, _mm256_add_ps, _mm256_castsi256_ps, _mm256_cvtps_epi32, _mm256_floor_ps, |
533 | | _mm256_fmadd_ps, _mm256_fnmadd_ps, _mm256_loadu_ps, _mm256_max_ps, _mm256_mul_ps, |
534 | | _mm256_rcp_ps, _mm256_set1_epi32, _mm256_set1_ps, _mm256_setzero_ps, _mm256_slli_epi32, |
535 | | _mm256_storeu_ps, _mm256_sub_ps, |
536 | | }; |
537 | | |
538 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
539 | | unsafe { |
540 | 15 | let n = gate.len(); |
541 | 15 | let mut i = 0; |
542 | | |
543 | | // Constants for exp approximation (polynomial coefficients) |
544 | | // Using 5th-degree polynomial approximation for exp(x) on [-87, 0] |
545 | 15 | let one = _mm256_set1_ps(1.0); |
546 | 15 | let ln2_inv = _mm256_set1_ps(1.442_695); // 1/ln(2) |
547 | 15 | let ln2 = _mm256_set1_ps(0.693_147_2); |
548 | 15 | let c0 = _mm256_set1_ps(1.0); |
549 | 15 | let c1 = _mm256_set1_ps(0.693_147_2); // ln(2) |
550 | 15 | let c2 = _mm256_set1_ps(0.240_226_5); // ln(2)^2 / 2! |
551 | 15 | let c3 = _mm256_set1_ps(0.055_504_11); // ln(2)^3 / 3! |
552 | 15 | let c4 = _mm256_set1_ps(0.009_618_13); // ln(2)^4 / 4! |
553 | 15 | let c5 = _mm256_set1_ps(0.001_333_36); // ln(2)^5 / 5! |
554 | 15 | let min_exp = _mm256_set1_ps(-87.0); // Minimum input to avoid underflow |
555 | 15 | let two = _mm256_set1_ps(2.0); // For Newton-Raphson |
556 | | |
557 | | // Process 8 elements at a time |
558 | 93 | while i + 8 <= n { |
559 | 78 | // Load gate and up values |
560 | 78 | let g = _mm256_loadu_ps(gate.as_ptr().add(i)); |
561 | 78 | let u = _mm256_loadu_ps(up.as_ptr().add(i)); |
562 | 78 | |
563 | 78 | // Compute -g for sigmoid |
564 | 78 | let neg_g = _mm256_sub_ps(_mm256_setzero_ps(), g); |
565 | 78 | |
566 | 78 | // Clamp to avoid exp underflow |
567 | 78 | let neg_g_clamped = _mm256_max_ps(neg_g, min_exp); |
568 | 78 | |
569 | 78 | // Fast exp approximation using 2^(x/ln2) = 2^n * 2^f where n=floor, f=frac |
570 | 78 | // n = floor(x * 1/ln2) |
571 | 78 | let xln2 = _mm256_mul_ps(neg_g_clamped, ln2_inv); |
572 | 78 | let n_f = _mm256_floor_ps(xln2); |
573 | 78 | let n_i = _mm256_cvtps_epi32(n_f); |
574 | 78 | |
575 | 78 | // f = x - n * ln2 (fractional part scaled back) |
576 | 78 | let f = _mm256_fnmadd_ps(n_f, ln2, neg_g_clamped); |
577 | 78 | |
578 | 78 | // Horner's method: c0 + f*(c1 + f*(c2 + f*(c3 + f*(c4 + f*c5)))) |
579 | 78 | let p = _mm256_fmadd_ps(f, c5, c4); |
580 | 78 | let p = _mm256_fmadd_ps(f, p, c3); |
581 | 78 | let p = _mm256_fmadd_ps(f, p, c2); |
582 | 78 | let p = _mm256_fmadd_ps(f, p, c1); |
583 | 78 | let p = _mm256_fmadd_ps(f, p, c0); |
584 | 78 | |
585 | 78 | // Scale by 2^n using integer bit manipulation |
586 | 78 | // 2^n = reinterpret((n + 127) << 23) as float |
587 | 78 | let bias = _mm256_set1_epi32(127); |
588 | 78 | let n_biased = _mm256_add_epi32(n_i, bias); |
589 | 78 | let exp_scale = _mm256_slli_epi32::<23>(n_biased); |
590 | 78 | let exp_scale_f = _mm256_castsi256_ps(exp_scale); |
591 | 78 | |
592 | 78 | // exp(-g) = 2^n * p(f) |
593 | 78 | let exp_neg_g = _mm256_mul_ps(p, exp_scale_f); |
594 | 78 | |
595 | 78 | // sigmoid(-(-g)) = 1 / (1 + exp(-g)) |
596 | 78 | // Use fast reciprocal approximation with Newton-Raphson refinement |
597 | 78 | let denom = _mm256_add_ps(one, exp_neg_g); |
598 | 78 | let rcp = _mm256_rcp_ps(denom); // ~12-bit precision |
599 | 78 | // One Newton-Raphson iteration: x' = x * (2 - d*x) |
600 | 78 | let sigmoid = _mm256_mul_ps(rcp, _mm256_fnmadd_ps(denom, rcp, two)); |
601 | 78 | |
602 | 78 | // silu(g) = g * sigmoid(g) |
603 | 78 | let silu_g = _mm256_mul_ps(g, sigmoid); |
604 | 78 | |
605 | 78 | // Result = silu(g) * u |
606 | 78 | let result = _mm256_mul_ps(silu_g, u); |
607 | 78 | |
608 | 78 | // Store result |
609 | 78 | _mm256_storeu_ps(gate.as_mut_ptr().add(i), result); |
610 | 78 | |
611 | 78 | i += 8; |
612 | 78 | } |
613 | | |
614 | | // Handle remainder with scalar code |
615 | 28 | while i < n { |
616 | 13 | let g = gate[i]; |
617 | 13 | let silu_g = g / (1.0 + (-g).exp()); |
618 | 13 | gate[i] = silu_g * up[i]; |
619 | 13 | i += 1; |
620 | 13 | } |
621 | | } |
622 | 15 | } |
623 | | |
624 | | /// SIMD-optimized in-place softmax |
625 | | /// |
626 | | /// Computes softmax(x) = exp(x - max) / sum(exp(x - max)) |
627 | | /// Uses AVX2/AVX-512 for vectorized exp and horizontal operations. |
628 | | /// |
629 | | /// # Arguments |
630 | | /// * `x` - Slice to softmax in-place |
631 | | #[inline] |
632 | 2.80k | pub fn softmax_simd(x: &mut [f32]) { |
633 | 2.80k | if x.is_empty() { |
634 | 2 | return; |
635 | 2.80k | } |
636 | | |
637 | | #[cfg(target_arch = "x86_64")] |
638 | | { |
639 | 2.80k | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
640 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
641 | 2.80k | unsafe { |
642 | 2.80k | softmax_avx2(x); |
643 | 2.80k | } |
644 | 2.80k | return; |
645 | 0 | } |
646 | | } |
647 | | |
648 | | // Scalar fallback |
649 | 0 | softmax_scalar(x); |
650 | 2.80k | } |
651 | | |
652 | | /// Scalar softmax |
653 | | /// |
654 | | /// Exposed as `pub(crate)` for direct testing on AVX2 machines. |
655 | | #[inline] |
656 | 0 | pub(crate) fn softmax_scalar(x: &mut [f32]) { |
657 | | // Find max for numerical stability |
658 | 0 | let max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max); |
659 | | |
660 | | // Compute exp(x - max) and sum |
661 | 0 | let mut sum = 0.0f32; |
662 | 0 | for v in x.iter_mut() { |
663 | 0 | *v = (*v - max).exp(); |
664 | 0 | sum += *v; |
665 | 0 | } |
666 | | |
667 | | // Normalize |
668 | 0 | let inv_sum = 1.0 / sum; |
669 | 0 | for v in x.iter_mut() { |
670 | 0 | *v *= inv_sum; |
671 | 0 | } |
672 | 0 | } |
673 | | |
674 | | /// AVX2 SIMD softmax - only SIMD for max-find and normalization |
675 | | /// (exp() uses libm which is faster than polynomial for short vectors) |
676 | | #[cfg(target_arch = "x86_64")] |
677 | | #[target_feature(enable = "avx2")] |
678 | | #[inline] |
679 | | #[allow(unsafe_op_in_unsafe_fn)] |
680 | 2.80k | unsafe fn softmax_avx2(x: &mut [f32]) { |
681 | | use std::arch::x86_64::{ |
682 | | _mm256_loadu_ps, _mm256_max_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_storeu_ps, |
683 | | }; |
684 | | |
685 | 2.80k | let n = x.len(); |
686 | 2.80k | if n == 0 { |
687 | 0 | return; |
688 | 2.80k | } |
689 | | |
690 | | // ============= Phase 1: Find max (SIMD) ============= |
691 | 2.80k | let mut max_vec = _mm256_set1_ps(f32::NEG_INFINITY); |
692 | 2.80k | let mut i = 0; |
693 | | |
694 | 9.78k | while i + 8 <= n { |
695 | 6.97k | let v = _mm256_loadu_ps(x.as_ptr().add(i)); |
696 | 6.97k | max_vec = _mm256_max_ps(max_vec, v); |
697 | 6.97k | i += 8; |
698 | 6.97k | } |
699 | | |
700 | 2.80k | let mut max_scalar = horizontal_max_avx2(max_vec); |
701 | 9.95k | for j in i2.80k ..n2.80k { |
702 | 9.95k | max_scalar = max_scalar.max(x[j]); |
703 | 9.95k | } |
704 | | |
705 | | // ============= Phase 2: Compute exp(x - max) (scalar libm) ============= |
706 | 2.80k | let mut sum_scalar = 0.0f32; |
707 | 65.7k | for j in 0..n2.80k { |
708 | 65.7k | let exp_v = (x[j] - max_scalar).exp(); |
709 | 65.7k | x[j] = exp_v; |
710 | 65.7k | sum_scalar += exp_v; |
711 | 65.7k | } |
712 | | |
713 | | // ============= Phase 3: Normalize (SIMD) ============= |
714 | 2.80k | let inv_sum = _mm256_set1_ps(1.0 / sum_scalar); |
715 | | |
716 | 2.80k | i = 0; |
717 | 9.78k | while i + 8 <= n { |
718 | 6.97k | let v = _mm256_loadu_ps(x.as_ptr().add(i)); |
719 | 6.97k | let normalized = _mm256_mul_ps(v, inv_sum); |
720 | 6.97k | _mm256_storeu_ps(x.as_mut_ptr().add(i), normalized); |
721 | 6.97k | i += 8; |
722 | 6.97k | } |
723 | | |
724 | 2.80k | let inv_sum_scalar = 1.0 / sum_scalar; |
725 | 9.95k | for j in i2.80k ..n2.80k { |
726 | 9.95k | x[j] *= inv_sum_scalar; |
727 | 9.95k | } |
728 | 2.80k | } |
729 | | |
730 | | /// Fast exp approximation using polynomial (AVX2) |
731 | | #[cfg(target_arch = "x86_64")] |
732 | | #[target_feature(enable = "avx2", enable = "fma")] |
733 | | #[inline] |
734 | | #[allow(unsafe_op_in_unsafe_fn)] |
735 | 0 | unsafe fn fast_exp_avx2( |
736 | 0 | x: std::arch::x86_64::__m256, |
737 | 0 | ln2_inv: std::arch::x86_64::__m256, |
738 | 0 | ln2: std::arch::x86_64::__m256, |
739 | 0 | c0: std::arch::x86_64::__m256, |
740 | 0 | c1: std::arch::x86_64::__m256, |
741 | 0 | c2: std::arch::x86_64::__m256, |
742 | 0 | c3: std::arch::x86_64::__m256, |
743 | 0 | c4: std::arch::x86_64::__m256, |
744 | 0 | c5: std::arch::x86_64::__m256, |
745 | 0 | min_exp: std::arch::x86_64::__m256, |
746 | 0 | ) -> std::arch::x86_64::__m256 { |
747 | | use std::arch::x86_64::{ |
748 | | _mm256_add_epi32, _mm256_castsi256_ps, _mm256_cvtps_epi32, _mm256_floor_ps, |
749 | | _mm256_fmadd_ps, _mm256_fnmadd_ps, _mm256_max_ps, _mm256_mul_ps, _mm256_set1_epi32, |
750 | | _mm256_slli_epi32, |
751 | | }; |
752 | | |
753 | | { |
754 | | // Clamp to avoid underflow |
755 | 0 | let x_clamped = _mm256_max_ps(x, min_exp); |
756 | | |
757 | | // n = floor(x / ln2) |
758 | 0 | let xln2 = _mm256_mul_ps(x_clamped, ln2_inv); |
759 | 0 | let n_f = _mm256_floor_ps(xln2); |
760 | 0 | let n_i = _mm256_cvtps_epi32(n_f); |
761 | | |
762 | | // f = x - n * ln2 |
763 | 0 | let f = _mm256_fnmadd_ps(n_f, ln2, x_clamped); |
764 | | |
765 | | // Polynomial: c0 + f*(c1 + f*(c2 + f*(c3 + f*(c4 + f*c5)))) |
766 | 0 | let p = _mm256_fmadd_ps(f, c5, c4); |
767 | 0 | let p = _mm256_fmadd_ps(f, p, c3); |
768 | 0 | let p = _mm256_fmadd_ps(f, p, c2); |
769 | 0 | let p = _mm256_fmadd_ps(f, p, c1); |
770 | 0 | let p = _mm256_fmadd_ps(f, p, c0); |
771 | | |
772 | | // 2^n via bit manipulation |
773 | 0 | let bias = _mm256_set1_epi32(127); |
774 | 0 | let n_biased = _mm256_add_epi32(n_i, bias); |
775 | 0 | let exp_scale = _mm256_slli_epi32::<23>(n_biased); |
776 | 0 | let exp_scale_f = _mm256_castsi256_ps(exp_scale); |
777 | | |
778 | 0 | _mm256_mul_ps(p, exp_scale_f) |
779 | | } |
780 | 0 | } |
781 | | |
782 | | /// Horizontal max of 8-wide AVX2 vector |
783 | | #[cfg(target_arch = "x86_64")] |
784 | | #[target_feature(enable = "avx2")] |
785 | | #[inline] |
786 | | #[allow(unsafe_op_in_unsafe_fn)] |
787 | 2.80k | unsafe fn horizontal_max_avx2(v: std::arch::x86_64::__m256) -> f32 { |
788 | | use std::arch::x86_64::{ |
789 | | _mm256_extractf128_ps, _mm_cvtss_f32, _mm_max_ps, _mm_max_ss, _mm_movehl_ps, _mm_shuffle_ps, |
790 | | }; |
791 | | |
792 | | { |
793 | | // Extract high and low 128-bit lanes |
794 | 2.80k | let hi = _mm256_extractf128_ps::<1>(v); |
795 | 2.80k | let lo = _mm256_extractf128_ps::<0>(v); |
796 | 2.80k | let max128 = _mm_max_ps(hi, lo); |
797 | | |
798 | | // Reduce 4 to 2 |
799 | 2.80k | let max64 = _mm_max_ps(max128, _mm_movehl_ps(max128, max128)); |
800 | | |
801 | | // Reduce 2 to 1 |
802 | 2.80k | let max32 = _mm_max_ss(max64, _mm_shuffle_ps::<0x55>(max64, max64)); |
803 | | |
804 | 2.80k | _mm_cvtss_f32(max32) |
805 | | } |
806 | 2.80k | } |
807 | | |
808 | | /// Horizontal sum of 8-wide AVX2 vector |
809 | | #[cfg(target_arch = "x86_64")] |
810 | | #[target_feature(enable = "avx2")] |
811 | | #[inline] |
812 | | #[allow(unsafe_op_in_unsafe_fn)] |
813 | 0 | unsafe fn horizontal_sum_avx2(v: std::arch::x86_64::__m256) -> f32 { |
814 | | use std::arch::x86_64::{ |
815 | | _mm256_extractf128_ps, _mm_add_ps, _mm_add_ss, _mm_cvtss_f32, _mm_movehl_ps, _mm_shuffle_ps, |
816 | | }; |
817 | | |
818 | | { |
819 | | // Extract high and low 128-bit lanes |
820 | 0 | let hi = _mm256_extractf128_ps::<1>(v); |
821 | 0 | let lo = _mm256_extractf128_ps::<0>(v); |
822 | 0 | let sum128 = _mm_add_ps(hi, lo); |
823 | | |
824 | | // Reduce 4 to 2 |
825 | 0 | let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
826 | | |
827 | | // Reduce 2 to 1 |
828 | 0 | let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps::<0x55>(sum64, sum64)); |
829 | | |
830 | 0 | _mm_cvtss_f32(sum32) |
831 | | } |
832 | 0 | } |
833 | | |
834 | | /// Quantize f32 activations to Q8_0 format for fast integer matmul |
835 | | /// |
836 | | /// Returns (scales, quantized_values) where each block of 32 values |
837 | | /// has one f32 scale and 32 int8 quantized values. |
838 | | #[inline] |
839 | 806 | pub fn quantize_activations_q8_0(activations: &[f32]) -> (Vec<f32>, Vec<i8>) { |
840 | 806 | let num_blocks = activations.len().div_ceil(32); |
841 | 806 | let mut scales = Vec::with_capacity(num_blocks); |
842 | 806 | let mut quants = Vec::with_capacity(num_blocks * 32); |
843 | | |
844 | 1.97k | for block_idx in 0..num_blocks806 { |
845 | 1.97k | let start = block_idx * 32; |
846 | 1.97k | let end = (start + 32).min(activations.len()); |
847 | | |
848 | | // Find max absolute value for symmetric quantization |
849 | 1.97k | let mut max_abs = 0.0f32; |
850 | 63.0k | for i in start1.97k ..end1.97k { |
851 | 63.0k | let abs = activations[i].abs(); |
852 | 63.0k | if abs > max_abs { |
853 | 2.00k | max_abs = abs; |
854 | 61.0k | } |
855 | | } |
856 | | |
857 | | // Compute scale (avoid division by zero) |
858 | 1.97k | let scale = if max_abs > 1e-10 { |
859 | 936 | max_abs / 127.0 |
860 | | } else { |
861 | 1.03k | 1.0 / 127.0 |
862 | | }; |
863 | 1.97k | let inv_scale = 1.0 / scale; |
864 | 1.97k | scales.push(scale); |
865 | | |
866 | | // Quantize values |
867 | 63.0k | for i in start1.97k ..end1.97k { |
868 | 63.0k | let q = (activations[i] * inv_scale).round(); |
869 | 63.0k | quants.push(q.clamp(-128.0, 127.0) as i8); |
870 | 63.0k | } |
871 | | // Pad to 32 if partial block |
872 | 1.97k | for _ in end..(start + 32) { |
873 | 52 | quants.push(0i8); |
874 | 52 | } |
875 | | } |
876 | | |
877 | 806 | (scales, quants) |
878 | 806 | } |