Skip to main content

rlx_ir/
binding_manifest.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//! Reflected binding layout from a specialized [`LirModule`].
6//!
7//! Host code uses this to fill weights and inputs without hand-maintaining parallel
8//! struct layouts (the shading-system “parameter block” pattern).
9
10use crate::lir::LirModule;
11use crate::{DType, NodeId, Shape};
12
13/// One named graph boundary with arena layout after buffer planning.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct IoBindingEntry {
16    pub name: String,
17    pub node: NodeId,
18    pub dtype: DType,
19    pub shape: Shape,
20    pub elem_count: usize,
21    pub byte_size: usize,
22    pub arena_offset: Option<usize>,
23    pub arena_size: Option<usize>,
24    pub is_view: bool,
25}
26
27/// Full I/O + parameter manifest for a compiled graph.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct BindingManifest {
30    pub graph_name: String,
31    pub arena_size: usize,
32    pub alignment: usize,
33    pub inputs: Vec<IoBindingEntry>,
34    pub params: Vec<IoBindingEntry>,
35    pub outputs: Vec<IoBindingEntry>,
36}
37
38impl BindingManifest {
39    pub fn from_lir(lir: &LirModule) -> Self {
40        let graph = lir.as_graph();
41        let plan = lir.plan();
42        let io = &plan.io;
43
44        let mut inputs = Vec::new();
45        for (name, id) in &io.inputs {
46            if let Some(e) = entry_for_node(graph, plan, name.clone(), *id) {
47                inputs.push(e);
48            }
49        }
50
51        let mut params = Vec::new();
52        for (name, id) in &io.params {
53            if let Some(e) = entry_for_node(graph, plan, name.clone(), *id) {
54                params.push(e);
55            }
56        }
57
58        let mut outputs = Vec::new();
59        for (i, id) in io.outputs.iter().enumerate() {
60            let name = format!("output{i}");
61            if let Some(e) = entry_for_node(graph, plan, name, *id) {
62                outputs.push(e);
63            }
64        }
65
66        Self {
67            graph_name: lir.name().to_string(),
68            arena_size: plan.arena_size,
69            alignment: plan.alignment,
70            inputs,
71            params,
72            outputs,
73        }
74    }
75
76    pub fn param_names(&self) -> impl Iterator<Item = &str> {
77        self.params.iter().map(|p| p.name.as_str())
78    }
79
80    pub fn input_names(&self) -> impl Iterator<Item = &str> {
81        self.inputs.iter().map(|p| p.name.as_str())
82    }
83
84    pub fn param_byte_size(&self, name: &str) -> Option<usize> {
85        self.params
86            .iter()
87            .find(|p| p.name == name)
88            .map(|p| p.byte_size)
89    }
90
91    pub fn total_param_bytes(&self) -> usize {
92        self.params.iter().map(|p| p.byte_size).sum()
93    }
94
95    /// Group parameters by dot-prefix (`layer0.attn` → block `layer0`).
96    pub fn weight_blocks(&self) -> Vec<WeightBlock> {
97        let mut blocks: std::collections::HashMap<String, Vec<IoBindingEntry>> =
98            std::collections::HashMap::new();
99        for p in &self.params {
100            let block = p.name.split('.').next().unwrap_or(&p.name).to_string();
101            blocks.entry(block).or_default().push(p.clone());
102        }
103        let mut out: Vec<WeightBlock> = blocks
104            .into_iter()
105            .map(|(prefix, params)| {
106                let byte_size = params.iter().map(|e| e.byte_size).sum();
107                WeightBlock {
108                    prefix,
109                    params,
110                    byte_size,
111                }
112            })
113            .collect();
114        out.sort_by(|a, b| a.prefix.cmp(&b.prefix));
115        out
116    }
117}
118
119/// Nested parameter block (Slang PerFrame / material grouping).
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct WeightBlock {
122    pub prefix: String,
123    pub params: Vec<IoBindingEntry>,
124    pub byte_size: usize,
125}
126
127fn entry_for_node(
128    graph: &crate::Graph,
129    plan: &crate::lir::LirBufferPlan,
130    name: String,
131    id: NodeId,
132) -> Option<IoBindingEntry> {
133    let node = graph.node(id);
134    let elem_count = node.shape.num_elements().unwrap_or(0);
135    let byte_size = elem_count * node.shape.dtype().size_bytes();
136    let (arena_offset, arena_size, is_view) = if let Some(alias) = plan.view_aliases.get(&id) {
137        let root_slot = plan.slot(alias.root)?;
138        (
139            Some(root_slot.offset + alias.byte_offset),
140            Some(byte_size),
141            true,
142        )
143    } else if let Some(slot) = plan.slot(id) {
144        (Some(slot.offset), Some(slot.size), false)
145    } else {
146        (None, None, false)
147    };
148    Some(IoBindingEntry {
149        name,
150        node: id,
151        dtype: node.shape.dtype(),
152        shape: node.shape.clone(),
153        elem_count,
154        byte_size,
155        arena_offset,
156        arena_size,
157        is_view,
158    })
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::Graph;
165    use crate::lir::{LirBufferPlan, LirBufferSlot, LirIoManifest};
166
167    #[test]
168    fn manifest_lists_params_with_sizes() {
169        let mut g = Graph::new("t");
170        let x = g.input("x", Shape::new(&[2, 4], DType::F32));
171        let w = g.param("w", Shape::new(&[4, 3], DType::F32));
172        let mm = g.matmul(x, w, Shape::new(&[2, 3], DType::F32));
173        g.set_outputs(vec![mm]);
174
175        let mut plan = LirBufferPlan {
176            io: LirIoManifest::collect(&g),
177            ..Default::default()
178        };
179        plan.assignments.insert(
180            x,
181            LirBufferSlot {
182                offset: 0,
183                size: 32,
184            },
185        );
186        plan.assignments.insert(
187            w,
188            LirBufferSlot {
189                offset: 32,
190                size: 48,
191            },
192        );
193        plan.assignments.insert(
194            mm,
195            LirBufferSlot {
196                offset: 80,
197                size: 24,
198            },
199        );
200        plan.arena_size = 104;
201
202        let lir = LirModule::new(crate::MirModule::from_graph(g), plan);
203        let m = BindingManifest::from_lir(&lir);
204        assert_eq!(m.params.len(), 1);
205        assert_eq!(m.params[0].name, "w");
206        assert_eq!(m.params[0].byte_size, 48);
207        assert_eq!(m.inputs[0].name, "x");
208    }
209}