/home/noah/src/realizar/src/quantize/parallel_dequant.rs
Line | Count | Source |
1 | | //! Parallel dequantization functions (PMAT-802) |
2 | | //! |
3 | | //! Implements parallel dequantization using rayon for multi-core acceleration: |
4 | | //! - `dequantize_q4_k_parallel` - Parallel Q4_K dequantization |
5 | | //! - `dequantize_q4_k_simd` - SIMD-accelerated Q4_K dequantization |
6 | | //! - `dequantize_q8_0_parallel` - Parallel Q8_0 dequantization |
7 | | //! - `dequantize_q8_0_simd` - SIMD-accelerated Q8_0 dequantization |
8 | | |
9 | | use crate::error::{RealizarError, Result}; |
10 | | use super::dequant::{read_f16, f16_to_f32}; |
11 | | use super::simd::extract_scale_min; |
12 | | use super::types::QK_K; |
13 | | |
14 | | // ============================================================================ |
15 | | |
16 | | /// Parallel Q4_K dequantization using rayon |
17 | | /// |
18 | | /// Processes super-blocks in parallel for faster model loading. |
19 | | /// Each super-block (256 values) is dequantized independently. |
20 | | /// |
21 | | /// # Arguments |
22 | | /// |
23 | | /// * `data` - Raw Q4_K quantized data (144 bytes per super-block) |
24 | | /// |
25 | | /// # Returns |
26 | | /// |
27 | | /// Dequantized f32 values |
28 | | /// |
29 | | /// # Errors |
30 | | /// |
31 | | /// Returns error if data length is not a multiple of super-block size |
32 | | /// |
33 | | /// # Performance |
34 | | /// |
35 | | /// On a 16-core system, achieves ~10x speedup over serial dequantization |
36 | | /// for large models (1B+ parameters). |
37 | 7 | pub fn dequantize_q4_k_parallel(data: &[u8]) -> Result<Vec<f32>> { |
38 | | use rayon::prelude::*; |
39 | | |
40 | | const SUPER_BLOCK_BYTES: usize = 144; |
41 | | |
42 | 7 | if !data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
43 | 2 | return Err(RealizarError::InvalidShape { |
44 | 2 | reason: format!( |
45 | 2 | "Q4_K data length {} is not a multiple of super-block size {}", |
46 | 2 | data.len(), |
47 | 2 | SUPER_BLOCK_BYTES |
48 | 2 | ), |
49 | 2 | }); |
50 | 5 | } |
51 | | |
52 | 5 | let num_super_blocks = data.len() / SUPER_BLOCK_BYTES; |
53 | | |
54 | | // Process super-blocks in parallel |
55 | 5 | let result: Vec<f32> = (0..num_super_blocks) |
56 | 5 | .into_par_iter() |
57 | 8 | .flat_map5 (|sb_idx| { |
58 | 8 | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
59 | 8 | let sb_data = &data[sb_start..sb_start + SUPER_BLOCK_BYTES]; |
60 | 8 | dequantize_q4_k_superblock(sb_data) |
61 | 8 | }) |
62 | 5 | .collect(); |
63 | | |
64 | 5 | Ok(result) |
65 | 7 | } |
66 | | |
67 | | /// Dequantize a single Q4_K super-block (256 values) |
68 | | /// |
69 | | /// Internal helper for parallel processing. |
70 | | /// |
71 | | /// Exposed as `pub(crate)` for direct testing. |
72 | | #[inline] |
73 | 8 | pub(crate) fn dequantize_q4_k_superblock(sb_data: &[u8]) -> Vec<f32> { |
74 | 8 | let mut result = vec![0.0f32; QK_K]; |
75 | | |
76 | | // Read d (f16 -> f32) |
77 | 8 | let d = read_f16(&sb_data[0..2]); |
78 | | |
79 | | // Read dmin (f16 -> f32) |
80 | 8 | let dmin = read_f16(&sb_data[2..4]); |
81 | | |
82 | | // Read scales (12 bytes) |
83 | 8 | let mut scales = [0u8; 12]; |
84 | 8 | scales.copy_from_slice(&sb_data[4..16]); |
85 | | |
86 | | // Read qs (128 bytes) |
87 | 8 | let qs = &sb_data[16..144]; |
88 | | |
89 | | // Dequantize following candle's layout: |
90 | | // For each 64-value chunk, output 32 low nibbles then 32 high nibbles |
91 | 8 | let mut ys_index = 0; |
92 | | |
93 | 32 | for j in (0..QK_K)8 .step_by8 (64) { |
94 | 32 | let q = &qs[j / 2..j / 2 + 32]; |
95 | | |
96 | | // Get scales for the two 32-value halves |
97 | 32 | let is = j / 32; |
98 | 32 | let (sc1, m1) = extract_scale_min(&scales, is); |
99 | 32 | let d1 = d * sc1; |
100 | 32 | let dm1 = dmin * m1; |
101 | | |
102 | 32 | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
103 | 32 | let d2 = d * sc2; |
104 | 32 | let dm2 = dmin * m2; |
105 | | |
106 | | // First pass: 32 low nibbles |
107 | 1.05k | for &byte1.02k in q { |
108 | 1.02k | result[ys_index] = d1 * (byte & 0xF) as f32 - dm1; |
109 | 1.02k | ys_index += 1; |
110 | 1.02k | } |
111 | | |
112 | | // Second pass: 32 high nibbles |
113 | 1.05k | for &byte1.02k in q { |
114 | 1.02k | result[ys_index] = d2 * (byte >> 4) as f32 - dm2; |
115 | 1.02k | ys_index += 1; |
116 | 1.02k | } |
117 | | } |
118 | | |
119 | 8 | result |
120 | 8 | } |
121 | | |
122 | | /// SIMD-accelerated Q4_K dequantization with runtime feature detection |
123 | | /// |
124 | | /// Uses AVX2 when available, falls back to parallel scalar otherwise. |
125 | | /// |
126 | | /// # Arguments |
127 | | /// |
128 | | /// * `data` - Raw Q4_K quantized data (144 bytes per super-block) |
129 | | /// |
130 | | /// # Returns |
131 | | /// |
132 | | /// Dequantized f32 values |
133 | | /// |
134 | | /// # Errors |
135 | | /// |
136 | | /// Returns error if data length is not a multiple of super-block size |
137 | | /// |
138 | | /// # Performance |
139 | | /// |
140 | | /// AVX2: ~4x speedup over scalar per super-block |
141 | | /// Combined with rayon: ~40x speedup on 16-core system |
142 | 6.48k | pub fn dequantize_q4_k_simd(data: &[u8]) -> Result<Vec<f32>> { |
143 | | #[cfg(target_arch = "x86_64")] |
144 | | { |
145 | 6.48k | if is_x86_feature_detected!("avx2") { |
146 | | // SAFETY: AVX2 verified at runtime |
147 | 6.48k | return unsafe { dequantize_q4_k_avx2_parallel(data) }; |
148 | 0 | } |
149 | | } |
150 | | |
151 | | // Fallback to parallel scalar |
152 | 0 | dequantize_q4_k_parallel(data) |
153 | 6.48k | } |
154 | | |
155 | | /// AVX2-accelerated parallel Q4_K dequantization |
156 | | /// |
157 | | /// # Safety |
158 | | /// |
159 | | /// Caller must ensure AVX2 is available (use runtime feature detection) |
160 | | #[cfg(target_arch = "x86_64")] |
161 | | #[target_feature(enable = "avx2")] |
162 | 6.48k | unsafe fn dequantize_q4_k_avx2_parallel(data: &[u8]) -> Result<Vec<f32>> { |
163 | | use rayon::prelude::*; |
164 | | |
165 | | const SUPER_BLOCK_BYTES: usize = 144; |
166 | | // Process 64 super-blocks per parallel task to reduce scheduling overhead |
167 | | const CHUNK_SIZE: usize = 64; |
168 | | const CHUNK_BYTES: usize = SUPER_BLOCK_BYTES * CHUNK_SIZE; |
169 | | |
170 | 6.48k | if !data.len().is_multiple_of(SUPER_BLOCK_BYTES) { |
171 | 2 | return Err(RealizarError::InvalidShape { |
172 | 2 | reason: format!( |
173 | 2 | "Q4_K data length {} is not a multiple of super-block size {}", |
174 | 2 | data.len(), |
175 | 2 | SUPER_BLOCK_BYTES |
176 | 2 | ), |
177 | 2 | }); |
178 | 6.48k | } |
179 | | |
180 | 6.48k | let num_super_blocks = data.len() / SUPER_BLOCK_BYTES; |
181 | | |
182 | | // For small data, skip parallelism overhead |
183 | 6.48k | if num_super_blocks < CHUNK_SIZE * 2 { |
184 | 6.47k | let mut result = Vec::with_capacity(num_super_blocks * QK_K); |
185 | 6.54k | for sb_idx in 0..num_super_blocks6.47k { |
186 | 6.54k | let sb_start = sb_idx * SUPER_BLOCK_BYTES; |
187 | 6.54k | let sb_data = &data[sb_start..sb_start + SUPER_BLOCK_BYTES]; |
188 | 6.54k | // SAFETY: AVX2 availability verified by caller |
189 | 6.54k | result.extend(unsafe { dequantize_q4_k_superblock_avx2(sb_data) }); |
190 | 6.54k | } |
191 | 6.47k | return Ok(result); |
192 | 5 | } |
193 | | |
194 | | // Process chunks of super-blocks in parallel |
195 | 5 | let result: Vec<f32> = data |
196 | 5 | .par_chunks(CHUNK_BYTES) |
197 | 136 | .flat_map5 (|chunk| { |
198 | 136 | let mut chunk_result = Vec::with_capacity(chunk.len() / SUPER_BLOCK_BYTES * QK_K); |
199 | 8.70k | for sb_data in chunk136 .chunks_exact136 (SUPER_BLOCK_BYTES) { |
200 | 8.70k | // SAFETY: AVX2 availability verified by caller |
201 | 8.70k | chunk_result.extend(unsafe { dequantize_q4_k_superblock_avx2(sb_data) }); |
202 | 8.70k | } |
203 | 136 | chunk_result |
204 | 136 | }) |
205 | 5 | .collect(); |
206 | | |
207 | 5 | Ok(result) |
208 | 6.48k | } |
209 | | |
210 | | /// AVX2 SIMD dequantization for a single Q4_K super-block |
211 | | /// |
212 | | /// Uses 256-bit SIMD to process 8 values simultaneously. |
213 | | #[cfg(target_arch = "x86_64")] |
214 | | #[target_feature(enable = "avx2", enable = "fma")] |
215 | 15.2k | unsafe fn dequantize_q4_k_superblock_avx2(sb_data: &[u8]) -> Vec<f32> { |
216 | | #[allow(clippy::wildcard_imports)] |
217 | | use std::arch::x86_64::*; |
218 | | |
219 | 15.2k | let mut result = vec![0.0f32; QK_K]; |
220 | | |
221 | | // Read d and dmin |
222 | 15.2k | let d = read_f16(&sb_data[0..2]); |
223 | 15.2k | let dmin = read_f16(&sb_data[2..4]); |
224 | | |
225 | | // SAFETY: AVX2 availability verified by caller's target_feature |
226 | | unsafe { |
227 | | // Read scales |
228 | 15.2k | let mut scales = [0u8; 12]; |
229 | 15.2k | scales.copy_from_slice(&sb_data[4..16]); |
230 | | |
231 | 15.2k | let qs = &sb_data[16..144]; |
232 | | |
233 | | // Dequantize following candle's layout: |
234 | | // For each 64-value chunk, output 32 low nibbles then 32 high nibbles |
235 | 15.2k | let mut ys_index = 0; |
236 | | |
237 | 60.9k | for j in (0..QK_K)15.2k .step_by15.2k (64) { |
238 | 60.9k | let q = &qs[j / 2..j / 2 + 32]; |
239 | | |
240 | | // Get scales for the two 32-value halves |
241 | 60.9k | let is = j / 32; |
242 | 60.9k | let (sc1, m1) = extract_scale_min(&scales, is); |
243 | 60.9k | let d1 = d * sc1; |
244 | 60.9k | let dm1 = dmin * m1; |
245 | 60.9k | let d1_vec = _mm256_set1_ps(d1); |
246 | 60.9k | let dm1_vec = _mm256_set1_ps(dm1); |
247 | | |
248 | 60.9k | let (sc2, m2) = extract_scale_min(&scales, is + 1); |
249 | 60.9k | let d2 = d * sc2; |
250 | 60.9k | let dm2 = dmin * m2; |
251 | 60.9k | let d2_vec = _mm256_set1_ps(d2); |
252 | 60.9k | let dm2_vec = _mm256_set1_ps(dm2); |
253 | | |
254 | | // First pass: 32 low nibbles in 4 iterations of 8 |
255 | 304k | for chunk243k in 0..4 { |
256 | 243k | let byte_start = chunk * 8; |
257 | 243k | |
258 | 243k | // Extract 8 low nibbles from 8 bytes |
259 | 243k | let q0 = (q[byte_start] & 0x0F) as i32; |
260 | 243k | let q1 = (q[byte_start + 1] & 0x0F) as i32; |
261 | 243k | let q2 = (q[byte_start + 2] & 0x0F) as i32; |
262 | 243k | let q3 = (q[byte_start + 3] & 0x0F) as i32; |
263 | 243k | let q4 = (q[byte_start + 4] & 0x0F) as i32; |
264 | 243k | let q5 = (q[byte_start + 5] & 0x0F) as i32; |
265 | 243k | let q6 = (q[byte_start + 6] & 0x0F) as i32; |
266 | 243k | let q7 = (q[byte_start + 7] & 0x0F) as i32; |
267 | 243k | |
268 | 243k | let q_vec = _mm256_setr_epi32(q0, q1, q2, q3, q4, q5, q6, q7); |
269 | 243k | let q_f32 = _mm256_cvtepi32_ps(q_vec); |
270 | 243k | let dequant = _mm256_fmsub_ps(d1_vec, q_f32, dm1_vec); |
271 | 243k | |
272 | 243k | _mm256_storeu_ps(result.as_mut_ptr().add(ys_index), dequant); |
273 | 243k | ys_index += 8; |
274 | 243k | } |
275 | | |
276 | | // Second pass: 32 high nibbles in 4 iterations of 8 |
277 | 304k | for chunk243k in 0..4 { |
278 | 243k | let byte_start = chunk * 8; |
279 | 243k | |
280 | 243k | // Extract 8 high nibbles from 8 bytes |
281 | 243k | let q0 = (q[byte_start] >> 4) as i32; |
282 | 243k | let q1 = (q[byte_start + 1] >> 4) as i32; |
283 | 243k | let q2 = (q[byte_start + 2] >> 4) as i32; |
284 | 243k | let q3 = (q[byte_start + 3] >> 4) as i32; |
285 | 243k | let q4 = (q[byte_start + 4] >> 4) as i32; |
286 | 243k | let q5 = (q[byte_start + 5] >> 4) as i32; |
287 | 243k | let q6 = (q[byte_start + 6] >> 4) as i32; |
288 | 243k | let q7 = (q[byte_start + 7] >> 4) as i32; |
289 | 243k | |
290 | 243k | let q_vec = _mm256_setr_epi32(q0, q1, q2, q3, q4, q5, q6, q7); |
291 | 243k | let q_f32 = _mm256_cvtepi32_ps(q_vec); |
292 | 243k | let dequant = _mm256_fmsub_ps(d2_vec, q_f32, dm2_vec); |
293 | 243k | |
294 | 243k | _mm256_storeu_ps(result.as_mut_ptr().add(ys_index), dequant); |
295 | 243k | ys_index += 8; |
296 | 243k | } |
297 | | } |
298 | | } |
299 | | |
300 | 15.2k | result |
301 | 15.2k | } |
302 | | |
303 | | /// Parallel Q8_0 dequantization using rayon |
304 | | /// |
305 | | /// Q8_0 is simpler than Q4_K (no scale packing), making SIMD even more effective. |
306 | | /// |
307 | | /// # Arguments |
308 | | /// |
309 | | /// * `data` - Raw Q8_0 quantized data (36 bytes per block: 4 scale + 32 quants) |
310 | | /// |
311 | | /// # Returns |
312 | | /// |
313 | | /// Dequantized f32 values |
314 | | /// |
315 | | /// # Errors |
316 | | /// |
317 | | /// Returns error if data length is not a multiple of block size |
318 | 7 | pub fn dequantize_q8_0_parallel(data: &[u8]) -> Result<Vec<f32>> { |
319 | | use rayon::prelude::*; |
320 | | |
321 | | const BLOCK_BYTES: usize = 34; // 2 (f16 scale) + 32 (i8 quants) |
322 | | |
323 | 7 | if !data.len().is_multiple_of(BLOCK_BYTES) { |
324 | 2 | return Err(RealizarError::InvalidShape { |
325 | 2 | reason: format!( |
326 | 2 | "Q8_0 data length {} is not a multiple of block size {}", |
327 | 2 | data.len(), |
328 | 2 | BLOCK_BYTES |
329 | 2 | ), |
330 | 2 | }); |
331 | 5 | } |
332 | | |
333 | 5 | let num_blocks = data.len() / BLOCK_BYTES; |
334 | | |
335 | | // Process blocks in parallel |
336 | 5 | let result: Vec<f32> = (0..num_blocks) |
337 | 5 | .into_par_iter() |
338 | 1.00k | .flat_map5 (|block_idx| { |
339 | 1.00k | let block_start = block_idx * BLOCK_BYTES; |
340 | 1.00k | let block_data = &data[block_start..block_start + BLOCK_BYTES]; |
341 | 1.00k | dequantize_q8_0_block(block_data) |
342 | 1.00k | }) |
343 | 5 | .collect(); |
344 | | |
345 | 5 | Ok(result) |
346 | 7 | } |
347 | | |
348 | | /// Dequantize a single Q8_0 block (32 values) |
349 | | /// |
350 | | /// Exposed as `pub(crate)` for direct testing. |
351 | | #[inline] |
352 | 1.00k | pub(crate) fn dequantize_q8_0_block(block_data: &[u8]) -> Vec<f32> { |
353 | 1.00k | let mut result = Vec::with_capacity(32); |
354 | | |
355 | | // Read scale (f16 -> f32) |
356 | 1.00k | let scale_bits = u16::from_le_bytes([block_data[0], block_data[1]]); |
357 | 1.00k | let scale = f16_to_f32(scale_bits); |
358 | | |
359 | | // Dequantize 32 int8 values |
360 | 32.1k | for &byte in &block_data[2..34]1.00k { |
361 | 32.1k | let value = i8::from_le_bytes([byte]); |
362 | 32.1k | result.push(scale * f32::from(value)); |
363 | 32.1k | } |
364 | | |
365 | 1.00k | result |
366 | 1.00k | } |
367 | | |
368 | | /// SIMD-accelerated Q8_0 dequantization |
369 | | /// |
370 | | /// Uses AVX2 when available for 8x parallel i8→f32 conversion. |
371 | | /// |
372 | | /// # Errors |
373 | | /// |
374 | | /// Returns error if data length is not a multiple of block size (36 bytes) |
375 | 7 | pub fn dequantize_q8_0_simd(data: &[u8]) -> Result<Vec<f32>> { |
376 | | #[cfg(target_arch = "x86_64")] |
377 | | { |
378 | 7 | if is_x86_feature_detected!("avx2") { |
379 | | // SAFETY: AVX2 verified at runtime |
380 | 7 | return unsafe { dequantize_q8_0_avx2_parallel(data) }; |
381 | 0 | } |
382 | | } |
383 | | |
384 | | // Fallback to parallel scalar |
385 | 0 | dequantize_q8_0_parallel(data) |
386 | 7 | } |
387 | | |
388 | | /// AVX2-accelerated parallel Q8_0 dequantization |
389 | | #[cfg(target_arch = "x86_64")] |
390 | | #[target_feature(enable = "avx2")] |
391 | 7 | unsafe fn dequantize_q8_0_avx2_parallel(data: &[u8]) -> Result<Vec<f32>> { |
392 | | use rayon::prelude::*; |
393 | | |
394 | | const BLOCK_BYTES: usize = 34; // 2 (f16 scale) + 32 (i8 quants) |
395 | | |
396 | 7 | if !data.len().is_multiple_of(BLOCK_BYTES) { |
397 | 2 | return Err(RealizarError::InvalidShape { |
398 | 2 | reason: format!( |
399 | 2 | "Q8_0 data length {} is not a multiple of block size {}", |
400 | 2 | data.len(), |
401 | 2 | BLOCK_BYTES |
402 | 2 | ), |
403 | 2 | }); |
404 | 5 | } |
405 | | |
406 | 5 | let num_blocks = data.len() / BLOCK_BYTES; |
407 | | |
408 | 5 | let result: Vec<f32> = (0..num_blocks) |
409 | 5 | .into_par_iter() |
410 | 5 | .flat_map(|block_idx| { |
411 | 5 | let block_start = block_idx * BLOCK_BYTES; |
412 | 5 | let block_data = &data[block_start..block_start + BLOCK_BYTES]; |
413 | | // SAFETY: AVX2 availability verified by caller |
414 | 5 | unsafe { dequantize_q8_0_block_avx2(block_data) } |
415 | 5 | }) |
416 | 5 | .collect(); |
417 | | |
418 | 5 | Ok(result) |
419 | 7 | } |
420 | | |
421 | | /// AVX2 SIMD dequantization for a single Q8_0 block |
422 | | #[cfg(target_arch = "x86_64")] |
423 | | #[target_feature(enable = "avx2")] |
424 | 5 | unsafe fn dequantize_q8_0_block_avx2(block_data: &[u8]) -> Vec<f32> { |
425 | | #[allow(clippy::wildcard_imports)] |
426 | | use std::arch::x86_64::*; |
427 | | |
428 | 5 | let mut result = vec![0.0f32; 32]; |
429 | | |
430 | | // Read scale (f16 -> f32) |
431 | 5 | let scale_bits = u16::from_le_bytes([block_data[0], block_data[1]]); |
432 | 5 | let scale = f16_to_f32(scale_bits); |
433 | | |
434 | | // SAFETY: AVX2 availability verified by caller's target_feature |
435 | | unsafe { |
436 | 5 | let scale_vec = _mm256_set1_ps(scale); |
437 | | |
438 | | // Process 32 i8 values in 4 iterations of 8 |
439 | 25 | for chunk20 in 0..4 { |
440 | 20 | let byte_start = 2 + chunk * 8; // Start at offset 2 (after f16 scale) |
441 | 20 | |
442 | 20 | // Load 8 i8 values and sign-extend to i32 |
443 | 20 | let q0 = block_data[byte_start] as i8 as i32; |
444 | 20 | let q1 = block_data[byte_start + 1] as i8 as i32; |
445 | 20 | let q2 = block_data[byte_start + 2] as i8 as i32; |
446 | 20 | let q3 = block_data[byte_start + 3] as i8 as i32; |
447 | 20 | let q4 = block_data[byte_start + 4] as i8 as i32; |
448 | 20 | let q5 = block_data[byte_start + 5] as i8 as i32; |
449 | 20 | let q6 = block_data[byte_start + 6] as i8 as i32; |
450 | 20 | let q7 = block_data[byte_start + 7] as i8 as i32; |
451 | 20 | |
452 | 20 | let q_vec = _mm256_setr_epi32(q0, q1, q2, q3, q4, q5, q6, q7); |
453 | 20 | let q_f32 = _mm256_cvtepi32_ps(q_vec); |
454 | 20 | |
455 | 20 | // Multiply by scale |
456 | 20 | let dequant = _mm256_mul_ps(scale_vec, q_f32); |
457 | 20 | |
458 | 20 | // Store 8 results |
459 | 20 | _mm256_storeu_ps(result.as_mut_ptr().add(chunk * 8), dequant); |
460 | 20 | } |
461 | | } |
462 | | |
463 | 5 | result |
464 | 5 | } |
465 | | |
466 | | // DequantStats, SimdBackend, detect_simd_backend moved to types.rs (PMAT-802) |
467 | | |
468 | | /// SIMD-optimized RoPE rotation for a single head |
469 | | /// |
470 | | /// Applies rotary position embedding rotation to a single attention head: |
471 | | /// x1[i] = x1[i] * cos[i] - x2[i] * sin[i] |
472 | | /// x2[i] = x1[i] * sin[i] + x2[i] * cos[i] |
473 | | /// |
474 | | /// # Arguments |
475 | | /// * `x1` - First half of head (will be modified in-place) |
476 | | /// * `x2` - Second half of head (will be modified in-place) |
477 | | /// * `cos_vals` - Precomputed cosine values |
478 | | /// * `sin_vals` - Precomputed sine values |
479 | | #[inline] |
480 | 9 | pub fn apply_rope_rotation_simd( |
481 | 9 | x1: &mut [f32], |
482 | 9 | x2: &mut [f32], |
483 | 9 | cos_vals: &[f32], |
484 | 9 | sin_vals: &[f32], |
485 | 9 | ) { |
486 | 9 | debug_assert_eq!(x1.len(), x2.len()); |
487 | 9 | debug_assert_eq!(x1.len(), cos_vals.len()); |
488 | 9 | debug_assert_eq!(x1.len(), sin_vals.len()); |
489 | | |
490 | | #[cfg(target_arch = "x86_64")] |
491 | | { |
492 | 9 | if is_x86_feature_detected!("avx512f") { |
493 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
494 | 9 | unsafe { |
495 | 9 | apply_rope_rotation_avx512(x1, x2, cos_vals, sin_vals); |
496 | 9 | } |
497 | 9 | return; |
498 | 0 | } |
499 | 0 | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
500 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
501 | 0 | unsafe { |
502 | 0 | apply_rope_rotation_avx2(x1, x2, cos_vals, sin_vals); |
503 | 0 | } |
504 | 0 | return; |
505 | 0 | } |
506 | | } |
507 | | |
508 | | // Scalar fallback |
509 | 0 | apply_rope_rotation_scalar(x1, x2, cos_vals, sin_vals); |
510 | 9 | } |
511 | | |
512 | | /// Scalar fallback for RoPE rotation |
513 | | /// |
514 | | /// Exposed as `pub(crate)` for direct testing on AVX2 machines. |
515 | | #[inline] |
516 | 0 | pub(crate) fn apply_rope_rotation_scalar( |
517 | 0 | x1: &mut [f32], |
518 | 0 | x2: &mut [f32], |
519 | 0 | cos_vals: &[f32], |
520 | 0 | sin_vals: &[f32], |
521 | 0 | ) { |
522 | 0 | for i in 0..x1.len() { |
523 | 0 | let v1 = x1[i]; |
524 | 0 | let v2 = x2[i]; |
525 | 0 | let cos_v = cos_vals[i]; |
526 | 0 | let sin_v = sin_vals[i]; |
527 | 0 | x1[i] = v1 * cos_v - v2 * sin_v; |
528 | 0 | x2[i] = v1 * sin_v + v2 * cos_v; |
529 | 0 | } |
530 | 0 | } |
531 | | |
532 | | #[cfg(target_arch = "x86_64")] |
533 | | #[target_feature(enable = "avx2", enable = "fma")] |
534 | | #[inline] |
535 | | #[allow(unsafe_op_in_unsafe_fn)] |
536 | 0 | unsafe fn apply_rope_rotation_avx2( |
537 | 0 | x1: &mut [f32], |
538 | 0 | x2: &mut [f32], |
539 | 0 | cos_vals: &[f32], |
540 | 0 | sin_vals: &[f32], |
541 | 0 | ) { |
542 | | use std::arch::x86_64::{ |
543 | | _mm256_fmadd_ps, _mm256_fnmadd_ps, _mm256_loadu_ps, _mm256_mul_ps, _mm256_storeu_ps, |
544 | | }; |
545 | | |
546 | 0 | let n = x1.len(); |
547 | 0 | let mut i = 0; |
548 | | |
549 | | // Process 8 elements at a time |
550 | 0 | while i + 8 <= n { |
551 | 0 | let v1 = _mm256_loadu_ps(x1.as_ptr().add(i)); |
552 | 0 | let v2 = _mm256_loadu_ps(x2.as_ptr().add(i)); |
553 | 0 | let cos_v = _mm256_loadu_ps(cos_vals.as_ptr().add(i)); |
554 | 0 | let sin_v = _mm256_loadu_ps(sin_vals.as_ptr().add(i)); |
555 | 0 |
|
556 | 0 | // r1 = v1 * cos - v2 * sin |
557 | 0 | let v1_cos = _mm256_mul_ps(v1, cos_v); |
558 | 0 | let r1 = _mm256_fnmadd_ps(v2, sin_v, v1_cos); |
559 | 0 |
|
560 | 0 | // r2 = v1 * sin + v2 * cos |
561 | 0 | let v1_sin = _mm256_mul_ps(v1, sin_v); |
562 | 0 | let r2 = _mm256_fmadd_ps(v2, cos_v, v1_sin); |
563 | 0 |
|
564 | 0 | _mm256_storeu_ps(x1.as_mut_ptr().add(i), r1); |
565 | 0 | _mm256_storeu_ps(x2.as_mut_ptr().add(i), r2); |
566 | 0 |
|
567 | 0 | i += 8; |
568 | 0 | } |
569 | | |
570 | | // Handle remainder |
571 | 0 | while i < n { |
572 | 0 | let v1 = x1[i]; |
573 | 0 | let v2 = x2[i]; |
574 | 0 | let cos_v = cos_vals[i]; |
575 | 0 | let sin_v = sin_vals[i]; |
576 | 0 | x1[i] = v1 * cos_v - v2 * sin_v; |
577 | 0 | x2[i] = v1 * sin_v + v2 * cos_v; |
578 | 0 | i += 1; |
579 | 0 | } |
580 | 0 | } |
581 | | |
582 | | #[cfg(target_arch = "x86_64")] |
583 | | #[target_feature(enable = "avx512f")] |
584 | | #[inline] |
585 | | #[allow(unsafe_op_in_unsafe_fn)] |
586 | 9 | unsafe fn apply_rope_rotation_avx512( |
587 | 9 | x1: &mut [f32], |
588 | 9 | x2: &mut [f32], |
589 | 9 | cos_vals: &[f32], |
590 | 9 | sin_vals: &[f32], |
591 | 9 | ) { |
592 | | use std::arch::x86_64::{ |
593 | | _mm512_fmadd_ps, _mm512_fnmadd_ps, _mm512_loadu_ps, _mm512_mul_ps, _mm512_storeu_ps, |
594 | | }; |
595 | | |
596 | 9 | let n = x1.len(); |
597 | 9 | let mut i = 0; |
598 | | |
599 | | // Process 16 elements at a time with AVX-512 |
600 | 16 | while i + 16 <= n { |
601 | 7 | let v1 = _mm512_loadu_ps(x1.as_ptr().add(i)); |
602 | 7 | let v2 = _mm512_loadu_ps(x2.as_ptr().add(i)); |
603 | 7 | let cos_v = _mm512_loadu_ps(cos_vals.as_ptr().add(i)); |
604 | 7 | let sin_v = _mm512_loadu_ps(sin_vals.as_ptr().add(i)); |
605 | 7 | |
606 | 7 | // r1 = v1 * cos - v2 * sin |
607 | 7 | let v1_cos = _mm512_mul_ps(v1, cos_v); |
608 | 7 | let r1 = _mm512_fnmadd_ps(v2, sin_v, v1_cos); |
609 | 7 | |
610 | 7 | // r2 = v1 * sin + v2 * cos |
611 | 7 | let v1_sin = _mm512_mul_ps(v1, sin_v); |
612 | 7 | let r2 = _mm512_fmadd_ps(v2, cos_v, v1_sin); |
613 | 7 | |
614 | 7 | _mm512_storeu_ps(x1.as_mut_ptr().add(i), r1); |
615 | 7 | _mm512_storeu_ps(x2.as_mut_ptr().add(i), r2); |
616 | 7 | |
617 | 7 | i += 16; |
618 | 7 | } |
619 | | |
620 | | // Handle remainder with AVX2 or scalar |
621 | 20 | while i < n { |
622 | 11 | let v1 = x1[i]; |
623 | 11 | let v2 = x2[i]; |
624 | 11 | let cos_v = cos_vals[i]; |
625 | 11 | let sin_v = sin_vals[i]; |
626 | 11 | x1[i] = v1 * cos_v - v2 * sin_v; |
627 | 11 | x2[i] = v1 * sin_v + v2 * cos_v; |
628 | 11 | i += 1; |
629 | 11 | } |
630 | 9 | } |