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
86#[cfg(not(feature = "model-io"))]
93fn needs_model_io(scheme: &str) -> String {
94 format!(
95 "weight URI scheme `{scheme}` needs rlx-runtime's `model-io` feature, which this build does not have. gguf://, safetensors:// and file:// work without it."
96 )
97}
98
99pub fn resolve_weight_uri(uri: &str) -> Result<Vec<f32>, String> {
111 if let Some(rest) = uri.strip_prefix("gguf://") {
112 let (path, tensor) = split_frag(rest)?;
113 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
114 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
115 let (data, _dims) = gguf.dequant_f32(tensor).map_err(|e| e.to_string())?;
116 Ok(data)
117 } else if let Some(_rest) = uri.strip_prefix("rlxp://") {
118 #[cfg(feature = "model-io")]
119 {
120 let (path, tensor) = split_frag(_rest)?;
121 let pack = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
122 pack.tensor_f32(tensor).map_err(|e| e.to_string())
123 }
124 #[cfg(not(feature = "model-io"))]
125 Err(needs_model_io("rlxp"))
126 } else if let Some(rest) = uri.strip_prefix("safetensors://") {
127 let (path, tensor) = split_frag(rest)?;
128 let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
129 read_safetensors_f32(&bytes, tensor)
130 } else if let Some(_rest) = uri
131 .strip_prefix("mlx://")
132 .or_else(|| uri.strip_prefix("npz://"))
133 {
134 #[cfg(feature = "model-io")]
135 {
136 let (path, tensor) = split_frag(_rest)?;
137 let w = rlx_mlx_io::load_path(path).map_err(|e| e.to_string())?;
138 let map = w.into_f32_map().map_err(|e| e.to_string())?;
139 map.get(tensor)
140 .cloned()
141 .ok_or_else(|| format!("tensor not found in mlx weights: {tensor}"))
142 }
143 #[cfg(not(feature = "model-io"))]
144 Err(needs_model_io("mlx/npz"))
145 } else if let Some(_rest) = uri.strip_prefix("dduf://") {
146 #[cfg(feature = "model-io")]
147 {
148 let (path, tensor) = split_frag(_rest)?;
149 let f = rlx_dduf::DdufFile::open(path).map_err(|e| e.to_string())?;
150 f.tensor_f32(tensor)
151 .map(|s| s.to_vec())
152 .map_err(|e| e.to_string())
153 }
154 #[cfg(not(feature = "model-io"))]
155 Err(needs_model_io("dduf"))
156 } else if let Some(path) = uri.strip_prefix("file://") {
157 let b = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
158 Ok(b.chunks_exact(4)
159 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
160 .collect())
161 } else {
162 Err(format!("unsupported weight URI scheme: {uri}"))
163 }
164}
165
166pub fn resolve_weight_bytes(uri: &str) -> Result<Vec<u8>, String> {
172 if let Some(rest) = uri.strip_prefix("gguf://") {
173 let (path, tensor) = split_frag(rest)?;
174 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
175 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
176 let t = gguf
177 .get(tensor)
178 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
179 Ok(gguf.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
180 } else if let Some(_rest) = uri.strip_prefix("rlxp://") {
181 #[cfg(feature = "model-io")]
182 {
183 let (path, tensor) = split_frag(_rest)?;
184 let pack = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
185 pack.tensor_bytes(tensor).map_err(|e| e.to_string())
186 }
187 #[cfg(not(feature = "model-io"))]
188 Err(needs_model_io("rlxp"))
189 } else if let Some(path) = uri.strip_prefix("file://") {
190 std::fs::read(path).map_err(|e| format!("read {path}: {e}"))
191 } else {
192 Err(format!("packed load unsupported for scheme: {uri}"))
193 }
194}
195
196#[derive(Default)]
202pub struct WeightCache {
203 gguf: HashMap<String, rlx_gguf::GgufFile>,
204 #[cfg(feature = "model-io")]
205 rlxp: HashMap<String, rlx_pkg::Package>,
206}
207
208impl WeightCache {
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 fn gguf_file(&mut self, path: &str) -> Result<&rlx_gguf::GgufFile, String> {
214 if !self.gguf.contains_key(path) {
215 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
216 let g = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
217 self.gguf.insert(path.to_string(), g);
218 }
219 Ok(&self.gguf[path])
220 }
221
222 #[cfg(feature = "model-io")]
223 fn rlxp_pack(&mut self, path: &str) -> Result<&rlx_pkg::Package, String> {
224 if !self.rlxp.contains_key(path) {
225 let p = rlx_pkg::Package::open(path).map_err(|e| e.to_string())?;
226 self.rlxp.insert(path.to_string(), p);
227 }
228 Ok(&self.rlxp[path])
229 }
230
231 pub fn f32(&mut self, uri: &str) -> Result<Vec<f32>, String> {
233 if let Some(rest) = uri.strip_prefix("gguf://") {
234 let (path, tensor) = split_frag(rest)?;
235 let (data, _dims) = self
236 .gguf_file(path)?
237 .dequant_f32(tensor)
238 .map_err(|e| e.to_string())?;
239 Ok(data)
240 } else if let Some(_rest) = uri.strip_prefix("rlxp://") {
241 #[cfg(feature = "model-io")]
242 {
243 let (path, tensor) = split_frag(_rest)?;
244 self.rlxp_pack(path)?
245 .tensor_f32(tensor)
246 .map_err(|e| e.to_string())
247 }
248 #[cfg(not(feature = "model-io"))]
249 Err(needs_model_io("rlxp"))
250 } else if uri.starts_with("safetensors://")
251 || uri.starts_with("file://")
252 || uri.starts_with("mlx://")
253 || uri.starts_with("npz://")
254 || uri.starts_with("dduf://")
255 {
256 resolve_weight_uri(uri) } else {
258 Err(format!("unsupported weight URI scheme: {uri}"))
259 }
260 }
261
262 pub fn bytes(&mut self, uri: &str) -> Result<Vec<u8>, String> {
264 if let Some(rest) = uri.strip_prefix("gguf://") {
265 let (path, tensor) = split_frag(rest)?;
266 let g = self.gguf_file(path)?;
267 let t = g
268 .get(tensor)
269 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
270 Ok(g.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
271 } else if let Some(_rest) = uri.strip_prefix("rlxp://") {
272 #[cfg(feature = "model-io")]
273 {
274 let (path, tensor) = split_frag(_rest)?;
275 self.rlxp_pack(path)?
276 .tensor_bytes(tensor)
277 .map_err(|e| e.to_string())
278 }
279 #[cfg(not(feature = "model-io"))]
280 Err(needs_model_io("rlxp"))
281 } else {
282 resolve_weight_bytes(uri)
283 }
284 }
285}
286
287pub fn uri_resolver(uri: &str) -> Vec<f32> {
291 match resolve_weight_uri(uri) {
292 Ok(v) => v,
293 Err(e) => {
294 eprintln!("rlx dist: weight resolve failed [{uri}]: {e}");
295 Vec::new()
296 }
297 }
298}
299
300fn bf16_to_f32(b: u16) -> f32 {
302 f32::from_bits((b as u32) << 16)
303}
304
305fn f16_to_f32(h: u16) -> f32 {
307 let sign = (h >> 15) & 1;
308 let exp = (h >> 10) & 0x1f;
309 let mant = h & 0x3ff;
310 let v = match exp {
311 0 => (mant as f32) * 2f32.powi(-24), 0x1f if mant == 0 => f32::INFINITY,
313 0x1f => f32::NAN,
314 _ => (1.0 + mant as f32 / 1024.0) * 2f32.powi(exp as i32 - 15),
315 };
316 if sign == 1 { -v } else { v }
317}
318
319fn read_safetensors_f32(buf: &[u8], name: &str) -> Result<Vec<f32>, String> {
322 if buf.len() < 8 {
323 return Err("safetensors file too small".into());
324 }
325 let hlen = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
326 let header = buf
327 .get(8..8 + hlen)
328 .ok_or("safetensors: truncated header")?;
329 let data = &buf[8 + hlen..];
330 let v: serde_json::Value = serde_json::from_slice(header).map_err(|e| e.to_string())?;
331 let t = v
332 .get(name)
333 .ok_or_else(|| format!("tensor not found: {name}"))?;
334 let dtype = t.get("dtype").and_then(|d| d.as_str()).ok_or("no dtype")?;
335 let off = t
336 .get("data_offsets")
337 .and_then(|o| o.as_array())
338 .ok_or("no data_offsets")?;
339 let start = off[0].as_u64().ok_or("bad data_offsets")? as usize;
340 let end = off[1].as_u64().ok_or("bad data_offsets")? as usize;
341 let raw = data
342 .get(start..end)
343 .ok_or("safetensors: data_offsets out of range")?;
344 Ok(match dtype {
345 "F32" => raw
346 .chunks_exact(4)
347 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
348 .collect(),
349 "F16" => raw
350 .chunks_exact(2)
351 .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
352 .collect(),
353 "BF16" => raw
354 .chunks_exact(2)
355 .map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
356 .collect(),
357 other => return Err(format!("unsupported safetensors dtype: {other}")),
358 })
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn resolve_safetensors_f32() {
367 let vals = [1.0f32, 2.0, -3.5, 4.25];
368 let data: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
369 let header = format!(
370 r#"{{"w":{{"dtype":"F32","shape":[4],"data_offsets":[0,{}]}}}}"#,
371 data.len()
372 );
373 let mut buf = (header.len() as u64).to_le_bytes().to_vec();
374 buf.extend_from_slice(header.as_bytes());
375 buf.extend_from_slice(&data);
376 let path =
377 std::env::temp_dir().join(format!("rlx_dist_{}.safetensors", std::process::id()));
378 std::fs::write(&path, &buf).unwrap();
379 let got = resolve_weight_uri(&format!("safetensors://{}#w", path.display())).unwrap();
380 std::fs::remove_file(&path).ok();
381 assert_eq!(got, vals);
382 }
383
384 #[test]
385 fn resolve_gguf_f32() {
386 use rlx_gguf::{GgmlType, GgufWriter};
387 let vals = [1.0f32, 2.0, -3.5, 4.25];
388 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
389 let mut w = GgufWriter::new();
390 w.add_tensor_bytes("w", vec![4], GgmlType::F32, bytes)
391 .unwrap();
392 let path = std::env::temp_dir().join(format!("rlx_dist_{}.gguf", std::process::id()));
393 w.write_to_path(&path).unwrap();
394 let got = resolve_weight_uri(&format!("gguf://{}#w", path.display())).unwrap();
395 std::fs::remove_file(&path).ok();
396 assert_eq!(got, vals);
397 }
398
399 #[test]
400 fn weight_cache_serves_many_tensors_from_one_file() {
401 use rlx_gguf::{GgmlType, GgufWriter};
402 let a = [1.0f32, 2.0, 3.0, 4.0];
403 let b = [10.0f32, 20.0];
404 let mut w = GgufWriter::new();
405 w.add_tensor_bytes(
406 "a",
407 vec![4],
408 GgmlType::F32,
409 a.iter().flat_map(|v| v.to_le_bytes()).collect(),
410 )
411 .unwrap();
412 w.add_tensor_bytes(
413 "b",
414 vec![2],
415 GgmlType::F32,
416 b.iter().flat_map(|v| v.to_le_bytes()).collect(),
417 )
418 .unwrap();
419 let path = std::env::temp_dir().join(format!("rlx_cache_{}.gguf", std::process::id()));
420 w.write_to_path(&path).unwrap();
421 let p = path.display();
422
423 let mut cache = WeightCache::new();
425 assert_eq!(cache.f32(&format!("gguf://{p}#a")).unwrap(), a);
426 assert_eq!(cache.f32(&format!("gguf://{p}#b")).unwrap(), b);
427 let raw = cache.bytes(&format!("gguf://{p}#a")).unwrap();
429 let raw_f32: Vec<f32> = raw
430 .chunks_exact(4)
431 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
432 .collect();
433 assert_eq!(raw_f32, a);
434 std::fs::remove_file(&path).ok();
435 }
436
437 #[test]
438 #[cfg(feature = "model-io")]
439 fn resolve_rlxp_f32() {
440 use rlx_ir::op::BinaryOp;
441 use rlx_ir::{DType, Graph, Shape};
442 use rlx_pkg::{BakeWeight, WriteOptions, package_from_bake};
443
444 let s = Shape::new(&[4], DType::F32);
445 let mut g = Graph::new("rlxp_uri");
446 let x = g.input("x", s.clone());
447 let w = g.param("w", s.clone());
448 let y = g.binary(BinaryOp::Mul, x, w, s);
449 g.set_outputs(vec![y]);
450 let vals = [1.0f32, 2.0, -3.5, 4.25];
451 let weights = [BakeWeight {
452 name: "w".into(),
453 shape: vec![4],
454 encoding: "f32".into(),
455 data: vals.iter().flat_map(|v| v.to_le_bytes()).collect(),
456 }];
457 let dir = tempfile::tempdir().unwrap();
458 let path = dir.path().join("m.rlxp");
459 package_from_bake(
460 &path,
461 &g,
462 &weights,
463 WriteOptions {
464 container: rlx_pkg::ContainerKind::Flat,
465 name: "rlxp_uri".into(),
466 ..WriteOptions::default()
467 },
468 )
469 .unwrap();
470 let got = resolve_weight_uri(&format!("rlxp://{}#w", path.display())).unwrap();
471 assert_eq!(got, vals);
472 let mut cache = WeightCache::new();
473 assert_eq!(
474 cache.f32(&format!("rlxp://{}#w", path.display())).unwrap(),
475 vals
476 );
477 }
478
479 #[test]
480 fn dequant_matmul_gguf_end_to_end() {
481 use crate::Session;
485 use rlx_ir::quant::QuantScheme;
486 use rlx_ir::{DType, Graph, Shape};
487
488 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();
490 let packed = rlx_gguf::quantize(&w_floats, rlx_gguf::GgmlType::Q8_0).unwrap();
491 let x: Vec<f32> = (0..m * k).map(|i| ((i % 13) as f32) * 0.05).collect();
492
493 let mut g = Graph::new("dq");
494 let xin = g.input("x", Shape::new(&[m, k], DType::F32));
495 let wp = g.param("W", Shape::new(&[packed.len()], DType::U8));
496 let out = g.dequant_matmul_packed(
497 xin,
498 wp,
499 QuantScheme::GgufQ8_0,
500 Shape::new(&[m, n], DType::F32),
501 );
502 g.set_outputs(vec![out]);
503
504 let mut c = Session::new(Device::Cpu).compile(g);
505 c.set_param_typed("W", &packed, DType::U8);
506 let got = c.run(&[("x", x.as_slice())]).into_iter().next().unwrap();
507
508 let w = rlx_gguf::dequant_q8_0(&packed, n * k).unwrap();
510 let mut expect = vec![0f32; m * n];
511 for i in 0..m {
512 for j in 0..n {
513 let mut acc = 0f32;
514 for c2 in 0..k {
515 acc += x[i * k + c2] * w[j * k + c2];
516 }
517 expect[i * n + j] = acc;
518 }
519 }
520 let err = got
521 .iter()
522 .zip(&expect)
523 .map(|(a, b)| (a - b).abs())
524 .fold(0f32, f32::max);
525 assert!(err < 1e-3, "max_err={err} got={got:?} expect={expect:?}");
526 }
527}