use crate::format::{RlxFile, RlxWeight};
use anyhow::{Result, bail};
use rlx_ir::{Graph, NodeId, Op};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MemoryMode {
Duplex,
Runtime,
#[default]
Compact,
}
impl MemoryMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Duplex => "duplex",
Self::Runtime => "runtime",
Self::Compact => "compact",
}
}
pub fn description(self) -> &'static str {
match self {
Self::Duplex => "weight bytes in graph and table (duplicate)",
Self::Runtime => "weight bytes in graph only; table is metadata",
Self::Compact => "weight bytes in table only; materialize before compile",
}
}
}
impl fmt::Display for MemoryMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for MemoryMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"duplex" | "both" | "full" => Ok(Self::Duplex),
"runtime" | "graph" => Ok(Self::Runtime),
"compact" | "table" | "lean" => Ok(Self::Compact),
other => Err(format!(
"unknown memory mode {other:?}; expected duplex, runtime, or compact"
)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MemoryStats {
pub mode: String,
pub constants_deduped: usize,
pub graph_bytes_stripped: usize,
pub table_bytes_stripped: usize,
pub folded_bindings_dropped: usize,
}
pub fn dedupe_identical_constants(graph: &Graph) -> (Graph, usize) {
let mut out = Graph::new(graph.name.clone());
let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
let mut blob_to_id: HashMap<Vec<u8>, NodeId> = HashMap::new();
let mut merged = 0usize;
for node in graph.nodes() {
let new_id = match &node.op {
Op::Constant { data } => {
let mut key = Vec::with_capacity(16 + data.len());
key.extend_from_slice(format!("{:?}", node.shape).as_bytes());
key.extend_from_slice(data);
if let Some(&existing) = blob_to_id.get(&key) {
merged += 1;
existing
} else {
let id = out.add_node(node.op.clone(), vec![], node.shape.clone());
if let Some(name) = &node.name {
out.node_mut(id).name = Some(name.clone());
}
blob_to_id.insert(key, id);
id
}
}
_ => {
let inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
let id = out.add_node(node.op.clone(), inputs, node.shape.clone());
if let Some(name) = &node.name {
out.node_mut(id).name = Some(name.clone());
}
id
}
};
id_map.insert(node.id, new_id);
}
out.set_outputs(graph.outputs.iter().map(|o| id_map[o]).collect());
(out, merged)
}
pub fn strip_graph_weight_bytes(graph: &mut Graph, weights: &[RlxWeight]) -> usize {
let names: std::collections::HashSet<&str> = weights.iter().map(|w| w.name.as_str()).collect();
let mut stripped = 0usize;
for node in graph.nodes_mut() {
let Some(name) = node.name.as_deref() else {
continue;
};
if !names.contains(name) {
continue;
}
if let Op::Constant { data } = &mut node.op {
stripped += data.len();
data.clear();
}
}
stripped
}
pub fn strip_table_weight_bytes(weights: &mut [RlxWeight]) -> usize {
let mut stripped = 0usize;
for w in weights.iter_mut() {
stripped += w.data.len();
w.data.clear();
if !w.note.contains("metadata-only") {
w.note = format!("{} [metadata-only]", w.note);
}
}
stripped
}
impl RlxFile {
pub fn needs_materialize(&self) -> bool {
for w in &self.weights {
if w.data.is_empty() {
continue;
}
for n in self.graph.nodes() {
if n.name.as_deref() != Some(w.name.as_str()) {
continue;
}
if let Op::Constant { data } = &n.op {
if data.is_empty() {
return true;
}
}
}
}
false
}
pub fn materialize_weights(&mut self) -> Result<usize> {
let mut filled = 0usize;
for w in &self.weights {
if w.data.is_empty() {
continue;
}
let mut hit = false;
for n in self.graph.nodes_mut() {
if n.name.as_deref() != Some(w.name.as_str()) {
continue;
}
if let Op::Constant { data } = &mut n.op {
*data = w.data.clone();
filled += w.data.len();
hit = true;
}
}
if !hit {
continue;
}
}
Ok(filled)
}
pub fn drop_weight_table_data(&mut self) -> usize {
strip_table_weight_bytes(&mut self.weights)
}
pub fn into_runtime_graph(mut self) -> Result<Graph> {
if self.needs_materialize() {
self.materialize_weights()?;
}
Ok(self.graph)
}
pub fn apply_memory_mode(&mut self, mode: MemoryMode) -> MemoryStats {
let mut stats = MemoryStats {
mode: mode.as_str().to_string(),
..Default::default()
};
match mode {
MemoryMode::Duplex => {}
MemoryMode::Runtime => {
stats.table_bytes_stripped = strip_table_weight_bytes(&mut self.weights);
self.meta.weight_bytes = self.weights.iter().map(|w| w.data.len()).sum();
}
MemoryMode::Compact => {
stats.graph_bytes_stripped =
strip_graph_weight_bytes(&mut self.graph, &self.weights);
self.meta.constant_bytes = self
.graph
.nodes()
.iter()
.map(|n| match &n.op {
Op::Constant { data } => data.len(),
_ => 0,
})
.sum();
}
}
stats
}
}
pub fn ensure_runtime_ready(file: &mut RlxFile) -> Result<()> {
if !file.needs_materialize() {
return Ok(());
}
let n = file.materialize_weights()?;
if file.needs_materialize() {
bail!(
"compact *.rlx still has empty Constants after materialize \
(filled {n} bytes) — weight table may be incomplete"
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BakeOptions, BakeProfile, bake};
use rlx_ir::op::BinaryOp;
use rlx_ir::{DType, Shape};
use std::collections::HashMap;
fn tiny() -> (rlx_ir::Graph, HashMap<String, Vec<f32>>) {
let s = Shape::new(&[4], DType::F32);
let mut g = Graph::new("m");
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 b = HashMap::new();
b.insert("w".into(), vec![1.0, 2.0, 3.0, 4.0]);
(g, b)
}
#[test]
fn compact_strips_and_materializes() {
let (g, b) = tiny();
let mut opts = BakeOptions::from_profile(BakeProfile::Exact);
opts.memory = MemoryMode::Compact;
let (mut file, _) = bake(&g, &b, &opts);
assert!(file.needs_materialize());
let table_bytes: usize = file.weights.iter().map(|w| w.data.len()).sum();
assert!(table_bytes > 0);
let const_bytes: usize = file
.graph
.nodes()
.iter()
.map(|n| match &n.op {
Op::Constant { data } => data.len(),
_ => 0,
})
.sum();
assert_eq!(const_bytes, 0);
file.materialize_weights().unwrap();
assert!(!file.needs_materialize());
}
#[test]
fn runtime_strips_table() {
let (g, b) = tiny();
let mut opts = BakeOptions::from_profile(BakeProfile::Exact);
opts.memory = MemoryMode::Runtime;
let (file, report) = bake(&g, &b, &opts);
assert!(!file.needs_materialize());
assert!(file.weights.iter().all(|w| w.data.is_empty()));
assert!(report.memory.table_bytes_stripped > 0);
}
}