Skip to main content

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}