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 let mut corrected: BTreeSet<crate::NodeId> = BTreeSet::new();
225 for node in &nodes {
226 let Op::Concat { axis } = &node.op else {
227 continue;
228 };
229 let shapes: Vec<Shape> = node
230 .inputs
231 .iter()
232 .map(|&id| graph.node(id).shape.clone())
233 .collect();
234 let refs: Vec<&Shape> = shapes.iter().collect();
235 if let Ok(out) = crate::shape::concat_shape(&refs, *axis) {
236 if graph.node(node.id).shape != out {
237 corrected.insert(node.id);
238 }
239 graph.node_mut(node.id).shape = out;
240 }
241 }
242 if corrected.is_empty() {
243 return;
244 }
245 for node in &nodes {
260 if !node.inputs.iter().any(|id| corrected.contains(id)) {
261 continue;
262 }
263 if let Some(shape) = crate::infer_shape::infer_output_shape(graph, node) {
264 if graph.node(node.id).shape != shape {
265 corrected.insert(node.id);
266 }
267 graph.node_mut(node.id).shape = shape;
268 } else {
269 corrected.insert(node.id);
272 }
273 }
274}
275
276pub fn sync_narrow_ops(graph: &mut Graph) {
278 use crate::Op;
279 let nodes = graph.nodes().to_vec();
280 for node in &nodes {
281 let Op::Narrow { axis, start, len } = &node.op else {
282 continue;
283 };
284 let in_shape = graph.node(node.inputs[0]).shape.clone();
285 if *axis >= in_shape.rank() || !in_shape.is_static() {
286 continue;
287 }
288 let ax_len = in_shape.dims()[*axis].unwrap_static();
289 if *start + *len > ax_len {
290 graph.node_mut(node.id).op = Op::Narrow {
291 axis: *axis,
292 start: ax_len.saturating_sub(*len),
293 len: *len,
294 };
295 }
296 }
297}
298
299pub fn sync_expand_ops(graph: &mut Graph) {
301 use crate::Op;
302 let nodes = graph.nodes().to_vec();
303 for node in &nodes {
304 let Op::Expand { .. } = &node.op else {
305 continue;
306 };
307 if !node.shape.is_static() {
308 continue;
309 }
310 let target: Vec<i64> = node
311 .shape
312 .dims()
313 .iter()
314 .map(|d| d.unwrap_static() as i64)
315 .collect();
316 graph.node_mut(node.id).op = Op::Expand {
317 target_shape: target,
318 };
319 }
320}
321
322pub fn infer_bindings_from_inputs(
327 graph: &Graph,
328 inputs: &[(&str, usize)],
329) -> Result<DimBinding, String> {
330 let by_name: HashMap<&str, usize> = inputs.iter().copied().collect();
331 let mut binding = DimBinding::new();
332 for node in graph.nodes() {
333 let Op::Input { name } = &node.op else {
334 continue;
335 };
336 let Some(&n_elems) = by_name.get(name.as_str()) else {
337 continue;
338 };
339 let mut static_prod: usize = 1;
340 let mut dynamic_sym: Option<u32> = None;
341 for d in node.shape.dims() {
342 match d {
343 Dim::Static(n) => static_prod *= *n,
344 Dim::Dynamic(sym) => {
345 if dynamic_sym.is_some() {
346 return Err(format!(
347 "Input '{name}' has multiple dynamic dims; \
348 pass an explicit DimBinding"
349 ));
350 }
351 dynamic_sym = Some(*sym);
352 }
353 }
354 }
355 let Some(sym) = dynamic_sym else {
356 continue;
357 };
358 if static_prod == 0 {
359 return Err(format!("Input '{name}': static dim product is zero"));
360 }
361 if n_elems % static_prod != 0 {
362 return Err(format!(
363 "Input '{name}': len {n_elems} not divisible by static product {static_prod}"
364 ));
365 }
366 let size = n_elems / static_prod;
367 if let Some(prev) = binding.get(sym) {
368 if prev != size {
369 return Err(format!(
370 "symbol {sym} bound to {prev} and {size} from different inputs"
371 ));
372 }
373 } else {
374 binding.set(sym, size);
375 }
376 }
377 complete_im2col_row_bindings(graph, &mut binding);
378 Ok(binding)
379}
380
381pub fn complete_im2col_row_bindings(graph: &Graph, binding: &mut DimBinding) {
384 let Some(batch) = binding.get(sym::BATCH) else {
385 return;
386 };
387 if binding.get(sym::ROWS).is_some() {
388 return;
389 }
390 for node in graph.nodes() {
391 let Op::Im2Col {
392 kernel_size,
393 stride,
394 padding,
395 dilation,
396 } = &node.op
397 else {
398 continue;
399 };
400 let x_shape = &graph.node(node.inputs[0]).shape;
401 if x_shape.rank() != 4 {
402 continue;
403 }
404 if !x_shape.dim(2).is_static() || !x_shape.dim(3).is_static() {
405 continue;
406 }
407 let h = x_shape.dim(2).unwrap_static();
408 let w = x_shape.dim(3).unwrap_static();
409 let kh = kernel_size.first().copied().unwrap_or(1);
410 let kw = kernel_size.get(1).copied().unwrap_or(1);
411 let sh = stride.first().copied().unwrap_or(1);
412 let sw = stride.get(1).copied().unwrap_or(1);
413 let ph = padding.first().copied().unwrap_or(0);
414 let pw = padding.get(1).copied().unwrap_or(0);
415 let dh = dilation.first().copied().unwrap_or(1);
416 let dw = dilation.get(1).copied().unwrap_or(1);
417 let h_out = crate::shape::conv2d_spatial_output(h, kh, sh, ph, dh);
418 let w_out = crate::shape::conv2d_spatial_output(w, kw, sw, pw, dw);
419 binding.set(sym::ROWS, batch * h_out * w_out);
420 return;
421 }
422}
423
424pub fn infer_bindings_from_f32_inputs(
426 graph: &Graph,
427 inputs: &[(&str, &[f32])],
428) -> Result<DimBinding, String> {
429 infer_bindings_from_inputs(
430 graph,
431 &inputs
432 .iter()
433 .map(|(n, d)| (*n, d.len()))
434 .collect::<Vec<_>>(),
435 )
436}
437
438pub fn same_binding(a: &DimBinding, b: &DimBinding) -> bool {
439 if a.len() != b.len() {
440 return false;
441 }
442 a.iter().all(|(sym, size)| b.get(sym) == Some(size))
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use crate::infer::GraphExt;
449
450 #[test]
451 fn bind_graph_specializes_matmul() {
452 let batch = sym::BATCH;
453 let seq = sym::SEQ;
454 let mut g = Graph::new("dyn");
455 let x = g.input("x", Shape::batch_seq(batch, seq, 4, DType::F32));
456 let w = g.param("w", Shape::new(&[4, 8], DType::F32));
457 let y = g.mm(x, w);
458 g.set_outputs(vec![y]);
459
460 assert!(has_dynamic_dims(&g));
461 let binding = DimBinding::batch_seq(2, 16);
462 let bound = bind_graph(&g, &binding);
463 assert!(!has_dynamic_dims(&bound));
464 assert_eq!(
465 bound.node(bound.outputs[0]).shape,
466 Shape::new(&[2, 16, 8], DType::F32)
467 );
468 }
469
470 #[test]
471 fn infer_bindings_from_input_data() {
472 let mut g = Graph::new("dyn");
473 let x = g.input(
474 "x",
475 Shape::from_dims(
476 &[Dim::Static(3), Dim::Dynamic(sym::SEQ), Dim::Static(64)],
477 DType::F32,
478 ),
479 );
480 g.set_outputs(vec![x]);
481
482 let b = infer_bindings_from_f32_inputs(&g, &[("x", &vec![0.0f32; 3 * 128 * 64])])
483 .expect("infer");
484 assert_eq!(b.get(sym::SEQ), Some(128));
485 }
486
487 #[test]
488 fn infer_bindings_sets_im2col_rows_from_batch() {
489 let mut g = Graph::new("im2col_rows");
490 let x = g.input(
491 "x",
492 Shape::from_dims(
493 &[
494 Dim::Dynamic(sym::BATCH),
495 Dim::Static(1),
496 Dim::Static(4),
497 Dim::Static(4),
498 ],
499 DType::F32,
500 ),
501 );
502 let _col = g.im2col(x, [3, 3], [1, 1], [1, 1], [1, 1]);
503 g.set_outputs(vec![x]);
504 let b = infer_bindings_from_f32_inputs(&g, &[("x", &[0.0f32; 2 * 16])]).expect("infer");
505 assert_eq!(b.get(sym::BATCH), Some(2));
506 assert_eq!(b.get(sym::ROWS), Some(2 * 4 * 4));
507 }
508}