1use std::collections::HashMap;
5
6use vyre_foundation::ir::{Ident, Program};
7
8use super::barrier_split::contains_grid_sync;
9use super::live_buffers::{
10 borrowed_grid_sync_inputs_by_name, collect_final_named_outputs, owned_accumulator_fingerprint,
11 refresh_named_outputs, GridSyncInput,
12};
13use super::segment_buffers::{
14 original_input_names, original_output_names, plan_host_grid_sync_segments,
15 PlannedGridSyncSegment,
16};
17use super::{
18 elapsed_wall_ns, grid_sync_segment_error, reserve_grid_sync_hash_map, reserve_grid_sync_vec,
19};
20use crate::backend::{
21 BackendError, DispatchConfig, OutputBuffers, TimedDispatchResult, VyreBackend,
22};
23
24pub fn dispatch_with_grid_sync_split(
44 backend: &dyn VyreBackend,
45 program: &Program,
46 inputs: &[&[u8]],
47 config: &DispatchConfig,
48) -> Result<Vec<Vec<u8>>, BackendError> {
49 let mut outputs = Vec::new();
50 reserve_grid_sync_vec(
51 &mut outputs,
52 program.output_buffer_indices().len().max(1),
53 "grid-sync final outputs",
54 )?;
55 dispatch_with_grid_sync_split_into(backend, program, inputs, config, &mut outputs)?;
56 Ok(outputs)
57}
58
59pub fn dispatch_with_grid_sync_split_timed(
64 backend: &dyn VyreBackend,
65 program: &Program,
66 inputs: &[&[u8]],
67 config: &DispatchConfig,
68) -> Result<TimedDispatchResult, BackendError> {
69 let started = std::time::Instant::now();
70 let outputs = dispatch_with_grid_sync_split(backend, program, inputs, config)?;
71 Ok(TimedDispatchResult {
72 outputs,
73 wall_ns: elapsed_wall_ns(started)?,
74 device_ns: None,
75 enqueue_ns: None,
76 wait_ns: None,
77 })
78}
79
80fn seed_backend_allocated_segment_inputs<'a>(
81 program: &Program,
82 segments: &[PlannedGridSyncSegment],
83 current_inputs: &mut HashMap<Ident, GridSyncInput<'a>>,
84) -> Result<(), BackendError> {
85 for name in segments
86 .iter()
87 .flat_map(|segment| segment.input_names.iter())
88 {
89 if current_inputs.contains_key(name) {
90 continue;
91 }
92 let Some(buffer) = program.buffer(name.as_str()) else {
93 return Err(BackendError::InvalidProgram {
94 fix: format!(
95 "Fix: grid-sync segment references undeclared input `{name}`. Rebuild the split from a Program whose buffer table covers every expression dependency."
96 ),
97 });
98 };
99 if !buffer.is_backend_allocated_output() {
100 continue;
101 }
102 let static_len =
103 buffer
104 .static_byte_len()
105 .map_err(|error| BackendError::InvalidProgram {
106 fix: format!("Fix: cannot seed grid-sync output `{name}`: {error}"),
107 })?;
108 let byte_len = static_len
109 .or_else(|| buffer.output_byte_range().map(|range| range.end))
110 .ok_or_else(|| BackendError::InvalidProgram {
111 fix: format!(
112 "Fix: grid-sync output `{name}` is read-modify-written before its first split output but has no static byte size. Declare a count or output byte range."
113 ),
114 })?;
115 let mut zeroed = Vec::new();
116 reserve_grid_sync_vec(
117 &mut zeroed,
118 byte_len,
119 "grid-sync backend-allocated output seed",
120 )?;
121 zeroed.resize(byte_len, 0);
122 current_inputs.insert(name.clone(), GridSyncInput::Owned(zeroed));
123 }
124 Ok(())
125}
126
127fn dispatch_grid_sync_split_generic<D>(
133 program: &Program,
134 inputs: &[&[u8]],
135 config: &DispatchConfig,
136 outputs: &mut OutputBuffers,
137 mut dispatch_segment: D,
138) -> Result<(), BackendError>
139where
140 D: FnMut(&Program, &[&[u8]], &DispatchConfig, &mut OutputBuffers) -> Result<(), BackendError>,
141{
142 if !contains_grid_sync(program) {
149 return dispatch_segment(program, inputs, config, outputs);
150 }
151 let segments = plan_host_grid_sync_segments(program)?;
152 if segments.is_empty() {
153 return Err(BackendError::InvalidProgram {
154 fix: "Fix: program contains GridSync barrier but split_on_grid_sync produced 0 \
155 segments. This is a grid_sync invariant bug - split_on_grid_sync must \
156 always return at least one segment."
157 .to_string(),
158 });
159 }
160 crate::observability::record_grid_sync_split(segments.len());
161 let initial_input_names = original_input_names(program)?;
168 if inputs.len() != initial_input_names.len() {
169 return Err(BackendError::InvalidProgram {
170 fix: format!(
171 "Fix: grid-sync split expected {} initial input buffer(s) but received {}. Rebuild the dispatch inputs from the Program buffer declarations before splitting.",
172 initial_input_names.len(),
173 inputs.len()
174 ),
175 });
176 }
177 let mut current_inputs: HashMap<Ident, GridSyncInput<'_>> = HashMap::new();
178 reserve_grid_sync_hash_map(
179 &mut current_inputs,
180 program.buffers().len(),
181 "grid-sync rotating input map",
182 )?;
183 for (name, bytes) in initial_input_names.into_iter().zip(inputs.iter().copied()) {
184 current_inputs.insert(name, GridSyncInput::Borrowed(bytes));
185 }
186 seed_backend_allocated_segment_inputs(program, &segments, &mut current_inputs)?;
187 let mut segment_outputs = Vec::new();
188 reserve_grid_sync_vec(
189 &mut segment_outputs,
190 outputs.capacity().max(1),
191 "grid-sync intermediate outputs",
192 )?;
193 let final_output_names = original_output_names(program)?;
194
195 let iterations =
217 crate::fixpoint_iterations::resolve_fixpoint_iterations(config, "grid-sync split")?;
218 let mut segment_config = config.clone();
219 segment_config.fixpoint_iterations = Some(1);
220
221 let mut prev_fingerprint: Option<u64> = None;
229 for _ in 0..iterations {
230 for (segment_idx, segment) in segments.iter().enumerate() {
231 let borrowed = borrowed_grid_sync_inputs_by_name(segment, ¤t_inputs)?;
232 dispatch_segment(
233 &segment.program,
234 borrowed.as_slice(),
235 &segment_config,
236 &mut segment_outputs,
237 )
238 .map_err(|error| grid_sync_segment_error(error, segment_idx, segments.len()))?;
239 drop(borrowed);
240 refresh_named_outputs(segment, &mut segment_outputs, &mut current_inputs)?;
241 }
242 let fingerprint = owned_accumulator_fingerprint(¤t_inputs);
243 if prev_fingerprint == Some(fingerprint) {
244 break;
245 }
246 prev_fingerprint = Some(fingerprint);
247 }
248 collect_final_named_outputs(&final_output_names, &mut current_inputs, outputs)?;
249 Ok(())
250}
251
252pub fn dispatch_with_grid_sync_split_into(
263 backend: &dyn VyreBackend,
264 program: &Program,
265 inputs: &[&[u8]],
266 config: &DispatchConfig,
267 outputs: &mut OutputBuffers,
268) -> Result<(), BackendError> {
269 dispatch_grid_sync_split_generic(program, inputs, config, outputs, |p, i, c, o| {
270 backend.dispatch_borrowed_into(p, i, c, o)
271 })
272}
273
274pub fn dispatch_with_grid_sync_split_via_into<F>(
294 program: &Program,
295 inputs: &[&[u8]],
296 config: &DispatchConfig,
297 dispatch: &F,
298 outputs: &mut OutputBuffers,
299) -> Result<(), BackendError>
300where
301 F: Fn(&Program, &[&[u8]], Option<[u32; 3]>, &mut Vec<Vec<u8>>) -> Result<(), String>,
302{
303 dispatch_grid_sync_split_generic(program, inputs, config, outputs, |p, i, c, o| {
304 dispatch(p, i, c.grid_override, o).map_err(BackendError::new)
305 })
306}
307
308pub fn dispatch_with_grid_sync_split_via<F>(
313 program: &Program,
314 inputs: &[&[u8]],
315 config: &DispatchConfig,
316 dispatch: &F,
317) -> Result<Vec<Vec<u8>>, BackendError>
318where
319 F: Fn(&Program, &[&[u8]], Option<[u32; 3]>, &mut Vec<Vec<u8>>) -> Result<(), String>,
320{
321 let mut outputs = Vec::new();
322 reserve_grid_sync_vec(
323 &mut outputs,
324 program.output_buffer_indices().len().max(1),
325 "grid-sync via final outputs",
326 )?;
327 dispatch_with_grid_sync_split_via_into(program, inputs, config, dispatch, &mut outputs)?;
328 Ok(outputs)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::grid_sync::barrier_split::entry_sequence;
335 use crate::grid_sync::segment_buffers::{
336 segment_buffer_consumes_input, segment_buffer_produces_output, segment_output_names,
337 };
338 use crate::grid_sync::test_programs::{buffer, region};
339 use std::sync::atomic::{AtomicUsize, Ordering};
340 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node};
341 use vyre_foundation::memory_model::MemoryOrdering;
342
343 struct ReuseCheckingBackend {
344 calls: AtomicUsize,
345 final_outputs_addr: usize,
346 final_slot_addr: usize,
347 }
348
349 impl crate::backend::private::Sealed for ReuseCheckingBackend {}
350
351 impl VyreBackend for ReuseCheckingBackend {
352 fn id(&self) -> &'static str {
353 "grid-sync-reuse-checking"
354 }
355
356 fn dispatch(
357 &self,
358 _program: &Program,
359 _inputs: &[Vec<u8>],
360 _config: &DispatchConfig,
361 ) -> Result<Vec<Vec<u8>>, BackendError> {
362 unreachable!("test uses dispatch_borrowed_into")
363 }
364
365 fn dispatch_borrowed_into(
366 &self,
367 _program: &Program,
368 inputs: &[&[u8]],
369 _config: &DispatchConfig,
370 outputs: &mut OutputBuffers,
371 ) -> Result<(), BackendError> {
372 let call = self.calls.fetch_add(1, Ordering::SeqCst);
373 if call == 1 && self.final_outputs_addr != 0 {
374 assert_eq!(outputs.as_ptr() as usize, self.final_outputs_addr);
375 assert_eq!(outputs[0].as_ptr() as usize, self.final_slot_addr);
376 }
377 if outputs.is_empty() {
378 outputs.push(Vec::new());
379 }
380 outputs[0].clear();
381 outputs[0].extend_from_slice(inputs[0]);
382 if call == 0 {
383 outputs[0][0] = 7;
384 } else {
385 outputs[0][0] = outputs[0][0].saturating_add(1);
386 }
387 Ok(())
388 }
389 }
390
391 #[test]
392 fn split_into_preserves_caller_output_slot_after_named_output_collection() {
393 let program = Program::wrapped(
394 vec![buffer()],
395 [1, 1, 1],
396 vec![
397 region("a", vec![Node::Return]),
398 Node::barrier_with_ordering(MemoryOrdering::GridSync),
399 region("b", vec![Node::Return]),
400 ],
401 );
402 let mut outputs = vec![Vec::with_capacity(8)];
403 let outputs_addr = outputs.as_ptr() as usize;
404 let slot_addr = outputs[0].as_ptr() as usize;
405 let backend = ReuseCheckingBackend {
406 calls: AtomicUsize::new(0),
407 final_outputs_addr: 0,
408 final_slot_addr: 0,
409 };
410 let input = [0u8, 0, 0, 0];
411 dispatch_with_grid_sync_split_into(
412 &backend,
413 &program,
414 &[input.as_slice()],
415 &DispatchConfig::default(),
416 &mut outputs,
417 )
418 .expect("Fix: grid-sync split should write into caller-owned output storage");
419
420 assert_eq!(backend.calls.load(Ordering::SeqCst), 2);
421 assert_eq!(outputs, vec![vec![8, 0, 0, 0]]);
422 assert_eq!(outputs.as_ptr() as usize, outputs_addr);
423 assert_eq!(outputs[0].as_ptr() as usize, slot_addr);
424 }
425
426 struct IncrementingBackend {
434 calls: AtomicUsize,
435 }
436
437 impl crate::backend::private::Sealed for IncrementingBackend {}
438
439 impl VyreBackend for IncrementingBackend {
440 fn id(&self) -> &'static str {
441 "grid-sync-incrementing"
442 }
443
444 fn dispatch(
445 &self,
446 _program: &Program,
447 _inputs: &[Vec<u8>],
448 _config: &DispatchConfig,
449 ) -> Result<Vec<Vec<u8>>, BackendError> {
450 unreachable!("test uses dispatch_borrowed_into")
451 }
452
453 fn dispatch_borrowed_into(
454 &self,
455 _program: &Program,
456 inputs: &[&[u8]],
457 config: &DispatchConfig,
458 outputs: &mut OutputBuffers,
459 ) -> Result<(), BackendError> {
460 self.calls.fetch_add(1, Ordering::SeqCst);
461 assert_eq!(
464 config.fixpoint_iterations,
465 Some(1),
466 "segment dispatch must receive fixpoint_iterations=1; the outer split loop owns the iteration count"
467 );
468 if outputs.is_empty() {
469 outputs.push(Vec::new());
470 }
471 outputs[0].clear();
472 outputs[0].extend_from_slice(inputs[0]);
473 outputs[0][0] = outputs[0][0].saturating_add(1);
474 Ok(())
475 }
476 }
477
478 #[test]
479 fn split_into_loops_whole_sequence_fixpoint_iterations_times() {
480 let program = Program::wrapped(
482 vec![buffer()],
483 [1, 1, 1],
484 vec![
485 region("a", vec![Node::Return]),
486 Node::barrier_with_ordering(MemoryOrdering::GridSync),
487 region("b", vec![Node::Return]),
488 ],
489 );
490
491 let backend = IncrementingBackend {
493 calls: AtomicUsize::new(0),
494 };
495 let mut outputs = vec![Vec::new()];
496 dispatch_with_grid_sync_split_into(
497 &backend,
498 &program,
499 &[[0u8, 0, 0, 0].as_slice()],
500 &DispatchConfig::default(),
501 &mut outputs,
502 )
503 .expect("single-pass split dispatch");
504 assert_eq!(backend.calls.load(Ordering::SeqCst), 2);
505 assert_eq!(outputs, vec![vec![2, 0, 0, 0]]);
506
507 let backend = IncrementingBackend {
512 calls: AtomicUsize::new(0),
513 };
514 let config = DispatchConfig {
515 fixpoint_iterations: Some(3),
516 ..DispatchConfig::default()
517 };
518 let mut outputs = vec![Vec::new()];
519 dispatch_with_grid_sync_split_into(
520 &backend,
521 &program,
522 &[[0u8, 0, 0, 0].as_slice()],
523 &config,
524 &mut outputs,
525 )
526 .expect("multi-pass split dispatch");
527 assert_eq!(
528 backend.calls.load(Ordering::SeqCst),
529 6,
530 "split must re-run the whole 2-segment sequence 3 times"
531 );
532 assert_eq!(
533 outputs,
534 vec![vec![6, 0, 0, 0]],
535 "accumulator must advance one hop per fixpoint pass (2 segments × 3 passes)"
536 );
537 }
538
539 #[test]
542 fn split_seeds_first_atomic_output_without_caller_bytes() {
543 let program = Program::wrapped(
544 vec![BufferDecl::output("out", 0, DataType::U32).with_count(1)],
545 [1, 1, 1],
546 vec![
547 Node::let_bind("prior", Expr::atomic_add("out", Expr::u32(0), Expr::u32(1))),
548 Node::barrier_with_ordering(MemoryOrdering::GridSync),
549 Node::Return,
550 ],
551 );
552 let dispatch = |segment: &Program,
553 inputs: &[&[u8]],
554 _grid: Option<[u32; 3]>,
555 outputs: &mut Vec<Vec<u8>>|
556 -> Result<(), String> {
557 outputs.clear();
558 if !segment_output_names(segment)
559 .map_err(|error| error.to_string())?
560 .is_empty()
561 {
562 assert_eq!(inputs, &[&[0, 0, 0, 0][..]]);
563 outputs.push(1_u32.to_le_bytes().to_vec());
564 }
565 Ok(())
566 };
567
568 let outputs =
569 dispatch_with_grid_sync_split_via(&program, &[], &DispatchConfig::default(), &dispatch)
570 .expect("backend-allocated atomic output must receive its zero seed");
571
572 assert_eq!(outputs, vec![1_u32.to_le_bytes().to_vec()]);
573 }
574
575 #[test]
576 fn split_via_closure_entry_matches_backend_entry_on_the_same_grid_sync_program() {
577 let program = Program::wrapped(
584 vec![buffer()],
585 [1, 1, 1],
586 vec![
587 region("a", vec![Node::Return]),
588 Node::barrier_with_ordering(MemoryOrdering::GridSync),
589 region("b", vec![Node::Return]),
590 ],
591 );
592 let config = DispatchConfig {
593 fixpoint_iterations: Some(3),
594 ..DispatchConfig::default()
595 };
596 let inputs: [&[u8]; 1] = [[0u8, 0, 0, 0].as_slice()];
597
598 let backend = IncrementingBackend {
599 calls: AtomicUsize::new(0),
600 };
601 let mut backend_outputs = vec![Vec::new()];
602 dispatch_with_grid_sync_split_into(
603 &backend,
604 &program,
605 &inputs,
606 &config,
607 &mut backend_outputs,
608 )
609 .expect("backend split dispatch");
610
611 let closure_backend = IncrementingBackend {
615 calls: AtomicUsize::new(0),
616 };
617 let dispatch = |program: &Program,
618 inputs: &[&[u8]],
619 grid: Option<[u32; 3]>,
620 outputs: &mut Vec<Vec<u8>>|
621 -> Result<(), String> {
622 let segment_config = DispatchConfig {
623 grid_override: grid,
624 fixpoint_iterations: Some(1),
625 ..DispatchConfig::default()
626 };
627 closure_backend
628 .dispatch_borrowed_into(program, inputs, &segment_config, outputs)
629 .map_err(|error| error.to_string())
630 };
631 let via_outputs = dispatch_with_grid_sync_split_via(&program, &inputs, &config, &dispatch)
632 .expect("closure split dispatch");
633
634 assert_eq!(
635 via_outputs, backend_outputs,
636 "closure and backend split entries must produce identical output"
637 );
638 assert_eq!(
639 closure_backend.calls.load(Ordering::SeqCst),
640 backend.calls.load(Ordering::SeqCst),
641 "both entries must drive the same number of segment dispatches (3 passes x 2 segments)"
642 );
643 assert_eq!(via_outputs, vec![vec![6u8, 0, 0, 0]]);
644 }
645
646 struct OwnedFinalReserveBackend {
647 calls: AtomicUsize,
648 }
649
650 impl crate::backend::private::Sealed for OwnedFinalReserveBackend {}
651
652 impl VyreBackend for OwnedFinalReserveBackend {
653 fn id(&self) -> &'static str {
654 "grid-sync-owned-final-reserve"
655 }
656
657 fn dispatch(
658 &self,
659 _program: &Program,
660 _inputs: &[Vec<u8>],
661 _config: &DispatchConfig,
662 ) -> Result<Vec<Vec<u8>>, BackendError> {
663 unreachable!("test uses dispatch_borrowed_into")
664 }
665
666 fn dispatch_borrowed_into(
667 &self,
668 _program: &Program,
669 inputs: &[&[u8]],
670 _config: &DispatchConfig,
671 outputs: &mut OutputBuffers,
672 ) -> Result<(), BackendError> {
673 let call = self.calls.fetch_add(1, Ordering::SeqCst);
674 if call == 1 {
675 assert!(
676 outputs.capacity() >= 1,
677 "owned grid-sync split wrapper must pre-reserve final output slots before the final segment dispatch"
678 );
679 }
680 if outputs.is_empty() {
681 outputs.push(Vec::new());
682 }
683 outputs[0].clear();
684 outputs[0].extend_from_slice(inputs[0]);
685 outputs[0][0] = outputs[0][0].saturating_add(1);
686 Ok(())
687 }
688 }
689
690 #[test]
691 fn split_owned_wrapper_reserves_final_output_vector_before_final_segment() {
692 let program = Program::wrapped(
693 vec![buffer()],
694 [1, 1, 1],
695 vec![
696 region("a", vec![Node::Return]),
697 Node::barrier_with_ordering(MemoryOrdering::GridSync),
698 region("b", vec![Node::Return]),
699 ],
700 );
701 let backend = OwnedFinalReserveBackend {
702 calls: AtomicUsize::new(0),
703 };
704 let input = [4u8, 0, 0, 0];
705
706 let outputs = dispatch_with_grid_sync_split(
707 &backend,
708 &program,
709 &[input.as_slice()],
710 &DispatchConfig::default(),
711 )
712 .expect("Fix: owned grid-sync split should reserve and return final outputs");
713
714 assert_eq!(backend.calls.load(Ordering::SeqCst), 2);
715 assert_eq!(outputs, vec![vec![6, 0, 0, 0]]);
716 }
717
718 #[test]
719 fn grid_sync_split_records_segment_telemetry() {
720 let program = Program::wrapped(
721 vec![buffer()],
722 [1, 1, 1],
723 vec![
724 region("a", vec![Node::Return]),
725 Node::barrier_with_ordering(MemoryOrdering::GridSync),
726 region("b", vec![Node::Return]),
727 Node::barrier_with_ordering(MemoryOrdering::GridSync),
728 region("c", vec![Node::Return]),
729 ],
730 );
731 let backend = ReuseCheckingBackend {
732 calls: AtomicUsize::new(0),
733 final_outputs_addr: 0,
734 final_slot_addr: 0,
735 };
736 let before = crate::observability::snapshot_dispatch_telemetry();
737 let input = [0u8, 0, 0, 0];
738 let mut outputs = Vec::new();
739
740 dispatch_with_grid_sync_split_into(
741 &backend,
742 &program,
743 &[input.as_slice()],
744 &DispatchConfig::default(),
745 &mut outputs,
746 )
747 .expect("Fix: grid-sync split should dispatch every segment");
748
749 let after = crate::observability::snapshot_dispatch_telemetry();
750 assert_eq!(backend.calls.load(Ordering::SeqCst), 3);
751 assert!(after.grid_sync_splits > before.grid_sync_splits);
752 assert!(after.grid_sync_segments >= before.grid_sync_segments + 3);
753 assert!(after.grid_sync_points >= before.grid_sync_points + 2);
754 }
755
756 struct IntermediateReuseBackend {
757 calls: AtomicUsize,
758 first_outputs_addr: AtomicUsize,
759 first_slot_addr: AtomicUsize,
760 }
761
762 impl crate::backend::private::Sealed for IntermediateReuseBackend {}
763
764 impl VyreBackend for IntermediateReuseBackend {
765 fn id(&self) -> &'static str {
766 "grid-sync-intermediate-reuse"
767 }
768
769 fn dispatch(
770 &self,
771 _program: &Program,
772 _inputs: &[Vec<u8>],
773 _config: &DispatchConfig,
774 ) -> Result<Vec<Vec<u8>>, BackendError> {
775 unreachable!("test uses dispatch_borrowed_into")
776 }
777
778 fn dispatch_borrowed_into(
779 &self,
780 _program: &Program,
781 inputs: &[&[u8]],
782 _config: &DispatchConfig,
783 outputs: &mut OutputBuffers,
784 ) -> Result<(), BackendError> {
785 let call = self.calls.fetch_add(1, Ordering::SeqCst);
786 if outputs.is_empty() {
787 outputs.push(Vec::with_capacity(8));
788 }
789 if call == 0 {
790 self.first_outputs_addr
791 .store(outputs.as_ptr() as usize, Ordering::SeqCst);
792 self.first_slot_addr
793 .store(outputs[0].as_ptr() as usize, Ordering::SeqCst);
794 } else if call == 1 {
795 assert_eq!(
796 outputs.as_ptr() as usize,
797 self.first_outputs_addr.load(Ordering::SeqCst)
798 );
799 assert_eq!(
800 outputs[0].as_ptr() as usize,
801 self.first_slot_addr.load(Ordering::SeqCst)
802 );
803 }
804 outputs[0].clear();
805 outputs[0].extend_from_slice(inputs[0]);
806 outputs[0][0] = outputs[0][0].saturating_add(1);
807 Ok(())
808 }
809 }
810
811 #[test]
812 fn split_reuses_intermediate_output_slot_between_segments() {
813 let program = Program::wrapped(
814 vec![buffer()],
815 [1, 1, 1],
816 vec![
817 region("a", vec![Node::Return]),
818 Node::barrier_with_ordering(MemoryOrdering::GridSync),
819 region("b", vec![Node::Return]),
820 Node::barrier_with_ordering(MemoryOrdering::GridSync),
821 region("c", vec![Node::Return]),
822 ],
823 );
824 let backend = IntermediateReuseBackend {
825 calls: AtomicUsize::new(0),
826 first_outputs_addr: AtomicUsize::new(0),
827 first_slot_addr: AtomicUsize::new(0),
828 };
829 let input = [1u8, 0, 0, 0];
830 let mut outputs = vec![Vec::with_capacity(8)];
831
832 dispatch_with_grid_sync_split_into(
833 &backend,
834 &program,
835 &[input.as_slice()],
836 &DispatchConfig::default(),
837 &mut outputs,
838 )
839 .expect("Fix: grid-sync split should reuse intermediate output scratch");
840
841 assert_eq!(backend.calls.load(Ordering::SeqCst), 3);
842 assert_eq!(outputs, vec![vec![4, 0, 0, 0]]);
843 }
844
845 struct SlotStoringBackend {
851 calls: AtomicUsize,
852 }
853
854 impl crate::backend::private::Sealed for SlotStoringBackend {}
855
856 impl VyreBackend for SlotStoringBackend {
857 fn id(&self) -> &'static str {
858 "grid-sync-slot-storing"
859 }
860
861 fn dispatch(
862 &self,
863 _program: &Program,
864 _inputs: &[Vec<u8>],
865 _config: &DispatchConfig,
866 ) -> Result<Vec<Vec<u8>>, BackendError> {
867 unreachable!("test uses dispatch_borrowed_into")
868 }
869
870 fn dispatch_borrowed_into(
871 &self,
872 program: &Program,
873 inputs: &[&[u8]],
874 _config: &DispatchConfig,
875 outputs: &mut OutputBuffers,
876 ) -> Result<(), BackendError> {
877 let mut in_pos = None;
880 let mut cur_in = 0usize;
881 let mut out_pos = None;
882 let mut cur_out = 0usize;
883 for buffer in program.buffers() {
884 if matches!(buffer.access(), BufferAccess::Workgroup) {
885 continue;
886 }
887 let consumes = segment_buffer_consumes_input(buffer);
888 let produces = segment_buffer_produces_output(buffer);
889 if buffer.name() == "out" {
890 if consumes {
891 in_pos = Some(cur_in);
892 }
893 if produces {
894 out_pos = Some(cur_out);
895 }
896 }
897 if consumes {
898 cur_in += 1;
899 }
900 if produces {
901 cur_out += 1;
902 }
903 }
904 let out_pos = out_pos.expect("every writing segment must produce `out`");
905 let mut state = match in_pos {
906 Some(i) => inputs[i].to_vec(),
907 None => vec![0u8; 16],
908 };
909
910 fn apply(nodes: &[Node], state: &mut [u8]) {
911 for node in nodes {
912 match node {
913 Node::Store {
914 buffer,
915 index: Expr::LitU32(i),
916 value: Expr::LitU32(v),
917 } if buffer.as_str() == "out" => {
918 let off = (*i as usize) * 4;
919 state[off] = (*v & 0xff) as u8;
920 }
921 Node::Region { body, .. } => apply(body, state),
922 Node::Block(body) => apply(body, state),
923 Node::If {
924 then, otherwise, ..
925 } => {
926 apply(then, state);
927 apply(otherwise, state);
928 }
929 Node::Loop { body, .. } => apply(body, state),
930 _ => {}
931 }
932 }
933 }
934 apply(entry_sequence(program), &mut state);
935
936 self.calls.fetch_add(1, Ordering::SeqCst);
937 while outputs.len() <= out_pos {
938 outputs.push(Vec::new());
939 }
940 outputs[out_pos].clear();
941 outputs[out_pos].extend_from_slice(&state);
942 Ok(())
943 }
944 }
945
946 #[test]
947 fn split_preserves_earlier_segment_output_slots_end_to_end() {
948 let out = BufferDecl::output("out", 0, DataType::U32).with_count(4);
955 let program = Program::wrapped(
956 vec![out],
957 [1, 1, 1],
958 vec![
959 region("a", vec![Node::store("out", Expr::u32(0), Expr::u32(0xAA))]),
960 Node::barrier_with_ordering(MemoryOrdering::GridSync),
961 region("b", vec![Node::store("out", Expr::u32(2), Expr::u32(0xBB))]),
962 ],
963 );
964 let backend = SlotStoringBackend {
965 calls: AtomicUsize::new(0),
966 };
967 let mut outputs = vec![Vec::new()];
968 dispatch_with_grid_sync_split_into(
969 &backend,
970 &program,
971 &[],
972 &DispatchConfig::default(),
973 &mut outputs,
974 )
975 .expect("split dispatch");
976 assert_eq!(
977 backend.calls.load(Ordering::SeqCst),
978 2,
979 "two segments, single fixpoint pass"
980 );
981 assert_eq!(outputs.len(), 1);
982 assert_eq!(outputs[0].len(), 16, "output buffer is 4 × u32 = 16 bytes");
983 assert_eq!(
984 outputs[0][0], 0xAA,
985 "segment 0's slot (element 0) must survive the final segment's write"
986 );
987 assert_eq!(
988 outputs[0][8], 0xBB,
989 "the final segment's slot (element 2) is also present"
990 );
991 }
992
993 struct SaturatingBackend {
997 calls: AtomicUsize,
998 cap: u8,
999 }
1000
1001 impl crate::backend::private::Sealed for SaturatingBackend {}
1002
1003 impl VyreBackend for SaturatingBackend {
1004 fn id(&self) -> &'static str {
1005 "grid-sync-saturating"
1006 }
1007
1008 fn dispatch(
1009 &self,
1010 _program: &Program,
1011 _inputs: &[Vec<u8>],
1012 _config: &DispatchConfig,
1013 ) -> Result<Vec<Vec<u8>>, BackendError> {
1014 unreachable!("test uses dispatch_borrowed_into")
1015 }
1016
1017 fn dispatch_borrowed_into(
1018 &self,
1019 _program: &Program,
1020 inputs: &[&[u8]],
1021 _config: &DispatchConfig,
1022 outputs: &mut OutputBuffers,
1023 ) -> Result<(), BackendError> {
1024 self.calls.fetch_add(1, Ordering::SeqCst);
1025 if outputs.is_empty() {
1026 outputs.push(Vec::new());
1027 }
1028 outputs[0].clear();
1029 outputs[0].extend_from_slice(inputs[0]);
1030 if outputs[0][0] < self.cap {
1031 outputs[0][0] += 1;
1032 }
1033 Ok(())
1034 }
1035 }
1036
1037 #[test]
1038 fn split_outer_loop_early_exits_when_accumulator_reaches_fixpoint() {
1039 let program = Program::wrapped(
1044 vec![buffer()],
1045 [1, 1, 1],
1046 vec![
1047 region("a", vec![Node::Return]),
1048 Node::barrier_with_ordering(MemoryOrdering::GridSync),
1049 region("b", vec![Node::Return]),
1050 ],
1051 );
1052 let backend = SaturatingBackend {
1053 calls: AtomicUsize::new(0),
1054 cap: 3,
1055 };
1056 let config = DispatchConfig {
1057 fixpoint_iterations: Some(10),
1058 ..DispatchConfig::default()
1059 };
1060 let mut outputs = vec![Vec::new()];
1061 dispatch_with_grid_sync_split_into(
1062 &backend,
1063 &program,
1064 &[[0u8, 0, 0, 0].as_slice()],
1065 &config,
1066 &mut outputs,
1067 )
1068 .expect("converging split dispatch");
1069 assert_eq!(
1072 backend.calls.load(Ordering::SeqCst),
1073 6,
1074 "outer loop must early-exit one pass after the accumulator stops changing, not run all 10 iterations"
1075 );
1076 assert_eq!(
1077 outputs,
1078 vec![vec![3, 0, 0, 0]],
1079 "early-exit must return the converged fixpoint value, identical to running every iteration"
1080 );
1081 }
1082
1083 #[test]
1084 fn split_non_converging_accumulator_runs_full_iteration_budget() {
1085 let program = Program::wrapped(
1089 vec![buffer()],
1090 [1, 1, 1],
1091 vec![
1092 region("a", vec![Node::Return]),
1093 Node::barrier_with_ordering(MemoryOrdering::GridSync),
1094 region("b", vec![Node::Return]),
1095 ],
1096 );
1097 let backend = SaturatingBackend {
1099 calls: AtomicUsize::new(0),
1100 cap: 255,
1101 };
1102 let config = DispatchConfig {
1103 fixpoint_iterations: Some(4),
1104 ..DispatchConfig::default()
1105 };
1106 let mut outputs = vec![Vec::new()];
1107 dispatch_with_grid_sync_split_into(
1108 &backend,
1109 &program,
1110 &[[0u8, 0, 0, 0].as_slice()],
1111 &config,
1112 &mut outputs,
1113 )
1114 .expect("non-converging split dispatch");
1115 assert_eq!(
1116 backend.calls.load(Ordering::SeqCst),
1117 8,
1118 "a still-advancing accumulator must run the full 4 iterations x 2 segments"
1119 );
1120 assert_eq!(outputs, vec![vec![8, 0, 0, 0]]);
1121 }
1122}