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