use crate::BakeReport;
use crate::optimize::{OptimizeStats, WeightEncoding, WeightRewrite};
use anyhow::{Context, Result, bail};
use rlx_ir::{Dim, Graph, Op, Shape};
use serde::{Deserialize, Serialize};
use std::path::Path;
pub const RLX_MAGIC: &[u8; 8] = b"RLXBAKE1";
pub const RLX_FORMAT_VERSION: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RlxIo {
pub name: String,
pub shape: Vec<usize>,
pub dtype: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RlxWeight {
pub name: String,
pub shape: Vec<usize>,
pub encoding: String,
pub data: Vec<u8>,
pub note: String,
}
impl RlxWeight {
pub fn from_rewrite(r: &WeightRewrite) -> Self {
Self {
name: r.name.clone(),
shape: r.shape.clone(),
encoding: r.encoding.as_str().to_string(),
data: r.data.clone(),
note: r.note.clone(),
}
}
pub fn from_f32(name: impl Into<String>, shape: Vec<usize>, values: &[f32]) -> Self {
let mut data = Vec::with_capacity(values.len() * 4);
for &v in values {
data.extend_from_slice(&v.to_le_bytes());
}
Self {
name: name.into(),
shape,
encoding: WeightEncoding::F32.as_str().to_string(),
data,
note: "baked f32".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RlxMeta {
pub name: String,
pub inputs: Vec<RlxIo>,
pub outputs: Vec<RlxIo>,
pub params_remaining: Vec<String>,
pub nodes: usize,
pub constant_bytes: usize,
pub params_baked: usize,
pub weight_count: usize,
pub weight_bytes: usize,
pub skipped_zero_matmuls: usize,
pub ternary_packed: usize,
pub quant_packed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RlxFile {
pub meta: RlxMeta,
pub graph: Graph,
pub weights: Vec<RlxWeight>,
}
#[derive(Deserialize)]
struct RlxFileV1 {
meta: RlxMetaV1,
graph: Graph,
}
#[derive(Deserialize)]
struct RlxMetaV1 {
name: String,
inputs: Vec<RlxIo>,
outputs: Vec<RlxIo>,
params_remaining: Vec<String>,
nodes: usize,
constant_bytes: usize,
params_baked: usize,
}
impl RlxFile {
pub fn from_baked(
graph: Graph,
report: &BakeReport,
weights: Vec<RlxWeight>,
opt: &OptimizeStats,
) -> Self {
let inputs = graph
.nodes()
.iter()
.filter_map(|n| match &n.op {
Op::Input { name } => Some(io_from_shape(name, &n.shape)),
_ => None,
})
.collect();
let outputs = graph
.outputs
.iter()
.map(|&id| {
let n = graph.node(id);
let name = n.name.clone().unwrap_or_else(|| format!("out{}", id.0));
io_from_shape(&name, &n.shape)
})
.collect();
let weight_bytes: usize = weights.iter().map(|w| w.data.len()).sum();
let weight_count = weights.len();
Self {
meta: RlxMeta {
name: graph.name.clone(),
inputs,
outputs,
params_remaining: report.params_remaining.clone(),
nodes: report.nodes_after,
constant_bytes: report.constant_bytes,
params_baked: report.params_baked,
weight_count,
weight_bytes,
skipped_zero_matmuls: opt.skipped_zero_matmuls,
ternary_packed: opt.ternary_packed,
quant_packed: opt.quant_packed,
},
graph,
weights,
}
}
}
fn io_from_shape(name: &str, shape: &Shape) -> RlxIo {
let dims: Vec<usize> = shape
.dims()
.iter()
.map(|d| match d {
Dim::Static(n) => *n,
Dim::Dynamic(_) => 0,
})
.collect();
RlxIo {
name: name.to_string(),
shape: dims,
dtype: format!("{:?}", shape.dtype()).to_lowercase(),
}
}
pub fn write_rlx(path: impl AsRef<Path>, file: &RlxFile) -> Result<()> {
let path = path.as_ref();
let bytes = to_bytes(file).context("serializing *.rlx")?;
std::fs::write(path, bytes).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
#[cfg(feature = "encrypt")]
pub fn write_rlx_encrypted(path: impl AsRef<Path>, file: &RlxFile, password: &str) -> Result<()> {
let path = path.as_ref();
let plain = to_bytes(file).context("serializing *.rlx")?;
let enc = crate::crypto::encrypt_bytes(&plain, password).context("encrypting *.rlx")?;
std::fs::write(path, enc).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn read_rlx(path: impl AsRef<Path>) -> Result<RlxFile> {
let path = path.as_ref();
let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
parse_rlx_bytes(&bytes, path)
}
#[cfg(feature = "mmap")]
pub fn read_rlx_mmap(path: impl AsRef<Path>) -> Result<RlxFile> {
use std::fs::File;
let path = path.as_ref();
let file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let map =
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("mmap {}", path.display()))?;
parse_rlx_bytes(&map, path)
}
fn parse_rlx_bytes(bytes: &[u8], path: &Path) -> Result<RlxFile> {
if looks_encrypted(bytes) {
#[cfg(feature = "encrypt")]
bail!(
"{} is encrypted; pass a password via read_rlx_with_password / --password",
path.display()
);
#[cfg(not(feature = "encrypt"))]
bail!(
"{} is encrypted; rebuild rlx-bake with `--features encrypt` to load it",
path.display()
);
}
from_bytes(bytes).with_context(|| format!("parsing {}", path.display()))
}
#[cfg(feature = "encrypt")]
pub fn read_rlx_with_password(path: impl AsRef<Path>, password: &str) -> Result<RlxFile> {
let path = path.as_ref();
let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
let plain = if crate::crypto::is_encrypted(&bytes) {
crate::crypto::decrypt_bytes(&bytes, password)
.with_context(|| format!("decrypting {}", path.display()))?
} else {
bytes
};
from_bytes(&plain).with_context(|| format!("parsing {}", path.display()))
}
fn looks_encrypted(bytes: &[u8]) -> bool {
bytes.len() >= 8 && &bytes[..8] == b"RLXENC01"
}
pub fn to_bytes(file: &RlxFile) -> Result<Vec<u8>> {
let body = bincode::serialize(file).context("bincode serialize RlxFile")?;
let mut out = Vec::with_capacity(12 + body.len());
out.extend_from_slice(RLX_MAGIC);
out.extend_from_slice(&RLX_FORMAT_VERSION.to_le_bytes());
out.extend_from_slice(&body);
Ok(out)
}
pub fn from_bytes(b: &[u8]) -> Result<RlxFile> {
if b.len() < 12 {
bail!("*.rlx too short ({} bytes)", b.len());
}
if &b[..8] != RLX_MAGIC {
bail!(
"bad *.rlx magic (expected RLXBAKE1, got {:?})",
String::from_utf8_lossy(&b[..8])
);
}
let ver = u32::from_le_bytes(b[8..12].try_into().unwrap());
let body = &b[12..];
match ver {
2 => bincode::deserialize(body).context("bincode deserialize RlxFile v2"),
1 => {
let v1: RlxFileV1 =
bincode::deserialize(body).context("bincode deserialize RlxFile v1")?;
Ok(RlxFile {
meta: RlxMeta {
name: v1.meta.name,
inputs: v1.meta.inputs,
outputs: v1.meta.outputs,
params_remaining: v1.meta.params_remaining,
nodes: v1.meta.nodes,
constant_bytes: v1.meta.constant_bytes,
params_baked: v1.meta.params_baked,
weight_count: 0,
weight_bytes: 0,
skipped_zero_matmuls: 0,
ternary_packed: 0,
quant_packed: 0,
},
graph: v1.graph,
weights: Vec::new(),
})
}
other => bail!("*.rlx schema version {other} unsupported (current {RLX_FORMAT_VERSION})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BakeOptions, bake};
use rlx_ir::op::BinaryOp;
use rlx_ir::{DType, Shape};
use std::collections::HashMap;
#[test]
fn roundtrip_bytes_with_weights() {
let s = Shape::new(&[2], DType::F32);
let mut g = Graph::new("rt");
let x = g.input("x", s.clone());
let w = g.param("w", s.clone());
let y = g.binary(BinaryOp::Mul, x, w, s);
g.set_outputs(vec![y]);
let mut bindings = HashMap::new();
bindings.insert("w".into(), vec![1.0, 2.0]);
let (file, report) = bake(&g, &bindings, &BakeOptions::default());
assert!(!file.weights.is_empty(), "expected weights in artifact");
assert_eq!(file.meta.weight_count, file.weights.len());
let bytes = to_bytes(&file).unwrap();
let back = from_bytes(&bytes).unwrap();
assert_eq!(back.meta.name, "rt");
assert_eq!(back.weights.len(), file.weights.len());
assert_eq!(back.weights[0].name, "w");
assert_eq!(back.meta.params_remaining, report.params_remaining);
}
#[cfg(feature = "encrypt")]
#[test]
fn encrypted_roundtrip_file() {
let s = Shape::new(&[2], DType::F32);
let mut g = Graph::new("enc");
let x = g.input("x", s.clone());
let w = g.param("w", s.clone());
let y = g.binary(BinaryOp::Mul, x, w, s);
g.set_outputs(vec![y]);
let mut bindings = HashMap::new();
bindings.insert("w".into(), vec![3.0, 4.0]);
let (file, _) = bake(&g, &bindings, &BakeOptions::default());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("model.rlx");
let plain = to_bytes(&file).unwrap();
let enc = crate::crypto::encrypt_bytes_with_params(&plain, "pw", 8, 1, 1).unwrap();
std::fs::write(&path, &enc).unwrap();
assert!(crate::crypto::is_encrypted(&enc));
assert!(read_rlx(&path).is_err());
let back = read_rlx_with_password(&path, "pw").unwrap();
assert_eq!(back.meta.name, "enc");
assert_eq!(back.weights.len(), file.weights.len());
assert_eq!(back.weights[0].data, file.weights[0].data);
}
}