1use crate::ltxdit::{Shared, rows};
26use crate::pool::Pool;
27use std::collections::HashMap;
28use std::path::Path;
29
30pub struct LoraBranch {
32 a: Vec<f32>, b: Vec<f32>, rank: usize,
35 inn: usize,
36 out: usize,
37 scale: f32,
38 id: usize,
41 resonance: std::sync::atomic::AtomicU32,
44 live: std::sync::atomic::AtomicBool,
45}
46
47pub fn route_threshold() -> Option<f32> {
62 use std::sync::atomic::Ordering::Relaxed;
63 let mut bits = ROUTE.load(Relaxed);
67 if bits == u32::MAX {
68 bits = std::env::var("CMF_LORA_ROUTE")
69 .ok()
70 .and_then(|v| v.parse::<f32>().ok())
71 .filter(|v| *v > 0.0)
72 .map_or(0, f32::to_bits);
73 ROUTE.store(bits, Relaxed);
74 }
75 (bits != 0).then(|| f32::from_bits(bits))
76}
77
78static ROUTE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(u32::MAX);
79
80pub fn set_route_threshold(t: Option<f32>) {
82 ROUTE.store(
83 t.filter(|v| *v > 0.0).map_or(0, f32::to_bits),
84 std::sync::atomic::Ordering::Relaxed,
85 );
86}
87
88pub fn wants_measurement() -> bool {
92 probe_on() || route_threshold().is_some()
93}
94
95pub fn probe_report_on() -> bool {
97 probe_on()
98}
99
100fn probe_on() -> bool {
101 static P: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
102 *P.get_or_init(|| std::env::var("CMF_LORA_PROBE").is_ok())
103}
104
105static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
106
107impl LoraBranch {
108 pub fn rank(&self) -> usize {
109 self.rank
110 }
111
112 #[cfg(target_os = "macos")]
114 pub(crate) fn side(&self) -> crate::gpu_metal::LoraSide<'_> {
115 crate::gpu_metal::LoraSide {
116 a: &self.a,
117 b: &self.b,
118 rank: self.rank,
119 scale: self.scale,
120 id: self.id,
121 }
122 }
123
124 pub fn live(&self) -> bool {
128 self.live.load(std::sync::atomic::Ordering::Relaxed)
129 }
130
131 pub fn resonance(&self) -> f32 {
133 f32::from_bits(self.resonance.load(std::sync::atomic::Ordering::Relaxed))
134 }
135
136 pub fn add(&self, x: &[f32], n: usize, dst: &mut [f32], pool: Option<&Pool>) {
145 debug_assert_eq!(x.len(), n * self.inn);
146 debug_assert_eq!(dst.len(), n * self.out);
147 if n == 0 || !self.live() {
148 return;
149 }
150 let mut h = vec![0f32; n * self.rank];
158 let mut d = vec![0f32; n * self.out];
159 crate::gpu::cpu_scope(|| {
160 crate::fcd_ops::gemm_nt(x, &self.a, &mut h, n, self.inn, self.rank, pool);
161 crate::fcd_ops::gemm_nt(&h, &self.b, &mut d, n, self.rank, self.out, pool);
162 });
163 let scale = self.scale;
164 let measure = (probe_on() || route_threshold().is_some())
170 && self.resonance.load(std::sync::atomic::Ordering::Relaxed) == 0;
171 if measure {
172 let (mut dd, mut bb) = (0f64, 0f64);
173 for (&dv, &bv) in d.iter().zip(dst.iter()) {
174 dd += (scale * dv) as f64 * (scale * dv) as f64;
175 bb += bv as f64 * bv as f64;
176 }
177 let r = if bb > 0.0 {
178 (dd / bb).sqrt() as f32
179 } else {
180 f32::INFINITY
181 };
182 self.resonance.store(
185 r.max(f32::MIN_POSITIVE).to_bits(),
186 std::sync::atomic::Ordering::Relaxed,
187 );
188 if let Some(t) = route_threshold() {
189 if r < t {
190 self.live.store(false, std::sync::atomic::Ordering::Relaxed);
191 }
192 }
193 }
194 let sink = Shared(dst.as_mut_ptr());
195 rows(pool, n, &|s, e| {
196 let row = unsafe { sink.at(s * self.out, (e - s) * self.out) };
197 for (o, v) in row.iter_mut().enumerate() {
198 *v += scale * d[s * self.out + o];
199 }
200 });
201 }
202}
203
204pub struct SlotEmbed {
211 freqs: Vec<f32>,
212 w0: Vec<f32>,
213 b0: Vec<f32>,
214 w2: Vec<f32>,
215 b2: Vec<f32>,
216 hidden: usize,
217 dim: usize,
218}
219
220impl SlotEmbed {
221 pub fn dim(&self) -> usize {
222 self.dim
223 }
224
225 pub fn embed(&self, slot_id: usize) -> Vec<f32> {
227 let scaled = slot_id as f32 / 16.0;
228 let mut feat = Vec::with_capacity(1 + 2 * self.freqs.len());
229 feat.push(scaled);
230 for f in &self.freqs {
231 feat.push((scaled * f).sin());
232 }
233 for f in &self.freqs {
234 feat.push((scaled * f).cos());
235 }
236 let win = feat.len();
237 let mut hid = vec![0f32; self.hidden];
238 for (o, hv) in hid.iter_mut().enumerate() {
239 let row = &self.w0[o * win..(o + 1) * win];
240 let mut acc = self.b0[o];
241 for (fv, wv) in feat.iter().zip(row) {
242 acc += fv * wv;
243 }
244 *hv = acc / (1.0 + (-acc).exp());
246 }
247 let mut outv = vec![0f32; self.dim];
248 for (o, ov) in outv.iter_mut().enumerate() {
249 let row = &self.w2[o * self.hidden..(o + 1) * self.hidden];
250 let mut acc = self.b2[o];
251 for (hv, wv) in hid.iter().zip(row) {
252 acc += hv * wv;
253 }
254 *ov = acc;
255 }
256 outv
257 }
258}
259
260pub struct LoraBank {
264 pairs: HashMap<String, (Vec<f32>, Vec<f32>, usize, usize, usize)>,
265 pub slot: Option<SlotEmbed>,
266 pub meta: HashMap<String, String>,
267 scale: f32,
268}
269
270pub(crate) fn read_safetensors(
273 path: &Path,
274) -> Result<HashMap<String, (Vec<usize>, Vec<f32>)>, String> {
275 st_read(path).map(|(t, _)| t)
276}
277
278fn st_read(
279 path: &Path,
280) -> Result<
281 (
282 HashMap<String, (Vec<usize>, Vec<f32>)>,
283 HashMap<String, String>,
284 ),
285 String,
286> {
287 let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
288 if bytes.len() < 8 {
289 return Err("lora: truncated safetensors header".into());
290 }
291 let hlen = u64::from_le_bytes(bytes[..8].try_into().unwrap()) as usize;
292 let header: serde_json::Value = serde_json::from_slice(
293 bytes
294 .get(8..8 + hlen)
295 .ok_or("lora: header past end of file")?,
296 )
297 .map_err(|e| format!("lora header: {e}"))?;
298 let base = 8 + hlen;
299 let obj = header.as_object().ok_or("lora: header not an object")?;
300 let mut meta = HashMap::new();
301 let mut out = HashMap::new();
302 for (name, m) in obj {
303 if name == "__metadata__" {
304 if let Some(o) = m.as_object() {
305 for (k, v) in o {
306 if let Some(s) = v.as_str() {
307 meta.insert(k.clone(), s.to_string());
308 }
309 }
310 }
311 continue;
312 }
313 let dtype = m["dtype"].as_str().ok_or("lora: dtype")?;
314 let shape: Vec<usize> = m["shape"]
315 .as_array()
316 .ok_or("lora: shape")?
317 .iter()
318 .map(|v| v.as_u64().unwrap_or(0) as usize)
319 .collect();
320 let offs = m["data_offsets"].as_array().ok_or("lora: offsets")?;
321 let s = offs[0].as_u64().unwrap_or(0) as usize + base;
322 let e = offs[1].as_u64().unwrap_or(0) as usize + base;
323 let raw = bytes
324 .get(s..e)
325 .ok_or("lora: tensor span past end of file")?;
326 let mut data = Vec::with_capacity(shape.iter().product::<usize>().max(1));
327 match dtype {
328 "F32" => {
329 for c in raw.chunks_exact(4) {
330 data.push(f32::from_le_bytes(c.try_into().unwrap()));
331 }
332 }
333 "F16" => {
334 for c in raw.chunks_exact(2) {
335 data.push(cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
336 c.try_into().unwrap(),
337 )));
338 }
339 }
340 "BF16" => {
341 for c in raw.chunks_exact(2) {
342 let b = u16::from_le_bytes(c.try_into().unwrap());
343 data.push(f32::from_bits((b as u32) << 16));
344 }
345 }
346 other => return Err(format!("lora: unsupported dtype {other} on {name}")),
347 }
348 out.insert(name.clone(), (shape, data));
349 }
350 Ok((out, meta))
351}
352
353impl LoraBank {
354 pub fn load(path: &Path, strength: f32) -> Result<LoraBank, String> {
361 let (tensors, meta) = st_read(path)?;
362 let mut a_side: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
363 let mut b_side: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
364 let mut slot_parts: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
365 for (name, val) in tensors {
366 let short = name
372 .strip_prefix("diffusion_model.")
373 .or_else(|| name.strip_prefix("base_model.model."))
374 .or_else(|| name.strip_prefix("transformer."))
375 .unwrap_or(&name);
376 let short = short.strip_prefix("dit.").unwrap_or(short).to_string();
381 if let Some(rest) = short.strip_prefix("reference_slot_embedding.") {
382 slot_parts.insert(rest.to_string(), val);
383 } else if let Some(base) = short.strip_suffix(".lora_A.weight") {
384 a_side.insert(base.to_string(), val);
385 } else if let Some(base) = short.strip_suffix(".lora_B.weight") {
386 b_side.insert(base.to_string(), val);
387 } else if let Some(base) = short.strip_suffix(".lora_down.weight") {
388 a_side.insert(base.to_string(), val);
389 } else if let Some(base) = short.strip_suffix(".lora_up.weight") {
390 b_side.insert(base.to_string(), val);
391 }
392 }
393 let alpha = meta
394 .get("alpha")
395 .or_else(|| meta.get("lora_alpha"))
396 .and_then(|v| v.parse::<f32>().ok());
397
398 let mut pairs = HashMap::new();
399 for (base, (ashape, adata)) in a_side {
400 let Some((bshape, bdata)) = b_side.remove(&base) else {
401 return Err(format!("lora: {base} has an A side and no B side"));
402 };
403 if ashape.len() != 2 || bshape.len() != 2 {
404 return Err(format!("lora: {base} is not a matrix pair"));
405 }
406 let (rank, inn) = (ashape[0], ashape[1]);
407 let (out, rank_b) = (bshape[0], bshape[1]);
408 if rank != rank_b {
409 return Err(format!(
410 "lora: {base} rank mismatch — A is {rank}, B is {rank_b}"
411 ));
412 }
413 let scale = match alpha {
414 Some(al) if rank > 0 => strength * al / rank as f32,
415 _ => strength,
416 };
417 pairs.insert(base, (adata, bdata, rank, inn, out));
418 let _ = scale; }
420 if !b_side.is_empty() {
421 let orphan = b_side.keys().next().cloned().unwrap_or_default();
422 return Err(format!("lora: {orphan} has a B side and no A side"));
423 }
424
425 let slot = if slot_parts.is_empty() {
429 None
430 } else {
431 let need = |k: &str| -> Result<&(Vec<usize>, Vec<f32>), String> {
432 slot_parts
433 .get(k)
434 .ok_or_else(|| format!("lora: reference_slot_embedding.{k} is missing"))
435 };
436 let freqs = need("frequencies")?.1.clone();
437 let (s0, w0) = need("net.0.weight").map(|t| (t.0.clone(), t.1.clone()))?;
438 let b0 = need("net.0.bias")?.1.clone();
439 let (s2, w2) = need("net.2.weight").map(|t| (t.0.clone(), t.1.clone()))?;
440 let b2 = need("net.2.bias")?.1.clone();
441 if s0.len() != 2 || s2.len() != 2 {
442 return Err("lora: slot embedding layers are not matrices".into());
443 }
444 if s0[1] != 1 + 2 * freqs.len() {
445 return Err(format!(
446 "lora: slot embedding takes {} features, {} frequencies imply {}",
447 s0[1],
448 freqs.len(),
449 1 + 2 * freqs.len()
450 ));
451 }
452 Some(SlotEmbed {
453 freqs,
454 w0,
455 b0,
456 w2,
457 b2,
458 hidden: s0[0],
459 dim: s2[0],
460 })
461 };
462
463 let scale = match alpha {
464 Some(al) => {
465 let r = pairs.values().next().map(|p| p.2).unwrap_or(1).max(1);
466 strength * al / r as f32
467 }
468 None => strength,
469 };
470 Ok(LoraBank {
471 pairs,
472 slot,
473 meta,
474 scale,
475 })
476 }
477
478 pub fn len(&self) -> usize {
479 self.pairs.len()
480 }
481
482 pub fn is_empty(&self) -> bool {
483 self.pairs.is_empty()
484 }
485
486 pub fn rank(&self) -> usize {
488 self.pairs.values().next().map(|p| p.2).unwrap_or(0)
489 }
490
491 pub fn keys(&self) -> Vec<&str> {
496 self.pairs.keys().map(|s| s.as_str()).collect()
497 }
498
499 pub fn branch_for(
504 &self,
505 name: &str,
506 out: usize,
507 inn: usize,
508 ) -> Result<Option<LoraBranch>, String> {
509 let Some(br) = self.branch(name) else {
510 return Ok(None);
511 };
512 if br.out != out || br.inn != inn {
513 return Err(format!(
514 "lora: {name} is [{}, {}] in the adapter and [{out}, {inn}] in this container \
515 — that adapter was trained for a different model",
516 br.out, br.inn
517 ));
518 }
519 Ok(Some(br))
520 }
521
522 pub fn branch(&self, name: &str) -> Option<LoraBranch> {
525 let key = name.strip_prefix("dit.").unwrap_or(name);
526 let (a, b, rank, inn, out) = self.pairs.get(key)?;
527 Some(LoraBranch {
528 a: a.clone(),
529 b: b.clone(),
530 rank: *rank,
531 inn: *inn,
532 out: *out,
533 scale: self.scale,
534 id: NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
535 resonance: Default::default(),
536 live: std::sync::atomic::AtomicBool::new(true),
537 })
538 }
539
540 pub fn check_reference_convention(&self) -> Result<(), String> {
543 if let Some(order) = self.meta.get("reference_token_order") {
544 if order != "prepend" {
545 return Err(format!(
546 "lora: reference_token_order={order}, this build only prepends"
547 ));
548 }
549 }
550 if let Some(off) = self.meta.get("reference_slot_time_offsets") {
551 if off != "pic1_based_negative_time" {
552 return Err(format!(
553 "lora: reference_slot_time_offsets={off}, this build only places \
554 references at negative latent frames"
555 ));
556 }
557 }
558 Ok(())
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
568 fn branch_matches_dense() {
569 let (n, inn, rank, out) = (3usize, 5usize, 2usize, 4usize);
570 let a: Vec<f32> = (0..rank * inn).map(|i| (i as f32 * 0.37).sin()).collect();
571 let b: Vec<f32> = (0..out * rank).map(|i| (i as f32 * 0.11).cos()).collect();
572 let x: Vec<f32> = (0..n * inn).map(|i| (i as f32 * 0.7).sin()).collect();
573 let br = LoraBranch {
574 a: a.clone(),
575 b: b.clone(),
576 rank,
577 inn,
578 out,
579 scale: 0.5,
580 id: 0,
581 resonance: Default::default(),
582 live: std::sync::atomic::AtomicBool::new(true),
583 };
584 let mut got = vec![1.5f32; n * out];
585 br.add(&x, n, &mut got, None);
586 for t in 0..n {
587 for o in 0..out {
588 let mut acc = 0f32;
589 for r in 0..rank {
590 let h: f32 = (0..inn).map(|i| x[t * inn + i] * a[r * inn + i]).sum();
591 acc += h * b[o * rank + r];
592 }
593 let want = 1.5 + 0.5 * acc;
594 assert!(
595 (got[t * out + o] - want).abs() < 1e-4,
596 "row {t} col {o}: {} vs {want}",
597 got[t * out + o]
598 );
599 }
600 }
601 }
602
603 fn write_pairs(path: &std::path::Path, pairs: &[(&str, usize, usize)]) {
605 use std::io::Write;
606 let mut header = serde_json::Map::new();
607 let mut blob: Vec<u8> = Vec::new();
608 for (base, inn, rank) in pairs {
609 for (suffix, shape) in [
610 ("lora_A.weight", vec![*rank, *inn]),
611 ("lora_B.weight", vec![*inn, *rank]),
612 ] {
613 let count: usize = shape.iter().product();
614 let start = blob.len();
615 for i in 0..count {
616 blob.extend_from_slice(&(i as f32 * 0.25).to_le_bytes());
617 }
618 header.insert(
619 format!("{base}.{suffix}"),
620 serde_json::json!({
621 "dtype": "F32",
622 "shape": shape,
623 "data_offsets": [start, blob.len()],
624 }),
625 );
626 }
627 }
628 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
629 let mut f = std::fs::File::create(path).unwrap();
630 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
631 f.write_all(&hdr).unwrap();
632 f.write_all(&blob).unwrap();
633 }
634
635 #[test]
640 fn mmh3_names_bind_to_container_projections() {
641 let dir = std::env::temp_dir().join(format!("mmh3lora{}", std::process::id()));
642 std::fs::create_dir_all(&dir).unwrap();
643 let path = dir.join("h3.safetensors");
644 write_pairs(
645 &path,
646 &[
647 ("diffusion_model.blocks.0.attn.qkv_proj", 3, 2),
648 ("base_model.model.dit.blocks.1.mlp.fc1", 3, 2),
649 ],
650 );
651 let bank = LoraBank::load(&path, 1.0).unwrap();
652 assert!(bank.branch("dit.blocks.0.attn.qkv_proj").is_some());
653 assert!(bank.branch("dit.blocks.1.mlp.fc1").is_some());
654 assert!(bank.branch("dit.blocks.2.mlp.fc1").is_none());
655 let _ = std::fs::remove_dir_all(&dir);
656 }
657
658 #[test]
662 fn a_branch_of_the_wrong_shape_is_refused() {
663 let dir = std::env::temp_dir().join(format!("lorashape{}", std::process::id()));
664 std::fs::create_dir_all(&dir).unwrap();
665 let path = dir.join("wrong.safetensors");
666 write_pairs(&path, &[("diffusion_model.blocks.0.attn.qkv_proj", 3, 2)]);
668 let bank = LoraBank::load(&path, 1.0).unwrap();
669 let key = "dit.blocks.0.attn.qkv_proj";
670 assert!(
671 matches!(bank.branch_for(key, 3, 3), Ok(Some(_))),
672 "its own shape binds"
673 );
674 let err = match bank.branch_for(key, 4096, 3) {
675 Err(e) => e,
676 Ok(_) => panic!("a [4096, 3] projection must not take a [3, 3] branch"),
677 };
678 assert!(err.contains("different model"), "{err}");
679 assert!(matches!(bank.branch_for(key, 3, 4096), Err(_)));
680 assert!(matches!(
681 bank.branch_for("dit.blocks.9.attn.qkv_proj", 3, 3),
682 Ok(None)
683 ));
684 let _ = std::fs::remove_dir_all(&dir);
685 }
686
687 #[test]
690 fn router_silences_a_quiet_branch() {
691 set_route_threshold(Some(0.01));
692 let quiet = LoraBranch {
693 a: vec![1.0; 2],
694 b: vec![1e-6; 2],
695 rank: 1,
696 inn: 2,
697 out: 2,
698 scale: 1.0,
699 id: 0,
700 resonance: Default::default(),
701 live: std::sync::atomic::AtomicBool::new(true),
702 };
703 let mut out = vec![1.0f32; 2];
704 quiet.add(&[1.0, 1.0], 1, &mut out, None);
705 assert!(
706 !quiet.live(),
707 "a 1e-6 branch on a unit base must be routed off"
708 );
709 let loud = LoraBranch {
710 a: vec![1.0; 2],
711 b: vec![1.0; 2],
712 rank: 1,
713 inn: 2,
714 out: 2,
715 scale: 1.0,
716 id: 0,
717 resonance: Default::default(),
718 live: std::sync::atomic::AtomicBool::new(true),
719 };
720 let mut out = vec![1.0f32; 2];
721 loud.add(&[1.0, 1.0], 1, &mut out, None);
722 assert!(loud.live(), "a branch the size of the base must stay");
723 assert!(loud.resonance() > 1.0);
724 set_route_threshold(None);
725 }
726
727 #[test]
729 fn zero_b_changes_nothing() {
730 let br = LoraBranch {
731 a: vec![1.0; 4],
732 b: vec![0.0; 6],
733 rank: 2,
734 inn: 2,
735 out: 3,
736 scale: 1.0,
737 id: 0,
738 resonance: Default::default(),
739 live: std::sync::atomic::AtomicBool::new(true),
740 };
741 let mut out = vec![7.0f32; 3];
742 br.add(&[1.0, 2.0], 1, &mut out, None);
743 assert_eq!(out, vec![7.0, 7.0, 7.0]);
744 }
745
746 #[test]
752 fn names_bind_to_container_projections() {
753 use std::io::Write;
754 let dir = std::env::temp_dir().join("cmf_lora_name_test");
755 std::fs::create_dir_all(&dir).unwrap();
756 let path = dir.join("tiny.safetensors");
757 let names = [
759 (
760 "diffusion_model.transformer_blocks.0.attn1.to_q.lora_A.weight",
761 vec![2usize, 3],
762 ),
763 (
764 "diffusion_model.transformer_blocks.0.attn1.to_q.lora_B.weight",
765 vec![4, 2],
766 ),
767 ];
768 let mut header = serde_json::Map::new();
769 let mut blob: Vec<u8> = Vec::new();
770 for (n, shape) in &names {
771 let count: usize = shape.iter().product();
772 let start = blob.len();
773 for i in 0..count {
774 blob.extend_from_slice(&(i as f32 * 0.25).to_le_bytes());
775 }
776 header.insert(
777 (*n).to_string(),
778 serde_json::json!({
779 "dtype": "F32",
780 "shape": shape,
781 "data_offsets": [start, blob.len()],
782 }),
783 );
784 }
785 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
786 let mut f = std::fs::File::create(&path).unwrap();
787 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
788 f.write_all(&hdr).unwrap();
789 f.write_all(&blob).unwrap();
790 drop(f);
791
792 let bank = LoraBank::load(&path, 1.0).expect("load");
793 assert_eq!(bank.len(), 1);
794 assert_eq!(bank.rank(), 2);
795 assert!(bank.slot.is_none());
796 let br = bank
797 .branch("dit.transformer_blocks.0.attn1.to_q")
798 .expect("the container's name must find the adapter's branch");
799 assert_eq!(br.rank(), 2);
800 assert!(bank.branch("dit.transformer_blocks.0.attn1.to_k").is_none());
801 let mut out = vec![0f32; 4];
803 br.add(&[1.0, 0.0, 0.0], 1, &mut out, None);
804 let want = [0.25, 0.75, 1.25, 1.75].map(|v: f32| v * 0.75);
807 for (g, w) in out.iter().zip(&want) {
808 assert!((g - w).abs() < 1e-5, "{g} vs {w}");
809 }
810 let _ = std::fs::remove_dir_all(&dir);
811 }
812
813 #[test]
816 fn orphan_side_is_refused() {
817 use std::io::Write;
818 let dir = std::env::temp_dir().join("cmf_lora_orphan_test");
819 std::fs::create_dir_all(&dir).unwrap();
820 let path = dir.join("orphan.safetensors");
821 let mut header = serde_json::Map::new();
822 let mut blob: Vec<u8> = Vec::new();
823 for i in 0..6 {
824 blob.extend_from_slice(&(i as f32).to_le_bytes());
825 }
826 header.insert(
827 "diffusion_model.transformer_blocks.0.attn1.to_q.lora_A.weight".to_string(),
828 serde_json::json!({"dtype":"F32","shape":[2,3],"data_offsets":[0,24]}),
829 );
830 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
831 let mut f = std::fs::File::create(&path).unwrap();
832 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
833 f.write_all(&hdr).unwrap();
834 f.write_all(&blob).unwrap();
835 drop(f);
836 let err = match LoraBank::load(&path, 1.0) {
837 Err(e) => e,
838 Ok(_) => panic!("an adapter with a lone A side must be refused"),
839 };
840 assert!(err.contains("no B side"), "{err}");
841 let _ = std::fs::remove_dir_all(&dir);
842 }
843
844 #[test]
847 fn slot_embedding_matches_definition() {
848 let s = SlotEmbed {
849 freqs: vec![2.0],
850 w0: vec![1.0, 0.5, -0.25],
852 b0: vec![0.1],
853 w2: vec![2.0],
854 b2: vec![-0.3],
855 hidden: 1,
856 dim: 1,
857 };
858 let v = 3.0f32 / 16.0;
859 let feat = [v, (v * 2.0).sin(), (v * 2.0).cos()];
860 let pre = 0.1 + feat[0] * 1.0 + feat[1] * 0.5 + feat[2] * -0.25;
861 let hid = pre / (1.0 + (-pre).exp());
862 let want = -0.3 + hid * 2.0;
863 let got = s.embed(3);
864 assert_eq!(got.len(), 1);
865 assert!((got[0] - want).abs() < 1e-6, "{} vs {want}", got[0]);
866 }
867}