1use std::fmt;
20use std::sync::Arc;
21
22use vyre_foundation::ir::model::expr::Ident;
23use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
24use vyre_foundation::MemoryOrdering;
25
26use crate::bitset::bitset_words;
27use crate::graph::csr_frontier_step::{
28 csr_queue_step_program, CsrQueueEmit, CsrQueueInputs, CsrQueueLanes, CsrQueueRowPlan,
29 CsrQueueStepSpec,
30};
31
32pub const FRONTIER_TO_QUEUE_OP_ID: &str = "vyre-primitives::graph::frontier_to_queue";
34pub const FRONTIER_TO_QUEUE_PARALLEL_OP_ID: &str =
36 "vyre-primitives::graph::frontier_to_queue_parallel";
37pub const FRONTIER_WORDS_TO_QUEUE_PARALLEL_OP_ID: &str =
39 "vyre-primitives::graph::frontier_words_to_queue_parallel";
40pub const FRONTIER_WORDS_TO_QUEUE_CLEAR_OUT_PARALLEL_OP_ID: &str =
42 "vyre-primitives::graph::frontier_words_to_queue_clear_out_parallel";
43pub const FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID: &str =
45 "vyre-primitives::graph::frontier_word_counts_scan_pass_a";
46pub const FRONTIER_WORD_BLOCK_PREFIX_TO_QUEUE_PARALLEL_OP_ID: &str =
48 "vyre-primitives::graph::frontier_word_block_prefix_to_queue_parallel";
49pub const FRONTIER_WORD_BLOCK_OFFSETS_IN_PLACE_OP_ID: &str =
51 "vyre-primitives::graph::frontier_word_block_offsets_in_place";
52pub const FRONTIER_WORD_BLOCK_OFFSETS_TO_QUEUE_PARALLEL_OP_ID: &str =
54 "vyre-primitives::graph::frontier_word_block_offsets_to_queue_parallel";
55pub const FRONTIER_TO_QUEUE_WORKGROUP_LANES: u32 = 256;
63pub const FRONTIER_WORD_SCAN_BLOCK_LANES: u32 = 1024;
65pub const FRONTIER_QUEUE_LEN_INIT_OP_ID: &str = "vyre-primitives::graph::frontier_queue_len_init";
67pub const CSR_QUEUE_FORWARD_OP_ID: &str = "vyre-primitives::graph::csr_queue_forward_traverse";
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71struct FrontierQueueSizingError {
72 message: String,
73}
74
75impl FrontierQueueSizingError {
76 fn new(message: String) -> Self {
77 Self { message }
78 }
79}
80
81impl fmt::Display for FrontierQueueSizingError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 f.write_str(&self.message)
84 }
85}
86
87fn checked_frontier_u32_product(
88 lhs: u32,
89 rhs: u32,
90 context: &str,
91) -> Result<u32, FrontierQueueSizingError> {
92 lhs.checked_mul(rhs).ok_or_else(|| {
93 FrontierQueueSizingError::new(format!(
94 "Fix: {context} overflows u32 word count for lhs={lhs} rhs={rhs}. Shard the frontier queue before GPU dispatch."
95 ))
96 })
97}
98
99fn try_u32_byte_range(words: u32, context: &str) -> Result<usize, FrontierQueueSizingError> {
100 try_u32_byte_range_with_word_size(words, std::mem::size_of::<u32>(), context)
101}
102
103fn try_u32_byte_range_with_word_size(
104 words: u32,
105 word_size: usize,
106 context: &str,
107) -> Result<usize, FrontierQueueSizingError> {
108 let count = usize::try_from(words).map_err(|_| {
109 FrontierQueueSizingError::new(format!(
110 "Fix: {context} words={words} cannot fit usize on this target. Shard the frontier queue before GPU dispatch."
111 ))
112 })?;
113 count.checked_mul(word_size).ok_or_else(|| {
114 FrontierQueueSizingError::new(format!(
115 "Fix: {context} words={words} word_size={word_size} overflows output byte range. Shard the frontier queue before GPU dispatch."
116 ))
117 })
118}
119
120fn invalid_frontier_queue_sizing_program(
121 op_id: &'static str,
122 output: &str,
123 error: FrontierQueueSizingError,
124) -> Program {
125 crate::invalid_output_program(op_id, output, DataType::U32, error.to_string())
126}
127
128#[must_use]
136pub fn frontier_queue_len_init(queue_len: &str) -> Program {
137 Program::wrapped(
138 vec![
139 BufferDecl::storage(queue_len, 0, BufferAccess::ReadWrite, DataType::U32).with_count(1),
140 ],
141 [1, 1, 1],
142 vec![Node::Region {
143 generator: Ident::from(FRONTIER_QUEUE_LEN_INIT_OP_ID),
144 source_region: None,
145 body: Arc::new(vec![Node::store(queue_len, Expr::u32(0), Expr::u32(0))]),
146 }],
147 )
148}
149
150#[must_use]
182pub fn frontier_to_queue(
183 frontier_in: &str,
184 active_queue: &str,
185 queue_len: &str,
186 node_count: u32,
187 queue_capacity: u32,
188) -> Program {
189 if node_count == 0 || queue_capacity == 0 {
190 return crate::invalid_output_program(FRONTIER_TO_QUEUE_OP_ID,
191 queue_len,
192 DataType::U32,
193 format!(
194 "Fix: frontier_to_queue requires node_count > 0 and queue_capacity > 0, got node_count={node_count} queue_capacity={queue_capacity}."
195 ),);
196 }
197 let lane = Expr::InvocationId { axis: 0 };
198 let words = bitset_words(node_count);
199 let lanes = FRONTIER_TO_QUEUE_WORKGROUP_LANES;
200 let scan_iters = node_count.div_ceil(lanes).max(1);
201 let body = vec![
202 Node::let_bind("q_lane", lane.clone()),
203 Node::if_then(
204 Expr::eq(Expr::var("q_lane"), Expr::u32(0)),
205 vec![Node::store(queue_len, Expr::u32(0), Expr::u32(0))],
206 ),
207 Node::barrier_with_ordering(MemoryOrdering::SeqCst),
208 Node::if_then(
212 Expr::lt(Expr::var("q_lane"), Expr::u32(lanes)),
213 vec![Node::loop_for(
214 "q_iter",
215 Expr::u32(0),
216 Expr::u32(scan_iters),
217 vec![
218 Node::let_bind(
219 "q_src",
220 Expr::add(
221 Expr::mul(Expr::var("q_iter"), Expr::u32(lanes)),
222 Expr::var("q_lane"),
223 ),
224 ),
225 Node::if_then(
226 Expr::lt(Expr::var("q_src"), Expr::u32(node_count)),
227 vec![
228 Node::let_bind(
229 "q_word_idx",
230 Expr::shr(Expr::var("q_src"), Expr::u32(5)),
231 ),
232 Node::let_bind(
233 "q_bit_mask",
234 Expr::shl(
235 Expr::u32(1),
236 Expr::bitand(Expr::var("q_src"), Expr::u32(31)),
237 ),
238 ),
239 Node::let_bind(
240 "q_src_word",
241 Expr::load(frontier_in, Expr::var("q_word_idx")),
242 ),
243 Node::if_then(
244 Expr::ne(
245 Expr::bitand(Expr::var("q_src_word"), Expr::var("q_bit_mask")),
246 Expr::u32(0),
247 ),
248 vec![
249 Node::let_bind(
250 "q_slot",
251 Expr::atomic_add(queue_len, Expr::u32(0), Expr::u32(1)),
252 ),
253 Node::if_then(
254 Expr::lt(Expr::var("q_slot"), Expr::u32(queue_capacity)),
255 vec![Node::store(
256 active_queue,
257 Expr::var("q_slot"),
258 Expr::var("q_src"),
259 )],
260 ),
261 ],
262 ),
263 ],
264 ),
265 ],
266 )],
267 ),
268 ];
269 Program::wrapped(
270 vec![
271 BufferDecl::storage(frontier_in, 0, BufferAccess::ReadOnly, DataType::U32)
272 .with_count(words),
273 BufferDecl::storage(active_queue, 1, BufferAccess::ReadWrite, DataType::U32)
274 .with_count(queue_capacity),
275 BufferDecl::storage(queue_len, 2, BufferAccess::ReadWrite, DataType::U32).with_count(1),
276 ],
277 [lanes, 1, 1],
278 vec![Node::Region {
279 generator: Ident::from(FRONTIER_TO_QUEUE_OP_ID),
280 source_region: None,
281 body: Arc::new(body),
282 }],
283 )
284}
285
286#[must_use]
293pub fn frontier_to_queue_parallel(
294 frontier_in: &str,
295 active_queue: &str,
296 queue_len: &str,
297 node_count: u32,
298 queue_capacity: u32,
299) -> Program {
300 if node_count == 0 || queue_capacity == 0 {
301 return crate::invalid_output_program(FRONTIER_TO_QUEUE_PARALLEL_OP_ID,
302 queue_len,
303 DataType::U32,
304 format!(
305 "Fix: frontier_to_queue_parallel requires node_count > 0 and queue_capacity > 0, got node_count={node_count} queue_capacity={queue_capacity}."
306 ),);
307 }
308 let lane = Expr::InvocationId { axis: 0 };
309 let words = bitset_words(node_count);
310 let body = vec![
311 Node::let_bind("qp_src", lane),
312 Node::if_then(
313 Expr::lt(Expr::var("qp_src"), Expr::u32(node_count)),
314 vec![
315 Node::let_bind("qp_word_idx", Expr::shr(Expr::var("qp_src"), Expr::u32(5))),
316 Node::let_bind(
317 "qp_bit_mask",
318 Expr::shl(
319 Expr::u32(1),
320 Expr::bitand(Expr::var("qp_src"), Expr::u32(31)),
321 ),
322 ),
323 Node::let_bind(
324 "qp_src_word",
325 Expr::load(frontier_in, Expr::var("qp_word_idx")),
326 ),
327 Node::if_then(
328 Expr::ne(
329 Expr::bitand(Expr::var("qp_src_word"), Expr::var("qp_bit_mask")),
330 Expr::u32(0),
331 ),
332 vec![
333 Node::let_bind(
334 "qp_slot",
335 Expr::atomic_add(queue_len, Expr::u32(0), Expr::u32(1)),
336 ),
337 Node::if_then(
338 Expr::lt(Expr::var("qp_slot"), Expr::u32(queue_capacity)),
339 vec![Node::store(
340 active_queue,
341 Expr::var("qp_slot"),
342 Expr::var("qp_src"),
343 )],
344 ),
345 ],
346 ),
347 ],
348 ),
349 ];
350 Program::wrapped(
351 vec![
352 BufferDecl::storage(frontier_in, 0, BufferAccess::ReadOnly, DataType::U32)
353 .with_count(words),
354 BufferDecl::storage(active_queue, 1, BufferAccess::ReadWrite, DataType::U32)
355 .with_count(queue_capacity),
356 BufferDecl::storage(queue_len, 2, BufferAccess::ReadWrite, DataType::U32).with_count(1),
357 ],
358 [256, 1, 1],
359 vec![Node::Region {
360 generator: Ident::from(FRONTIER_TO_QUEUE_PARALLEL_OP_ID),
361 source_region: None,
362 body: Arc::new(body),
363 }],
364 )
365}
366
367#[must_use]
375pub fn frontier_words_to_queue_parallel(
376 frontier_in: &str,
377 active_queue: &str,
378 queue_len: &str,
379 node_count: u32,
380 queue_capacity: u32,
381) -> Program {
382 frontier_words_to_queue_parallel_program(
383 FRONTIER_WORDS_TO_QUEUE_PARALLEL_OP_ID,
384 frontier_in,
385 active_queue,
386 queue_len,
387 None,
388 node_count,
389 queue_capacity,
390 )
391}
392
393#[must_use]
399pub fn frontier_words_to_queue_clear_out_parallel(
400 frontier_in: &str,
401 active_queue: &str,
402 queue_len: &str,
403 frontier_out: &str,
404 node_count: u32,
405 queue_capacity: u32,
406) -> Program {
407 frontier_words_to_queue_parallel_program(
408 FRONTIER_WORDS_TO_QUEUE_CLEAR_OUT_PARALLEL_OP_ID,
409 frontier_in,
410 active_queue,
411 queue_len,
412 Some(frontier_out),
413 node_count,
414 queue_capacity,
415 )
416}
417
418fn frontier_words_to_queue_parallel_program(
419 op_id: &'static str,
420 frontier_in: &str,
421 active_queue: &str,
422 queue_len: &str,
423 frontier_out_to_clear: Option<&str>,
424 node_count: u32,
425 queue_capacity: u32,
426) -> Program {
427 if node_count == 0 || queue_capacity == 0 {
428 return crate::invalid_output_program(op_id,
429 queue_len,
430 DataType::U32,
431 format!(
432 "Fix: {op_id} requires node_count > 0 and queue_capacity > 0, got node_count={node_count} queue_capacity={queue_capacity}."
433 ),);
434 }
435 let lane = Expr::InvocationId { axis: 0 };
436 let words = bitset_words(node_count);
437 let tail_bits = node_count & 31;
438 let tail_mask = if tail_bits == 0 {
439 u32::MAX
440 } else {
441 (1_u32 << tail_bits) - 1
442 };
443 let mut word_body = vec![
444 Node::let_bind(
445 "qw_src_base",
446 Expr::mul(Expr::var("qw_word_idx"), Expr::u32(32)),
447 ),
448 Node::let_bind(
449 "qw_remaining",
450 Expr::load(frontier_in, Expr::var("qw_word_idx")),
451 ),
452 ];
453 if tail_bits != 0 {
454 word_body.push(Node::if_then(
455 Expr::eq(Expr::var("qw_word_idx"), Expr::u32(words - 1)),
456 vec![Node::assign(
457 "qw_remaining",
458 Expr::bitand(Expr::var("qw_remaining"), Expr::u32(tail_mask)),
459 )],
460 ));
461 }
462 word_body.push(Node::if_then(
463 Expr::ne(Expr::var("qw_remaining"), Expr::u32(0)),
464 vec![
465 Node::let_bind("qw_active_bits", Expr::popcount(Expr::var("qw_remaining"))),
466 Node::let_bind(
467 "qw_base_slot",
468 Expr::atomic_add(queue_len, Expr::u32(0), Expr::var("qw_active_bits")),
469 ),
470 Node::loop_for(
471 "qw_rank",
472 Expr::u32(0),
473 Expr::var("qw_active_bits"),
474 vec![
475 Node::let_bind("qw_bit", Expr::ctz(Expr::var("qw_remaining"))),
476 Node::let_bind(
477 "qw_src",
478 Expr::add(Expr::var("qw_src_base"), Expr::var("qw_bit")),
479 ),
480 Node::let_bind(
481 "qw_slot",
482 Expr::add(Expr::var("qw_base_slot"), Expr::var("qw_rank")),
483 ),
484 Node::if_then(
485 Expr::lt(Expr::var("qw_slot"), Expr::u32(queue_capacity)),
486 vec![Node::store(
487 active_queue,
488 Expr::var("qw_slot"),
489 Expr::var("qw_src"),
490 )],
491 ),
492 Node::assign(
493 "qw_remaining",
494 Expr::bitand(
495 Expr::var("qw_remaining"),
496 Expr::sub(Expr::var("qw_remaining"), Expr::u32(1)),
497 ),
498 ),
499 ],
500 ),
501 ],
502 ));
503 if let Some(frontier_out) = frontier_out_to_clear {
504 word_body.insert(
505 0,
506 Node::store(frontier_out, Expr::var("qw_word_idx"), Expr::u32(0)),
507 );
508 }
509
510 let body = vec![
511 Node::let_bind("qw_word_idx", lane),
512 Node::if_then(
513 Expr::lt(Expr::var("qw_word_idx"), Expr::u32(words)),
514 word_body,
515 ),
516 ];
517 let mut buffers = vec![
518 BufferDecl::storage(frontier_in, 0, BufferAccess::ReadOnly, DataType::U32)
519 .with_count(words),
520 BufferDecl::storage(active_queue, 1, BufferAccess::ReadWrite, DataType::U32)
521 .with_count(queue_capacity),
522 BufferDecl::storage(queue_len, 2, BufferAccess::ReadWrite, DataType::U32).with_count(1),
523 ];
524 if let Some(frontier_out) = frontier_out_to_clear {
525 buffers.push(
526 BufferDecl::storage(frontier_out, 3, BufferAccess::ReadWrite, DataType::U32)
527 .with_count(words),
528 );
529 }
530 Program::wrapped(
531 buffers,
532 [256, 1, 1],
533 vec![Node::Region {
534 generator: Ident::from(op_id),
535 source_region: None,
536 body: Arc::new(body),
537 }],
538 )
539}
540
541#[must_use]
549pub fn frontier_word_counts_scan_pass_a(
550 frontier_in: &str,
551 word_partials: &str,
552 block_totals: &str,
553 node_count: u32,
554) -> Program {
555 if node_count == 0 {
556 return crate::invalid_output_program(
557 FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID,
558 word_partials,
559 DataType::U32,
560 "Fix: frontier_word_counts_scan_pass_a requires node_count > 0.".to_string(),
561 );
562 }
563 let words = bitset_words(node_count);
564 let num_blocks = words.div_ceil(FRONTIER_WORD_SCAN_BLOCK_LANES).max(1);
565 let total_partials = match checked_frontier_u32_product(
566 num_blocks,
567 FRONTIER_WORD_SCAN_BLOCK_LANES,
568 "frontier_word_counts_scan_pass_a partial word count",
569 ) {
570 Ok(total_partials) => total_partials,
571 Err(error) => {
572 return invalid_frontier_queue_sizing_program(
573 FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID,
574 word_partials,
575 error,
576 );
577 }
578 };
579 let partial_bytes =
580 match try_u32_byte_range(total_partials, "frontier_word_counts_scan_pass_a partials") {
581 Ok(partial_bytes) => partial_bytes,
582 Err(error) => {
583 return invalid_frontier_queue_sizing_program(
584 FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID,
585 word_partials,
586 error,
587 );
588 }
589 };
590 let block_total_bytes =
591 match try_u32_byte_range(num_blocks, "frontier_word_counts_scan_pass_a block totals") {
592 Ok(block_total_bytes) => block_total_bytes,
593 Err(error) => {
594 return invalid_frontier_queue_sizing_program(
595 FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID,
596 block_totals,
597 error,
598 );
599 }
600 };
601 let tail_bits = node_count & 31;
602 let tail_mask = if tail_bits == 0 {
603 u32::MAX
604 } else {
605 (1_u32 << tail_bits) - 1
606 };
607
608 let lane = Expr::var("fwcs_lane");
609 let block = Expr::var("fwcs_block");
610 let global = Expr::var("fwcs_global");
611 let scratch_a = format!("__{word_partials}_fwcs_scratch_a");
612 let scratch_b = format!("__{word_partials}_fwcs_scratch_b");
613
614 let mut body = Vec::new();
615 body.push(Node::let_bind("fwcs_lane", Expr::LocalId { axis: 0 }));
616 body.push(Node::let_bind("fwcs_block", Expr::WorkgroupId { axis: 0 }));
617 body.push(Node::let_bind(
618 "fwcs_global",
619 Expr::add(
620 Expr::mul(block.clone(), Expr::u32(FRONTIER_WORD_SCAN_BLOCK_LANES)),
621 lane.clone(),
622 ),
623 ));
624 body.push(Node::store(&scratch_a, lane.clone(), Expr::u32(0)));
625 let mut load_word = vec![Node::let_bind(
626 "fwcs_word",
627 Expr::load(frontier_in, global.clone()),
628 )];
629 if tail_bits != 0 {
630 load_word.push(Node::if_then(
631 Expr::eq(global.clone(), Expr::u32(words - 1)),
632 vec![Node::assign(
633 "fwcs_word",
634 Expr::bitand(Expr::var("fwcs_word"), Expr::u32(tail_mask)),
635 )],
636 ));
637 }
638 load_word.push(Node::store(
639 &scratch_a,
640 lane.clone(),
641 Expr::popcount(Expr::var("fwcs_word")),
642 ));
643 body.push(Node::if_then(
644 Expr::lt(global.clone(), Expr::u32(words)),
645 load_word,
646 ));
647 body.push(Node::Barrier {
648 ordering: MemoryOrdering::SeqCst,
649 });
650
651 let mut stride = 1_u32;
652 while stride < FRONTIER_WORD_SCAN_BLOCK_LANES {
653 body.push(Node::store(
654 &scratch_b,
655 lane.clone(),
656 Expr::load(&scratch_a, lane.clone()),
657 ));
658 let previous_lane = Expr::add(lane.clone(), Expr::u32(0u32.wrapping_sub(stride)));
659 body.push(Node::if_then(
660 Expr::lt(Expr::u32(stride - 1), lane.clone()),
661 vec![Node::store(
662 &scratch_b,
663 lane.clone(),
664 Expr::add(
665 Expr::load(&scratch_a, lane.clone()),
666 Expr::load(&scratch_a, previous_lane),
667 ),
668 )],
669 ));
670 body.push(Node::Barrier {
671 ordering: MemoryOrdering::SeqCst,
672 });
673 body.push(Node::store(
674 &scratch_a,
675 lane.clone(),
676 Expr::load(&scratch_b, lane.clone()),
677 ));
678 body.push(Node::Barrier {
679 ordering: MemoryOrdering::SeqCst,
680 });
681 stride *= 2;
682 }
683
684 body.push(Node::if_then(
685 Expr::lt(global.clone(), Expr::u32(words)),
686 vec![Node::store(
687 word_partials,
688 global.clone(),
689 Expr::load(&scratch_a, lane.clone()),
690 )],
691 ));
692 body.push(Node::if_then(
693 Expr::eq(lane.clone(), Expr::u32(FRONTIER_WORD_SCAN_BLOCK_LANES - 1)),
694 vec![Node::store(
695 block_totals,
696 block.clone(),
697 Expr::load(&scratch_a, lane.clone()),
698 )],
699 ));
700
701 Program::wrapped(
702 vec![
703 BufferDecl::storage(frontier_in, 0, BufferAccess::ReadOnly, DataType::U32)
704 .with_count(words),
705 BufferDecl::output(word_partials, 1, DataType::U32)
706 .with_count(total_partials)
707 .with_output_byte_range(0..partial_bytes),
708 BufferDecl::storage(block_totals, 2, BufferAccess::ReadWrite, DataType::U32)
709 .with_count(num_blocks)
710 .with_pipeline_live_out(true)
711 .with_output_byte_range(0..block_total_bytes),
712 BufferDecl::workgroup(&scratch_a, FRONTIER_WORD_SCAN_BLOCK_LANES, DataType::U32),
713 BufferDecl::workgroup(&scratch_b, FRONTIER_WORD_SCAN_BLOCK_LANES, DataType::U32),
714 ],
715 [FRONTIER_WORD_SCAN_BLOCK_LANES, 1, 1],
716 vec![Node::Region {
717 generator: Ident::from(FRONTIER_WORD_COUNTS_SCAN_PASS_A_OP_ID),
718 source_region: None,
719 body: Arc::new(body),
720 }],
721 )
722}
723
724#[must_use]
732pub fn frontier_word_block_offsets_in_place(block_totals: &str, node_count: u32) -> Program {
733 if node_count == 0 {
734 return crate::invalid_output_program(
735 FRONTIER_WORD_BLOCK_OFFSETS_IN_PLACE_OP_ID,
736 block_totals,
737 DataType::U32,
738 "Fix: frontier_word_block_offsets_in_place requires node_count > 0.".to_string(),
739 );
740 }
741 let words = bitset_words(node_count);
742 let num_blocks = words.div_ceil(FRONTIER_WORD_SCAN_BLOCK_LANES).max(1);
743 let block_total_bytes = match try_u32_byte_range(
744 num_blocks,
745 "frontier_word_block_offsets_in_place block totals",
746 ) {
747 Ok(block_total_bytes) => block_total_bytes,
748 Err(error) => {
749 return invalid_frontier_queue_sizing_program(
750 FRONTIER_WORD_BLOCK_OFFSETS_IN_PLACE_OP_ID,
751 block_totals,
752 error,
753 );
754 }
755 };
756 if num_blocks <= FRONTIER_WORD_SCAN_BLOCK_LANES {
757 return frontier_word_block_offsets_single_workgroup(
758 block_totals,
759 num_blocks,
760 block_total_bytes,
761 );
762 }
763 frontier_word_block_offsets_single_lane(block_totals, num_blocks, block_total_bytes)
764}
765
766fn frontier_word_block_offsets_single_workgroup(
767 block_totals: &str,
768 num_blocks: u32,
769 block_total_bytes: usize,
770) -> Program {
771 let lane = Expr::var("fwbo_lane");
772 let scratch_a = format!("__{block_totals}_fwbo_scratch_a");
773 let scratch_b = format!("__{block_totals}_fwbo_scratch_b");
774 let mut body = Vec::new();
775 body.push(Node::let_bind("fwbo_lane", Expr::LocalId { axis: 0 }));
776 body.push(Node::store(&scratch_a, lane.clone(), Expr::u32(0)));
777 body.push(Node::if_then(
778 Expr::lt(lane.clone(), Expr::u32(num_blocks)),
779 vec![Node::store(
780 &scratch_a,
781 lane.clone(),
782 Expr::load(block_totals, lane.clone()),
783 )],
784 ));
785 body.push(Node::Barrier {
786 ordering: MemoryOrdering::SeqCst,
787 });
788
789 let mut stride = 1_u32;
790 while stride < FRONTIER_WORD_SCAN_BLOCK_LANES {
791 body.push(Node::store(
792 &scratch_b,
793 lane.clone(),
794 Expr::load(&scratch_a, lane.clone()),
795 ));
796 let previous_lane = Expr::add(lane.clone(), Expr::u32(0u32.wrapping_sub(stride)));
797 body.push(Node::if_then(
798 Expr::lt(Expr::u32(stride - 1), lane.clone()),
799 vec![Node::store(
800 &scratch_b,
801 lane.clone(),
802 Expr::add(
803 Expr::load(&scratch_a, lane.clone()),
804 Expr::load(&scratch_a, previous_lane),
805 ),
806 )],
807 ));
808 body.push(Node::Barrier {
809 ordering: MemoryOrdering::SeqCst,
810 });
811 body.push(Node::store(
812 &scratch_a,
813 lane.clone(),
814 Expr::load(&scratch_b, lane.clone()),
815 ));
816 body.push(Node::Barrier {
817 ordering: MemoryOrdering::SeqCst,
818 });
819 stride *= 2;
820 }
821
822 body.push(Node::if_then(
823 Expr::lt(lane.clone(), Expr::u32(num_blocks)),
824 vec![
825 Node::if_then(
826 Expr::eq(lane.clone(), Expr::u32(0)),
827 vec![Node::store(block_totals, lane.clone(), Expr::u32(0))],
828 ),
829 Node::if_then(
830 Expr::ne(lane.clone(), Expr::u32(0)),
831 vec![Node::store(
832 block_totals,
833 lane.clone(),
834 Expr::load(&scratch_a, Expr::sub(lane.clone(), Expr::u32(1))),
835 )],
836 ),
837 ],
838 ));
839
840 Program::wrapped(
841 vec![
842 BufferDecl::storage(block_totals, 0, BufferAccess::ReadWrite, DataType::U32)
843 .with_count(num_blocks)
844 .with_pipeline_live_out(true)
845 .with_output_byte_range(0..block_total_bytes),
846 BufferDecl::workgroup(&scratch_a, FRONTIER_WORD_SCAN_BLOCK_LANES, DataType::U32),
847 BufferDecl::workgroup(&scratch_b, FRONTIER_WORD_SCAN_BLOCK_LANES, DataType::U32),
848 ],
849 [FRONTIER_WORD_SCAN_BLOCK_LANES, 1, 1],
850 vec![Node::Region {
851 generator: Ident::from(FRONTIER_WORD_BLOCK_OFFSETS_IN_PLACE_OP_ID),
852 source_region: None,
853 body: Arc::new(body),
854 }],
855 )
856}
857
858fn frontier_word_block_offsets_single_lane(
859 block_totals: &str,
860 num_blocks: u32,
861 block_total_bytes: usize,
862) -> Program {
863 let body = vec![
864 Node::let_bind("fwbo_running", Expr::u32(0)),
865 Node::loop_for(
866 "fwbo_block",
867 Expr::u32(0),
868 Expr::u32(num_blocks),
869 vec![
870 Node::let_bind(
871 "fwbo_total",
872 Expr::load(block_totals, Expr::var("fwbo_block")),
873 ),
874 Node::store(
875 block_totals,
876 Expr::var("fwbo_block"),
877 Expr::var("fwbo_running"),
878 ),
879 Node::assign(
880 "fwbo_running",
881 Expr::add(Expr::var("fwbo_running"), Expr::var("fwbo_total")),
882 ),
883 ],
884 ),
885 ];
886 Program::wrapped(
887 vec![
888 BufferDecl::storage(block_totals, 0, BufferAccess::ReadWrite, DataType::U32)
889 .with_count(num_blocks)
890 .with_pipeline_live_out(true)
891 .with_output_byte_range(0..block_total_bytes),
892 ],
893 [1, 1, 1],
894 vec![Node::Region {
895 generator: Ident::from(FRONTIER_WORD_BLOCK_OFFSETS_IN_PLACE_OP_ID),
896 source_region: None,
897 body: Arc::new(body),
898 }],
899 )
900}
901
902#[must_use]
911pub fn frontier_word_block_prefix_to_queue_parallel(
912 frontier_in: &str,
913 word_partials: &str,
914 block_totals: &str,
915 active_queue: &str,
916 queue_len: &str,
917 node_count: u32,
918 queue_capacity: u32,
919) -> Program {
920 frontier_word_queue_scatter_program(
921 FRONTIER_WORD_BLOCK_PREFIX_TO_QUEUE_PARALLEL_OP_ID,
922 FrontierWordBlockOffsetSource::SumPreviousTotals { block_totals },
923 frontier_in,
924 word_partials,
925 active_queue,
926 queue_len,
927 node_count,
928 queue_capacity,
929 )
930}
931
932#[must_use]
939pub fn frontier_word_block_offsets_to_queue_parallel(
940 frontier_in: &str,
941 word_partials: &str,
942 block_offsets: &str,
943 active_queue: &str,
944 queue_len: &str,
945 node_count: u32,
946 queue_capacity: u32,
947) -> Program {
948 frontier_word_queue_scatter_program(
949 FRONTIER_WORD_BLOCK_OFFSETS_TO_QUEUE_PARALLEL_OP_ID,
950 FrontierWordBlockOffsetSource::PrecomputedOffsets { block_offsets },
951 frontier_in,
952 word_partials,
953 active_queue,
954 queue_len,
955 node_count,
956 queue_capacity,
957 )
958}
959
960#[derive(Clone, Copy)]
961enum FrontierWordBlockOffsetSource<'a> {
962 SumPreviousTotals { block_totals: &'a str },
963 PrecomputedOffsets { block_offsets: &'a str },
964}
965
966impl FrontierWordBlockOffsetSource<'_> {
967 fn buffer_name(&self) -> &str {
968 match self {
969 FrontierWordBlockOffsetSource::SumPreviousTotals { block_totals } => block_totals,
970 FrontierWordBlockOffsetSource::PrecomputedOffsets { block_offsets } => block_offsets,
971 }
972 }
973}
974
975#[allow(clippy::too_many_arguments)]
976fn frontier_word_queue_scatter_program(
977 op_id: &'static str,
978 block_offset_source: FrontierWordBlockOffsetSource<'_>,
979 frontier_in: &str,
980 word_partials: &str,
981 active_queue: &str,
982 queue_len: &str,
983 node_count: u32,
984 queue_capacity: u32,
985) -> Program {
986 if node_count == 0 || queue_capacity == 0 {
987 return crate::invalid_output_program(op_id,
988 queue_len,
989 DataType::U32,
990 format!(
991 "Fix: {op_id} requires node_count > 0 and queue_capacity > 0, got node_count={node_count} queue_capacity={queue_capacity}."
992 ),);
993 }
994 let words = bitset_words(node_count);
995 let num_blocks = words.div_ceil(FRONTIER_WORD_SCAN_BLOCK_LANES).max(1);
996 let total_partials =
997 match checked_frontier_u32_product(num_blocks, FRONTIER_WORD_SCAN_BLOCK_LANES, op_id) {
998 Ok(total_partials) => total_partials,
999 Err(error) => return invalid_frontier_queue_sizing_program(op_id, queue_len, error),
1000 };
1001 let tail_bits = node_count & 31;
1002 let tail_mask = if tail_bits == 0 {
1003 u32::MAX
1004 } else {
1005 (1_u32 << tail_bits) - 1
1006 };
1007 let lane = Expr::InvocationId { axis: 0 };
1008 let mut block_offset_body = Vec::new();
1009 match block_offset_source {
1010 FrontierWordBlockOffsetSource::SumPreviousTotals { block_totals } => {
1011 block_offset_body.push(Node::let_bind("fwq_block_offset", Expr::u32(0)));
1012 block_offset_body.push(Node::loop_for(
1013 "fwq_prev_block",
1014 Expr::u32(0),
1015 Expr::var("fwq_block"),
1016 vec![Node::assign(
1017 "fwq_block_offset",
1018 Expr::add(
1019 Expr::var("fwq_block_offset"),
1020 Expr::load(block_totals, Expr::var("fwq_prev_block")),
1021 ),
1022 )],
1023 ));
1024 }
1025 FrontierWordBlockOffsetSource::PrecomputedOffsets { block_offsets } => {
1026 block_offset_body.push(Node::let_bind(
1027 "fwq_block_offset",
1028 Expr::load(block_offsets, Expr::var("fwq_block")),
1029 ));
1030 }
1031 }
1032 let mut word_body = vec![
1033 Node::let_bind(
1034 "fwq_src_base",
1035 Expr::mul(Expr::var("fwq_word_idx"), Expr::u32(32)),
1036 ),
1037 Node::let_bind(
1038 "fwq_block",
1039 Expr::div(
1040 Expr::var("fwq_word_idx"),
1041 Expr::u32(FRONTIER_WORD_SCAN_BLOCK_LANES),
1042 ),
1043 ),
1044 Node::let_bind(
1045 "fwq_word",
1046 Expr::load(frontier_in, Expr::var("fwq_word_idx")),
1047 ),
1048 ];
1049 if tail_bits != 0 {
1050 word_body.push(Node::if_then(
1051 Expr::eq(Expr::var("fwq_word_idx"), Expr::u32(words - 1)),
1052 vec![Node::assign(
1053 "fwq_word",
1054 Expr::bitand(Expr::var("fwq_word"), Expr::u32(tail_mask)),
1055 )],
1056 ));
1057 }
1058 word_body.extend(block_offset_body);
1059 word_body.extend([
1060 Node::let_bind("fwq_active_bits", Expr::popcount(Expr::var("fwq_word"))),
1061 Node::let_bind(
1062 "fwq_end",
1063 Expr::add(
1064 Expr::load(word_partials, Expr::var("fwq_word_idx")),
1065 Expr::var("fwq_block_offset"),
1066 ),
1067 ),
1068 Node::let_bind(
1069 "fwq_start",
1070 Expr::sub(Expr::var("fwq_end"), Expr::var("fwq_active_bits")),
1071 ),
1072 Node::let_bind("fwq_remaining", Expr::var("fwq_word")),
1073 Node::loop_for(
1074 "fwq_rank",
1075 Expr::u32(0),
1076 Expr::var("fwq_active_bits"),
1077 vec![
1078 Node::let_bind("fwq_bit", Expr::ctz(Expr::var("fwq_remaining"))),
1079 Node::let_bind(
1080 "fwq_src",
1081 Expr::add(Expr::var("fwq_src_base"), Expr::var("fwq_bit")),
1082 ),
1083 Node::let_bind(
1084 "fwq_slot",
1085 Expr::add(Expr::var("fwq_start"), Expr::var("fwq_rank")),
1086 ),
1087 Node::if_then(
1088 Expr::and(
1089 Expr::lt(Expr::var("fwq_slot"), Expr::u32(queue_capacity)),
1090 Expr::lt(Expr::var("fwq_src"), Expr::u32(node_count)),
1091 ),
1092 vec![Node::store(
1093 active_queue,
1094 Expr::var("fwq_slot"),
1095 Expr::var("fwq_src"),
1096 )],
1097 ),
1098 Node::assign(
1099 "fwq_remaining",
1100 Expr::bitand(
1101 Expr::var("fwq_remaining"),
1102 Expr::sub(Expr::var("fwq_remaining"), Expr::u32(1)),
1103 ),
1104 ),
1105 ],
1106 ),
1107 Node::if_then(
1108 Expr::eq(Expr::var("fwq_word_idx"), Expr::u32(words - 1)),
1109 vec![Node::store(queue_len, Expr::u32(0), Expr::var("fwq_end"))],
1110 ),
1111 ]);
1112
1113 let body = vec![
1114 Node::let_bind("fwq_word_idx", lane),
1115 Node::if_then(
1116 Expr::lt(Expr::var("fwq_word_idx"), Expr::u32(words)),
1117 word_body,
1118 ),
1119 ];
1120
1121 Program::wrapped(
1122 vec![
1123 BufferDecl::storage(frontier_in, 0, BufferAccess::ReadOnly, DataType::U32)
1124 .with_count(words),
1125 BufferDecl::storage(word_partials, 1, BufferAccess::ReadOnly, DataType::U32)
1126 .with_count(total_partials),
1127 BufferDecl::storage(
1128 block_offset_source.buffer_name(),
1129 2,
1130 BufferAccess::ReadOnly,
1131 DataType::U32,
1132 )
1133 .with_count(num_blocks),
1134 BufferDecl::storage(active_queue, 3, BufferAccess::ReadWrite, DataType::U32)
1135 .with_count(queue_capacity),
1136 BufferDecl::storage(queue_len, 4, BufferAccess::ReadWrite, DataType::U32).with_count(1),
1137 ],
1138 [256, 1, 1],
1139 vec![Node::Region {
1140 generator: Ident::from(op_id),
1141 source_region: None,
1142 body: Arc::new(body),
1143 }],
1144 )
1145}
1146
1147#[derive(Clone, Copy, Debug)]
1149pub struct CsrQueueForwardTraverseParams<'a> {
1150 pub active_queue: &'a str,
1152 pub queue_len: &'a str,
1154 pub edge_offsets: &'a str,
1156 pub edge_targets: &'a str,
1158 pub edge_kind_mask: &'a str,
1160 pub frontier_out: &'a str,
1162 pub node_count: u32,
1164 pub edge_count: u32,
1166 pub queue_capacity: u32,
1168 pub allow_mask: u32,
1170}
1171
1172#[must_use]
1174#[allow(clippy::too_many_arguments)]
1175pub fn csr_queue_forward_traverse(
1176 active_queue: &str,
1177 queue_len: &str,
1178 edge_offsets: &str,
1179 edge_targets: &str,
1180 edge_kind_mask: &str,
1181 frontier_out: &str,
1182 node_count: u32,
1183 edge_count: u32,
1184 queue_capacity: u32,
1185 allow_mask: u32,
1186) -> Program {
1187 csr_queue_forward_traverse_with(CsrQueueForwardTraverseParams {
1188 active_queue,
1189 queue_len,
1190 edge_offsets,
1191 edge_targets,
1192 edge_kind_mask,
1193 frontier_out,
1194 node_count,
1195 edge_count,
1196 queue_capacity,
1197 allow_mask,
1198 })
1199}
1200
1201#[must_use]
1203pub fn csr_queue_forward_traverse_with(params: CsrQueueForwardTraverseParams<'_>) -> Program {
1204 let CsrQueueForwardTraverseParams {
1205 active_queue,
1206 queue_len,
1207 edge_offsets,
1208 edge_targets,
1209 edge_kind_mask,
1210 frontier_out,
1211 node_count,
1212 edge_count,
1213 queue_capacity,
1214 allow_mask,
1215 } = params;
1216 if node_count == 0 || queue_capacity == 0 {
1217 return crate::invalid_output_program(CSR_QUEUE_FORWARD_OP_ID,
1218 frontier_out,
1219 DataType::U32,
1220 format!(
1221 "Fix: csr_queue_forward_traverse requires node_count > 0 and queue_capacity > 0, got node_count={node_count} queue_capacity={queue_capacity}."
1222 ),);
1223 }
1224 csr_queue_step_program(&CsrQueueStepSpec {
1225 op_id: CSR_QUEUE_FORWARD_OP_ID,
1226 builder_name: "csr_queue_forward_traverse",
1227 prefix: "qt",
1228 workgroup_size: [256, 1, 1],
1229 inputs: CsrQueueInputs {
1230 active_queue,
1231 queue_len,
1232 edge_offsets,
1233 edge_targets,
1234 edge_kind_mask,
1235 },
1236 lanes: CsrQueueLanes::Scalar,
1237 row_plan: CsrQueueRowPlan::ExpandAll,
1238 emit: CsrQueueEmit::Frontier { frontier_out },
1239 node_count,
1240 edge_count,
1241 queue_capacity,
1242 allow_mask,
1243 })
1244}
1245
1246#[must_use]
1248#[cfg(any(test, feature = "cpu-parity"))]
1249pub fn frontier_to_queue_cpu(
1250 frontier_in: &[u32],
1251 node_count: u32,
1252 queue_capacity: usize,
1253) -> (Vec<u32>, u32) {
1254 try_frontier_to_queue_cpu(frontier_in, node_count, queue_capacity).unwrap_or_else(|err| {
1255 panic!("frontier_to_queue CPU oracle received malformed input. {err}")
1256 })
1257}
1258
1259#[cfg(any(test, feature = "cpu-parity"))]
1261pub fn try_frontier_to_queue_cpu(
1262 frontier_in: &[u32],
1263 node_count: u32,
1264 queue_capacity: usize,
1265) -> Result<(Vec<u32>, u32), String> {
1266 let mut queue: Vec<u32> = Vec::new();
1267 let seen = try_frontier_to_queue_cpu_into(frontier_in, node_count, queue_capacity, &mut queue)?;
1268 Ok((queue, seen))
1269}
1270
1271#[cfg(any(test, feature = "cpu-parity"))]
1277pub fn try_frontier_to_queue_cpu_into(
1278 frontier_in: &[u32],
1279 node_count: u32,
1280 queue_capacity: usize,
1281 queue: &mut Vec<u32>,
1282) -> Result<u32, String> {
1283 crate::bitset::frontier::materialize_frontier_queue_prefix_into(
1284 node_count,
1285 frontier_in,
1286 queue_capacity,
1287 queue,
1288 )
1289 .map_err(|error| match error {
1290 crate::bitset::frontier::FrontierError::BadShape {
1291 expected_words,
1292 actual_words,
1293 ..
1294 } => format!(
1295 "Fix: frontier_to_queue requires frontier_in.len() == bitset_words(node_count), got len={actual_words} but expected {expected_words} for node_count={node_count}."
1296 ),
1297 other => format!(
1298 "Fix: frontier_to_queue CPU oracle could not materialize the active frontier queue: {other}"
1299 ),
1300 })
1301}
1302
1303#[must_use]
1305#[cfg(any(test, feature = "cpu-parity"))]
1306pub fn csr_queue_forward_traverse_cpu(
1307 active_queue: &[u32],
1308 queue_len: u32,
1309 edge_offsets: &[u32],
1310 edge_targets: &[u32],
1311 edge_kind_mask: &[u32],
1312 node_count: u32,
1313 allow_mask: u32,
1314) -> Vec<u32> {
1315 try_csr_queue_forward_traverse_cpu(
1316 active_queue,
1317 queue_len,
1318 edge_offsets,
1319 edge_targets,
1320 edge_kind_mask,
1321 node_count,
1322 allow_mask,
1323 )
1324 .unwrap_or_else(|err| {
1325 panic!("csr_queue_forward_traverse CPU oracle received malformed input. {err}")
1326 })
1327}
1328
1329#[cfg(any(test, feature = "cpu-parity"))]
1331pub fn try_csr_queue_forward_traverse_cpu(
1332 active_queue: &[u32],
1333 queue_len: u32,
1334 edge_offsets: &[u32],
1335 edge_targets: &[u32],
1336 edge_kind_mask: &[u32],
1337 node_count: u32,
1338 allow_mask: u32,
1339) -> Result<Vec<u32>, String> {
1340 let mut out: Vec<u32> = Vec::new();
1341 try_csr_queue_forward_traverse_cpu_into(
1342 active_queue,
1343 queue_len,
1344 edge_offsets,
1345 edge_targets,
1346 edge_kind_mask,
1347 node_count,
1348 allow_mask,
1349 &mut out,
1350 )?;
1351 Ok(out)
1352}
1353
1354#[cfg(any(test, feature = "cpu-parity"))]
1356#[allow(clippy::too_many_arguments)]
1357pub fn try_csr_queue_forward_traverse_cpu_into(
1358 active_queue: &[u32],
1359 queue_len: u32,
1360 edge_offsets: &[u32],
1361 edge_targets: &[u32],
1362 edge_kind_mask: &[u32],
1363 node_count: u32,
1364 allow_mask: u32,
1365 out: &mut Vec<u32>,
1366) -> Result<(), String> {
1367 let layout = validate_csr_queue_graph(node_count, edge_offsets, edge_targets, edge_kind_mask)?;
1368 crate::graph::scratch::reserve_graph_items(
1369 out,
1370 layout.words,
1371 "CSR frontier queue CPU oracle",
1372 "frontier output bitset",
1373 )?;
1374 out.clear();
1375 out.resize(layout.words, 0);
1376 let take = (queue_len as usize).min(active_queue.len());
1377 for &src in &active_queue[..take] {
1378 if src >= node_count {
1379 continue;
1380 }
1381 let start = edge_offsets[src as usize] as usize;
1382 let end = edge_offsets[src as usize + 1] as usize;
1383 for edge in start..end.min(edge_targets.len()).min(edge_kind_mask.len()) {
1384 if edge_kind_mask[edge] & allow_mask == 0 {
1385 continue;
1386 }
1387 let dst = edge_targets[edge];
1388 if dst < node_count {
1389 out[dst as usize / 32] |= 1u32 << (dst % 32);
1390 }
1391 }
1392 }
1393 Ok(())
1394}
1395
1396#[cfg(test)]
1397mod generated_cpu_oracle_tests {
1398 use super::*;
1399
1400 #[test]
1401 fn frontier_to_queue_rejects_missing_words_without_clobbering_queue() {
1402 let mut queue = vec![7, 3, 1];
1403
1404 let err = try_frontier_to_queue_cpu_into(&[0b101], 64, 4, &mut queue)
1405 .expect_err("short frontier bitset must fail exact-width validation");
1406
1407 assert!(
1408 err.contains("frontier_in.len() == bitset_words(node_count)"),
1409 "Fix: frontier width error must identify the exact bitset contract, got: {err}"
1410 );
1411 assert_eq!(
1412 queue,
1413 vec![7, 3, 1],
1414 "failed frontier materialization must preserve previous queue diagnostics"
1415 );
1416 }
1417
1418 #[test]
1419 fn frontier_to_queue_clamps_queue_prefix_and_masks_tail_bits() {
1420 let frontier = [0b1010_u32, u32::MAX];
1421 let mut queue = Vec::new();
1422
1423 let seen = try_frontier_to_queue_cpu_into(&frontier, 33, 2, &mut queue)
1424 .expect("Fix: canonical frontier should materialize through the CPU oracle");
1425
1426 assert_eq!(seen, 3);
1427 assert_eq!(queue, vec![1, 3]);
1428 assert!(
1429 queue.iter().all(|node| *node < 33),
1430 "out-of-domain tail bits must not enter the compact queue prefix"
1431 );
1432 }
1433
1434 #[test]
1435 fn queue_forward_traverse_into_rejects_bad_graph_without_clobbering_output() {
1436 let mut out = vec![0xDEAD_BEEF];
1437
1438 let err = try_csr_queue_forward_traverse_cpu_into(
1439 &[0],
1440 1,
1441 &[0, 1, 1],
1442 &[2],
1443 &[1],
1444 2,
1445 1,
1446 &mut out,
1447 )
1448 .expect_err("out-of-range target must fail CSR queue graph validation");
1449
1450 assert!(
1451 err.contains("outside node_count"),
1452 "Fix: queue traversal graph errors must identify invalid targets, got: {err}"
1453 );
1454 assert_eq!(
1455 out,
1456 vec![0xDEAD_BEEF],
1457 "failed queue traversal preflight must preserve previous output diagnostics"
1458 );
1459 }
1460
1461 #[test]
1462 fn generated_frontier_queue_and_traverse_cpu_oracles_match_shape_contracts() {
1463 for node_count in 1u32..=128 {
1464 let edge_offsets: Vec<u32> = (0..=node_count).collect();
1465 let edge_targets: Vec<u32> = (0..node_count)
1466 .map(|node| (node + 1) % node_count)
1467 .collect();
1468 let edge_kind_mask = vec![1u32; node_count as usize];
1469 for queue_capacity in 0usize..32 {
1470 let mut frontier = vec![0u32; bitset_words(node_count) as usize];
1471 let period = (queue_capacity as u32 % 7) + 1;
1472 let mut expected_seen = 0u32;
1473 for node in 0..node_count {
1474 if node % period == 0 {
1475 frontier[node as usize / 32] |= 1u32 << (node % 32);
1476 expected_seen = expected_seen.saturating_add(1);
1477 }
1478 }
1479 let (queue, seen) =
1480 try_frontier_to_queue_cpu(&frontier, node_count, queue_capacity).unwrap();
1481 assert_eq!(seen, expected_seen);
1482 assert_eq!(queue.len(), queue_capacity.min(expected_seen as usize));
1483 let out = try_csr_queue_forward_traverse_cpu(
1484 &queue,
1485 seen,
1486 &edge_offsets,
1487 &edge_targets,
1488 &edge_kind_mask,
1489 node_count,
1490 1,
1491 )
1492 .unwrap();
1493 assert_eq!(out.len(), bitset_words(node_count) as usize);
1494 for &src in &queue {
1495 let dst = (src + 1) % node_count;
1496 assert_ne!(out[dst as usize / 32] & (1u32 << (dst % 32)), 0);
1497 }
1498 }
1499 }
1500 }
1501}
1502
1503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1508pub struct CsrQueueGraphLayout {
1509 pub node_count: u32,
1511 pub edge_count: u32,
1513 pub max_row_degree: u32,
1515 pub words: usize,
1517 pub edge_storage_words: usize,
1519}
1520
1521pub fn validate_csr_queue_graph(
1531 node_count: u32,
1532 edge_offsets: &[u32],
1533 edge_targets: &[u32],
1534 edge_kind_mask: &[u32],
1535) -> Result<CsrQueueGraphLayout, String> {
1536 if node_count == 0 {
1537 return Err("Fix: csr_queue_forward_traverse requires node_count > 0.".to_string());
1538 }
1539 let expected_offsets = (node_count as usize).checked_add(1).ok_or_else(|| {
1540 format!(
1541 "Fix: csr_queue_forward_traverse node_count + 1 overflows usize for node_count={node_count}."
1542 )
1543 })?;
1544 if edge_offsets.len() != expected_offsets {
1545 return Err(format!(
1546 "Fix: csr_queue_forward_traverse requires edge_offsets.len() == node_count + 1, got len={}, node_count={node_count}.",
1547 edge_offsets.len()
1548 ));
1549 }
1550 if edge_targets.len() != edge_kind_mask.len() {
1551 return Err(format!(
1552 "Fix: csr_queue_forward_traverse requires edge_targets.len() == edge_kind_mask.len(), got {} vs {}.",
1553 edge_targets.len(),
1554 edge_kind_mask.len()
1555 ));
1556 }
1557 if edge_offsets[0] != 0 {
1558 return Err(format!(
1559 "Fix: csr_queue_forward_traverse requires edge_offsets[0] == 0, got {}.",
1560 edge_offsets[0]
1561 ));
1562 }
1563 let mut max_row_degree = 0u32;
1564 for (row, pair) in edge_offsets.windows(2).enumerate() {
1565 if pair[0] > pair[1] {
1566 return Err(format!(
1567 "Fix: csr_queue_forward_traverse offsets must be monotonic at row {row}: {} > {}.",
1568 pair[0], pair[1]
1569 ));
1570 }
1571 max_row_degree = max_row_degree.max(pair[1] - pair[0]);
1572 }
1573 let edge_count = edge_offsets[expected_offsets - 1] as usize;
1574 if edge_targets.len() != edge_count {
1575 return Err(format!(
1576 "Fix: csr_queue_forward_traverse final offset declares edge_count={edge_count}, but targets_len={} and kind_mask_len={}.",
1577 edge_targets.len(),
1578 edge_kind_mask.len()
1579 ));
1580 }
1581 for (index, &target) in edge_targets.iter().enumerate() {
1582 if target >= node_count {
1583 return Err(format!(
1584 "Fix: csr_queue_forward_traverse edge_targets[{index}]={target} is outside node_count {node_count}."
1585 ));
1586 }
1587 }
1588 let edge_count = u32::try_from(edge_count).map_err(|_| {
1589 format!("Fix: csr_queue_forward_traverse edge count {edge_count} exceeds u32 index space.")
1590 })?;
1591 Ok(CsrQueueGraphLayout {
1592 node_count,
1593 edge_count,
1594 max_row_degree,
1595 words: bitset_words(node_count) as usize,
1596 edge_storage_words: edge_targets.len().max(1),
1597 })
1598}
1599
1600pub fn validate_frontier_queue_batch(
1612 node_count: u32,
1613 frontiers: &[&[u32]],
1614 queue_capacity: u32,
1615) -> Result<usize, String> {
1616 if node_count == 0 {
1617 return Err("Fix: resident CSR queue batch requires node_count > 0.".to_string());
1618 }
1619 if frontiers.is_empty() {
1620 return Err("Fix: resident CSR queue batch requires at least one frontier.".to_string());
1621 }
1622 if queue_capacity == 0 {
1623 return Err("Fix: resident CSR queue batch requires queue_capacity > 0.".to_string());
1624 }
1625
1626 let expected_words = bitset_words(node_count) as usize;
1627 for (query_index, frontier) in frontiers.iter().enumerate() {
1628 if frontier.len() != expected_words {
1629 return Err(format!(
1630 "Fix: resident CSR queue batch query {query_index} expected {expected_words} frontier word(s) for node_count={node_count} but received {}.",
1631 frontier.len()
1632 ));
1633 }
1634 }
1635 Ok(expected_words)
1636}
1637
1638pub fn validate_frontier_queue_query(
1649 node_count: u32,
1650 frontier: &[u32],
1651 queue_capacity: u32,
1652) -> Result<usize, String> {
1653 validate_frontier_queue_batch(node_count, &[frontier], queue_capacity).map_err(|error| {
1654 error
1655 .replace("resident CSR queue batch", "resident CSR queue query")
1656 .replace("query 0", "query")
1657 })
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663 use vyre_foundation::transform::visit::{walk_exprs, walk_nodes};
1664
1665 #[test]
1666 fn cpu_queue_preserves_node_order_and_reports_overflow_pressure() {
1667 let (queue, len) = frontier_to_queue_cpu(&[0b10111], 5, 3);
1668 assert_eq!(queue, vec![0, 1, 2]);
1669 assert_eq!(len, 4);
1670 }
1671
1672 #[test]
1673 fn cpu_queue_traverse_expands_only_queued_sources() {
1674 let edge_offsets = vec![0, 2, 3, 3, 3];
1675 let edge_targets = vec![1, 2, 3];
1676 let edge_kind_mask = vec![1, 2, 1];
1677 let out = csr_queue_forward_traverse_cpu(
1678 &[0, 1],
1679 2,
1680 &edge_offsets,
1681 &edge_targets,
1682 &edge_kind_mask,
1683 4,
1684 1,
1685 );
1686 assert_eq!(out, vec![0b1010]);
1687 }
1688
1689 #[test]
1690 fn packed_word_queue_reserves_once_per_nonzero_word() {
1691 let program = frontier_words_to_queue_parallel("frontier", "queue", "len", 35, 16);
1692
1693 assert_eq!(
1694 count_atomic_exprs(&program),
1695 1,
1696 "packed-word queue materialization should have one static queue reservation site"
1697 );
1698 assert_eq!(
1699 loop_atomic_count(&program, "qw_rank"),
1700 Some(0),
1701 "per-active-bit scatter loop must not issue atomics after the word-level reservation"
1702 );
1703 assert!(
1704 assignment_contains_u32(&program, "qw_remaining", 0b111),
1705 "the packed-word materializer must mask tail bits before popcounting the final frontier word"
1706 );
1707 }
1708
1709 #[test]
1710 fn emitted_programs_have_stable_shapes() {
1711 let queue_len_init = frontier_queue_len_init("len");
1712 assert_eq!(queue_len_init.workgroup_size, [1, 1, 1]);
1713 assert_eq!(queue_len_init.buffers.len(), 1);
1714 let queue = frontier_to_queue("frontier", "queue", "len", 64, 8);
1715 assert_eq!(queue.workgroup_size, [256, 1, 1]);
1716 assert_eq!(queue.buffers.len(), 3);
1717 let parallel_queue = frontier_to_queue_parallel("frontier", "queue", "len", 64, 8);
1718 assert_eq!(parallel_queue.workgroup_size, [256, 1, 1]);
1719 assert_eq!(parallel_queue.buffers.len(), 3);
1720 let word_queue = frontier_words_to_queue_parallel("frontier", "queue", "len", 64, 8);
1721 assert_eq!(word_queue.workgroup_size, [256, 1, 1]);
1722 assert_eq!(word_queue.buffers.len(), 3);
1723 assert_eq!(word_queue.buffers[0].count, 2);
1724 let word_queue_clear =
1725 frontier_words_to_queue_clear_out_parallel("frontier", "queue", "len", "out", 64, 8);
1726 assert_eq!(word_queue_clear.workgroup_size, [256, 1, 1]);
1727 assert_eq!(word_queue_clear.buffers.len(), 4);
1728 assert_eq!(word_queue_clear.buffers[0].count, 2);
1729 assert_eq!(word_queue_clear.buffers[3].name.as_ref(), "out");
1730 assert_eq!(word_queue_clear.buffers[3].count, 2);
1731 let word_scan =
1732 frontier_word_counts_scan_pass_a("frontier", "partials", "block_totals", 64);
1733 assert_eq!(word_scan.workgroup_size, [1024, 1, 1]);
1734 assert_eq!(word_scan.buffers.len(), 5);
1735 assert_eq!(word_scan.buffers[0].count, 2);
1736 assert_eq!(word_scan.buffers[1].count, 1024);
1737 assert_eq!(word_scan.buffers[2].count, 1);
1738 let block_offsets = frontier_word_block_offsets_in_place("block_totals", 32_897);
1739 assert_eq!(block_offsets.workgroup_size, [1024, 1, 1]);
1740 assert_eq!(block_offsets.buffers.len(), 3);
1741 assert_eq!(block_offsets.buffers[0].count, 2);
1742 let huge_block_offsets = frontier_word_block_offsets_in_place("block_totals", 33_554_433);
1743 assert_eq!(huge_block_offsets.workgroup_size, [1, 1, 1]);
1744 assert_eq!(huge_block_offsets.buffers.len(), 1);
1745 assert_eq!(huge_block_offsets.buffers[0].count, 1025);
1746 let prefix_queue = frontier_word_block_prefix_to_queue_parallel(
1747 "frontier",
1748 "partials",
1749 "block_totals",
1750 "queue",
1751 "len",
1752 64,
1753 8,
1754 );
1755 assert_eq!(prefix_queue.workgroup_size, [256, 1, 1]);
1756 assert_eq!(prefix_queue.buffers.len(), 5);
1757 assert_eq!(prefix_queue.buffers[0].count, 2);
1758 assert_eq!(prefix_queue.buffers[1].count, 1024);
1759 assert_eq!(prefix_queue.buffers[2].count, 1);
1760 let offset_queue = frontier_word_block_offsets_to_queue_parallel(
1761 "frontier",
1762 "partials",
1763 "block_offsets",
1764 "queue",
1765 "len",
1766 32_897,
1767 8,
1768 );
1769 assert_eq!(offset_queue.workgroup_size, [256, 1, 1]);
1770 assert_eq!(offset_queue.buffers.len(), 5);
1771 assert_eq!(offset_queue.buffers[0].count, 1029);
1772 assert_eq!(offset_queue.buffers[1].count, 2048);
1773 assert_eq!(offset_queue.buffers[2].count, 2);
1774 assert!(
1775 !format!("{:?}", offset_queue.entry()).contains("fwq_prev_block"),
1776 "precomputed-offset scatter must not retain the per-word previous-block loop"
1777 );
1778 let traverse = csr_queue_forward_traverse(
1779 "queue", "len", "offsets", "targets", "kinds", "out", 64, 7, 8, 1,
1780 );
1781 assert_eq!(traverse.workgroup_size, [256, 1, 1]);
1782 assert_eq!(traverse.buffers.len(), 6);
1783 }
1784
1785 #[test]
1786 fn frontier_queue_sizing_overflow_returns_error_without_panic() {
1787 let byte_result = std::panic::catch_unwind(|| {
1788 try_u32_byte_range_with_word_size(2, usize::MAX, "test frontier queue bytes")
1789 });
1790 assert!(
1791 byte_result.is_ok(),
1792 "checked frontier byte sizing must return an error instead of panicking"
1793 );
1794 let err = byte_result.unwrap().unwrap_err().to_string();
1795 assert!(
1796 err.contains("overflows output byte range"),
1797 "Fix: byte sizing overflow must name the byte-range contract, got: {err}"
1798 );
1799 assert!(
1800 err.contains("Shard the frontier queue"),
1801 "Fix: byte sizing overflow must tell the operator how to recover, got: {err}"
1802 );
1803
1804 let product_result = std::panic::catch_unwind(|| {
1805 checked_frontier_u32_product(u32::MAX, 2, "test partial word count")
1806 });
1807 assert!(
1808 product_result.is_ok(),
1809 "checked frontier word products must return an error instead of panicking"
1810 );
1811 let err = product_result.unwrap().unwrap_err().to_string();
1812 assert!(
1813 err.contains("overflows u32 word count"),
1814 "Fix: word-count overflow must name the u32 product contract, got: {err}"
1815 );
1816 }
1817
1818 #[test]
1819 fn csr_queue_traverse_rejects_offset_count_overflow_without_panic() {
1820 let result = std::panic::catch_unwind(|| {
1821 csr_queue_forward_traverse(
1822 "queue",
1823 "len",
1824 "offsets",
1825 "targets",
1826 "kinds",
1827 "out",
1828 u32::MAX,
1829 0,
1830 1,
1831 1,
1832 )
1833 });
1834 assert!(
1835 result.is_ok(),
1836 "csr_queue_forward_traverse must emit an invalid program instead of panicking"
1837 );
1838
1839 let program = result.unwrap();
1840 assert_eq!(program.workgroup_size, [1, 1, 1]);
1841 assert_eq!(program.buffers.len(), 1);
1842 assert_eq!(program.buffers[0].name.as_ref(), "out");
1843 let entry = format!("{:?}", program.entry());
1844 assert!(
1845 entry.contains("node_count + 1 overflows u32"),
1846 "Fix: invalid CSR queue program must preserve the offset overflow diagnostic, got: {entry}"
1847 );
1848 }
1849
1850 fn count_atomic_exprs(program: &Program) -> usize {
1851 let mut count = 0;
1852 walk_exprs(program, |expr| {
1853 if matches!(expr, Expr::Atomic { .. }) {
1854 count += 1;
1855 }
1856 });
1857 count
1858 }
1859
1860 fn loop_atomic_count(program: &Program, loop_var: &str) -> Option<usize> {
1861 let mut count = None;
1862 walk_nodes(program, |node| {
1863 if count.is_some() {
1864 return;
1865 }
1866 if let Node::Loop { var, body, .. } = node {
1867 if var.as_ref() == loop_var {
1868 let loop_program = Program::wrapped(Vec::new(), [1, 1, 1], body.clone());
1869 count = Some(count_atomic_exprs(&loop_program));
1870 }
1871 }
1872 });
1873 count
1874 }
1875
1876 fn assignment_contains_u32(program: &Program, target: &str, value: u32) -> bool {
1877 let mut found = false;
1878 walk_nodes(program, |node| {
1879 if found {
1880 return;
1881 }
1882 if let Node::Assign { name, value: expr } = node {
1883 if name.as_ref() == target && expr_contains_u32(expr, value) {
1884 found = true;
1885 }
1886 }
1887 });
1888 found
1889 }
1890
1891 fn expr_contains_u32(expr: &Expr, value: u32) -> bool {
1892 let mut found = false;
1893 let expr_program = Program::wrapped(
1894 Vec::new(),
1895 [1, 1, 1],
1896 vec![Node::let_bind("__expr_probe", expr.clone())],
1897 );
1898 walk_exprs(&expr_program, |expr| {
1899 if matches!(expr, Expr::LitU32(found_value) if *found_value == value) {
1900 found = true;
1901 }
1902 });
1903 found
1904 }
1905
1906 #[test]
1907 fn validate_csr_queue_graph_accepts_zero_edge_graph_and_canonical_graph() {
1908 assert_eq!(
1909 validate_csr_queue_graph(3, &[0, 0, 0, 0], &[], &[]).unwrap(),
1910 CsrQueueGraphLayout {
1911 node_count: 3,
1912 edge_count: 0,
1913 max_row_degree: 0,
1914 words: 1,
1915 edge_storage_words: 1,
1916 }
1917 );
1918 assert_eq!(
1919 validate_csr_queue_graph(4, &[0, 2, 3, 3, 3], &[1, 2, 3], &[1, 2, 1]).unwrap(),
1920 CsrQueueGraphLayout {
1921 node_count: 4,
1922 edge_count: 3,
1923 max_row_degree: 2,
1924 words: 1,
1925 edge_storage_words: 3,
1926 }
1927 );
1928 }
1929
1930 #[test]
1931 fn validate_csr_queue_graph_rejects_malformed_inputs() {
1932 let err = validate_csr_queue_graph(0, &[0], &[], &[]).unwrap_err();
1933 assert!(err.contains("node_count > 0"));
1934
1935 let err = validate_csr_queue_graph(2, &[0, 1, 1], &[1], &[]).unwrap_err();
1936 assert!(err.contains("edge_targets.len() == edge_kind_mask.len()"));
1937
1938 let err = validate_csr_queue_graph(2, &[0, 2, 1], &[1], &[1]).unwrap_err();
1939 assert!(err.contains("offsets must be monotonic"));
1940
1941 let err = validate_csr_queue_graph(2, &[0, 1, 1], &[5], &[1]).unwrap_err();
1942 assert!(err.contains("outside node_count"));
1943 }
1944
1945 #[test]
1946 fn validate_frontier_queue_batch_accepts_canonical_frontiers() {
1947 let frontiers: [&[u32]; 2] = [&[1, 0], &[0, 2]];
1948
1949 let words = validate_frontier_queue_batch(64, &frontiers, 8)
1950 .expect("Fix: two 64-node frontiers should be valid");
1951
1952 assert_eq!(words, 2);
1953 }
1954
1955 #[test]
1956 fn validate_frontier_queue_batch_rejects_invalid_batch_shapes() {
1957 let frontier: [&[u32]; 1] = [&[1]];
1958
1959 let err = validate_frontier_queue_batch(0, &frontier, 8).unwrap_err();
1960 assert!(err.contains("node_count > 0"));
1961
1962 let empty: [&[u32]; 0] = [];
1963 let err = validate_frontier_queue_batch(64, &empty, 8).unwrap_err();
1964 assert!(err.contains("at least one frontier"));
1965
1966 let err = validate_frontier_queue_batch(64, &frontier, 0).unwrap_err();
1967 assert!(err.contains("queue_capacity > 0"));
1968
1969 let err = validate_frontier_queue_batch(64, &frontier, 8).unwrap_err();
1970 assert!(err.contains("query 0 expected 2 frontier word"));
1971 }
1972
1973 #[test]
1974 fn validate_frontier_queue_query_delegates_single_frontier_contract() {
1975 assert_eq!(validate_frontier_queue_query(64, &[1, 0], 8).unwrap(), 2);
1976
1977 let err = validate_frontier_queue_query(64, &[1], 8).unwrap_err();
1978 assert!(err.contains("query expected 2 frontier word"));
1979
1980 let err = validate_frontier_queue_query(64, &[1, 0], 0).unwrap_err();
1981 assert!(err.contains("queue_capacity > 0"));
1982 }
1983}