spark_runtime/weights/
adapter.rs1use std::borrow::Cow;
16use std::collections::HashMap;
17use std::path::Path;
18
19use anyhow::{Context, Result, bail};
20use half::{bf16, f16};
21
22use super::{WeightDtype, WeightStore, WeightTensor, evict_page_cache};
23use crate::gpu::GpuBackend;
24
25pub fn load_adapter_safetensors(
30 adapter_dir: &Path,
31 gpu: &dyn GpuBackend,
32 oom_reserve_bytes: usize,
33) -> Result<WeightStore> {
34 let path = adapter_dir.join("adapter_model.safetensors");
35 if !path.exists() {
36 if adapter_dir.join("adapter_model.bin").exists() {
37 bail!(
38 "REJECT[pickle-adapter]: {} ships adapter_model.bin (torch pickle); \
39 re-save with safe_serialization=True",
40 adapter_dir.display()
41 );
42 }
43 bail!("No adapter_model.safetensors in {}", adapter_dir.display());
44 }
45
46 let estimated =
49 super::estimate_load_bytes(std::slice::from_ref(&path), &|_| false, &|_, _| false)?;
50 let free = gpu.free_memory()?;
51 if estimated + oom_reserve_bytes > free {
52 bail!(
53 "OOM pre-flight (LoRA adapter): {estimated} B adapter tensors + \
54 {oom_reserve_bytes} B reserve exceeds {free} B free"
55 );
56 }
57
58 let file = std::fs::File::open(&path)?;
59 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
60 let tensors = safetensors::SafeTensors::deserialize(&mmap)?;
61
62 let mut weights = HashMap::new();
63 for (name, view) in tensors.tensors() {
64 let shape: Vec<usize> = view.shape().to_vec();
65 let data = view.data();
66 let (bytes, dtype): (Cow<'_, [u8]>, WeightDtype) = match view.dtype() {
67 safetensors::Dtype::F16 => {
68 let conv: Vec<u8> = data
71 .chunks_exact(2)
72 .flat_map(|c| {
73 bf16::from_f32(f16::from_le_bytes([c[0], c[1]]).to_f32()).to_le_bytes()
74 })
75 .collect();
76 (Cow::Owned(conv), WeightDtype::BF16)
77 }
78 safetensors::Dtype::F32 => {
79 let conv: Vec<u8> = data
88 .chunks_exact(4)
89 .flat_map(|c| {
90 bf16::from_f32(f32::from_le_bytes([c[0], c[1], c[2], c[3]])).to_le_bytes()
91 })
92 .collect();
93 (Cow::Owned(conv), WeightDtype::BF16)
94 }
95 other => (
96 Cow::Borrowed(data),
97 WeightDtype::from_safetensors(other)
98 .with_context(|| format!("LoRA adapter tensor '{name}'"))?,
99 ),
100 };
101 let ptr = gpu.alloc(bytes.len())?;
102 gpu.copy_h2d(&bytes, ptr)?;
103 weights.insert(name, WeightTensor { ptr, shape, dtype });
104 }
105
106 drop(tensors);
108 drop(mmap);
109 evict_page_cache(&file);
110
111 Ok(WeightStore::from_map(weights))
112}
113
114#[cfg(all(test, feature = "cuda"))]
117mod tests {
118 use super::load_adapter_safetensors;
119 use crate::cuda_backend::AvarokCudaBackend;
120 use crate::gpu::GpuBackend; use crate::weights::WeightDtype;
122 use half::bf16;
123 use safetensors::Dtype;
124 use safetensors::serialize_to_file;
125 use safetensors::tensor::TensorView;
126 use std::collections::HashMap;
127
128 #[test]
141 #[ignore = "requires a free CUDA device (GB10)"]
142 fn f32_peft_adapter_loads_and_round_trips_as_bf16() {
143 let a_vals: [f32; 8] = [1.0, -2.0, 0.5, 0.25, 3.0, -1.5, 0.0, 8.0];
146 let b_vals: [f32; 8] = [4.0, -0.75, 0.125, 16.0, -6.0, 2.0, 0.0625, -1.0];
147 let a_shape = vec![2usize, 4usize];
149 let b_shape = vec![4usize, 2usize];
150 let a_bytes: Vec<u8> = a_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
151 let b_bytes: Vec<u8> = b_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
152
153 let a_key = "base_model.model.model.layers.3.self_attn.k_proj.lora_A.weight";
156 let b_key = "base_model.model.model.layers.3.self_attn.k_proj.lora_B.weight";
157
158 let dir = std::env::temp_dir().join(format!(
161 "avarok_adapter_test_{}_{:?}",
162 std::process::id(),
163 std::thread::current().id()
164 ));
165 std::fs::create_dir_all(&dir).unwrap();
166 let path = dir.join("adapter_model.safetensors");
167
168 let a_view = TensorView::new(Dtype::F32, a_shape.clone(), &a_bytes).unwrap();
169 let b_view = TensorView::new(Dtype::F32, b_shape.clone(), &b_bytes).unwrap();
170 let mut map: HashMap<String, TensorView> = HashMap::new();
171 map.insert(a_key.to_string(), a_view);
172 map.insert(b_key.to_string(), b_view);
173 serialize_to_file(map, None, &path).unwrap();
174
175 let gpu = AvarokCudaBackend::new(0, &avarok_kernels::ptx_modules()).unwrap();
178
179 let store = load_adapter_safetensors(&dir, &gpu, 0).unwrap();
180
181 for (key, shape, vals) in [(a_key, &a_shape, &a_vals), (b_key, &b_shape, &b_vals)] {
183 let t = store.get(key).unwrap();
184 assert_eq!(
185 t.dtype,
186 WeightDtype::BF16,
187 "F32 adapter tensor must load as BF16"
188 );
189 assert_eq!(&t.shape, shape);
190
191 let mut back = vec![0u8; vals.len() * 2];
193 gpu.copy_d2h(t.ptr, &mut back).unwrap();
194 for (i, chunk) in back.chunks_exact(2).enumerate() {
195 let got = bf16::from_bits(u16::from_le_bytes([chunk[0], chunk[1]])).to_f32();
196 assert_eq!(got, vals[i], "tensor '{key}' elem {i} F32->BF16 round-trip");
197 }
198 }
199
200 let _ = std::fs::remove_dir_all(&dir);
201 }
202}