Skip to main content

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}