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 = super::estimate_load_bytes(std::slice::from_ref(&path), &|_| false)?;
49 let free = gpu.free_memory()?;
50 if estimated + oom_reserve_bytes > free {
51 bail!(
52 "OOM pre-flight (LoRA adapter): {estimated} B adapter tensors + \
53 {oom_reserve_bytes} B reserve exceeds {free} B free"
54 );
55 }
56
57 let file = std::fs::File::open(&path)?;
58 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
59 let tensors = safetensors::SafeTensors::deserialize(&mmap)?;
60
61 let mut weights = HashMap::new();
62 for (name, view) in tensors.tensors() {
63 let shape: Vec<usize> = view.shape().to_vec();
64 let data = view.data();
65 let (bytes, dtype): (Cow<'_, [u8]>, WeightDtype) = match view.dtype() {
66 safetensors::Dtype::F16 => {
67 let conv: Vec<u8> = data
70 .chunks_exact(2)
71 .flat_map(|c| {
72 bf16::from_f32(f16::from_le_bytes([c[0], c[1]]).to_f32()).to_le_bytes()
73 })
74 .collect();
75 (Cow::Owned(conv), WeightDtype::BF16)
76 }
77 safetensors::Dtype::F32 => {
78 let conv: Vec<u8> = data
87 .chunks_exact(4)
88 .flat_map(|c| {
89 bf16::from_f32(f32::from_le_bytes([c[0], c[1], c[2], c[3]])).to_le_bytes()
90 })
91 .collect();
92 (Cow::Owned(conv), WeightDtype::BF16)
93 }
94 other => (
95 Cow::Borrowed(data),
96 WeightDtype::from_safetensors(other)
97 .with_context(|| format!("LoRA adapter tensor '{name}'"))?,
98 ),
99 };
100 let ptr = gpu.alloc(bytes.len())?;
101 gpu.copy_h2d(&bytes, ptr)?;
102 weights.insert(name, WeightTensor { ptr, shape, dtype });
103 }
104
105 drop(tensors);
107 drop(mmap);
108 evict_page_cache(&file);
109
110 Ok(WeightStore::from_map(weights))
111}
112
113#[cfg(all(test, feature = "cuda"))]
116mod tests {
117 use super::load_adapter_safetensors;
118 use crate::cuda_backend::AtlasCudaBackend;
119 use crate::gpu::GpuBackend; use crate::weights::WeightDtype;
121 use half::bf16;
122 use safetensors::Dtype;
123 use safetensors::serialize_to_file;
124 use safetensors::tensor::TensorView;
125 use std::collections::HashMap;
126
127 #[test]
140 #[ignore = "requires a free CUDA device (GB10)"]
141 fn f32_peft_adapter_loads_and_round_trips_as_bf16() {
142 let a_vals: [f32; 8] = [1.0, -2.0, 0.5, 0.25, 3.0, -1.5, 0.0, 8.0];
145 let b_vals: [f32; 8] = [4.0, -0.75, 0.125, 16.0, -6.0, 2.0, 0.0625, -1.0];
146 let a_shape = vec![2usize, 4usize];
148 let b_shape = vec![4usize, 2usize];
149 let a_bytes: Vec<u8> = a_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
150 let b_bytes: Vec<u8> = b_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
151
152 let a_key = "base_model.model.model.layers.3.self_attn.k_proj.lora_A.weight";
155 let b_key = "base_model.model.model.layers.3.self_attn.k_proj.lora_B.weight";
156
157 let dir = std::env::temp_dir().join(format!(
160 "atlas_adapter_test_{}_{:?}",
161 std::process::id(),
162 std::thread::current().id()
163 ));
164 std::fs::create_dir_all(&dir).unwrap();
165 let path = dir.join("adapter_model.safetensors");
166
167 let a_view = TensorView::new(Dtype::F32, a_shape.clone(), &a_bytes).unwrap();
168 let b_view = TensorView::new(Dtype::F32, b_shape.clone(), &b_bytes).unwrap();
169 let mut map: HashMap<String, TensorView> = HashMap::new();
170 map.insert(a_key.to_string(), a_view);
171 map.insert(b_key.to_string(), b_view);
172 serialize_to_file(map, None, &path).unwrap();
173
174 let gpu = AtlasCudaBackend::new(0, &atlas_kernels::ptx_modules()).unwrap();
177
178 let store = load_adapter_safetensors(&dir, &gpu, 0).unwrap();
179
180 for (key, shape, vals) in [(a_key, &a_shape, &a_vals), (b_key, &b_shape, &b_vals)] {
182 let t = store.get(key).unwrap();
183 assert_eq!(
184 t.dtype,
185 WeightDtype::BF16,
186 "F32 adapter tensor must load as BF16"
187 );
188 assert_eq!(&t.shape, shape);
189
190 let mut back = vec![0u8; vals.len() * 2];
192 gpu.copy_d2h(t.ptr, &mut back).unwrap();
193 for (i, chunk) in back.chunks_exact(2).enumerate() {
194 let got = bf16::from_bits(u16::from_le_bytes([chunk[0], chunk[1]])).to_f32();
195 assert_eq!(got, vals[i], "tensor '{key}' elem {i} F32->BF16 round-trip");
196 }
197 }
198
199 let _ = std::fs::remove_dir_all(&dir);
200 }
201}