1use crate::Device;
46use serde::{Deserialize, Serialize};
47use std::collections::HashMap;
48
49mod diagnostics;
50mod inference;
51mod training;
52
53pub use diagnostics::*;
54pub use inference::*;
55pub use training::*;
56
57#[derive(Serialize, Deserialize, Clone, Debug)]
60pub struct WeightRef {
61 pub name: String,
62 pub uri: String,
65 #[serde(default)]
71 pub packed: bool,
72}
73
74fn resolve_device(spec: &str) -> Device {
75 if spec.eq_ignore_ascii_case("auto") || spec.is_empty() {
76 return crate::fastest_device();
77 }
78 match crate::parse_device(spec) {
79 Ok(d) if crate::is_available(d) => d,
80 _ => crate::fastest_device(),
81 }
82}
83
84fn split_frag(rest: &str) -> Result<(&str, &str), String> {
91 rest.split_once('#')
92 .ok_or_else(|| format!("weight URI needs a #<tensor> fragment: {rest}"))
93}
94
95pub fn resolve_weight_uri(uri: &str) -> Result<Vec<f32>, String> {
103 if let Some(rest) = uri.strip_prefix("gguf://") {
104 let (path, tensor) = split_frag(rest)?;
105 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
106 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
107 let (data, _dims) = gguf.dequant_f32(tensor).map_err(|e| e.to_string())?;
108 Ok(data)
109 } else if let Some(rest) = uri.strip_prefix("safetensors://") {
110 let (path, tensor) = split_frag(rest)?;
111 let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
112 read_safetensors_f32(&bytes, tensor)
113 } else if let Some(path) = uri.strip_prefix("file://") {
114 let b = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
115 Ok(b.chunks_exact(4)
116 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
117 .collect())
118 } else {
119 Err(format!("unsupported weight URI scheme: {uri}"))
120 }
121}
122
123pub fn resolve_weight_bytes(uri: &str) -> Result<Vec<u8>, String> {
128 if let Some(rest) = uri.strip_prefix("gguf://") {
129 let (path, tensor) = split_frag(rest)?;
130 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
131 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
132 let t = gguf
133 .get(tensor)
134 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
135 Ok(gguf.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
136 } else if let Some(path) = uri.strip_prefix("file://") {
137 std::fs::read(path).map_err(|e| format!("read {path}: {e}"))
138 } else {
139 Err(format!("packed load unsupported for scheme: {uri}"))
140 }
141}
142
143#[derive(Default)]
148pub struct WeightCache {
149 gguf: HashMap<String, rlx_gguf::GgufFile>,
150}
151
152impl WeightCache {
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 fn gguf_file(&mut self, path: &str) -> Result<&rlx_gguf::GgufFile, String> {
158 if !self.gguf.contains_key(path) {
159 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
160 let g = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
161 self.gguf.insert(path.to_string(), g);
162 }
163 Ok(&self.gguf[path])
164 }
165
166 pub fn f32(&mut self, uri: &str) -> Result<Vec<f32>, String> {
168 if let Some(rest) = uri.strip_prefix("gguf://") {
169 let (path, tensor) = split_frag(rest)?;
170 let (data, _dims) = self
171 .gguf_file(path)?
172 .dequant_f32(tensor)
173 .map_err(|e| e.to_string())?;
174 Ok(data)
175 } else if uri.starts_with("safetensors://") || uri.starts_with("file://") {
176 resolve_weight_uri(uri) } else {
178 Err(format!("unsupported weight URI scheme: {uri}"))
179 }
180 }
181
182 pub fn bytes(&mut self, uri: &str) -> Result<Vec<u8>, String> {
184 if let Some(rest) = uri.strip_prefix("gguf://") {
185 let (path, tensor) = split_frag(rest)?;
186 let g = self.gguf_file(path)?;
187 let t = g
188 .get(tensor)
189 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
190 Ok(g.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
191 } else {
192 resolve_weight_bytes(uri)
193 }
194 }
195}
196
197pub fn uri_resolver(uri: &str) -> Vec<f32> {
201 match resolve_weight_uri(uri) {
202 Ok(v) => v,
203 Err(e) => {
204 eprintln!("rlx dist: weight resolve failed [{uri}]: {e}");
205 Vec::new()
206 }
207 }
208}
209
210fn bf16_to_f32(b: u16) -> f32 {
212 f32::from_bits((b as u32) << 16)
213}
214
215fn f16_to_f32(h: u16) -> f32 {
217 let sign = (h >> 15) & 1;
218 let exp = (h >> 10) & 0x1f;
219 let mant = h & 0x3ff;
220 let v = match exp {
221 0 => (mant as f32) * 2f32.powi(-24), 0x1f if mant == 0 => f32::INFINITY,
223 0x1f => f32::NAN,
224 _ => (1.0 + mant as f32 / 1024.0) * 2f32.powi(exp as i32 - 15),
225 };
226 if sign == 1 { -v } else { v }
227}
228
229fn read_safetensors_f32(buf: &[u8], name: &str) -> Result<Vec<f32>, String> {
232 if buf.len() < 8 {
233 return Err("safetensors file too small".into());
234 }
235 let hlen = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
236 let header = buf
237 .get(8..8 + hlen)
238 .ok_or("safetensors: truncated header")?;
239 let data = &buf[8 + hlen..];
240 let v: serde_json::Value = serde_json::from_slice(header).map_err(|e| e.to_string())?;
241 let t = v
242 .get(name)
243 .ok_or_else(|| format!("tensor not found: {name}"))?;
244 let dtype = t.get("dtype").and_then(|d| d.as_str()).ok_or("no dtype")?;
245 let off = t
246 .get("data_offsets")
247 .and_then(|o| o.as_array())
248 .ok_or("no data_offsets")?;
249 let start = off[0].as_u64().ok_or("bad data_offsets")? as usize;
250 let end = off[1].as_u64().ok_or("bad data_offsets")? as usize;
251 let raw = data
252 .get(start..end)
253 .ok_or("safetensors: data_offsets out of range")?;
254 Ok(match dtype {
255 "F32" => raw
256 .chunks_exact(4)
257 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
258 .collect(),
259 "F16" => raw
260 .chunks_exact(2)
261 .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
262 .collect(),
263 "BF16" => raw
264 .chunks_exact(2)
265 .map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
266 .collect(),
267 other => return Err(format!("unsupported safetensors dtype: {other}")),
268 })
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn resolve_safetensors_f32() {
277 let vals = [1.0f32, 2.0, -3.5, 4.25];
278 let data: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
279 let header = format!(
280 r#"{{"w":{{"dtype":"F32","shape":[4],"data_offsets":[0,{}]}}}}"#,
281 data.len()
282 );
283 let mut buf = (header.len() as u64).to_le_bytes().to_vec();
284 buf.extend_from_slice(header.as_bytes());
285 buf.extend_from_slice(&data);
286 let path =
287 std::env::temp_dir().join(format!("rlx_dist_{}.safetensors", std::process::id()));
288 std::fs::write(&path, &buf).unwrap();
289 let got = resolve_weight_uri(&format!("safetensors://{}#w", path.display())).unwrap();
290 std::fs::remove_file(&path).ok();
291 assert_eq!(got, vals);
292 }
293
294 #[test]
295 fn resolve_gguf_f32() {
296 use rlx_gguf::{GgmlType, GgufWriter};
297 let vals = [1.0f32, 2.0, -3.5, 4.25];
298 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
299 let mut w = GgufWriter::new();
300 w.add_tensor_bytes("w", vec![4], GgmlType::F32, bytes)
301 .unwrap();
302 let path = std::env::temp_dir().join(format!("rlx_dist_{}.gguf", std::process::id()));
303 w.write_to_path(&path).unwrap();
304 let got = resolve_weight_uri(&format!("gguf://{}#w", path.display())).unwrap();
305 std::fs::remove_file(&path).ok();
306 assert_eq!(got, vals);
307 }
308
309 #[test]
310 fn weight_cache_serves_many_tensors_from_one_file() {
311 use rlx_gguf::{GgmlType, GgufWriter};
312 let a = [1.0f32, 2.0, 3.0, 4.0];
313 let b = [10.0f32, 20.0];
314 let mut w = GgufWriter::new();
315 w.add_tensor_bytes(
316 "a",
317 vec![4],
318 GgmlType::F32,
319 a.iter().flat_map(|v| v.to_le_bytes()).collect(),
320 )
321 .unwrap();
322 w.add_tensor_bytes(
323 "b",
324 vec![2],
325 GgmlType::F32,
326 b.iter().flat_map(|v| v.to_le_bytes()).collect(),
327 )
328 .unwrap();
329 let path = std::env::temp_dir().join(format!("rlx_cache_{}.gguf", std::process::id()));
330 w.write_to_path(&path).unwrap();
331 let p = path.display();
332
333 let mut cache = WeightCache::new();
335 assert_eq!(cache.f32(&format!("gguf://{p}#a")).unwrap(), a);
336 assert_eq!(cache.f32(&format!("gguf://{p}#b")).unwrap(), b);
337 let raw = cache.bytes(&format!("gguf://{p}#a")).unwrap();
339 let raw_f32: Vec<f32> = raw
340 .chunks_exact(4)
341 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
342 .collect();
343 assert_eq!(raw_f32, a);
344 std::fs::remove_file(&path).ok();
345 }
346
347 #[test]
348 fn dequant_matmul_gguf_end_to_end() {
349 use crate::Session;
353 use rlx_ir::quant::QuantScheme;
354 use rlx_ir::{DType, Graph, Shape};
355
356 let (m, k, n) = (2usize, 32usize, 4usize); let w_floats: Vec<f32> = (0..n * k).map(|i| ((i % 17) as f32 - 8.0) * 0.1).collect();
358 let packed = rlx_gguf::quantize(&w_floats, rlx_gguf::GgmlType::Q8_0).unwrap();
359 let x: Vec<f32> = (0..m * k).map(|i| ((i % 13) as f32) * 0.05).collect();
360
361 let mut g = Graph::new("dq");
362 let xin = g.input("x", Shape::new(&[m, k], DType::F32));
363 let wp = g.param("W", Shape::new(&[packed.len()], DType::U8));
364 let out = g.dequant_matmul_packed(
365 xin,
366 wp,
367 QuantScheme::GgufQ8_0,
368 Shape::new(&[m, n], DType::F32),
369 );
370 g.set_outputs(vec![out]);
371
372 let mut c = Session::new(Device::Cpu).compile(g);
373 c.set_param_typed("W", &packed, DType::U8);
374 let got = c.run(&[("x", x.as_slice())]).into_iter().next().unwrap();
375
376 let w = rlx_gguf::dequant_q8_0(&packed, n * k).unwrap();
378 let mut expect = vec![0f32; m * n];
379 for i in 0..m {
380 for j in 0..n {
381 let mut acc = 0f32;
382 for c2 in 0..k {
383 acc += x[i * k + c2] * w[j * k + c2];
384 }
385 expect[i * n + j] = acc;
386 }
387 }
388 let err = got
389 .iter()
390 .zip(&expect)
391 .map(|(a, b)| (a - b).abs())
392 .fold(0f32, f32::max);
393 assert!(err < 1e-3, "max_err={err} got={got:?} expect={expect:?}");
394 }
395}