1use vyre_foundation::execution_plan::fusion::fuse_programs;
34use vyre_foundation::ir::{BufferDecl, DataType, Program};
35
36#[derive(Debug, thiserror::Error)]
38pub enum DecodeScanFuseError {
39 #[error(
44 "Fix: handoff buffer {handoff:?} does not appear in the decoder or scanner Program's \
45 buffer list. Add a `BufferDecl::storage({handoff:?}, ..., DataType::U32)` to both \
46 Programs before calling `fuse_decode_scan`."
47 )]
48 HandoffBufferMissing {
49 handoff: String,
51 },
52 #[error(
59 "Fix: fuse_decode_scan(handoff_byte_count = 0) is rejected on buffer {handoff:?}. \
60 Pass the decoder's peak output-bytes-per-workgroup."
61 )]
62 ZeroHandoff {
63 handoff: String,
65 },
66 #[error(
69 "Fix: kernel-level fusion failed - run the autotune pass to normalise workgroup \
70 sizes and rename any self-aliasing buffers before calling `fuse_decode_scan`. \
71 Inner: {0}"
72 )]
73 Fusion(#[from] vyre_foundation::execution_plan::fusion::FusionError),
74}
75
76pub fn fuse_decode_scan(
85 decoder: Program,
86 scanner: Program,
87 handoff_buf: &str,
88 handoff_byte_count: u32,
89) -> Result<Program, DecodeScanFuseError> {
90 if handoff_byte_count == 0 {
91 return Err(DecodeScanFuseError::ZeroHandoff {
92 handoff: handoff_buf.to_string(),
93 });
94 }
95 let decoder_has = decoder.buffers.iter().any(|b| b.name() == handoff_buf);
96 let scanner_has = scanner.buffers.iter().any(|b| b.name() == handoff_buf);
97 if !decoder_has && !scanner_has {
98 return Err(DecodeScanFuseError::HandoffBufferMissing {
99 handoff: handoff_buf.to_string(),
100 });
101 }
102
103 let fused = fuse_programs(&[decoder, scanner])?;
104 Ok(promote_to_workgroup(fused, handoff_buf, handoff_byte_count))
105}
106
107fn promote_to_workgroup(program: Program, handoff_buf: &str, count: u32) -> Program {
108 let mut new_buffers: Vec<BufferDecl> = program
111 .buffers
112 .iter()
113 .filter(|b| b.name() != handoff_buf)
114 .cloned()
115 .collect();
116 new_buffers.push(BufferDecl::workgroup(handoff_buf, count, DataType::U32));
117 program.with_rewritten_buffers(new_buffers)
121}
122
123#[must_use]
127pub fn dram_bytes_saved(handoff_byte_count: u32, invocations: u32) -> u64 {
128 2_u64 * u64::from(handoff_byte_count) * u64::from(invocations)
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use vyre_foundation::ir::{BufferAccess, Expr, MemoryKind, Node};
137
138 fn decoder_with_handoff(handoff: &str) -> Program {
139 Program::wrapped(
140 vec![
141 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
142 .with_count(64),
143 BufferDecl::storage(handoff, 1, BufferAccess::ReadWrite, DataType::U32)
144 .with_count(64),
145 ],
146 [64, 1, 1],
147 vec![Node::store(
148 handoff,
149 Expr::InvocationId { axis: 0 },
150 Expr::u32(0xAA),
151 )],
152 )
153 }
154
155 fn scanner_with_handoff(handoff: &str) -> Program {
156 Program::wrapped(
157 vec![
158 BufferDecl::storage(handoff, 1, BufferAccess::ReadOnly, DataType::U32)
159 .with_count(64),
160 BufferDecl::storage("matches", 2, BufferAccess::ReadWrite, DataType::U32)
161 .with_count(64),
162 ],
163 [64, 1, 1],
164 vec![Node::let_bind(
165 "byte",
166 Expr::load(handoff, Expr::InvocationId { axis: 0 }),
167 )],
168 )
169 }
170
171 #[test]
172 fn missing_handoff_buffer_errors_with_actionable_fix() {
173 let decoder = Program::wrapped(
174 vec![
175 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
176 .with_count(1),
177 ],
178 [64, 1, 1],
179 vec![],
180 );
181 let scanner = Program::wrapped(
182 vec![
183 BufferDecl::storage("matches", 0, BufferAccess::ReadWrite, DataType::U32)
184 .with_count(1),
185 ],
186 [64, 1, 1],
187 vec![],
188 );
189 let err = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap_err();
190 let msg = format!("{err}");
191 assert!(msg.contains("Fix:"));
192 assert!(msg.contains("decoded"));
193 }
194
195 #[test]
196 fn zero_handoff_byte_count_returns_structured_error() {
197 let decoder = decoder_with_handoff("decoded");
198 let scanner = scanner_with_handoff("decoded");
199 let err = fuse_decode_scan(decoder, scanner, "decoded", 0).unwrap_err();
200 assert!(matches!(err, DecodeScanFuseError::ZeroHandoff { .. }));
201 assert!(err.to_string().contains("Fix:"));
202 }
203
204 #[test]
205 fn fused_program_promotes_handoff_to_workgroup_memory() {
206 let decoder = decoder_with_handoff("decoded");
207 let scanner = scanner_with_handoff("decoded");
208 let fused = fuse_decode_scan(decoder, scanner, "decoded", 128).unwrap();
209 let handoff = fused.buffers.iter().find(|b| b.name() == "decoded").expect(
210 "Fix: handoff buffer survives fusion; restore this invariant before continuing.",
211 );
212 assert_eq!(handoff.access(), BufferAccess::Workgroup);
213 assert_eq!(handoff.kind(), MemoryKind::Shared);
214 assert_eq!(handoff.count(), 128);
215 }
216
217 #[test]
218 fn non_handoff_buffers_stay_as_declared() {
219 let decoder = decoder_with_handoff("decoded");
220 let scanner = scanner_with_handoff("decoded");
221 let fused = fuse_decode_scan(decoder, scanner, "decoded", 128).unwrap();
222 let input = fused.buffers.iter().find(|b| b.name() == "input").unwrap();
223 assert_eq!(input.access(), BufferAccess::ReadOnly);
224 let matches = fused
225 .buffers
226 .iter()
227 .find(|b| b.name() == "matches")
228 .unwrap();
229 assert_eq!(matches.access(), BufferAccess::ReadWrite);
230 }
231
232 #[test]
233 fn fused_body_contains_both_decoder_and_scanner_nodes() {
234 let decoder = decoder_with_handoff("decoded");
235 let scanner = scanner_with_handoff("decoded");
236 let fused = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap();
237 assert_eq!(
241 fused.entry.len(),
242 1,
243 "wrapped entry must be a single root Region"
244 );
245 let body = match &fused.entry[0] {
246 vyre_foundation::ir::Node::Region { body, .. } => body.as_ref(),
247 other => panic!("Fix: fused entry root must be a Region, got {other:?}"),
248 };
249 assert!(
250 body.len() >= 2,
251 "fused root region body should contain both arms, got {} nodes",
252 body.len()
253 );
254 }
255
256 #[test]
257 fn dram_bytes_saved_scales_with_invocations() {
258 assert_eq!(dram_bytes_saved(0, 1_000_000), 0);
259 assert_eq!(dram_bytes_saved(1024, 1000), 2 * 1024 * 1000);
260 assert_eq!(dram_bytes_saved(1, u32::MAX), 2 * u64::from(u32::MAX));
261 }
262
263 #[test]
264 fn handoff_present_in_only_scanner_still_fuses() {
265 let decoder = Program::wrapped(
266 vec![
267 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
268 .with_count(1),
269 ],
270 [64, 1, 1],
271 vec![],
272 );
273 let scanner = scanner_with_handoff("decoded");
274 let fused = fuse_decode_scan(decoder, scanner, "decoded", 64).unwrap();
275 let handoff = fused
276 .buffers
277 .iter()
278 .find(|b| b.name() == "decoded")
279 .unwrap();
280 assert_eq!(handoff.access(), BufferAccess::Workgroup);
281 }
282}