metrale_model_engine/model/
impl_lora_rotate.rs1use anyhow::{Context, Result};
10
11use metrale_gpu_runtime::gpu::DevicePtr;
12
13use super::types::TransformerModel;
14use metrale_model_layers::layers::ops;
15
16impl TransformerModel {
17 pub(super) fn install_lora_layers(
21 &mut self,
22 layers: &[Option<metrale_model_layers::lora::LoraLayerWeights>],
23 kernels: ops::lora_delta::LoraKernels,
24 tables: &std::collections::BTreeMap<
25 (usize, metrale_model_layers::lora::LoraModule),
26 (
27 metrale_gpu_runtime::gpu::DevicePtr,
28 metrale_gpu_runtime::gpu::DevicePtr,
29 ),
30 >,
31 scale_table: metrale_gpu_runtime::gpu::DevicePtr,
32 ) -> Result<usize> {
33 use metrale_model_layers::lora::LoraModule;
34 let mk_route = |layer_idx: usize,
38 module: LoraModule,
39 pair: &Option<ops::lora_delta::LoraPair>|
40 -> Option<ops::lora_delta::LoraRoute> {
41 let p = pair.as_ref()?;
42 let (a_table, b_table) = *tables.get(&(layer_idx, module))?;
43 Some(ops::lora_delta::LoraRoute {
44 a_table,
45 b_table,
46 scale_table,
47 k_in: p.k_in,
48 n_out: p.n_out,
49 max_rank: p.max_rank,
50 })
51 };
52 let mut installed = 0usize;
53 for (idx, layer) in self.layers.iter_mut().enumerate() {
54 let Some(layer_weights) = layers.get(idx).and_then(|o| o.as_ref()) else {
55 continue;
56 };
57 let has_moe = layer_weights.router.is_some()
58 || layer_weights
59 .experts
60 .as_ref()
61 .is_some_and(|e| !e.is_empty());
62 let ffn_weights = if layer_weights.gate_proj.is_some()
66 || layer_weights.up_proj.is_some()
67 || layer_weights.down_proj.is_some()
68 {
69 Some(ops::lora_delta::LoraFfnWeights {
70 gate: layer_weights.gate_proj,
71 up: layer_weights.up_proj,
72 down: layer_weights.down_proj,
73 kernels,
74 })
75 } else {
76 None
77 };
78 let any = layer
79 .as_any_mut()
80 .ok_or_else(|| anyhow::anyhow!("LoRA: adapted layer {idx} is not downcastable"))?;
81 if let Some(attn) =
86 any.downcast_mut::<metrale_model_layers::layers::Qwen3AttentionLayer>()
87 {
88 let attn_weights = ops::lora_delta::LoraAttnWeights {
89 layer_idx: idx,
90 q: layer_weights.q_proj,
91 k: layer_weights.k_proj,
92 v: layer_weights.v_proj,
93 o: layer_weights.o_proj,
94 kernels,
95 q_route: mk_route(idx, LoraModule::QProj, &layer_weights.q_proj),
96 k_route: mk_route(idx, LoraModule::KProj, &layer_weights.k_proj),
97 v_route: mk_route(idx, LoraModule::VProj, &layer_weights.v_proj),
98 o_route: mk_route(idx, LoraModule::OProj, &layer_weights.o_proj),
99 };
100 attn.set_lora_weights(attn_weights, ffn_weights)?;
101 if has_moe {
102 attn.set_moe_lora_weights(
103 layer_weights.router,
104 layer_weights.experts.clone().unwrap_or_default(),
105 kernels,
106 self.gpu.as_ref(),
107 )?;
108 }
109 } else if let Some(ssm) =
110 any.downcast_mut::<metrale_model_layers::layers::Qwen3SsmLayer>()
111 {
112 let has_attn_proj = layer_weights.q_proj.is_some()
115 || layer_weights.k_proj.is_some()
116 || layer_weights.v_proj.is_some()
117 || layer_weights.o_proj.is_some();
118 if has_attn_proj {
119 anyhow::bail!(
120 "LoRA: attention-projection delta on linear-attention layer {idx} — \
121 classify should have rejected this (that layer has no q/k/v/o)"
122 );
123 }
124 if let Some(ffn) = ffn_weights {
125 ssm.set_ffn_lora_weights(ffn).with_context(|| {
126 format!("LoRA: installing dense-FFN delta on linear-attention layer {idx}")
127 })?;
128 }
129 if let Some(pair) = layer_weights.out_proj {
131 ssm.set_out_proj_lora(pair, kernels);
132 }
133 if has_moe {
134 ssm.set_moe_lora_weights(
135 layer_weights.router,
136 layer_weights.experts.clone().unwrap_or_default(),
137 kernels,
138 self.gpu.as_ref(),
139 )?;
140 }
141 } else {
142 anyhow::bail!(
143 "LoRA: adapted layer {idx} is neither a Qwen3AttentionLayer nor a \
144 Qwen3SsmLayer (loader/adapter layer-type mismatch)"
145 );
146 }
147 installed += 1;
148 }
149 Ok(installed)
150 }
151
152 pub(super) fn destroy_lora_decode_graphs(&self) {
159 let drain = |name: &str, graphs: Vec<metrale_gpu_runtime::gpu::GraphHandle>| {
160 for g in graphs {
161 if g.0 != 0
162 && let Err(e) = self.gpu.destroy_graph(g)
163 {
164 tracing::warn!("LoRA graph clear: destroy {name}: {e:#}");
165 }
166 }
167 };
168 drain(
169 "decode_graph",
170 self.decode_graph.lock().drain().map(|(_, g)| g).collect(),
171 );
172 drain(
173 "batch_decode_graph",
174 self.batch_decode_graphs
175 .lock()
176 .0
177 .drain()
178 .map(|(_, (g, _))| g)
179 .collect(),
180 );
181 drain(
182 "verify2_graph",
183 self.verify2_graph.lock().drain().map(|(_, g)| g).collect(),
184 );
185 drain(
186 "verify3_graph",
187 self.verify3_graph.lock().drain().map(|(_, g)| g).collect(),
188 );
189 drain(
190 "verify4_graph",
191 self.verify4_graph.lock().drain().map(|(_, g)| g).collect(),
192 );
193 drain(
194 "verify_kgamma_graph",
195 self.verify_kgamma_graph
196 .lock()
197 .drain()
198 .map(|(_, g)| g)
199 .collect(),
200 );
201 drain(
202 "fused_graph",
203 self.fused_graph.lock().drain().map(|(_, g)| g).collect(),
204 );
205 }
206
207 pub fn rotate_lora_to(&mut self, name: &str) -> Result<()> {
217 let slot = {
218 let lw = self
219 .lora
220 .as_ref()
221 .ok_or_else(|| anyhow::anyhow!("LoRA rotation: no adapter loaded"))?;
222 lw.slot_of(name).ok_or_else(|| {
223 anyhow::anyhow!(
224 "LoRA rotation: adapter '{name}' is not resident (have [{}])",
225 lw.adapter_names().join(", ")
226 )
227 })?
228 };
229 if !self.lora_rotatable {
230 anyhow::bail!(
231 "LoRA rotation not armed (single adapter, METRALE_LORA_ROTATE unset); \
232 set METRALE_LORA_ROTATE=1 (forces eager decode) to rotate at runtime"
233 );
234 }
235 {
240 let lw = self.lora.as_ref().unwrap();
241 let cur = lw.active;
242 if lw.slot_ref_count(cur) > 0 {
243 anyhow::bail!(
244 "LoRA rotation refused: active slot {cur} has in-flight \
245 sequences (ref_count>0); rotate at a quiescent point"
246 );
247 }
248 }
249 let (layers, active_name, r, tables, scale_table) = {
250 let lw = self.lora.as_mut().unwrap();
251 lw.active = slot;
252 lw.name = lw.slots[slot].name.clone();
253 lw.adapter_config = lw.slots[slot].adapter_config.clone();
254 (
255 lw.slots[slot].layers.clone(),
256 lw.name.clone(),
257 lw.adapter_config.r,
258 lw.tables.clone(),
259 lw.scale_table,
260 )
261 };
262 let kernels = ops::lora_delta::LoraKernels::new(self.gpu.as_ref())?;
263 let installed = self.install_lora_layers(&layers, kernels, &tables, scale_table)?;
264 self.destroy_lora_decode_graphs();
265 tracing::info!(
266 "LoRA rotation → slot {slot} '{active_name}' (r={r}) re-installed on \
267 {installed} layers"
268 );
269 Ok(())
270 }
271}