Skip to main content

rlx_pkg/
from_mlx.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//! Import MLX weight dumps into an `.rlxp` package.
6
7use crate::tier::StorageTier;
8use crate::write::{ContainerKind, PackedWeight, WriteOptions, write_package};
9use anyhow::{Context, Result};
10use rlx_ir::quant::QuantScheme;
11use rlx_ir::{DType, Graph, Shape};
12use rlx_mlx_io::{
13    PackedLinearBinding, build_llama_like_prefill, build_parallel_dequant_graph,
14    collect_packed_linears, load_path,
15};
16use std::path::Path;
17
18/// Options for [`mlx_to_rlxp`].
19#[derive(Debug, Clone)]
20pub struct MlxImportOptions {
21    pub container: ContainerKind,
22    pub include_graph: bool,
23    pub compress_sidecars: bool,
24    pub auto_tier: bool,
25    /// When true (default), dequantize affine/mxfp layers to f32 before packing.
26    /// When false, keep `{base}.weight/.scales/.biases` as packed scheme rows
27    /// for `Op::DequantMatMul`.
28    pub dequant_to_f32: bool,
29    /// Prefill batch / seq when embedding a Llama-like graph (`include_graph`
30    /// + keep-packed + arch in config). Defaults: batch=1, seq=1.
31    pub graph_batch: usize,
32    pub graph_seq: usize,
33    /// Cap decoder layers in the embedded graph (`None` = all).
34    pub graph_num_layers: Option<usize>,
35}
36
37impl Default for MlxImportOptions {
38    fn default() -> Self {
39        Self {
40            container: ContainerKind::Flat,
41            include_graph: false,
42            compress_sidecars: true,
43            auto_tier: true,
44            dequant_to_f32: true,
45            graph_batch: 1,
46            graph_seq: 1,
47            graph_num_layers: None,
48        }
49    }
50}
51
52fn push_packed_linear(weights: &mut Vec<PackedWeight>, b: &PackedLinearBinding) {
53    let n = b.packed.out_shape[0];
54    let n_groups = b.packed.n_groups().max(1);
55    // Logical `[n, k]` for DequantMatMul; packed bytes length is `data.len()`.
56    weights.push(PackedWeight {
57        name: format!("{}.weight", b.name),
58        shape: b.packed.out_shape.clone(),
59        scheme: b.packed.scheme.to_string(),
60        layout: "mlx_nk".into(),
61        data: b.packed.w_q.clone(),
62        rank: None,
63        tier: StorageTier::Hot,
64    });
65    weights.push(PackedWeight {
66        name: format!("{}.scales", b.name),
67        shape: vec![n, n_groups],
68        scheme: if matches!(b.packed.scheme, QuantScheme::MlxAffine { .. }) {
69            "f32".into()
70        } else {
71            "u8".into()
72        },
73        layout: "row_major".into(),
74        data: b.packed.scales.clone(),
75        rank: None,
76        tier: StorageTier::Hot,
77    });
78    if matches!(b.packed.scheme, QuantScheme::MlxAffine { .. }) {
79        weights.push(PackedWeight {
80            name: format!("{}.biases", b.name),
81            shape: vec![n, n_groups],
82            scheme: "f32".into(),
83            layout: "row_major".into(),
84            data: b.packed.biases.clone(),
85            rank: None,
86            tier: StorageTier::Hot,
87        });
88    }
89}
90
91fn push_remaining_dense(
92    loaded: &mut rlx_mlx_io::MlxWeights,
93    weights: &mut Vec<PackedWeight>,
94) -> Result<()> {
95    for name in loaded.logical_keys() {
96        if let Ok((data, shape)) = loaded.take_dense_f32(&name) {
97            let mut bytes = Vec::with_capacity(data.len() * 4);
98            for x in &data {
99                bytes.extend_from_slice(&x.to_le_bytes());
100            }
101            weights.push(PackedWeight {
102                name,
103                shape,
104                scheme: "f32".into(),
105                layout: "row_major".into(),
106                data: bytes,
107                rank: None,
108                tier: StorageTier::Hot,
109            });
110        }
111    }
112    Ok(())
113}
114
115/// Convert an MLX weight path (dir / `.safetensors` / `.npz` / `.npy`) → `.rlxp`.
116pub fn mlx_to_rlxp(
117    mlx_path: impl AsRef<Path>,
118    out: impl AsRef<Path>,
119    opts: &MlxImportOptions,
120) -> Result<()> {
121    let mlx_path = mlx_path.as_ref();
122    let mut loaded =
123        load_path(mlx_path).with_context(|| format!("open mlx {}", mlx_path.display()))?;
124    let config_bytes = loaded
125        .config
126        .raw
127        .as_ref()
128        .map(serde_json::to_vec_pretty)
129        .transpose()?;
130
131    let gname = mlx_path
132        .file_stem()
133        .and_then(|s| s.to_str())
134        .unwrap_or("mlx")
135        .to_string();
136
137    let mut weights = Vec::new();
138    let graph = if opts.dequant_to_f32 {
139        let shaped = loaded.into_shaped_f32()?;
140        for (name, (shape, data)) in shaped {
141            let mut bytes = Vec::with_capacity(data.len() * 4);
142            for x in &data {
143                bytes.extend_from_slice(&x.to_le_bytes());
144            }
145            weights.push(PackedWeight {
146                name,
147                shape,
148                scheme: "f32".into(),
149                layout: "row_major".into(),
150                data: bytes,
151                rank: None,
152                tier: StorageTier::Hot,
153            });
154        }
155        if opts.include_graph {
156            let mut g = Graph::new(&gname);
157            let s = Shape::new(&[1], DType::F32);
158            let x = g.input("x", s);
159            g.set_outputs(vec![x]);
160            g
161        } else {
162            Graph::new(&gname)
163        }
164    } else {
165        let linears = collect_packed_linears(&mut loaded)?;
166        for b in &linears {
167            push_packed_linear(&mut weights, b);
168        }
169        push_remaining_dense(&mut loaded, &mut weights)?;
170        if opts.include_graph && !linears.is_empty() {
171            if let Some(arch) = loaded.config.arch.as_ref() {
172                build_llama_like_prefill(
173                    &gname,
174                    arch,
175                    &linears,
176                    opts.graph_batch.max(1),
177                    opts.graph_seq.max(1),
178                    opts.graph_num_layers,
179                )?
180            } else {
181                build_parallel_dequant_graph(&gname, &linears, 1)?
182            }
183        } else if opts.include_graph {
184            let mut g = Graph::new(&gname);
185            let s = Shape::new(&[1], DType::F32);
186            let x = g.input("x", s);
187            g.set_outputs(vec![x]);
188            g
189        } else {
190            Graph::new(&gname)
191        }
192    };
193
194    let mut wopts = WriteOptions {
195        name: gname,
196        producer: Some("rlx-pkg/mlx_import".into()),
197        container: opts.container,
198        compress_sidecars: opts.compress_sidecars,
199        include_graph: opts.include_graph,
200        write_checksums: true,
201        ..WriteOptions::default()
202    };
203
204    if opts.auto_tier {
205        crate::auto_tier::apply_auto_tier(&mut weights, &Default::default());
206    }
207
208    if let Some(bytes) = config_bytes {
209        wopts
210            .sidecars
211            .push(("config.json".into(), "application/json".into(), bytes));
212    }
213
214    write_package(out.as_ref(), &graph, &weights, &wopts)
215        .with_context(|| format!("write {}", out.as_ref().display()))?;
216    Ok(())
217}