1use std::collections::{HashMap, HashSet};
5
6use vyre_foundation::ir::{BufferAccess, BufferDecl, Expr, Ident, MemoryKind, Node, Program};
7
8use super::barrier_split::{entry_sequence, try_split_on_grid_sync};
9use super::{reserve_grid_sync_hash_map, reserve_grid_sync_hash_set, reserve_grid_sync_vec};
10use crate::backend::BackendError;
11
12pub(super) struct PlannedGridSyncSegment {
13 pub(super) program: Program,
14 pub(super) input_names: Vec<Ident>,
15 pub(super) output_names: Vec<Ident>,
16}
17
18pub fn plan_host_grid_sync_segment_programs(
28 program: &Program,
29) -> Result<Vec<Program>, BackendError> {
30 Ok(plan_host_grid_sync_segments(program)?
31 .into_iter()
32 .map(|segment| segment.program)
33 .collect())
34}
35
36pub(super) fn plan_host_grid_sync_segments(
37 program: &Program,
38) -> Result<Vec<PlannedGridSyncSegment>, BackendError> {
39 let split = try_split_on_grid_sync(program)?;
40 let first_writer = first_writer_segment_per_buffer(&split, program)?;
41 let mut planned = Vec::new();
42 reserve_grid_sync_vec(&mut planned, split.len(), "grid-sync planned host segments")?;
43 for (segment_idx, segment) in split.into_iter().enumerate() {
44 let rewritten =
45 rewrite_segment_buffers_for_host_split(program, &segment, segment_idx, &first_writer)?;
46 let input_names = segment_input_names(&rewritten)?;
47 let output_names = segment_output_names(&rewritten)?;
48 planned.push(PlannedGridSyncSegment {
49 program: rewritten,
50 input_names,
51 output_names,
52 });
53 }
54 Ok(planned)
55}
56
57fn first_writer_segment_per_buffer(
70 split: &[Program],
71 program: &Program,
72) -> Result<HashMap<Ident, usize>, BackendError> {
73 let mut first_writer: HashMap<Ident, usize> = HashMap::new();
74 reserve_grid_sync_hash_map(
75 &mut first_writer,
76 program.buffers().len(),
77 "grid-sync first-writer map",
78 )?;
79 for (segment_idx, segment) in split.iter().enumerate() {
80 let mut reads = HashSet::new();
81 let mut writes = HashSet::new();
82 reserve_grid_sync_hash_set(
83 &mut reads,
84 program.buffers().len(),
85 "grid-sync first-writer read scan",
86 )?;
87 reserve_grid_sync_hash_set(
88 &mut writes,
89 program.buffers().len(),
90 "grid-sync first-writer write scan",
91 )?;
92 for node in entry_sequence(segment) {
93 collect_segment_buffer_targets(node, &mut reads, &mut writes);
94 }
95 for name in writes {
96 first_writer.entry(name).or_insert(segment_idx);
97 }
98 }
99 Ok(first_writer)
100}
101
102fn rewrite_segment_buffers_for_host_split(
103 source: &Program,
104 segment: &Program,
105 segment_idx: usize,
106 first_writer: &HashMap<Ident, usize>,
107) -> Result<Program, BackendError> {
108 let mut reads = HashSet::new();
109 let mut writes = HashSet::new();
110 reserve_grid_sync_hash_set(
111 &mut reads,
112 source.buffers().len(),
113 "grid-sync segment read set",
114 )?;
115 reserve_grid_sync_hash_set(
116 &mut writes,
117 source.buffers().len(),
118 "grid-sync segment write set",
119 )?;
120 for node in entry_sequence(segment) {
121 collect_segment_buffer_targets(node, &mut reads, &mut writes);
122 }
123
124 let mut buffers = Vec::new();
125 reserve_grid_sync_vec(
126 &mut buffers,
127 source.buffers().len(),
128 "grid-sync segment buffers",
129 )?;
130 for buffer in source.buffers() {
131 let name = Ident::from(buffer.name());
132 let reads_this = reads.contains(&name);
133 let writes_this = writes.contains(&name);
134 let readwrite_passthrough = matches!(buffer.access(), BufferAccess::ReadWrite)
135 && !buffer.is_output()
136 && !buffer.is_pipeline_live_out()
137 && !reads_this
138 && !writes_this;
139
140 if !reads_this && !writes_this && !readwrite_passthrough {
141 continue;
142 }
143
144 let mut rewritten = buffer.clone();
145 if matches!(rewritten.access(), BufferAccess::Workgroup) {
146 buffers.push(rewritten);
147 continue;
148 }
149
150 let is_source_output = buffer.is_output() || buffer.is_pipeline_live_out();
157 let earlier_segment_wrote_output = is_source_output
158 && first_writer
159 .get(&name)
160 .is_some_and(|&first| first < segment_idx);
161
162 let access = if readwrite_passthrough {
163 BufferAccess::ReadWrite
164 } else if earlier_segment_wrote_output && writes_this {
165 BufferAccess::ReadWrite
169 } else {
170 match (reads_this, writes_this) {
171 (true, true) => BufferAccess::ReadWrite,
172 (true, false) => BufferAccess::ReadOnly,
173 (false, true) => BufferAccess::WriteOnly,
174 (false, false) => BufferAccess::ReadWrite,
175 }
176 };
177 rewrite_segment_buffer_access(&mut rewritten, access);
178 rewritten.is_output = false;
187 rewritten.pipeline_live_out = false;
188 buffers.push(rewritten);
189 }
190
191 Ok(segment.with_rewritten_buffers(buffers))
192}
193
194fn rewrite_segment_buffer_access(buffer: &mut BufferDecl, access: BufferAccess) {
195 buffer.kind = match &access {
196 BufferAccess::ReadOnly => MemoryKind::Readonly,
197 BufferAccess::Uniform => MemoryKind::Uniform,
198 BufferAccess::Workgroup => MemoryKind::Shared,
199 _ => MemoryKind::Global,
200 };
201 buffer.access = access;
202}
203
204pub(super) fn segment_input_names(segment: &Program) -> Result<Vec<Ident>, BackendError> {
205 let mut names = Vec::new();
206 reserve_grid_sync_vec(
207 &mut names,
208 segment.buffers().len(),
209 "grid-sync segment input names",
210 )?;
211 for buffer in segment.buffers() {
212 if matches!(buffer.access(), BufferAccess::Workgroup) {
213 continue;
214 }
215 if segment_buffer_consumes_input(buffer) {
216 names.push(Ident::from(buffer.name()));
217 }
218 }
219 Ok(names)
220}
221
222pub(super) fn segment_output_names(segment: &Program) -> Result<Vec<Ident>, BackendError> {
223 let mut names = Vec::new();
224 reserve_grid_sync_vec(
225 &mut names,
226 segment.buffers().len(),
227 "grid-sync segment output names",
228 )?;
229 for buffer in segment.buffers() {
230 if matches!(buffer.access(), BufferAccess::Workgroup) {
231 continue;
232 }
233 if segment_buffer_produces_output(buffer) {
234 names.push(Ident::from(buffer.name()));
235 }
236 }
237 Ok(names)
238}
239
240pub(super) fn original_input_names(program: &Program) -> Result<Vec<Ident>, BackendError> {
241 segment_input_names(program)
242}
243
244pub(super) fn original_output_names(program: &Program) -> Result<Vec<Ident>, BackendError> {
245 segment_output_names(program)
246}
247
248pub(super) fn segment_buffer_consumes_input(buffer: &BufferDecl) -> bool {
249 if buffer.is_output() || buffer.is_pipeline_live_out() {
250 return false;
251 }
252 matches!(
253 buffer.access(),
254 BufferAccess::ReadOnly | BufferAccess::ReadWrite | BufferAccess::Uniform
255 )
256}
257
258pub(super) fn segment_buffer_produces_output(buffer: &BufferDecl) -> bool {
259 buffer.is_output()
260 || buffer.is_pipeline_live_out()
261 || matches!(
262 buffer.access(),
263 BufferAccess::ReadWrite | BufferAccess::WriteOnly
264 )
265}
266
267fn collect_segment_buffer_targets(
268 node: &Node,
269 reads: &mut HashSet<Ident>,
270 writes: &mut HashSet<Ident>,
271) {
272 match node {
273 Node::Let { value, .. } | Node::Assign { value, .. } => {
274 collect_segment_expr_targets(value, reads, writes);
275 }
276 Node::Store {
277 buffer,
278 index,
279 value,
280 } => {
281 writes.insert(Ident::from(buffer));
282 collect_segment_expr_targets(index, reads, writes);
283 collect_segment_expr_targets(value, reads, writes);
284 }
285 Node::If {
286 cond,
287 then,
288 otherwise,
289 } => {
290 collect_segment_expr_targets(cond, reads, writes);
291 for child in then.iter().chain(otherwise.iter()) {
292 collect_segment_buffer_targets(child, reads, writes);
293 }
294 }
295 Node::Loop { from, to, body, .. } => {
296 collect_segment_expr_targets(from, reads, writes);
297 collect_segment_expr_targets(to, reads, writes);
298 for child in body {
299 collect_segment_buffer_targets(child, reads, writes);
300 }
301 }
302 Node::Block(body) => {
303 for child in body {
304 collect_segment_buffer_targets(child, reads, writes);
305 }
306 }
307 Node::Region { body, .. } => {
308 for child in body.iter() {
309 collect_segment_buffer_targets(child, reads, writes);
310 }
311 }
312 Node::AllReduce { buffer, .. } | Node::Broadcast { buffer, .. } => {
313 reads.insert(buffer.clone());
314 writes.insert(buffer.clone());
315 }
316 Node::AllGather { input, output, .. } | Node::ReduceScatter { input, output, .. } => {
317 reads.insert(input.clone());
318 writes.insert(output.clone());
319 }
320 Node::IndirectDispatch { .. }
321 | Node::Return
322 | Node::Barrier { .. }
323 | Node::AsyncLoad { .. }
324 | Node::AsyncStore { .. }
325 | Node::AsyncWait { .. }
326 | Node::Trap { .. }
327 | Node::Resume { .. }
328 | Node::Opaque(_) => {}
329 _ => {}
330 }
331}
332
333fn collect_segment_expr_targets(
334 expr: &Expr,
335 reads: &mut HashSet<Ident>,
336 writes: &mut HashSet<Ident>,
337) {
338 vyre_foundation::visit::visit_expr_buffer_accesses(expr, |access, buffer| {
339 reads.insert(buffer.clone());
340 if access == vyre_foundation::visit::ExprBufferAccess::Atomic {
341 writes.insert(buffer.clone());
342 }
343 });
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::grid_sync::test_programs::region;
350 use vyre_foundation::ir::DataType;
351 use vyre_foundation::memory_model::MemoryOrdering;
352
353 #[test]
354 fn split_keeps_multi_segment_output_as_readwrite_accumulator() {
355 let out = BufferDecl::output("out", 0, DataType::U32).with_count(4);
364 let program = Program::wrapped(
365 vec![out],
366 [1, 1, 1],
367 vec![
368 region("a", vec![Node::store("out", Expr::u32(0), Expr::u32(0xAA))]),
369 Node::barrier_with_ordering(MemoryOrdering::GridSync),
370 region("b", vec![Node::store("out", Expr::u32(2), Expr::u32(0xBB))]),
371 ],
372 );
373 let segments =
374 plan_host_grid_sync_segment_programs(&program).expect("plan host grid-sync segments");
375 assert_eq!(segments.len(), 2, "one GridSync barrier -> two segments");
376
377 let seg0_out = segments[0]
378 .buffers()
379 .iter()
380 .find(|b| b.name() == "out")
381 .expect("segment 0 must declare the output it writes");
382 assert_eq!(
383 seg0_out.access(),
384 BufferAccess::WriteOnly,
385 "the first writer establishes the accumulator as write-only"
386 );
387 assert!(
388 !seg0_out.is_output() && !seg0_out.is_pipeline_live_out(),
389 "split segment buffers must never be marked program-output; final values are reassembled by name"
390 );
391
392 let seg1_out = segments[1]
393 .buffers()
394 .iter()
395 .find(|b| b.name() == "out")
396 .expect("segment 1 must declare the output it writes");
397 assert_eq!(
398 seg1_out.access(),
399 BufferAccess::ReadWrite,
400 "a later writer of a multi-segment output must read+merge the accumulated value, not overwrite it"
401 );
402 assert!(
403 !seg1_out.is_output() && !seg1_out.is_pipeline_live_out(),
404 "the later writer must consume its forwarded prior value, which `segment_buffer_consumes_input` refuses for is_output buffers"
405 );
406 assert!(
407 segment_input_names(&segments[1])
408 .expect("segment 1 input names")
409 .iter()
410 .any(|n| n.as_str() == "out"),
411 "the accumulated output must be forwarded as an input to the later writing segment"
412 );
413 }
414}