entrenar/train/transformer_trainer/
trainer.rs1use crate::autograd::{checkpoint, GradScaler};
4use crate::io::{save_model, Model, ModelFormat, ModelMetadata, SaveConfig};
5use crate::lora::LoRALayer;
6use crate::optim::{clip_grad_norm_refs, AdamW, Optimizer};
7use crate::train::{CausalLMLoss, LossFn, MetricsTracker};
8use crate::transformer::Transformer;
9use crate::Tensor;
10use std::path::Path;
11
12use super::batch::LMBatch;
13use super::config::TransformerTrainConfig;
14
15pub struct TransformerTrainer {
17 model: Transformer,
19 loss_fn: CausalLMLoss,
21 optimizer: AdamW,
23 grad_scaler: GradScaler,
25 config: TransformerTrainConfig,
27 pub metrics: MetricsTracker,
29 step: usize,
31 accumulated_loss: f32,
33 accumulated_batches: usize,
35 lora_layers: Option<Vec<LoRALayer>>,
38}
39
40impl TransformerTrainer {
41 pub fn new(config: TransformerTrainConfig) -> Self {
43 let seed_guard = crate::transformer::init::lock_init_seed(config.seed);
51 let model = Transformer::new(&config.model_config);
52 drop(seed_guard);
53 Self::build(model, config)
54 }
55
56 pub fn with_model(model: Transformer, config: TransformerTrainConfig) -> Self {
58 Self::build(model, config)
59 }
60
61 fn build(model: Transformer, config: TransformerTrainConfig) -> Self {
63 let loss_fn = CausalLMLoss::new(config.model_config.vocab_size);
64 let optimizer = AdamW::default_params(config.lr);
65 let grad_scaler = GradScaler::from_config(&config.precision_config);
66
67 let lora_layers = if let Some(rank) = config.lora_rank {
69 let alpha = config.lora_alpha.unwrap_or(rank as f32 * 2.0);
70 let default_targets = vec!["q_proj".to_string(), "v_proj".to_string()];
71 let raw_targets = config.lora_target_modules.as_deref().unwrap_or(&default_targets);
73 let expanded = crate::lora::LoRAConfig::expand_shorthand(raw_targets);
74 let target_modules = expanded.as_slice();
75
76 let mut layers = Vec::new();
77 let hidden_size = config.model_config.hidden_size;
78 let num_kv_heads = config.model_config.num_kv_heads;
79 let head_dim = config.model_config.head_dim();
80 let q_dim = config.model_config.q_dim();
81 let kv_hidden_size = num_kv_heads * head_dim;
82
83 let intermediate = config.model_config.intermediate_size;
84
85 for block in &model.layers {
86 if target_modules.iter().any(|m| m == "q_proj") {
88 layers.push(LoRALayer::new(
89 block.self_attn.w_q.clone(),
90 q_dim,
91 hidden_size,
92 rank,
93 alpha,
94 ));
95 }
96 if target_modules.iter().any(|m| m == "k_proj") {
97 layers.push(LoRALayer::new(
98 block.self_attn.w_k.clone(),
99 kv_hidden_size,
100 hidden_size,
101 rank,
102 alpha,
103 ));
104 }
105 if target_modules.iter().any(|m| m == "v_proj") {
106 layers.push(LoRALayer::new(
107 block.self_attn.w_v.clone(),
108 kv_hidden_size,
109 hidden_size,
110 rank,
111 alpha,
112 ));
113 }
114 if target_modules.iter().any(|m| m == "o_proj") {
115 layers.push(LoRALayer::new(
116 block.self_attn.w_o.clone(),
117 hidden_size,
118 q_dim,
119 rank,
120 alpha,
121 ));
122 }
123 if target_modules.iter().any(|m| m == "gate_proj") {
125 layers.push(LoRALayer::new(
126 block.ffn.w_gate.clone(),
127 intermediate,
128 hidden_size,
129 rank,
130 alpha,
131 ));
132 }
133 if target_modules.iter().any(|m| m == "up_proj") {
134 layers.push(LoRALayer::new(
135 block.ffn.w_up.clone(),
136 intermediate,
137 hidden_size,
138 rank,
139 alpha,
140 ));
141 }
142 if target_modules.iter().any(|m| m == "down_proj") {
143 layers.push(LoRALayer::new(
144 block.ffn.w_down.clone(),
145 hidden_size,
146 intermediate,
147 rank,
148 alpha,
149 ));
150 }
151 }
152
153 let lora_param_count: usize =
154 layers.iter().map(|l| l.rank() * (l.d_in() + l.d_out())).sum();
155 let total_params: usize = model.parameters().iter().map(|p| p.len()).sum();
156 println!(
157 " LoRA enabled: rank={rank}, alpha={alpha}, \
158 {lora_param_count} trainable params ({:.2}% of {total_params})",
159 100.0 * lora_param_count as f64 / total_params as f64
160 );
161
162 Some(layers)
163 } else {
164 None
165 };
166
167 Self {
168 model,
169 loss_fn,
170 optimizer,
171 grad_scaler,
172 config,
173 metrics: MetricsTracker::new(),
174 step: 0,
175 accumulated_loss: 0.0,
176 accumulated_batches: 0,
177 lora_layers,
178 }
179 }
180
181 pub fn forward_single(&self, input_ids: &[u32], target_ids: &[u32]) -> (f32, Tensor, Tensor) {
187 let logits = if let Some(ref lora) = self.lora_layers {
189 self.model.forward_with_lora(input_ids, lora)
191 } else if self.config.checkpoint_config.enabled {
192 checkpoint(|_| self.model.forward(input_ids), &Tensor::zeros(1, false))
193 } else {
194 self.model.forward(input_ids)
195 };
196
197 let targets = Tensor::from_vec(target_ids.iter().map(|&id| id as f32).collect(), false);
199 let loss = self.loss_fn.forward(&logits, &targets);
200 let loss_val = loss.data()[0];
201
202 (loss_val, loss, logits)
203 }
204
205 fn compute_batch_gradients(&self, batch: &LMBatch) -> f32 {
207 let mut total_loss = 0.0;
208
209 for i in 0..batch.batch_size {
210 let Some(input_ids) = batch.get_input(i) else {
211 continue;
212 };
213 let Some(target_ids) = batch.get_target(i) else {
214 continue;
215 };
216
217 let (loss_val, loss, _logits) = self.forward_single(input_ids, target_ids);
218
219 if let Some(backward_op) = loss.backward_op() {
220 backward_op.backward();
221 }
222
223 total_loss += loss_val / self.config.accumulation_steps as f32;
224 }
225
226 total_loss / batch.batch_size as f32
227 }
228
229 fn clip_and_step(&mut self) {
231 if let Some(ref mut lora) = self.lora_layers {
233 let ratio = self.config.lora_plus_ratio;
235 if ratio != 1.0 {
236 for layer in lora.iter_mut() {
237 if let Some(grad) = layer.lora_b_mut().grad() {
238 let scaled = grad.mapv(|g| g * ratio);
239 layer.lora_b_mut().set_grad(scaled);
240 }
241 }
242 }
243
244 let mut params: Vec<&mut Tensor> =
245 lora.iter_mut().flat_map(|l| l.trainable_params()).collect();
246 for layer in &mut self.model.layers {
248 params.push(&mut layer.input_norm.weight);
249 params.push(&mut layer.post_attn_norm.weight);
250 }
251 params.push(&mut self.model.norm.weight);
252 if let Some(max_norm) = self.config.base.max_grad_norm {
255 clip_grad_norm_refs(&mut params, max_norm);
256 }
257 self.optimizer.step_refs(&mut params);
258 } else {
259 let mut params = self.model.parameters_mut();
260 if let Some(max_norm) = self.config.base.max_grad_norm {
261 clip_grad_norm_refs(&mut params, max_norm);
262 }
263 self.optimizer.step_refs(&mut params);
264 }
265
266 self.step += 1;
267 self.metrics.losses.push(self.accumulated_loss);
268 self.metrics.increment_step();
269
270 self.accumulated_loss = 0.0;
271 self.accumulated_batches = 0;
272 }
273
274 pub fn train_batch(&mut self, batch: &LMBatch) -> f32 {
278 if batch.batch_size == 0 {
279 return 0.0;
280 }
281
282 if self.accumulated_batches == 0 {
283 if let Some(ref mut lora) = self.lora_layers {
285 let mut params: Vec<&mut Tensor> =
286 lora.iter_mut().flat_map(|l| l.trainable_params()).collect();
287 for layer in &mut self.model.layers {
288 params.push(&mut layer.input_norm.weight);
289 params.push(&mut layer.post_attn_norm.weight);
290 }
291 params.push(&mut self.model.norm.weight);
292 self.optimizer.zero_grad_refs(&mut params);
293 } else {
294 let mut params = self.model.parameters_mut();
295 self.optimizer.zero_grad_refs(&mut params);
296 }
297 }
298
299 let avg_loss = self.compute_batch_gradients(batch);
300
301 self.accumulated_loss += avg_loss;
302 self.accumulated_batches += 1;
303
304 if self.accumulated_batches >= self.config.accumulation_steps {
305 self.clip_and_step();
306 }
307
308 avg_loss
309 }
310
311 pub fn train_epoch(&mut self, batches: &[LMBatch]) -> f32 {
313 self.train_epoch_with_callback(batches, |_, _, _| {})
314 }
315
316 pub fn train_epoch_with_callback<F>(&mut self, batches: &[LMBatch], mut on_batch: F) -> f32
324 where
325 F: FnMut(usize, f32, &Self),
326 {
327 if batches.is_empty() {
328 return 0.0;
329 }
330
331 let mut total_loss = 0.0;
332 let mut batches_processed = 0;
333
334 for (i, batch) in batches.iter().enumerate() {
335 if let Some(max) = self.config.max_steps {
337 if self.step >= max {
338 break;
339 }
340 }
341
342 let batch_loss = self.train_batch(batch);
343 total_loss += batch_loss;
344 batches_processed += 1;
345 on_batch(i, batch_loss, self);
346 }
347
348 total_loss / batches_processed.max(1) as f32
349 }
350
351 pub fn reached_max_steps(&self) -> bool {
353 self.config.max_steps.is_some_and(|max| self.step >= max)
354 }
355
356 pub fn step(&self) -> usize {
358 self.step
359 }
360
361 pub fn model(&self) -> &Transformer {
363 &self.model
364 }
365
366 pub fn model_mut(&mut self) -> &mut Transformer {
368 &mut self.model
369 }
370
371 pub fn current_lr(&self) -> f32 {
373 let base_lr = self.config.lr;
374
375 if self.step < self.config.warmup_steps {
376 base_lr * (self.step as f32 / self.config.warmup_steps as f32)
378 } else {
379 base_lr
380 }
381 }
382
383 pub fn grad_scaler_stats(&self) -> (f32, usize, usize) {
385 (
386 self.grad_scaler.scale(),
387 self.grad_scaler.overflow_count(),
388 self.grad_scaler.successful_steps(),
389 )
390 }
391
392 pub fn is_mixed_precision(&self) -> bool {
394 self.config.precision_config.is_mixed()
395 }
396
397 pub fn is_checkpointing(&self) -> bool {
399 self.config.checkpoint_config.enabled
400 }
401
402 pub fn is_lora(&self) -> bool {
404 self.lora_layers.is_some()
405 }
406
407 pub fn lora_layers(&self) -> Option<&[LoRALayer]> {
409 self.lora_layers.as_deref()
410 }
411
412 pub fn lora_layers_mut(&mut self) -> Option<&mut Vec<LoRALayer>> {
414 self.lora_layers.as_mut()
415 }
416
417 pub fn save_lora_adapter(
429 &self,
430 output_dir: impl AsRef<Path>,
431 base_model_name: Option<&str>,
432 ) -> crate::Result<()> {
433 let lora = self.lora_layers.as_ref().ok_or_else(|| {
434 crate::error::Error::ConfigError("Cannot save adapter: LoRA not enabled".into())
435 })?;
436
437 let rank = self.config.lora_rank.unwrap_or(8);
438 let alpha = self.config.lora_alpha.unwrap_or(rank as f32 * 2.0);
439 let target_modules = self
440 .config
441 .lora_target_modules
442 .clone()
443 .unwrap_or_else(|| vec!["q_proj".to_string(), "v_proj".to_string()]);
444
445 let expanded = crate::lora::LoRAConfig::expand_shorthand(&target_modules);
447 let lora_config = crate::lora::LoRAConfig::new(rank, alpha)
448 .target_modules(&expanded.iter().map(String::as_str).collect::<Vec<_>>());
449
450 let num_layers = self.model.layers.len();
453
454 let module_paths: Vec<(&str, &str)> = [
456 ("q_proj", "self_attn.q_proj"),
457 ("k_proj", "self_attn.k_proj"),
458 ("v_proj", "self_attn.v_proj"),
459 ("o_proj", "self_attn.o_proj"),
460 ("gate_proj", "mlp.gate_proj"),
461 ("up_proj", "mlp.up_proj"),
462 ("down_proj", "mlp.down_proj"),
463 ]
464 .iter()
465 .filter(|(name, _)| expanded.iter().any(|t| t == *name))
466 .copied()
467 .collect();
468
469 let all_names: Vec<String> = (0..num_layers)
471 .flat_map(|i| {
472 module_paths.iter().map(move |(_, path)| format!("model.layers.{i}.{path}"))
473 })
474 .collect();
475
476 let mut adapters: Vec<(&str, &LoRALayer)> = Vec::new();
477 for (idx, layer) in lora.iter().enumerate() {
478 if idx < all_names.len() {
479 adapters.push((&all_names[idx], layer));
480 }
481 }
482
483 crate::lora::save_adapter_peft(&adapters, &lora_config, base_model_name, output_dir)
484 .map_err(|e| crate::error::Error::Io(e.to_string()))
485 }
486
487 pub fn save(
502 &self,
503 path: impl AsRef<Path>,
504 name: &str,
505 architecture: &str,
506 ) -> crate::Result<()> {
507 let params: Vec<(String, Tensor)> = self
509 .model
510 .named_parameters()
511 .into_iter()
512 .map(|(name, tensor)| (name, tensor.clone()))
513 .collect();
514
515 let metadata = ModelMetadata::new(name, architecture);
516 let model = Model::new(metadata, params);
517 let config = SaveConfig::new(ModelFormat::SafeTensors);
518
519 save_model(&model, path, &config)
520 }
521
522 pub fn save_apr(
537 &self,
538 path: impl AsRef<Path>,
539 name: &str,
540 architecture: &str,
541 ) -> crate::Result<()> {
542 let params: Vec<(String, Tensor)> =
543 self.model.named_parameters().into_iter().map(|(n, t)| (n, t.clone())).collect();
544 let metadata = ModelMetadata::new(name, architecture);
545 let model = Model::new(metadata, params);
546 let config = SaveConfig::new(ModelFormat::Apr);
547 save_model(&model, path, &config)
548 }
549
550 #[must_use]
561 pub fn optimizer_state_sha256(&self) -> String {
562 use sha2::{Digest, Sha256};
563 let mut hasher = Sha256::new();
564 hasher.update(b"aprender-train:adamw:optstate:v1");
565 hasher.update(self.optimizer.step_count().to_le_bytes());
566 let moment_streams: [(&[u8], &[Option<ndarray::Array1<f32>>]); 2] =
567 [(b"m", self.optimizer.first_moments()), (b"v", self.optimizer.second_moments())];
568 for (tag, buffers) in moment_streams {
569 hasher.update(tag);
570 hasher.update((buffers.len() as u64).to_le_bytes());
571 for slot in buffers {
572 match slot {
573 Some(arr) => {
574 hasher.update(b"some");
575 hasher.update((arr.len() as u64).to_le_bytes());
576 let bytes: &[u8] = bytemuck::cast_slice(
577 arr.as_slice().expect("AdamW buffers are contiguous"),
578 );
579 hasher.update(bytes);
580 }
581 None => hasher.update(b"none"),
582 }
583 }
584 }
585 format!("{:x}", hasher.finalize())
586 }
587}
588
589#[cfg(test)]
590mod clip_and_step_tests {
591 use super::*;
592 use crate::transformer::TransformerConfig;
593
594 #[test]
601 fn falsify_clip_and_step_actually_clips_gradients() {
602 let config = TransformerTrainConfig::new(TransformerConfig::tiny())
603 .with_lr(0.001)
604 .with_grad_clip(1.0);
605 let mut trainer = TransformerTrainer::new(config);
606
607 for p in trainer.model.parameters() {
609 p.set_grad(ndarray::Array1::from_elem(p.len(), 10.0_f32));
610 }
611 let norm = |t: &TransformerTrainer| -> f32 {
612 t.model
613 .parameters()
614 .iter()
615 .filter_map(|p| p.grad())
616 .map(|g| g.iter().map(|x| x * x).sum::<f32>())
617 .sum::<f32>()
618 .sqrt()
619 };
620 let before = norm(&trainer);
621 assert!(before > 1.0, "precondition: injected grad norm {before} must exceed max_norm 1.0");
622
623 trainer.clip_and_step();
624
625 let after = norm(&trainer);
628 assert!(
629 after <= 1.0 + 1e-2,
630 "clip_and_step did not clip gradients: global norm = {after} (expected <= 1.0); --grad-clip is a no-op"
631 );
632 }
633}