Skip to main content

vyre_driver/backend/
vyre_backend.rs

1//! The frozen `VyreBackend` contract.
2
3use std::sync::Arc;
4
5use smallvec::SmallVec;
6use vyre_foundation::ir::Program;
7
8use crate::backend::{
9    device_buffer::unsupported_device_buffer, private, BackendError, CompiledPipeline,
10    DeviceBuffer, DispatchConfig, OutputBuffers, PendingDispatch, Resource, TimedDispatchResult,
11};
12
13/// One backend-resident program dispatch in an ordered sequence.
14pub struct ResidentDispatchStep<'a> {
15    /// Program to dispatch.
16    pub program: &'a Program,
17    /// Resident resources in binding order.
18    pub resources: &'a [Resource],
19    /// Optional CUDA/grid-style launch override.
20    pub grid_override: Option<[u32; 3]>,
21    /// Optional workgroup override. MUST be carried alongside `grid_override`:
22    /// a caller that sizes its grid for a specific workgroup (grid =
23    /// ceil(work / workgroup)) will under-cover the work if the step falls back
24    /// to a different default workgroup. `None` keeps the backend's resolved
25    /// default (correct only when `grid_override` is also `None`).
26    pub workgroup_override: Option<[u32; 3]>,
27}
28
29/// One compact byte range to read from a backend-resident resource.
30pub struct ResidentReadRange<'a> {
31    /// Resident resource to read.
32    pub resource: &'a Resource,
33    /// Byte offset inside the resident resource.
34    pub byte_offset: usize,
35    /// Number of bytes to read.
36    pub byte_len: usize,
37}
38
39/// Timing captured for an ordered resident dispatch sequence.
40#[derive(Clone, Debug, Default, Eq, PartialEq)]
41pub struct ResidentSequenceTiming {
42    /// Host-observed sequence duration including requested readbacks.
43    pub wall_ns: u64,
44    /// Device-observed elapsed dispatch time when the backend exposes timers.
45    pub device_ns: Option<u64>,
46    /// Host time spent enqueueing backend work before waiting.
47    pub enqueue_ns: Option<u64>,
48    /// Host time spent waiting for completion and collecting outputs.
49    pub wait_ns: Option<u64>,
50}
51
52/// The frozen contract between vyre and every execution backend.
53///
54/// A backend is a pure function from a validated `Program` and input buffers
55/// to output buffers. Implementations must be `Send + Sync`, deterministic
56/// for identical inputs, and byte-identical to the CPU reference on success.
57/// This trait is the keystone of the vyre abstraction thesis: frontends do
58/// not know which backend runs their IR, and backends do not know which
59/// frontend produced it.
60///
61/// # Examples
62///
63pub trait VyreBackend: private::Sealed + Send + Sync {
64    /// Stable backend identifier used for logging, certificates, and adapter selection.
65    ///
66    /// The identifier must be unique among all backends linked into the
67    /// current process. Conformance reports include this string so that
68    /// consumers know exactly which implementation was certified.
69    fn id(&self) -> &'static str;
70
71    /// Backend implementation version string used for certificates and
72    /// regression tracking.
73    ///
74    /// The default returns `"unspecified"`. Concrete backends should
75    /// override this with their crate version (e.g. `"0.4.0"`) so that
76    /// certificates can detect backend upgrades that may require re-cert.
77    fn version(&self) -> &'static str {
78        "unspecified"
79    }
80
81    /// Operation ids this backend can execute without further lowering.
82    fn supported_ops(&self) -> &std::collections::HashSet<vyre_foundation::ir::OpId> {
83        use crate::backend::validation::default_supported_ops;
84        default_supported_ops()
85    }
86
87    // Raw backend shader text is a concrete-driver implementation
88    // detail, not part of the substrate-neutral `VyreBackend`
89    // contract.
90
91    /// Executes the program with the given input buffers and returns the output buffers.
92    ///
93    /// On success the returned bytes must match the pure-Rust reference
94    /// implementation bit-for-bit. On failure the backend must return a
95    /// [`BackendError`] whose message contains an actionable `Fix: ` hint.
96    ///
97    /// # Examples
98    ///
99    /// ```no_run
100    /// use vyre::{Program, VyreBackend, DispatchConfig};
101    ///
102    /// # fn example(backend: &dyn VyreBackend, program: &Program) -> Result<Vec<Vec<u8>>, vyre::BackendError> {
103    /// let inputs = vec![vec![1u8, 2, 3]];
104    /// let config = DispatchConfig::default();
105    /// backend.dispatch(program, &inputs, &config)
106    /// # }
107    /// ```
108    ///
109    /// # Errors
110    ///
111    /// Returns [`BackendError`] when the backend cannot complete dispatch.
112    /// The error message always includes a `Fix: ` remediation section.
113    fn dispatch(
114        &self,
115        program: &Program,
116        inputs: &[Vec<u8>],
117        config: &DispatchConfig,
118    ) -> Result<Vec<Vec<u8>>, BackendError>;
119
120    /// Executes the program with borrowed input buffers.
121    ///
122    /// Backends may override this method to avoid staging borrowed bytes into
123    /// owned `Vec<u8>` buffers. The default is non-breaking: it performs one
124    /// owned vector allocation for the call and delegates to
125    /// [`VyreBackend::dispatch`].
126    ///
127    /// # Errors
128    ///
129    /// Returns [`BackendError`] when the backend cannot complete dispatch.
130    fn dispatch_borrowed(
131        &self,
132        program: &Program,
133        inputs: &[&[u8]],
134        config: &DispatchConfig,
135    ) -> Result<Vec<Vec<u8>>, BackendError> {
136        let owned =
137            crate::backend::clone_borrowed_inputs_for_dispatch(inputs, "backend input staging")?;
138        let outputs = self.dispatch(program, &owned, config)?;
139        crate::observability::record_dispatch_io(inputs, &outputs);
140        Ok(outputs)
141    }
142
143    /// Executes a borrowed-input dispatch and returns backend-owned timing.
144    ///
145    /// The default records host wall time and delegates to
146    /// [`VyreBackend::dispatch_borrowed`]. Device-specific backends override
147    /// this only inside their driver crates so benchmark crates never import
148    /// vendor APIs directly.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`BackendError`] when the backend cannot complete dispatch.
153    fn dispatch_borrowed_timed(
154        &self,
155        program: &Program,
156        inputs: &[&[u8]],
157        config: &DispatchConfig,
158    ) -> Result<TimedDispatchResult, BackendError> {
159        let started = std::time::Instant::now();
160        let outputs = self.dispatch_borrowed(program, inputs, config)?;
161        Ok(TimedDispatchResult {
162            outputs,
163            wall_ns: crate::backend::checked_elapsed_wall_ns(started, "backend borrowed dispatch")?,
164            device_ns: None,
165            enqueue_ns: None,
166            wait_ns: None,
167        })
168    }
169
170    /// Executes the program with borrowed input buffers and writes outputs into
171    /// caller-owned storage.
172    ///
173    /// Backends may override this method to reuse output buffers across
174    /// dispatches. The default preserves the existing dispatch contract and
175    /// copies returned bytes into existing output slots where possible.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`BackendError`] when the backend cannot complete dispatch.
180    fn dispatch_borrowed_into(
181        &self,
182        program: &Program,
183        inputs: &[&[u8]],
184        config: &DispatchConfig,
185        outputs: &mut OutputBuffers,
186    ) -> Result<(), BackendError> {
187        let result = self.dispatch_borrowed(program, inputs, config)?;
188        let stats = crate::backend::dispatch_result::replace_output_buffers_preserving_slots_with_memory_stats(
189            result,
190            outputs,
191        );
192        crate::observability::record_output_replacement_stats(stats);
193        Ok(())
194    }
195
196    /// Allocate a backend-resident buffer and return a stable resource handle.
197    ///
198    /// Backends that support resident resources override this method so callers
199    /// can keep hot inputs on the device without importing a concrete driver
200    /// crate. The returned [`Resource`] is only meaningful to the backend that
201    /// produced it.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`BackendError`] when the backend cannot allocate a resident
206    /// resource of the requested size.
207    fn allocate_resident(&self, _byte_len: usize) -> Result<Resource, BackendError> {
208        Err(BackendError::UnsupportedFeature {
209            name: "resident buffer allocation".to_string(),
210            backend: self.id().to_string(),
211        })
212    }
213
214    /// Upload bytes into a backend-resident resource.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`BackendError`] when the resource is not owned by this backend
219    /// or the byte length does not match the resident allocation.
220    fn upload_resident(&self, _resource: &Resource, _bytes: &[u8]) -> Result<(), BackendError> {
221        Err(BackendError::UnsupportedFeature {
222            name: "resident buffer upload".to_string(),
223            backend: self.id().to_string(),
224        })
225    }
226
227    /// Upload several backend-resident resources as one logical staging
228    /// operation.
229    ///
230    /// Backends that support resident graph/dataflow hot paths should override
231    /// this with a native batched transfer. The default fails loudly instead
232    /// of looping over [`VyreBackend::upload_resident`], because a hidden
233    /// per-buffer synchronization loop destroys the performance contract.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`BackendError`] when the backend cannot batch resident uploads
238    /// or when any resource/byte length is invalid.
239    fn upload_resident_many(&self, _uploads: &[(&Resource, &[u8])]) -> Result<(), BackendError> {
240        Err(BackendError::UnsupportedFeature {
241            name: "resident buffer batch upload".to_string(),
242            backend: self.id().to_string(),
243        })
244    }
245
246    /// Upload bytes into a subrange of a backend-resident resource.
247    ///
248    /// This is the hot-loop path for reusable resident slots whose capacity is
249    /// larger than the current logical payload. Backends must not require the
250    /// upload length to equal the allocation length.
251    ///
252    /// # Errors
253    ///
254    /// Returns [`BackendError`] when ranged upload is unsupported, the resource
255    /// is not owned by this backend, or the destination range is out of bounds.
256    fn upload_resident_at(
257        &self,
258        _resource: &Resource,
259        _dst_offset_bytes: usize,
260        _bytes: &[u8],
261    ) -> Result<(), BackendError> {
262        Err(BackendError::UnsupportedFeature {
263            name: "resident buffer ranged upload".to_string(),
264            backend: self.id().to_string(),
265        })
266    }
267
268    /// Upload several resident subranges as one logical staging operation.
269    ///
270    /// The default fails loudly instead of looping over
271    /// [`VyreBackend::upload_resident_at`], because hidden per-range
272    /// synchronization breaks resident hot-loop performance.
273    ///
274    /// # Errors
275    ///
276    /// Returns [`BackendError`] when ranged batch upload is unsupported or when
277    /// any resource/range is invalid.
278    fn upload_resident_at_many(
279        &self,
280        _uploads: &[(&Resource, usize, &[u8])],
281    ) -> Result<(), BackendError> {
282        Err(BackendError::UnsupportedFeature {
283            name: "resident buffer ranged batch upload".to_string(),
284            backend: self.id().to_string(),
285        })
286    }
287
288    /// Download a backend-resident resource into a new host buffer.
289    ///
290    /// Prefer [`VyreBackend::download_resident_into`] in hot loops so repeated
291    /// validation does not allocate a fresh `Vec` for every readback.
292    ///
293    /// # Errors
294    ///
295    /// Returns [`BackendError`] when the backend cannot download resident
296    /// resources or when `resource` is not owned by this backend.
297    fn download_resident(&self, resource: &Resource) -> Result<Vec<u8>, BackendError> {
298        let mut bytes = Vec::new();
299        self.download_resident_into(resource, &mut bytes)?;
300        Ok(bytes)
301    }
302
303    /// Download a backend-resident resource into caller-owned storage.
304    ///
305    /// Implementations must clear and reuse `out`; hidden compatibility
306    /// allocation defeats resident hot-loop validation.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`BackendError`] when resident download is unsupported or when
311    /// `resource` is not owned by this backend.
312    fn download_resident_into(
313        &self,
314        _resource: &Resource,
315        _out: &mut Vec<u8>,
316    ) -> Result<(), BackendError> {
317        Err(BackendError::UnsupportedFeature {
318            name: "resident buffer download".to_string(),
319            backend: self.id().to_string(),
320        })
321    }
322
323    /// Download a byte range from a backend-resident resource into a new host
324    /// buffer.
325    ///
326    /// Prefer [`VyreBackend::download_resident_range_into`] in hot loops.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`BackendError`] when resident ranged download is unsupported,
331    /// the range is invalid, or `resource` is not owned by this backend.
332    fn download_resident_range(
333        &self,
334        resource: &Resource,
335        byte_offset: usize,
336        byte_len: usize,
337    ) -> Result<Vec<u8>, BackendError> {
338        let mut bytes = Vec::new();
339        bytes.try_reserve_exact(byte_len).map_err(|error| {
340            BackendError::InvalidProgram {
341                fix: format!(
342                    "Fix: resident ranged download could not reserve {byte_len} output byte(s): {error}. Split the readback range before dispatch."
343                ),
344            }
345        })?;
346        self.download_resident_range_into(resource, byte_offset, byte_len, &mut bytes)?;
347        Ok(bytes)
348    }
349
350    /// Download a byte range from a backend-resident resource into
351    /// caller-owned storage.
352    ///
353    /// # Errors
354    ///
355    /// Returns [`BackendError`] when resident ranged download is unsupported,
356    /// the range is invalid, or `resource` is not owned by this backend.
357    fn download_resident_range_into(
358        &self,
359        _resource: &Resource,
360        _byte_offset: usize,
361        _byte_len: usize,
362        _out: &mut Vec<u8>,
363    ) -> Result<(), BackendError> {
364        Err(BackendError::UnsupportedFeature {
365            name: "resident buffer ranged download".to_string(),
366            backend: self.id().to_string(),
367        })
368    }
369
370    /// Download several byte ranges from backend-resident resources into
371    /// caller-owned storage as one logical readback operation.
372    ///
373    /// Backends with a real multi-readback path should override this to issue
374    /// all copies behind one backend synchronization boundary.
375    ///
376    /// # Errors
377    ///
378    /// Returns [`BackendError`] when counts do not match, any range is invalid,
379    /// or resident ranged download is unsupported.
380    fn download_resident_ranges_into(
381        &self,
382        ranges: &[(&Resource, usize, usize)],
383        outputs: &mut [&mut Vec<u8>],
384    ) -> Result<(), BackendError> {
385        if ranges.len() != outputs.len() {
386            return Err(BackendError::InvalidProgram {
387                fix: format!(
388                    "Fix: resident ranged batch download expected matching range/output counts but got {} range(s) and {} output(s).",
389                    ranges.len(),
390                    outputs.len()
391                ),
392            });
393        }
394        for ((resource, byte_offset, byte_len), output) in ranges.iter().zip(outputs.iter_mut()) {
395            self.download_resident_range_into(resource, *byte_offset, *byte_len, output)?;
396        }
397        Ok(())
398    }
399
400    /// Free a backend-resident resource previously returned by
401    /// [`VyreBackend::allocate_resident`].
402    ///
403    /// # Errors
404    ///
405    /// Returns [`BackendError`] when the resource is unknown or still in use.
406    fn free_resident(&self, _resource: Resource) -> Result<(), BackendError> {
407        Err(BackendError::UnsupportedFeature {
408            name: "resident buffer free".to_string(),
409            backend: self.id().to_string(),
410        })
411    }
412
413    /// Dispatch using backend-resident resources and return backend-owned
414    /// timing.
415    ///
416    /// # Errors
417    ///
418    /// Returns [`BackendError`] when the backend does not support resident
419    /// dispatch or any resource is invalid for the program.
420    fn dispatch_resident_timed(
421        &self,
422        _program: &Program,
423        _resources: &[Resource],
424        _config: &DispatchConfig,
425    ) -> Result<TimedDispatchResult, BackendError> {
426        Err(BackendError::UnsupportedFeature {
427            name: "resident timed dispatch".to_string(),
428            backend: self.id().to_string(),
429        })
430    }
431
432    /// Dispatch an ordered sequence of resident-buffer programs and read
433    /// selected resident byte ranges into caller-owned storage.
434    ///
435    /// The default preserves correctness by dispatching each step through
436    /// [`VyreBackend::dispatch_resident_timed`] and then calling
437    /// [`VyreBackend::download_resident_ranges_into`]. CUDA overrides this to
438    /// enqueue the whole dependent chain plus D2H readbacks on one stream and
439    /// pay one host synchronization point.
440    ///
441    /// # Errors
442    ///
443    /// Returns [`BackendError`] when any step fails, when readback ranges are
444    /// invalid, or when the backend cannot perform resident dispatch/readback.
445    fn dispatch_resident_sequence_read_ranges_into(
446        &self,
447        steps: &[ResidentDispatchStep<'_>],
448        read_ranges: &[ResidentReadRange<'_>],
449        outputs: &mut [&mut Vec<u8>],
450    ) -> Result<(), BackendError> {
451        for step in steps {
452            let mut config = DispatchConfig::default();
453            config.grid_override = step.grid_override;
454            self.dispatch_resident_timed(step.program, step.resources, &config)?;
455        }
456        let ranges = read_ranges
457            .iter()
458            .map(|range| (range.resource, range.byte_offset, range.byte_len))
459            .collect::<SmallVec<[_; 8]>>();
460        self.download_resident_ranges_into(&ranges, outputs)
461    }
462
463    /// Timed variant of
464    /// [`VyreBackend::dispatch_resident_sequence_read_ranges_into`].
465    ///
466    /// The default preserves correctness by summing each step's resident
467    /// dispatch timing and then downloading the requested ranges. Backends with
468    /// a fused resident sequence path should override this to keep their
469    /// optimized stream/queue behavior while exposing device timing.
470    ///
471    /// # Errors
472    ///
473    /// Returns [`BackendError`] when any step fails, when readback ranges are
474    /// invalid, or when resident sequence timing overflows.
475    fn dispatch_resident_sequence_read_ranges_timed_into(
476        &self,
477        steps: &[ResidentDispatchStep<'_>],
478        read_ranges: &[ResidentReadRange<'_>],
479        outputs: &mut [&mut Vec<u8>],
480    ) -> Result<ResidentSequenceTiming, BackendError> {
481        let started = std::time::Instant::now();
482        let mut device_ns = Some(0_u64);
483        let mut enqueue_ns = Some(0_u64);
484        let mut wait_ns = Some(0_u64);
485        for step in steps {
486            let mut config = DispatchConfig::default();
487            config.grid_override = step.grid_override;
488            let timed = self.dispatch_resident_timed(step.program, step.resources, &config)?;
489            device_ns = crate::accounting::sum_optional_timing(
490                device_ns,
491                timed.device_ns,
492                "device timing",
493                "resident sequence",
494                "per-step",
495            )?;
496            enqueue_ns = crate::accounting::sum_optional_timing(
497                enqueue_ns,
498                timed.enqueue_ns,
499                "enqueue timing",
500                "resident sequence",
501                "per-step",
502            )?;
503            wait_ns = crate::accounting::sum_optional_timing(
504                wait_ns,
505                timed.wait_ns,
506                "wait timing",
507                "resident sequence",
508                "per-step",
509            )?;
510        }
511        let ranges = read_ranges
512            .iter()
513            .map(|range| (range.resource, range.byte_offset, range.byte_len))
514            .collect::<SmallVec<[_; 8]>>();
515        self.download_resident_ranges_into(&ranges, outputs)?;
516        Ok(ResidentSequenceTiming {
517            wall_ns: elapsed_resident_sequence_wall_ns(started)?,
518            device_ns,
519            enqueue_ns,
520            wait_ns,
521        })
522    }
523
524    /// Dispatch a resident prefix, repeat a resident sub-sequence, and read
525    /// selected resident byte ranges into caller-owned storage.
526    ///
527    /// This is the fixed-point hot-path contract: dataflow clients can express
528    /// a repeated kernel group without allocating one host sequence entry per
529    /// iteration. Backends that understand repetition should override this to
530    /// keep launch preparation and parameter upload sublinear in
531    /// `repeat_count`.
532    ///
533    /// # Errors
534    ///
535    /// Returns [`BackendError`] when any step fails, when readback ranges are
536    /// invalid, or when the backend cannot perform resident dispatch/readback.
537    fn dispatch_resident_repeated_sequence_read_ranges_into(
538        &self,
539        prefix_steps: &[ResidentDispatchStep<'_>],
540        repeated_steps: &[ResidentDispatchStep<'_>],
541        repeat_count: u32,
542        read_ranges: &[ResidentReadRange<'_>],
543        outputs: &mut [&mut Vec<u8>],
544    ) -> Result<(), BackendError> {
545        for step in prefix_steps {
546            let mut config = DispatchConfig::default();
547            config.grid_override = step.grid_override;
548            self.dispatch_resident_timed(step.program, step.resources, &config)?;
549        }
550        for _ in 0..repeat_count {
551            for step in repeated_steps {
552                let mut config = DispatchConfig::default();
553                config.grid_override = step.grid_override;
554                self.dispatch_resident_timed(step.program, step.resources, &config)?;
555            }
556        }
557        let ranges = read_ranges
558            .iter()
559            .map(|range| (range.resource, range.byte_offset, range.byte_len))
560            .collect::<SmallVec<[_; 8]>>();
561        self.download_resident_ranges_into(&ranges, outputs)
562    }
563
564    /// Optional pre-compilation hook for the pipeline-mode API.
565    ///
566    /// Default returns `Ok(None)`  -  the framework wraps in a passthrough
567    /// pipeline whose `dispatch` calls back into [`VyreBackend::dispatch`]
568    /// every time. Backends that genuinely cache compiled state (compute
569    /// pipeline, bind-group layout, lowered shader text) override this and
570    /// return `Ok(Some(...))` so repeated dispatches skip the compilation
571    /// overhead.
572    ///
573    /// The returned pipeline MUST be bit-identical to repeated
574    /// `dispatch(program, inputs, config)` for the program it was compiled
575    /// from. The cache key is the backend's responsibility  -  the framework
576    /// does not deduplicate compile calls.
577    ///
578    /// Implementing this method is the P-6 contract from
579    /// `docs/audits/ROADMAP_PERFORMANCE.md`: "compile target-text + pipeline +
580    /// bind-group-layout once; dispatch repeatedly with different inputs."
581    ///
582    /// # Errors
583    ///
584    /// Returns [`BackendError`] when the backend cannot complete the
585    /// pre-compilation. Callers should treat this as fatal for the program
586    /// (the program will not dispatch successfully via any path).
587    fn compile_native(
588        &self,
589        _program: &Program,
590        _config: &DispatchConfig,
591    ) -> Result<Option<Arc<dyn CompiledPipeline>>, BackendError> {
592        Ok(None)
593    }
594
595    /// Optional pre-compilation hook for callers that already own a shared
596    /// program allocation.
597    ///
598    /// Backends that store the program inside the compiled pipeline should
599    /// override this method and keep the supplied [`Arc<Program>`] instead of
600    /// cloning the IR. The default preserves the older borrowed-program hook
601    /// for backends that only inspect the program while compiling.
602    ///
603    /// # Errors
604    ///
605    /// Returns [`BackendError`] when backend-native compilation fails.
606    fn compile_native_shared(
607        &self,
608        program: Arc<Program>,
609        config: &DispatchConfig,
610    ) -> Result<Option<Arc<dyn CompiledPipeline>>, BackendError> {
611        self.compile_native(&program, config)
612    }
613
614    /// Optional compiled-pipeline cache counters for compile telemetry.
615    ///
616    /// Return `None` unless the backend can report real cache hits and misses.
617    fn pipeline_cache_snapshot(&self) -> Option<crate::pipeline::PipelineCacheSnapshot> {
618        None
619    }
620
621    /// Optional backend-specific numeric telemetry for release evidence.
622    ///
623    /// Return an empty vector unless the backend can report real counters.
624    /// Metric names must be stable ASCII identifiers suitable for JSON and
625    /// Prometheus-style export.
626    fn backend_metric_snapshot(&self) -> Vec<(&'static str, u64)> {
627        Vec::new()
628    }
629
630    /// Non-blocking dispatch primitive.
631    ///
632    /// Returns a [`PendingDispatch`] handle immediately; the caller
633    /// polls via [`PendingDispatch::is_ready`] and consumes the result
634    /// via [`PendingDispatch::await_result`]. Backends that genuinely
635    /// pipeline dispatches override this so N concurrent dispatches
636    /// do not serialize on the host.
637    ///
638    /// Default: run the synchronous [`VyreBackend::dispatch`] path and
639    /// wrap the result in a trivially-ready handle. This keeps every
640    /// backend useful from the async API without forcing an async
641    /// rewrite.
642    ///
643    /// # Errors
644    ///
645    /// Returns [`BackendError`] if the dispatch cannot start. Errors
646    /// that surface only during GPU execution come back through
647    /// [`PendingDispatch::await_result`], not from this call.
648    fn dispatch_async(
649        &self,
650        program: &Program,
651        inputs: &[Vec<u8>],
652        config: &DispatchConfig,
653    ) -> Result<Box<dyn PendingDispatch>, BackendError> {
654        let outputs = self.dispatch(program, inputs, config)?;
655        Ok(Box::new(crate::backend::pending_dispatch::ReadyPending {
656            outputs,
657        }))
658    }
659
660    /// Non-blocking dispatch with borrowed input buffers.
661    ///
662    /// Backends that record GPU commands synchronously before returning can
663    /// override this to avoid cloning input buffers just to create a pending
664    /// handle. The returned [`PendingDispatch`] must not borrow from `inputs`.
665    ///
666    /// # Errors
667    ///
668    /// Returns [`BackendError`] if the dispatch cannot start.
669    fn dispatch_borrowed_async(
670        &self,
671        program: &Program,
672        inputs: &[&[u8]],
673        config: &DispatchConfig,
674    ) -> Result<Box<dyn PendingDispatch>, BackendError> {
675        let outputs = self.dispatch_borrowed(program, inputs, config)?;
676        Ok(Box::new(crate::backend::pending_dispatch::ReadyPending {
677            outputs,
678        }))
679    }
680
681    // ---------------------------------------------------------------
682    // Capability queries (all default to conservative "no" / minimal).
683    //
684    // These are the stable capability surface. Additional backends implement
685    // this trait by default-inheriting every capability below and OVERRIDING
686    // only the ones where they are more capable than the conservative floor.
687    // This means adding a backend is strictly additive  -  no existing
688    // backend impl has to change when a new capability query is added.
689    //
690    // Backends MUST report HONESTLY. Returning `true` from a capability
691    // query is a promise the lowering path emits the corresponding
692    // intrinsic and the adapter supports it. "Supported but broken" is a
693    // LAW 9 evasion (see AGENTS.md). If the feature bit is set on the
694    // device but the lowering emits a slower emulation sequence, the
695    // answer is `false` until the native lowering catches up.
696    // ---------------------------------------------------------------
697
698    /// Whether this backend's lowering path emits subgroup / wave
699    /// intrinsics AND the current adapter exposes them.
700    ///
701    /// Default: `false` (conservative  -  assumes no native subgroup lowering).
702    #[must_use]
703    fn supports_subgroup_ops(&self) -> bool {
704        false
705    }
706
707    /// Whether this backend lowers IEEE 754 binary16 (`DataType::F16`)
708    /// natively rather than emulating through `f32`.
709    ///
710    /// Default: `false`.
711    #[must_use]
712    fn supports_f16(&self) -> bool {
713        false
714    }
715
716    /// Whether this backend lowers bfloat16 (`DataType::BF16`) natively.
717    ///
718    /// Default: `false`.
719    #[must_use]
720    fn supports_bf16(&self) -> bool {
721        false
722    }
723
724    /// Whether this backend emits tensor-core / matrix-engine intrinsics
725    /// for supported tensor shapes.
726    ///
727    /// Default: `false`.
728    #[must_use]
729    fn supports_tensor_cores(&self) -> bool {
730        false
731    }
732
733    /// Whether this backend overlaps copies and compute via independent
734    /// queues or async engines.
735    ///
736    /// Default: `false` (host serializes copy ↔ compute).
737    #[must_use]
738    fn supports_async_compute(&self) -> bool {
739        false
740    }
741
742    /// Whether this backend supports indirect dispatch
743    /// (`Node::IndirectDispatch`).
744    ///
745    /// Default: `false`.
746    #[must_use]
747    fn supports_indirect_dispatch(&self) -> bool {
748        false
749    }
750
751    /// Whether this backend supports speculative dispatch  -  a fused
752    /// prefilter + confirmer kernel with commit-gated output and a
753    /// counter tail read back by the host.
754    ///
755    /// Default: `false`.
756    #[must_use]
757    fn supports_speculation(&self) -> bool {
758        false
759    }
760
761    /// Whether this backend supports device-side persistent-thread
762    /// dispatch (a long-running kernel that polls a work queue).
763    ///
764    /// Default: `false`.
765    #[must_use]
766    fn supports_persistent_thread_dispatch(&self) -> bool {
767        false
768    }
769
770    /// Whether this backend can satisfy `Node::Barrier { ordering:
771    /// MemoryOrdering::GridSync }` inside a single dispatch  -  i.e.
772    /// every thread in the entire grid waits at the barrier and
773    /// every prior write is globally visible afterwards. Backends
774    /// that lack a native grid barrier (workgroup-only fences) must
775    /// return `false`; registration-based dispatch may lower a
776    /// `GridSync` barrier to a host-orchestrated kernel split only
777    /// when [`VyreBackend::allows_host_grid_sync_split`] also returns
778    /// `true`.
779    ///
780    /// Backends with cooperative whole-grid launch support can return
781    /// `true`; backends limited to workgroup-local synchronization return
782    /// `false` until the target exposes a compatible grid-barrier primitive.
783    ///
784    /// Default: `false`.
785    #[must_use]
786    fn supports_grid_sync(&self) -> bool {
787        false
788    }
789
790    /// Whether a native cooperative grid-sync launch of `program` with these
791    /// `inputs` and `config` can be made fully resident on this device.
792    ///
793    /// [`VyreBackend::supports_grid_sync`] reports that native lowering is
794    /// *available*; this reports whether it *fits* for a specific dispatch. A
795    /// cooperative launch requires every block co-resident, so a grid whose
796    /// block count exceeds the device's cooperative residency cannot run
797    /// natively and must route to the resident-fixpoint or host-split path.
798    /// Orchestrators call this to choose the native route only when it fits,
799    /// avoiding a wasted allocate/upload that would otherwise end in
800    /// [`crate::backend::ErrorCode::CooperativeResidencyExceeded`].
801    ///
802    /// Default: `Ok(false)` (no native cooperative launch). Backends that lower
803    /// grid sync override this with the real residency check. Returns `Ok(false)`
804    ///: not an error, when the program carries no grid-sync barrier, since
805    /// there is then nothing to launch cooperatively.
806    ///
807    /// # Errors
808    ///
809    /// Returns [`BackendError`] if the launch geometry cannot be computed for
810    /// the program/inputs (a structurally invalid dispatch).
811    fn cooperative_grid_sync_fits(
812        &self,
813        _program: &Program,
814        _inputs: &[&[u8]],
815        _config: &DispatchConfig,
816    ) -> Result<bool, BackendError> {
817        Ok(false)
818    }
819
820    /// Whether the shared registry wrapper may emulate whole-grid
821    /// synchronization for this backend by splitting one program into
822    /// multiple host-dispatched kernels.
823    ///
824    /// This exists separately from [`VyreBackend::supports_grid_sync`]
825    /// because a backend can intentionally reject hidden host
826    /// orchestration while native cooperative-grid lowering is absent.
827    /// CUDA uses that policy in the release path so missing native
828    /// grid-barrier lowering is surfaced as an unsupported feature
829    /// instead of silently becoming a slower multi-launch path.
830    ///
831    /// Default: `true` to preserve existing behavior for simple
832    /// backends that intentionally rely on shared split lowering.
833    #[must_use]
834    fn allows_host_grid_sync_split(&self) -> bool {
835        true
836    }
837
838    /// Whether this backend implements the resident half of the contract
839    /// (`allocate_resident` / `upload_resident` / `dispatch_resident_timed` /
840    /// `dispatch_resident_repeated_sequence_read_ranges_into` /
841    /// `download_resident_*` / `free_resident`) well enough to run a
842    /// device-resident dispatch sequence.
843    ///
844    /// Consumers use this to choose
845    /// [`crate::grid_sync::dispatch_resident_grid_sync_fixpoint_into`] (which
846    /// keeps live buffers device-resident across every grid-sync segment and
847    /// fixpoint pass) over the host-orchestrated
848    /// [`crate::grid_sync::dispatch_with_grid_sync_split_into`] (which
849    /// round-trips every live buffer host↔device between segments). Both are
850    /// correct; the choice is a performance route on a probed capability, not
851    /// a silent failure fallback.
852    ///
853    /// Default: `false`. Backends that implement resident dispatch override
854    /// this to `true`.
855    #[must_use]
856    fn supports_resident_dispatch(&self) -> bool {
857        false
858    }
859
860    /// Whether this backend partitions a program across more than one
861    /// physical device / node.
862    ///
863    /// Default: `false` (single-device execution).
864    #[must_use]
865    fn is_distributed(&self) -> bool {
866        false
867    }
868
869    /// Whether this backend lowers distributed collective communication
870    /// nodes (`AllReduce`, `AllGather`, `ReduceScatter`, `Broadcast`).
871    ///
872    /// This is intentionally separate from [`VyreBackend::is_distributed`]:
873    /// a backend may partition work across devices without yet exposing a
874    /// correct collective transport/lowering stack. Default: `false`.
875    #[must_use]
876    fn supports_distributed_collectives(&self) -> bool {
877        false
878    }
879
880    /// Maximum supported workgroup size per axis `[x, y, z]`.
881    ///
882    /// Default: `[1, 1, 1]` (scalar dispatch  -  a backend that has not
883    /// reported a real limit cannot be trusted to execute parallel
884    /// workgroups).
885    #[must_use]
886    fn max_workgroup_size(&self) -> [u32; 3] {
887        [1, 1, 1]
888    }
889
890    /// Maximum number of compute workgroups the backend can launch in one
891    /// dispatch dimension.
892    ///
893    /// Default: `1`, which is safe for scalar/reference backends but must be
894    /// overridden by real GPU backends so schedulers do not under-launch.
895    #[must_use]
896    fn max_compute_workgroups_per_dimension(&self) -> u32 {
897        1
898    }
899
900    /// Maximum total invocations allowed in a single workgroup.
901    ///
902    /// Default derives from [`max_workgroup_size`](Self::max_workgroup_size)
903    /// and fails loudly if a backend reports an unrepresentable product.
904    #[must_use]
905    fn max_compute_invocations_per_workgroup(&self) -> u32 {
906        let [x, y, z] = self.max_workgroup_size();
907        let invocations = u128::from(x) * u128::from(y) * u128::from(z);
908        u32::try_from(invocations).unwrap_or(u32::MAX)
909    }
910
911    /// Native subgroup size for the backing device when the backend
912    /// knows it. Returning
913    /// `None` tells the dispatch planner the backend can't report a
914    /// subgroup width  -  the planner falls back to `max_workgroup_size`
915    /// for its sizing heuristic.
916    ///
917    /// I.6  -  adaptive workgroup sizing reads this capability to pick
918    /// a workgroup multiple of the subgroup so threads don't straddle
919    /// subgroups. Typical devices expose 16, 32, or 64 lanes.
920    #[must_use]
921    fn subgroup_size(&self) -> Option<u32> {
922        None
923    }
924
925    /// Maximum size in bytes of a single storage buffer the backend
926    /// accepts. `0` means the backend has not reported a limit, not
927    /// "unlimited".
928    ///
929    /// Default: `0`.
930    #[must_use]
931    fn max_storage_buffer_bytes(&self) -> u64 {
932        0
933    }
934
935    /// Unified backend-neutral device profile.
936    ///
937    /// Shared planner code should prefer this single profile over reading
938    /// individual capability methods one by one. Concrete backends may
939    /// override it when they can report richer device facts such as shared
940    /// memory size or native lowering-strategy features.
941    #[must_use]
942    fn device_profile(&self) -> crate::DeviceProfile {
943        let max_workgroup_size = self.max_workgroup_size();
944        crate::DeviceProfile {
945            backend: self.id(),
946            supports_subgroup_ops: self.supports_subgroup_ops(),
947            supports_indirect_dispatch: self.supports_indirect_dispatch(),
948            supports_distributed_collectives: self.supports_distributed_collectives(),
949            supports_specialization_constants: false,
950            supports_f16: self.supports_f16(),
951            supports_bf16: self.supports_bf16(),
952            supports_trap_propagation: false,
953            supports_tensor_cores: self.supports_tensor_cores(),
954            has_mul_high: false,
955            has_dual_issue_fp32_int32: false,
956            has_subgroup_shuffle: self.supports_subgroup_ops(),
957            has_shared_memory: false,
958            max_native_int_width: 32,
959            max_workgroup_size,
960            max_invocations_per_workgroup: self.max_compute_invocations_per_workgroup(),
961            max_shared_memory_bytes: 0,
962            max_storage_buffer_binding_size: self.max_storage_buffer_bytes(),
963            subgroup_size: self.subgroup_size().unwrap_or(0),
964            compute_units: 0,
965            regs_per_thread_max: 0,
966            l1_cache_bytes: 0,
967            l2_cache_bytes: 0,
968            mem_bw_gbps: 0,
969            timing_quality: crate::DeviceTimingQuality::HostOnly,
970            supports_device_timestamps: false,
971            supports_hardware_counters: false,
972            ideal_unroll_depth: 0,
973            ideal_vector_pack_bits: 0,
974            ideal_workgroup_tile: [0, 0, 0],
975            shared_memory_bank_count: 0,
976            shared_memory_bank_width_bytes: 0,
977        }
978    }
979
980    // ---------------------------------------------------------------
981    // Lifecycle hooks (defaulted, override as needed).
982    //
983    // These let a backend warm caches, flush pending work, recover from
984    // device loss, or tear down cleanly. Every hook defaults to a
985    // no-op-or-structured-error, so existing impls do not have to add
986    // any code.
987    // ---------------------------------------------------------------
988
989    /// Pre-dispatch warmup. Called before the first dispatch on a new
990    /// program so the backend can warm caches, compile ahead-of-time, or
991    /// acquire a device handle without paying that cost on the hot path.
992    ///
993    /// Default: no-op `Ok(())`.
994    ///
995    /// # Errors
996    ///
997    /// Returns [`BackendError`] if warmup cannot complete.
998    fn prepare(&self) -> Result<(), BackendError> {
999        Ok(())
1000    }
1001
1002    /// Flush any queued work to the device and wait for it to complete.
1003    ///
1004    /// Useful before tearing down a context or before reading back data
1005    /// that was produced by the last asynchronous dispatch.
1006    ///
1007    /// Default: no-op `Ok(())`  -  backends that do not queue work
1008    /// implicitly satisfy flush.
1009    ///
1010    /// # Errors
1011    ///
1012    /// Returns [`BackendError`] on device failure.
1013    fn flush(&self) -> Result<(), BackendError> {
1014        Ok(())
1015    }
1016
1017    /// Release device resources held by this backend. After `shutdown`
1018    /// returns the backend is in an unspecified state and may not be
1019    /// used for further dispatches.
1020    ///
1021    /// Default: no-op `Ok(())`.
1022    ///
1023    /// # Errors
1024    ///
1025    /// Returns [`BackendError`] on device failure during teardown.
1026    fn shutdown(&self) -> Result<(), BackendError> {
1027        Ok(())
1028    }
1029
1030    /// Probe whether the underlying device has been lost since the last
1031    /// successful dispatch.
1032    ///
1033    /// Default: `false` (assume healthy  -  backends that have no
1034    /// device-loss story do not need to probe).
1035    #[must_use]
1036    fn device_lost(&self) -> bool {
1037        false
1038    }
1039
1040    /// Attempt to recover from device loss by reacquiring the underlying
1041    /// device and invalidating pipeline caches.
1042    ///
1043    /// Default: returns an `UnsupportedFeature` error  -  recovery must be
1044    /// opt-in, because a backend that silently re-acquires without
1045    /// notifying the caller is a correctness hazard.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns [`BackendError::UnsupportedFeature`] by default. Backends
1050    /// that implement recovery return any error encountered during
1051    /// re-acquisition.
1052    fn try_recover(&self) -> Result<(), BackendError> {
1053        Err(BackendError::UnsupportedFeature {
1054            name: "device recovery".to_string(),
1055            backend: self.id().to_string(),
1056        })
1057    }
1058
1059    /// Allocate a backend-owned device buffer of `byte_len` bytes.
1060    ///
1061    /// The returned [`DeviceBuffer`] handle is only meaningful to the
1062    /// backend that produced it. Backends that have not opted in return
1063    /// [`BackendError::UnsupportedFeature`] with the
1064    /// `DEVICE_BUFFER_FEATURE` name; production callers that require
1065    /// resident-buffer performance must treat that as a hard capability
1066    /// failure rather than silently routing through host `Vec<u8>`
1067    /// dispatch. Real device backends (cuda/wgpu/spirv) override this to
1068    /// wrap their concrete handle (for example, a vendor device allocation,
1069    /// vulkan buffer) in a `DeviceBuffer` impl.
1070    ///
1071    /// See `crate::backend::device_buffer` for the substrate.
1072    ///
1073    /// # Errors
1074    ///
1075    /// Returns [`BackendError::UnsupportedFeature`] when the backend
1076    /// has not yet implemented persistent device-buffer allocation.
1077    fn allocate_device_buffer(
1078        &self,
1079        _byte_len: usize,
1080    ) -> Result<Box<dyn DeviceBuffer>, BackendError> {
1081        Err(unsupported_device_buffer(self.id()))
1082    }
1083
1084    /// Upload host bytes into a previously-allocated device buffer.
1085    ///
1086    /// # Errors
1087    ///
1088    /// Returns [`BackendError`] when the buffer was not allocated by this
1089    /// backend, the byte length does not match the allocation, or the
1090    /// backend has not opted in to device-buffer dispatch.
1091    fn upload_device_buffer(
1092        &self,
1093        _buffer: &mut dyn DeviceBuffer,
1094        _bytes: &[u8],
1095    ) -> Result<(), BackendError> {
1096        Err(unsupported_device_buffer(self.id()))
1097    }
1098
1099    /// Download bytes from a device buffer back to a host `Vec<u8>`.
1100    ///
1101    /// # Errors
1102    ///
1103    /// Returns [`BackendError`] when the buffer was not allocated by this
1104    /// backend or the backend has not opted in to device-buffer dispatch.
1105    fn download_device_buffer(&self, _buffer: &dyn DeviceBuffer) -> Result<Vec<u8>, BackendError> {
1106        Err(unsupported_device_buffer(self.id()))
1107    }
1108
1109    /// Free a device buffer previously returned by
1110    /// [`Self::allocate_device_buffer`]. Explicit-free is required
1111    /// because the substrate does not assume reference-counted backend
1112    /// handles; consumers are responsible for calling this when done.
1113    ///
1114    /// # Errors
1115    ///
1116    /// Returns [`BackendError`] when the buffer was not allocated by this
1117    /// backend or the underlying free fails.
1118    fn free_device_buffer(&self, _buffer: Box<dyn DeviceBuffer>) -> Result<(), BackendError> {
1119        Err(unsupported_device_buffer(self.id()))
1120    }
1121
1122    /// Dispatch a Program with backend-owned device buffers as inputs and
1123    /// outputs.
1124    ///
1125    /// Backends that have implemented [`Self::allocate_device_buffer`]
1126    /// override this method to bind their concrete buffer type without
1127    /// the host upload/download round trip the legacy `dispatch` API
1128    /// requires. Backends that have not opted in return
1129    /// [`BackendError::UnsupportedFeature`]. Callers that choose this
1130    /// API are asking for resident-buffer execution; falling back to
1131    /// host-buffer dispatch would hide the copy cost and violate the
1132    /// performance contract.
1133    ///
1134    /// # Errors
1135    ///
1136    /// Returns [`BackendError::UnsupportedFeature`] by default. Real
1137    /// implementations return any error encountered during dispatch.
1138    fn dispatch_with_device_buffers(
1139        &self,
1140        _program: &Program,
1141        _inputs: &[&dyn DeviceBuffer],
1142        _outputs: &mut [&mut dyn DeviceBuffer],
1143        _config: &DispatchConfig,
1144    ) -> Result<(), BackendError> {
1145        Err(unsupported_device_buffer(self.id()))
1146    }
1147}
1148
1149fn elapsed_resident_sequence_wall_ns(started: std::time::Instant) -> Result<u64, BackendError> {
1150    u64::try_from(started.elapsed().as_nanos()).map_err(|error| BackendError::InvalidProgram {
1151        fix: format!(
1152            "Fix: resident sequence wall timing cannot fit u64 nanoseconds: {error}. Split telemetry windows or report per-step timing."
1153        ),
1154    })
1155}
1156
1157#[cfg(test)]
1158
1159mod tests {
1160    use super::*;
1161    use std::sync::atomic::{AtomicUsize, Ordering};
1162
1163    struct TelemetryBackend;
1164
1165    impl private::Sealed for TelemetryBackend {}
1166
1167    impl VyreBackend for TelemetryBackend {
1168        fn id(&self) -> &'static str {
1169            "telemetry-test"
1170        }
1171
1172        fn dispatch(
1173            &self,
1174            _program: &Program,
1175            _inputs: &[Vec<u8>],
1176            _config: &DispatchConfig,
1177        ) -> Result<Vec<Vec<u8>>, BackendError> {
1178            Ok(vec![vec![1, 2], vec![3, 4]])
1179        }
1180    }
1181
1182    struct SequenceTimingBackend {
1183        dispatches: AtomicUsize,
1184    }
1185
1186    impl private::Sealed for SequenceTimingBackend {}
1187
1188    impl VyreBackend for SequenceTimingBackend {
1189        fn id(&self) -> &'static str {
1190            "sequence-timing-test"
1191        }
1192
1193        fn dispatch(
1194            &self,
1195            _program: &Program,
1196            _inputs: &[Vec<u8>],
1197            _config: &DispatchConfig,
1198        ) -> Result<Vec<Vec<u8>>, BackendError> {
1199            Ok(Vec::new())
1200        }
1201
1202        fn dispatch_resident_timed(
1203            &self,
1204            _program: &Program,
1205            _resources: &[Resource],
1206            config: &DispatchConfig,
1207        ) -> Result<TimedDispatchResult, BackendError> {
1208            let index = self.dispatches.fetch_add(1, Ordering::SeqCst) as u64;
1209            assert_eq!(
1210                config.grid_override,
1211                Some([index as u32 + 1, 1, 1]),
1212                "Fix: default resident sequence timing must preserve each step's grid override."
1213            );
1214            Ok(TimedDispatchResult {
1215                outputs: Vec::new(),
1216                wall_ns: 10 + index,
1217                device_ns: Some(7 + index),
1218                enqueue_ns: Some(3 + index),
1219                wait_ns: Some(4 + index),
1220            })
1221        }
1222
1223        fn download_resident_ranges_into(
1224            &self,
1225            ranges: &[(&Resource, usize, usize)],
1226            outputs: &mut [&mut Vec<u8>],
1227        ) -> Result<(), BackendError> {
1228            assert_eq!(ranges.len(), outputs.len());
1229            for ((resource, offset, len), output) in ranges.iter().zip(outputs.iter_mut()) {
1230                let Resource::Resident(handle) = resource else {
1231                    panic!("Fix: default timed resident sequence test expects resident resources.");
1232                };
1233                output.clear();
1234                output.extend_from_slice(&handle.id().to_le_bytes());
1235                output.extend_from_slice(&(*offset as u64).to_le_bytes());
1236                output.extend_from_slice(&(*len as u64).to_le_bytes());
1237            }
1238            Ok(())
1239        }
1240    }
1241
1242    #[test]
1243    fn default_borrowed_into_dispatch_records_runtime_telemetry() {
1244        let _guard = crate::observability::audit_events_test_lock();
1245        let before = crate::observability::snapshot_dispatch_telemetry();
1246        let backend = TelemetryBackend;
1247        let mut outputs = vec![Vec::with_capacity(4), Vec::with_capacity(1)];
1248
1249        backend
1250            .dispatch_borrowed_into(
1251                &Program::empty(),
1252                &[&[9, 8, 7]],
1253                &DispatchConfig::default(),
1254                &mut outputs,
1255            )
1256            .expect("Fix: default borrowed-into dispatch must succeed");
1257
1258        let telemetry = crate::observability::snapshot_dispatch_telemetry();
1259        assert!(telemetry.launches >= before.launches + 1);
1260        assert!(telemetry.input_bytes >= before.input_bytes + 3);
1261        assert!(telemetry.output_bytes >= before.output_bytes + 4);
1262        assert!(telemetry.output_slots >= before.output_slots + 2);
1263        assert!(telemetry.output_slots_reused >= before.output_slots_reused + 1);
1264        assert!(telemetry.output_slots_moved >= before.output_slots_moved + 1);
1265        assert!(telemetry.output_slots_appended >= before.output_slots_appended);
1266    }
1267
1268    #[test]
1269    fn default_resident_sequence_timing_sums_step_device_times_and_reads_ranges() {
1270        let backend = SequenceTimingBackend {
1271            dispatches: AtomicUsize::new(0),
1272        };
1273        let program = Program::empty();
1274        let owner = crate::ResidentOwner::new().expect("Fix: owner ids must be available");
1275        let first_resources = [Resource::Resident(owner.handle(11))];
1276        let second_resources = [Resource::Resident(owner.handle(22))];
1277        let steps = [
1278            ResidentDispatchStep {
1279                program: &program,
1280                resources: &first_resources,
1281                grid_override: Some([1, 1, 1]),
1282                workgroup_override: None,
1283            },
1284            ResidentDispatchStep {
1285                program: &program,
1286                resources: &second_resources,
1287                grid_override: Some([2, 1, 1]),
1288                workgroup_override: None,
1289            },
1290        ];
1291        let read_resource = Resource::Resident(owner.handle(33));
1292        let reads = [ResidentReadRange {
1293            resource: &read_resource,
1294            byte_offset: 4,
1295            byte_len: 8,
1296        }];
1297        let mut output = Vec::new();
1298
1299        let timing = backend
1300            .dispatch_resident_sequence_read_ranges_timed_into(&steps, &reads, &mut [&mut output])
1301            .expect("Fix: default timed resident sequence must execute and read ranges.");
1302
1303        assert_eq!(backend.dispatches.load(Ordering::SeqCst), 2);
1304        assert_eq!(timing.device_ns, Some(15));
1305        assert_eq!(timing.enqueue_ns, Some(7));
1306        assert_eq!(timing.wait_ns, Some(9));
1307        assert!(timing.wall_ns > 0);
1308        assert_eq!(output.len(), 24);
1309        assert_eq!(u64::from_le_bytes(output[0..8].try_into().unwrap()), 33);
1310        assert_eq!(u64::from_le_bytes(output[8..16].try_into().unwrap()), 4);
1311        assert_eq!(u64::from_le_bytes(output[16..24].try_into().unwrap()), 8);
1312    }
1313
1314    #[test]
1315    fn default_dispatch_paths_use_shared_fallible_staging_and_checked_timing() {
1316        let backend_source = include_str!("vyre_backend.rs");
1317        let compiled_source = include_str!("compiled_pipeline.rs");
1318        let module_source = include_str!("../backend.rs");
1319
1320        assert!(
1321            module_source.contains("fn clone_borrowed_inputs_for_dispatch")
1322                && module_source.contains("fn reserve_batch_output_slots")
1323                && module_source.contains("fn checked_elapsed_wall_ns"),
1324            "Fix: backend defaults must share one fallible staging and checked timing contract."
1325        );
1326        for source in [backend_source, compiled_source] {
1327            let production = source
1328                .split("#[cfg(test)]")
1329                .next()
1330                .expect("Fix: backend source must contain production section before tests");
1331            assert!(
1332                production.contains("clone_borrowed_inputs_for_dispatch")
1333                    && production.contains("checked_elapsed_wall_ns")
1334                    && !production.contains(".as_nanos() as u64")
1335                    && !production.contains("inputs.iter().map(|input| (*input).to_vec()).collect()"),
1336                "Fix: inherited backend dispatch defaults must avoid infallible borrowed-input collection and lossy wall timing."
1337            );
1338        }
1339        let compiled_production = compiled_source
1340            .split("#[cfg(test)]")
1341            .next()
1342            .expect("Fix: compiled pipeline source must contain production section before tests");
1343        assert!(
1344            compiled_production.contains("reserved_batch_output_slots")
1345                && !compiled_production.contains("Vec::with_capacity(batches.len())"),
1346            "Fix: compiled-pipeline batch defaults must construct output slots through shared fallible staging."
1347        );
1348    }
1349}