vyre_driver/grid_sync/mod.rs
1//! Grid-sync kernel splitting.
2//!
3//! Op id: `vyre-driver::grid_sync`. Soundness: `Exact` over the
4//! cross-grid barrier contract.
5//!
6//! ## Why this lives in vyre-driver, not the backend
7//!
8//! Every backend that lacks a native cooperative whole-grid launch
9//! needs the same kernel-split semantics for
10//! `Node::Barrier { ordering: GridSync }`: split the program at the
11//! barrier, dispatch each segment as its own kernel launch, and
12//! re-feed the prior segment's outputs as inputs to the next. The
13//! kernel-launch boundary itself is the grid-level fence - every
14//! prior write becomes globally visible before the next launch reads.
15//!
16//! Backends route through [`crate::grid_sync::dispatch_with_grid_sync_split`] when
17//! [`crate::backend::VyreBackend::supports_grid_sync`] is `false` and the program
18//! contains any `Node::Barrier { ordering: GridSync }`. Backends that
19//! return `true` emit one kernel and satisfy the barrier device-side.
20//!
21//! ## Algorithm
22//!
23//! 1. Walk the program's top-level entry sequence.
24//! 2. Each prefix-suffix split at a `Node::Barrier { GridSync }`
25//! becomes one segment.
26//! 3. For each segment, build a `Program` with a segment-local buffer
27//! table: buffers read or written by that segment plus passthrough
28//! read-write buffers that must preserve caller-visible storage.
29//! 4. Dispatch segments in order, threading live buffers by buffer name
30//! rather than positional output slot. Segment read-only inputs are
31//! assembled from the caller's original bytes or prior segment
32//! outputs; final host-visible output slots are reassembled in the
33//! original program's output declaration order.
34//!
35//! ## Device-resident variant
36//!
37//! [`crate::grid_sync::dispatch_with_grid_sync_split_into`] round-trips every live buffer
38//! host↔device between each segment and on every fixpoint pass. For a fused
39//! multi-rule program whose shared output accumulator is hundreds of MiB and
40//! which splits into hundreds of segments, that transfer, not launch
41//! latency, dominates wall time. [`crate::grid_sync::dispatch_resident_grid_sync_fixpoint_into`]
42//! is the device-resident counterpart: it uploads inputs into backend-resident
43//! resources once, keeps them bound across every segment and fixpoint pass (so
44//! the accumulator threads in place on-device, since resident dispatch never
45//! clears a bound buffer between launches), and reads back only the final
46//! outputs. It requires
47//! [`crate::backend::VyreBackend::supports_resident_dispatch`]; callers route
48//! to it on resident-capable backends and to the host split otherwise.
49//! Both paths are recall- and proof-identical (proven by a host/resident
50//! differential gate); the choice is purely a host↔device-traffic optimization.
51//!
52//! ## Soundness
53//!
54//! - Atomicity preserved: every `atomic_or` that fired in segment N
55//! has flushed to global memory by the time segment N+1 launches -
56//! backend launch APIs issue an implicit grid-level fence at
57//! submission boundaries.
58//! - Ordering preserved: the original program's host-visible output
59//! is byte-identical to the un-split version, modulo timing.
60//! - No re-validation surprise: each split segment validates against
61//! the same backend supported-ops set as the original.
62
63use std::collections::{HashMap, HashSet};
64
65use crate::backend::BackendError;
66
67mod barrier_split;
68mod host_dispatch;
69mod let_propagation;
70mod live_buffers;
71mod resident_dispatch;
72mod segment_buffers;
73#[cfg(test)]
74mod test_programs;
75
76pub use barrier_split::{contains_grid_sync, split_on_grid_sync, try_split_on_grid_sync};
77pub use host_dispatch::{
78 dispatch_with_grid_sync_split, dispatch_with_grid_sync_split_into,
79 dispatch_with_grid_sync_split_timed, dispatch_with_grid_sync_split_via,
80 dispatch_with_grid_sync_split_via_into,
81};
82pub use resident_dispatch::{
83 dispatch_resident_grid_sync_fixpoint_into, dispatch_resident_with_grid_sync_split_timed,
84};
85pub use segment_buffers::plan_host_grid_sync_segment_programs;
86
87// Split plumbing shared by more than one child module: fallible capacity
88// reservation, segment error context, and the timed-dispatch wall clock.
89
90fn reserve_grid_sync_vec<T>(
91 vec: &mut Vec<T>,
92 capacity: usize,
93 field: &'static str,
94) -> Result<(), BackendError> {
95 crate::allocation::try_reserve_vec_to_capacity(vec, capacity).map_err(|error| {
96 BackendError::InvalidProgram {
97 fix: format!(
98 "Fix: failed to reserve {field} for {capacity} entries during grid-sync dispatch splitting: {error}. Split the program into fewer grid-sync segments or run on a backend with native grid sync."
99 ),
100 }
101 })
102}
103
104fn reserve_grid_sync_hash_map<K, V>(
105 map: &mut HashMap<K, V>,
106 capacity: usize,
107 field: &'static str,
108) -> Result<(), BackendError>
109where
110 K: Eq + std::hash::Hash,
111{
112 map.try_reserve(capacity)
113 .map_err(|error| BackendError::InvalidProgram {
114 fix: format!(
115 "Fix: failed to reserve {field} for {capacity} entries during grid-sync dispatch splitting: {error}. Split the program into fewer grid-sync segments or run on a backend with native grid sync."
116 ),
117 })
118}
119
120fn reserve_grid_sync_hash_set<T>(
121 set: &mut HashSet<T>,
122 capacity: usize,
123 field: &'static str,
124) -> Result<(), BackendError>
125where
126 T: Eq + std::hash::Hash,
127{
128 set.try_reserve(capacity)
129 .map_err(|error| BackendError::InvalidProgram {
130 fix: format!(
131 "Fix: failed to reserve {field} for {capacity} entries during grid-sync dispatch splitting: {error}. Split the program into fewer grid-sync segments or run on a backend with native grid sync."
132 ),
133 })
134}
135
136fn grid_sync_segment_error(
137 error: BackendError,
138 segment_idx: usize,
139 segment_count: usize,
140) -> BackendError {
141 match error {
142 BackendError::InvalidProgram { fix } => BackendError::InvalidProgram {
143 fix: format!(
144 "Fix: grid-sync split segment {segment_idx} of {segment_count} dispatch failed: {fix}"
145 ),
146 },
147 other => other,
148 }
149}
150
151fn elapsed_wall_ns(started: std::time::Instant) -> Result<u64, BackendError> {
152 u64::try_from(started.elapsed().as_nanos()).map_err(|error| BackendError::InvalidProgram {
153 fix: format!(
154 "Fix: grid-sync segmented wall timing cannot fit u64 nanoseconds: {error}. Split telemetry windows or report per-segment timing."
155 ),
156 })
157}