1use 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#[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 pub dequant_to_f32: bool,
29 pub graph_batch: usize,
32 pub graph_seq: usize,
33 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 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
115pub 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}