Skip to main content

entrenar/finetune/instruct_pipeline/
accessors.rs

1//! Small accessor and utility methods on `InstructPipeline`:
2//! `tokenize`, `has_tokenizer`, `num_trainable_parameters`, `set_learning_rate`,
3//! `learning_rate`, `set_model_path`, `sync_lora_to_cpu`, `is_cuda`, `gpu_name`,
4//! `gpu_total_memory`, `summary`, `tokenizer`.
5
6#[allow(clippy::wildcard_imports)]
7use super::*;
8
9#[cfg(feature = "cuda")]
10use crate::autograd::cuda_training::CudaTrainer;
11
12impl InstructPipeline {
13    /// Tokenize text without truncation.
14    ///
15    /// Returns the full token sequence. Callers (e.g., `train_step`) are
16    /// responsible for budget allocation and truncation of the concatenated
17    /// prompt+response sequence.
18    ///
19    /// Falls back to byte-level encoding (each UTF-8 byte as a u32 token ID)
20    /// when no BPE tokenizer is loaded.
21    pub fn tokenize(&self, text: &str) -> Vec<u32> {
22        match self.tokenizer.as_ref() {
23            Some(tok) => tok.encode(text),
24            None => {
25                // Byte-level fallback when no BPE tokenizer is loaded
26                text.bytes().map(u32::from).collect()
27            }
28        }
29    }
30
31    /// Returns `true` if a BPE tokenizer is loaded.
32    #[must_use]
33    pub fn has_tokenizer(&self) -> bool {
34        self.tokenizer.is_some()
35    }
36
37    /// Number of trainable LoRA parameters.
38    #[must_use]
39    pub fn num_trainable_parameters(&self) -> usize {
40        // LoRA layers store weight + lora_a + lora_b; we count lora_a + lora_b
41        self.lora_layers.len()
42            * 2
43            * self.config.lora_rank
44            * (self.lora_layers.first().map_or(0, |_| {
45                // Approximate: each LoRA pair has rank * (rows + cols) params
46                // This is a rough estimate since layers may differ in size
47                1
48            }))
49    }
50
51    /// Update learning rate (for LR scheduling).
52    pub fn set_learning_rate(&mut self, lr: f32) {
53        self.optimizer.set_lr(lr);
54    }
55
56    /// Get current learning rate.
57    #[must_use]
58    pub fn learning_rate(&self) -> f32 {
59        contract_pre_learning_rate_scaling!();
60        self.optimizer.lr()
61    }
62
63    /// Set model path for checkpoint provenance.
64    pub fn set_model_path(&mut self, path: &Path) {
65        self.model_dir = Some(path.to_path_buf());
66    }
67
68    /// Synchronize GPU LoRA weights back to CPU LoRA layers (NF4 QLoRA).
69    ///
70    /// Required for checkpointing after NF4 QLoRA training. Downloads A_q, B_q,
71    /// A_v, B_v from each NF4 block and updates the corresponding CPU LoRA layers.
72    ///
73    /// # Contract (C-QLORA-CKPT-001)
74    ///
75    /// - **Precondition**: NF4 QLoRA training completed (optimizer steps applied)
76    /// - **Postcondition**: CPU LoRA layers match GPU-trained LoRA weights
77    #[cfg(feature = "cuda")]
78    pub fn sync_lora_to_cpu(&mut self) {
79        let blocks = match self.cuda_blocks.as_ref() {
80            Some(b) => b,
81            None => return,
82        };
83
84        let lora_scale = self.config.lora_alpha / self.config.lora_rank.max(1) as f32;
85        let inv_scale = if lora_scale.abs() > 1e-10 { 1.0 / lora_scale } else { 1.0 };
86
87        for (layer_idx, block) in blocks.iter().enumerate() {
88            if let Ok((a_q, b_q, a_v, b_v)) = block.download_lora_weights() {
89                let q_lora_idx = layer_idx * 2;
90                let v_lora_idx = layer_idx * 2 + 1;
91
92                // Un-scale B matrices (GPU stores B * lora_scale)
93                let b_q_unscaled: Vec<f32> = b_q.iter().map(|&v| v * inv_scale).collect();
94                let b_v_unscaled: Vec<f32> = b_v.iter().map(|&v| v * inv_scale).collect();
95
96                if q_lora_idx < self.lora_layers.len() {
97                    *self.lora_layers[q_lora_idx].lora_a_mut() = crate::Tensor::from_vec(a_q, true);
98                    *self.lora_layers[q_lora_idx].lora_b_mut() =
99                        crate::Tensor::from_vec(b_q_unscaled, true);
100                }
101                if v_lora_idx < self.lora_layers.len() {
102                    *self.lora_layers[v_lora_idx].lora_a_mut() = crate::Tensor::from_vec(a_v, true);
103                    *self.lora_layers[v_lora_idx].lora_b_mut() =
104                        crate::Tensor::from_vec(b_v_unscaled, true);
105                }
106            }
107        }
108    }
109
110    /// Synchronize GPU LoRA weights back to CPU LoRA layers — no-op without the
111    /// `cuda` feature.
112    ///
113    /// On the CPU/WGPU training paths the adapters in `self.lora_layers` are
114    /// updated in place by `train_step`, so they are always current and no sync
115    /// is required. This twin keeps the call site in `evaluate()` unconditional.
116    #[cfg(not(feature = "cuda"))]
117    #[allow(clippy::unused_self)]
118    pub fn sync_lora_to_cpu(&mut self) {}
119
120    /// Check if this pipeline is using CUDA acceleration.
121    #[must_use]
122    pub fn is_cuda(&self) -> bool {
123        #[cfg(feature = "cuda")]
124        {
125            self.cuda_blocks.is_some()
126        }
127        #[cfg(not(feature = "cuda"))]
128        {
129            false
130        }
131    }
132
133    /// Get GPU device name, or `None` if not using CUDA.
134    #[must_use]
135    pub fn gpu_name(&self) -> Option<String> {
136        #[cfg(feature = "cuda")]
137        {
138            self.cuda_trainer.as_ref().map(CudaTrainer::device_name)
139        }
140        #[cfg(not(feature = "cuda"))]
141        {
142            None
143        }
144    }
145
146    /// Get total GPU memory in bytes, or `None` if not using CUDA.
147    #[must_use]
148    pub fn gpu_total_memory(&self) -> Option<usize> {
149        #[cfg(feature = "cuda")]
150        {
151            self.cuda_trainer.as_ref().map(CudaTrainer::total_memory)
152        }
153        #[cfg(not(feature = "cuda"))]
154        {
155            None
156        }
157    }
158
159    /// Summary of pipeline configuration.
160    #[must_use]
161    pub fn summary(&self) -> String {
162        format!(
163            "InstructPipeline: {} LoRA layers, rank={}, alpha={:.1}{}",
164            self.lora_layers.len(),
165            self.config.lora_rank,
166            self.config.lora_alpha,
167            if self.config.quantize_nf4 { ", NF4 QLoRA" } else { "" },
168        )
169    }
170
171    /// Get a reference to the tokenizer, if loaded.
172    #[must_use]
173    pub fn tokenizer(&self) -> Option<&HfTokenizer> {
174        self.tokenizer.as_ref()
175    }
176
177    /// PMAT-483: Enable the per-step profiler with the given report interval.
178    /// When enabled, profiler measures per-phase and per-layer timing.
179    /// Call `profiler.print_report()` or `profiler.print_json_report()` to get results.
180    pub fn enable_profiler(&mut self, report_interval: usize) {
181        self.profiler = StepProfiler::new(true, report_interval);
182    }
183}