1use crate::common::{BiasCorrection, ParameterUpdate};
16use std::alloc::{alloc, dealloc, Layout};
17use std::collections::HashMap;
18use std::ptr::{self, NonNull};
19use trustformers_core::errors::{Result, TrustformersError};
20use trustformers_core::tensor::Tensor;
21use trustformers_core::traits::Optimizer;
22
23#[derive(Debug, Clone, Copy)]
25pub struct AlignmentConfig {
26 pub cache_line_size: usize,
28 pub vector_size: usize,
30 pub page_size: usize,
32 pub use_huge_pages: bool,
34}
35
36impl Default for AlignmentConfig {
37 fn default() -> Self {
38 Self {
39 cache_line_size: 64,
40 vector_size: 32, page_size: 4096,
42 use_huge_pages: false,
43 }
44 }
45}
46
47impl AlignmentConfig {
48 pub fn avx512() -> Self {
50 Self {
51 vector_size: 64,
52 ..Default::default()
53 }
54 }
55
56 pub fn with_huge_pages() -> Self {
58 Self {
59 use_huge_pages: true,
60 ..Default::default()
61 }
62 }
63
64 pub fn alignment_for_size(&self, size: usize) -> usize {
66 if size >= self.page_size {
67 self.page_size
68 } else if size >= self.cache_line_size {
69 self.cache_line_size
70 } else {
71 self.vector_size.min(size)
72 }
73 }
74}
75
76#[derive(Debug)]
78pub struct AlignedAllocator {
79 config: AlignmentConfig,
80 allocated_blocks: Vec<(NonNull<u8>, Layout)>,
81}
82
83impl AlignedAllocator {
84 pub fn new(config: AlignmentConfig) -> Self {
86 Self {
87 config,
88 allocated_blocks: Vec::new(),
89 }
90 }
91
92 pub fn allocate_aligned<T>(&mut self, count: usize) -> Result<NonNull<T>> {
94 let size = count * std::mem::size_of::<T>();
95 let alignment = self.config.alignment_for_size(size);
96
97 let layout = Layout::from_size_align(size, alignment).map_err(|e| {
98 TrustformersError::tensor_op_error(
99 &format!("Invalid layout: {}", e),
100 "allocate_aligned",
101 )
102 })?;
103
104 let ptr = unsafe { alloc(layout) };
105 if ptr.is_null() {
106 return Err(TrustformersError::tensor_op_error(
107 "Memory allocation failed",
108 "allocate_aligned",
109 ));
110 }
111
112 let non_null = NonNull::new(ptr).ok_or_else(|| {
113 TrustformersError::tensor_op_error("Null pointer in allocation", "allocate_aligned")
114 })?;
115
116 self.allocated_blocks.push((non_null, layout));
117
118 let typed_ptr = non_null.as_ptr() as *mut T;
120 NonNull::new(typed_ptr).ok_or_else(|| {
121 TrustformersError::tensor_op_error("Type casting failed", "allocate_aligned")
122 })
123 }
124
125 pub fn allocate_initialized<T: Clone>(&mut self, count: usize, value: T) -> Result<NonNull<T>> {
127 let ptr = self.allocate_aligned::<T>(count)?;
128
129 unsafe {
130 for i in 0..count {
131 ptr::write(ptr.as_ptr().add(i), value.clone());
132 }
133 }
134
135 Ok(ptr)
136 }
137
138 pub fn memory_usage(&self) -> usize {
140 self.allocated_blocks.iter().map(|(_, layout)| layout.size()).sum()
141 }
142}
143
144impl Drop for AlignedAllocator {
145 fn drop(&mut self) {
146 for (ptr, layout) in &self.allocated_blocks {
147 unsafe {
148 dealloc(ptr.as_ptr(), *layout);
149 }
150 }
151 }
152}
153
154unsafe impl Send for AlignedAllocator {}
157unsafe impl Sync for AlignedAllocator {}
158
159#[derive(Debug)]
173pub struct SoAOptimizerState {
174 momentum: Vec<f32>,
176 variance: Vec<f32>,
178 parameters: Vec<ParameterInfo>,
180 parameter_index: HashMap<String, usize>,
182 step: usize,
184 alignment: AlignmentConfig,
186}
187
188#[derive(Debug, Clone)]
190pub struct ParameterInfo {
191 pub id: String,
193 pub momentum_offset: usize,
195 pub variance_offset: usize,
197 pub size: usize,
199 pub chunk_size: usize,
201}
202
203impl SoAOptimizerState {
204 pub fn new(alignment: AlignmentConfig) -> Self {
206 Self {
207 momentum: Vec::new(),
208 variance: Vec::new(),
209 parameters: Vec::new(),
210 parameter_index: HashMap::new(),
211 step: 0,
212 alignment,
213 }
214 }
215
216 fn cache_line_elements(&self) -> usize {
218 (self.alignment.cache_line_size / std::mem::size_of::<f32>()).max(1)
219 }
220
221 pub fn add_parameter(&mut self, id: String, size: usize) -> Result<()> {
229 if self.parameter_index.contains_key(&id) {
230 return Ok(());
231 }
232 if size == 0 {
233 return Err(TrustformersError::tensor_op_error(
234 "cannot register a zero-sized parameter in the SoA layout",
235 "add_parameter",
236 ));
237 }
238
239 let chunk_size = self.calculate_optimal_chunk_size(size);
241
242 let line = self.cache_line_elements();
245 let momentum_offset = self.momentum.len().div_ceil(line) * line;
246 let variance_offset = self.variance.len().div_ceil(line) * line;
247
248 self.momentum.resize(momentum_offset + size, 0.0);
249 self.variance.resize(variance_offset + size, 0.0);
250
251 let param_info = ParameterInfo {
252 id: id.clone(),
253 momentum_offset,
254 variance_offset,
255 size,
256 chunk_size,
257 };
258
259 self.parameter_index.insert(id, self.parameters.len());
260 self.parameters.push(param_info);
261 Ok(())
262 }
263
264 fn calculate_optimal_chunk_size(&self, size: usize) -> usize {
266 let vector_elements = self.alignment.vector_size / std::mem::size_of::<f32>();
267 let cache_line_elements = self.alignment.cache_line_size / std::mem::size_of::<f32>();
268
269 let min_chunk = vector_elements;
271 let preferred_chunk = cache_line_elements;
272
273 if size >= preferred_chunk {
274 preferred_chunk
275 } else if size >= min_chunk {
276 (size / min_chunk) * min_chunk
278 } else {
279 size
280 }
281 }
282
283 pub fn get_parameter_info(&self, id: &str) -> Option<&ParameterInfo> {
285 self.parameter_index.get(id).and_then(|&i| self.parameters.get(i))
286 }
287
288 pub fn momentum_of(&self, id: &str) -> Option<&[f32]> {
290 let info = self.get_parameter_info(id)?;
291 self.momentum.get(info.momentum_offset..info.momentum_offset + info.size)
292 }
293
294 pub fn variance_of(&self, id: &str) -> Option<&[f32]> {
296 let info = self.get_parameter_info(id)?;
297 self.variance.get(info.variance_offset..info.variance_offset + info.size)
298 }
299
300 pub fn step_count(&self) -> usize {
302 self.step
303 }
304
305 pub fn update_parameter_soa(
307 &mut self,
308 param_id: &str,
309 param: &mut [f32],
310 grad: &[f32],
311 lr: f32,
312 betas: (f32, f32),
313 eps: f32,
314 weight_decay: f32,
315 ) -> Result<()> {
316 let param_info = self
317 .get_parameter_info(param_id)
318 .ok_or_else(|| {
319 TrustformersError::tensor_op_error("Parameter not found", "update_parameter_soa")
320 })?
321 .clone();
322
323 if param.len() != param_info.size || grad.len() != param_info.size {
324 return Err(TrustformersError::tensor_op_error(
325 "Size mismatch",
326 "update_parameter_soa",
327 ));
328 }
329
330 self.step += 1;
331 let (bias_correction1, bias_correction2) =
332 BiasCorrection::compute_adam_corrections(betas.0, betas.1, self.step);
333
334 let chunk_size = param_info.chunk_size;
336 let num_chunks = param_info.size.div_ceil(chunk_size);
337
338 for chunk_idx in 0..num_chunks {
339 let start = chunk_idx * chunk_size;
340 let end = (start + chunk_size).min(param_info.size);
341
342 self.process_chunk_soa(
343 &mut param[start..end],
344 &grad[start..end],
345 start,
346 ¶m_info,
347 lr,
348 betas,
349 bias_correction1,
350 bias_correction2,
351 eps,
352 weight_decay,
353 )?;
354 }
355
356 Ok(())
357 }
358
359 fn process_chunk_soa(
364 &mut self,
365 param_chunk: &mut [f32],
366 grad_chunk: &[f32],
367 offset: usize,
368 param_info: &ParameterInfo,
369 lr: f32,
370 betas: (f32, f32),
371 bias_correction1: f32,
372 bias_correction2: f32,
373 eps: f32,
374 weight_decay: f32,
375 ) -> Result<()> {
376 let momentum_start = param_info.momentum_offset + offset;
377 let variance_start = param_info.variance_offset + offset;
378 let len = param_chunk.len();
379
380 let momentum_slice =
383 self.momentum.get_mut(momentum_start..momentum_start + len).ok_or_else(|| {
384 TrustformersError::tensor_op_error(
385 "momentum block out of range for SoA layout",
386 "process_chunk_soa",
387 )
388 })?;
389 let variance_slice =
390 self.variance.get_mut(variance_start..variance_start + len).ok_or_else(|| {
391 TrustformersError::tensor_op_error(
392 "variance block out of range for SoA layout",
393 "process_chunk_soa",
394 )
395 })?;
396
397 for i in 0..len {
398 let grad_val = grad_chunk[i] + weight_decay * param_chunk[i];
399
400 let momentum = &mut momentum_slice[i];
402 let variance = &mut variance_slice[i];
403
404 ParameterUpdate::update_ema(momentum, grad_val, betas.0);
406 ParameterUpdate::update_ema(variance, grad_val * grad_val, betas.1);
407
408 let m_hat = *momentum / bias_correction1;
410 let v_hat = *variance / bias_correction2;
411
412 ParameterUpdate::adam_update(&mut param_chunk[i], lr, m_hat, v_hat, eps);
414 }
415
416 Ok(())
417 }
418
419 pub fn layout_stats(&self) -> LayoutStats {
421 let momentum_memory = self.momentum.len() * std::mem::size_of::<f32>();
422 let variance_memory = self.variance.len() * std::mem::size_of::<f32>();
423 let total_elements: usize = self.parameters.iter().map(|p| p.size).sum();
424
425 LayoutStats {
426 total_parameters: self.parameters.len(),
427 total_elements,
428 momentum_memory_bytes: momentum_memory,
429 variance_memory_bytes: variance_memory,
430 total_memory_bytes: momentum_memory + variance_memory,
431 alignment_config: self.alignment,
432 cache_line_utilization: self.calculate_cache_line_utilization(),
433 }
434 }
435
436 fn calculate_cache_line_utilization(&self) -> f32 {
438 if self.parameters.is_empty() {
439 return 1.0;
440 }
441
442 let cache_line_elements = self.alignment.cache_line_size / std::mem::size_of::<f32>();
443 let mut total_utilization = 0.0;
444
445 for param in &self.parameters {
446 let lines_used = param.size.div_ceil(cache_line_elements);
447 let elements_in_lines = lines_used * cache_line_elements;
448 let utilization = param.size as f32 / elements_in_lines as f32;
449 total_utilization += utilization;
450 }
451
452 total_utilization / self.parameters.len() as f32
453 }
454}
455
456unsafe impl Send for SoAOptimizerState {}
458unsafe impl Sync for SoAOptimizerState {}
459
460#[derive(Debug, Clone)]
462pub struct LayoutStats {
463 pub total_parameters: usize,
465 pub total_elements: usize,
467 pub momentum_memory_bytes: usize,
469 pub variance_memory_bytes: usize,
471 pub total_memory_bytes: usize,
473 pub alignment_config: AlignmentConfig,
475 pub cache_line_utilization: f32,
477}
478
479impl LayoutStats {
480 pub fn memory_overhead(&self) -> f32 {
482 let naive_memory = self.total_elements * std::mem::size_of::<f32>() * 2; if naive_memory == 0 {
484 return 0.0;
485 }
486 (self.total_memory_bytes as f32 / naive_memory as f32) - 1.0
487 }
488
489 pub fn optimization_suggestions(&self) -> Vec<String> {
491 let mut suggestions = Vec::new();
492
493 if self.cache_line_utilization < 0.8 {
494 suggestions.push("Poor cache line utilization; consider parameter padding".to_string());
495 }
496
497 let overhead = self.memory_overhead();
498 if overhead > 0.2 {
499 suggestions.push(format!(
500 "High memory overhead ({:.1}%); review alignment requirements",
501 overhead * 100.0
502 ));
503 }
504
505 if self.alignment_config.vector_size > 32 && self.total_elements < 1000 {
506 suggestions.push("Vector size may be too large for small parameters".to_string());
507 }
508
509 if !self.alignment_config.use_huge_pages && self.total_memory_bytes > 1024 * 1024 {
510 suggestions.push("Consider enabling huge pages for large memory usage".to_string());
511 }
512
513 if suggestions.is_empty() {
514 suggestions.push("Memory layout appears well optimized".to_string());
515 }
516
517 suggestions
518 }
519}
520
521#[derive(Debug)]
523pub struct LayoutOptimizedAdam {
524 lr: f32,
526 betas: (f32, f32),
528 eps: f32,
530 weight_decay: f32,
532 state: SoAOptimizerState,
534 params: crate::param_id::ParamRegistry,
539}
540
541impl LayoutOptimizedAdam {
542 pub fn new(lr: f32, betas: (f32, f32), eps: f32, weight_decay: f32) -> Self {
544 Self::with_alignment(lr, betas, eps, weight_decay, AlignmentConfig::default())
545 }
546
547 pub fn with_alignment(
549 lr: f32,
550 betas: (f32, f32),
551 eps: f32,
552 weight_decay: f32,
553 alignment: AlignmentConfig,
554 ) -> Self {
555 Self {
556 lr,
557 betas,
558 eps,
559 weight_decay,
560 state: SoAOptimizerState::new(alignment),
561 params: crate::param_id::ParamRegistry::new(),
562 }
563 }
564
565 pub fn avx512_optimized(lr: f32, betas: (f32, f32), eps: f32, weight_decay: f32) -> Self {
567 Self::with_alignment(lr, betas, eps, weight_decay, AlignmentConfig::avx512())
568 }
569
570 pub fn layout_stats(&self) -> LayoutStats {
572 self.state.layout_stats()
573 }
574
575 pub fn add_parameter(&mut self, id: String, size: usize) -> Result<()> {
577 self.state.add_parameter(id, size)
578 }
579}
580
581impl Optimizer for LayoutOptimizedAdam {
582 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
583 match (parameter, grad) {
584 (Tensor::F32(param), Tensor::F32(grad_arr)) => {
585 let param_id = self.params.key_for_addr(param.as_ptr() as usize, param.len())?;
586
587 if self.state.get_parameter_info(¶m_id).is_none() {
589 self.state.add_parameter(param_id.clone(), param.len())?;
590 }
591
592 let param_slice = param.as_slice_mut().ok_or_else(|| {
593 TrustformersError::tensor_op_error(
594 "Failed to get mutable slice from param tensor",
595 "update",
596 )
597 })?;
598 let grad_slice = grad_arr.as_slice().ok_or_else(|| {
599 TrustformersError::tensor_op_error(
600 "Failed to get slice from gradient tensor",
601 "update",
602 )
603 })?;
604 self.state.update_parameter_soa(
605 ¶m_id,
606 param_slice,
607 grad_slice,
608 self.lr,
609 self.betas,
610 self.eps,
611 self.weight_decay,
612 )
613 },
614 _ => Err(TrustformersError::tensor_op_error(
615 "Unsupported tensor types for LayoutOptimizedAdam",
616 "update",
617 )),
618 }
619 }
620
621 fn zero_grad(&mut self) {
622 }
624
625 fn step(&mut self) {
626 }
628
629 fn get_lr(&self) -> f32 {
630 self.lr
631 }
632
633 fn set_lr(&mut self, lr: f32) {
634 self.lr = lr;
635 }
636}
637
638unsafe impl Send for LayoutOptimizedAdam {}
640unsafe impl Sync for LayoutOptimizedAdam {}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn test_alignment_config() {
648 let config = AlignmentConfig::default();
649 assert_eq!(config.cache_line_size, 64);
650 assert_eq!(config.vector_size, 32);
651 assert!(!config.use_huge_pages);
652
653 let avx512_config = AlignmentConfig::avx512();
654 assert_eq!(avx512_config.vector_size, 64);
655
656 let alignment = config.alignment_for_size(1000);
657 assert!(alignment > 0);
658 assert!(alignment <= config.cache_line_size);
659 }
660
661 #[test]
662 fn test_aligned_allocator() {
663 let config = AlignmentConfig::default();
664 let mut allocator = AlignedAllocator::new(config);
665
666 let _ptr = allocator.allocate_aligned::<f32>(1000).expect("Operation failed in test");
667 let memory_usage = allocator.memory_usage();
670 assert!(memory_usage >= 1000 * std::mem::size_of::<f32>());
671 }
672
673 #[test]
674 fn test_soa_optimizer_state() {
675 let config = AlignmentConfig::default();
676 let mut state = SoAOptimizerState::new(config);
677
678 state
679 .add_parameter("param1".to_string(), 1000)
680 .expect("Operation failed in test");
681 assert!(state.get_parameter_info("param1").is_some());
682
683 let stats = state.layout_stats();
684 assert_eq!(stats.total_parameters, 1);
685 assert_eq!(stats.total_elements, 1000);
686 }
687
688 #[test]
689 fn test_layout_optimized_adam() {
690 let optimizer = LayoutOptimizedAdam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
691 assert_eq!(optimizer.get_lr(), 1e-3);
692 assert_eq!(optimizer.betas, (0.9, 0.999));
693
694 let stats = optimizer.layout_stats();
695 assert_eq!(stats.total_parameters, 0);
696 }
697
698 #[test]
699 fn test_layout_stats() {
700 let config = AlignmentConfig::default();
701 let mut state = SoAOptimizerState::new(config);
702
703 state
704 .add_parameter("param1".to_string(), 100)
705 .expect("Operation failed in test");
706 state
707 .add_parameter("param2".to_string(), 200)
708 .expect("Operation failed in test");
709
710 let stats = state.layout_stats();
711 assert_eq!(stats.total_parameters, 2);
712 assert_eq!(stats.total_elements, 300);
713 assert!(stats.cache_line_utilization > 0.0);
714 assert!(stats.cache_line_utilization <= 1.0);
715
716 let overhead = stats.memory_overhead();
717 assert!(overhead >= 0.0);
718
719 let suggestions = stats.optimization_suggestions();
720 assert!(!suggestions.is_empty());
721 }
722
723 #[test]
724 fn test_chunk_size_calculation() {
725 let config = AlignmentConfig::default();
726 let state = SoAOptimizerState::new(config);
727
728 let chunk_size_large = state.calculate_optimal_chunk_size(10000);
729 let chunk_size_small = state.calculate_optimal_chunk_size(5);
730
731 assert!(chunk_size_large > chunk_size_small);
732 assert!(
733 chunk_size_large.is_multiple_of(config.vector_size / std::mem::size_of::<f32>())
734 || chunk_size_large == 10000
735 );
736 }
737
738 #[test]
739 fn test_avx512_optimization() {
740 let optimizer = LayoutOptimizedAdam::avx512_optimized(1e-3, (0.9, 0.999), 1e-8, 0.01);
741 let stats = optimizer.layout_stats();
742 assert_eq!(stats.alignment_config.vector_size, 64);
743 }
744
745 #[test]
749 fn test_soa_state_persists_across_steps() {
750 let mut state = SoAOptimizerState::new(AlignmentConfig::default());
751 state.add_parameter("w".to_string(), 4).expect("register");
752
753 let mut param = vec![0.0_f32; 4];
754 let grad = vec![1.0_f32; 4];
755
756 state
757 .update_parameter_soa("w", &mut param, &grad, 0.1, (0.9, 0.999), 1e-8, 0.0)
758 .expect("step 1");
759 let momentum_after_first = state.momentum_of("w").expect("momentum block").to_vec();
760 assert!(
761 momentum_after_first.iter().all(|m| (m - 0.1).abs() < 1e-6),
762 "first EMA must be (1-beta1)*g = 0.1, got {momentum_after_first:?}"
763 );
764
765 let before_second = param.clone();
768 let zero_grad = vec![0.0_f32; 4];
769 state
770 .update_parameter_soa("w", &mut param, &zero_grad, 0.1, (0.9, 0.999), 1e-8, 0.0)
771 .expect("step 2");
772
773 let momentum_after_second = state.momentum_of("w").expect("momentum block").to_vec();
774 assert!(
775 momentum_after_second.iter().all(|m| (m - 0.09).abs() < 1e-6),
776 "second EMA must decay to beta1*0.1 = 0.09, got {momentum_after_second:?}"
777 );
778 for (before, after) in before_second.iter().zip(param.iter()) {
779 assert!(
780 (before - after).abs() > 1e-6,
781 "carried momentum must still move the parameter on a zero gradient"
782 );
783 }
784 }
785
786 #[test]
788 fn test_soa_parameters_get_disjoint_blocks() {
789 let mut state = SoAOptimizerState::new(AlignmentConfig::default());
790 state.add_parameter("a".to_string(), 4).expect("a");
791 state.add_parameter("b".to_string(), 4).expect("b");
792
793 let mut param_a = vec![0.0_f32; 4];
794 let mut param_b = vec![0.0_f32; 4];
795 let grad_a = vec![1.0_f32; 4];
796 let grad_b = vec![0.0_f32; 4];
797
798 state
799 .update_parameter_soa("a", &mut param_a, &grad_a, 0.1, (0.9, 0.999), 1e-8, 0.0)
800 .expect("update a");
801 state
802 .update_parameter_soa("b", &mut param_b, &grad_b, 0.1, (0.9, 0.999), 1e-8, 0.0)
803 .expect("update b");
804
805 let momentum_b = state.momentum_of("b").expect("b momentum");
806 assert!(
807 momentum_b.iter().all(|m| m.abs() < 1e-9),
808 "parameter b saw a zero gradient; its momentum must stay zero: {momentum_b:?}"
809 );
810 let momentum_a = state.momentum_of("a").expect("a momentum");
811 assert!(momentum_a.iter().all(|m| *m > 0.0), "a must have momentum");
812 }
813
814 #[test]
816 fn test_layout_optimized_adam_converges_on_quadratic() {
817 let mut optimizer = LayoutOptimizedAdam::new(0.05, (0.9, 0.999), 1e-8, 0.0);
818 let mut param = Tensor::from_vec(vec![1.0_f32; 4], &[4]).expect("param");
819 let initial_loss: f32 = param.data().expect("data").iter().map(|v| v * v).sum();
820
821 for _ in 0..400 {
822 let grad_data: Vec<f32> = param.data().expect("data").iter().map(|v| 2.0 * v).collect();
823 let grad = Tensor::from_vec(grad_data, &[4]).expect("grad");
824 optimizer.update(&mut param, &grad).expect("update");
825 }
826
827 let final_loss: f32 = param.data().expect("data").iter().map(|v| v * v).sum();
828 assert!(
829 final_loss < initial_loss * 1e-2,
830 "loss must decrease: {initial_loss} -> {final_loss}"
831 );
832 }
833}