1use crate::pass::Pass;
47use rlx_ir::op::BinaryOp;
48use rlx_ir::shape::Dim;
49use rlx_ir::{DType, Graph, NodeId, Op, Shape};
50use std::collections::HashMap;
51
52pub struct LowerControlFlow;
55
56impl Pass for LowerControlFlow {
57 fn name(&self) -> &str {
58 "LowerControlFlow"
59 }
60 fn run(&self, graph: Graph) -> Graph {
61 let g = inline_if(graph);
62 unroll_while(g)
63 }
64}
65
66pub struct LowerScan;
70
71impl Pass for LowerScan {
72 fn name(&self) -> &str {
73 "LowerScan"
74 }
75 fn run(&self, graph: Graph) -> Graph {
76 unroll_scan(graph)
77 }
78}
79
80pub fn unroll_scan(g: Graph) -> Graph {
88 let mut out = Graph::new(g.name.clone());
89 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
90 let nodes: Vec<rlx_ir::Node> = g.nodes().to_vec();
91
92 for node in &nodes {
93 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
94 let new_id = match &node.op {
95 Op::Scan {
96 body,
97 length,
98 save_trajectory,
99 num_bcast,
100 num_xs,
101 ..
102 } => {
103 let nb = *num_bcast as usize;
104 let nx = *num_xs as usize;
105 let length = *length as usize;
106 debug_assert!(length >= 1, "unroll_scan: length must be ≥1");
107 let init = new_inputs[0];
108 let bcasts: Vec<NodeId> = new_inputs[1..1 + nb].to_vec();
109 let xs: Vec<NodeId> = new_inputs[1 + nb..1 + nb + nx].to_vec();
110 let carry_shape = out.node(init).shape.clone();
111
112 let mut carry = init;
113 let mut rows: Vec<NodeId> = Vec::with_capacity(length);
114 for t in 0..length {
115 let mut xs_t: Vec<NodeId> = Vec::with_capacity(nx);
117 for &x in &xs {
118 let xsh = out.node(x).shape.clone();
119 let ps_usize: Vec<usize> = xsh
120 .dims()
121 .iter()
122 .skip(1)
123 .map(|d| d.unwrap_static())
124 .collect();
125 let ps_i64: Vec<i64> = ps_usize.iter().map(|&d| d as i64).collect();
126 let mut nar_dims = vec![1usize];
127 nar_dims.extend(ps_usize.iter().copied());
128 let narrowed = out.add_node(
129 Op::Narrow {
130 axis: 0,
131 start: t,
132 len: 1,
133 },
134 vec![x],
135 Shape::new(&nar_dims, xsh.dtype()),
136 );
137 let reshaped = out.add_node(
138 Op::Reshape { new_shape: ps_i64 },
139 vec![narrowed],
140 Shape::new(&ps_usize, xsh.dtype()),
141 );
142 xs_t.push(reshaped);
143 }
144 let mut captures = vec![carry];
145 captures.extend(bcasts.iter().copied());
146 captures.extend(xs_t);
147 let outs = inline_subgraph_into_outputs(body, &captures, &mut out);
148 carry = outs[0];
149 if *save_trajectory {
150 rows.push(carry);
151 }
152 }
153
154 if *save_trajectory {
155 let cdims: Vec<usize> = carry_shape
156 .dims()
157 .iter()
158 .map(|d| d.unwrap_static())
159 .collect();
160 let mut rdims = vec![1usize];
161 rdims.extend(cdims.iter().copied());
162 let ri64: Vec<i64> = rdims.iter().map(|&d| d as i64).collect();
163 let reshaped_rows: Vec<NodeId> = rows
164 .iter()
165 .map(|&r| {
166 out.add_node(
167 Op::Reshape {
168 new_shape: ri64.clone(),
169 },
170 vec![r],
171 Shape::new(&rdims, carry_shape.dtype()),
172 )
173 })
174 .collect();
175 out.add_node(Op::Concat { axis: 0 }, reshaped_rows, node.shape.clone())
176 } else {
177 carry
178 }
179 }
180 _ => out.add_node(node.op.clone(), new_inputs, node.shape.clone()),
181 };
182 id_map.insert(node.id, new_id);
183 }
184 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|i| id_map[i]).collect();
185 out.set_outputs(new_outputs);
186 out
187}
188
189pub fn inline_if(g: Graph) -> Graph {
193 let mut out = Graph::new(g.name.clone());
194 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
195 let nodes: Vec<rlx_ir::Node> = g.nodes().to_vec();
196
197 for node in &nodes {
198 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
199 let new_id = match &node.op {
200 Op::If {
201 then_branch,
202 else_branch,
203 } => {
204 let captures: Vec<NodeId> = new_inputs[1..].to_vec();
205 let then_out = inline_subgraph_into(then_branch, &captures, &mut out);
206 let else_out = inline_subgraph_into(else_branch, &captures, &mut out);
207 let predicate = expand_to_shape(new_inputs[0], &node.shape, &mut out);
213 out.add_node(
214 Op::Where,
215 vec![predicate, then_out, else_out],
216 node.shape.clone(),
217 )
218 }
219 _ => out.add_node(node.op.clone(), new_inputs, node.shape.clone()),
220 };
221 id_map.insert(node.id, new_id);
222 }
223 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|i| id_map[i]).collect();
224 out.set_outputs(new_outputs);
225 out
226}
227
228pub fn unroll_while(g: Graph) -> Graph {
232 let mut out = Graph::new(g.name.clone());
233 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
234 let nodes: Vec<rlx_ir::Node> = g.nodes().to_vec();
235 let scalar_f32 = Shape::new(&[1], DType::F32);
236
237 for node in &nodes {
238 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
239 let new_id = match &node.op {
240 Op::While {
241 cond,
242 body,
243 max_iterations: Some(n),
244 ..
245 } => {
246 if new_inputs.is_empty() {
247 panic!(
248 "Op::While unroll: at least one \
249 loop-carried input required"
250 );
251 }
252 let one = out.add_node(
253 Op::Constant {
254 data: 1.0_f32.to_le_bytes().to_vec(),
255 },
256 vec![],
257 scalar_f32.clone(),
258 );
259 let mut active = one;
260 let mut carried = new_inputs;
261 for _ in 0..*n {
262 let cond_out = inline_subgraph_into(cond, &carried, &mut out);
263 let cond_f = cond_to_f32_mask(cond_out, &mut out);
264 let cond_shape = out.node(cond_f).shape.clone();
265 let active_lhs = expand_to_shape(active, &cond_shape, &mut out);
266 active = out.binary(BinaryOp::Mul, active_lhs, cond_f, cond_shape);
267
268 let body_outs = inline_subgraph_into_outputs(body, &carried, &mut out);
269 assert_eq!(
270 body_outs.len(),
271 carried.len(),
272 "Op::While: body output count must match loop-carried arity"
273 );
274 let mut next = Vec::with_capacity(carried.len());
275 for (body_out, &prev) in body_outs.iter().zip(carried.iter()) {
276 let shape = out.node(prev).shape.clone();
277 let mask = expand_to_shape(active, &shape, &mut out);
278 let merged = out.add_node(Op::Where, vec![mask, *body_out, prev], shape);
279 next.push(merged);
280 }
281 carried = next;
282 }
283 carried[0]
284 }
285 Op::While {
286 max_iterations: None,
287 ..
288 } => {
289 panic!(
290 "LowerControlFlow: Op::While requires \
291 max_iterations = Some(N) for unrolling. \
292 Either set a bounded max_iterations on the \
293 forward graph, or use the dynamic \
294 `rlx_runtime::subgraph::run_while` helper."
295 );
296 }
297 _ => out.add_node(node.op.clone(), new_inputs, node.shape.clone()),
298 };
299 id_map.insert(node.id, new_id);
300 }
301 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|i| id_map[i]).collect();
302 out.set_outputs(new_outputs);
303 out
304}
305
306fn cond_to_f32_mask(cond_out: NodeId, out: &mut Graph) -> NodeId {
309 let cond_shape = out.node(cond_out).shape.clone();
310 match cond_shape.dtype() {
311 DType::F32 => cond_out,
312 DType::Bool => {
313 let f32_shape = cond_shape.clone().with_dtype(DType::F32);
314 let i32_shape = cond_shape.with_dtype(DType::I32);
315 let as_i32 = out.add_node(Op::Cast { to: DType::I32 }, vec![cond_out], i32_shape);
316 out.add_node(Op::Cast { to: DType::F32 }, vec![as_i32], f32_shape)
317 }
318 _ => out.add_node(
319 Op::Cast { to: DType::F32 },
320 vec![cond_out],
321 cond_shape.with_dtype(DType::F32),
322 ),
323 }
324}
325
326fn expand_to_shape(src: NodeId, target: &rlx_ir::Shape, out: &mut Graph) -> NodeId {
331 let src_shape = out.node(src).shape.clone();
332 let src_n = src_shape
333 .dims()
334 .iter()
335 .filter_map(|d| match d {
336 Dim::Static(n) => Some(*n),
337 _ => None,
338 })
339 .product::<usize>();
340 let tgt_n = target
341 .dims()
342 .iter()
343 .filter_map(|d| match d {
344 Dim::Static(n) => Some(*n),
345 _ => None,
346 })
347 .product::<usize>();
348 if src_shape.dims() == target.dims() {
349 return src;
350 }
351 let target_dims_i64: Vec<i64> = target
352 .dims()
353 .iter()
354 .map(|d| match d {
355 Dim::Static(n) => *n as i64,
356 _ => -1,
357 })
358 .collect();
359 let src_rank = src_shape.rank();
362 let tgt_rank = target.dims().len();
363 let to_expand = if src_rank < tgt_rank {
364 let mut padded_dims: Vec<Dim> = std::iter::repeat_n(Dim::Static(1), tgt_rank - src_rank)
365 .chain(src_shape.dims().iter().copied())
366 .collect();
367 let _ = src_n;
369 let _ = tgt_n;
370 let dtype = src_shape.dtype();
371 let pad_dims_i64: Vec<i64> = padded_dims
372 .iter()
373 .map(|d| match d {
374 Dim::Static(n) => *n as i64,
375 _ => -1,
376 })
377 .collect();
378 let pad_shape = rlx_ir::Shape::from_dims(&padded_dims, dtype);
380 padded_dims.clear();
381 out.reshape(src, pad_dims_i64, pad_shape)
382 } else {
383 src
384 };
385 out.add_node(
386 Op::Expand {
387 target_shape: target_dims_i64,
388 },
389 vec![to_expand],
390 target.clone(),
391 )
392}
393
394pub fn inline_subgraph_into_outputs(
397 sub: &Graph,
398 captures: &[NodeId],
399 out: &mut Graph,
400) -> Vec<NodeId> {
401 let mut sub_to_parent: HashMap<NodeId, NodeId> = HashMap::new();
402 let mut input_idx = 0usize;
403 for sub_node in sub.nodes() {
404 let new_id = match &sub_node.op {
405 Op::Input { .. } => {
406 let parent_id = captures[input_idx];
407 input_idx += 1;
408 parent_id
409 }
410 _ => {
411 let new_inputs: Vec<NodeId> =
412 sub_node.inputs.iter().map(|i| sub_to_parent[i]).collect();
413 out.add_node(sub_node.op.clone(), new_inputs, sub_node.shape.clone())
414 }
415 };
416 sub_to_parent.insert(sub_node.id, new_id);
417 }
418 assert_eq!(
419 input_idx,
420 captures.len(),
421 "Op::While/If sub-graph: {} Op::Input nodes but {} captures",
422 input_idx,
423 captures.len()
424 );
425 sub.outputs.iter().map(|o| sub_to_parent[o]).collect()
426}
427
428pub fn inline_subgraph_into(sub: &Graph, captures: &[NodeId], out: &mut Graph) -> NodeId {
432 inline_subgraph_into_outputs(sub, captures, out)[0]
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use rlx_ir::op::{Activation, BinaryOp};
439 use rlx_ir::{DType, Shape};
440
441 #[test]
442 fn lower_control_flow_pass_handles_both_if_and_while() {
443 let s = Shape::new(&[2], DType::F32);
444
445 let mut then_g = Graph::new("th");
446 let ti = then_g.input("c", s.clone());
447 let to = then_g.activation(Activation::Relu, ti, s.clone());
448 then_g.set_outputs(vec![to]);
449 let mut else_g = Graph::new("el");
450 let ei = else_g.input("c", s.clone());
451 let eo = else_g.activation(Activation::Sigmoid, ei, s.clone());
452 else_g.set_outputs(vec![eo]);
453
454 let mut body_g = Graph::new("body");
455 let bi = body_g.input("c", s.clone());
456 let bo = body_g.binary(BinaryOp::Mul, bi, bi, s.clone());
457 body_g.set_outputs(vec![bo]);
458 let mut cond_g = Graph::new("cond");
459 let ci = cond_g.input("c", s.clone());
460 cond_g.set_outputs(vec![ci]);
461
462 let mut g = Graph::new("parent");
463 let x = g.input("x", s.clone());
464 let pred = g.input("p", Shape::new(&[1], DType::F32));
465 let if_out = g.add_node(
466 Op::If {
467 then_branch: Box::new(then_g),
468 else_branch: Box::new(else_g),
469 },
470 vec![pred, x],
471 s.clone(),
472 );
473 let w_out = g.add_node(
474 Op::While {
475 cond: Box::new(cond_g),
476 body: Box::new(body_g),
477 max_iterations: Some(2),
478 },
479 vec![if_out],
480 s.clone(),
481 );
482 g.set_outputs(vec![w_out]);
483
484 let lowered = LowerControlFlow.run(g);
485 let has_if = lowered
486 .nodes()
487 .iter()
488 .any(|n| matches!(n.op, Op::If { .. }));
489 let has_while = lowered
490 .nodes()
491 .iter()
492 .any(|n| matches!(n.op, Op::While { .. }));
493 assert!(
494 !has_if && !has_while,
495 "LowerControlFlow should erase both If and While"
496 );
497 let n_where = lowered
500 .nodes()
501 .iter()
502 .filter(|n| matches!(n.op, Op::Where))
503 .count();
504 let n_mul = lowered
505 .nodes()
506 .iter()
507 .filter(|n| matches!(n.op, Op::Binary(BinaryOp::Mul)))
508 .count();
509 assert_eq!(
510 n_where, 3,
511 "expected 1 Where from If + 2 from While (N=2, 1 carry)"
512 );
513 assert_eq!(
514 n_mul, 4,
515 "expected 2 body Mul + 2 active*cond_f Mul from While (N=2)"
516 );
517 }
518
519 #[test]
520 fn unroll_while_multi_carry_cond_freezes_updates() {
521 let v_shape = Shape::new(&[2], DType::F32);
522 let s_shape = Shape::new(&[1], DType::F32);
523
524 let mut body = Graph::new("body");
525 let v_in = body.input("v", v_shape.clone());
526 let s_in = body.input("s", s_shape.clone());
527 let one = body.add_node(
528 Op::Constant {
529 data: 1.0_f32.to_le_bytes().to_vec(),
530 },
531 vec![],
532 s_shape.clone(),
533 );
534 let v_out = body.binary(BinaryOp::Add, v_in, one, v_shape.clone());
535 body.set_outputs(vec![v_out, s_in]);
536
537 let mut cond = Graph::new("cond");
538 let v_c = cond.input("v", v_shape.clone());
539 let _s_c = cond.input("s", s_shape.clone());
540 let ten = cond.add_node(
541 Op::Constant {
542 data: 10.0_f32.to_le_bytes().to_vec(),
543 },
544 vec![],
545 s_shape.clone(),
546 );
547 let lt = cond.add_node(
548 Op::Compare(rlx_ir::op::CmpOp::Lt),
549 vec![v_c, ten],
550 Shape::new(&[1], DType::Bool),
551 );
552 cond.set_outputs(vec![lt]);
553
554 let mut g = Graph::new("parent");
555 let v0 = g.input("v0", v_shape.clone());
556 let s0 = g.input("s0", s_shape.clone());
557 let w = g.add_node(
558 Op::While {
559 cond: Box::new(cond),
560 body: Box::new(body),
561 max_iterations: Some(3),
562 },
563 vec![v0, s0],
564 v_shape.clone(),
565 );
566 g.set_outputs(vec![w]);
567
568 let lowered = unroll_while(g);
569 assert!(
570 !lowered
571 .nodes()
572 .iter()
573 .any(|n| matches!(n.op, Op::While { .. })),
574 "While should be erased"
575 );
576 let n_where = lowered
577 .nodes()
578 .iter()
579 .filter(|n| matches!(n.op, Op::Where))
580 .count();
581 assert_eq!(n_where, 6, "expected 3 iters × 2 carries Where masks");
582 }
583
584 #[test]
585 fn unroll_while_squares_on_cpu_thunks() {
586 let s = Shape::new(&[2], DType::F32);
587 let mut body_g = Graph::new("body");
588 let bi = body_g.input("c", s.clone());
589 let bo = body_g.binary(BinaryOp::Mul, bi, bi, s.clone());
590 body_g.set_outputs(vec![bo]);
591 let mut cond_g = Graph::new("cond");
592 let ci = cond_g.input("c", s.clone());
593 cond_g.set_outputs(vec![ci]);
594
595 let mut g = Graph::new("while_test");
596 let x = g.input("x", s.clone());
597 let y = g.add_node(
598 Op::While {
599 cond: Box::new(cond_g),
600 body: Box::new(body_g),
601 max_iterations: Some(3),
602 },
603 vec![x],
604 s.clone(),
605 );
606 g.set_outputs(vec![y]);
607
608 let lowered = unroll_while(g);
609 assert!(
610 !lowered
611 .nodes()
612 .iter()
613 .any(|n| matches!(n.op, Op::While { .. }))
614 );
615
616 let x_id = lowered
617 .nodes()
618 .iter()
619 .find(|n| matches!(&n.op, Op::Input { name, .. } if name == "x"))
620 .expect("lowered graph missing input x")
621 .id;
622 let plan = rlx_opt::memory::plan_memory(&lowered);
623 let mut arena = rlx_cpu::arena::Arena::from_plan(plan);
624 let sched = rlx_cpu::thunk::compile_thunks(&lowered, &arena);
625 for node in lowered.nodes() {
626 if let Op::Constant { data } = &node.op
627 && arena.has_buffer(node.id)
628 && !data.is_empty()
629 {
630 let buf = arena.slice_mut(node.id);
631 let n_floats = data.len() / 4;
632 let n = buf.len().min(n_floats);
633 for i in 0..n {
634 let bytes = [
635 data[i * 4],
636 data[i * 4 + 1],
637 data[i * 4 + 2],
638 data[i * 4 + 3],
639 ];
640 buf[i] = f32::from_le_bytes(bytes);
641 }
642 }
643 }
644 let x_off = arena.byte_offset(x_id);
645 let out_id = lowered.outputs[0];
646 let out_off = arena.byte_offset(out_id);
647 let buf = arena.raw_buf_mut();
648 unsafe {
649 let px = buf.as_mut_ptr().add(x_off) as *mut f32;
650 *px.add(0) = 2.0;
651 *px.add(1) = 3.0;
652 }
653 rlx_cpu::thunk::execute_thunks(&sched, arena.raw_buf_mut());
654 let got: Vec<f32> = unsafe {
655 let p = arena.raw_buf().as_ptr().add(out_off) as *const f32;
656 vec![*p.add(0), *p.add(1)]
657 };
658 let want = [256.0_f32, 6561.0_f32];
659 for (i, (&a, &b)) in got.iter().zip(&want).enumerate() {
660 assert!(
661 (a - b).abs() < 1e-3,
662 "unrolled while[{i}]: got {a} want {b}"
663 );
664 }
665 }
666}