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(path: &Path) -> Result<(HashMap<String, (Vec<usize>, Vec<f32>)>, HashMap<String, String>), String> {
279 let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
280 if bytes.len() < 8 {
281 return Err("lora: truncated safetensors header".into());
282 }
283 let hlen = u64::from_le_bytes(bytes[..8].try_into().unwrap()) as usize;
284 let header: serde_json::Value = serde_json::from_slice(
285 bytes.get(8..8 + hlen).ok_or("lora: header past end of file")?,
286 )
287 .map_err(|e| format!("lora header: {e}"))?;
288 let base = 8 + hlen;
289 let obj = header.as_object().ok_or("lora: header not an object")?;
290 let mut meta = HashMap::new();
291 let mut out = HashMap::new();
292 for (name, m) in obj {
293 if name == "__metadata__" {
294 if let Some(o) = m.as_object() {
295 for (k, v) in o {
296 if let Some(s) = v.as_str() {
297 meta.insert(k.clone(), s.to_string());
298 }
299 }
300 }
301 continue;
302 }
303 let dtype = m["dtype"].as_str().ok_or("lora: dtype")?;
304 let shape: Vec<usize> = m["shape"]
305 .as_array()
306 .ok_or("lora: shape")?
307 .iter()
308 .map(|v| v.as_u64().unwrap_or(0) as usize)
309 .collect();
310 let offs = m["data_offsets"].as_array().ok_or("lora: offsets")?;
311 let s = offs[0].as_u64().unwrap_or(0) as usize + base;
312 let e = offs[1].as_u64().unwrap_or(0) as usize + base;
313 let raw = bytes.get(s..e).ok_or("lora: tensor span past end of file")?;
314 let mut data = Vec::with_capacity(shape.iter().product::<usize>().max(1));
315 match dtype {
316 "F32" => {
317 for c in raw.chunks_exact(4) {
318 data.push(f32::from_le_bytes(c.try_into().unwrap()));
319 }
320 }
321 "F16" => {
322 for c in raw.chunks_exact(2) {
323 data.push(cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
324 c.try_into().unwrap(),
325 )));
326 }
327 }
328 "BF16" => {
329 for c in raw.chunks_exact(2) {
330 let b = u16::from_le_bytes(c.try_into().unwrap());
331 data.push(f32::from_bits((b as u32) << 16));
332 }
333 }
334 other => return Err(format!("lora: unsupported dtype {other} on {name}")),
335 }
336 out.insert(name.clone(), (shape, data));
337 }
338 Ok((out, meta))
339}
340
341impl LoraBank {
342 pub fn load(path: &Path, strength: f32) -> Result<LoraBank, String> {
349 let (tensors, meta) = st_read(path)?;
350 let mut a_side: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
351 let mut b_side: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
352 let mut slot_parts: HashMap<String, (Vec<usize>, Vec<f32>)> = HashMap::new();
353 for (name, val) in tensors {
354 let short = name
360 .strip_prefix("diffusion_model.")
361 .or_else(|| name.strip_prefix("base_model.model."))
362 .or_else(|| name.strip_prefix("transformer."))
363 .unwrap_or(&name);
364 let short = short.strip_prefix("dit.").unwrap_or(short).to_string();
369 if let Some(rest) = short.strip_prefix("reference_slot_embedding.") {
370 slot_parts.insert(rest.to_string(), val);
371 } else if let Some(base) = short.strip_suffix(".lora_A.weight") {
372 a_side.insert(base.to_string(), val);
373 } else if let Some(base) = short.strip_suffix(".lora_B.weight") {
374 b_side.insert(base.to_string(), val);
375 } else if let Some(base) = short.strip_suffix(".lora_down.weight") {
376 a_side.insert(base.to_string(), val);
377 } else if let Some(base) = short.strip_suffix(".lora_up.weight") {
378 b_side.insert(base.to_string(), val);
379 }
380 }
381 let alpha = meta
382 .get("alpha")
383 .or_else(|| meta.get("lora_alpha"))
384 .and_then(|v| v.parse::<f32>().ok());
385
386 let mut pairs = HashMap::new();
387 for (base, (ashape, adata)) in a_side {
388 let Some((bshape, bdata)) = b_side.remove(&base) else {
389 return Err(format!("lora: {base} has an A side and no B side"));
390 };
391 if ashape.len() != 2 || bshape.len() != 2 {
392 return Err(format!("lora: {base} is not a matrix pair"));
393 }
394 let (rank, inn) = (ashape[0], ashape[1]);
395 let (out, rank_b) = (bshape[0], bshape[1]);
396 if rank != rank_b {
397 return Err(format!(
398 "lora: {base} rank mismatch — A is {rank}, B is {rank_b}"
399 ));
400 }
401 let scale = match alpha {
402 Some(al) if rank > 0 => strength * al / rank as f32,
403 _ => strength,
404 };
405 pairs.insert(base, (adata, bdata, rank, inn, out));
406 let _ = scale; }
408 if !b_side.is_empty() {
409 let orphan = b_side.keys().next().cloned().unwrap_or_default();
410 return Err(format!("lora: {orphan} has a B side and no A side"));
411 }
412
413 let slot = if slot_parts.is_empty() {
417 None
418 } else {
419 let need = |k: &str| -> Result<&(Vec<usize>, Vec<f32>), String> {
420 slot_parts
421 .get(k)
422 .ok_or_else(|| format!("lora: reference_slot_embedding.{k} is missing"))
423 };
424 let freqs = need("frequencies")?.1.clone();
425 let (s0, w0) = need("net.0.weight").map(|t| (t.0.clone(), t.1.clone()))?;
426 let b0 = need("net.0.bias")?.1.clone();
427 let (s2, w2) = need("net.2.weight").map(|t| (t.0.clone(), t.1.clone()))?;
428 let b2 = need("net.2.bias")?.1.clone();
429 if s0.len() != 2 || s2.len() != 2 {
430 return Err("lora: slot embedding layers are not matrices".into());
431 }
432 if s0[1] != 1 + 2 * freqs.len() {
433 return Err(format!(
434 "lora: slot embedding takes {} features, {} frequencies imply {}",
435 s0[1],
436 freqs.len(),
437 1 + 2 * freqs.len()
438 ));
439 }
440 Some(SlotEmbed {
441 freqs,
442 w0,
443 b0,
444 w2,
445 b2,
446 hidden: s0[0],
447 dim: s2[0],
448 })
449 };
450
451 let scale = match alpha {
452 Some(al) => {
453 let r = pairs.values().next().map(|p| p.2).unwrap_or(1).max(1);
454 strength * al / r as f32
455 }
456 None => strength,
457 };
458 Ok(LoraBank { pairs, slot, meta, scale })
459 }
460
461 pub fn len(&self) -> usize {
462 self.pairs.len()
463 }
464
465 pub fn is_empty(&self) -> bool {
466 self.pairs.is_empty()
467 }
468
469 pub fn rank(&self) -> usize {
471 self.pairs.values().next().map(|p| p.2).unwrap_or(0)
472 }
473
474 pub fn keys(&self) -> Vec<&str> {
479 self.pairs.keys().map(|s| s.as_str()).collect()
480 }
481
482 pub fn branch_for(
487 &self,
488 name: &str,
489 out: usize,
490 inn: usize,
491 ) -> Result<Option<LoraBranch>, String> {
492 let Some(br) = self.branch(name) else {
493 return Ok(None);
494 };
495 if br.out != out || br.inn != inn {
496 return Err(format!(
497 "lora: {name} is [{}, {}] in the adapter and [{out}, {inn}] in this container \
498 — that adapter was trained for a different model",
499 br.out, br.inn
500 ));
501 }
502 Ok(Some(br))
503 }
504
505 pub fn branch(&self, name: &str) -> Option<LoraBranch> {
508 let key = name.strip_prefix("dit.").unwrap_or(name);
509 let (a, b, rank, inn, out) = self.pairs.get(key)?;
510 Some(LoraBranch {
511 a: a.clone(),
512 b: b.clone(),
513 rank: *rank,
514 inn: *inn,
515 out: *out,
516 scale: self.scale,
517 id: NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
518 resonance: Default::default(),
519 live: std::sync::atomic::AtomicBool::new(true),
520 })
521 }
522
523 pub fn check_reference_convention(&self) -> Result<(), String> {
526 if let Some(order) = self.meta.get("reference_token_order") {
527 if order != "prepend" {
528 return Err(format!(
529 "lora: reference_token_order={order}, this build only prepends"
530 ));
531 }
532 }
533 if let Some(off) = self.meta.get("reference_slot_time_offsets") {
534 if off != "pic1_based_negative_time" {
535 return Err(format!(
536 "lora: reference_slot_time_offsets={off}, this build only places \
537 references at negative latent frames"
538 ));
539 }
540 }
541 Ok(())
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
551 fn branch_matches_dense() {
552 let (n, inn, rank, out) = (3usize, 5usize, 2usize, 4usize);
553 let a: Vec<f32> = (0..rank * inn).map(|i| (i as f32 * 0.37).sin()).collect();
554 let b: Vec<f32> = (0..out * rank).map(|i| (i as f32 * 0.11).cos()).collect();
555 let x: Vec<f32> = (0..n * inn).map(|i| (i as f32 * 0.7).sin()).collect();
556 let br = LoraBranch {
557 a: a.clone(),
558 b: b.clone(),
559 rank,
560 inn,
561 out,
562 scale: 0.5,
563 id: 0,
564 resonance: Default::default(),
565 live: std::sync::atomic::AtomicBool::new(true),
566 };
567 let mut got = vec![1.5f32; n * out];
568 br.add(&x, n, &mut got, None);
569 for t in 0..n {
570 for o in 0..out {
571 let mut acc = 0f32;
572 for r in 0..rank {
573 let h: f32 = (0..inn).map(|i| x[t * inn + i] * a[r * inn + i]).sum();
574 acc += h * b[o * rank + r];
575 }
576 let want = 1.5 + 0.5 * acc;
577 assert!(
578 (got[t * out + o] - want).abs() < 1e-4,
579 "row {t} col {o}: {} vs {want}",
580 got[t * out + o]
581 );
582 }
583 }
584 }
585
586 fn write_pairs(path: &std::path::Path, pairs: &[(&str, usize, usize)]) {
588 use std::io::Write;
589 let mut header = serde_json::Map::new();
590 let mut blob: Vec<u8> = Vec::new();
591 for (base, inn, rank) in pairs {
592 for (suffix, shape) in [
593 ("lora_A.weight", vec![*rank, *inn]),
594 ("lora_B.weight", vec![*inn, *rank]),
595 ] {
596 let count: usize = shape.iter().product();
597 let start = blob.len();
598 for i in 0..count {
599 blob.extend_from_slice(&(i as f32 * 0.25).to_le_bytes());
600 }
601 header.insert(
602 format!("{base}.{suffix}"),
603 serde_json::json!({
604 "dtype": "F32",
605 "shape": shape,
606 "data_offsets": [start, blob.len()],
607 }),
608 );
609 }
610 }
611 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
612 let mut f = std::fs::File::create(path).unwrap();
613 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
614 f.write_all(&hdr).unwrap();
615 f.write_all(&blob).unwrap();
616 }
617
618 #[test]
623 fn mmh3_names_bind_to_container_projections() {
624 let dir = std::env::temp_dir().join(format!("mmh3lora{}", std::process::id()));
625 std::fs::create_dir_all(&dir).unwrap();
626 let path = dir.join("h3.safetensors");
627 write_pairs(
628 &path,
629 &[
630 ("diffusion_model.blocks.0.attn.qkv_proj", 3, 2),
631 ("base_model.model.dit.blocks.1.mlp.fc1", 3, 2),
632 ],
633 );
634 let bank = LoraBank::load(&path, 1.0).unwrap();
635 assert!(bank.branch("dit.blocks.0.attn.qkv_proj").is_some());
636 assert!(bank.branch("dit.blocks.1.mlp.fc1").is_some());
637 assert!(bank.branch("dit.blocks.2.mlp.fc1").is_none());
638 let _ = std::fs::remove_dir_all(&dir);
639 }
640
641 #[test]
645 fn a_branch_of_the_wrong_shape_is_refused() {
646 let dir = std::env::temp_dir().join(format!("lorashape{}", std::process::id()));
647 std::fs::create_dir_all(&dir).unwrap();
648 let path = dir.join("wrong.safetensors");
649 write_pairs(&path, &[("diffusion_model.blocks.0.attn.qkv_proj", 3, 2)]);
651 let bank = LoraBank::load(&path, 1.0).unwrap();
652 let key = "dit.blocks.0.attn.qkv_proj";
653 assert!(
654 matches!(bank.branch_for(key, 3, 3), Ok(Some(_))),
655 "its own shape binds"
656 );
657 let err = match bank.branch_for(key, 4096, 3) {
658 Err(e) => e,
659 Ok(_) => panic!("a [4096, 3] projection must not take a [3, 3] branch"),
660 };
661 assert!(err.contains("different model"), "{err}");
662 assert!(matches!(bank.branch_for(key, 3, 4096), Err(_)));
663 assert!(matches!(
664 bank.branch_for("dit.blocks.9.attn.qkv_proj", 3, 3),
665 Ok(None)
666 ));
667 let _ = std::fs::remove_dir_all(&dir);
668 }
669
670 #[test]
673 fn router_silences_a_quiet_branch() {
674 set_route_threshold(Some(0.01));
675 let quiet = LoraBranch {
676 a: vec![1.0; 2],
677 b: vec![1e-6; 2],
678 rank: 1,
679 inn: 2,
680 out: 2,
681 scale: 1.0,
682 id: 0,
683 resonance: Default::default(),
684 live: std::sync::atomic::AtomicBool::new(true),
685 };
686 let mut out = vec![1.0f32; 2];
687 quiet.add(&[1.0, 1.0], 1, &mut out, None);
688 assert!(!quiet.live(), "a 1e-6 branch on a unit base must be routed off");
689 let loud = LoraBranch {
690 a: vec![1.0; 2],
691 b: vec![1.0; 2],
692 rank: 1,
693 inn: 2,
694 out: 2,
695 scale: 1.0,
696 id: 0,
697 resonance: Default::default(),
698 live: std::sync::atomic::AtomicBool::new(true),
699 };
700 let mut out = vec![1.0f32; 2];
701 loud.add(&[1.0, 1.0], 1, &mut out, None);
702 assert!(loud.live(), "a branch the size of the base must stay");
703 assert!(loud.resonance() > 1.0);
704 set_route_threshold(None);
705 }
706
707 #[test]
709 fn zero_b_changes_nothing() {
710 let br = LoraBranch {
711 a: vec![1.0; 4],
712 b: vec![0.0; 6],
713 rank: 2,
714 inn: 2,
715 out: 3,
716 scale: 1.0,
717 id: 0,
718 resonance: Default::default(),
719 live: std::sync::atomic::AtomicBool::new(true),
720 };
721 let mut out = vec![7.0f32; 3];
722 br.add(&[1.0, 2.0], 1, &mut out, None);
723 assert_eq!(out, vec![7.0, 7.0, 7.0]);
724 }
725
726 #[test]
732 fn names_bind_to_container_projections() {
733 use std::io::Write;
734 let dir = std::env::temp_dir().join("cmf_lora_name_test");
735 std::fs::create_dir_all(&dir).unwrap();
736 let path = dir.join("tiny.safetensors");
737 let names = [
739 ("diffusion_model.transformer_blocks.0.attn1.to_q.lora_A.weight", vec![2usize, 3]),
740 ("diffusion_model.transformer_blocks.0.attn1.to_q.lora_B.weight", vec![4, 2]),
741 ];
742 let mut header = serde_json::Map::new();
743 let mut blob: Vec<u8> = Vec::new();
744 for (n, shape) in &names {
745 let count: usize = shape.iter().product();
746 let start = blob.len();
747 for i in 0..count {
748 blob.extend_from_slice(&(i as f32 * 0.25).to_le_bytes());
749 }
750 header.insert(
751 (*n).to_string(),
752 serde_json::json!({
753 "dtype": "F32",
754 "shape": shape,
755 "data_offsets": [start, blob.len()],
756 }),
757 );
758 }
759 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
760 let mut f = std::fs::File::create(&path).unwrap();
761 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
762 f.write_all(&hdr).unwrap();
763 f.write_all(&blob).unwrap();
764 drop(f);
765
766 let bank = LoraBank::load(&path, 1.0).expect("load");
767 assert_eq!(bank.len(), 1);
768 assert_eq!(bank.rank(), 2);
769 assert!(bank.slot.is_none());
770 let br = bank
771 .branch("dit.transformer_blocks.0.attn1.to_q")
772 .expect("the container's name must find the adapter's branch");
773 assert_eq!(br.rank(), 2);
774 assert!(bank.branch("dit.transformer_blocks.0.attn1.to_k").is_none());
775 let mut out = vec![0f32; 4];
777 br.add(&[1.0, 0.0, 0.0], 1, &mut out, None);
778 let want = [0.25, 0.75, 1.25, 1.75].map(|v: f32| v * 0.75);
781 for (g, w) in out.iter().zip(&want) {
782 assert!((g - w).abs() < 1e-5, "{g} vs {w}");
783 }
784 let _ = std::fs::remove_dir_all(&dir);
785 }
786
787 #[test]
790 fn orphan_side_is_refused() {
791 use std::io::Write;
792 let dir = std::env::temp_dir().join("cmf_lora_orphan_test");
793 std::fs::create_dir_all(&dir).unwrap();
794 let path = dir.join("orphan.safetensors");
795 let mut header = serde_json::Map::new();
796 let mut blob: Vec<u8> = Vec::new();
797 for i in 0..6 {
798 blob.extend_from_slice(&(i as f32).to_le_bytes());
799 }
800 header.insert(
801 "diffusion_model.transformer_blocks.0.attn1.to_q.lora_A.weight".to_string(),
802 serde_json::json!({"dtype":"F32","shape":[2,3],"data_offsets":[0,24]}),
803 );
804 let hdr = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
805 let mut f = std::fs::File::create(&path).unwrap();
806 f.write_all(&(hdr.len() as u64).to_le_bytes()).unwrap();
807 f.write_all(&hdr).unwrap();
808 f.write_all(&blob).unwrap();
809 drop(f);
810 let err = match LoraBank::load(&path, 1.0) {
811 Err(e) => e,
812 Ok(_) => panic!("an adapter with a lone A side must be refused"),
813 };
814 assert!(err.contains("no B side"), "{err}");
815 let _ = std::fs::remove_dir_all(&dir);
816 }
817
818 #[test]
821 fn slot_embedding_matches_definition() {
822 let s = SlotEmbed {
823 freqs: vec![2.0],
824 w0: vec![1.0, 0.5, -0.25],
826 b0: vec![0.1],
827 w2: vec![2.0],
828 b2: vec![-0.3],
829 hidden: 1,
830 dim: 1,
831 };
832 let v = 3.0f32 / 16.0;
833 let feat = [v, (v * 2.0).sin(), (v * 2.0).cos()];
834 let pre = 0.1 + feat[0] * 1.0 + feat[1] * 0.5 + feat[2] * -0.25;
835 let hid = pre / (1.0 + (-pre).exp());
836 let want = -0.3 + hid * 2.0;
837 let got = s.embed(3);
838 assert_eq!(got.len(), 1);
839 assert!((got[0] - want).abs() < 1e-6, "{} vs {want}", got[0]);
840 }
841}