1use crate::flat::{self, FlatView};
17use crate::manifest::{Manifest, SidecarRef};
18use crate::placement::Placement;
19use crate::store_zip::{self, MemberRange};
20use crate::tier::{
21 Codec, StorageTier, decode_payload, decode_payload_parallel, decode_zstd_block_at,
22};
23use crate::weights_index::{WeightEntry, WeightsIndex};
24use anyhow::{Context, Result, bail};
25use memmap2::Mmap;
26use rlx_ir::{Graph, Op};
27use std::collections::{BTreeMap, HashMap};
28use std::fs::File;
29use std::path::{Path, PathBuf};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum MaterializeMode {
37 #[default]
39 HotOnly,
40 All,
42 None,
44}
45
46#[derive(Debug)]
48pub enum MemberSource {
49 Flat { path: PathBuf, map: Mmap },
51 Dir {
53 root: PathBuf,
54 shard_maps: BTreeMap<String, Mmap>,
55 },
56 Zip {
58 path: PathBuf,
59 map: Mmap,
60 ranges: BTreeMap<String, MemberRange>,
61 },
62}
63
64pub struct Package {
66 source: MemberSource,
67 manifest: Manifest,
68 weights: Option<WeightsIndex>,
69 name_index: HashMap<String, usize>,
70 placement: Option<Placement>,
71 flat_data_start: u64,
73 flat_graph: Option<(u64, u64)>,
74 flat_sidecars: Vec<(String, u64, u64, Codec)>,
76}
77
78impl Package {
79 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
81 let path = path.as_ref();
82 if path.is_dir() {
83 return Self::open_dir(path);
84 }
85 let file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
86 let map =
88 unsafe { Mmap::map(&file) }.with_context(|| format!("mmap {}", path.display()))?;
89 if flat::is_flat_magic(&map) {
90 Self::open_flat(path, map)
91 } else if map.len() >= 4 && &map[..2] == b"PK" {
92 Self::open_zip_mapped(path, map)
93 } else {
94 bail!(
95 "{} is not a flat RLXPFLAT, ZIP, or package directory",
96 path.display()
97 )
98 }
99 }
100
101 fn finish(
102 source: MemberSource,
103 manifest: Manifest,
104 weights: Option<WeightsIndex>,
105 placement: Option<Placement>,
106 flat_data_start: u64,
107 flat_graph: Option<(u64, u64)>,
108 flat_sidecars: Vec<(String, u64, u64, Codec)>,
109 ) -> Self {
110 let name_index = weights
111 .as_ref()
112 .map(|idx| {
113 idx.tensors
114 .iter()
115 .enumerate()
116 .map(|(i, t)| (t.name.clone(), i))
117 .collect()
118 })
119 .unwrap_or_default();
120 Self {
121 source,
122 manifest,
123 weights,
124 name_index,
125 placement,
126 flat_data_start,
127 flat_graph,
128 flat_sidecars,
129 }
130 }
131
132 fn open_flat(path: &Path, map: Mmap) -> Result<Self> {
133 let view = FlatView::parse(&map)?;
134 let weights = Some(view.weights_index());
135 let placement = view.placement()?;
136 let flat_sidecars = view
137 .toc
138 .sidecars
139 .iter()
140 .map(|s| (s.id.clone(), s.offset, s.length, s.codec))
141 .collect();
142 let flat_graph = Some((view.toc.graph_offset, view.toc.graph_length));
143 let flat_data_start = view.data_start;
144 let manifest = view.toc.manifest.clone();
145 Ok(Self::finish(
146 MemberSource::Flat {
147 path: path.to_path_buf(),
148 map,
149 },
150 manifest,
151 weights,
152 placement,
153 flat_data_start,
154 flat_graph,
155 flat_sidecars,
156 ))
157 }
158
159 fn open_dir(dir: &Path) -> Result<Self> {
160 let manifest_path = dir.join("rlx.json");
161 let bytes = std::fs::read(&manifest_path)
162 .with_context(|| format!("reading {}", manifest_path.display()))?;
163 let manifest: Manifest = serde_json::from_slice(&bytes).context("parsing rlx.json")?;
164 manifest.validate()?;
165 let weights = load_weights_index_dir(dir, &manifest)?;
166 let placement = load_placement_dir(dir, &manifest)?;
167 let mut shard_maps = BTreeMap::new();
168 if let Some(idx) = &weights {
169 for t in &idx.tensors {
170 if shard_maps.contains_key(&t.shard) {
171 continue;
172 }
173 let p = dir.join(&t.shard);
174 let f = File::open(&p).with_context(|| format!("open shard {}", p.display()))?;
175 let map = unsafe { Mmap::map(&f) }
177 .with_context(|| format!("mmap shard {}", p.display()))?;
178 shard_maps.insert(t.shard.clone(), map);
179 }
180 }
181 Ok(Self::finish(
182 MemberSource::Dir {
183 root: dir.to_path_buf(),
184 shard_maps,
185 },
186 manifest,
187 weights,
188 placement,
189 0,
190 None,
191 Vec::new(),
192 ))
193 }
194
195 fn open_zip_mapped(path: &Path, map: Mmap) -> Result<Self> {
196 let ranges = store_zip::resolve_store_ranges(path)?;
197 let manifest_bytes = store_zip::read_member_from_map(&map, &ranges, "rlx.json")
198 .context("rlx.json in package")?;
199 let manifest: Manifest =
200 serde_json::from_slice(manifest_bytes).context("parsing rlx.json")?;
201 manifest.validate()?;
202 let weights = load_weights_index_zip(&map, &ranges, &manifest)?;
203 let placement = load_placement_zip(&map, &ranges, &manifest)?;
204 Ok(Self::finish(
205 MemberSource::Zip {
206 path: path.to_path_buf(),
207 map,
208 ranges,
209 },
210 manifest,
211 weights,
212 placement,
213 0,
214 None,
215 Vec::new(),
216 ))
217 }
218
219 pub fn manifest(&self) -> &Manifest {
220 &self.manifest
221 }
222
223 pub fn weights_index(&self) -> Option<&WeightsIndex> {
224 self.weights.as_ref()
225 }
226
227 pub fn placement(&self) -> Option<&Placement> {
228 self.placement.as_ref()
229 }
230
231 pub fn has_graph(&self) -> bool {
233 self.manifest.graph.encoding != "none"
234 }
235
236 pub fn graph(&self) -> Result<Graph> {
239 self.graph_with(MaterializeMode::HotOnly)
240 }
241
242 pub fn graph_with(&self, mode: MaterializeMode) -> Result<Graph> {
244 if !self.has_graph() {
245 bail!("package has no embedded graph (weight-only / encoding=none)");
246 }
247 let mut g: Graph = match &self.source {
248 MemberSource::Flat { map, .. } => {
249 let (off, len) = self.flat_graph.context("flat graph missing")?;
250 if len == 0 {
251 bail!("flat graph length is 0");
252 }
253 let start = (self.flat_data_start + off) as usize;
254 let end = start
255 .checked_add(len as usize)
256 .context("flat graph range")?;
257 let bytes = map.get(start..end).context("flat graph slice")?;
258 bincode::deserialize(bytes).context("deserialize flat graph")?
259 }
260 MemberSource::Dir { .. } | MemberSource::Zip { .. } => {
261 let bytes = self.member_bytes(&self.manifest.graph.path)?;
262 bincode::deserialize(&bytes).context("deserialize graph/mir.bin")?
263 }
264 };
265 self.materialize_graph_weights(&mut g, mode)?;
266 Ok(g)
267 }
268
269 fn materialize_graph_weights(&self, g: &mut Graph, mode: MaterializeMode) -> Result<()> {
270 if matches!(mode, MaterializeMode::None) {
271 return Ok(());
272 }
273 let Some(idx) = &self.weights else {
274 return Ok(());
275 };
276 for entry in &idx.tensors {
277 let bind = match mode {
278 MaterializeMode::All => true,
279 MaterializeMode::HotOnly => entry.is_mmapable(),
280 MaterializeMode::None => false,
281 };
282 if !bind {
283 continue;
284 }
285 let bytes = if entry.is_mmapable() {
286 self.tensor_mmap(&entry.name)?.to_vec()
287 } else {
288 self.tensor_bytes(&entry.name)?
289 };
290 fill_constant(g, &entry.name, bytes);
291 }
292 Ok(())
293 }
294
295 pub fn bind_tensor(&self, g: &mut Graph, name: &str) -> Result<()> {
297 let bytes = self.tensor_bytes(name)?;
298 fill_constant(g, name, bytes);
299 Ok(())
300 }
301
302 pub fn bind_warm_and_cold(&self, g: &mut Graph) -> Result<()> {
304 let Some(idx) = &self.weights else {
305 return Ok(());
306 };
307 for entry in &idx.tensors {
308 if entry.is_mmapable() {
309 continue;
310 }
311 self.bind_tensor(g, &entry.name)?;
312 }
313 Ok(())
314 }
315
316 pub fn advise_hot_willneed(&self) {
318 let Some(idx) = &self.weights else {
319 return;
320 };
321 for entry in &idx.tensors {
322 if !entry.is_mmapable() {
323 continue;
324 }
325 self.advise_range(entry.shard.as_str(), entry.offset, entry.length);
326 }
327 }
328
329 pub fn prefetch_tensor(&self, name: &str) -> Result<()> {
331 let entry = self.weight_entry(name)?;
332 self.advise_range(entry.shard.as_str(), entry.offset, entry.length);
333 Ok(())
334 }
335
336 fn advise_range(&self, shard: &str, offset: u64, length: u64) {
337 #[cfg(unix)]
339 {
340 let len = length as usize;
341 if len == 0 {
342 return;
343 }
344 match &self.source {
345 MemberSource::Flat { map, .. } => {
346 let abs = (self.flat_data_start + offset) as usize;
347 let _ = map.advise_range(memmap2::Advice::WillNeed, abs, len);
348 }
349 MemberSource::Dir { shard_maps, .. } => {
350 if let Some(map) = shard_maps.get(shard) {
351 let _ = map.advise_range(memmap2::Advice::WillNeed, offset as usize, len);
352 }
353 }
354 MemberSource::Zip { map, ranges, .. } => {
355 if let Some(r) = ranges.get(shard) {
356 let abs = r.data_offset as usize + offset as usize;
357 let _ = map.advise_range(memmap2::Advice::WillNeed, abs, len);
358 }
359 }
360 }
361 }
362 #[cfg(not(unix))]
363 {
364 let _ = (shard, offset, length);
365 }
366 }
367
368 pub fn sidecar(&self, id: &str) -> Result<Vec<u8>> {
370 if let MemberSource::Flat { map, .. } = &self.source {
371 let (off, len, codec) = self
372 .flat_sidecars
373 .iter()
374 .find(|(i, _, _, _)| i == id)
375 .map(|(_, o, l, c)| (*o, *l, *c))
376 .with_context(|| format!("sidecar id {id} not in flat TOC"))?;
377 let start = (self.flat_data_start + off) as usize;
378 let end = start.checked_add(len as usize).context("sidecar range")?;
379 let stored = map.get(start..end).context("sidecar slice")?;
380 return decode_payload(codec, stored);
381 }
382 let sc = self
383 .manifest
384 .sidecars
385 .iter()
386 .find(|s: &&SidecarRef| s.id == id)
387 .with_context(|| format!("sidecar id {id} not in manifest"))?;
388 let stored = self.member_bytes(&sc.path)?;
389 decode_payload(sc.codec, &stored)
390 }
391
392 pub fn weight_entry(&self, name: &str) -> Result<&WeightEntry> {
393 let idx = self
394 .weights
395 .as_ref()
396 .context("package has no weights index")?;
397 let i = self
398 .name_index
399 .get(name)
400 .copied()
401 .with_context(|| format!("weight tensor {name} not in index"))?;
402 Ok(&idx.tensors[i])
403 }
404
405 pub fn tensor_bytes(&self, name: &str) -> Result<Vec<u8>> {
408 let entry = self.weight_entry(name)?;
409 let stored = self.shard_slice(&entry.shard, entry.offset, entry.length)?;
410 if entry.codec == Codec::ZstdBlocks && stored.len() > (1 << 20) {
411 decode_payload_parallel(entry.codec, stored)
412 } else {
413 decode_payload(entry.codec, stored)
414 }
415 }
416
417 pub fn tensor_mmap(&self, name: &str) -> Result<&[u8]> {
419 let entry = self.weight_entry(name)?;
420 if !entry.is_mmapable() {
421 bail!(
422 "tensor {name} is {:?} / {:?}; use tensor_bytes (or tensor_warm_block)",
423 entry.tier,
424 entry.codec
425 );
426 }
427 self.shard_slice(&entry.shard, entry.offset, entry.length)
428 }
429
430 pub fn tensor_warm_block(&self, name: &str, block_index: usize) -> Result<Vec<u8>> {
432 let entry = self.weight_entry(name)?;
433 if entry.codec != Codec::ZstdBlocks {
434 bail!("tensor {name} is not zstd_blocks (codec={:?})", entry.codec);
435 }
436 let stored = self.shard_slice(&entry.shard, entry.offset, entry.length)?;
437 decode_zstd_block_at(stored, block_index)
438 }
439
440 pub fn tensor_f32(&self, name: &str) -> Result<Vec<f32>> {
441 let entry = self.weight_entry(name)?;
442 if entry.scheme != "f32" {
443 bail!(
444 "tensor {name} scheme {:?} is not f32; use tensor_bytes for packed weights",
445 entry.scheme
446 );
447 }
448 let raw = self.tensor_bytes(name)?;
449 if raw.len() % 4 != 0 {
450 bail!("f32 tensor {name} length {} not multiple of 4", raw.len());
451 }
452 Ok(raw
453 .as_chunks::<4>()
454 .0
455 .iter()
456 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
457 .collect())
458 }
459
460 pub fn tensors_for_rank(&self, rank: u32) -> Vec<&WeightEntry> {
461 let Some(idx) = &self.weights else {
462 return Vec::new();
463 };
464 idx.tensors
465 .iter()
466 .filter(|t| t.rank.is_none() || t.rank == Some(rank))
467 .collect()
468 }
469
470 pub fn tensors_by_tier(&self, tier: StorageTier) -> Vec<&WeightEntry> {
471 let Some(idx) = &self.weights else {
472 return Vec::new();
473 };
474 idx.tensors.iter().filter(|t| t.tier == tier).collect()
475 }
476
477 pub fn rank_root(&self, rank: u32) -> Option<String> {
478 let rel = format!("dist/rank-{rank}");
479 if self.member_exists(&rel) || self.member_exists(&format!("{rel}/weights/index.json")) {
480 Some(rel)
481 } else {
482 None
483 }
484 }
485
486 pub fn member_bytes(&self, rel: &str) -> Result<Vec<u8>> {
487 match &self.source {
488 MemberSource::Flat { .. } => {
489 bail!("flat packages have no path members ({rel}); use graph()/sidecar()/tensor_*")
490 }
491 MemberSource::Dir { root, .. } => {
492 let p = root.join(rel);
493 std::fs::read(&p).with_context(|| format!("reading {}", p.display()))
494 }
495 MemberSource::Zip { map, ranges, .. } => {
496 Ok(store_zip::read_member_from_map(map, ranges, rel)?.to_vec())
497 }
498 }
499 }
500
501 fn member_exists(&self, rel: &str) -> bool {
502 match &self.source {
503 MemberSource::Flat { .. } => false,
504 MemberSource::Dir { root, .. } => root.join(rel).exists(),
505 MemberSource::Zip { ranges, .. } => {
506 ranges.contains_key(rel) || ranges.keys().any(|k| k.starts_with(&format!("{rel}/")))
507 }
508 }
509 }
510
511 fn shard_slice(&self, shard: &str, offset: u64, length: u64) -> Result<&[u8]> {
512 let start = offset as usize;
513 let end = start
514 .checked_add(length as usize)
515 .context("shard slice overflow")?;
516 match &self.source {
517 MemberSource::Flat { map, .. } => {
518 let abs = (self.flat_data_start + offset) as usize;
519 let abs_end = abs
520 .checked_add(length as usize)
521 .context("flat shard overflow")?;
522 map.get(abs..abs_end)
523 .with_context(|| format!("flat slice [{offset}..+{length}]"))
524 }
525 MemberSource::Dir { shard_maps, .. } => {
526 let map = shard_maps
527 .get(shard)
528 .with_context(|| format!("shard {shard} not mapped"))?;
529 map.get(start..end)
530 .with_context(|| format!("slice {shard}[{offset}..+{length}]"))
531 }
532 MemberSource::Zip { map, ranges, .. } => {
533 let base = store_zip::read_member_from_map(map, ranges, shard)?;
534 base.get(start..end)
535 .with_context(|| format!("slice {shard}[{offset}..+{length}]"))
536 }
537 }
538 }
539}
540
541fn fill_constant(g: &mut Graph, name: &str, bytes: Vec<u8>) {
542 for n in g.nodes_mut() {
543 if n.name.as_deref() != Some(name) {
544 continue;
545 }
546 if let Op::Constant { data } = &mut n.op {
547 if data.is_empty() {
548 *data = bytes.clone();
549 }
550 }
551 }
552}
553
554pub fn dtype_for_weight_scheme(scheme: &str) -> rlx_ir::DType {
556 use rlx_ir::DType;
557 if scheme == "f32" || scheme.starts_with("f32") {
558 return DType::F32;
559 }
560 if scheme == "f16" || scheme == "bf16" {
561 return DType::F16;
562 }
563 if scheme == "u8" || scheme.starts_with("mlx_") || scheme.starts_with("gguf_") || scheme == "i8"
564 {
565 return DType::U8;
566 }
567 DType::F32
568}
569
570impl Package {
571 pub fn typed_weight_bindings(&self) -> Result<Vec<(String, Vec<u8>, rlx_ir::DType)>> {
574 let Some(idx) = &self.weights else {
575 return Ok(Vec::new());
576 };
577 let mut out = Vec::with_capacity(idx.tensors.len());
578 for entry in &idx.tensors {
579 let bytes = self.tensor_bytes(&entry.name)?;
580 out.push((
581 entry.name.clone(),
582 bytes,
583 dtype_for_weight_scheme(&entry.scheme),
584 ));
585 }
586 Ok(out)
587 }
588}
589
590fn load_weights_index_dir(dir: &Path, manifest: &Manifest) -> Result<Option<WeightsIndex>> {
591 let Some(wref) = &manifest.weights else {
592 return Ok(None);
593 };
594 let p = dir.join(&wref.index);
595 let bytes = std::fs::read(&p).with_context(|| format!("reading {}", p.display()))?;
596 let idx: WeightsIndex = serde_json::from_slice(&bytes).context("parsing weights/index.json")?;
597 Ok(Some(idx))
598}
599
600fn load_weights_index_zip(
601 map: &[u8],
602 ranges: &BTreeMap<String, MemberRange>,
603 manifest: &Manifest,
604) -> Result<Option<WeightsIndex>> {
605 let Some(wref) = &manifest.weights else {
606 return Ok(None);
607 };
608 let bytes = store_zip::read_member_from_map(map, ranges, &wref.index)?;
609 let idx: WeightsIndex = serde_json::from_slice(bytes).context("parsing weights/index.json")?;
610 Ok(Some(idx))
611}
612
613fn load_placement_dir(dir: &Path, manifest: &Manifest) -> Result<Option<Placement>> {
614 let Some(dist) = &manifest.dist else {
615 return Ok(None);
616 };
617 let Some(path) = &dist.placement else {
618 return Ok(None);
619 };
620 let p = dir.join(path);
621 if !p.exists() {
622 return Ok(None);
623 }
624 let bytes = std::fs::read(&p).with_context(|| format!("reading {}", p.display()))?;
625 let pl: Placement = serde_json::from_slice(&bytes).context("parsing placement.json")?;
626 Ok(Some(pl))
627}
628
629fn load_placement_zip(
630 map: &[u8],
631 ranges: &BTreeMap<String, MemberRange>,
632 manifest: &Manifest,
633) -> Result<Option<Placement>> {
634 let Some(dist) = &manifest.dist else {
635 return Ok(None);
636 };
637 let Some(path) = &dist.placement else {
638 return Ok(None);
639 };
640 if !ranges.contains_key(path) {
641 return Ok(None);
642 }
643 let bytes = store_zip::read_member_from_map(map, ranges, path)?;
644 let pl: Placement = serde_json::from_slice(bytes).context("parsing placement.json")?;
645 Ok(Some(pl))
646}