use crate::Device;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
mod diagnostics;
mod inference;
mod training;
pub use diagnostics::*;
pub use inference::*;
pub use training::*;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct WeightRef {
pub name: String,
pub uri: String,
#[serde(default)]
pub packed: bool,
}
fn resolve_device(spec: &str) -> Device {
if spec.eq_ignore_ascii_case("auto") || spec.is_empty() {
return crate::fastest_device();
}
match crate::parse_device(spec) {
Ok(d) if crate::is_available(d) => d,
_ => crate::fastest_device(),
}
}
fn split_frag(rest: &str) -> Result<(&str, &str), String> {
rest.split_once('#')
.ok_or_else(|| format!("weight URI needs a #<tensor> fragment: {rest}"))
}
pub fn resolve_weight_uri(uri: &str) -> Result<Vec<f32>, String> {
if let Some(rest) = uri.strip_prefix("gguf://") {
let (path, tensor) = split_frag(rest)?;
let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
let (data, _dims) = gguf.dequant_f32(tensor).map_err(|e| e.to_string())?;
Ok(data)
} else if let Some(rest) = uri.strip_prefix("safetensors://") {
let (path, tensor) = split_frag(rest)?;
let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
read_safetensors_f32(&bytes, tensor)
} else if let Some(path) = uri.strip_prefix("file://") {
let b = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
Ok(b.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
} else {
Err(format!("unsupported weight URI scheme: {uri}"))
}
}
pub fn resolve_weight_bytes(uri: &str) -> Result<Vec<u8>, String> {
if let Some(rest) = uri.strip_prefix("gguf://") {
let (path, tensor) = split_frag(rest)?;
let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
let t = gguf
.get(tensor)
.ok_or_else(|| format!("tensor not found: {tensor}"))?;
Ok(gguf.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
} else if let Some(path) = uri.strip_prefix("file://") {
std::fs::read(path).map_err(|e| format!("read {path}: {e}"))
} else {
Err(format!("packed load unsupported for scheme: {uri}"))
}
}
#[derive(Default)]
pub struct WeightCache {
gguf: HashMap<String, rlx_gguf::GgufFile>,
}
impl WeightCache {
pub fn new() -> Self {
Self::default()
}
fn gguf_file(&mut self, path: &str) -> Result<&rlx_gguf::GgufFile, String> {
if !self.gguf.contains_key(path) {
let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
let g = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
self.gguf.insert(path.to_string(), g);
}
Ok(&self.gguf[path])
}
pub fn f32(&mut self, uri: &str) -> Result<Vec<f32>, String> {
if let Some(rest) = uri.strip_prefix("gguf://") {
let (path, tensor) = split_frag(rest)?;
let (data, _dims) = self
.gguf_file(path)?
.dequant_f32(tensor)
.map_err(|e| e.to_string())?;
Ok(data)
} else if uri.starts_with("safetensors://") || uri.starts_with("file://") {
resolve_weight_uri(uri) } else {
Err(format!("unsupported weight URI scheme: {uri}"))
}
}
pub fn bytes(&mut self, uri: &str) -> Result<Vec<u8>, String> {
if let Some(rest) = uri.strip_prefix("gguf://") {
let (path, tensor) = split_frag(rest)?;
let g = self.gguf_file(path)?;
let t = g
.get(tensor)
.ok_or_else(|| format!("tensor not found: {tensor}"))?;
Ok(g.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
} else {
resolve_weight_bytes(uri)
}
}
}
pub fn uri_resolver(uri: &str) -> Vec<f32> {
match resolve_weight_uri(uri) {
Ok(v) => v,
Err(e) => {
eprintln!("rlx dist: weight resolve failed [{uri}]: {e}");
Vec::new()
}
}
}
fn bf16_to_f32(b: u16) -> f32 {
f32::from_bits((b as u32) << 16)
}
fn f16_to_f32(h: u16) -> f32 {
let sign = (h >> 15) & 1;
let exp = (h >> 10) & 0x1f;
let mant = h & 0x3ff;
let v = match exp {
0 => (mant as f32) * 2f32.powi(-24), 0x1f if mant == 0 => f32::INFINITY,
0x1f => f32::NAN,
_ => (1.0 + mant as f32 / 1024.0) * 2f32.powi(exp as i32 - 15),
};
if sign == 1 { -v } else { v }
}
fn read_safetensors_f32(buf: &[u8], name: &str) -> Result<Vec<f32>, String> {
if buf.len() < 8 {
return Err("safetensors file too small".into());
}
let hlen = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
let header = buf
.get(8..8 + hlen)
.ok_or("safetensors: truncated header")?;
let data = &buf[8 + hlen..];
let v: serde_json::Value = serde_json::from_slice(header).map_err(|e| e.to_string())?;
let t = v
.get(name)
.ok_or_else(|| format!("tensor not found: {name}"))?;
let dtype = t.get("dtype").and_then(|d| d.as_str()).ok_or("no dtype")?;
let off = t
.get("data_offsets")
.and_then(|o| o.as_array())
.ok_or("no data_offsets")?;
let start = off[0].as_u64().ok_or("bad data_offsets")? as usize;
let end = off[1].as_u64().ok_or("bad data_offsets")? as usize;
let raw = data
.get(start..end)
.ok_or("safetensors: data_offsets out of range")?;
Ok(match dtype {
"F32" => raw
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
"F16" => raw
.chunks_exact(2)
.map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
.collect(),
"BF16" => raw
.chunks_exact(2)
.map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
.collect(),
other => return Err(format!("unsupported safetensors dtype: {other}")),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_safetensors_f32() {
let vals = [1.0f32, 2.0, -3.5, 4.25];
let data: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
let header = format!(
r#"{{"w":{{"dtype":"F32","shape":[4],"data_offsets":[0,{}]}}}}"#,
data.len()
);
let mut buf = (header.len() as u64).to_le_bytes().to_vec();
buf.extend_from_slice(header.as_bytes());
buf.extend_from_slice(&data);
let path =
std::env::temp_dir().join(format!("rlx_dist_{}.safetensors", std::process::id()));
std::fs::write(&path, &buf).unwrap();
let got = resolve_weight_uri(&format!("safetensors://{}#w", path.display())).unwrap();
std::fs::remove_file(&path).ok();
assert_eq!(got, vals);
}
#[test]
fn resolve_gguf_f32() {
use rlx_gguf::{GgmlType, GgufWriter};
let vals = [1.0f32, 2.0, -3.5, 4.25];
let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
let mut w = GgufWriter::new();
w.add_tensor_bytes("w", vec![4], GgmlType::F32, bytes)
.unwrap();
let path = std::env::temp_dir().join(format!("rlx_dist_{}.gguf", std::process::id()));
w.write_to_path(&path).unwrap();
let got = resolve_weight_uri(&format!("gguf://{}#w", path.display())).unwrap();
std::fs::remove_file(&path).ok();
assert_eq!(got, vals);
}
#[test]
fn weight_cache_serves_many_tensors_from_one_file() {
use rlx_gguf::{GgmlType, GgufWriter};
let a = [1.0f32, 2.0, 3.0, 4.0];
let b = [10.0f32, 20.0];
let mut w = GgufWriter::new();
w.add_tensor_bytes(
"a",
vec![4],
GgmlType::F32,
a.iter().flat_map(|v| v.to_le_bytes()).collect(),
)
.unwrap();
w.add_tensor_bytes(
"b",
vec![2],
GgmlType::F32,
b.iter().flat_map(|v| v.to_le_bytes()).collect(),
)
.unwrap();
let path = std::env::temp_dir().join(format!("rlx_cache_{}.gguf", std::process::id()));
w.write_to_path(&path).unwrap();
let p = path.display();
let mut cache = WeightCache::new();
assert_eq!(cache.f32(&format!("gguf://{p}#a")).unwrap(), a);
assert_eq!(cache.f32(&format!("gguf://{p}#b")).unwrap(), b);
let raw = cache.bytes(&format!("gguf://{p}#a")).unwrap();
let raw_f32: Vec<f32> = raw
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
assert_eq!(raw_f32, a);
std::fs::remove_file(&path).ok();
}
#[test]
fn dequant_matmul_gguf_end_to_end() {
use crate::Session;
use rlx_ir::quant::QuantScheme;
use rlx_ir::{DType, Graph, Shape};
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();
let packed = rlx_gguf::quantize(&w_floats, rlx_gguf::GgmlType::Q8_0).unwrap();
let x: Vec<f32> = (0..m * k).map(|i| ((i % 13) as f32) * 0.05).collect();
let mut g = Graph::new("dq");
let xin = g.input("x", Shape::new(&[m, k], DType::F32));
let wp = g.param("W", Shape::new(&[packed.len()], DType::U8));
let out = g.dequant_matmul_packed(
xin,
wp,
QuantScheme::GgufQ8_0,
Shape::new(&[m, n], DType::F32),
);
g.set_outputs(vec![out]);
let mut c = Session::new(Device::Cpu).compile(g);
c.set_param_typed("W", &packed, DType::U8);
let got = c.run(&[("x", x.as_slice())]).into_iter().next().unwrap();
let w = rlx_gguf::dequant_q8_0(&packed, n * k).unwrap();
let mut expect = vec![0f32; m * n];
for i in 0..m {
for j in 0..n {
let mut acc = 0f32;
for c2 in 0..k {
acc += x[i * k + c2] * w[j * k + c2];
}
expect[i * n + j] = acc;
}
}
let err = got
.iter()
.zip(&expect)
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(err < 1e-3, "max_err={err} got={got:?} expect={expect:?}");
}
}