1use crate::flat::{FlatPayload, FlatTensorMeta, write_flat};
8use crate::manifest::{DistRef, Manifest, SidecarRef};
9use crate::placement::Placement;
10use crate::store_zip;
11use crate::tier::{DEFAULT_WARM_BLOCK, StorageTier, checksum_hex, encode_for_tier};
12use crate::weights_index::{WeightEntry, WeightsIndex};
13use anyhow::{Context, Result, bail};
14use rlx_ir::{Graph, Op};
15use serde::Serialize;
16use std::path::{Path, PathBuf};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum ContainerKind {
22 #[default]
24 Flat,
25 Zip,
27 Dir,
29}
30
31#[derive(Debug, Clone)]
36pub struct WriteOptions {
37 pub name: String,
39 pub producer: Option<String>,
41 pub features: Vec<String>,
43 pub container: ContainerKind,
45 pub sidecars: Vec<(String, String, Vec<u8>)>,
47 pub placement: Option<Placement>,
49 pub strip_graph_weights: bool,
52 pub compress_sidecars: bool,
54 pub warm_block_size: u32,
56 pub include_graph: bool,
58 pub write_checksums: bool,
60 pub bincode_toc: bool,
62 pub intern_strings: bool,
64}
65
66impl Default for WriteOptions {
67 fn default() -> Self {
68 Self {
69 name: "model".into(),
70 producer: None,
71 features: Vec::new(),
72 container: ContainerKind::Flat,
73 sidecars: Vec::new(),
74 placement: None,
75 strip_graph_weights: true,
76 compress_sidecars: true,
77 warm_block_size: DEFAULT_WARM_BLOCK,
78 include_graph: true,
79 write_checksums: true,
80 bincode_toc: false,
81 intern_strings: true,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
91pub struct PackedWeight {
92 pub name: String,
93 pub shape: Vec<usize>,
94 pub scheme: String,
95 pub layout: String,
96 pub data: Vec<u8>,
97 pub rank: Option<u32>,
98 pub tier: StorageTier,
100}
101
102impl PackedWeight {
103 pub fn hot(
105 name: impl Into<String>,
106 shape: Vec<usize>,
107 scheme: impl Into<String>,
108 layout: impl Into<String>,
109 data: Vec<u8>,
110 ) -> Self {
111 Self {
112 name: name.into(),
113 shape,
114 scheme: scheme.into(),
115 layout: layout.into(),
116 data,
117 rank: None,
118 tier: StorageTier::Hot,
119 }
120 }
121}
122
123pub fn write_package(
126 out: impl AsRef<Path>,
127 graph: &Graph,
128 weights: &[PackedWeight],
129 opts: &WriteOptions,
130) -> Result<()> {
131 let out = out.as_ref();
132 match opts.container {
133 ContainerKind::Flat => write_package_flat(out, graph, weights, opts),
134 ContainerKind::Zip => write_package_zip(out, graph, weights, opts),
135 ContainerKind::Dir => write_package_dir(out, graph, weights, opts),
136 }
137}
138
139pub fn graph_with_stripped_weights(graph: &Graph, weight_names: &[&str]) -> Graph {
141 let mut g = graph.clone();
142 for n in g.nodes_mut() {
143 let Some(name) = n.name.as_deref() else {
144 continue;
145 };
146 if !weight_names.contains(&name) {
147 continue;
148 }
149 if let Op::Constant { data } = &mut n.op {
150 data.clear();
151 }
152 }
153 g
154}
155
156pub fn write_package_flat(
158 path: impl AsRef<Path>,
159 graph: &Graph,
160 weights: &[PackedWeight],
161 opts: &WriteOptions,
162) -> Result<()> {
163 let names: Vec<&str> = weights.iter().map(|w| w.name.as_str()).collect();
164 let graph_for_disk = if opts.strip_graph_weights {
165 graph_with_stripped_weights(graph, &names)
166 } else {
167 graph.clone()
168 };
169
170 let (mut manifest, _index, _shard, _io, _extra) =
171 build_manifest_only(&graph_for_disk, weights, opts)?;
172 manifest.weights = None;
174 manifest.graph.path = "__flat__/graph".into();
175 manifest.sidecars.clear();
176 for (id, media, _) in &opts.sidecars {
177 manifest.sidecars.push(SidecarRef {
178 id: id.clone(),
179 path: format!("__flat__/sidecar/{id}"),
180 media_type: Some(media.clone()),
181 codec: crate::tier::Codec::None, });
183 }
184 if opts.placement.is_some() {
185 manifest.dist = Some(DistRef {
186 placement: Some("__flat__/placement".into()),
187 });
188 }
189
190 let placement_bytes = opts
191 .placement
192 .as_ref()
193 .map(serde_json::to_vec)
194 .transpose()
195 .context("serialize placement")?;
196
197 let graph_bytes = if opts.include_graph {
198 bincode::serialize(&graph_for_disk).context("serialize graph")?
199 } else {
200 Vec::new()
201 };
202
203 let payload = FlatPayload {
204 graph: graph_bytes,
205 weights: weights
206 .iter()
207 .map(|w| {
208 (
209 FlatTensorMeta {
210 name: w.name.clone(),
211 shape: w.shape.clone(),
212 scheme: w.scheme.clone(),
213 layout: w.layout.clone(),
214 rank: w.rank,
215 tier: w.tier,
216 checksum: if opts.write_checksums {
217 Some(checksum_hex(&w.data))
218 } else {
219 None
220 },
221 },
222 w.data.clone(),
223 )
224 })
225 .collect(),
226 sidecars: opts
227 .sidecars
228 .iter()
229 .map(|(id, media, data)| (id.clone(), Some(media.clone()), data.clone()))
230 .collect(),
231 placement: placement_bytes,
232 warm_block_size: opts.warm_block_size,
233 compress_sidecars: opts.compress_sidecars,
234 placement_hot: true,
235 bincode_toc: opts.bincode_toc,
236 intern_strings: opts.intern_strings,
237 include_graph: opts.include_graph,
238 };
239 write_flat(path, manifest, payload)
240}
241
242pub fn write_package_dir(
244 dir: impl AsRef<Path>,
245 graph: &Graph,
246 weights: &[PackedWeight],
247 opts: &WriteOptions,
248) -> Result<()> {
249 let dir = dir.as_ref();
250 std::fs::create_dir_all(dir.join("graph"))
251 .with_context(|| format!("mkdir {}", dir.display()))?;
252 std::fs::create_dir_all(dir.join("weights/shards"))?;
253 if !opts.sidecars.is_empty() {
254 std::fs::create_dir_all(dir.join("sidecars"))?;
255 }
256 if opts.placement.is_some() {
257 std::fs::create_dir_all(dir.join("dist"))?;
258 }
259
260 let names: Vec<&str> = weights.iter().map(|w| w.name.as_str()).collect();
261 let graph_for_disk = if opts.strip_graph_weights {
262 graph_with_stripped_weights(graph, &names)
263 } else {
264 graph.clone()
265 };
266
267 let (manifest, index, shard_bytes, io_json, members_extra) =
268 build_members(&graph_for_disk, weights, opts)?;
269
270 write_json(dir.join("rlx.json"), &manifest)?;
271 if opts.include_graph {
272 write_bytes(
273 dir.join("graph/mir.bin"),
274 &bincode::serialize(&graph_for_disk)?,
275 )?;
276 }
277 write_json(dir.join("graph/io.json"), &io_json)?;
278 if let Some(idx) = &index {
279 write_json(dir.join("weights/index.json"), idx)?;
280 write_bytes(dir.join("weights/shards/000.pack.bin"), &shard_bytes)?;
281 }
282 for (rel, data) in &members_extra {
283 let p = dir.join(rel);
284 if let Some(parent) = p.parent() {
285 std::fs::create_dir_all(parent)?;
286 }
287 write_bytes(p, data)?;
288 }
289 Ok(())
290}
291
292pub fn write_package_zip(
294 path: impl AsRef<Path>,
295 graph: &Graph,
296 weights: &[PackedWeight],
297 opts: &WriteOptions,
298) -> Result<()> {
299 let path = path.as_ref();
300 if let Some(parent) = path.parent() {
301 if !parent.as_os_str().is_empty() {
302 std::fs::create_dir_all(parent)?;
303 }
304 }
305 let names: Vec<&str> = weights.iter().map(|w| w.name.as_str()).collect();
306 let graph_for_disk = if opts.strip_graph_weights {
307 graph_with_stripped_weights(graph, &names)
308 } else {
309 graph.clone()
310 };
311 let (manifest, index, shard_bytes, io_json, members_extra) =
312 build_members(&graph_for_disk, weights, opts)?;
313
314 let mut members: Vec<(String, Vec<u8>)> = Vec::new();
315 members.push((
316 "rlx.json".into(),
317 serde_json::to_vec_pretty(&manifest).context("serialize rlx.json")?,
318 ));
319 if opts.include_graph {
320 members.push((
321 "graph/mir.bin".into(),
322 bincode::serialize(&graph_for_disk).context("serialize graph")?,
323 ));
324 }
325 members.push((
326 "graph/io.json".into(),
327 serde_json::to_vec_pretty(&io_json).context("serialize io.json")?,
328 ));
329 if let Some(idx) = &index {
330 members.push((
331 "weights/index.json".into(),
332 serde_json::to_vec_pretty(idx).context("serialize weights index")?,
333 ));
334 members.push(("weights/shards/000.pack.bin".into(), shard_bytes));
335 }
336 members.extend(members_extra);
337
338 store_zip::write_store_zip(path, &members)?;
339 Ok(())
340}
341
342#[derive(Serialize)]
343struct IoJson {
344 inputs: Vec<IoEntry>,
345 outputs: Vec<IoEntry>,
346}
347
348#[derive(Serialize)]
349struct IoEntry {
350 name: String,
351 shape: Vec<usize>,
352 dtype: String,
353}
354
355fn build_manifest_only(
356 graph: &Graph,
357 weights: &[PackedWeight],
358 opts: &WriteOptions,
359) -> Result<(
360 Manifest,
361 Option<WeightsIndex>,
362 Vec<u8>,
363 IoJson,
364 Vec<(String, Vec<u8>)>,
365)> {
366 build_members(graph, weights, opts)
367}
368
369fn build_members(
370 graph: &Graph,
371 weights: &[PackedWeight],
372 opts: &WriteOptions,
373) -> Result<(
374 Manifest,
375 Option<WeightsIndex>,
376 Vec<u8>,
377 IoJson,
378 Vec<(String, Vec<u8>)>,
379)> {
380 let mut manifest = Manifest::new_v1(opts.name.clone());
381 manifest.producer = opts.producer.clone();
382 manifest.features = opts.features.clone();
383 manifest.created_unix = Some(
384 SystemTime::now()
385 .duration_since(UNIX_EPOCH)
386 .map(|d| d.as_secs())
387 .unwrap_or(0),
388 );
389
390 let mut features = manifest.features.clone();
391 if weights.iter().any(|w| w.scheme.starts_with("gguf_")) {
392 push_feat(&mut features, "dequant_matmul");
393 }
394 if weights
395 .iter()
396 .any(|w| w.scheme.starts_with("mlx_affine") || w.scheme.starts_with("mlx_mxfp"))
397 {
398 push_feat(&mut features, "dequant_matmul");
399 push_feat(&mut features, "mlx_packed");
400 }
401 if opts.placement.is_some() {
402 push_feat(&mut features, "dist_placement");
403 }
404 for (id, _, _) in &opts.sidecars {
405 push_feat(&mut features, &format!("sidecar_{id}"));
406 }
407 push_feat(&mut features, "strip_graph_weights");
408 if !opts.include_graph {
409 push_feat(&mut features, "weight_only");
410 manifest.graph.encoding = "none".into();
411 }
412 if weights.iter().any(|w| w.tier != StorageTier::Hot) {
413 push_feat(&mut features, "hybrid_storage");
414 if weights.iter().any(|w| w.tier == StorageTier::Warm) {
415 push_feat(&mut features, "zstd_blocks_warm");
416 }
417 if weights.iter().any(|w| w.tier == StorageTier::Cold) {
418 push_feat(&mut features, "zstd_cold");
419 }
420 }
421 if opts.compress_sidecars && !opts.sidecars.is_empty() {
422 push_feat(&mut features, "zstd_cold");
423 }
424 manifest.features = features;
425
426 let mut extra: Vec<(String, Vec<u8>)> = Vec::new();
427 for (id, media, data) in &opts.sidecars {
428 let (codec, stored) = if opts.compress_sidecars {
429 encode_for_tier(StorageTier::Cold, data, opts.warm_block_size)?
430 } else {
431 (crate::tier::Codec::None, data.clone())
432 };
433 let rel = if media.contains("json") {
434 format!("sidecars/{id}.json")
435 } else {
436 format!("sidecars/{id}")
437 };
438 manifest.sidecars.push(SidecarRef {
439 id: id.clone(),
440 path: rel.clone(),
441 media_type: Some(media.clone()),
442 codec,
443 });
444 extra.push((rel, stored));
445 }
446
447 if let Some(pl) = &opts.placement {
448 let rel = "dist/placement.json".to_string();
449 manifest.dist = Some(DistRef {
450 placement: Some(rel.clone()),
451 });
452 extra.push((
453 rel,
454 serde_json::to_vec_pretty(pl).context("serialize placement")?,
455 ));
456 }
457
458 let (index, shard_bytes) = if weights.is_empty() {
459 manifest.weights = None;
460 (None, Vec::new())
461 } else {
462 let mut shard = Vec::new();
463 let mut tensors = Vec::new();
464 for w in weights {
465 let (codec, stored) = encode_for_tier(w.tier, &w.data, opts.warm_block_size)?;
466 let offset = shard.len() as u64;
467 let length = stored.len() as u64;
468 let raw_length = if codec == crate::tier::Codec::None {
469 None
470 } else {
471 Some(w.data.len() as u64)
472 };
473 shard.extend_from_slice(&stored);
474 tensors.push(WeightEntry {
475 name: w.name.clone(),
476 shape: w.shape.clone(),
477 scheme: w.scheme.clone(),
478 layout: w.layout.clone(),
479 shard: "weights/shards/000.pack.bin".into(),
480 offset,
481 length,
482 raw_length,
483 tier: w.tier,
484 codec,
485 rank: w.rank,
486 checksum: if opts.write_checksums {
487 Some(checksum_hex(&w.data))
488 } else {
489 None
490 },
491 });
492 }
493 (Some(WeightsIndex { tensors }), shard)
494 };
495
496 let io_json = IoJson {
497 inputs: graph
498 .nodes()
499 .iter()
500 .filter_map(|n| match &n.op {
501 Op::Input { name } => Some(io_entry(name, &n.shape)),
502 _ => None,
503 })
504 .collect(),
505 outputs: graph
506 .outputs
507 .iter()
508 .map(|&id| {
509 let n = graph.node(id);
510 let name = n.name.clone().unwrap_or_else(|| format!("out{}", id.0));
511 io_entry(&name, &n.shape)
512 })
513 .collect(),
514 };
515
516 Ok((manifest, index, shard_bytes, io_json, extra))
517}
518
519fn io_entry(name: &str, shape: &rlx_ir::Shape) -> IoEntry {
520 let dims: Vec<usize> = shape
521 .dims()
522 .iter()
523 .map(|d| match d {
524 rlx_ir::Dim::Static(n) => *n,
525 rlx_ir::Dim::Dynamic(_) => 0,
526 })
527 .collect();
528 IoEntry {
529 name: name.to_string(),
530 shape: dims,
531 dtype: format!("{:?}", shape.dtype()).to_lowercase(),
532 }
533}
534
535fn push_feat(feats: &mut Vec<String>, f: &str) {
536 if !feats.iter().any(|x| x == f) {
537 feats.push(f.to_string());
538 }
539}
540
541fn write_json(path: PathBuf, v: &impl Serialize) -> Result<()> {
542 let bytes = serde_json::to_vec_pretty(v)?;
543 write_bytes(path, &bytes)
544}
545
546fn write_bytes(path: PathBuf, bytes: &[u8]) -> Result<()> {
547 std::fs::write(&path, bytes).with_context(|| format!("writing {}", path.display()))
548}
549
550pub fn infer_container(path: &Path, explicit: Option<ContainerKind>) -> Result<ContainerKind> {
552 if let Some(c) = explicit {
553 return Ok(c);
554 }
555 if path.is_dir() {
556 return Ok(ContainerKind::Dir);
557 }
558 match path.extension().and_then(|e| e.to_str()) {
559 Some("rlxp") => Ok(ContainerKind::Flat),
560 Some("zip") => Ok(ContainerKind::Zip),
561 None => Ok(ContainerKind::Dir),
562 Some(other) => {
563 bail!("cannot infer package kind from .{other}; use .rlxp (flat), .zip, or a directory")
564 }
565 }
566}
567
568pub fn infer_zip_from_path(path: &Path, explicit_zip: Option<bool>) -> Result<bool> {
571 match explicit_zip {
572 Some(true) => Ok(true),
573 Some(false) => Ok(false),
574 None => Ok(matches!(infer_container(path, None)?, ContainerKind::Zip)),
575 }
576}