Skip to main content

vyre_self_substrate/optimizer/
dispatcher.rs

1//! Dispatcher trait  -  the seam between the self-hosted optimizer and
2//! a backend that can actually run vyre Programs.
3//!
4//! The optimizer encodes the user's Program into ProgramGraph buffers,
5//! builds a vyre Program that does the analysis (e.g. `persistent_bfs`),
6//! and asks an `OptimizerDispatcher` to run that analysis Program. The
7//! returned bytes drive the rewrite.
8//!
9//! `vyre-self-substrate` cannot depend on a concrete backend  -  it sits
10//! below the driver layer. The trait inverts that dependency: the
11//! orchestrator code stays in self-substrate, and a backend crate
12//! (e.g. `vyre-driver-wgpu` or a runtime wrapper) provides the impl.
13//!
14//! Test code in this crate uses `oracle::CpuOracleDispatcher` so the
15//! encoder can be proven sound against the existing primitive oracles
16//! before any GPU backend is wired. The CPU oracle is gated to tests
17//! only  -  it is never on a production code path.
18
19use vyre_foundation::ir::Program;
20
21/// One resident-buffer kernel launch in an ordered optimizer sequence.
22pub struct ResidentDispatchStep<'a> {
23    /// Program to launch.
24    pub program: &'a Program,
25    /// Resident handle ids in canonical buffer binding order.
26    pub handle_ids: &'a [u64],
27    /// Optional launch grid override.
28    pub grid_override: Option<[u32; 3]>,
29}
30
31/// One byte range to read from a resident buffer after an ordered sequence.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct ResidentReadRange {
34    /// Resident handle id.
35    pub handle_id: u64,
36    /// First byte to read from the device buffer.
37    pub byte_offset: usize,
38    /// Number of meaningful bytes to transfer.
39    pub byte_len: usize,
40}
41
42/// Resident handles for immutable payloads that may stay device-resident
43/// across optimizer calls.
44///
45/// `retained_by_dispatcher` means the dispatcher owns the handles after the
46/// caller is done with the current launch sequence. Call
47/// [`OptimizerDispatcher::release_resident_static_uploads`] instead of
48/// `free_resident` so CUDA can keep read-only graph/arena buffers hot while
49/// portable dispatchers free them immediately.
50#[derive(Debug)]
51pub struct ResidentStaticBufferSet {
52    /// Resident handle ids in the same order as the payload slice passed to
53    /// `acquire_resident_static_uploads`.
54    pub handles: Vec<u64>,
55    /// True when the handles were already resident and no host upload was paid.
56    pub cache_hit: bool,
57    /// True when the dispatcher retained ownership for future reuse.
58    pub retained_by_dispatcher: bool,
59}
60
61/// Errors a dispatcher may surface. Concrete backends compose their
62/// own error types into this; the orchestrator only needs the
63/// boundary message.
64#[derive(Debug)]
65pub enum DispatchError {
66    /// The dispatcher rejected the Program. The string carries the
67    /// backend's actionable message (must contain `Fix:`).
68    Rejected(String),
69    /// Input arity or shape did not match the Program's declared
70    /// buffer set. Hard error  -  not retryable.
71    BadInputs(String),
72    /// Backend raised an internal error. Same shape as `Rejected` but
73    /// the cause is in the backend, not the Program.
74    BackendError(String),
75}
76
77impl std::fmt::Display for DispatchError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::Rejected(msg) => write!(f, "dispatcher rejected program: {msg}"),
81            Self::BadInputs(msg) => write!(f, "dispatcher input mismatch: {msg}"),
82            Self::BackendError(msg) => write!(f, "dispatcher backend error: {msg}"),
83        }
84    }
85}
86
87impl std::error::Error for DispatchError {}
88
89/// Run a vyre Program with byte inputs, return byte outputs in the
90/// Program's declared output order.
91///
92/// This is the canonical dispatch boundary. Production impls go
93/// through `vyre-driver-wgpu` or `vyre-driver-cuda`; test impls use
94/// CPU oracles (gated to test-only builds).
95pub trait OptimizerDispatcher {
96    /// Dispatch `program` with the given byte inputs (one `Vec<u8>`
97    /// per declared input buffer in canonical buffer order). Returns
98    /// the declared outputs in the same canonical order.
99    ///
100    /// `grid_override` lets parallel kernels dispatch enough
101    /// workgroups to cover their input. `None` means "use the
102    /// backend's default grid" (typically `[1, 1, 1]`), which is what
103    /// sequential single-thread Programs want. Parallel passes
104    /// compute `Some([ceil(work/wg_x), 1, 1])` based on the input
105    /// size and their declared workgroup_size.
106    fn dispatch(
107        &self,
108        program: &Program,
109        inputs: &[Vec<u8>],
110        grid_override: Option<[u32; 3]>,
111    ) -> Result<Vec<Vec<u8>>, DispatchError>;
112
113    /// Whether this dispatcher supports the persistent-resident path.
114    /// Default: false. CUDA backend overrides to true. The orchestrator
115    /// uses this to decide whether to take the persistent fast-path
116    /// (encode arena once → upload once → dispatch many → readback once)
117    /// or use the non-resident per-call GPU dispatch path.
118    fn supports_persistent(&self) -> bool {
119        false
120    }
121
122    /// Device/lowering feature bits that affect reusable plan identity.
123    ///
124    /// Backends with feature-dependent lowering must override this so
125    /// self-substrate plan caches cannot replay a Program shape prepared for a
126    /// different hardware/lowering capability set. Test-only and reference
127    /// dispatchers keep the zero default because they do not specialize plans by
128    /// device.
129    fn device_feature_cache_key(&self) -> u64 {
130        0
131    }
132
133    /// Allocate a backend-resident buffer. Returns an opaque u64
134    /// handle. Callers must `free_resident` to release.
135    fn alloc_resident(&self, _byte_len: usize) -> Result<u64, DispatchError> {
136        Err(DispatchError::Rejected(
137            "Fix: this dispatcher does not implement the persistent path; \
138             use `dispatch` instead, or wire the resident-buffer methods."
139                .to_string(),
140        ))
141    }
142
143    /// Allocate a logical group of resident buffers and roll back partial state
144    /// if any allocation fails.
145    fn alloc_resident_many(&self, byte_lens: &[usize]) -> Result<Vec<u64>, DispatchError> {
146        let mut handles = Vec::new();
147        handles.try_reserve(byte_lens.len()).map_err(|error| {
148            DispatchError::BackendError(format!(
149                "Fix: reserve resident handle group before allocation; requested {} buffer(s): {error}.",
150                byte_lens.len()
151            ))
152        })?;
153        for (index, &byte_len) in byte_lens.iter().enumerate() {
154            match self.alloc_resident(byte_len) {
155                Ok(handle) => handles.push(handle),
156                Err(error) => {
157                    let allocation_error = error.to_string();
158                    if let Err(free_error) = free_resident_handles(
159                        self,
160                        &handles,
161                        "resident grouped allocation rollback",
162                    ) {
163                        return Err(DispatchError::BackendError(format!(
164                            "Fix: resident grouped allocation failed at buffer {index} after {} partial allocation(s): {allocation_error}; rollback also failed: {free_error}.",
165                            handles.len()
166                        )));
167                    }
168                    return Err(error);
169                }
170            }
171        }
172        Ok(handles)
173    }
174
175    /// Upload host bytes into a resident buffer.
176    fn upload_resident(&self, _handle: u64, _bytes: &[u8]) -> Result<(), DispatchError> {
177        Err(DispatchError::Rejected(
178            "Fix: dispatcher does not implement upload_resident.".to_string(),
179        ))
180    }
181
182    /// Upload several resident buffers with one backend fence when supported.
183    fn upload_resident_many(&self, uploads: &[(u64, &[u8])]) -> Result<(), DispatchError> {
184        for &(handle, bytes) in uploads {
185            self.upload_resident(handle, bytes)?;
186        }
187        Ok(())
188    }
189
190    /// Acquire resident handles for immutable payloads.
191    ///
192    /// Portable default behavior allocates and uploads exactly like
193    /// `alloc_resident` + `upload_resident_many`, then returns
194    /// `retained_by_dispatcher = false` so release frees the buffers. CUDA
195    /// overrides this to content-address immutable optimizer buffers and skip
196    /// H2D traffic on warmed identical programs.
197    fn acquire_resident_static_uploads(
198        &self,
199        _cache_domain: u64,
200        payloads: &[&[u8]],
201    ) -> Result<ResidentStaticBufferSet, DispatchError> {
202        let mut byte_lens = Vec::new();
203        byte_lens.try_reserve(payloads.len()).map_err(|error| {
204            DispatchError::BackendError(format!(
205                "Fix: reserve resident static byte lengths before upload; requested {} payload(s): {error}.",
206                payloads.len()
207            ))
208        })?;
209        for payload in payloads {
210            byte_lens.push(payload.len());
211        }
212        let handles = self.alloc_resident_many(&byte_lens)?;
213
214        let mut uploads = Vec::new();
215        uploads.try_reserve(payloads.len()).map_err(|error| {
216            DispatchError::BackendError(format!(
217                "Fix: reserve resident static upload storage before upload; requested {} payload(s): {error}.",
218                payloads.len()
219            ))
220        })?;
221        for (&handle, &payload) in handles.iter().zip(payloads.iter()) {
222            uploads.push((handle, payload));
223        }
224
225        if let Err(error) = self.upload_resident_many(&uploads) {
226            let upload_error = error.to_string();
227            if let Err(free_error) =
228                free_resident_handles(self, &handles, "resident static upload rollback")
229            {
230                return Err(DispatchError::BackendError(format!(
231                    "Fix: resident static upload failed after allocating {} buffer(s): {upload_error}; rollback also failed: {free_error}.",
232                    handles.len()
233                )));
234            }
235            return Err(error);
236        }
237
238        Ok(ResidentStaticBufferSet {
239            handles,
240            cache_hit: false,
241            retained_by_dispatcher: false,
242        })
243    }
244
245    /// Release a static resident buffer set acquired from
246    /// [`Self::acquire_resident_static_uploads`].
247    fn release_resident_static_uploads(
248        &self,
249        set: ResidentStaticBufferSet,
250    ) -> Result<(), DispatchError> {
251        if set.retained_by_dispatcher {
252            return Ok(());
253        }
254        for handle in set.handles {
255            self.free_resident(handle)?;
256        }
257        Ok(())
258    }
259
260    /// Download a resident buffer's current contents to host bytes.
261    fn read_resident(&self, _handle: u64) -> Result<Vec<u8>, DispatchError> {
262        Err(DispatchError::Rejected(
263            "Fix: dispatcher does not implement read_resident.".to_string(),
264        ))
265    }
266
267    /// Download several resident buffers with one backend fence when supported.
268    fn read_resident_many(&self, handles: &[u64]) -> Result<Vec<Vec<u8>>, DispatchError> {
269        handles
270            .iter()
271            .map(|&handle| self.read_resident(handle))
272            .collect()
273    }
274
275    /// Download selected byte ranges from resident buffers.
276    fn read_resident_ranges(
277        &self,
278        ranges: &[ResidentReadRange],
279    ) -> Result<Vec<Vec<u8>>, DispatchError> {
280        let mut outputs = Vec::new();
281        self.read_resident_ranges_into(ranges, &mut outputs)?;
282        Ok(outputs)
283    }
284
285    /// Download selected byte ranges from resident buffers into caller-owned
286    /// byte slots.
287    fn read_resident_ranges_into(
288        &self,
289        ranges: &[ResidentReadRange],
290        outputs: &mut Vec<Vec<u8>>,
291    ) -> Result<(), DispatchError> {
292        let mut unique_handles = Vec::new();
293        unique_handles.try_reserve(ranges.len()).map_err(|error| {
294            DispatchError::BackendError(format!(
295                "Fix: reserve resident ranged-read handle dedupe storage before dispatch; requested {} range(s): {error}.",
296                ranges.len()
297            ))
298        })?;
299        let mut range_handle_indices = Vec::new();
300        range_handle_indices
301            .try_reserve(ranges.len())
302            .map_err(|error| {
303                DispatchError::BackendError(format!(
304                    "Fix: reserve resident ranged-read index storage before dispatch; requested {} range(s): {error}.",
305                    ranges.len()
306                ))
307            })?;
308        for range in ranges {
309            if let Some(index) = unique_handles
310                .iter()
311                .position(|&handle| handle == range.handle_id)
312            {
313                range_handle_indices.push(index);
314            } else {
315                let index = unique_handles.len();
316                unique_handles.push(range.handle_id);
317                range_handle_indices.push(index);
318            }
319        }
320        let full_buffers = self.read_resident_many(&unique_handles)?;
321        if full_buffers.len() != unique_handles.len() {
322            return Err(DispatchError::BackendError(format!(
323                "Fix: resident ranged-read batch returned {} buffer(s) for {} unique handle(s).",
324                full_buffers.len(),
325                unique_handles.len()
326            )));
327        }
328        if outputs.len() < ranges.len() {
329            outputs
330                .try_reserve(ranges.len() - outputs.len())
331                .map_err(|error| {
332                    DispatchError::BackendError(format!(
333                        "Fix: reserve resident ranged-read output storage before dispatch; requested {} range(s): {error}.",
334                        ranges.len()
335                    ))
336                })?;
337            outputs.resize_with(ranges.len(), Vec::new);
338        } else {
339            outputs.truncate(ranges.len());
340        }
341        for ((range, &buffer_index), output) in ranges
342            .iter()
343            .zip(range_handle_indices.iter())
344            .zip(outputs.iter_mut())
345        {
346            let full = full_buffers.get(buffer_index).ok_or_else(|| {
347                DispatchError::BackendError(format!(
348                    "Fix: resident ranged-read handle index {buffer_index} missing from {} readback buffer(s).",
349                    full_buffers.len()
350                ))
351            })?;
352            let end = range
353                .byte_offset
354                .checked_add(range.byte_len)
355                .ok_or_else(|| {
356                    DispatchError::BadInputs(format!(
357                    "Fix: resident read range for handle {} overflows usize at offset {} len {}.",
358                    range.handle_id, range.byte_offset, range.byte_len
359                ))
360                })?;
361            if end > full.len() {
362                return Err(DispatchError::BadInputs(format!(
363                    "Fix: resident read range for handle {} requested bytes [{}..{}) but buffer readback has {} bytes.",
364                    range.handle_id,
365                    range.byte_offset,
366                    end,
367                    full.len()
368                )));
369            }
370            output.clear();
371            output.extend_from_slice(&full[range.byte_offset..end]);
372        }
373        Ok(())
374    }
375
376    /// Free a resident buffer previously returned by `alloc_resident`.
377    fn free_resident(&self, _handle: u64) -> Result<(), DispatchError> {
378        Err(DispatchError::Rejected(
379            "Fix: dispatcher does not implement free_resident.".to_string(),
380        ))
381    }
382
383    /// Dispatch a Program against resident-buffer handles. Each
384    /// handle is referenced from the Program's declared buffer in the
385    /// same canonical buffer order. RW buffers are not read back  -
386    /// caller invokes `read_resident` once at end of pipeline.
387    fn dispatch_resident(
388        &self,
389        _program: &Program,
390        _handles: &[u64],
391        _grid_override: Option<[u32; 3]>,
392    ) -> Result<(), DispatchError> {
393        Err(DispatchError::Rejected(
394            "Fix: dispatcher does not implement dispatch_resident.".to_string(),
395        ))
396    }
397
398    /// Dispatch an ordered sequence of resident-buffer Programs.
399    ///
400    /// Default implementation preserves correctness by fencing each step
401    /// through `dispatch_resident`. CUDA overrides this to enqueue the whole
402    /// dependent chain on one stream and synchronize once.
403    fn dispatch_resident_sequence(
404        &self,
405        steps: &[ResidentDispatchStep<'_>],
406    ) -> Result<(), DispatchError> {
407        for step in steps {
408            self.dispatch_resident(step.program, step.handle_ids, step.grid_override)?;
409        }
410        Ok(())
411    }
412
413    /// Dispatch an ordered resident sequence and read selected resident buffers.
414    ///
415    /// Default implementation keeps the portable contract: execute the ordered
416    /// sequence, then read buffers through `read_resident_many`. CUDA overrides
417    /// this to enqueue the D2H readbacks behind the kernels on the same stream
418    /// and pay one host fence.
419    fn dispatch_resident_sequence_read_many(
420        &self,
421        steps: &[ResidentDispatchStep<'_>],
422        read_handles: &[u64],
423    ) -> Result<Vec<Vec<u8>>, DispatchError> {
424        self.dispatch_resident_sequence(steps)?;
425        self.read_resident_many(read_handles)
426    }
427
428    /// Dispatch an ordered resident sequence and read selected byte ranges.
429    fn dispatch_resident_sequence_read_ranges(
430        &self,
431        steps: &[ResidentDispatchStep<'_>],
432        read_ranges: &[ResidentReadRange],
433    ) -> Result<Vec<Vec<u8>>, DispatchError> {
434        self.dispatch_resident_sequence(steps)?;
435        self.read_resident_ranges(read_ranges)
436    }
437
438    /// Upload resident buffers, dispatch an ordered resident sequence, then
439    /// read selected resident buffers.
440    ///
441    /// Default implementation fences at each portable boundary. CUDA overrides
442    /// this so H2D uploads, kernels, and D2H readbacks are ordered on one stream
443    /// with one host synchronization point.
444    fn upload_resident_many_sequence_read_many(
445        &self,
446        uploads: &[(u64, &[u8])],
447        steps: &[ResidentDispatchStep<'_>],
448        read_handles: &[u64],
449    ) -> Result<Vec<Vec<u8>>, DispatchError> {
450        self.upload_resident_many(uploads)?;
451        self.dispatch_resident_sequence_read_many(steps, read_handles)
452    }
453
454    /// Upload resident buffers, dispatch an ordered resident sequence, then
455    /// read selected byte ranges.
456    fn upload_resident_many_sequence_read_ranges(
457        &self,
458        uploads: &[(u64, &[u8])],
459        steps: &[ResidentDispatchStep<'_>],
460        read_ranges: &[ResidentReadRange],
461    ) -> Result<Vec<Vec<u8>>, DispatchError> {
462        self.upload_resident_many(uploads)?;
463        self.dispatch_resident_sequence_read_ranges(steps, read_ranges)
464    }
465
466    /// Same contract as [`Self::upload_resident_many_sequence_read_many`],
467    /// but writes readbacks into caller-owned byte slots.
468    fn upload_resident_many_sequence_read_many_into(
469        &self,
470        uploads: &[(u64, &[u8])],
471        steps: &[ResidentDispatchStep<'_>],
472        read_handles: &[u64],
473        outputs: &mut Vec<Vec<u8>>,
474    ) -> Result<(), DispatchError> {
475        let readbacks =
476            self.upload_resident_many_sequence_read_many(uploads, steps, read_handles)?;
477        if outputs.len() < readbacks.len() {
478            outputs.resize_with(readbacks.len(), Vec::new);
479        } else {
480            outputs.truncate(readbacks.len());
481        }
482        for (slot, readback) in outputs.iter_mut().zip(readbacks) {
483            slot.clear();
484            slot.extend_from_slice(&readback);
485        }
486        Ok(())
487    }
488
489    /// Same contract as [`Self::upload_resident_many_sequence_read_many_into`],
490    /// but first clears full resident buffers to zero.
491    ///
492    /// Portable dispatchers emulate clears as zero-byte payload uploads and
493    /// still pay one upload/sequence/read boundary. CUDA overrides this to
494    /// enqueue device-side memset operations on the same stream before explicit
495    /// uploads and kernels, avoiding PCIe traffic for scratch initialization
496    /// without adding host fences.
497    fn clear_upload_resident_many_sequence_read_many_into(
498        &self,
499        clears: &[(u64, usize)],
500        uploads: &[(u64, &[u8])],
501        steps: &[ResidentDispatchStep<'_>],
502        read_handles: &[u64],
503        outputs: &mut Vec<Vec<u8>>,
504    ) -> Result<(), DispatchError> {
505        if clears.is_empty() {
506            return self.upload_resident_many_sequence_read_many_into(
507                uploads,
508                steps,
509                read_handles,
510                outputs,
511            );
512        }
513        let mut fills = Vec::new();
514        fills.try_reserve(clears.len()).map_err(|error| {
515            DispatchError::BackendError(format!(
516                "Fix: reserve resident clear fill descriptors before dispatch; requested {} clear(s): {error}.",
517                clears.len()
518            ))
519        })?;
520        for &(handle, byte_len) in clears {
521            fills.push((handle, byte_len, 0));
522        }
523        self.fill_upload_resident_many_sequence_read_many_into(
524            &fills,
525            uploads,
526            steps,
527            read_handles,
528            outputs,
529        )
530    }
531
532    /// Same contract as
533    /// [`Self::clear_upload_resident_many_sequence_read_many_into`], but fills
534    /// each resident buffer with an arbitrary byte value.
535    fn fill_upload_resident_many_sequence_read_many_into(
536        &self,
537        fills: &[(u64, usize, u8)],
538        uploads: &[(u64, &[u8])],
539        steps: &[ResidentDispatchStep<'_>],
540        read_handles: &[u64],
541        outputs: &mut Vec<Vec<u8>>,
542    ) -> Result<(), DispatchError> {
543        if fills.is_empty() {
544            return self.upload_resident_many_sequence_read_many_into(
545                uploads,
546                steps,
547                read_handles,
548                outputs,
549            );
550        }
551
552        with_staged_fill_uploads(
553            fills,
554            uploads,
555            "resident fill payloads",
556            "resident fill/upload payloads",
557            |combined_uploads| {
558                self.upload_resident_many_sequence_read_many_into(
559                    combined_uploads,
560                    steps,
561                    read_handles,
562                    outputs,
563                )
564            },
565        )
566    }
567
568    /// Same contract as [`Self::upload_resident_many_sequence_read_ranges_into`],
569    /// but fills resident buffers first. CUDA overrides this to use device
570    /// memset and compact D2H range copies on the same stream.
571    fn fill_upload_resident_many_sequence_read_ranges_into(
572        &self,
573        fills: &[(u64, usize, u8)],
574        uploads: &[(u64, &[u8])],
575        steps: &[ResidentDispatchStep<'_>],
576        read_ranges: &[ResidentReadRange],
577        outputs: &mut Vec<Vec<u8>>,
578    ) -> Result<(), DispatchError> {
579        if fills.is_empty() {
580            return self.upload_resident_many_sequence_read_ranges_into(
581                uploads,
582                steps,
583                read_ranges,
584                outputs,
585            );
586        }
587
588        with_staged_fill_uploads(
589            fills,
590            uploads,
591            "resident range-fill payloads",
592            "resident range-fill/upload payloads",
593            |combined_uploads| {
594                self.upload_resident_many_sequence_read_ranges_into(
595                    combined_uploads,
596                    steps,
597                    read_ranges,
598                    outputs,
599                )
600            },
601        )
602    }
603
604    /// Same contract as [`Self::upload_resident_many_sequence_read_ranges`],
605    /// but writes compact readbacks into caller-owned byte slots.
606    fn upload_resident_many_sequence_read_ranges_into(
607        &self,
608        uploads: &[(u64, &[u8])],
609        steps: &[ResidentDispatchStep<'_>],
610        read_ranges: &[ResidentReadRange],
611        outputs: &mut Vec<Vec<u8>>,
612    ) -> Result<(), DispatchError> {
613        self.upload_resident_many(uploads)?;
614        self.dispatch_resident_sequence(steps)?;
615        self.read_resident_ranges_into(read_ranges, outputs)
616    }
617}
618
619fn free_resident_handles<D: OptimizerDispatcher + ?Sized>(
620    dispatcher: &D,
621    handles: &[u64],
622    context: &str,
623) -> Result<(), DispatchError> {
624    for (index, &handle) in handles.iter().enumerate() {
625        dispatcher.free_resident(handle).map_err(|error| {
626            DispatchError::BackendError(format!(
627                "Fix: {context} failed to free resident handle {handle} at index {index}: {error}."
628            ))
629        })?;
630    }
631    Ok(())
632}
633
634fn with_staged_fill_uploads<R>(
635    fills: &[(u64, usize, u8)],
636    uploads: &[(u64, &[u8])],
637    fill_context: &'static str,
638    combined_context: &'static str,
639    run: impl FnOnce(&[(u64, &[u8])]) -> Result<R, DispatchError>,
640) -> Result<R, DispatchError> {
641    let mut fill_payloads = Vec::new();
642    fill_payloads.try_reserve(fills.len()).map_err(|error| {
643        DispatchError::BackendError(format!(
644            "Fix: reserve {fill_context} before dispatch; requested {} fill(s): {error}.",
645            fills.len()
646        ))
647    })?;
648    for &(_, byte_len, value) in fills {
649        fill_payloads.push(vec![value; byte_len]);
650    }
651
652    let mut combined_uploads = Vec::new();
653    combined_uploads
654        .try_reserve(fills.len() + uploads.len())
655        .map_err(|error| {
656            DispatchError::BackendError(format!(
657                "Fix: reserve {combined_context} before dispatch; requested {} fill(s) and {} upload(s): {error}.",
658                fills.len(),
659                uploads.len()
660            ))
661        })?;
662    for ((handle, _, _), fill) in fills.iter().zip(fill_payloads.iter()) {
663        combined_uploads.push((*handle, fill.as_slice()));
664    }
665    combined_uploads.extend_from_slice(uploads);
666
667    run(&combined_uploads)
668}
669
670#[cfg(any(test, feature = "cpu-parity"))]
671pub mod oracle {
672    //! CPU oracle dispatcher for tests and explicit CPU-parity builds. Maps a small allowlist of
673    //! self-hosted-optimizer Programs onto their `vyre_primitives`
674    //! `cpu_ref` reference implementations and reproduces the
675    //! dispatch byte contract.
676    //!
677    //! This module exists to prove the encoder/decoder are sound
678    //! against the same numerical contract the production GPU path
679    //! must honor. It is not compiled unless tests or `cpu-parity` are enabled.
680    //!
681    //! Adding a Program here means the oracle hand-writes the byte
682    //! marshalling that the WgpuBackend dispatcher infers from
683    //! `BufferDecl`s. That duplication is acceptable for tests; a
684    //! production dispatcher reflectively reads BufferDecls.
685    //!
686    //! For now we cover the Programs the orchestrator currently
687    //! invokes (DCE → `persistent_bfs`). When CSE / const-fold land
688    //! they each add a small case here.
689
690    use super::{DispatchError, OptimizerDispatcher};
691    use vyre_foundation::ir::Program;
692
693    /// CPU oracle dispatcher. Recognizes only the optimizer's own
694    /// canonical Programs by matching the wrapping Region's generator
695    /// op-id and the declared buffer set.
696    pub struct CpuOracleDispatcher;
697
698    impl CpuOracleDispatcher {
699        /// Construct the oracle dispatcher. Cheap; does no backend
700        /// probing.
701        #[must_use]
702        pub fn new() -> Self {
703            Self
704        }
705    }
706
707    impl Default for CpuOracleDispatcher {
708        fn default() -> Self {
709            Self::new()
710        }
711    }
712
713    impl OptimizerDispatcher for CpuOracleDispatcher {
714        fn dispatch(
715            &self,
716            program: &Program,
717            inputs: &[Vec<u8>],
718            _grid_override: Option<[u32; 3]>,
719        ) -> Result<Vec<Vec<u8>>, DispatchError> {
720            // Identify the optimizer Program by its top-level Region
721            // generator. Self-hosted Programs all wrap their bodies
722            // in a Region with a known op-id.
723            let generator = top_level_region_generator(program).ok_or_else(|| {
724                DispatchError::Rejected(
725                    "Fix: oracle dispatcher only accepts canonical \
726                     graph-primitive Programs whose entry is a single \
727                     wrapping Region with a generator id."
728                        .to_string(),
729                )
730            })?;
731
732            match generator {
733                vyre_primitives::graph::persistent_bfs::OP_ID => {
734                    persistent_bfs_oracle(program, inputs)
735                }
736                crate::optimizer::dce_program::OP_ID => persistent_bfs_oracle(program, inputs),
737                vyre_primitives::graph::exploded::OP_ID => {
738                    exploded_ifds_csr_oracle(program, inputs)
739                }
740                other => Err(DispatchError::Rejected(format!(
741                    "Fix: oracle dispatcher does not recognize generator \
742                     `{other}`. Wire the oracle for this primitive or \
743                     dispatch through the production backend."
744                ))),
745            }
746        }
747    }
748
749    fn top_level_region_generator(program: &Program) -> Option<&str> {
750        match program.entry() {
751            [vyre_foundation::ir::Node::Region { generator, .. }] => Some(generator.as_str()),
752            _ => None,
753        }
754    }
755
756    fn persistent_bfs_oracle(
757        program: &Program,
758        inputs: &[Vec<u8>],
759    ) -> Result<Vec<Vec<u8>>, DispatchError> {
760        // Buffer order (per `persistent_bfs.rs::persistent_bfs`):
761        //   0 pg_nodes (RO)
762        //   1 pg_edge_offsets (RO)
763        //   2 pg_edge_targets (RO)
764        //   3 pg_edge_kind_mask (RO)
765        //   4 pg_node_tags (RO)
766        //   5 frontier_in (RO)
767        //   6 frontier_out (RW)
768        //   7 changed (RW)
769        //   8 wg_scratch (workgroup)   -  not an input
770        if inputs.len() < 6 {
771            return Err(DispatchError::BadInputs(format!(
772                "Fix: persistent_bfs oracle expects ≥ 6 input buffers, got {}",
773                inputs.len()
774            )));
775        }
776        let nodes = crate::hardware::dispatch_buffers::read_u32s(&inputs[0]);
777        let edge_offsets = crate::hardware::dispatch_buffers::read_u32s(&inputs[1]);
778        let edge_targets_raw = crate::hardware::dispatch_buffers::read_u32s(&inputs[2]);
779        let edge_kind_mask_raw = crate::hardware::dispatch_buffers::read_u32s(&inputs[3]);
780        let _node_tags = crate::hardware::dispatch_buffers::read_u32s(&inputs[4]);
781        let frontier_in = crate::hardware::dispatch_buffers::read_u32s(&inputs[5]);
782
783        // The Region carries the shape and max_iters in its body
784        // structure; rather than re-derive that from IR walks, the
785        // oracle re-computes via cpu_ref using the buffers' lengths.
786        let node_count = nodes.len() as u32;
787
788        // Iteration cap: if the caller declared `frontier_in` of length L
789        // (= bitset_words(node_count)) the oracle uses `node_count` as
790        // the saturation budget  -  same default the Program builder uses
791        // when callers want closure.
792        let max_iters = node_count.max(1);
793
794        let _ = program; // reserved for future cross-checks
795        let allow_mask = u32::MAX;
796        let edge_count = declared_edge_count(&edge_offsets)?;
797        let edge_targets = trim_padded_edge_buffer("edge_targets", &edge_targets_raw, edge_count)?;
798        let edge_kind_mask =
799            trim_padded_edge_buffer("edge_kind_mask", &edge_kind_mask_raw, edge_count)?;
800
801        let (frontier_out, changed) = vyre_primitives::graph::persistent_bfs::cpu_ref(
802            node_count,
803            &edge_offsets,
804            edge_targets,
805            edge_kind_mask,
806            &frontier_in,
807            allow_mask,
808            max_iters,
809        );
810
811        // Outputs in declared order: frontier_out first, then changed.
812        let frontier_bytes = u32_buffer_to_bytes(&frontier_out);
813        let changed_bytes = u32_buffer_to_bytes(&[changed]);
814        Ok(vec![frontier_bytes, changed_bytes])
815    }
816
817    fn exploded_ifds_csr_oracle(
818        program: &Program,
819        inputs: &[Vec<u8>],
820    ) -> Result<Vec<Vec<u8>>, DispatchError> {
821        if inputs.len() != 18 {
822            return Err(DispatchError::BadInputs(format!(
823                "Fix: exploded IFDS oracle expected 18 input buffers, got {}.",
824                inputs.len()
825            )));
826        }
827
828        let key = vyre_primitives::graph::exploded::ifds_program_cache_key_from_program(program)
829            .map_err(DispatchError::BackendError)?;
830        let (intra_edges, inter_edges, flow_gen, flow_kill) = parse_ifds_rule_inputs(&key, inputs)?;
831
832        let (row_ptr, col_idx) = vyre_primitives::graph::exploded::build_cpu_reference(
833            key.num_procs,
834            key.blocks_per_proc,
835            key.facts_per_proc,
836            &intra_edges,
837            &inter_edges,
838            &flow_gen,
839            &flow_kill,
840        );
841
842        let col_len = u32::try_from(col_idx.len()).map_err(|error| {
843            DispatchError::BackendError(format!(
844                "Fix: exploded IFDS oracle col_idx length does not fit u32: {error}."
845            ))
846        })?;
847        let col_idx_words = program
848            .buffer("col_idx")
849            .map(|buffer| buffer.count() as usize)
850            .unwrap_or(1);
851        let mut col_idx_padded = vec![0u32; col_idx_words];
852        if col_idx.len() > col_idx_words {
853            return Err(DispatchError::BackendError(format!(
854                "Fix: exploded IFDS oracle emitted {} columns but program allocates {col_idx_words}."
855                ,
856                col_idx.len()
857            )));
858        }
859        col_idx_padded[..col_idx.len()].copy_from_slice(&col_idx);
860
861        let row_cursor_words = program
862            .buffer("row_cursor")
863            .map(|buffer| buffer.count() as usize)
864            .unwrap_or(1);
865        let row_cursor = vec![0u32; row_cursor_words];
866
867        Ok(vec![
868            u32_buffer_to_bytes(&row_ptr),
869            u32_buffer_to_bytes(&row_cursor),
870            u32_buffer_to_bytes(&col_idx_padded),
871            u32_buffer_to_bytes(&[col_len]),
872        ])
873    }
874
875    fn parse_ifds_rule_inputs(
876        key: &vyre_primitives::graph::exploded::IfdsCsrProgramCacheKey,
877        inputs: &[Vec<u8>],
878    ) -> Result<
879        (
880            Vec<(u32, u32, u32)>,
881            Vec<(u32, u32, u32, u32)>,
882            Vec<(u32, u32, u32)>,
883            Vec<(u32, u32, u32)>,
884        ),
885        DispatchError,
886    > {
887        let intra_proc = crate::hardware::dispatch_buffers::read_u32s(&inputs[0]);
888        let intra_src_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[1]);
889        let intra_dst_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[2]);
890        let inter_src_proc = crate::hardware::dispatch_buffers::read_u32s(&inputs[3]);
891        let inter_src_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[4]);
892        let inter_dst_proc = crate::hardware::dispatch_buffers::read_u32s(&inputs[5]);
893        let inter_dst_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[6]);
894        let gen_proc = crate::hardware::dispatch_buffers::read_u32s(&inputs[7]);
895        let gen_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[8]);
896        let gen_fact = crate::hardware::dispatch_buffers::read_u32s(&inputs[9]);
897        let kill_proc = crate::hardware::dispatch_buffers::read_u32s(&inputs[10]);
898        let kill_block = crate::hardware::dispatch_buffers::read_u32s(&inputs[11]);
899        let kill_fact = crate::hardware::dispatch_buffers::read_u32s(&inputs[12]);
900
901        let intra_edges = read_ifds_triples(
902            "intra",
903            key.intra_count,
904            &intra_proc,
905            &intra_src_block,
906            &intra_dst_block,
907        )?;
908        let inter_edges = read_ifds_quads(
909            "inter",
910            key.inter_count,
911            &inter_src_proc,
912            &inter_src_block,
913            &inter_dst_proc,
914            &inter_dst_block,
915        )?;
916        let flow_gen = read_ifds_triples("GEN", key.gen_count, &gen_proc, &gen_block, &gen_fact)?;
917        let flow_kill =
918            read_ifds_triples("KILL", key.kill_count, &kill_proc, &kill_block, &kill_fact)?;
919
920        Ok((intra_edges, inter_edges, flow_gen, flow_kill))
921    }
922
923    fn read_ifds_triples(
924        kind: &str,
925        count: u32,
926        proc: &[u32],
927        a: &[u32],
928        b: &[u32],
929    ) -> Result<Vec<(u32, u32, u32)>, DispatchError> {
930        let count = count as usize;
931        for (name, column) in [("proc", proc), ("a", a), ("b", b)] {
932            if column.len() < count {
933                return Err(DispatchError::BadInputs(format!(
934                    "Fix: exploded IFDS oracle {kind} {name} column has {} word(s), expected {count}."
935                    ,
936                    column.len()
937                )));
938            }
939        }
940        Ok((0..count)
941            .map(|index| (proc[index], a[index], b[index]))
942            .collect())
943    }
944
945    fn read_ifds_quads(
946        kind: &str,
947        count: u32,
948        a: &[u32],
949        b: &[u32],
950        c: &[u32],
951        d: &[u32],
952    ) -> Result<Vec<(u32, u32, u32, u32)>, DispatchError> {
953        let count = count as usize;
954        for (name, column) in [
955            ("src_proc", a),
956            ("src_block", b),
957            ("dst_proc", c),
958            ("dst_block", d),
959        ] {
960            if column.len() < count {
961                return Err(DispatchError::BadInputs(format!(
962                    "Fix: exploded IFDS oracle {kind} {name} column has {} word(s), expected {count}."
963                    ,
964                    column.len()
965                )));
966            }
967        }
968        Ok((0..count)
969            .map(|index| (a[index], b[index], c[index], d[index]))
970            .collect())
971    }
972
973    fn declared_edge_count(edge_offsets: &[u32]) -> Result<usize, DispatchError> {
974        edge_offsets
975            .last()
976            .copied()
977            .map(|edge_count| edge_count as usize)
978            .ok_or_else(|| {
979                DispatchError::BadInputs(
980                    "Fix: persistent_bfs oracle requires a CSR offset sentinel.".to_string(),
981                )
982            })
983    }
984
985    fn trim_padded_edge_buffer<'a>(
986        name: &str,
987        buffer: &'a [u32],
988        edge_count: usize,
989    ) -> Result<&'a [u32], DispatchError> {
990        if buffer.len() < edge_count {
991            return Err(DispatchError::BadInputs(format!(
992                "Fix: persistent_bfs oracle {name} has {} words but CSR declares {edge_count} edges.",
993                buffer.len()
994            )));
995        }
996        Ok(&buffer[..edge_count])
997    }
998
999    fn u32_buffer_to_bytes(words: &[u32]) -> Vec<u8> {
1000        vyre_primitives::wire::pack_u32_slice(words)
1001    }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006    use super::*;
1007    use std::cell::{Cell, RefCell};
1008
1009    struct RangedReadDispatcher {
1010        buffers: Vec<(u64, Vec<u8>)>,
1011        read_calls: Cell<usize>,
1012        batched_handles: RefCell<Vec<u64>>,
1013    }
1014
1015    impl OptimizerDispatcher for RangedReadDispatcher {
1016        fn dispatch(
1017            &self,
1018            _program: &Program,
1019            _inputs: &[Vec<u8>],
1020            _grid_override: Option<[u32; 3]>,
1021        ) -> Result<Vec<Vec<u8>>, DispatchError> {
1022            Err(DispatchError::Rejected(
1023                "Fix: ranged-read test dispatcher does not implement dispatch.".to_string(),
1024            ))
1025        }
1026
1027        fn read_resident(&self, handle: u64) -> Result<Vec<u8>, DispatchError> {
1028            self.read_calls.set(self.read_calls.get() + 1);
1029            self.buffers
1030                .iter()
1031                .find(|(candidate, _)| *candidate == handle)
1032                .map(|(_, bytes)| bytes.clone())
1033                .ok_or_else(|| {
1034                    DispatchError::BadInputs(format!(
1035                        "Fix: test dispatcher missing resident handle {handle}."
1036                    ))
1037                })
1038        }
1039
1040        fn read_resident_many(&self, handles: &[u64]) -> Result<Vec<Vec<u8>>, DispatchError> {
1041            self.batched_handles.borrow_mut().extend_from_slice(handles);
1042            handles
1043                .iter()
1044                .map(|&handle| self.read_resident(handle))
1045                .collect()
1046        }
1047    }
1048
1049    struct FailingAllocDispatcher {
1050        next_handle: Cell<u64>,
1051        fail_at_call: usize,
1052        allocations: RefCell<Vec<usize>>,
1053        freed: RefCell<Vec<u64>>,
1054    }
1055
1056    impl FailingAllocDispatcher {
1057        fn new(first_handle: u64, fail_at_call: usize) -> Self {
1058            Self {
1059                next_handle: Cell::new(first_handle),
1060                fail_at_call,
1061                allocations: RefCell::new(Vec::new()),
1062                freed: RefCell::new(Vec::new()),
1063            }
1064        }
1065    }
1066
1067    impl OptimizerDispatcher for FailingAllocDispatcher {
1068        fn dispatch(
1069            &self,
1070            _program: &Program,
1071            _inputs: &[Vec<u8>],
1072            _grid_override: Option<[u32; 3]>,
1073        ) -> Result<Vec<Vec<u8>>, DispatchError> {
1074            Err(DispatchError::Rejected(
1075                "Fix: failing allocation test dispatcher does not implement dispatch.".to_string(),
1076            ))
1077        }
1078
1079        fn alloc_resident(&self, byte_len: usize) -> Result<u64, DispatchError> {
1080            let call = self.allocations.borrow().len();
1081            self.allocations.borrow_mut().push(byte_len);
1082            if call == self.fail_at_call {
1083                return Err(DispatchError::BackendError(
1084                    "Fix: injected optimizer resident allocation failure".to_string(),
1085                ));
1086            }
1087            let handle = self.next_handle.get();
1088            self.next_handle.set(handle + 1);
1089            Ok(handle)
1090        }
1091
1092        fn free_resident(&self, handle: u64) -> Result<(), DispatchError> {
1093            self.freed.borrow_mut().push(handle);
1094            Ok(())
1095        }
1096    }
1097
1098    #[test]
1099    fn generated_fill_upload_staging_preserves_fill_then_upload_order() {
1100        let host_payload = [0xA5_u8, 0x5A];
1101        let mut staged = Vec::new();
1102
1103        with_staged_fill_uploads(
1104            &[(7, 3, 0x11), (9, 2, 0x22)],
1105            &[(13, host_payload.as_slice())],
1106            "test fill payloads",
1107            "test combined uploads",
1108            |uploads| {
1109                for &(handle, bytes) in uploads {
1110                    staged.push((handle, bytes.to_vec()));
1111                }
1112                Ok(())
1113            },
1114        )
1115        .expect("Fix: shared resident fill staging should succeed");
1116
1117        assert_eq!(
1118            staged,
1119            vec![
1120                (7, vec![0x11, 0x11, 0x11]),
1121                (9, vec![0x22, 0x22]),
1122                (13, host_payload.to_vec()),
1123            ],
1124            "resident fill staging must preserve device-fill uploads before caller uploads"
1125        );
1126    }
1127
1128    #[test]
1129    fn resident_grouped_allocation_rolls_back_partial_handles() {
1130        let dispatcher = FailingAllocDispatcher::new(90, 2);
1131
1132        let err = dispatcher
1133            .alloc_resident_many(&[4, 8, 12])
1134            .expect_err("Fix: injected grouped allocation failure should surface");
1135
1136        assert!(
1137            matches!(err, DispatchError::BackendError(message) if message.contains("injected optimizer resident allocation failure"))
1138        );
1139        assert_eq!(dispatcher.allocations.borrow().as_slice(), &[4, 8, 12]);
1140        assert_eq!(
1141            dispatcher.freed.borrow().as_slice(),
1142            &[90, 91],
1143            "Fix: grouped resident allocation must free every prior handle on failure."
1144        );
1145    }
1146
1147    #[test]
1148    fn ranged_readback_deduplicates_full_buffer_reads_by_handle() {
1149        let dispatcher = RangedReadDispatcher {
1150            buffers: vec![(7, (0u8..32).collect()), (9, (100u8..132).collect())],
1151            read_calls: Cell::new(0),
1152            batched_handles: RefCell::new(Vec::new()),
1153        };
1154
1155        let outputs = dispatcher
1156            .read_resident_ranges(&[
1157                ResidentReadRange {
1158                    handle_id: 7,
1159                    byte_offset: 4,
1160                    byte_len: 4,
1161                },
1162                ResidentReadRange {
1163                    handle_id: 9,
1164                    byte_offset: 2,
1165                    byte_len: 3,
1166                },
1167                ResidentReadRange {
1168                    handle_id: 7,
1169                    byte_offset: 12,
1170                    byte_len: 5,
1171                },
1172            ])
1173            .expect("Fix: ranged readback must succeed for in-bounds dedup keys; return Err on overlap violations - deduplicated ranged readback must succeed");
1174
1175        assert_eq!(
1176            outputs,
1177            vec![
1178                vec![4, 5, 6, 7],
1179                vec![102, 103, 104],
1180                vec![12, 13, 14, 15, 16]
1181            ]
1182        );
1183        assert_eq!(
1184            dispatcher.read_calls.get(),
1185            2,
1186            "Fix: default ranged readback must read each unique resident handle once, not once per range."
1187        );
1188        assert_eq!(
1189            dispatcher.batched_handles.borrow().as_slice(),
1190            &[7, 9],
1191            "Fix: default ranged readback must preserve first-seen handle order for batched backend overrides."
1192        );
1193    }
1194
1195    #[test]
1196    fn ranged_readback_into_reuses_output_slots_without_intermediate_readbacks() {
1197        let dispatcher = RangedReadDispatcher {
1198            buffers: vec![(7, (0u8..32).collect()), (9, (100u8..132).collect())],
1199            read_calls: Cell::new(0),
1200            batched_handles: RefCell::new(Vec::new()),
1201        };
1202        let mut outputs = vec![
1203            Vec::with_capacity(16),
1204            Vec::with_capacity(16),
1205            Vec::with_capacity(16),
1206        ];
1207        let capacities = outputs.iter().map(Vec::capacity).collect::<Vec<_>>();
1208
1209        dispatcher
1210            .read_resident_ranges_into(
1211                &[
1212                    ResidentReadRange {
1213                        handle_id: 7,
1214                        byte_offset: 0,
1215                        byte_len: 4,
1216                    },
1217                    ResidentReadRange {
1218                        handle_id: 9,
1219                        byte_offset: 4,
1220                        byte_len: 4,
1221                    },
1222                    ResidentReadRange {
1223                        handle_id: 7,
1224                        byte_offset: 8,
1225                        byte_len: 4,
1226                    },
1227                ],
1228                &mut outputs,
1229            )
1230            .expect("Fix: caller buffer must be sized for readback range; return Err if storage too small - ranged readback into caller storage must succeed");
1231
1232        assert_eq!(
1233            outputs,
1234            vec![
1235                vec![0, 1, 2, 3],
1236                vec![104, 105, 106, 107],
1237                vec![8, 9, 10, 11]
1238            ]
1239        );
1240        assert_eq!(
1241            outputs.iter().map(Vec::capacity).collect::<Vec<_>>(),
1242            capacities,
1243            "Fix: ranged readback into caller storage must retain output slot capacity."
1244        );
1245        assert_eq!(dispatcher.read_calls.get(), 2);
1246
1247        let source = include_str!("dispatcher.rs");
1248        assert!(
1249            source.contains("self.read_resident_ranges_into(read_ranges, outputs)")
1250                && !source.contains(concat!(
1251                    "let readbacks =\n            self.upload_resident_many_sequence_read_ranges"
1252                )),
1253            "Fix: resident range readback _into path must not allocate an intermediate Vec<Vec<u8>> before copying into caller-owned outputs."
1254        );
1255    }
1256
1257    #[test]
1258    fn generated_ranged_readbacks_deduplicate_handles_without_reordering_ranges() {
1259        let dispatcher = RangedReadDispatcher {
1260            buffers: (0..8u64)
1261                .map(|handle| {
1262                    (
1263                        handle,
1264                        (0..64u8)
1265                            .map(|byte| byte.wrapping_add((handle as u8).wrapping_mul(17)))
1266                            .collect::<Vec<_>>(),
1267                    )
1268                })
1269                .collect(),
1270            read_calls: Cell::new(0),
1271            batched_handles: RefCell::new(Vec::new()),
1272        };
1273        let ranges = (0..2048usize)
1274            .map(|case| ResidentReadRange {
1275                handle_id: ((case.wrapping_mul(5).wrapping_add(case / 11)) % 8) as u64,
1276                byte_offset: (case.wrapping_mul(7)) % 48,
1277                byte_len: (case % 16) + 1,
1278            })
1279            .collect::<Vec<_>>();
1280
1281        let outputs = dispatcher
1282            .read_resident_ranges(&ranges)
1283            .expect("Fix: generated matrix fixtures must stay in-bounds; fix fixture or return Err - generated ranged readback matrix must succeed");
1284
1285        assert_eq!(outputs.len(), ranges.len());
1286        for (range, output) in ranges.iter().zip(outputs.iter()) {
1287            let full = dispatcher
1288                .buffers
1289                .iter()
1290                .find(|(handle, _)| *handle == range.handle_id)
1291                .map(|(_, bytes)| bytes.as_slice())
1292                .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - generated range uses known handle");
1293            assert_eq!(
1294                output.as_slice(),
1295                &full[range.byte_offset..range.byte_offset + range.byte_len],
1296                "generated range must preserve caller range order and byte-exact slices"
1297            );
1298        }
1299        assert_eq!(
1300            dispatcher.read_calls.get(),
1301            8,
1302            "Fix: generated ranged readback matrix must issue one full read per unique handle."
1303        );
1304    }
1305}