1use crate::Device;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38mod diagnostics;
39mod inference;
40mod training;
41
42pub use diagnostics::*;
43pub use inference::*;
44pub use training::*;
45
46#[derive(Serialize, Deserialize, Clone, Debug)]
49pub struct WeightRef {
50 pub name: String,
51 pub uri: String,
54 #[serde(default)]
60 pub packed: bool,
61}
62
63fn resolve_device(spec: &str) -> Device {
64 if spec.eq_ignore_ascii_case("auto") || spec.is_empty() {
65 return crate::fastest_device();
66 }
67 match crate::parse_device(spec) {
68 Ok(d) if crate::is_available(d) => d,
69 _ => crate::fastest_device(),
70 }
71}
72
73fn split_frag(rest: &str) -> Result<(&str, &str), String> {
80 rest.split_once('#')
81 .ok_or_else(|| format!("weight URI needs a #<tensor> fragment: {rest}"))
82}
83
84pub fn resolve_weight_uri(uri: &str) -> Result<Vec<f32>, String> {
96 if let Some(rest) = uri.strip_prefix("gguf://") {
97 let (path, tensor) = split_frag(rest)?;
98 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
99 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
100 let (data, _dims) = gguf.dequant_f32(tensor).map_err(|e| e.to_string())?;
101 Ok(data)
102 } else if let Some(rest) = uri.strip_prefix("rlxp://") {
103 let (path, tensor) = split_frag(rest)?;
104 let pack = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
105 pack.tensor_f32(tensor).map_err(|e| e.to_string())
106 } else if let Some(rest) = uri.strip_prefix("safetensors://") {
107 let (path, tensor) = split_frag(rest)?;
108 let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
109 read_safetensors_f32(&bytes, tensor)
110 } else if let Some(rest) = uri
111 .strip_prefix("mlx://")
112 .or_else(|| uri.strip_prefix("npz://"))
113 {
114 let (path, tensor) = split_frag(rest)?;
115 let w = rlx_mlx_io::load_path(path).map_err(|e| e.to_string())?;
116 let map = w.into_f32_map().map_err(|e| e.to_string())?;
117 map.get(tensor)
118 .cloned()
119 .ok_or_else(|| format!("tensor not found in mlx weights: {tensor}"))
120 } else if let Some(rest) = uri.strip_prefix("dduf://") {
121 let (path, tensor) = split_frag(rest)?;
122 let f = rlx_dduf::DdufFile::open(path).map_err(|e| e.to_string())?;
123 f.tensor_f32(tensor)
124 .map(|s| s.to_vec())
125 .map_err(|e| e.to_string())
126 } else if let Some(path) = uri.strip_prefix("file://") {
127 let b = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
128 Ok(b.chunks_exact(4)
129 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
130 .collect())
131 } else {
132 Err(format!("unsupported weight URI scheme: {uri}"))
133 }
134}
135
136pub fn resolve_weight_bytes(uri: &str) -> Result<Vec<u8>, String> {
142 if let Some(rest) = uri.strip_prefix("gguf://") {
143 let (path, tensor) = split_frag(rest)?;
144 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
145 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
146 let t = gguf
147 .get(tensor)
148 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
149 Ok(gguf.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
150 } else if let Some(rest) = uri.strip_prefix("rlxp://") {
151 let (path, tensor) = split_frag(rest)?;
152 let pack = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
153 pack.tensor_bytes(tensor).map_err(|e| e.to_string())
154 } else if let Some(path) = uri.strip_prefix("file://") {
155 std::fs::read(path).map_err(|e| format!("read {path}: {e}"))
156 } else {
157 Err(format!("packed load unsupported for scheme: {uri}"))
158 }
159}
160
161#[derive(Default)]
167pub struct WeightCache {
168 gguf: HashMap<String, rlx_gguf::GgufFile>,
169 rlxp: HashMap<String, rlx_pkg::Package>,
170}
171
172impl WeightCache {
173 pub fn new() -> Self {
174 Self::default()
175 }
176
177 fn gguf_file(&mut self, path: &str) -> Result<&rlx_gguf::GgufFile, String> {
178 if !self.gguf.contains_key(path) {
179 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
180 let g = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
181 self.gguf.insert(path.to_string(), g);
182 }
183 Ok(&self.gguf[path])
184 }
185
186 fn rlxp_pack(&mut self, path: &str) -> Result<&rlx_pkg::Package, String> {
187 if !self.rlxp.contains_key(path) {
188 let p = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
189 self.rlxp.insert(path.to_string(), p);
190 }
191 Ok(&self.rlxp[path])
192 }
193
194 pub fn f32(&mut self, uri: &str) -> Result<Vec<f32>, String> {
196 if let Some(rest) = uri.strip_prefix("gguf://") {
197 let (path, tensor) = split_frag(rest)?;
198 let (data, _dims) = self
199 .gguf_file(path)?
200 .dequant_f32(tensor)
201 .map_err(|e| e.to_string())?;
202 Ok(data)
203 } else if let Some(rest) = uri.strip_prefix("rlxp://") {
204 let (path, tensor) = split_frag(rest)?;
205 self.rlxp_pack(path)?
206 .tensor_f32(tensor)
207 .map_err(|e| e.to_string())
208 } else if uri.starts_with("safetensors://")
209 || uri.starts_with("file://")
210 || uri.starts_with("mlx://")
211 || uri.starts_with("npz://")
212 || uri.starts_with("dduf://")
213 {
214 resolve_weight_uri(uri) } else {
216 Err(format!("unsupported weight URI scheme: {uri}"))
217 }
218 }
219
220 pub fn bytes(&mut self, uri: &str) -> Result<Vec<u8>, String> {
222 if let Some(rest) = uri.strip_prefix("gguf://") {
223 let (path, tensor) = split_frag(rest)?;
224 let g = self.gguf_file(path)?;
225 let t = g
226 .get(tensor)
227 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
228 Ok(g.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
229 } else if let Some(rest) = uri.strip_prefix("rlxp://") {
230 let (path, tensor) = split_frag(rest)?;
231 self.rlxp_pack(path)?
232 .tensor_bytes(tensor)
233 .map_err(|e| e.to_string())
234 } else {
235 resolve_weight_bytes(uri)
236 }
237 }
238}
239
240pub fn uri_resolver(uri: &str) -> Vec<f32> {
244 match resolve_weight_uri(uri) {
245 Ok(v) => v,
246 Err(e) => {
247 eprintln!("rlx dist: weight resolve failed [{uri}]: {e}");
248 Vec::new()
249 }
250 }
251}
252
253fn bf16_to_f32(b: u16) -> f32 {
255 f32::from_bits((b as u32) << 16)
256}
257
258fn f16_to_f32(h: u16) -> f32 {
260 let sign = (h >> 15) & 1;
261 let exp = (h >> 10) & 0x1f;
262 let mant = h & 0x3ff;
263 let v = match exp {
264 0 => (mant as f32) * 2f32.powi(-24), 0x1f if mant == 0 => f32::INFINITY,
266 0x1f => f32::NAN,
267 _ => (1.0 + mant as f32 / 1024.0) * 2f32.powi(exp as i32 - 15),
268 };
269 if sign == 1 { -v } else { v }
270}
271
272fn read_safetensors_f32(buf: &[u8], name: &str) -> Result<Vec<f32>, String> {
275 if buf.len() < 8 {
276 return Err("safetensors file too small".into());
277 }
278 let hlen = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
279 let header = buf
280 .get(8..8 + hlen)
281 .ok_or("safetensors: truncated header")?;
282 let data = &buf[8 + hlen..];
283 let v: serde_json::Value = serde_json::from_slice(header).map_err(|e| e.to_string())?;
284 let t = v
285 .get(name)
286 .ok_or_else(|| format!("tensor not found: {name}"))?;
287 let dtype = t.get("dtype").and_then(|d| d.as_str()).ok_or("no dtype")?;
288 let off = t
289 .get("data_offsets")
290 .and_then(|o| o.as_array())
291 .ok_or("no data_offsets")?;
292 let start = off[0].as_u64().ok_or("bad data_offsets")? as usize;
293 let end = off[1].as_u64().ok_or("bad data_offsets")? as usize;
294 let raw = data
295 .get(start..end)
296 .ok_or("safetensors: data_offsets out of range")?;
297 Ok(match dtype {
298 "F32" => raw
299 .chunks_exact(4)
300 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
301 .collect(),
302 "F16" => raw
303 .chunks_exact(2)
304 .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
305 .collect(),
306 "BF16" => raw
307 .chunks_exact(2)
308 .map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
309 .collect(),
310 other => return Err(format!("unsupported safetensors dtype: {other}")),
311 })
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn resolve_safetensors_f32() {
320 let vals = [1.0f32, 2.0, -3.5, 4.25];
321 let data: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
322 let header = format!(
323 r#"{{"w":{{"dtype":"F32","shape":[4],"data_offsets":[0,{}]}}}}"#,
324 data.len()
325 );
326 let mut buf = (header.len() as u64).to_le_bytes().to_vec();
327 buf.extend_from_slice(header.as_bytes());
328 buf.extend_from_slice(&data);
329 let path =
330 std::env::temp_dir().join(format!("rlx_dist_{}.safetensors", std::process::id()));
331 std::fs::write(&path, &buf).unwrap();
332 let got = resolve_weight_uri(&format!("safetensors://{}#w", path.display())).unwrap();
333 std::fs::remove_file(&path).ok();
334 assert_eq!(got, vals);
335 }
336
337 #[test]
338 fn resolve_gguf_f32() {
339 use rlx_gguf::{GgmlType, GgufWriter};
340 let vals = [1.0f32, 2.0, -3.5, 4.25];
341 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
342 let mut w = GgufWriter::new();
343 w.add_tensor_bytes("w", vec![4], GgmlType::F32, bytes)
344 .unwrap();
345 let path = std::env::temp_dir().join(format!("rlx_dist_{}.gguf", std::process::id()));
346 w.write_to_path(&path).unwrap();
347 let got = resolve_weight_uri(&format!("gguf://{}#w", path.display())).unwrap();
348 std::fs::remove_file(&path).ok();
349 assert_eq!(got, vals);
350 }
351
352 #[test]
353 fn weight_cache_serves_many_tensors_from_one_file() {
354 use rlx_gguf::{GgmlType, GgufWriter};
355 let a = [1.0f32, 2.0, 3.0, 4.0];
356 let b = [10.0f32, 20.0];
357 let mut w = GgufWriter::new();
358 w.add_tensor_bytes(
359 "a",
360 vec![4],
361 GgmlType::F32,
362 a.iter().flat_map(|v| v.to_le_bytes()).collect(),
363 )
364 .unwrap();
365 w.add_tensor_bytes(
366 "b",
367 vec![2],
368 GgmlType::F32,
369 b.iter().flat_map(|v| v.to_le_bytes()).collect(),
370 )
371 .unwrap();
372 let path = std::env::temp_dir().join(format!("rlx_cache_{}.gguf", std::process::id()));
373 w.write_to_path(&path).unwrap();
374 let p = path.display();
375
376 let mut cache = WeightCache::new();
378 assert_eq!(cache.f32(&format!("gguf://{p}#a")).unwrap(), a);
379 assert_eq!(cache.f32(&format!("gguf://{p}#b")).unwrap(), b);
380 let raw = cache.bytes(&format!("gguf://{p}#a")).unwrap();
382 let raw_f32: Vec<f32> = raw
383 .chunks_exact(4)
384 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
385 .collect();
386 assert_eq!(raw_f32, a);
387 std::fs::remove_file(&path).ok();
388 }
389
390 #[test]
391 fn resolve_rlxp_f32() {
392 use rlx_ir::op::BinaryOp;
393 use rlx_ir::{DType, Graph, Shape};
394 use rlx_pkg::{BakeWeight, WriteOptions, package_from_bake};
395
396 let s = Shape::new(&[4], DType::F32);
397 let mut g = Graph::new("rlxp_uri");
398 let x = g.input("x", s.clone());
399 let w = g.param("w", s.clone());
400 let y = g.binary(BinaryOp::Mul, x, w, s);
401 g.set_outputs(vec![y]);
402 let vals = [1.0f32, 2.0, -3.5, 4.25];
403 let weights = [BakeWeight {
404 name: "w".into(),
405 shape: vec![4],
406 encoding: "f32".into(),
407 data: vals.iter().flat_map(|v| v.to_le_bytes()).collect(),
408 }];
409 let dir = tempfile::tempdir().unwrap();
410 let path = dir.path().join("m.rlxp");
411 package_from_bake(
412 &path,
413 &g,
414 &weights,
415 WriteOptions {
416 container: rlx_pkg::ContainerKind::Flat,
417 name: "rlxp_uri".into(),
418 ..WriteOptions::default()
419 },
420 )
421 .unwrap();
422 let got = resolve_weight_uri(&format!("rlxp://{}#w", path.display())).unwrap();
423 assert_eq!(got, vals);
424 let mut cache = WeightCache::new();
425 assert_eq!(
426 cache.f32(&format!("rlxp://{}#w", path.display())).unwrap(),
427 vals
428 );
429 }
430
431 #[test]
432 fn dequant_matmul_gguf_end_to_end() {
433 use crate::Session;
437 use rlx_ir::quant::QuantScheme;
438 use rlx_ir::{DType, Graph, Shape};
439
440 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();
442 let packed = rlx_gguf::quantize(&w_floats, rlx_gguf::GgmlType::Q8_0).unwrap();
443 let x: Vec<f32> = (0..m * k).map(|i| ((i % 13) as f32) * 0.05).collect();
444
445 let mut g = Graph::new("dq");
446 let xin = g.input("x", Shape::new(&[m, k], DType::F32));
447 let wp = g.param("W", Shape::new(&[packed.len()], DType::U8));
448 let out = g.dequant_matmul_packed(
449 xin,
450 wp,
451 QuantScheme::GgufQ8_0,
452 Shape::new(&[m, n], DType::F32),
453 );
454 g.set_outputs(vec![out]);
455
456 let mut c = Session::new(Device::Cpu).compile(g);
457 c.set_param_typed("W", &packed, DType::U8);
458 let got = c.run(&[("x", x.as_slice())]).into_iter().next().unwrap();
459
460 let w = rlx_gguf::dequant_q8_0(&packed, n * k).unwrap();
462 let mut expect = vec![0f32; m * n];
463 for i in 0..m {
464 for j in 0..n {
465 let mut acc = 0f32;
466 for c2 in 0..k {
467 acc += x[i * k + c2] * w[j * k + c2];
468 }
469 expect[i * n + j] = acc;
470 }
471 }
472 let err = got
473 .iter()
474 .zip(&expect)
475 .map(|(a, b)| (a - b).abs())
476 .fold(0f32, f32::max);
477 assert!(err < 1e-3, "max_err={err} got={got:?} expect={expect:?}");
478 }
479}