Skip to main content

vyre_runtime/megakernel/planner/
barriers.rs

1//! Runtime megakernel barrier elision for independent arm chains.
2//!
3//! Foundation coalesces adjacent barriers. This pass handles the runtime
4//! composition case: `Block/Region, Barrier, Block/Region` sequences emitted
5//! while stitching megakernel arms. A barrier is removed only when both
6//! neighboring arms have known buffer effects and no same-buffer read/write or
7//! write/write dependency crosses the barrier.
8
9use std::sync::Arc;
10
11use smallvec::SmallVec;
12use vyre_foundation::ir::{Expr, Ident, Node, Program};
13
14/// Report returned by [`elide_value_flow_barriers`].
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub struct BarrierElisionReport {
17    /// Number of `Node::Barrier` values removed.
18    pub removed: usize,
19}
20
21/// Remove barriers between independent megakernel arms.
22///
23/// The rewrite is intentionally conservative. It only removes a barrier when
24/// the previous and next sibling are explicit arm containers (`Block` or
25/// `Region`) and their recursively collected buffer effects cannot conflict.
26///
27/// INFALLIBLE by construction (Law 10): every working buffer is sized by the
28/// program's IR node count, kernel STRUCTURE (the fused arms + scan loop), NOT
29/// input/catalog/data-scaled, so it is bounded and reserved with
30/// `Vec::with_capacity`, exactly like the sibling `rule_catalog` host build.
31/// There is therefore no fallible-staging error to swallow, so the pass ALWAYS
32/// elides; the previous `try_*` + `Err(_) => fallback` silently shipped the
33/// un-elided (slower) program on a staging-reserve failure, which this removes.
34#[must_use]
35pub fn elide_value_flow_barriers(program: Program) -> (Program, BarrierElisionReport) {
36    let mut report = BarrierElisionReport::default();
37    if !nodes_have_barrier(program.entry()) {
38        return (program, report);
39    }
40    let entry = rewrite_nodes(program.entry().to_vec(), &mut report);
41    let rewritten = if report.removed == 0 {
42        program
43    } else {
44        program.with_rewritten_entry(entry)
45    };
46    (rewritten, report)
47}
48
49fn nodes_have_barrier(nodes: &[Node]) -> bool {
50    nodes.iter().any(node_has_barrier)
51}
52
53fn node_has_barrier(node: &Node) -> bool {
54    match node {
55        Node::Barrier { .. } => true,
56        Node::If {
57            then, otherwise, ..
58        } => nodes_have_barrier(then) || nodes_have_barrier(otherwise),
59        Node::Loop { body, .. } | Node::Block(body) => nodes_have_barrier(body),
60        Node::Region { body, .. } => nodes_have_barrier(body),
61        _ => false,
62    }
63}
64
65fn rewrite_nodes(nodes: Vec<Node>, report: &mut BarrierElisionReport) -> Vec<Node> {
66    if !nodes_have_barrier(&nodes) {
67        return nodes;
68    }
69    let mut rewritten = Vec::with_capacity(nodes.len());
70    for node in nodes {
71        rewritten.push(rewrite_node(node, report));
72    }
73    elide_barrier_siblings(rewritten, report)
74}
75
76fn rewrite_node(node: Node, report: &mut BarrierElisionReport) -> Node {
77    match node {
78        Node::If {
79            cond,
80            then,
81            otherwise,
82        } => Node::If {
83            cond,
84            then: rewrite_nodes(then, report),
85            otherwise: rewrite_nodes(otherwise, report),
86        },
87        Node::Loop {
88            var,
89            from,
90            to,
91            body,
92        } => Node::Loop {
93            var,
94            from,
95            to,
96            body: rewrite_nodes(body, report),
97        },
98        Node::Block(body) => Node::Block(rewrite_nodes(body, report)),
99        Node::Region {
100            generator,
101            source_region,
102            body,
103        } => {
104            if !nodes_have_barrier(&body) {
105                Node::Region {
106                    generator,
107                    source_region,
108                    body,
109                }
110            } else {
111                Node::Region {
112                    generator,
113                    source_region,
114                    body: Arc::new(rewrite_nodes(arc_vec_into_vec(body), report)),
115                }
116            }
117        }
118        other => other,
119    }
120}
121
122fn elide_barrier_siblings(nodes: Vec<Node>, report: &mut BarrierElisionReport) -> Vec<Node> {
123    let mut out = Vec::with_capacity(nodes.len());
124    let mut iter = nodes.into_iter().peekable();
125    while let Some(node) = iter.next() {
126        if matches!(&node, Node::Barrier { .. }) {
127            if let (Some(left), Some(right)) = (out.last(), iter.peek()) {
128                if is_runtime_arm(left)
129                    && is_runtime_arm(right)
130                    && arms_are_independent(left, right)
131                {
132                    report.removed += 1;
133                    continue;
134                }
135            }
136        }
137        out.push(node);
138    }
139    out
140}
141
142/// Take ownership of an `Arc<Vec<T>>`'s contents without the shared `Arc`: the
143/// sole owner is moved out, otherwise the bounded inner `Vec` is cloned.
144fn arc_vec_into_vec<T: Clone>(body: Arc<Vec<T>>) -> Vec<T> {
145    match Arc::try_unwrap(body) {
146        Ok(nodes) => nodes,
147        Err(shared) => shared.as_ref().clone(),
148    }
149}
150
151fn is_runtime_arm(node: &Node) -> bool {
152    matches!(node, Node::Block(_) | Node::Region { .. })
153}
154
155fn arms_are_independent(left: &Node, right: &Node) -> bool {
156    let mut left_access = AccessSet::default();
157    let mut right_access = AccessSet::default();
158    collect_node_access(left, &mut left_access);
159    collect_node_access(right, &mut right_access);
160    !left_access.unknown && !right_access.unknown && !left_access.conflicts_with(&right_access)
161}
162
163#[derive(Debug, Default)]
164struct AccessSet<'a> {
165    reads: SmallVec<[&'a Ident; 8]>,
166    writes: SmallVec<[&'a Ident; 8]>,
167    unknown: bool,
168}
169
170impl<'a> AccessSet<'a> {
171    fn read(&mut self, buffer: &'a Ident) {
172        push_unique(&mut self.reads, buffer);
173    }
174
175    fn write(&mut self, buffer: &'a Ident) {
176        push_unique(&mut self.writes, buffer);
177    }
178
179    fn read_write(&mut self, buffer: &'a Ident) {
180        self.read(buffer);
181        self.write(buffer);
182    }
183
184    fn conflicts_with(&self, other: &Self) -> bool {
185        intersects(&self.writes, &other.reads)
186            || intersects(&self.reads, &other.writes)
187            || intersects(&self.writes, &other.writes)
188    }
189}
190
191fn push_unique<'a>(set: &mut SmallVec<[&'a Ident; 8]>, value: &'a Ident) {
192    if !set.iter().any(|existing| *existing == value) {
193        set.push(value);
194    }
195}
196
197fn intersects(left: &[&Ident], right: &[&Ident]) -> bool {
198    if left.len() <= right.len() {
199        left.iter()
200            .any(|value| right.iter().any(|other| other == value))
201    } else {
202        right
203            .iter()
204            .any(|value| left.iter().any(|other| other == value))
205    }
206}
207
208fn collect_node_access<'a>(node: &'a Node, out: &mut AccessSet<'a>) {
209    match node {
210        Node::Let { value, .. } | Node::Assign { value, .. } => collect_expr_access(value, out),
211        Node::Store {
212            buffer,
213            index,
214            value,
215        } => {
216            out.write(buffer);
217            collect_expr_access(index, out);
218            collect_expr_access(value, out);
219        }
220        Node::If {
221            cond,
222            then,
223            otherwise,
224        } => {
225            collect_expr_access(cond, out);
226            collect_nodes_access(then, out);
227            collect_nodes_access(otherwise, out);
228        }
229        Node::Loop { from, to, body, .. } => {
230            collect_expr_access(from, out);
231            collect_expr_access(to, out);
232            collect_nodes_access(body, out);
233        }
234        Node::IndirectDispatch { count_buffer, .. } => out.read(count_buffer),
235        Node::AsyncLoad {
236            source,
237            destination,
238            offset,
239            size,
240            ..
241        } => {
242            out.read(source);
243            out.write(destination);
244            collect_expr_access(offset, out);
245            collect_expr_access(size, out);
246        }
247        Node::AsyncStore {
248            source,
249            destination,
250            offset,
251            size,
252            ..
253        } => {
254            out.read(source);
255            out.write(destination);
256            collect_expr_access(offset, out);
257            collect_expr_access(size, out);
258        }
259        Node::AsyncWait { .. } | Node::Return | Node::Barrier { .. } | Node::Resume { .. } => {}
260        Node::Trap { address, .. } => {
261            collect_expr_access(address, out);
262            out.unknown = true;
263        }
264        Node::Block(body) => collect_nodes_access(body, out),
265        Node::Region { body, .. } => collect_nodes_access(body, out),
266        Node::Opaque(_) => out.unknown = true,
267        _ => out.unknown = true,
268    }
269}
270
271fn collect_nodes_access<'a>(nodes: &'a [Node], out: &mut AccessSet<'a>) {
272    for node in nodes {
273        collect_node_access(node, out);
274    }
275}
276
277fn collect_expr_access<'a>(expr: &'a Expr, out: &mut AccessSet<'a>) {
278    match expr {
279        Expr::Load { buffer, index } => {
280            out.read(buffer);
281            collect_expr_access(index, out);
282        }
283        Expr::BufLen { buffer } => out.read(buffer),
284        Expr::BinOp { left, right, .. } => {
285            collect_expr_access(left, out);
286            collect_expr_access(right, out);
287        }
288        Expr::UnOp { operand, .. } => collect_expr_access(operand, out),
289        Expr::Call { args, .. } => {
290            for arg in args {
291                collect_expr_access(arg, out);
292            }
293        }
294        Expr::Select {
295            cond,
296            true_val,
297            false_val,
298        } => {
299            collect_expr_access(cond, out);
300            collect_expr_access(true_val, out);
301            collect_expr_access(false_val, out);
302        }
303        Expr::Cast { value, .. } => collect_expr_access(value, out),
304        Expr::Fma { a, b, c } => {
305            collect_expr_access(a, out);
306            collect_expr_access(b, out);
307            collect_expr_access(c, out);
308        }
309        Expr::SubgroupBallot { cond } => collect_expr_access(cond, out),
310        Expr::SubgroupShuffle { value, lane } => {
311            collect_expr_access(value, out);
312            collect_expr_access(lane, out);
313        }
314        Expr::SubgroupReduce { value, .. } => collect_expr_access(value, out),
315        Expr::Atomic {
316            buffer,
317            index,
318            expected,
319            value,
320            ..
321        } => {
322            out.read_write(buffer);
323            collect_expr_access(index, out);
324            if let Some(expected) = expected {
325                collect_expr_access(expected, out);
326            }
327            collect_expr_access(value, out);
328        }
329        Expr::Opaque(_) => out.unknown = true,
330        Expr::LitU32(_)
331        | Expr::LitI32(_)
332        | Expr::LitF32(_)
333        | Expr::LitBool(_)
334        | Expr::Var(_)
335        | Expr::InvocationId { .. }
336        | Expr::WorkgroupId { .. }
337        | Expr::LocalId { .. }
338        | Expr::SubgroupLocalId
339        | Expr::SubgroupSize => {}
340        _ => out.unknown = true,
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType};
347
348    use super::*;
349
350    fn buffer(name: &str, binding: u32) -> BufferDecl {
351        BufferDecl::storage(name, binding, BufferAccess::ReadWrite, DataType::U32)
352    }
353
354    fn barrier_count(nodes: &[Node]) -> usize {
355        nodes
356            .iter()
357            .map(|node| match node {
358                Node::Barrier { .. } => 1,
359                Node::If {
360                    then, otherwise, ..
361                } => barrier_count(then) + barrier_count(otherwise),
362                Node::Loop { body, .. } | Node::Block(body) => barrier_count(body),
363                Node::Region { body, .. } => barrier_count(body),
364                _ => 0,
365            })
366            .sum()
367    }
368
369    fn store_count(nodes: &[Node]) -> usize {
370        nodes
371            .iter()
372            .map(|node| match node {
373                Node::Store { .. } => 1,
374                Node::If {
375                    then, otherwise, ..
376                } => store_count(then) + store_count(otherwise),
377                Node::Loop { body, .. } | Node::Block(body) => store_count(body),
378                Node::Region { body, .. } => store_count(body),
379                _ => 0,
380            })
381            .sum()
382    }
383
384    #[test]
385    fn removes_barrier_between_disjoint_runtime_arms() {
386        let program = Program::wrapped(
387            vec![buffer("a", 0), buffer("b", 1)],
388            [64, 1, 1],
389            vec![
390                Node::Block(vec![Node::store("a", Expr::u32(0), Expr::u32(1))]),
391                Node::barrier(),
392                Node::Block(vec![Node::store("b", Expr::u32(0), Expr::u32(2))]),
393            ],
394        );
395
396        let (rewritten, report) = elide_value_flow_barriers(program);
397
398        assert_eq!(report.removed, 1);
399        assert_eq!(barrier_count(rewritten.entry()), 0);
400    }
401
402    /// Law 10 / infallibility lock: a program with SEVERAL barriers between
403    /// pairwise-disjoint runtime arms must have EVERY such barrier elided in one
404    /// pass. The pass is infallible (its working buffers are sized by the bounded
405    /// IR node count, reserved with `Vec::with_capacity`), so it can never bail to
406    /// the old `Err(_) => fallback` that silently shipped the un-elided program
407    /// with these barriers still present. Three barriers between four disjoint
408    /// arms must all go (removed == 3, zero barriers left).
409    #[test]
410    fn elides_every_barrier_across_many_disjoint_arms_in_one_pass() {
411        let program = Program::wrapped(
412            vec![
413                buffer("a", 0),
414                buffer("b", 1),
415                buffer("c", 2),
416                buffer("d", 3),
417            ],
418            [64, 1, 1],
419            vec![
420                Node::Block(vec![Node::store("a", Expr::u32(0), Expr::u32(1))]),
421                Node::barrier(),
422                Node::Block(vec![Node::store("b", Expr::u32(0), Expr::u32(2))]),
423                Node::barrier(),
424                Node::Block(vec![Node::store("c", Expr::u32(0), Expr::u32(3))]),
425                Node::barrier(),
426                Node::Block(vec![Node::store("d", Expr::u32(0), Expr::u32(4))]),
427            ],
428        );
429
430        let (rewritten, report) = elide_value_flow_barriers(program);
431
432        assert_eq!(
433            report.removed, 3,
434            "all three disjoint-arm barriers must be elided"
435        );
436        assert_eq!(barrier_count(rewritten.entry()), 0);
437        // All four independent store arms must survive the rewrite (no arm dropped
438        // while elliding barriers, regardless of how `Program::wrapped` nests them).
439        assert_eq!(
440            store_count(rewritten.entry()),
441            4,
442            "all four independent store arms must survive the rewrite"
443        );
444    }
445
446    #[test]
447    fn no_barrier_program_is_returned_without_rewrite() {
448        let program = Program::wrapped(
449            vec![buffer("a", 0)],
450            [64, 1, 1],
451            vec![Node::Block(vec![Node::store(
452                "a",
453                Expr::u32(0),
454                Expr::u32(1),
455            )])],
456        );
457        let expected = program.clone();
458
459        let (rewritten, report) = elide_value_flow_barriers(program);
460
461        assert_eq!(report.removed, 0);
462        assert_eq!(
463            rewritten.fingerprint(),
464            expected.fingerprint(),
465            "Fix: barrier-free megakernel plans must avoid structural rewrites."
466        );
467    }
468
469    #[test]
470    fn keeps_barrier_when_next_arm_reads_previous_write() {
471        let program = Program::wrapped(
472            vec![buffer("a", 0)],
473            [64, 1, 1],
474            vec![
475                Node::Block(vec![Node::store("a", Expr::u32(0), Expr::u32(1))]),
476                Node::barrier(),
477                Node::Block(vec![Node::let_bind("x", Expr::load("a", Expr::u32(0)))]),
478            ],
479        );
480
481        let (rewritten, report) = elide_value_flow_barriers(program);
482
483        assert_eq!(report.removed, 0);
484        assert_eq!(barrier_count(rewritten.entry()), 1);
485    }
486
487    #[test]
488    fn keeps_barrier_around_unknown_opaque_arm() {
489        let program = Program::wrapped(
490            vec![buffer("a", 0), buffer("b", 1)],
491            [64, 1, 1],
492            vec![
493                Node::Block(vec![Node::Opaque(Arc::new(TestOpaqueNode))]),
494                Node::barrier(),
495                Node::Block(vec![Node::store("b", Expr::u32(0), Expr::u32(2))]),
496            ],
497        );
498
499        let (rewritten, report) = elide_value_flow_barriers(program);
500
501        assert_eq!(report.removed, 0);
502        assert_eq!(barrier_count(rewritten.entry()), 1);
503    }
504
505    #[derive(Debug)]
506    struct TestOpaqueNode;
507
508    impl vyre_foundation::ir::NodeExtension for TestOpaqueNode {
509        fn extension_kind(&self) -> &'static str {
510            "test.opaque"
511        }
512
513        fn debug_identity(&self) -> &str {
514            "test.opaque"
515        }
516
517        fn stable_fingerprint(&self) -> [u8; 32] {
518            [7; 32]
519        }
520
521        fn validate_extension(&self) -> Result<(), String> {
522            Ok(())
523        }
524
525        fn as_any(&self) -> &dyn std::any::Any {
526            self
527        }
528    }
529}