Skip to main content

vyre_primitives/graph/
level_wave.rs

1//! `level_wave_program`  -  GPU-resident depth-wave dispatcher for
2//! bottom-up callee-before-caller computations.
3//!
4//! Semantically distinct from `fixpoint::persistent_fixpoint`:
5//! - **persistent_fixpoint**: re-run a transfer step until convergence.
6//!   No depth ordering  -  every lane runs the step every iteration.
7//! - **level_wave**: deterministic ordered traversal. Each lane runs
8//!   the step only when `current_depth == depth`lane``. Used for
9//!   bottom-up summary computations where children must complete
10//!   before parents.
11//!
12//! ## LEGO discipline
13//!
14//! Composes:
15//! - [`crate::graph::toposort::toposort()`]  -  CPU reference for the depth
16//!   assignment (caller computes `depth[node]` from the topological
17//!   ordering before invoking this primitive).
18//! - `Node::Loop` (vyre-foundation IR primitive)  -  outer per-depth
19//!   loop.
20//! - `Node::Barrier { ordering: vyre_foundation::MemoryOrdering::SeqCst }`  -  synchronisation between depth waves.
21//! - `Expr::eq` + `Node::if_then`  -  depth predicate per lane.
22//!
23//! No new sub-op invented. The caller composes its own per-lane work
24//! body; this primitive provides the wave harness.
25//!
26//! ## Composition contract
27//!
28//! Caller supplies:
29//!
30//! - `depth_buf`: per-node depth bitset (u32 per lane). The lane
31//!   reads its own depth and gates its work on equality with the
32//!   current wave depth.
33//! - `step_body`: caller-provided IR body that runs ONE node's work.
34//!   Reads `current_depth` and `depth_buf`lane``; the
35//!   level_wave_program guards the body in `if depth == current`
36//!   already, so the body itself doesn't need to re-check.
37//! - `max_depth`: maximum depth value in the topology.
38//!
39//! Caller receives a `Program` that runs every lane at every depth wave
40//! from 0..max_depth. Single-workgroup waves use one compact loop.
41//! Multi-workgroup waves expose top-level `GridSync` boundaries so
42//! backends without native grid barriers can split the traversal into
43//! launch-separated depth waves.
44
45use std::sync::Arc;
46
47use vyre_foundation::ir::model::expr::Ident;
48use vyre_foundation::ir::{
49    BufferAccess, BufferDecl, DataType, Expr, MemoryOrdering, Node, Program,
50};
51
52/// Canonical op id.
53pub const OP_ID: &str = "vyre-primitives::graph::level_wave";
54/// Workgroup shape for per-node depth-wave traversal.
55pub const LEVEL_WAVE_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
56
57/// Dispatch grid that covers every level-wave lane.
58#[must_use]
59pub const fn level_wave_dispatch_grid(lane_count: u32) -> [u32; 3] {
60    let blocks = lane_count.div_ceil(LEVEL_WAVE_WORKGROUP_SIZE[0]);
61    [if blocks == 0 { 1 } else { blocks }, 1, 1]
62}
63
64fn depth_wave_body(
65    step_body: Vec<Node>,
66    depth_buf: &str,
67    depth: Expr,
68    lane_count: u32,
69) -> Vec<Node> {
70    let lane = Expr::InvocationId { axis: 0 };
71    // The range check MUST control-flow-nest the depth load, not `Expr::and` it:
72    // `and(lane < lane_count, load(depth_buf, lane) == depth)` evaluates BOTH operands
73    // (the IR has no short-circuit), so `load(depth_buf, lane)` reads `depth_buf` for the
74    // whole-workgroup lanes a real GPU fires past `lane_count`: an OOB read the reference
75    // silently masks but hardware faults on (the ssa_dominance_scan gather-class bug,
76    // BACKLOG BUG-level-wave-depth-guard-eager-oob-load). Nesting means the load only runs
77    // when `lane < lane_count`.
78    vec![Node::if_then(
79        Expr::lt(lane.clone(), Expr::u32(lane_count)),
80        vec![Node::if_then(
81            Expr::eq(Expr::load(depth_buf, lane), depth),
82            step_body,
83        )],
84    )]
85}
86
87/// Build a Program that runs `step_body` per lane in
88/// depth-ordered waves.
89///
90/// Each lane reads `depth_buf[invocation_id]`. The kernel walks
91/// `current_depth = 0..max_depth`. At each depth, every lane whose
92/// depth equals `current_depth` executes `step_body`. A `Barrier` is
93/// emitted between depths so the caller can rely on depth-N effects
94/// being globally visible before depth-N+1 begins.
95///
96/// # Parameters
97///
98/// - `step_body`: caller's per-lane work body. Free to read/write
99///   any buffer the caller declares; it does NOT need to re-check
100///   the depth predicate (the wrapper does that).
101/// - `depth_buf`: buffer-name holding per-lane depth (u32). Read-only.
102/// - `max_depth`: number of waves to execute.
103/// - `lane_count`: total number of lanes in the dispatch grid.
104#[must_use]
105pub fn level_wave_program(
106    step_body: Vec<Node>,
107    depth_buf: &str,
108    max_depth: u32,
109    lane_count: u32,
110) -> Program {
111    level_wave_program_with_buffers(step_body, depth_buf, Vec::new(), max_depth, lane_count)
112}
113
114/// Like [`level_wave_program`], but declares `extra_buffers` after the depth
115/// buffer so the `step_body` can read/write the caller's own storage.
116///
117/// `depth_buf` is bound at index 0; every entry in `extra_buffers` MUST carry
118/// a distinct binding index `>= 1` (the caller owns the binding layout its
119/// `step_body` references). This is the composition point used by
120/// depth-ordered evaluators (e.g. `sum_product_evaluate_leveled`) that need
121/// their own inputs/outputs visible inside the per-lane wave body while still
122/// getting the ONE-PLACE depth-wave harness + inter-wave barriers.
123#[must_use]
124pub fn level_wave_program_with_buffers(
125    step_body: Vec<Node>,
126    depth_buf: &str,
127    extra_buffers: Vec<BufferDecl>,
128    max_depth: u32,
129    lane_count: u32,
130) -> Program {
131    let body = if lane_count <= LEVEL_WAVE_WORKGROUP_SIZE[0] {
132        vec![Node::loop_for(
133            "__lw_depth__",
134            Expr::u32(0),
135            Expr::u32(max_depth),
136            {
137                let mut loop_body = depth_wave_body(
138                    step_body.clone(),
139                    depth_buf,
140                    Expr::var("__lw_depth__"),
141                    lane_count,
142                );
143                loop_body.push(Node::Barrier {
144                    ordering: MemoryOrdering::SeqCst,
145                });
146                loop_body
147            },
148        )]
149    } else {
150        let mut waves = Vec::with_capacity(max_depth.saturating_mul(2) as usize);
151        for depth in 0..max_depth {
152            waves.extend(depth_wave_body(
153                step_body.clone(),
154                depth_buf,
155                Expr::u32(depth),
156                lane_count,
157            ));
158            if depth + 1 < max_depth {
159                waves.push(Node::Barrier {
160                    ordering: MemoryOrdering::GridSync,
161                });
162            }
163        }
164        waves
165    };
166
167    let mut buffers =
168        vec![
169            BufferDecl::storage(depth_buf, 0, BufferAccess::ReadOnly, DataType::U32)
170                .with_count(lane_count),
171        ];
172    buffers.extend(extra_buffers);
173
174    Program::wrapped(
175        buffers,
176        LEVEL_WAVE_WORKGROUP_SIZE,
177        vec![Node::Region {
178            generator: Ident::from(OP_ID),
179            source_region: None,
180            body: Arc::new(body),
181        }],
182    )
183}
184
185/// CPU oracle. Iterates depth waves on the host and calls
186/// `step_for_lane(lane, depth)` exactly once per (lane, depth ==
187/// depth_for_lane`lane`). Used by the conformance harness to verify
188/// that the GPU kernel respects the depth ordering.
189#[cfg(any(test, feature = "cpu-parity"))]
190pub fn cpu_ref<F>(depths: &[u32], max_depth: u32, mut step_for_lane: F)
191where
192    F: FnMut(u32, u32),
193{
194    for current_depth in 0..max_depth {
195        for (lane_idx, lane_depth) in depths.iter().enumerate() {
196            if *lane_depth == current_depth {
197                step_for_lane(lane_idx as u32, current_depth);
198            }
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn entry_region_body(program: &Program) -> &[Node] {
208        match &program.entry()[0] {
209            Node::Region { body, .. } => body.as_slice(),
210            other => panic!("expected wrapped level-wave region, got {other:?}"),
211        }
212    }
213
214    fn contains_grid_sync(nodes: &[Node]) -> bool {
215        nodes.iter().any(|node| match node {
216            Node::Barrier {
217                ordering: MemoryOrdering::GridSync,
218            } => true,
219            Node::Block(children) | Node::Loop { body: children, .. } => {
220                contains_grid_sync(children)
221            }
222            Node::If {
223                then, otherwise, ..
224            } => contains_grid_sync(then) || contains_grid_sync(otherwise),
225            Node::Region { body, .. } => contains_grid_sync(body),
226            _ => false,
227        })
228    }
229
230    fn contains_loop(nodes: &[Node]) -> bool {
231        nodes.iter().any(|node| match node {
232            Node::Loop { .. } => true,
233            Node::Block(children) => contains_loop(children),
234            Node::If {
235                then, otherwise, ..
236            } => contains_loop(then) || contains_loop(otherwise),
237            Node::Region { body, .. } => contains_loop(body),
238            _ => false,
239        })
240    }
241
242    #[test]
243    fn cpu_ref_visits_each_lane_at_its_depth() {
244        let depths = vec![0u32, 1, 2, 1, 0];
245        let mut visits: Vec<(u32, u32)> = Vec::new();
246        cpu_ref(&depths, 3, |lane, depth| visits.push((lane, depth)));
247        // Every lane visited exactly once, in depth order.
248        assert_eq!(visits.len(), depths.len());
249        for (idx, &(lane, depth)) in visits.iter().enumerate() {
250            assert_eq!(depth, depths[lane as usize]);
251            // Visits are sorted by depth (waves).
252            if idx > 0 {
253                assert!(depth >= visits[idx - 1].1);
254            }
255        }
256    }
257
258    #[test]
259    fn dispatch_grid_packs_lane_count_into_workgroups() {
260        assert_eq!(level_wave_dispatch_grid(0), [1, 1, 1]);
261        assert_eq!(level_wave_dispatch_grid(1), [1, 1, 1]);
262        assert_eq!(level_wave_dispatch_grid(256), [1, 1, 1]);
263        assert_eq!(level_wave_dispatch_grid(257), [2, 1, 1]);
264        assert_eq!(level_wave_dispatch_grid(1029), [5, 1, 1]);
265    }
266
267    #[test]
268    fn program_shape_matches_contract() {
269        let step = vec![Node::store("out", Expr::u32(0), Expr::u32(1))];
270        let program = level_wave_program(step, "depths", 8, 64);
271        assert_eq!(program.workgroup_size(), LEVEL_WAVE_WORKGROUP_SIZE);
272        assert!(
273            program.buffers.iter().any(|b| b.name() == "depths"),
274            "depth buffer must be declared"
275        );
276        assert!(!contains_grid_sync(entry_region_body(&program)));
277    }
278
279    #[test]
280    fn program_with_buffers_declares_depth_then_caller_buffers() {
281        let step = vec![Node::store("out", Expr::u32(0), Expr::u32(1))];
282        let extra = vec![
283            BufferDecl::storage("kinds", 1, BufferAccess::ReadOnly, DataType::U32).with_count(4),
284            BufferDecl::storage("out", 2, BufferAccess::ReadWrite, DataType::U32).with_count(4),
285        ];
286        let program = level_wave_program_with_buffers(step, "depths", extra, 8, 4);
287        let names: Vec<&str> = program.buffers.iter().map(|b| b.name()).collect();
288        assert_eq!(
289            names,
290            vec!["depths", "kinds", "out"],
291            "depth buffer is bound first (index 0), then the caller's extra buffers in order"
292        );
293        // The empty-extra delegation path (`level_wave_program`) must declare only the depth buffer.
294        let plain = level_wave_program(
295            vec![Node::store("out", Expr::u32(0), Expr::u32(1))],
296            "depths",
297            8,
298            4,
299        );
300        assert_eq!(
301            plain.buffers.len(),
302            1,
303            "plain level-wave declares only depths"
304        );
305    }
306
307    #[test]
308    fn multi_block_program_uses_top_level_grid_sync_waves() {
309        let step = vec![Node::store(
310            "out",
311            Expr::InvocationId { axis: 0 },
312            Expr::u32(1),
313        )];
314        let program = level_wave_program(step, "depths", 4, LEVEL_WAVE_WORKGROUP_SIZE[0] + 1);
315        let body = entry_region_body(&program);
316        assert!(contains_grid_sync(body));
317        assert!(
318            !contains_loop(body),
319            "multi-block level-wave must expose GridSync at split-visible depth-wave boundaries"
320        );
321        assert_eq!(
322            body.iter()
323                .filter(|node| matches!(
324                    node,
325                    Node::Barrier {
326                        ordering: MemoryOrdering::GridSync,
327                    }
328                ))
329                .count(),
330            3
331        );
332    }
333}