1use crate::specialization::SpecMap;
4
5#[derive(Debug, Clone)]
7pub struct DispatchShape {
8 pub id: &'static str,
10 pub workgroup_size: [u32; 3],
12 pub shared_memory_bytes: u32,
14 pub inputs: Vec<&'static str>,
16 pub outputs: Vec<&'static str>,
18 pub specs: SpecMap,
20}
21
22#[derive(Debug, Clone, Copy)]
24pub struct FusionCaps {
25 pub max_shared_memory_bytes: u32,
27 pub max_invocations_per_workgroup: u32,
29}
30
31impl Default for FusionCaps {
32 fn default() -> Self {
33 Self {
34 max_shared_memory_bytes: 16 * 1024,
35 max_invocations_per_workgroup: 256,
36 }
37 }
38}
39
40impl FusionCaps {
41 #[must_use]
43 pub const fn high_end() -> Self {
44 Self {
45 max_shared_memory_bytes: 128 * 1024,
46 max_invocations_per_workgroup: 1024,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum FusionDecision {
55 Accept,
57 WorkgroupSizeMismatch {
59 upstream: [u32; 3],
61 downstream: [u32; 3],
63 },
64 InvocationBudgetExceeded {
66 workgroup: [u32; 3],
68 invocations: u64,
70 cap: u32,
72 },
73 SharedMemoryBudget {
75 needed: u64,
77 cap: u32,
79 },
80 OutputConsumedElsewhere,
82 NoPipelineDependency,
84}
85
86pub struct FusionPass;
88
89impl FusionPass {
90 #[must_use]
92 pub fn decide(
93 upstream: &DispatchShape,
94 downstream: &DispatchShape,
95 caps: FusionCaps,
96 other_consumers: &[&str],
97 ) -> FusionDecision {
98 if upstream.workgroup_size != downstream.workgroup_size {
99 return FusionDecision::WorkgroupSizeMismatch {
100 upstream: upstream.workgroup_size,
101 downstream: downstream.workgroup_size,
102 };
103 }
104 let invocations = u128::from(upstream.workgroup_size[0])
105 * u128::from(upstream.workgroup_size[1])
106 * u128::from(upstream.workgroup_size[2]);
107 if invocations > u128::from(caps.max_invocations_per_workgroup) {
108 return FusionDecision::InvocationBudgetExceeded {
109 workgroup: upstream.workgroup_size,
110 invocations: u64::try_from(invocations).unwrap_or(u64::MAX),
111 cap: caps.max_invocations_per_workgroup,
112 };
113 }
114 let needed =
115 u64::from(upstream.shared_memory_bytes) + u64::from(downstream.shared_memory_bytes);
116 if needed > u64::from(caps.max_shared_memory_bytes) {
117 return FusionDecision::SharedMemoryBudget {
118 needed,
119 cap: caps.max_shared_memory_bytes,
120 };
121 }
122
123 let mut has_pipeline_dependency = false;
124 for output in &upstream.outputs {
125 if !downstream.inputs.iter().any(|input| input == output) {
126 continue;
127 }
128 has_pipeline_dependency = true;
129 if other_consumers.iter().any(|consumer| consumer == output) {
130 return FusionDecision::OutputConsumedElsewhere;
131 }
132 }
133 if !has_pipeline_dependency {
134 return FusionDecision::NoPipelineDependency;
135 }
136 FusionDecision::Accept
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn dispatch(
145 id: &'static str,
146 inputs: &[&'static str],
147 outputs: &[&'static str],
148 ) -> DispatchShape {
149 DispatchShape {
150 id,
151 workgroup_size: [64, 1, 1],
152 shared_memory_bytes: 1024,
153 inputs: inputs.to_vec(),
154 outputs: outputs.to_vec(),
155 specs: SpecMap::new(),
156 }
157 }
158
159 #[test]
160 fn straight_producer_consumer_fuses() {
161 let up = dispatch("load", &["in"], &["stage"]);
162 let down = dispatch("xor", &["stage"], &["out"]);
163 assert_eq!(
164 FusionPass::decide(&up, &down, FusionCaps::high_end(), &[]),
165 FusionDecision::Accept
166 );
167 }
168
169 #[test]
170 fn third_consumer_rejects() {
171 let up = dispatch("a", &[], &["x"]);
172 let down = dispatch("b", &["x"], &[]);
173 assert_eq!(
174 FusionPass::decide(&up, &down, FusionCaps::high_end(), &["x"]),
175 FusionDecision::OutputConsumedElsewhere
176 );
177 }
178
179 #[test]
180 fn workgroup_invocation_overflow_rejects_instead_of_wrapping_or_clamping() {
181 let mut up = dispatch("wide-a", &["in"], &["stage"]);
182 up.workgroup_size = [u32::MAX, u32::MAX, 2];
183 let mut down = dispatch("wide-b", &["stage"], &["out"]);
184 down.workgroup_size = up.workgroup_size;
185 assert_eq!(
186 FusionPass::decide(&up, &down, FusionCaps::high_end(), &[]),
187 FusionDecision::InvocationBudgetExceeded {
188 workgroup: up.workgroup_size,
189 invocations: u64::MAX,
190 cap: FusionCaps::high_end().max_invocations_per_workgroup,
191 }
192 );
193 }
194
195 #[test]
196 fn shared_memory_overflow_rejects_instead_of_appearing_under_cap() {
197 let mut up = dispatch("smem-a", &["in"], &["stage"]);
198 up.shared_memory_bytes = u32::MAX;
199 let mut down = dispatch("smem-b", &["stage"], &["out"]);
200 down.shared_memory_bytes = 1;
201 assert_eq!(
202 FusionPass::decide(&up, &down, FusionCaps::high_end(), &[]),
203 FusionDecision::SharedMemoryBudget {
204 needed: u64::from(u32::MAX) + 1,
205 cap: FusionCaps::high_end().max_shared_memory_bytes,
206 }
207 );
208 }
209
210 #[test]
215 fn invocation_budget_exceeded_returns_distinct_variant_not_workgroup_size_mismatch() {
216 let caps = FusionCaps {
218 max_shared_memory_bytes: 128 * 1024,
219 max_invocations_per_workgroup: 64,
220 };
221 let mut up = dispatch("overinvoke-a", &["in"], &["stage"]);
222 up.workgroup_size = [32, 4, 1]; let mut down = dispatch("overinvoke-b", &["stage"], &["out"]);
224 down.workgroup_size = up.workgroup_size; let decision = FusionPass::decide(&up, &down, caps, &[]);
227
228 assert_ne!(
230 decision,
231 FusionDecision::WorkgroupSizeMismatch {
232 upstream: up.workgroup_size,
233 downstream: down.workgroup_size,
234 },
235 "Fix: when invocations exceed the cap and sizes are equal, the decision must not be WorkgroupSizeMismatch"
236 );
237 assert_eq!(
239 decision,
240 FusionDecision::InvocationBudgetExceeded {
241 workgroup: [32, 4, 1],
242 invocations: 128,
243 cap: 64,
244 },
245 "Fix: FusionPass must return InvocationBudgetExceeded{{workgroup=[32,4,1], invocations=128, cap=64}} when invocations > cap"
246 );
247 }
248
249 #[test]
250 fn workgroup_size_mismatch_variant_is_only_returned_when_sizes_actually_differ() {
251 let mut up = dispatch("mismatch-a", &["in"], &["mid"]);
253 up.workgroup_size = [32, 1, 1];
254 let mut down = dispatch("mismatch-b", &["mid"], &["out"]);
255 down.workgroup_size = [64, 1, 1];
256 assert_eq!(
257 FusionPass::decide(&up, &down, FusionCaps::high_end(), &[]),
258 FusionDecision::WorkgroupSizeMismatch {
259 upstream: [32, 1, 1],
260 downstream: [64, 1, 1],
261 },
262 "Fix: WorkgroupSizeMismatch must carry the actual differing sizes"
263 );
264 }
265}