/home/noah/src/realizar/src/apr/helpers.rs
Line | Count | Source |
1 | | //! APR inference helper functions (PMAT-802) |
2 | | //! |
3 | | //! Extracted from apr/mod.rs - Pure Rust inference primitives. |
4 | | //! |
5 | | //! ## Contents |
6 | | //! - RMS normalization |
7 | | //! - Matrix multiplication with SIMD |
8 | | //! - Transpose operations |
9 | | //! - Simple attention computation |
10 | | //! - Format detection utilities |
11 | | |
12 | | use std::path::Path; |
13 | | use std::fs; |
14 | | use super::MAGIC; |
15 | | |
16 | | /// RMS normalization |
17 | 10 | pub(crate) fn rms_norm(x: &[f32], weight: &[f32], eps: f32) -> Vec<f32> { |
18 | 10 | let hidden_dim = weight.len(); |
19 | 10 | let seq_len = x.len() / hidden_dim; |
20 | 10 | let mut output = Vec::with_capacity(x.len()); |
21 | | |
22 | 13 | for s in 0..seq_len10 { |
23 | 13 | let start = s * hidden_dim; |
24 | 13 | let slice = &x[start..start + hidden_dim]; |
25 | | |
26 | | // Compute RMS |
27 | 104 | let sum_sq13 : f3213 = slice13 .iter13 ().map13 (|&v| v * v).sum13 (); |
28 | 13 | let rms = (sum_sq / hidden_dim as f32 + eps).sqrt(); |
29 | | |
30 | | // Normalize and scale |
31 | 104 | for (i, &v) in slice13 .iter13 ().enumerate13 () { |
32 | 104 | output.push((v / rms) * weight.get(i).copied().unwrap_or(1.0)); |
33 | 104 | } |
34 | | } |
35 | 10 | output |
36 | 10 | } |
37 | | |
38 | | /// Matrix multiplication with SIMD dot products |
39 | | /// [seq, in_dim] @ [out_dim, in_dim]^T -> [seq, out_dim] |
40 | 10 | pub(crate) fn matmul(x: &[f32], w: &[f32], seq_len: usize, in_dim: usize, out_dim: usize) -> Vec<f32> { |
41 | 10 | let mut output = vec![0.0; seq_len * out_dim]; |
42 | | |
43 | 15 | for s in 0..seq_len10 { |
44 | 15 | let x_start = s * in_dim; |
45 | 15 | let x_end = x_start + in_dim; |
46 | 15 | if x_end > x.len() { |
47 | 1 | continue; // Skip if out of bounds |
48 | 14 | } |
49 | 14 | let x_row = &x[x_start..x_end]; |
50 | | |
51 | 277 | for o in 0..out_dim14 { |
52 | 277 | let w_start = o * in_dim; |
53 | 277 | let w_end = w_start + in_dim; |
54 | 277 | if w_end > w.len() { |
55 | 2 | continue; // Skip if out of bounds |
56 | 275 | } |
57 | 275 | let w_row = &w[w_start..w_end]; |
58 | | // SIMD dot product |
59 | 275 | output[s * out_dim + o] = simd_dot(x_row, w_row); |
60 | | } |
61 | | } |
62 | 10 | output |
63 | 10 | } |
64 | | |
65 | | /// Transpose a matrix from [rows, cols] to [cols, rows] for GEMM compatibility. |
66 | | /// Weight matrices are stored as [out_dim, in_dim] but GEMM needs [in_dim, out_dim]. |
67 | | #[cfg(feature = "cuda")] |
68 | | pub(crate) fn transpose_matrix(m: &[f32], rows: usize, cols: usize) -> Vec<f32> { |
69 | | let mut transposed = vec![0.0f32; rows * cols]; |
70 | | for r in 0..rows { |
71 | | for c in 0..cols { |
72 | | // m[r, c] -> transposed[c, r] |
73 | | let src_idx = r * cols + c; |
74 | | let dst_idx = c * rows + r; |
75 | | if src_idx < m.len() && dst_idx < transposed.len() { |
76 | | transposed[dst_idx] = m[src_idx]; |
77 | | } |
78 | | } |
79 | | } |
80 | | transposed |
81 | | } |
82 | | |
83 | | /// SIMD-accelerated dot product |
84 | | #[inline] |
85 | 286 | pub fn simd_dot(a: &[f32], b: &[f32]) -> f32 { |
86 | | #[cfg(target_arch = "x86_64")] |
87 | | { |
88 | 286 | if is_x86_feature_detected!("avx2") { |
89 | | // SAFETY: AVX2 feature is runtime-checked above, simd_dot_avx2 requires AVX2 |
90 | 286 | return unsafe { simd_dot_avx2(a, b) }; |
91 | 0 | } |
92 | | } |
93 | | // Scalar fallback |
94 | 0 | a.iter().zip(b.iter()).map(|(x, y)| x * y).sum() |
95 | 286 | } |
96 | | |
97 | | #[cfg(target_arch = "x86_64")] |
98 | | #[target_feature(enable = "avx2", enable = "fma")] |
99 | 286 | unsafe fn simd_dot_avx2(a: &[f32], b: &[f32]) -> f32 { |
100 | | use std::arch::x86_64::{ |
101 | | _mm256_castps256_ps128, _mm256_extractf128_ps, _mm256_fmadd_ps, _mm256_loadu_ps, |
102 | | _mm256_setzero_ps, _mm_add_ps, _mm_add_ss, _mm_cvtss_f32, _mm_movehl_ps, _mm_shuffle_ps, |
103 | | }; |
104 | | |
105 | 286 | let n = a.len().min(b.len()); |
106 | 286 | let chunks = n / 8; |
107 | | |
108 | | // SAFETY: This entire fn is unsafe with target_feature(avx2, fma) |
109 | | // All intrinsics are safe to call given the target_feature guarantee |
110 | | // The unsafe block is required for Rust 2024 edition compliance |
111 | | unsafe { |
112 | 286 | let mut sum = _mm256_setzero_ps(); |
113 | | |
114 | 2.08k | for i in 0..chunks286 { |
115 | 2.08k | let av = _mm256_loadu_ps(a.as_ptr().add(i * 8)); |
116 | 2.08k | let bv = _mm256_loadu_ps(b.as_ptr().add(i * 8)); |
117 | 2.08k | sum = _mm256_fmadd_ps(av, bv, sum); |
118 | 2.08k | } |
119 | | |
120 | | // Horizontal sum |
121 | 286 | let hi = _mm256_extractf128_ps(sum, 1); |
122 | 286 | let lo = _mm256_castps256_ps128(sum); |
123 | 286 | let sum128 = _mm_add_ps(lo, hi); |
124 | 286 | let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
125 | 286 | let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); |
126 | 286 | let mut result = _mm_cvtss_f32(sum32); |
127 | | |
128 | | // Handle remainder (scalar) |
129 | 286 | for i67 in (chunks * 8)..n { |
130 | 67 | result += a.get(i).copied().unwrap_or(0.0) * b.get(i).copied().unwrap_or(0.0); |
131 | 67 | } |
132 | | |
133 | 286 | result |
134 | | } |
135 | 286 | } |
136 | | |
137 | | /// Simplified multi-head attention (no RoPE, causal mask) |
138 | 7 | pub(crate) fn simple_attention( |
139 | 7 | q: &[f32], |
140 | 7 | k: &[f32], |
141 | 7 | v: &[f32], |
142 | 7 | seq_len: usize, |
143 | 7 | num_heads: usize, |
144 | 7 | num_kv_heads: usize, |
145 | 7 | head_dim: usize, |
146 | 7 | ) -> Vec<f32> { |
147 | 7 | let hidden_dim = num_heads * head_dim; |
148 | 7 | let kv_dim = num_kv_heads * head_dim; |
149 | 7 | let heads_per_kv = num_heads / num_kv_heads; |
150 | 7 | let scale = 1.0 / (head_dim as f32).sqrt(); |
151 | | |
152 | 7 | let mut output = vec![0.0; seq_len * hidden_dim]; |
153 | | |
154 | 10 | for s in 0..seq_len7 { |
155 | 21 | for h in 0..num_heads10 { |
156 | 21 | let kv_h = h / heads_per_kv; |
157 | | |
158 | | // Compute attention scores for this head |
159 | 21 | let mut scores = vec![0.0; seq_len]; |
160 | 30 | for t in 0..=s21 { |
161 | | // Causal: only attend to past |
162 | 30 | let mut score = 0.0; |
163 | 96 | for d in 0..head_dim30 { |
164 | 96 | let q_val = q |
165 | 96 | .get(s * hidden_dim + h * head_dim + d) |
166 | 96 | .copied() |
167 | 96 | .unwrap_or(0.0); |
168 | 96 | let k_val = k |
169 | 96 | .get(t * kv_dim + kv_h * head_dim + d) |
170 | 96 | .copied() |
171 | 96 | .unwrap_or(0.0); |
172 | 96 | score += q_val * k_val; |
173 | 96 | } |
174 | 30 | scores[t] = score * scale; |
175 | | } |
176 | | |
177 | | // Softmax |
178 | 21 | let max_score = scores[..=s] |
179 | 21 | .iter() |
180 | 21 | .cloned() |
181 | 21 | .fold(f32::NEG_INFINITY, f32::max); |
182 | 21 | let mut sum = 0.0; |
183 | 30 | for score in &mut scores[..=s]21 { |
184 | 30 | *score = (*score - max_score).exp(); |
185 | 30 | sum += *score; |
186 | 30 | } |
187 | 30 | for score in &mut scores[..=s]21 { |
188 | 30 | *score /= sum; |
189 | 30 | } |
190 | | |
191 | | // Weighted sum of values |
192 | 62 | for d in 0..head_dim21 { |
193 | 62 | let mut val = 0.0; |
194 | 96 | for t in 0..=s62 { |
195 | 96 | let v_val = v |
196 | 96 | .get(t * kv_dim + kv_h * head_dim + d) |
197 | 96 | .copied() |
198 | 96 | .unwrap_or(0.0); |
199 | 96 | val += scores[t] * v_val; |
200 | 96 | } |
201 | 62 | output[s * hidden_dim + h * head_dim + d] = val; |
202 | | } |
203 | | } |
204 | | } |
205 | | |
206 | 7 | output |
207 | 7 | } |
208 | | |
209 | | /// Check if a file is a valid .apr v2 file |
210 | 13 | pub fn is_apr_file<P: AsRef<Path>>(path: P) -> bool { |
211 | 13 | fs::read(path.as_ref()).is_ok_and(|data| data.len() >= 45 && data[0..4] == MAGIC5 ) |
212 | 13 | } |
213 | | |
214 | | /// Detect model format from magic bytes |
215 | 15 | pub fn detect_format<P: AsRef<Path>>(path: P) -> &'static str { |
216 | 15 | let path = path.as_ref(); |
217 | | |
218 | 15 | if let Some(ext10 ) = path.extension() { |
219 | 10 | let ext = ext.to_string_lossy().to_lowercase(); |
220 | 10 | match ext.as_str() { |
221 | 10 | "apr" => return "apr"2 , |
222 | 8 | "gguf" => return "gguf"2 , |
223 | 6 | "safetensors" => return "safetensors"2 , |
224 | 4 | _ => {}, |
225 | | } |
226 | 5 | } |
227 | | |
228 | 9 | if let Ok(data3 ) = fs::read(path) { |
229 | 3 | if data.len() >= 4 { |
230 | 3 | if data[0..4] == MAGIC { |
231 | 1 | return "apr"; |
232 | 2 | } |
233 | 2 | if data[0..4] == [0x47, 0x47, 0x55, 0x46] { |
234 | 1 | return "gguf"; |
235 | 1 | } |
236 | 1 | if data[0] == b'{' { |
237 | 1 | return "safetensors"; |
238 | 0 | } |
239 | 0 | } |
240 | 6 | } |
241 | | |
242 | 6 | "unknown" |
243 | 15 | } |