1use crate::compiled::CompiledGraph;
46use crate::{Device, Session};
47use rlx_driver::ProcessGroup;
48use rlx_ir::Graph;
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51
52const TAG_SPEC: u32 = 40;
55const TAG_ACT: u32 = 41;
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
74#[derive(Serialize, Deserialize, Clone, Debug)]
77pub struct StageSpec {
78 pub graph: Graph,
79 pub input: String,
81 pub output: String,
83 pub weights: Vec<WeightRef>,
84 pub device: String,
87}
88
89impl StageSpec {
90 pub fn new(
92 graph: Graph,
93 input: impl Into<String>,
94 output: impl Into<String>,
95 device: impl Into<String>,
96 ) -> Self {
97 Self {
98 graph,
99 input: input.into(),
100 output: output.into(),
101 weights: Vec::new(),
102 device: device.into(),
103 }
104 }
105 pub fn weight(mut self, name: impl Into<String>, uri: impl Into<String>) -> Self {
107 self.weights.push(WeightRef {
108 name: name.into(),
109 uri: uri.into(),
110 packed: false,
111 });
112 self
113 }
114 pub fn weight_packed(mut self, name: impl Into<String>, uri: impl Into<String>) -> Self {
117 self.weights.push(WeightRef {
118 name: name.into(),
119 uri: uri.into(),
120 packed: true,
121 });
122 self
123 }
124}
125
126pub fn ship_stage(group: &ProcessGroup, rank: u32, spec: &StageSpec) -> Result<(), String> {
128 let bytes = serde_json::to_vec(spec).map_err(|e| e.to_string())?;
129 group
130 .transport()
131 .send_bytes(rank, TAG_SPEC, &bytes)
132 .map_err(|e| format!("ship_stage: {e}"))
133}
134
135pub fn send_activation(group: &ProcessGroup, to: u32, data: &[f32]) -> Result<(), String> {
137 group.send_f32(to, TAG_ACT, data).map_err(|e| e.to_string())
138}
139
140pub fn recv_activation(group: &ProcessGroup, from: u32) -> Result<Vec<f32>, String> {
142 group.recv_f32(from, TAG_ACT).map_err(|e| e.to_string())
143}
144
145pub struct WorkerStage {
147 pub device: Device,
149 pub nodes: usize,
151 input: String,
152 compiled: CompiledGraph,
153}
154
155impl WorkerStage {
156 pub fn run(&mut self, x: &[f32]) -> Vec<f32> {
158 self.compiled
159 .run(&[(self.input.as_str(), x)])
160 .into_iter()
161 .next()
162 .unwrap_or_default()
163 }
164 pub fn input_name(&self) -> &str {
165 &self.input
166 }
167}
168
169fn resolve_device(spec: &str) -> Device {
170 if spec.eq_ignore_ascii_case("auto") || spec.is_empty() {
171 return crate::fastest_device();
172 }
173 match crate::parse_device(spec) {
174 Ok(d) if crate::is_available(d) => d,
175 _ => crate::fastest_device(),
176 }
177}
178
179pub fn recv_stage<F>(group: &ProcessGroup, mut fallback: F) -> Result<WorkerStage, String>
187where
188 F: FnMut(&str) -> Vec<f32>,
189{
190 let bytes = group
191 .transport()
192 .recv_bytes(0, TAG_SPEC)
193 .map_err(|e| format!("recv_stage: {e}"))?;
194 let spec: StageSpec = serde_json::from_slice(&bytes).map_err(|e| format!("StageSpec: {e}"))?;
195 let device = resolve_device(&spec.device);
196 let nodes = spec.graph.len();
197 let mut compiled = Session::new(device).compile(spec.graph);
198 let mut cache = WeightCache::new();
201 for w in &spec.weights {
202 if w.packed {
203 let bytes = cache
206 .bytes(&w.uri)
207 .map_err(|e| format!("weight {}: {e}", w.name))?;
208 compiled.set_param_typed(&w.name, &bytes, rlx_ir::DType::U8);
209 } else {
210 let vals = cache.f32(&w.uri).unwrap_or_else(|_| fallback(&w.uri));
213 compiled.set_param(&w.name, &vals);
214 }
215 }
216 Ok(WorkerStage {
217 device,
218 nodes,
219 input: spec.input,
220 compiled,
221 })
222}
223
224pub fn serve_stage<F>(group: &ProcessGroup, resolve: F) -> Result<Device, String>
230where
231 F: FnMut(&str) -> Vec<f32>,
232{
233 let n = group.world_size();
234 let rank = group.rank();
235 let mut stage = recv_stage(group, resolve)?;
236 let input = recv_activation(group, rank - 1)?;
237 let out = stage.run(&input);
238 let next = if rank + 1 < n { rank + 1 } else { 0 };
239 send_activation(group, next, &out)?;
240 Ok(stage.device)
241}
242
243pub fn serve_stage_uri(group: &ProcessGroup) -> Result<Device, String> {
247 serve_stage(group, |uri| {
248 eprintln!("rlx dist: no resolver for weight URI [{uri}]");
249 Vec::new()
250 })
251}
252
253fn split_frag(rest: &str) -> Result<(&str, &str), String> {
260 rest.split_once('#')
261 .ok_or_else(|| format!("weight URI needs a #<tensor> fragment: {rest}"))
262}
263
264pub fn resolve_weight_uri(uri: &str) -> Result<Vec<f32>, String> {
272 if let Some(rest) = uri.strip_prefix("gguf://") {
273 let (path, tensor) = split_frag(rest)?;
274 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
275 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
276 let (data, _dims) = gguf.dequant_f32(tensor).map_err(|e| e.to_string())?;
277 Ok(data)
278 } else if let Some(rest) = uri.strip_prefix("safetensors://") {
279 let (path, tensor) = split_frag(rest)?;
280 let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
281 read_safetensors_f32(&bytes, tensor)
282 } else if let Some(path) = uri.strip_prefix("file://") {
283 let b = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
284 Ok(b.chunks_exact(4)
285 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
286 .collect())
287 } else {
288 Err(format!("unsupported weight URI scheme: {uri}"))
289 }
290}
291
292pub fn resolve_weight_bytes(uri: &str) -> Result<Vec<u8>, String> {
297 if let Some(rest) = uri.strip_prefix("gguf://") {
298 let (path, tensor) = split_frag(rest)?;
299 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
300 let gguf = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
301 let t = gguf
302 .get(tensor)
303 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
304 Ok(gguf.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
305 } else if let Some(path) = uri.strip_prefix("file://") {
306 std::fs::read(path).map_err(|e| format!("read {path}: {e}"))
307 } else {
308 Err(format!("packed load unsupported for scheme: {uri}"))
309 }
310}
311
312#[derive(Default)]
317pub struct WeightCache {
318 gguf: HashMap<String, rlx_gguf::GgufFile>,
319}
320
321impl WeightCache {
322 pub fn new() -> Self {
323 Self::default()
324 }
325
326 fn gguf_file(&mut self, path: &str) -> Result<&rlx_gguf::GgufFile, String> {
327 if !self.gguf.contains_key(path) {
328 let mut f = std::fs::File::open(path).map_err(|e| format!("open {path}: {e}"))?;
329 let g = rlx_gguf::GgufFile::from_reader(&mut f).map_err(|e| e.to_string())?;
330 self.gguf.insert(path.to_string(), g);
331 }
332 Ok(&self.gguf[path])
333 }
334
335 pub fn f32(&mut self, uri: &str) -> Result<Vec<f32>, String> {
337 if let Some(rest) = uri.strip_prefix("gguf://") {
338 let (path, tensor) = split_frag(rest)?;
339 let (data, _dims) = self
340 .gguf_file(path)?
341 .dequant_f32(tensor)
342 .map_err(|e| e.to_string())?;
343 Ok(data)
344 } else if uri.starts_with("safetensors://") || uri.starts_with("file://") {
345 resolve_weight_uri(uri) } else {
347 Err(format!("unsupported weight URI scheme: {uri}"))
348 }
349 }
350
351 pub fn bytes(&mut self, uri: &str) -> Result<Vec<u8>, String> {
353 if let Some(rest) = uri.strip_prefix("gguf://") {
354 let (path, tensor) = split_frag(rest)?;
355 let g = self.gguf_file(path)?;
356 let t = g
357 .get(tensor)
358 .ok_or_else(|| format!("tensor not found: {tensor}"))?;
359 Ok(g.tensor_bytes(t).map_err(|e| e.to_string())?.to_vec())
360 } else {
361 resolve_weight_bytes(uri)
362 }
363 }
364}
365
366pub fn uri_resolver(uri: &str) -> Vec<f32> {
370 match resolve_weight_uri(uri) {
371 Ok(v) => v,
372 Err(e) => {
373 eprintln!("rlx dist: weight resolve failed [{uri}]: {e}");
374 Vec::new()
375 }
376 }
377}
378
379fn bf16_to_f32(b: u16) -> f32 {
381 f32::from_bits((b as u32) << 16)
382}
383
384fn f16_to_f32(h: u16) -> f32 {
386 let sign = (h >> 15) & 1;
387 let exp = (h >> 10) & 0x1f;
388 let mant = h & 0x3ff;
389 let v = match exp {
390 0 => (mant as f32) * 2f32.powi(-24), 0x1f if mant == 0 => f32::INFINITY,
392 0x1f => f32::NAN,
393 _ => (1.0 + mant as f32 / 1024.0) * 2f32.powi(exp as i32 - 15),
394 };
395 if sign == 1 { -v } else { v }
396}
397
398fn read_safetensors_f32(buf: &[u8], name: &str) -> Result<Vec<f32>, String> {
401 if buf.len() < 8 {
402 return Err("safetensors file too small".into());
403 }
404 let hlen = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
405 let header = buf
406 .get(8..8 + hlen)
407 .ok_or("safetensors: truncated header")?;
408 let data = &buf[8 + hlen..];
409 let v: serde_json::Value = serde_json::from_slice(header).map_err(|e| e.to_string())?;
410 let t = v
411 .get(name)
412 .ok_or_else(|| format!("tensor not found: {name}"))?;
413 let dtype = t.get("dtype").and_then(|d| d.as_str()).ok_or("no dtype")?;
414 let off = t
415 .get("data_offsets")
416 .and_then(|o| o.as_array())
417 .ok_or("no data_offsets")?;
418 let start = off[0].as_u64().ok_or("bad data_offsets")? as usize;
419 let end = off[1].as_u64().ok_or("bad data_offsets")? as usize;
420 let raw = data
421 .get(start..end)
422 .ok_or("safetensors: data_offsets out of range")?;
423 Ok(match dtype {
424 "F32" => raw
425 .chunks_exact(4)
426 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
427 .collect(),
428 "F16" => raw
429 .chunks_exact(2)
430 .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
431 .collect(),
432 "BF16" => raw
433 .chunks_exact(2)
434 .map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
435 .collect(),
436 other => return Err(format!("unsupported safetensors dtype: {other}")),
437 })
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn resolve_safetensors_f32() {
446 let vals = [1.0f32, 2.0, -3.5, 4.25];
447 let data: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
448 let header = format!(
449 r#"{{"w":{{"dtype":"F32","shape":[4],"data_offsets":[0,{}]}}}}"#,
450 data.len()
451 );
452 let mut buf = (header.len() as u64).to_le_bytes().to_vec();
453 buf.extend_from_slice(header.as_bytes());
454 buf.extend_from_slice(&data);
455 let path =
456 std::env::temp_dir().join(format!("rlx_dist_{}.safetensors", std::process::id()));
457 std::fs::write(&path, &buf).unwrap();
458 let got = resolve_weight_uri(&format!("safetensors://{}#w", path.display())).unwrap();
459 std::fs::remove_file(&path).ok();
460 assert_eq!(got, vals);
461 }
462
463 #[test]
464 fn resolve_gguf_f32() {
465 use rlx_gguf::{GgmlType, GgufWriter};
466 let vals = [1.0f32, 2.0, -3.5, 4.25];
467 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
468 let mut w = GgufWriter::new();
469 w.add_tensor_bytes("w", vec![4], GgmlType::F32, bytes)
470 .unwrap();
471 let path = std::env::temp_dir().join(format!("rlx_dist_{}.gguf", std::process::id()));
472 w.write_to_path(&path).unwrap();
473 let got = resolve_weight_uri(&format!("gguf://{}#w", path.display())).unwrap();
474 std::fs::remove_file(&path).ok();
475 assert_eq!(got, vals);
476 }
477
478 #[test]
479 fn weight_cache_serves_many_tensors_from_one_file() {
480 use rlx_gguf::{GgmlType, GgufWriter};
481 let a = [1.0f32, 2.0, 3.0, 4.0];
482 let b = [10.0f32, 20.0];
483 let mut w = GgufWriter::new();
484 w.add_tensor_bytes(
485 "a",
486 vec![4],
487 GgmlType::F32,
488 a.iter().flat_map(|v| v.to_le_bytes()).collect(),
489 )
490 .unwrap();
491 w.add_tensor_bytes(
492 "b",
493 vec![2],
494 GgmlType::F32,
495 b.iter().flat_map(|v| v.to_le_bytes()).collect(),
496 )
497 .unwrap();
498 let path = std::env::temp_dir().join(format!("rlx_cache_{}.gguf", std::process::id()));
499 w.write_to_path(&path).unwrap();
500 let p = path.display();
501
502 let mut cache = WeightCache::new();
504 assert_eq!(cache.f32(&format!("gguf://{p}#a")).unwrap(), a);
505 assert_eq!(cache.f32(&format!("gguf://{p}#b")).unwrap(), b);
506 let raw = cache.bytes(&format!("gguf://{p}#a")).unwrap();
508 let raw_f32: Vec<f32> = raw
509 .chunks_exact(4)
510 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
511 .collect();
512 assert_eq!(raw_f32, a);
513 std::fs::remove_file(&path).ok();
514 }
515
516 #[test]
517 fn dequant_matmul_gguf_end_to_end() {
518 use rlx_ir::quant::QuantScheme;
522 use rlx_ir::{DType, Graph, Shape};
523
524 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();
526 let packed = rlx_gguf::quantize(&w_floats, rlx_gguf::GgmlType::Q8_0).unwrap();
527 let x: Vec<f32> = (0..m * k).map(|i| ((i % 13) as f32) * 0.05).collect();
528
529 let mut g = Graph::new("dq");
530 let xin = g.input("x", Shape::new(&[m, k], DType::F32));
531 let wp = g.param("W", Shape::new(&[packed.len()], DType::U8));
532 let out = g.dequant_matmul_packed(
533 xin,
534 wp,
535 QuantScheme::GgufQ8_0,
536 Shape::new(&[m, n], DType::F32),
537 );
538 g.set_outputs(vec![out]);
539
540 let mut c = Session::new(Device::Cpu).compile(g);
541 c.set_param_typed("W", &packed, DType::U8);
542 let got = c.run(&[("x", x.as_slice())]).into_iter().next().unwrap();
543
544 let w = rlx_gguf::dequant_q8_0(&packed, n * k).unwrap();
546 let mut expect = vec![0f32; m * n];
547 for i in 0..m {
548 for j in 0..n {
549 let mut acc = 0f32;
550 for c2 in 0..k {
551 acc += x[i * k + c2] * w[j * k + c2];
552 }
553 expect[i * n + j] = acc;
554 }
555 }
556 let err = got
557 .iter()
558 .zip(&expect)
559 .map(|(a, b)| (a - b).abs())
560 .fold(0f32, f32::max);
561 assert!(err < 1e-3, "max_err={err} got={got:?} expect={expect:?}");
562 }
563}