Skip to main content

rlx_pkg/
package.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Open and query an `.rlxp` flat / directory / ZIP package.
6//!
7//! # Load path
8//!
9//! 1. [`Package::open`] — mmap file / shards, parse TOC (JSON or bincode)
10//! 2. [`Package::advise_hot_willneed`] — optional `madvise(WILLNEED)` on hot ranges
11//! 3. [`Package::graph`] — deserialize MIR; fill **hot** stripped Constants
12//! 4. [`Package::bind_warm_and_cold`] — optional late bind of compressed weights
13//!
14//! Weight-only packs (`graph.encoding = "none"`) refuse [`Package::graph`].
15
16use 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/// Which weight tiers to bind into graph Constants on [`Package::graph`].
32///
33/// Default [`MaterializeMode::HotOnly`] keeps warm/cold out of the compile path
34/// until [`Package::bind_tensor`] / [`Package::bind_warm_and_cold`].
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum MaterializeMode {
37    /// Hot / mmapable tensors only (default).
38    #[default]
39    HotOnly,
40    /// Inflate every tier into Constant payloads.
41    All,
42    /// Leave all weight Constants empty (caller binds later).
43    None,
44}
45
46/// Where package members are served from.
47#[derive(Debug)]
48pub enum MemberSource {
49    /// Flat `RLXPFLAT` single-file mmap.
50    Flat { path: PathBuf, map: Mmap },
51    /// Unpacked directory tree with per-shard mmaps.
52    Dir {
53        root: PathBuf,
54        shard_maps: BTreeMap<String, Mmap>,
55    },
56    /// ZIP64 STORE archive.
57    Zip {
58        path: PathBuf,
59        map: Mmap,
60        ranges: BTreeMap<String, MemberRange>,
61    },
62}
63
64/// Opened RLX package.
65pub struct Package {
66    source: MemberSource,
67    manifest: Manifest,
68    weights: Option<WeightsIndex>,
69    name_index: HashMap<String, usize>,
70    placement: Option<Placement>,
71    /// Absolute file offset of the flat data region (Flat only).
72    flat_data_start: u64,
73    flat_graph: Option<(u64, u64)>,
74    /// id, offset, length, codec
75    flat_sidecars: Vec<(String, u64, u64, Codec)>,
76}
77
78impl Package {
79    /// Open a directory, flat `.rlxp`, or ZIP archive.
80    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        // SAFETY: read-only map of a local package file for the Package lifetime.
87        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                // SAFETY: read-only map of a package shard for the Package lifetime.
176                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    /// True when the package embeds a MIR graph (`encoding != "none"`).
232    pub fn has_graph(&self) -> bool {
233        self.manifest.graph.encoding != "none"
234    }
235
236    /// Deserialize the MIR graph and fill stripped weight Constants from the
237    /// package (hot tier only by default — see [`MaterializeMode`]).
238    pub fn graph(&self) -> Result<Graph> {
239        self.graph_with(MaterializeMode::HotOnly)
240    }
241
242    /// Like [`Self::graph`] with an explicit materialize mode.
243    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    /// Bind one tensor (any tier) into matching empty Constants.
296    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    /// Inflate all non-hot tensors into the graph (after [`MaterializeMode::HotOnly`]).
303    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    /// Hint the OS to prefetch hot weight pages (`madvise` WILLNEED). Best-effort.
317    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    /// Prefetch a single named tensor's stored range.
330    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        // memmap2::Advice / advise_range are Unix-only; no-op on Windows.
338        #[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    /// Sidecar bytes by id (decompresses cold sidecars).
369    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    /// Owned tensor bytes (decompresses warm/cold; copies hot). Uses parallel
406    /// warm-block decode for large `zstd_blocks` blobs.
407    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    /// Zero-copy mmap view — **hot / raw only**.
418    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    /// Decode one warm block by index (0-based) without inflating the whole tensor.
431    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            .chunks_exact(4)
454            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
455            .collect())
456    }
457
458    pub fn tensors_for_rank(&self, rank: u32) -> Vec<&WeightEntry> {
459        let Some(idx) = &self.weights else {
460            return Vec::new();
461        };
462        idx.tensors
463            .iter()
464            .filter(|t| t.rank.is_none() || t.rank == Some(rank))
465            .collect()
466    }
467
468    pub fn tensors_by_tier(&self, tier: StorageTier) -> Vec<&WeightEntry> {
469        let Some(idx) = &self.weights else {
470            return Vec::new();
471        };
472        idx.tensors.iter().filter(|t| t.tier == tier).collect()
473    }
474
475    pub fn rank_root(&self, rank: u32) -> Option<String> {
476        let rel = format!("dist/rank-{rank}");
477        if self.member_exists(&rel) || self.member_exists(&format!("{rel}/weights/index.json")) {
478            Some(rel)
479        } else {
480            None
481        }
482    }
483
484    pub fn member_bytes(&self, rel: &str) -> Result<Vec<u8>> {
485        match &self.source {
486            MemberSource::Flat { .. } => {
487                bail!("flat packages have no path members ({rel}); use graph()/sidecar()/tensor_*")
488            }
489            MemberSource::Dir { root, .. } => {
490                let p = root.join(rel);
491                std::fs::read(&p).with_context(|| format!("reading {}", p.display()))
492            }
493            MemberSource::Zip { map, ranges, .. } => {
494                Ok(store_zip::read_member_from_map(map, ranges, rel)?.to_vec())
495            }
496        }
497    }
498
499    fn member_exists(&self, rel: &str) -> bool {
500        match &self.source {
501            MemberSource::Flat { .. } => false,
502            MemberSource::Dir { root, .. } => root.join(rel).exists(),
503            MemberSource::Zip { ranges, .. } => {
504                ranges.contains_key(rel) || ranges.keys().any(|k| k.starts_with(&format!("{rel}/")))
505            }
506        }
507    }
508
509    fn shard_slice(&self, shard: &str, offset: u64, length: u64) -> Result<&[u8]> {
510        let start = offset as usize;
511        let end = start
512            .checked_add(length as usize)
513            .context("shard slice overflow")?;
514        match &self.source {
515            MemberSource::Flat { map, .. } => {
516                let abs = (self.flat_data_start + offset) as usize;
517                let abs_end = abs
518                    .checked_add(length as usize)
519                    .context("flat shard overflow")?;
520                map.get(abs..abs_end)
521                    .with_context(|| format!("flat slice [{offset}..+{length}]"))
522            }
523            MemberSource::Dir { shard_maps, .. } => {
524                let map = shard_maps
525                    .get(shard)
526                    .with_context(|| format!("shard {shard} not mapped"))?;
527                map.get(start..end)
528                    .with_context(|| format!("slice {shard}[{offset}..+{length}]"))
529            }
530            MemberSource::Zip { map, ranges, .. } => {
531                let base = store_zip::read_member_from_map(map, ranges, shard)?;
532                base.get(start..end)
533                    .with_context(|| format!("slice {shard}[{offset}..+{length}]"))
534            }
535        }
536    }
537}
538
539fn fill_constant(g: &mut Graph, name: &str, bytes: Vec<u8>) {
540    for n in g.nodes_mut() {
541        if n.name.as_deref() != Some(name) {
542            continue;
543        }
544        if let Op::Constant { data } = &mut n.op {
545            if data.is_empty() {
546                *data = bytes.clone();
547            }
548        }
549    }
550}
551
552/// Map RLXP `scheme` strings to a host bind dtype for `set_param_typed`.
553pub fn dtype_for_weight_scheme(scheme: &str) -> rlx_ir::DType {
554    use rlx_ir::DType;
555    if scheme == "f32" || scheme.starts_with("f32") {
556        return DType::F32;
557    }
558    if scheme == "f16" || scheme == "bf16" {
559        return DType::F16;
560    }
561    if scheme == "u8" || scheme.starts_with("mlx_") || scheme.starts_with("gguf_") || scheme == "i8"
562    {
563        return DType::U8;
564    }
565    DType::F32
566}
567
568impl Package {
569    /// Yield `(name, bytes, dtype)` for every weight — used to
570    /// `CompiledGraph::set_param_typed` after compile (Param path).
571    pub fn typed_weight_bindings(&self) -> Result<Vec<(String, Vec<u8>, rlx_ir::DType)>> {
572        let Some(idx) = &self.weights else {
573            return Ok(Vec::new());
574        };
575        let mut out = Vec::with_capacity(idx.tensors.len());
576        for entry in &idx.tensors {
577            let bytes = self.tensor_bytes(&entry.name)?;
578            out.push((
579                entry.name.clone(),
580                bytes,
581                dtype_for_weight_scheme(&entry.scheme),
582            ));
583        }
584        Ok(out)
585    }
586}
587
588fn load_weights_index_dir(dir: &Path, manifest: &Manifest) -> Result<Option<WeightsIndex>> {
589    let Some(wref) = &manifest.weights else {
590        return Ok(None);
591    };
592    let p = dir.join(&wref.index);
593    let bytes = std::fs::read(&p).with_context(|| format!("reading {}", p.display()))?;
594    let idx: WeightsIndex = serde_json::from_slice(&bytes).context("parsing weights/index.json")?;
595    Ok(Some(idx))
596}
597
598fn load_weights_index_zip(
599    map: &[u8],
600    ranges: &BTreeMap<String, MemberRange>,
601    manifest: &Manifest,
602) -> Result<Option<WeightsIndex>> {
603    let Some(wref) = &manifest.weights else {
604        return Ok(None);
605    };
606    let bytes = store_zip::read_member_from_map(map, ranges, &wref.index)?;
607    let idx: WeightsIndex = serde_json::from_slice(bytes).context("parsing weights/index.json")?;
608    Ok(Some(idx))
609}
610
611fn load_placement_dir(dir: &Path, manifest: &Manifest) -> Result<Option<Placement>> {
612    let Some(dist) = &manifest.dist else {
613        return Ok(None);
614    };
615    let Some(path) = &dist.placement else {
616        return Ok(None);
617    };
618    let p = dir.join(path);
619    if !p.exists() {
620        return Ok(None);
621    }
622    let bytes = std::fs::read(&p).with_context(|| format!("reading {}", p.display()))?;
623    let pl: Placement = serde_json::from_slice(&bytes).context("parsing placement.json")?;
624    Ok(Some(pl))
625}
626
627fn load_placement_zip(
628    map: &[u8],
629    ranges: &BTreeMap<String, MemberRange>,
630    manifest: &Manifest,
631) -> Result<Option<Placement>> {
632    let Some(dist) = &manifest.dist else {
633        return Ok(None);
634    };
635    let Some(path) = &dist.placement else {
636        return Ok(None);
637    };
638    if !ranges.contains_key(path) {
639        return Ok(None);
640    }
641    let bytes = store_zip::read_member_from_map(map, ranges, path)?;
642    let pl: Placement = serde_json::from_slice(bytes).context("parsing placement.json")?;
643    Ok(Some(pl))
644}