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