vyre_foundation/execution_plan/fusion/mod.rs
1#![allow(clippy::unwrap_used)]
2//! Fuse multiple independent Programs into a single combined Program.
3//!
4//! This is the cross-dispatch fusion layer that the megakernel builder and
5//! rule-composition pipeline use to collapse sibling dispatches into one
6//! kernel body. It is **not** the expression-level fusion pass
7//! (`optimizer::passes::fusion`) - that pass lives inside one Program.
8//!
9//! The module splits into:
10//! - `mod.rs`: crate-level attribute + error types + module decls/re-exports
11//! - `fuse.rs`: `fuse_programs` family + multi-program implementation
12//! - `collectors.rs`: `collect_*_targets_*` walkers
13//! - `divergence.rs`: divergence + invocation-gate analysis
14//! - `tests/`: full proptest + unit-test suite
15//!
16//! # Safety invariants
17//!
18//! * Every buffer name that appears in more than one arm is treated as the
19//! *same* physical GPU buffer. The caller must ensure this is intentional.
20//! * Access-mode upgrades are applied automatically (`ReadOnly` -> `ReadWrite`)
21//! when any arm needs to write.
22//! * A `Node::Barrier` is inserted between arms when a later arm writes a
23//! buffer that an earlier arm reads, preventing write-after-read corruption.
24//! * Programs marked `non_composable_with_self` cannot be fused with another
25//! copy of the same `entry_op_id`.
26
27mod alpha_rename;
28mod collectors;
29mod divergence;
30mod fuse;
31
32#[cfg(test)]
33mod tests;
34
35pub use fuse::{fuse_programs, fuse_programs_vec, merge_programs_shared};
36
37/// Error returned when a fusion batch cannot be combined safely.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum FusionError {
41 /// Two copies of a non-composable parser were placed in the same batch.
42 SelfAliasing(FusionSelfAliasingError),
43 /// A cross-arm buffer alias was detected that cannot be fixed by a
44 /// barrier (e.g. both arms write the same buffer without an intervening
45 /// read-only phase).
46 Aliasing(FusionAliasingError),
47 /// The fused launch geometry would over-dispatch the largest arm by
48 /// more than the shared scheduling policy allows. Caller should fall back
49 /// to per-arm dispatch or split the batch.
50 OverDispatch(FusionOverDispatchError),
51 /// An arm whose correctness depends on its own workgroup geometry was
52 /// asked to run under the widened fused geometry.
53 WorkgroupGeometry(FusionWorkgroupGeometryError),
54}
55
56impl std::fmt::Display for FusionError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 FusionError::SelfAliasing(e) => write!(f, "{e}"),
60 FusionError::Aliasing(e) => write!(f, "{e}"),
61 FusionError::OverDispatch(e) => write!(f, "{e}"),
62 FusionError::WorkgroupGeometry(e) => write!(f, "{e}"),
63 }
64 }
65}
66
67/// An arm that reasons about its own workgroup cannot run under a wider one.
68///
69/// The fused launch takes the axis-wise maximum of the arms' workgroup sizes,
70/// which is harmless for an arm whose invocations are independent. It is not
71/// harmless for an arm that synchronizes its workgroup or keeps state in
72/// workgroup memory: a workgroup barrier has to be reached by every invocation
73/// in the workgroup, and an arm written for 4 invocations guards its body so
74/// that the other 252 skip it. The barrier is then non-uniform, which is
75/// undefined, and the workgroup buffers are sized for the narrow geometry.
76///
77/// The observed symptom was an inclusive prefix scan built for 4 elements,
78/// fused behind a 256-wide elementwise arm, returning the wrong last lane on
79/// roughly one dispatch in ten.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct FusionWorkgroupGeometryError {
82 /// Index of the arm whose geometry would change.
83 pub arm: usize,
84 /// The workgroup size that arm was built for.
85 pub arm_workgroup: [u32; 3],
86 /// The workgroup size the fused program would run it under.
87 pub fused_workgroup: [u32; 3],
88 /// What in the arm makes the widening unsafe.
89 pub reason: &'static str,
90 /// Actionable fix hint.
91 pub fix: &'static str,
92}
93
94impl std::fmt::Display for FusionWorkgroupGeometryError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(
97 f,
98 "fusion would run arm {} under workgroup {:?} instead of the {:?} it was built for, and that arm {}. Fix: {}",
99 self.arm, self.fused_workgroup, self.arm_workgroup, self.reason, self.fix
100 )
101 }
102}
103
104/// Axis-wise workgroup-max would over-dispatch far above any single arm.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct FusionOverDispatchError {
107 /// Total threads required by the largest single arm.
108 pub max_arm_threads: u64,
109 /// Total threads the fused launch geometry would request.
110 pub fused_threads: u64,
111 /// Actionable fix hint.
112 pub fix: &'static str,
113}
114
115impl std::fmt::Display for FusionOverDispatchError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 write!(
118 f,
119 "fusion would over-dispatch: fused geometry launches {} threads vs largest single arm {}. Fix: {}",
120 self.fused_threads, self.max_arm_threads, self.fix
121 )
122 }
123}
124
125impl std::error::Error for FusionError {}
126
127/// Two copies of the same parser appeared in one fusion batch.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct FusionSelfAliasingError {
130 /// Operation id shared by both programs.
131 pub op_id: String,
132 /// Actionable fix hint.
133 pub fix: &'static str,
134}
135
136impl std::fmt::Display for FusionSelfAliasingError {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(
139 f,
140 "fusion self-aliasing on op_id `{}`: two copies of a non-composable parser were fused. Fix: {}",
141 self.op_id, self.fix
142 )
143 }
144}
145
146/// Cross-arm buffer access hazard that cannot be repaired automatically.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct FusionAliasingError {
149 /// Buffer involved in the hazard.
150 pub buffer_name: String,
151 /// Index of the arm that reads the buffer.
152 pub read_arm: usize,
153 /// Index of the arm that writes the buffer.
154 pub write_arm: usize,
155 /// Actionable fix hint.
156 pub fix_hint: &'static str,
157}
158
159impl std::fmt::Display for FusionAliasingError {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(
162 f,
163 "fusion aliasing on buffer `{}`: arm {} reads and arm {} writes without a barrier. Fix: {}",
164 self.buffer_name, self.read_arm, self.write_arm, self.fix_hint
165 )
166 }
167}