vyre_foundation/execution_plan/fusion/
fuse.rs1use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::execution_plan::SchedulingPolicy;
6use crate::ir::{BufferAccess, BufferDecl, Ident, Node, Program};
7
8use super::alpha_rename::{multiply_declared_names, push_alpha_renamed_arm_entry_node, ArmRenamer};
9use super::collectors::collect_buffer_targets;
10use super::divergence::{
11 has_divergent_invocation_gated_store, has_launch_geometry_dependent_write,
12};
13use super::helpers::{fallback_composition_key, upgrade_buffer_access};
14use super::{
15 FusionError, FusionOverDispatchError, FusionSelfAliasingError, FusionWorkgroupGeometryError,
16};
17
18pub fn fuse_programs(programs: &[Program]) -> Result<Program, FusionError> {
26 match programs.len() {
27 0 => Ok(Program::empty()),
28 1 => Ok(programs[0].clone()),
29 _ => fuse_programs_multi(programs),
30 }
31}
32
33#[inline]
42#[must_use]
43pub fn fuse_programs_vec(mut programs: Vec<Program>) -> Result<Program, FusionError> {
44 match programs.len() {
45 0 => Ok(Program::empty()),
46 1 => {
47 let Some(program) = programs.pop() else {
48 return Ok(Program::empty());
49 };
50 Ok(program)
51 }
52 _ => fuse_programs_multi(programs.as_slice()),
53 }
54}
55
56#[derive(Clone, Copy, PartialEq, Eq)]
58pub(crate) enum ArmNamespace {
59 Isolated,
66 Shared,
77}
78
79pub fn merge_programs_shared(programs: &[Program]) -> Result<Program, FusionError> {
93 match programs.len() {
94 0 => Ok(Program::empty()),
95 1 => Ok(programs[0].clone()),
96 _ => fuse_programs_multi_with(programs, ArmNamespace::Shared),
97 }
98}
99
100fn fuse_programs_multi(programs: &[Program]) -> Result<Program, FusionError> {
101 fuse_programs_multi_with(programs, ArmNamespace::Isolated)
102}
103
104fn fuse_programs_multi_with(
105 programs: &[Program],
106 namespace: ArmNamespace,
107) -> Result<Program, FusionError> {
108 reject_non_composable_self_fusion(programs)?;
109
110 let mut merged_buffers: Vec<BufferDecl> = Vec::new();
115 let mut name_to_index: FxHashMap<Ident, usize> = FxHashMap::default();
116 let mut next_binding = 0_u32;
117
118 let mut read_arms_per_buffer: FxHashMap<Ident, Vec<usize>> = FxHashMap::default();
119 let mut write_arms_per_buffer: FxHashMap<Ident, Vec<usize>> = FxHashMap::default();
126 let mut barrier_after_arm: FxHashSet<usize> = FxHashSet::default();
127 let mut grid_sync_writer_arms: FxHashSet<usize> = FxHashSet::default();
132
133 let mut fused_workgroup = [1u32, 1, 1];
134 let mut max_arm_threads: u64 = 1;
135
136 let mut arm_entries: Vec<Vec<Node>> = Vec::with_capacity(programs.len());
137
138 let multiply_declared: FxHashSet<Ident> = match namespace {
145 ArmNamespace::Isolated => FxHashSet::default(),
146 ArmNamespace::Shared => {
147 let entries: Vec<&[Node]> = programs.iter().map(Program::entry).collect();
148 multiply_declared_names(&entries)
149 }
150 };
151
152 for (arm_idx, prog) in programs.iter().enumerate() {
153 let entry = prog.entry();
163 let mut segment = Vec::with_capacity(entry.len());
164 let mut atomic_targets: FxHashSet<Ident> = FxHashSet::default();
165 let mut load_targets: FxHashSet<Ident> = FxHashSet::default();
166 let mut store_targets: FxHashSet<Ident> = FxHashSet::default();
167 let mut divergent_store_seen = false;
168 for node in entry {
169 match namespace {
170 ArmNamespace::Isolated => {
171 push_alpha_renamed_arm_entry_node(&mut segment, node, arm_idx);
172 }
173 ArmNamespace::Shared => {
174 ArmRenamer::shared(arm_idx, &multiply_declared)
175 .push_entry_node(&mut segment, node);
176 }
177 }
178 collect_buffer_targets(
179 node,
180 &mut load_targets,
181 &mut store_targets,
182 &mut atomic_targets,
183 );
184 if has_divergent_invocation_gated_store(node, false) {
185 divergent_store_seen = true;
186 }
187 }
188 if divergent_store_seen || has_launch_geometry_dependent_write(prog.entry()) {
189 grid_sync_writer_arms.insert(arm_idx);
190 }
191 arm_entries.push(segment);
192
193 let mut arm_reads: FxHashSet<Ident> = FxHashSet::default();
194 let mut arm_explicit_writes: FxHashSet<Ident> = FxHashSet::default();
195 classify_and_merge_arm_buffers(
196 prog,
197 &mut arm_reads,
198 &mut arm_explicit_writes,
199 &mut merged_buffers,
200 &mut name_to_index,
201 &mut next_binding,
202 );
203
204 for target in &load_targets {
209 arm_reads.insert(target.clone());
210 }
211 for target in &store_targets {
213 arm_explicit_writes.insert(target.clone());
214 }
215
216 let mut arm_writes = arm_explicit_writes.clone();
218 for target in &atomic_targets {
219 if !arm_reads.contains(target) && !arm_explicit_writes.contains(target) {
220 arm_writes.insert(target.clone());
221 }
222 }
223
224 for write_buf in &arm_writes {
228 if let Some(read_arms) = read_arms_per_buffer.get(write_buf) {
229 for &read_arm in read_arms {
230 barrier_after_arm.insert(read_arm);
231 }
232 }
233 }
234
235 for read_buf in &arm_reads {
246 if let Some(write_arms) = write_arms_per_buffer.get(read_buf) {
247 for &write_arm in write_arms {
248 barrier_after_arm.insert(write_arm);
249 }
250 }
251 }
252
253 for read_buf in &arm_reads {
255 read_arms_per_buffer
256 .entry(read_buf.clone())
257 .or_default()
258 .push(arm_idx);
259 }
260 for write_buf in &arm_writes {
262 write_arms_per_buffer
263 .entry(write_buf.clone())
264 .or_default()
265 .push(arm_idx);
266 }
267
268 let wg = prog.workgroup_size();
270 fused_workgroup[0] = fused_workgroup[0].max(wg[0]);
271 fused_workgroup[1] = fused_workgroup[1].max(wg[1]);
272 fused_workgroup[2] = fused_workgroup[2].max(wg[2]);
273 let arm_threads = u64::from(wg[0]) * u64::from(wg[1]) * u64::from(wg[2]);
274 max_arm_threads = max_arm_threads.max(arm_threads);
275 }
276
277 reject_workgroup_geometry_change(programs, fused_workgroup)?;
278
279 let combined_entry = flatten_arm_entries(
280 arm_entries,
281 &barrier_after_arm,
282 &grid_sync_writer_arms,
283 programs.len(),
284 namespace,
285 );
286 reject_overdispatch(fused_workgroup, max_arm_threads)?;
287
288 let non_composable = programs.iter().any(Program::is_non_composable_with_self);
299 Ok(
300 Program::wrapped(merged_buffers, fused_workgroup, combined_entry)
301 .with_non_composable_with_self(non_composable),
302 )
303}
304
305fn classify_and_merge_arm_buffers(
306 prog: &Program,
307 arm_reads: &mut FxHashSet<Ident>,
308 arm_explicit_writes: &mut FxHashSet<Ident>,
309 merged_buffers: &mut Vec<BufferDecl>,
310 name_to_index: &mut FxHashMap<Ident, usize>,
311 next_binding: &mut u32,
312) {
313 for buf in prog.buffers() {
314 let name = Ident::from(buf.name());
315 match buf.access() {
316 BufferAccess::ReadOnly | BufferAccess::Uniform => {
317 arm_reads.insert(name.clone());
318 }
319 BufferAccess::ReadWrite => {
320 arm_explicit_writes.insert(name.clone());
321 }
322 _ => {}
323 }
324 if let Some(&idx) = name_to_index.get(&name) {
325 let existing = &mut merged_buffers[idx];
326 let access = buf.access();
327 upgrade_buffer_access(existing, &access);
328 if buf.count > existing.count {
329 existing.count = buf.count;
330 }
331 if buf.is_output() {
332 existing.is_output = true;
333 existing.pipeline_live_out = true;
334 }
335 } else {
336 let mut merged = buf.clone();
337 if merged.access() != BufferAccess::Workgroup {
338 merged.binding = *next_binding;
339 *next_binding += 1;
340 }
341 name_to_index.insert(Ident::from(merged.name()), merged_buffers.len());
342 merged_buffers.push(merged);
343 }
344 }
345}
346
347fn reject_non_composable_self_fusion(programs: &[Program]) -> Result<(), FusionError> {
348 let mut seen_op_ids: FxHashMap<String, bool> = FxHashMap::default();
349 for prog in programs {
350 let key = prog
351 .entry_op_id()
352 .map_or_else(|| fallback_composition_key(prog), ToString::to_string);
353 let is_non_comp = prog.is_non_composable_with_self();
354 match seen_op_ids.get_mut(&key) {
355 Some(has_non_comp) if *has_non_comp || is_non_comp => {
356 return Err(FusionError::SelfAliasing(FusionSelfAliasingError {
357 op_id: key,
358 fix: "rename the second parser's workgroup buffer or split into two separate dispatches",
359 }));
360 }
361 Some(_) => {}
362 None => {
363 seen_op_ids.insert(key, is_non_comp);
364 }
365 }
366 }
367 Ok(())
368}
369
370fn reject_workgroup_geometry_change(
385 programs: &[Program],
386 fused_workgroup: [u32; 3],
387) -> Result<(), FusionError> {
388 for (arm, prog) in programs.iter().enumerate() {
389 let arm_workgroup = prog.workgroup_size();
390 if arm_workgroup == fused_workgroup {
391 continue;
392 }
393 let uses_workgroup_memory = prog
394 .buffers()
395 .iter()
396 .any(|buf| buf.access() == BufferAccess::Workgroup);
397 let synchronizes = has_barrier(prog.entry());
398 let reason = match (uses_workgroup_memory, synchronizes) {
399 (true, true) => "keeps state in workgroup memory and synchronizes its workgroup",
400 (true, false) => "keeps state in workgroup memory sized for its own workgroup",
401 (false, true) => "synchronizes its workgroup with a barrier",
402 (false, false) => continue,
403 };
404 return Err(FusionError::WorkgroupGeometry(
405 FusionWorkgroupGeometryError {
406 arm,
407 arm_workgroup,
408 fused_workgroup,
409 reason,
410 fix: "dispatch this arm separately, or rebuild it for the wider workgroup before fusing",
411 },
412 ));
413 }
414 Ok(())
415}
416
417fn has_barrier(nodes: &[Node]) -> bool {
419 nodes.iter().any(|node| match node {
420 Node::Barrier { .. } => true,
421 Node::Region { body, .. } => has_barrier(body),
422 Node::Block(body) | Node::Loop { body, .. } => has_barrier(body),
423 Node::If {
424 then, otherwise, ..
425 } => has_barrier(then) || has_barrier(otherwise),
426 _ => false,
427 })
428}
429
430fn flatten_arm_entries(
431 arm_entries: Vec<Vec<Node>>,
432 barrier_after_arm: &FxHashSet<usize>,
433 grid_sync_writer_arms: &FxHashSet<usize>,
434 program_count: usize,
435 namespace: ArmNamespace,
436) -> Vec<Node> {
437 let total_nodes: usize = arm_entries.iter().map(Vec::len).sum();
438 let mut combined_entry = Vec::with_capacity(total_nodes + program_count);
439 for (arm_idx, segment) in arm_entries.into_iter().enumerate() {
440 match namespace {
441 ArmNamespace::Isolated => combined_entry.push(Node::Block(segment)),
444 ArmNamespace::Shared => combined_entry.extend(segment),
447 }
448 if barrier_after_arm.contains(&arm_idx) {
449 let ordering = if grid_sync_writer_arms.contains(&arm_idx) {
455 crate::memory_model::MemoryOrdering::GridSync
456 } else {
457 crate::memory_model::MemoryOrdering::SeqCst
458 };
459 combined_entry.push(Node::barrier_with_ordering(ordering));
460 }
461 }
462 combined_entry
463}
464
465fn reject_overdispatch(fused_workgroup: [u32; 3], max_arm_threads: u64) -> Result<(), FusionError> {
466 let fused_threads = u64::from(fused_workgroup[0])
467 * u64::from(fused_workgroup[1])
468 * u64::from(fused_workgroup[2]);
469 let policy = SchedulingPolicy::standard();
470 if policy.allow_fused_threads(fused_threads, max_arm_threads) {
471 return Ok(());
472 }
473 Err(FusionError::OverDispatch(FusionOverDispatchError {
474 max_arm_threads,
475 fused_threads,
476 fix: "split the batch or use per-arm dispatch; axis-wise max exceeds the shared over-dispatch policy",
477 }))
478}