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}