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