/home/noah/src/realizar/src/layers/position.rs
Line | Count | Source |
1 | | //! Position embeddings for transformer models |
2 | | //! |
3 | | //! Extracted from layers/mod.rs (PMAT-802) to reduce module size. |
4 | | //! Contains: |
5 | | //! - RoPE: Rotary Position Embeddings (RoFormer, LLaMA, PaLM) |
6 | | //! - RopeScalingType: Context length extension methods (NTK, YaRN, Linear) |
7 | | //! - ScaledRoPE: RoPE with scaling for extended context |
8 | | //! - ALiBi: Attention with Linear Biases |
9 | | |
10 | | use crate::{ |
11 | | error::{RealizarError, Result}, |
12 | | tensor::Tensor, |
13 | | }; |
14 | | |
15 | | /// Rotary Position Embeddings (`RoPE`) |
16 | | /// |
17 | | /// Applies position-dependent rotations to query and key vectors. |
18 | | /// Used in `LLaMA`, `PaLM`, and other modern transformers for relative |
19 | | /// position encoding. |
20 | | /// |
21 | | /// The rotation is applied pairwise to dimensions, encoding position |
22 | | /// information directly into the embeddings. |
23 | | /// |
24 | | /// # Formula |
25 | | /// |
26 | | /// For each pair of dimensions (2i, 2i+1): |
27 | | /// ```text |
28 | | /// x'_{2i} = x_{2i} * cos(θ_i * pos) - x_{2i+1} * sin(θ_i * pos) |
29 | | /// x'_{2i+1} = x_{2i} * sin(θ_i * pos) + x_{2i+1} * cos(θ_i * pos) |
30 | | /// ``` |
31 | | /// |
32 | | /// Where `θ_i` = base^(-2i/dim) |
33 | | /// |
34 | | /// # References |
35 | | /// |
36 | | /// `RoFormer`: Enhanced Transformer with Rotary Position Embedding - Su et al., 2021 |
37 | | #[derive(Debug, Clone)] |
38 | | pub struct RoPE { |
39 | | /// Embedding dimension (must be even) |
40 | | dim: usize, |
41 | | /// Base for computing frequencies (default: 10000) |
42 | | base: f32, |
43 | | /// Precomputed inverse frequencies for each dimension pair |
44 | | inv_freq: Vec<f32>, |
45 | | } |
46 | | |
47 | | impl RoPE { |
48 | | /// Create a new `RoPE` layer |
49 | | /// |
50 | | /// # Arguments |
51 | | /// |
52 | | /// * `dim` - Embedding dimension (must be even) |
53 | | /// * `base` - Base for computing frequencies (typically 10000) |
54 | | /// |
55 | | /// # Errors |
56 | | /// |
57 | | /// Returns error if `dim` is zero or odd |
58 | 18 | pub fn new(dim: usize, base: f32) -> Result<Self> { |
59 | 18 | if dim == 0 { |
60 | 2 | return Err(RealizarError::InvalidShape { |
61 | 2 | reason: "dim must be > 0".to_string(), |
62 | 2 | }); |
63 | 16 | } |
64 | 16 | if !dim.is_multiple_of(2) { |
65 | 2 | return Err(RealizarError::InvalidShape { |
66 | 2 | reason: "dim must be even for RoPE".to_string(), |
67 | 2 | }); |
68 | 14 | } |
69 | | |
70 | | // Compute inverse frequencies: base^(-2i/dim) for i in 0..dim/2 |
71 | 14 | let half_dim = dim / 2; |
72 | 14 | let mut inv_freq = Vec::with_capacity(half_dim); |
73 | | |
74 | | #[allow(clippy::cast_precision_loss)] |
75 | 270 | for i in 0..half_dim14 { |
76 | 270 | let exponent = -2.0 * (i as f32) / (dim as f32); |
77 | 270 | inv_freq.push(base.powf(exponent)); |
78 | 270 | } |
79 | | |
80 | 14 | Ok(Self { |
81 | 14 | dim, |
82 | 14 | base, |
83 | 14 | inv_freq, |
84 | 14 | }) |
85 | 18 | } |
86 | | |
87 | | /// Create `RoPE` with default base (10000) |
88 | | /// |
89 | | /// # Errors |
90 | | /// |
91 | | /// Returns error if `dim` is zero or odd |
92 | 7 | pub fn with_default_base(dim: usize) -> Result<Self> { |
93 | 7 | Self::new(dim, 10000.0) |
94 | 7 | } |
95 | | |
96 | | /// Apply rotary embeddings to input at given position |
97 | | /// |
98 | | /// # Arguments |
99 | | /// |
100 | | /// * `input` - Input tensor with last dimension equal to `dim` |
101 | | /// * `position` - Position index for computing rotation angles |
102 | | /// |
103 | | /// # Returns |
104 | | /// |
105 | | /// Tensor with same shape as input, with rotary embeddings applied |
106 | | /// |
107 | | /// # Errors |
108 | | /// |
109 | | /// Returns error if input's last dimension doesn't match `dim` |
110 | 10 | pub fn forward(&self, input: &Tensor<f32>, position: usize) -> Result<Tensor<f32>> { |
111 | 10 | let shape = input.shape(); |
112 | | |
113 | 10 | if shape.is_empty() { |
114 | 0 | return Err(RealizarError::InvalidShape { |
115 | 0 | reason: "Input tensor must have at least 1 dimension".to_string(), |
116 | 0 | }); |
117 | 10 | } |
118 | | |
119 | 10 | let last_dim = shape[shape.len() - 1]; |
120 | 10 | if last_dim != self.dim { |
121 | 1 | return Err(RealizarError::InvalidShape { |
122 | 1 | reason: format!("Expected last dimension {}, got {}", self.dim, last_dim), |
123 | 1 | }); |
124 | 9 | } |
125 | | |
126 | 9 | let data = input.data(); |
127 | 9 | let num_vectors = data.len() / self.dim; |
128 | 9 | let mut output = Vec::with_capacity(data.len()); |
129 | | |
130 | | // Compute sin/cos for this position |
131 | 9 | let half_dim = self.dim / 2; |
132 | 9 | let mut cos_vals = Vec::with_capacity(half_dim); |
133 | 9 | let mut sin_vals = Vec::with_capacity(half_dim); |
134 | | |
135 | | #[allow(clippy::cast_precision_loss)] |
136 | 87 | for inv_f78 in &self.inv_freq { |
137 | 78 | let angle = inv_f * (position as f32); |
138 | 78 | cos_vals.push(angle.cos()); |
139 | 78 | sin_vals.push(angle.sin()); |
140 | 78 | } |
141 | | |
142 | | // Apply rotation to each vector |
143 | 12 | for vec_idx in 0..num_vectors9 { |
144 | 12 | let offset = vec_idx * self.dim; |
145 | | |
146 | 84 | for i in 0..half_dim12 { |
147 | 84 | let x0 = data[offset + 2 * i]; |
148 | 84 | let x1 = data[offset + 2 * i + 1]; |
149 | 84 | let cos_val = cos_vals[i]; |
150 | 84 | let sin_val = sin_vals[i]; |
151 | 84 | |
152 | 84 | // Apply 2D rotation |
153 | 84 | let y0 = x0 * cos_val - x1 * sin_val; |
154 | 84 | let y1 = x0 * sin_val + x1 * cos_val; |
155 | 84 | |
156 | 84 | output.push(y0); |
157 | 84 | output.push(y1); |
158 | 84 | } |
159 | | } |
160 | | |
161 | 9 | Tensor::from_vec(shape.to_vec(), output) |
162 | 10 | } |
163 | | |
164 | | /// Get embedding dimension |
165 | | #[must_use] |
166 | 5 | pub fn dim(&self) -> usize { |
167 | 5 | self.dim |
168 | 5 | } |
169 | | |
170 | | /// Get base frequency |
171 | | #[must_use] |
172 | 3 | pub fn base(&self) -> f32 { |
173 | 3 | self.base |
174 | 3 | } |
175 | | |
176 | | /// Get inverse frequencies |
177 | | #[must_use] |
178 | 2 | pub fn inv_freq(&self) -> &[f32] { |
179 | 2 | &self.inv_freq |
180 | 2 | } |
181 | | } |
182 | | |
183 | | // ============================================================================ |
184 | | // RoPE Scaling Methods (NTK, YaRN, Linear, Dynamic NTK) |
185 | | // ============================================================================ |
186 | | // |
187 | | // These methods extend RoPE to handle longer context lengths than trained. |
188 | | // References: |
189 | | // - NTK-aware: https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ |
190 | | // - YaRN: https://arxiv.org/abs/2309.00071 |
191 | | // - Code Llama linear scaling: https://arxiv.org/abs/2308.12950 |
192 | | // ============================================================================ |
193 | | |
194 | | /// RoPE scaling type for context length extension |
195 | | #[derive(Debug, Clone, Copy, PartialEq, Default)] |
196 | | pub enum RopeScalingType { |
197 | | /// No scaling (original RoPE) |
198 | | #[default] |
199 | | None, |
200 | | /// Linear interpolation (Code Llama style) |
201 | | /// scale = trained_length / target_length |
202 | | Linear { |
203 | | /// Scale factor (typically trained_length / target_length) |
204 | | scale: f32, |
205 | | }, |
206 | | /// NTK-aware scaling |
207 | | /// Modifies base frequency: base' = base * scale^(dim / (dim - 2)) |
208 | | Ntk { |
209 | | /// Scale factor for context extension |
210 | | scale: f32, |
211 | | }, |
212 | | /// Dynamic NTK-aware scaling |
213 | | /// Adjusts scale dynamically based on current sequence length |
214 | | DynamicNtk { |
215 | | /// Original training context length |
216 | | original_max_len: usize, |
217 | | /// Target extended context length |
218 | | target_max_len: usize, |
219 | | }, |
220 | | /// YaRN (Yet another RoPE extensioN) |
221 | | /// Combines NTK interpolation with attention scaling |
222 | | Yarn { |
223 | | /// Original training context length |
224 | | original_max_len: usize, |
225 | | /// Target extended context length |
226 | | target_max_len: usize, |
227 | | /// Attention scaling factor (typically sqrt(scale)) |
228 | | attn_factor: f32, |
229 | | /// Beta for interpolation ramp (default: 32) |
230 | | beta_fast: f32, |
231 | | /// Beta for extrapolation (default: 1) |
232 | | beta_slow: f32, |
233 | | }, |
234 | | } |
235 | | |
236 | | /// Scaled Rotary Position Embeddings |
237 | | /// |
238 | | /// Extends `RoPE` with various scaling methods for context length extension. |
239 | | /// Supports NTK-aware, Linear, Dynamic NTK, and YaRN scaling. |
240 | | /// |
241 | | /// # Scaling Methods |
242 | | /// |
243 | | /// ## Linear Scaling (Code Llama) |
244 | | /// Simply scales down the position: pos' = pos / scale |
245 | | /// |
246 | | /// ## NTK-aware Scaling |
247 | | /// Modifies the base frequency to reduce high-frequency component decay: |
248 | | /// base' = base * scale^(dim / (dim - 2)) |
249 | | /// |
250 | | /// ## Dynamic NTK |
251 | | /// Dynamically adjusts NTK scale based on current sequence length |
252 | | /// |
253 | | /// ## YaRN (Yet another RoPE extensioN) |
254 | | /// Combines NTK with attention factor and interpolation ramp |
255 | | /// |
256 | | /// # References |
257 | | /// |
258 | | /// - "Code Llama: Open Foundation Models for Code" - Rozière et al., 2023 |
259 | | /// - "YaRN: Efficient Context Window Extension of Large Language Models" - Peng et al., 2023 |
260 | | #[derive(Debug, Clone)] |
261 | | pub struct ScaledRoPE { |
262 | | /// Base RoPE parameters |
263 | | dim: usize, |
264 | | /// Original base frequency |
265 | | original_base: f32, |
266 | | /// Scaled base frequency (after NTK adjustment) |
267 | | scaled_base: f32, |
268 | | /// Scaling configuration |
269 | | scaling: RopeScalingType, |
270 | | /// Precomputed inverse frequencies (with scaling applied) |
271 | | inv_freq: Vec<f32>, |
272 | | /// Attention scaling factor (for YaRN) |
273 | | mscale: f32, |
274 | | } |
275 | | |
276 | | impl ScaledRoPE { |
277 | | /// Create a new scaled `RoPE` layer |
278 | | /// |
279 | | /// # Arguments |
280 | | /// |
281 | | /// * `dim` - Embedding dimension (must be even) |
282 | | /// * `base` - Base frequency (typically 10000) |
283 | | /// * `scaling` - Scaling method to use |
284 | | /// |
285 | | /// # Errors |
286 | | /// |
287 | | /// Returns error if `dim` is zero or odd |
288 | 19 | pub fn new(dim: usize, base: f32, scaling: RopeScalingType) -> Result<Self> { |
289 | 19 | if dim == 0 { |
290 | 1 | return Err(RealizarError::InvalidShape { |
291 | 1 | reason: "dim must be > 0".to_string(), |
292 | 1 | }); |
293 | 18 | } |
294 | 18 | if !dim.is_multiple_of(2) { |
295 | 1 | return Err(RealizarError::InvalidShape { |
296 | 1 | reason: "dim must be even for RoPE".to_string(), |
297 | 1 | }); |
298 | 17 | } |
299 | | |
300 | 17 | let (scaled_base, mscale, inv_freq) = Self::compute_frequencies(dim, base, &scaling); |
301 | | |
302 | 17 | Ok(Self { |
303 | 17 | dim, |
304 | 17 | original_base: base, |
305 | 17 | scaled_base, |
306 | 17 | scaling, |
307 | 17 | inv_freq, |
308 | 17 | mscale, |
309 | 17 | }) |
310 | 19 | } |
311 | | |
312 | | /// Create scaled `RoPE` with default base (10000) |
313 | | /// |
314 | | /// # Errors |
315 | | /// |
316 | | /// Returns error if `dim` is zero or odd |
317 | 1 | pub fn with_default_base(dim: usize, scaling: RopeScalingType) -> Result<Self> { |
318 | 1 | Self::new(dim, 10000.0, scaling) |
319 | 1 | } |
320 | | |
321 | | /// Compute inverse frequencies with scaling applied |
322 | 17 | fn compute_frequencies( |
323 | 17 | dim: usize, |
324 | 17 | base: f32, |
325 | 17 | scaling: &RopeScalingType, |
326 | 17 | ) -> (f32, f32, Vec<f32>) { |
327 | 17 | let half_dim = dim / 2; |
328 | | |
329 | | // Compute scaled base and mscale based on scaling type |
330 | | #[allow(clippy::cast_precision_loss)] |
331 | 17 | let (scaled_base, mscale) = match scaling { |
332 | 10 | RopeScalingType::None | RopeScalingType::Linear { .. } => (base, 1.0), |
333 | 3 | RopeScalingType::Ntk { scale } => { |
334 | | // NTK formula: base' = base * scale^(dim / (dim - 2)) |
335 | 3 | let dim_f = dim as f32; |
336 | 3 | let exponent = dim_f / (dim_f - 2.0); |
337 | 3 | let ntk_base = base * scale.powf(exponent); |
338 | 3 | (ntk_base, 1.0) |
339 | | }, |
340 | | RopeScalingType::DynamicNtk { |
341 | 1 | original_max_len, |
342 | 1 | target_max_len, |
343 | | } => { |
344 | | // Dynamic NTK uses scale = target / original |
345 | 1 | let scale = (*target_max_len as f32) / (*original_max_len as f32); |
346 | 1 | let dim_f = dim as f32; |
347 | 1 | let exponent = dim_f / (dim_f - 2.0); |
348 | 1 | let ntk_base = base * scale.powf(exponent); |
349 | 1 | (ntk_base, 1.0) |
350 | | }, |
351 | | RopeScalingType::Yarn { |
352 | 3 | original_max_len, |
353 | 3 | target_max_len, |
354 | 3 | attn_factor, |
355 | 3 | beta_fast, |
356 | 3 | beta_slow, |
357 | | } => { |
358 | | // YaRN combines NTK with attention scaling |
359 | 3 | let scale = (*target_max_len as f32) / (*original_max_len as f32); |
360 | 3 | let dim_f = dim as f32; |
361 | | |
362 | | // Compute NTK-style base modification |
363 | | // YaRN uses a smoother interpolation based on frequency |
364 | 3 | let exponent = dim_f / (dim_f - 2.0); |
365 | 3 | let ntk_base = base * scale.powf(exponent); |
366 | | |
367 | | // Compute mscale (attention factor) |
368 | | // Default: sqrt(1 + ln(scale) / ln(original_max_len)) |
369 | 3 | let mscale = if *attn_factor > 0.0 { |
370 | 2 | *attn_factor |
371 | | } else { |
372 | 1 | let log_scale = scale.ln(); |
373 | 1 | let log_orig = (*original_max_len as f32).ln(); |
374 | 1 | (1.0 + log_scale / log_orig).sqrt() |
375 | | }; |
376 | | |
377 | | // The beta parameters affect the interpolation ramp |
378 | | // but are applied per-frequency in the forward pass |
379 | 3 | let _ = (beta_fast, beta_slow); // Used in forward |
380 | | |
381 | 3 | (ntk_base, mscale) |
382 | | }, |
383 | | }; |
384 | | |
385 | | // Compute inverse frequencies with scaled base |
386 | 17 | let mut inv_freq = Vec::with_capacity(half_dim); |
387 | | |
388 | | #[allow(clippy::cast_precision_loss)] |
389 | 426 | for i in 0..half_dim17 { |
390 | 426 | let exponent = -2.0 * (i as f32) / (dim as f32); |
391 | 426 | inv_freq.push(scaled_base.powf(exponent)); |
392 | 426 | } |
393 | | |
394 | 17 | (scaled_base, mscale, inv_freq) |
395 | 17 | } |
396 | | |
397 | | /// Apply scaled rotary embeddings to input at given position |
398 | | /// |
399 | | /// # Arguments |
400 | | /// |
401 | | /// * `input` - Input tensor with last dimension equal to `dim` |
402 | | /// * `position` - Position index for computing rotation angles |
403 | | /// |
404 | | /// # Returns |
405 | | /// |
406 | | /// Tensor with same shape as input, with scaled rotary embeddings applied |
407 | | /// |
408 | | /// # Errors |
409 | | /// |
410 | | /// Returns error if input's last dimension doesn't match `dim` |
411 | 5 | pub fn forward(&self, input: &Tensor<f32>, position: usize) -> Result<Tensor<f32>> { |
412 | 5 | let shape = input.shape(); |
413 | | |
414 | 5 | if shape.is_empty() { |
415 | 0 | return Err(RealizarError::InvalidShape { |
416 | 0 | reason: "Input tensor must have at least 1 dimension".to_string(), |
417 | 0 | }); |
418 | 5 | } |
419 | | |
420 | 5 | let last_dim = shape[shape.len() - 1]; |
421 | 5 | if last_dim != self.dim { |
422 | 1 | return Err(RealizarError::InvalidShape { |
423 | 1 | reason: format!("Expected last dimension {}, got {}", self.dim, last_dim), |
424 | 1 | }); |
425 | 4 | } |
426 | | |
427 | 4 | let data = input.data(); |
428 | 4 | let num_vectors = data.len() / self.dim; |
429 | 4 | let mut output = Vec::with_capacity(data.len()); |
430 | | |
431 | | // Compute effective position based on scaling type |
432 | | #[allow(clippy::cast_precision_loss)] |
433 | 4 | let effective_pos = match &self.scaling { |
434 | | RopeScalingType::None |
435 | | | RopeScalingType::Ntk { .. } |
436 | | | RopeScalingType::DynamicNtk { .. } |
437 | 3 | | RopeScalingType::Yarn { .. } => position as f32, |
438 | 1 | RopeScalingType::Linear { scale } => (position as f32) / scale, |
439 | | }; |
440 | | |
441 | | // Compute sin/cos for this position |
442 | 4 | let half_dim = self.dim / 2; |
443 | 4 | let mut cos_vals = Vec::with_capacity(half_dim); |
444 | 4 | let mut sin_vals = Vec::with_capacity(half_dim); |
445 | | |
446 | | // Apply YaRN interpolation ramp if applicable |
447 | | #[allow(clippy::cast_precision_loss)] |
448 | 8 | for (i, inv_f) in self.inv_freq.iter()4 .enumerate4 () { |
449 | 8 | let angle = inv_f * effective_pos; |
450 | | |
451 | | // For YaRN, apply interpolation ramp based on frequency index |
452 | 8 | let (cos_val, sin_val) = if let RopeScalingType::Yarn { |
453 | 2 | original_max_len, |
454 | 2 | target_max_len, |
455 | 2 | beta_fast, |
456 | 2 | beta_slow, |
457 | | .. |
458 | 8 | } = &self.scaling |
459 | | { |
460 | | // Compute wavelength for this frequency |
461 | 2 | let freq = 1.0 / inv_f; |
462 | 2 | let wavelength = 2.0 * std::f32::consts::PI * freq; |
463 | | |
464 | | // Compute interpolation factor |
465 | 2 | let low_freq_wavelen = (*original_max_len as f32) / *beta_slow; |
466 | 2 | let high_freq_wavelen = (*original_max_len as f32) / *beta_fast; |
467 | | |
468 | 2 | let ramp = if wavelength < high_freq_wavelen { |
469 | 1 | 0.0 // Full extrapolation (use NTK-scaled frequency) |
470 | 1 | } else if wavelength > low_freq_wavelen { |
471 | 1 | 1.0 // Full interpolation (use linear-scaled position) |
472 | | } else { |
473 | | // Linear ramp between extrapolation and interpolation |
474 | 0 | (wavelength - high_freq_wavelen) / (low_freq_wavelen - high_freq_wavelen) |
475 | | }; |
476 | | |
477 | | // Interpolate between NTK angle and linear angle |
478 | 2 | let scale = (*target_max_len as f32) / (*original_max_len as f32); |
479 | 2 | let linear_pos = effective_pos / scale; |
480 | | |
481 | | // Original frequency from unscaled base |
482 | 2 | let orig_inv_f = self |
483 | 2 | .original_base |
484 | 2 | .powf(-2.0 * (i as f32) / (self.dim as f32)); |
485 | 2 | let linear_angle = orig_inv_f * linear_pos; |
486 | | |
487 | | // Interpolate angles |
488 | 2 | let final_angle = angle * (1.0 - ramp) + linear_angle * ramp; |
489 | | |
490 | 2 | (final_angle.cos(), final_angle.sin()) |
491 | | } else { |
492 | 6 | (angle.cos(), angle.sin()) |
493 | | }; |
494 | | |
495 | 8 | cos_vals.push(cos_val); |
496 | 8 | sin_vals.push(sin_val); |
497 | | } |
498 | | |
499 | | // Apply rotation to each vector (with mscale for YaRN) |
500 | 4 | for vec_idx in 0..num_vectors { |
501 | 4 | let offset = vec_idx * self.dim; |
502 | | |
503 | 8 | for i in 0..half_dim4 { |
504 | 8 | let x0 = data[offset + 2 * i]; |
505 | 8 | let x1 = data[offset + 2 * i + 1]; |
506 | 8 | let cos_val = cos_vals[i]; |
507 | 8 | let sin_val = sin_vals[i]; |
508 | 8 | |
509 | 8 | // Apply 2D rotation with mscale |
510 | 8 | let y0 = (x0 * cos_val - x1 * sin_val) * self.mscale; |
511 | 8 | let y1 = (x0 * sin_val + x1 * cos_val) * self.mscale; |
512 | 8 | |
513 | 8 | output.push(y0); |
514 | 8 | output.push(y1); |
515 | 8 | } |
516 | | } |
517 | | |
518 | 4 | Tensor::from_vec(shape.to_vec(), output) |
519 | 5 | } |
520 | | |
521 | | /// Get embedding dimension |
522 | | #[must_use] |
523 | 6 | pub fn dim(&self) -> usize { |
524 | 6 | self.dim |
525 | 6 | } |
526 | | |
527 | | /// Get original base frequency |
528 | | #[must_use] |
529 | 2 | pub fn original_base(&self) -> f32 { |
530 | 2 | self.original_base |
531 | 2 | } |
532 | | |
533 | | /// Get scaled base frequency |
534 | | #[must_use] |
535 | 6 | pub fn scaled_base(&self) -> f32 { |
536 | 6 | self.scaled_base |
537 | 6 | } |
538 | | |
539 | | /// Get scaling configuration |
540 | | #[must_use] |
541 | 0 | pub fn scaling(&self) -> &RopeScalingType { |
542 | 0 | &self.scaling |
543 | 0 | } |
544 | | |
545 | | /// Get inverse frequencies |
546 | | #[must_use] |
547 | 1 | pub fn inv_freq(&self) -> &[f32] { |
548 | 1 | &self.inv_freq |
549 | 1 | } |
550 | | |
551 | | /// Get attention scaling factor (mscale) |
552 | | #[must_use] |
553 | 5 | pub fn mscale(&self) -> f32 { |
554 | 5 | self.mscale |
555 | 5 | } |
556 | | |
557 | | /// Compute effective context length multiplier |
558 | | /// |
559 | | /// Returns the factor by which the original context length is extended |
560 | | #[must_use] |
561 | 5 | pub fn context_length_multiplier(&self) -> f32 { |
562 | 5 | match &self.scaling { |
563 | 1 | RopeScalingType::None => 1.0, |
564 | 2 | RopeScalingType::Linear { scale1 } | RopeScalingType::Ntk { scale1 } => *scale, |
565 | | RopeScalingType::DynamicNtk { |
566 | 1 | original_max_len, |
567 | 1 | target_max_len, |
568 | | } |
569 | | | RopeScalingType::Yarn { |
570 | 1 | original_max_len, |
571 | 1 | target_max_len, |
572 | | .. |
573 | 2 | } => (*target_max_len as f32) / (*original_max_len as f32), |
574 | | } |
575 | 5 | } |
576 | | } |
577 | | |
578 | | /// Attention with Linear Biases (`ALiBi`) |
579 | | /// |
580 | | /// Replaces traditional position embeddings by adding a static, non-learned |
581 | | /// bias to query-key attention scores. The bias is proportional to the distance |
582 | | /// between positions, with head-specific slopes that enable better length |
583 | | /// extrapolation. |
584 | | /// |
585 | | /// # Algorithm |
586 | | /// |
587 | | /// For each attention head h, `ALiBi` adds the following bias to attention scores: |
588 | | /// |
589 | | /// ```text |
590 | | /// bias[i, j] = -m[h] * |i - j| |
591 | | /// ``` |
592 | | /// |
593 | | /// where m[h] is the head-specific slope computed as: |
594 | | /// - For powers of 2: m[h] = 2^(-8h/n) where n is the number of heads |
595 | | /// - For non-powers of 2: interpolation between adjacent powers of 2 |
596 | | /// |
597 | | /// # References |
598 | | /// |
599 | | /// - "Train Short, Test Long: Attention with Linear Biases Enables Input Length |
600 | | /// Extrapolation" - Press et al., ICLR 2022 |
601 | | /// - <https://arxiv.org/abs/2108.12409> |
602 | | /// |
603 | | /// # Example |
604 | | /// |
605 | | /// ```rust,ignore |
606 | | /// use realizar::layers::ALiBi; |
607 | | /// |
608 | | /// // Create ALiBi for 8 attention heads |
609 | | /// let alibi = ALiBi::new(8)?; |
610 | | /// |
611 | | /// // Get bias matrix for sequence length 10 |
612 | | /// let bias = alibi.get_bias(10)?; |
613 | | /// |
614 | | /// // Add to attention scores before softmax |
615 | | /// // scores: [seq_len, seq_len, num_heads] |
616 | | /// // scores = scores + bias |
617 | | /// ``` |
618 | | #[derive(Debug, Clone)] |
619 | | pub struct ALiBi { |
620 | | /// Number of attention heads |
621 | | num_heads: usize, |
622 | | /// Head-specific slopes (one per head) |
623 | | slopes: Vec<f32>, |
624 | | } |
625 | | |
626 | | impl ALiBi { |
627 | | /// Create a new `ALiBi` layer |
628 | | /// |
629 | | /// # Arguments |
630 | | /// |
631 | | /// * `num_heads` - Number of attention heads |
632 | | /// |
633 | | /// # Errors |
634 | | /// |
635 | | /// Returns error if `num_heads` is zero |
636 | 20 | pub fn new(num_heads: usize) -> Result<Self> { |
637 | 20 | if num_heads == 0 { |
638 | 2 | return Err(RealizarError::InvalidShape { |
639 | 2 | reason: "num_heads must be > 0".to_string(), |
640 | 2 | }); |
641 | 18 | } |
642 | | |
643 | | // Compute slopes for each head |
644 | 18 | let slopes = Self::compute_slopes(num_heads); |
645 | | |
646 | 18 | Ok(Self { num_heads, slopes }) |
647 | 20 | } |
648 | | |
649 | | /// Compute head-specific slopes following `ALiBi` paper algorithm |
650 | | /// |
651 | | /// For powers of 2: m[h] = 2^(-8h/n) |
652 | | /// For non-powers of 2: interpolate between adjacent powers of 2 |
653 | 18 | fn compute_slopes(num_heads: usize) -> Vec<f32> { |
654 | | // Find closest power of 2 |
655 | 18 | let closest_power_of_2 = if num_heads.is_power_of_two() { |
656 | 15 | num_heads |
657 | | } else { |
658 | 3 | num_heads.next_power_of_two() / 2 |
659 | | }; |
660 | | |
661 | | #[allow(clippy::cast_precision_loss)] |
662 | 18 | let ratio = 8.0 / (closest_power_of_2 as f32); |
663 | | |
664 | 18 | let mut slopes = Vec::with_capacity(num_heads); |
665 | | |
666 | | // Compute slopes for power of 2 heads |
667 | 102 | for i in 0..closest_power_of_218 .min18 (num_heads18 ) { |
668 | 102 | #[allow(clippy::cast_precision_loss)] |
669 | 102 | let exponent = -(i as f32) * ratio; |
670 | 102 | slopes.push(2_f32.powf(exponent)); |
671 | 102 | } |
672 | | |
673 | | // If not power of 2, add extra slopes with step=2 |
674 | 18 | if num_heads > closest_power_of_2 { |
675 | | #[allow(clippy::cast_precision_loss)] |
676 | 3 | let extra_ratio = 4.0 / (closest_power_of_2 as f32); |
677 | | |
678 | 10 | for i in 0..(num_heads - closest_power_of_2)3 { |
679 | 10 | #[allow(clippy::cast_precision_loss)] |
680 | 10 | let exponent = -((2 * i + 1) as f32) * extra_ratio; |
681 | 10 | slopes.push(2_f32.powf(exponent)); |
682 | 10 | } |
683 | 15 | } |
684 | | |
685 | 18 | slopes |
686 | 18 | } |
687 | | |
688 | | /// Get bias matrix for a given sequence length |
689 | | /// |
690 | | /// Returns a tensor of shape `[seq_len, seq_len, num_heads]` where: |
691 | | /// ```text |
692 | | /// bias[i, j, h] = -slopes[h] * abs(i - j) |
693 | | /// ``` |
694 | | /// |
695 | | /// # Arguments |
696 | | /// |
697 | | /// * `seq_len` - Sequence length for computing bias |
698 | | /// |
699 | | /// # Returns |
700 | | /// |
701 | | /// Tensor of shape `[seq_len, seq_len, num_heads]` containing position biases |
702 | | /// |
703 | | /// # Errors |
704 | | /// |
705 | | /// Returns error if `seq_len` is zero |
706 | 9 | pub fn get_bias(&self, seq_len: usize) -> Result<Tensor<f32>> { |
707 | 9 | if seq_len == 0 { |
708 | 1 | return Err(RealizarError::InvalidShape { |
709 | 1 | reason: "seq_len must be > 0".to_string(), |
710 | 1 | }); |
711 | 8 | } |
712 | | |
713 | 8 | let total_size = seq_len * seq_len * self.num_heads; |
714 | 8 | let mut data = Vec::with_capacity(total_size); |
715 | | |
716 | | // Compute bias for each position pair and head |
717 | 173 | for i in 0..seq_len8 { |
718 | 16.7k | for j in 0..seq_len173 { |
719 | 149k | for &slope132k in &self.slopes { |
720 | 132k | #[allow(clippy::cast_precision_loss)] |
721 | 132k | let distance = (i as f32 - j as f32).abs(); |
722 | 132k | let bias = -slope * distance; |
723 | 132k | data.push(bias); |
724 | 132k | } |
725 | | } |
726 | | } |
727 | | |
728 | 8 | Tensor::from_vec(vec![seq_len, seq_len, self.num_heads], data) |
729 | 9 | } |
730 | | |
731 | | /// Get number of attention heads |
732 | | #[must_use] |
733 | 8 | pub fn num_heads(&self) -> usize { |
734 | 8 | self.num_heads |
735 | 8 | } |
736 | | |
737 | | /// Get head-specific slopes |
738 | | #[must_use] |
739 | 9 | pub fn slopes(&self) -> &[f32] { |
740 | 9 | &self.slopes |
741 | 9 | } |
742 | | } |