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