1use std::collections::{BTreeSet, HashMap};
11
12use crate::shape::{Dim, DimBinding, Shape};
13use crate::{DType, Graph, Op};
14
15pub mod sym {
18 pub const BATCH: u32 = 0;
19 pub const SEQ: u32 = 1;
20 pub const PAST_SEQ: u32 = 3;
22 pub const ROWS: u32 = 2;
24}
25
26#[derive(Debug, Clone, Default)]
28pub struct DimEnv {
29 next: u32,
30 names: HashMap<String, u32>,
31}
32
33impl DimEnv {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn sym(&mut self, name: &str) -> u32 {
40 if let Some(&id) = self.names.get(name) {
41 return id;
42 }
43 let id = self.next;
44 self.next += 1;
45 self.names.insert(name.into(), id);
46 id
47 }
48
49 pub fn name(&self, symbol: u32) -> Option<&str> {
50 self.names
51 .iter()
52 .find_map(|(n, &s)| (s == symbol).then_some(n.as_str()))
53 }
54}
55
56impl Shape {
57 pub fn batch_seq(batch: u32, seq: u32, hidden: usize, dtype: DType) -> Self {
59 Self::from_dims(
60 &[Dim::Dynamic(batch), Dim::Dynamic(seq), Dim::Static(hidden)],
61 dtype,
62 )
63 }
64
65 pub fn batch_seq_2d(batch: u32, seq: u32, dtype: DType) -> Self {
67 Self::from_dims(&[Dim::Dynamic(batch), Dim::Dynamic(seq)], dtype)
68 }
69
70 pub fn batch_seq_heads(
72 batch: u32,
73 seq: u32,
74 heads: usize,
75 head_dim: usize,
76 dtype: DType,
77 ) -> Self {
78 Self::from_dims(
79 &[
80 Dim::Dynamic(batch),
81 Dim::Dynamic(seq),
82 Dim::Static(heads),
83 Dim::Static(head_dim),
84 ],
85 dtype,
86 )
87 }
88}
89
90impl DimBinding {
91 pub fn from_pairs(pairs: &[(u32, usize)]) -> Self {
92 let mut b = Self::new();
93 for &(sym, size) in pairs {
94 b.set(sym, size);
95 }
96 b
97 }
98
99 pub fn batch_seq(batch: usize, seq: usize) -> Self {
100 let mut b = Self::from_pairs(&[(sym::BATCH, batch), (sym::SEQ, seq)]);
101 if batch > 1 {
102 b.set(sym::ROWS, batch * seq);
103 }
104 b
105 }
106
107 pub fn batch_past_seq(batch: usize, past_seq: usize) -> Self {
108 Self::from_pairs(&[(sym::BATCH, batch), (sym::PAST_SEQ, past_seq)])
109 }
110}
111
112pub fn has_dynamic_dims(graph: &Graph) -> bool {
114 graph
115 .nodes()
116 .iter()
117 .any(|n| n.shape.dims().iter().any(|d| matches!(d, Dim::Dynamic(_))))
118}
119
120pub fn collect_dynamic_symbols(graph: &Graph) -> Vec<u32> {
122 let mut syms = BTreeSet::new();
123 for node in graph.nodes() {
124 for s in node.shape.dynamic_symbols() {
125 syms.insert(s);
126 }
127 }
128 syms.into_iter().collect()
129}
130
131pub fn bind_graph(graph: &Graph, bindings: &DimBinding) -> Graph {
136 let mut out = Graph::new(&graph.name);
137 for node in graph.nodes() {
138 let bound = node.shape.bind(bindings);
139 out.push_ext(
140 node.op.clone(),
141 node.inputs.clone(),
142 bound,
143 node.name.clone(),
144 node.origin.clone(),
145 );
146 }
147 out.set_outputs(graph.outputs.clone());
148 out
149}
150
151pub fn sync_reshape_ops(graph: &mut Graph) {
153 use crate::Op;
154 for node in graph.nodes_mut() {
155 if let Op::Reshape { new_shape } = &mut node.op {
156 if node.shape.is_static() {
157 *new_shape = node
158 .shape
159 .dims()
160 .iter()
161 .map(|d| d.unwrap_static() as i64)
162 .collect();
163 }
164 }
165 }
166}
167
168pub fn sync_graph_shapes(graph: &mut Graph) {
187 fn extents(s: &Shape) -> Vec<usize> {
190 s.dims()
191 .iter()
192 .map(|d| d.unwrap_static())
193 .filter(|&v| v != 1)
194 .collect()
195 }
196 let nodes = graph.nodes().to_vec();
197 for node in &nodes {
198 if let Some(shape) = crate::infer_shape::infer_output_shape(graph, node) {
199 if node.shape.is_static()
200 && shape.is_static()
201 && extents(&node.shape) != extents(&shape)
202 {
203 panic!(
204 "rlx-ir: node {:?} ({:?}) output shape mismatch: declares {} but its \
205 operands give {}. These disagree and both are static, so this is a \
206 malformed graph, not a shape that needs refining — the arena is sized \
207 from the declaration while kernels read their extents from the operands, \
208 so letting it through under- or over-writes the output slot",
209 node.id, node.op, node.shape, shape
210 );
211 }
212 graph.node_mut(node.id).shape = shape;
213 }
214 }
215}
216
217pub fn sync_concat_shapes(graph: &mut Graph) {
219 use crate::Op;
220 let nodes = graph.nodes().to_vec();
221 for node in &nodes {
222 let Op::Concat { axis } = &node.op else {
223 continue;
224 };
225 let shapes: Vec<Shape> = node
226 .inputs
227 .iter()
228 .map(|&id| graph.node(id).shape.clone())
229 .collect();
230 let refs: Vec<&Shape> = shapes.iter().collect();
231 if let Ok(out) = crate::shape::concat_shape(&refs, *axis) {
232 graph.node_mut(node.id).shape = out;
233 }
234 }
235}
236
237pub fn sync_narrow_ops(graph: &mut Graph) {
239 use crate::Op;
240 let nodes = graph.nodes().to_vec();
241 for node in &nodes {
242 let Op::Narrow { axis, start, len } = &node.op else {
243 continue;
244 };
245 let in_shape = graph.node(node.inputs[0]).shape.clone();
246 if *axis >= in_shape.rank() || !in_shape.is_static() {
247 continue;
248 }
249 let ax_len = in_shape.dims()[*axis].unwrap_static();
250 if *start + *len > ax_len {
251 graph.node_mut(node.id).op = Op::Narrow {
252 axis: *axis,
253 start: ax_len.saturating_sub(*len),
254 len: *len,
255 };
256 }
257 }
258}
259
260pub fn sync_expand_ops(graph: &mut Graph) {
262 use crate::Op;
263 let nodes = graph.nodes().to_vec();
264 for node in &nodes {
265 let Op::Expand { .. } = &node.op else {
266 continue;
267 };
268 if !node.shape.is_static() {
269 continue;
270 }
271 let target: Vec<i64> = node
272 .shape
273 .dims()
274 .iter()
275 .map(|d| d.unwrap_static() as i64)
276 .collect();
277 graph.node_mut(node.id).op = Op::Expand {
278 target_shape: target,
279 };
280 }
281}
282
283pub fn infer_bindings_from_inputs(
288 graph: &Graph,
289 inputs: &[(&str, usize)],
290) -> Result<DimBinding, String> {
291 let by_name: HashMap<&str, usize> = inputs.iter().copied().collect();
292 let mut binding = DimBinding::new();
293 for node in graph.nodes() {
294 let Op::Input { name } = &node.op else {
295 continue;
296 };
297 let Some(&n_elems) = by_name.get(name.as_str()) else {
298 continue;
299 };
300 let mut static_prod: usize = 1;
301 let mut dynamic_sym: Option<u32> = None;
302 for d in node.shape.dims() {
303 match d {
304 Dim::Static(n) => static_prod *= *n,
305 Dim::Dynamic(sym) => {
306 if dynamic_sym.is_some() {
307 return Err(format!(
308 "Input '{name}' has multiple dynamic dims; \
309 pass an explicit DimBinding"
310 ));
311 }
312 dynamic_sym = Some(*sym);
313 }
314 }
315 }
316 let Some(sym) = dynamic_sym else {
317 continue;
318 };
319 if static_prod == 0 {
320 return Err(format!("Input '{name}': static dim product is zero"));
321 }
322 if n_elems % static_prod != 0 {
323 return Err(format!(
324 "Input '{name}': len {n_elems} not divisible by static product {static_prod}"
325 ));
326 }
327 let size = n_elems / static_prod;
328 if let Some(prev) = binding.get(sym) {
329 if prev != size {
330 return Err(format!(
331 "symbol {sym} bound to {prev} and {size} from different inputs"
332 ));
333 }
334 } else {
335 binding.set(sym, size);
336 }
337 }
338 complete_im2col_row_bindings(graph, &mut binding);
339 Ok(binding)
340}
341
342pub fn complete_im2col_row_bindings(graph: &Graph, binding: &mut DimBinding) {
345 let Some(batch) = binding.get(sym::BATCH) else {
346 return;
347 };
348 if binding.get(sym::ROWS).is_some() {
349 return;
350 }
351 for node in graph.nodes() {
352 let Op::Im2Col {
353 kernel_size,
354 stride,
355 padding,
356 dilation,
357 } = &node.op
358 else {
359 continue;
360 };
361 let x_shape = &graph.node(node.inputs[0]).shape;
362 if x_shape.rank() != 4 {
363 continue;
364 }
365 if !x_shape.dim(2).is_static() || !x_shape.dim(3).is_static() {
366 continue;
367 }
368 let h = x_shape.dim(2).unwrap_static();
369 let w = x_shape.dim(3).unwrap_static();
370 let kh = kernel_size.first().copied().unwrap_or(1);
371 let kw = kernel_size.get(1).copied().unwrap_or(1);
372 let sh = stride.first().copied().unwrap_or(1);
373 let sw = stride.get(1).copied().unwrap_or(1);
374 let ph = padding.first().copied().unwrap_or(0);
375 let pw = padding.get(1).copied().unwrap_or(0);
376 let dh = dilation.first().copied().unwrap_or(1);
377 let dw = dilation.get(1).copied().unwrap_or(1);
378 let h_out = crate::shape::conv2d_spatial_output(h, kh, sh, ph, dh);
379 let w_out = crate::shape::conv2d_spatial_output(w, kw, sw, pw, dw);
380 binding.set(sym::ROWS, batch * h_out * w_out);
381 return;
382 }
383}
384
385pub fn infer_bindings_from_f32_inputs(
387 graph: &Graph,
388 inputs: &[(&str, &[f32])],
389) -> Result<DimBinding, String> {
390 infer_bindings_from_inputs(
391 graph,
392 &inputs
393 .iter()
394 .map(|(n, d)| (*n, d.len()))
395 .collect::<Vec<_>>(),
396 )
397}
398
399pub fn same_binding(a: &DimBinding, b: &DimBinding) -> bool {
400 if a.len() != b.len() {
401 return false;
402 }
403 a.iter().all(|(sym, size)| b.get(sym) == Some(size))
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use crate::infer::GraphExt;
410
411 #[test]
412 fn bind_graph_specializes_matmul() {
413 let batch = sym::BATCH;
414 let seq = sym::SEQ;
415 let mut g = Graph::new("dyn");
416 let x = g.input("x", Shape::batch_seq(batch, seq, 4, DType::F32));
417 let w = g.param("w", Shape::new(&[4, 8], DType::F32));
418 let y = g.mm(x, w);
419 g.set_outputs(vec![y]);
420
421 assert!(has_dynamic_dims(&g));
422 let binding = DimBinding::batch_seq(2, 16);
423 let bound = bind_graph(&g, &binding);
424 assert!(!has_dynamic_dims(&bound));
425 assert_eq!(
426 bound.node(bound.outputs[0]).shape,
427 Shape::new(&[2, 16, 8], DType::F32)
428 );
429 }
430
431 #[test]
432 fn infer_bindings_from_input_data() {
433 let mut g = Graph::new("dyn");
434 let x = g.input(
435 "x",
436 Shape::from_dims(
437 &[Dim::Static(3), Dim::Dynamic(sym::SEQ), Dim::Static(64)],
438 DType::F32,
439 ),
440 );
441 g.set_outputs(vec![x]);
442
443 let b = infer_bindings_from_f32_inputs(&g, &[("x", &vec![0.0f32; 3 * 128 * 64])])
444 .expect("infer");
445 assert_eq!(b.get(sym::SEQ), Some(128));
446 }
447
448 #[test]
449 fn infer_bindings_sets_im2col_rows_from_batch() {
450 let mut g = Graph::new("im2col_rows");
451 let x = g.input(
452 "x",
453 Shape::from_dims(
454 &[
455 Dim::Dynamic(sym::BATCH),
456 Dim::Static(1),
457 Dim::Static(4),
458 Dim::Static(4),
459 ],
460 DType::F32,
461 ),
462 );
463 let _col = g.im2col(x, [3, 3], [1, 1], [1, 1], [1, 1]);
464 g.set_outputs(vec![x]);
465 let b = infer_bindings_from_f32_inputs(&g, &[("x", &[0.0f32; 2 * 16])]).expect("infer");
466 assert_eq!(b.get(sym::BATCH), Some(2));
467 assert_eq!(b.get(sym::ROWS), Some(2 * 4 * 4));
468 }
469}