1use vyre_foundation::ir::Program;
20
21pub struct ResidentDispatchStep<'a> {
23 pub program: &'a Program,
25 pub handle_ids: &'a [u64],
27 pub grid_override: Option<[u32; 3]>,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct ResidentReadRange {
34 pub handle_id: u64,
36 pub byte_offset: usize,
38 pub byte_len: usize,
40}
41
42#[derive(Debug)]
51pub struct ResidentStaticBufferSet {
52 pub handles: Vec<u64>,
55 pub cache_hit: bool,
57 pub retained_by_dispatcher: bool,
59}
60
61#[derive(Debug)]
65pub enum DispatchError {
66 Rejected(String),
69 BadInputs(String),
72 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
89pub trait OptimizerDispatcher {
96 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 fn supports_persistent(&self) -> bool {
119 false
120 }
121
122 fn device_feature_cache_key(&self) -> u64 {
130 0
131 }
132
133 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 use super::{DispatchError, OptimizerDispatcher};
691 use vyre_foundation::ir::Program;
692
693 pub struct CpuOracleDispatcher;
697
698 impl CpuOracleDispatcher {
699 #[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 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 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 let node_count = nodes.len() as u32;
787
788 let max_iters = node_count.max(1);
793
794 let _ = program; 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 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}