1use std::path::{Path, PathBuf};
41use std::sync::Arc;
42
43use frink_core::weight_matrix::{LoraDelta, LoraScale, WeightMatrix};
44use frink_gguf::TensorSource;
45
46use crate::decoder::{Decoder, ExpertBacking};
47use crate::lora::{LoraAdapter, LoraError, LoraPair};
48
49#[derive(Debug, Clone)]
52pub struct LoraAttached {
53 pub path: PathBuf,
54 pub alpha: f32,
55 pub task_name: String,
56 pub prompt_prefix: String,
57 pub scale: Arc<LoraScale>,
59 pub n_tensors: usize,
61}
62
63impl LoraAttached {
64 pub fn scale(&self) -> f32 {
65 self.scale.get()
66 }
67}
68
69#[derive(Debug, Clone, PartialEq)]
73pub struct LoraSpec {
74 pub path: PathBuf,
75 pub scale: f32,
76}
77
78impl LoraSpec {
79 pub fn plain(path: impl Into<PathBuf>) -> Self {
81 Self {
82 path: path.into(),
83 scale: 1.0,
84 }
85 }
86
87 pub fn parse_scaled(spec: &str) -> Result<Self, String> {
92 let (path, scale) = spec
93 .rsplit_once(':')
94 .ok_or_else(|| format!("lora-scaled format: FNAME:SCALE (got {spec:?})"))?;
95 let scale: f32 = scale
96 .trim()
97 .parse()
98 .map_err(|_| format!("lora-scaled format: FNAME:SCALE ({scale:?} is not a number)"))?;
99 if path.is_empty() {
100 return Err(format!("lora-scaled format: FNAME:SCALE (got {spec:?})"));
101 }
102 Ok(Self {
103 path: PathBuf::from(path),
104 scale,
105 })
106 }
107
108 pub fn from_flags(plain: &[String], scaled: &[String]) -> Result<Vec<Self>, String> {
112 let mut out = Vec::new();
113 for item in plain.iter().flat_map(|v| v.split(',')) {
114 let item = item.trim();
115 if !item.is_empty() {
116 out.push(Self::plain(item));
117 }
118 }
119 for item in scaled.iter().flat_map(|v| v.split(',')) {
120 let item = item.trim();
121 if !item.is_empty() {
122 out.push(Self::parse_scaled(item)?);
123 }
124 }
125 Ok(out)
126 }
127}
128
129impl Decoder {
130 pub fn attach_lora_specs(
133 &mut self,
134 base: &impl TensorSource,
135 specs: &[LoraSpec],
136 ) -> Result<(), LoraError> {
137 for spec in specs {
138 let adapter = LoraAdapter::open(&spec.path)?;
139 let id = self.attach_lora(base, adapter, spec.scale)?;
140 let a = &self.lora_adapters[id];
141 eprintln!(
142 "frink: lora adapter {id}: {} ({} tensor(s), alpha {}, scale {})",
143 spec.path.display(),
144 a.n_tensors,
145 a.alpha,
146 spec.scale
147 );
148 }
149 Ok(())
150 }
151}
152
153enum Parts<'a> {
155 Split(Vec<&'a mut WeightMatrix>),
158 Alias(Vec<&'a mut WeightMatrix>),
160}
161
162enum Target {
163 Embedding,
164 Output,
165 Layer(usize, LayerTarget),
166}
167
168enum LayerTarget {
169 Q,
170 K,
171 V,
172 Qkv,
173 O,
174 AttnGate,
175 FfnGate,
176 FfnUp,
177 FfnDown,
178 ShexpGate,
179 ShexpUp,
180 ShexpDown,
181 Router,
182}
183
184fn target_of(name: &str, path: &Path) -> Result<Target, LoraError> {
185 let no_projection = || LoraError::NoProjection {
186 path: path.to_path_buf(),
187 name: name.to_string(),
188 };
189 match name {
190 "token_embd.weight" => return Ok(Target::Embedding),
191 "output.weight" => return Ok(Target::Output),
192 _ => {}
193 }
194 let rest = name.strip_prefix("blk.").ok_or_else(no_projection)?;
195 let (il, tensor) = rest.split_once('.').ok_or_else(no_projection)?;
196 let il: usize = il.parse().map_err(|_| no_projection())?;
197 let t = tensor.strip_suffix(".weight").ok_or_else(no_projection)?;
198 if t.ends_with("_exps") {
199 return Err(LoraError::RoutedExperts {
200 path: path.to_path_buf(),
201 name: name.to_string(),
202 });
203 }
204 let kind = match t {
205 "attn_q" => LayerTarget::Q,
206 "attn_k" => LayerTarget::K,
207 "attn_v" => LayerTarget::V,
208 "attn_qkv" => LayerTarget::Qkv,
209 "attn_output" => LayerTarget::O,
210 "attn_gate" => LayerTarget::AttnGate,
211 "ffn_gate" => LayerTarget::FfnGate,
212 "ffn_up" => LayerTarget::FfnUp,
213 "ffn_down" => LayerTarget::FfnDown,
214 "ffn_gate_shexp" => LayerTarget::ShexpGate,
215 "ffn_up_shexp" => LayerTarget::ShexpUp,
216 "ffn_down_shexp" => LayerTarget::ShexpDown,
217 "ffn_gate_inp" => LayerTarget::Router,
218 _ => return Err(no_projection()),
219 };
220 Ok(Target::Layer(il, kind))
221}
222
223impl Decoder {
224 pub fn lora_attached(&self) -> bool {
227 !self.lora_adapters.is_empty()
228 }
229
230 pub fn attach_lora(
240 &mut self,
241 base: &impl TensorSource,
242 adapter: LoraAdapter,
243 scale: f32,
244 ) -> Result<usize, LoraError> {
245 let path = adapter.path.to_path_buf();
246 let base_arch = base.metadata_str("general.architecture").unwrap_or("");
247 if adapter.arch != base_arch {
248 return Err(LoraError::ArchMismatch {
249 path,
250 adapter: adapter.arch,
251 base: base_arch.to_string(),
252 });
253 }
254 let handle = LoraScale::new(scale);
255 let n_tensors = adapter.pairs.len();
256 for (name, pair) in &adapter.pairs {
257 let info = base.find_tensor(name).ok_or_else(|| LoraError::NotInBase {
258 path: path.to_path_buf(),
259 name: name.clone(),
260 })?;
261 let target = target_of(name, &path)?;
262 if matches!(target, Target::Embedding) && base.find_tensor("output.weight").is_none() {
268 return Err(LoraError::TiedHead { path });
269 }
270 if info.shape.len() != 2 {
271 return Err(LoraError::NoProjection {
272 path: path.to_path_buf(),
273 name: name.clone(),
274 });
275 }
276 let (rows, cols) = (info.shape[1] as usize, info.shape[0] as usize);
277 let whole = checked_pair(&path, name, pair, &target, rows, cols)?;
278 let shape_err = || LoraError::Shape {
279 path: path.to_path_buf(),
280 name: name.clone(),
281 rows,
282 cols,
283 a: pair.a.shape,
284 b: pair.b.shape,
285 };
286 let alpha = adapter.alpha;
287 match self.parts_mut(&target, &path, name, rows)? {
288 Parts::Split(parts) => {
289 if parts.iter().map(|m| m.rows()).sum::<usize>() != rows
293 || parts.iter().any(|m| m.cols() != cols)
294 {
295 return Err(shape_err());
296 }
297 let mut row0 = 0;
298 for m in parts {
299 let n = m.rows();
300 let b = whole.b[row0 * whole.rank..(row0 + n) * whole.rank].to_vec();
301 row0 += n;
302 let delta = LoraDelta::new(
303 whole.a.clone(),
304 b,
305 whole.rank,
306 n,
307 cols,
308 alpha,
309 Arc::clone(&handle),
310 )
311 .map_err(|_| shape_err())?;
312 m.attach_lora(delta);
313 }
314 }
315 Parts::Alias(parts) => {
316 for m in parts {
317 if m.rows() != rows || m.cols() != cols {
318 return Err(shape_err());
319 }
320 let delta = LoraDelta::new(
321 whole.a.clone(),
322 whole.b.clone(),
323 whole.rank,
324 rows,
325 cols,
326 alpha,
327 Arc::clone(&handle),
328 )
329 .map_err(|_| shape_err())?;
330 m.attach_lora(delta);
331 }
332 }
333 }
334 }
335 self.lora_adapters.push(LoraAttached {
336 path,
337 alpha: adapter.alpha,
338 task_name: adapter.task_name,
339 prompt_prefix: adapter.prompt_prefix,
340 scale: handle,
341 n_tensors,
342 });
343 Ok(self.lora_adapters.len() - 1)
344 }
345
346 pub fn set_lora_scales(&self, scales: &[(usize, f32)]) -> Result<(), String> {
351 for &(id, _) in scales {
352 if id >= self.lora_adapters.len() {
353 return Err(format!(
354 "lora adapter id {id} is out of range: {} adapter(s) loaded",
355 self.lora_adapters.len()
356 ));
357 }
358 }
359 for (id, a) in self.lora_adapters.iter().enumerate() {
360 let s = scales
361 .iter()
362 .rev()
363 .find(|(i, _)| *i == id)
364 .map(|(_, s)| *s)
365 .unwrap_or(0.0);
366 a.scale.set(s);
367 }
368 Ok(())
369 }
370
371 pub fn lora_scales(&self) -> Vec<f32> {
373 self.lora_adapters.iter().map(LoraAttached::scale).collect()
374 }
375
376 fn parts_mut(
378 &mut self,
379 target: &Target,
380 path: &Path,
381 name: &str,
382 file_rows: usize,
383 ) -> Result<Parts<'_>, LoraError> {
384 let no_projection = || LoraError::NoProjection {
385 path: path.to_path_buf(),
386 name: name.to_string(),
387 };
388 let ungated = self.config.ffn_is_ungated();
389 Ok(match target {
390 Target::Embedding => Parts::Split(vec![&mut self.embedding]),
391 Target::Output => Parts::Split(vec![&mut self.output_head]),
392 Target::Layer(il, kind) => {
393 let layer = self.layers.get_mut(*il).ok_or_else(no_projection)?;
394 match kind {
395 LayerTarget::Q => Parts::Split(vec![&mut layer.attn.q_proj]),
396 LayerTarget::K => Parts::Split(vec![&mut layer.attn.k_proj]),
397 LayerTarget::V => Parts::Split(vec![&mut layer.attn.v_proj]),
398 LayerTarget::Qkv => Parts::Split(vec![
399 &mut layer.attn.q_proj,
400 &mut layer.attn.k_proj,
401 &mut layer.attn.v_proj,
402 ]),
403 LayerTarget::O => Parts::Split(vec![&mut layer.attn.o_proj]),
404 LayerTarget::AttnGate => match layer.attn.output_gate.as_mut() {
405 Some(g) => Parts::Split(vec![&mut g.proj]),
406 None => return Err(no_projection()),
407 },
408 LayerTarget::FfnGate | LayerTarget::FfnUp | LayerTarget::FfnDown => {
409 let ex = match &mut layer.moe.experts {
410 ExpertBacking::Resident(v) if v.len() == 1 => &mut v[0],
411 ExpertBacking::Resident(_) => return Err(no_projection()),
412 ExpertBacking::Stored { .. } => {
413 return Err(LoraError::StoredExperts {
414 path: path.to_path_buf(),
415 name: name.to_string(),
416 })
417 }
418 };
419 match kind {
420 LayerTarget::FfnGate => Parts::Split(vec![&mut ex.gate]),
421 LayerTarget::FfnDown => Parts::Split(vec![&mut ex.down]),
422 _ if ungated => Parts::Alias(vec![&mut ex.up, &mut ex.gate]),
427 _ if file_rows == ex.gate.rows() + ex.up.rows()
428 && file_rows != ex.up.rows() =>
429 {
430 Parts::Split(vec![&mut ex.gate, &mut ex.up])
431 }
432 _ => Parts::Split(vec![&mut ex.up]),
433 }
434 }
435 LayerTarget::ShexpGate | LayerTarget::ShexpUp | LayerTarget::ShexpDown => {
436 let sh = layer
437 .moe
438 .shared_experts
439 .first_mut()
440 .ok_or_else(no_projection)?;
441 Parts::Split(vec![match kind {
442 LayerTarget::ShexpGate => &mut sh.gate,
443 LayerTarget::ShexpUp => &mut sh.up,
444 _ => &mut sh.down,
445 }])
446 }
447 LayerTarget::Router => Parts::Split(vec![&mut layer.moe.router]),
448 }
449 }
450 })
451 }
452}
453
454struct CheckedPair {
457 a: Vec<f32>,
458 b: Vec<f32>,
459 rank: usize,
460}
461
462fn checked_pair(
464 path: &Path,
465 name: &str,
466 pair: &LoraPair,
467 target: &Target,
468 rows: usize,
469 cols: usize,
470) -> Result<CheckedPair, LoraError> {
471 let shape_err = || LoraError::Shape {
472 path: path.to_path_buf(),
473 name: name.to_string(),
474 rows,
475 cols,
476 a: pair.a.shape,
477 b: pair.b.shape,
478 };
479 if matches!(target, Target::Embedding) {
480 if cols != pair.b.rows() || rows != pair.a.rows() {
486 return Err(shape_err());
487 }
488 let rank = pair.a.cols();
489 if rank != pair.b.cols() {
490 return Err(shape_err());
491 }
492 let n_embd = cols;
493 let mut a_eff = vec![0f32; rank * n_embd];
494 for e in 0..n_embd {
495 for k in 0..rank {
496 a_eff[k * n_embd + e] = pair.b.data[e * rank + k];
497 }
498 }
499 return Ok(CheckedPair {
500 a: a_eff,
501 b: pair.a.data.clone(),
502 rank,
503 });
504 }
505 if cols != pair.a.cols() || rows != pair.b.rows() {
507 return Err(shape_err());
508 }
509 let rank = pair.a.rows();
511 if rank != pair.b.cols() {
512 return Err(LoraError::NotTransposed {
513 path: path.to_path_buf(),
514 name: name.to_string(),
515 a: pair.a.shape,
516 b: pair.b.shape,
517 });
518 }
519 Ok(CheckedPair {
520 a: pair.a.data.clone(),
521 b: pair.b.data.clone(),
522 rank,
523 })
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn scaled_specs_split_on_the_last_colon() {
532 let s = LoraSpec::parse_scaled("a/b.gguf:0.5").unwrap();
533 assert_eq!(s.path, PathBuf::from("a/b.gguf"));
534 assert_eq!(s.scale, 0.5);
535 let s = LoraSpec::parse_scaled("C:/x/y.gguf:2").unwrap();
536 assert_eq!(s.path, PathBuf::from("C:/x/y.gguf"));
537 assert_eq!(s.scale, 2.0);
538 let s = LoraSpec::parse_scaled("z.gguf:-1").unwrap();
539 assert_eq!(s.scale, -1.0);
540 assert!(LoraSpec::parse_scaled("z.gguf")
541 .unwrap_err()
542 .contains("FNAME:SCALE"));
543 assert!(LoraSpec::parse_scaled("z.gguf:abc")
544 .unwrap_err()
545 .contains("not a number"));
546 assert!(LoraSpec::parse_scaled(":0.5").is_err());
547 }
548
549 #[test]
550 fn the_two_flags_become_one_ordered_list() {
551 let specs = LoraSpec::from_flags(
552 &["a.gguf,b.gguf".to_string(), "c.gguf".to_string()],
553 &["d.gguf:0.25".to_string()],
554 )
555 .unwrap();
556 assert_eq!(
557 specs,
558 vec![
559 LoraSpec::plain("a.gguf"),
560 LoraSpec::plain("b.gguf"),
561 LoraSpec::plain("c.gguf"),
562 LoraSpec {
563 path: "d.gguf".into(),
564 scale: 0.25
565 },
566 ]
567 );
568 assert!(LoraSpec::from_flags(&[], &["x".to_string()]).is_err());
569 }
570}