1use super::FrontierDensityTelemetry;
2
3#[derive(Clone, Debug, PartialEq)]
5pub(crate) struct FixedPointForwardChangedProgramCache {
6 pub(crate) kind: crate::fixed_point_graph::FixedPointAnalysisKind,
7 pub(crate) node_count: u32,
8 pub(crate) edge_count: u32,
9 pub(crate) program: vyre::ir::Program,
10}
11
12#[derive(Clone, Debug, Default, PartialEq)]
14pub struct FixedPointScratch {
15 pub(crate) frontier_words: Vec<u32>,
16 pub(crate) frontier_bytes: Vec<u8>,
17 pub(crate) previous_frontier_bytes: Vec<u8>,
18 pub(crate) changed_bytes: Vec<u8>,
19 pub(crate) static_input_bytes: Vec<Vec<u8>>,
20 pub(crate) outputs: Vec<Vec<u8>>,
21 pub(crate) next: Vec<u32>,
22 pub(crate) resources: Vec<vyre::backend::Resource>,
23 pub(crate) alternate_resources: Vec<vyre::backend::Resource>,
24 pub(crate) changed_resources: Vec<vyre::backend::Resource>,
25 pub(crate) clear_changed_program: Option<vyre::ir::Program>,
26 pub(crate) forward_changed_program: Option<FixedPointForwardChangedProgramCache>,
27 pub(crate) edge_kind_masks: Vec<u32>,
28 pub(crate) reverse_indegree: Vec<usize>,
29 pub(crate) reverse_cursor: Vec<usize>,
30 pub(crate) reverse_offsets: Vec<u32>,
31 pub(crate) reverse_targets: Vec<u32>,
32 pub(crate) reverse_masks: Vec<u32>,
33 pub(crate) frontier_density: FrontierDensityTelemetry,
34 pub(crate) dispatch_config: vyre::DispatchConfig,
35 #[cfg(feature = "gpu-telemetry")]
36 pub(crate) telemetry_popcount_input_bytes: Vec<u8>,
37 #[cfg(feature = "gpu-telemetry")]
38 pub(crate) telemetry_popcount_output_bytes: Vec<u8>,
39 #[cfg(feature = "gpu-telemetry")]
40 pub(crate) telemetry_sum_bytes: Vec<u8>,
41 #[cfg(feature = "gpu-telemetry")]
42 pub(crate) telemetry_xor_words: Vec<u32>,
43 #[cfg(feature = "gpu-telemetry")]
44 pub(crate) telemetry_popcount_program: Option<(u32, vyre::ir::Program)>,
45 #[cfg(feature = "gpu-telemetry")]
46 pub(crate) telemetry_reduce_program: Option<(u32, vyre::ir::Program)>,
47}
48
49impl FixedPointScratch {
50 pub fn with_capacities(
60 frontier_words: usize,
61 output_slots: usize,
62 resource_slots: usize,
63 ) -> Result<Self, String> {
64 let frontier_bytes = frontier_words
65 .checked_mul(std::mem::size_of::<u32>())
66 .ok_or_else(|| {
67 "weir fixed-point scratch frontier byte capacity overflowed usize. Fix: lower frontier_words before allocating reusable scratch."
68 .to_string()
69 })?;
70 let mut scratch = Self {
71 frontier_words: Vec::new(),
72 frontier_bytes: Vec::new(),
73 previous_frontier_bytes: Vec::new(),
74 changed_bytes: Vec::new(),
75 static_input_bytes: Vec::new(),
76 outputs: Vec::new(),
77 next: Vec::new(),
78 resources: Vec::new(),
79 alternate_resources: Vec::new(),
80 changed_resources: Vec::new(),
81 clear_changed_program: None,
82 forward_changed_program: None,
83 edge_kind_masks: Vec::new(),
84 reverse_indegree: Vec::new(),
85 reverse_cursor: Vec::new(),
86 reverse_offsets: Vec::new(),
87 reverse_targets: Vec::new(),
88 reverse_masks: Vec::new(),
89 frontier_density: FrontierDensityTelemetry::default(),
90 dispatch_config: vyre::DispatchConfig::default(),
91 #[cfg(feature = "gpu-telemetry")]
92 telemetry_popcount_input_bytes: Vec::new(),
93 #[cfg(feature = "gpu-telemetry")]
94 telemetry_popcount_output_bytes: Vec::new(),
95 #[cfg(feature = "gpu-telemetry")]
96 telemetry_sum_bytes: Vec::new(),
97 #[cfg(feature = "gpu-telemetry")]
98 telemetry_xor_words: Vec::new(),
99 #[cfg(feature = "gpu-telemetry")]
100 telemetry_popcount_program: None,
101 #[cfg(feature = "gpu-telemetry")]
102 telemetry_reduce_program: None,
103 };
104 crate::staging_reserve::reserve_vec(
105 &mut scratch.frontier_words,
106 frontier_words,
107 "fixed-point frontier word",
108 )?;
109 crate::staging_reserve::reserve_vec(
110 &mut scratch.frontier_bytes,
111 frontier_bytes,
112 "fixed-point frontier byte",
113 )?;
114 crate::staging_reserve::reserve_vec(
115 &mut scratch.previous_frontier_bytes,
116 frontier_bytes,
117 "fixed-point previous frontier byte",
118 )?;
119 crate::staging_reserve::reserve_vec(
120 &mut scratch.changed_bytes,
121 std::mem::size_of::<u32>(),
122 "fixed-point changed flag byte",
123 )?;
124 crate::staging_reserve::reserve_vec(
125 &mut scratch.static_input_bytes,
126 resource_slots,
127 "fixed-point static input byte slot",
128 )?;
129 crate::staging_reserve::resize_vec_slots(
130 &mut scratch.outputs,
131 output_slots,
132 "fixed-point output slot",
133 )?;
134 crate::staging_reserve::reserve_vec(
135 &mut scratch.next,
136 frontier_words,
137 "fixed-point next frontier word",
138 )?;
139 crate::staging_reserve::reserve_vec(
140 &mut scratch.resources,
141 resource_slots,
142 "fixed-point resident resource",
143 )?;
144 crate::staging_reserve::reserve_vec(
145 &mut scratch.alternate_resources,
146 resource_slots,
147 "fixed-point alternate resident resource",
148 )?;
149 crate::staging_reserve::reserve_vec(
150 &mut scratch.changed_resources,
151 resource_slots,
152 "fixed-point changed resident resource",
153 )?;
154 #[cfg(feature = "gpu-telemetry")]
155 {
156 crate::staging_reserve::reserve_vec(
157 &mut scratch.telemetry_popcount_input_bytes,
158 frontier_bytes,
159 "telemetry popcount input byte",
160 )?;
161 crate::staging_reserve::reserve_vec(
162 &mut scratch.telemetry_popcount_output_bytes,
163 frontier_bytes,
164 "telemetry popcount output byte",
165 )?;
166 crate::staging_reserve::reserve_vec(
167 &mut scratch.telemetry_sum_bytes,
168 std::mem::size_of::<u32>(),
169 "telemetry reduce sum byte",
170 )?;
171 crate::staging_reserve::reserve_vec(
172 &mut scratch.telemetry_xor_words,
173 frontier_words,
174 "telemetry xor word",
175 )?;
176 }
177 Ok(scratch)
178 }
179
180 pub fn clear(&mut self) {
182 self.frontier_words.clear();
183 self.frontier_bytes.clear();
184 self.previous_frontier_bytes.clear();
185 self.changed_bytes.clear();
186 crate::output_scratch::clear_outputs_preserving_slots(&mut self.static_input_bytes);
187 crate::output_scratch::clear_outputs_preserving_slots(&mut self.outputs);
188 self.next.clear();
189 self.resources.clear();
190 self.alternate_resources.clear();
191 self.changed_resources.clear();
192 self.edge_kind_masks.clear();
193 self.reverse_indegree.clear();
194 self.reverse_cursor.clear();
195 self.reverse_offsets.clear();
196 self.reverse_targets.clear();
197 self.reverse_masks.clear();
198 self.frontier_density = FrontierDensityTelemetry::default();
199 #[cfg(feature = "gpu-telemetry")]
200 {
201 self.telemetry_popcount_input_bytes.clear();
202 self.telemetry_popcount_output_bytes.clear();
203 self.telemetry_sum_bytes.clear();
204 self.telemetry_xor_words.clear();
205 self.telemetry_popcount_program = None;
206 self.telemetry_reduce_program = None;
207 }
208 }
209
210 pub(crate) fn prepare_static_input_byte_slots(&mut self, slots: usize) -> Result<(), String> {
212 crate::staging_reserve::resize_vec_slots(
213 &mut self.static_input_bytes,
214 slots,
215 "fixed-point static input byte slot",
216 )?;
217 crate::output_scratch::clear_outputs_preserving_slots(&mut self.static_input_bytes);
218 Ok(())
219 }
220
221 #[must_use]
223 pub fn output_slot_count(&self) -> usize {
224 self.outputs.len()
225 }
226
227 #[must_use]
229 pub fn output_slot_capacity(&self) -> usize {
230 self.outputs.capacity()
231 }
232
233 #[must_use]
235 pub fn static_input_slot_capacity(&self) -> usize {
236 self.static_input_bytes.capacity()
237 }
238
239 #[must_use]
241 pub fn frontier_byte_capacity(&self) -> usize {
242 self.frontier_bytes.capacity()
243 }
244
245 #[must_use]
247 pub fn frontier_word_capacity(&self) -> usize {
248 self.frontier_words.capacity()
249 }
250
251 #[must_use]
253 pub fn next_word_capacity(&self) -> usize {
254 self.next.capacity()
255 }
256
257 #[must_use]
259 pub fn resource_capacity(&self) -> usize {
260 self.resources.capacity()
261 }
262
263 #[must_use]
265 pub fn alternate_resource_capacity(&self) -> usize {
266 self.alternate_resources.capacity()
267 }
268
269 #[must_use]
271 pub fn edge_kind_mask_capacity(&self) -> usize {
272 self.edge_kind_masks.capacity()
273 }
274
275 #[must_use]
277 pub fn reverse_offset_capacity(&self) -> usize {
278 self.reverse_offsets.capacity()
279 }
280
281 #[must_use]
283 pub fn reverse_target_capacity(&self) -> usize {
284 self.reverse_targets.capacity()
285 }
286
287 #[must_use]
289 pub fn frontier_density(&self) -> FrontierDensityTelemetry {
290 self.frontier_density
291 }
292
293 pub(crate) fn prepare_frontier_words(
294 &mut self,
295 words: usize,
296 seed_bits: &[u32],
297 ) -> Result<(), String> {
298 crate::dispatch_decode::try_write_zero_words(
299 &mut self.frontier_words,
300 words,
301 "fixed-point frontier word staging",
302 )?;
303 for (dst, src) in self
304 .frontier_words
305 .iter_mut()
306 .zip(seed_bits.iter().copied())
307 {
308 *dst = src;
309 }
310 Ok(())
311 }
312
313 pub(crate) fn begin_frontier_density(&mut self, domain_bits: u32) {
314 self.frontier_density = FrontierDensityTelemetry::begin(domain_bits);
315 self.record_current_frontier_sample();
316 }
317
318 pub(crate) fn record_decoded_frontier_transition(&mut self) {
319 let domain_bits = domain_bits_to_usize(
320 self.frontier_density.domain_bits,
321 &mut self.frontier_density,
322 );
323 let active = count_set_domain_bits(&self.next, domain_bits, &mut self.frontier_density);
324 let delta = count_changed_domain_bits(
325 &self.frontier_words,
326 &self.next,
327 domain_bits,
328 &mut self.frontier_density,
329 );
330 self.frontier_density.record_transition(active, delta);
331 }
332
333 pub(crate) fn record_compacted_frontier_window(&mut self, iterations: u32) {
334 let domain_bits = domain_bits_to_usize(
335 self.frontier_density.domain_bits,
336 &mut self.frontier_density,
337 );
338 let active = count_set_domain_bits(&self.next, domain_bits, &mut self.frontier_density);
339 let delta = count_changed_domain_bits(
340 &self.frontier_words,
341 &self.next,
342 domain_bits,
343 &mut self.frontier_density,
344 );
345 self.frontier_density
346 .record_window(iterations, active, delta);
347 }
348
349 fn record_current_frontier_sample(&mut self) {
350 let domain_bits = domain_bits_to_usize(
351 self.frontier_density.domain_bits,
352 &mut self.frontier_density,
353 );
354 let active = count_set_domain_bits(
355 &self.frontier_words,
356 domain_bits,
357 &mut self.frontier_density,
358 );
359 self.frontier_density.record_sample(active);
360 }
361
362 pub(crate) fn prepare_next_words(&mut self, words: usize) -> Result<(), String> {
363 self.next.clear();
364 reserve_total(&mut self.next, words, "fixed-point next frontier word")
365 }
366
367 pub(crate) fn prepare_grid_dispatch_config(
368 &mut self,
369 base: &vyre::DispatchConfig,
370 grid_override: [u32; 3],
371 ) {
372 if !dispatch_config_matches_base_except_grid(&self.dispatch_config, base) {
373 self.dispatch_config.profile.clone_from(&base.profile);
374 self.dispatch_config.ulp_budget = base.ulp_budget;
375 self.dispatch_config.timeout = base.timeout;
376 self.dispatch_config.label.clone_from(&base.label);
377 self.dispatch_config.max_output_bytes = base.max_output_bytes;
378 self.dispatch_config.workgroup_override = base.workgroup_override;
379 self.dispatch_config.fixpoint_iterations = base.fixpoint_iterations;
380 self.dispatch_config.speculation = base.speculation;
381 self.dispatch_config.persistent_thread = base.persistent_thread;
382 self.dispatch_config.cooperative = base.cooperative;
383 }
384 self.dispatch_config.grid_override = Some(grid_override);
385 }
386
387 pub(crate) fn prepare_edge_kind_masks(
388 &mut self,
389 edge_kind_mask: &[u32],
390 allowed_mask: u32,
391 ) -> Result<(), String> {
392 reserve_total(
393 &mut self.edge_kind_masks,
394 edge_kind_mask.len(),
395 "fixed-point filtered edge-kind mask",
396 )?;
397 if self.edge_kind_masks.len() == edge_kind_mask.len() {
398 for (slot, kind) in self.edge_kind_masks.iter_mut().zip(edge_kind_mask) {
399 *slot = *kind & allowed_mask;
400 }
401 } else {
402 self.edge_kind_masks.clear();
403 self.edge_kind_masks
404 .extend(edge_kind_mask.iter().map(|kind| kind & allowed_mask));
405 }
406 Ok(())
407 }
408
409 pub(crate) fn prepare_reverse_csr_scratch(
410 &mut self,
411 node_count: usize,
412 edge_count: usize,
413 ) -> Result<(), String> {
414 self.reverse_indegree.clear();
415 reserve_total(
416 &mut self.reverse_indegree,
417 node_count,
418 "fixed-point reverse CSR indegree",
419 )?;
420 self.reverse_indegree
421 .extend(std::iter::repeat_n(0, node_count));
422 self.reverse_cursor.clear();
423 reserve_total(
424 &mut self.reverse_cursor,
425 node_count,
426 "fixed-point reverse CSR cursor",
427 )?;
428 self.reverse_cursor
429 .extend(std::iter::repeat_n(0, node_count));
430 let offset_words = node_count.checked_add(1).ok_or_else(|| {
431 "fixed-point reverse CSR scratch node_count overflows sentinel offset. Fix: shard the graph before reverse CSR preparation."
432 .to_string()
433 })?;
434 crate::dispatch_decode::try_write_zero_words(
435 &mut self.reverse_offsets,
436 offset_words,
437 "fixed-point reverse CSR offset",
438 )?;
439 self.reverse_targets.clear();
440 reserve_total(
441 &mut self.reverse_targets,
442 edge_count.max(1),
443 "fixed-point reverse CSR target",
444 )?;
445 self.reverse_masks.clear();
446 reserve_total(
447 &mut self.reverse_masks,
448 edge_count.max(1),
449 "fixed-point reverse CSR mask",
450 )?;
451 Ok(())
452 }
453
454 #[cfg(test)]
455 pub(crate) fn replace_outputs_preserving_slots(&mut self, incoming: Vec<Vec<u8>>) {
456 crate::output_scratch::replace_outputs_preserving_slots(&mut self.outputs, incoming);
457 }
458}
459
460#[inline]
461fn reserve_total<T>(values: &mut Vec<T>, total: usize, field: &'static str) -> Result<(), String> {
462 crate::staging_reserve::reserve_vec(values, total, field)
463}
464
465fn dispatch_config_matches_base_except_grid(
466 cached: &vyre::DispatchConfig,
467 base: &vyre::DispatchConfig,
468) -> bool {
469 cached.profile == base.profile
470 && cached.ulp_budget == base.ulp_budget
471 && cached.timeout == base.timeout
472 && cached.label == base.label
473 && cached.max_output_bytes == base.max_output_bytes
474 && cached.workgroup_override == base.workgroup_override
475 && cached.fixpoint_iterations == base.fixpoint_iterations
476 && cached.speculation == base.speculation
477 && cached.persistent_thread == base.persistent_thread
478 && cached.cooperative == base.cooperative
479}
480
481#[inline]
482pub fn count_set_domain_bits(
483 words: &[u32],
484 domain_bits: usize,
485 telemetry: &mut FrontierDensityTelemetry,
486) -> u64 {
487 let full_words = domain_bits / 32;
488 let tail_bits = domain_bits % 32;
489 let required_words = full_words + usize::from(tail_bits != 0);
490 if words.len() < required_words {
491 telemetry.record_truncated_frontier_sample();
492 }
493 let available_full_words = full_words.min(words.len());
494 let mut count = 0_u64;
495 for word in words.iter().take(available_full_words) {
496 count = bounded_telemetry_add(count, u64::from(word.count_ones()), telemetry);
497 }
498 if tail_bits != 0 && words.len() > full_words {
499 let mask = (1u32 << tail_bits) - 1;
500 count = bounded_telemetry_add(
501 count,
502 u64::from((words[full_words] & mask).count_ones()),
503 telemetry,
504 );
505 }
506 count
507}
508
509#[inline]
510pub fn count_changed_domain_bits(
511 current: &[u32],
512 next: &[u32],
513 domain_bits: usize,
514 telemetry: &mut FrontierDensityTelemetry,
515) -> u64 {
516 let full_words = domain_bits / 32;
517 let tail_bits = domain_bits % 32;
518 let required_words = full_words + usize::from(tail_bits != 0);
519 if current.len() < required_words || next.len() < required_words {
520 telemetry.record_truncated_frontier_sample();
521 }
522 let available_full_words = full_words.min(current.len()).min(next.len());
523 let mut count = 0_u64;
524 for index in 0..available_full_words {
525 count = bounded_telemetry_add(
526 count,
527 u64::from((current[index] ^ next[index]).count_ones()),
528 telemetry,
529 );
530 }
531 if tail_bits != 0 && current.len() > full_words && next.len() > full_words {
532 let mask = (1u32 << tail_bits) - 1;
533 count = bounded_telemetry_add(
534 count,
535 u64::from(((current[full_words] ^ next[full_words]) & mask).count_ones()),
536 telemetry,
537 );
538 }
539 count
540}
541
542#[cfg(feature = "gpu-telemetry")]
553pub fn count_set_domain_bits_gpu(
554 backend: &dyn vyre::VyreBackend,
555 config: &vyre::DispatchConfig,
556 words: &[u32],
557 domain_bits: usize,
558 telemetry: &mut FrontierDensityTelemetry,
559 scratch: &mut FixedPointScratch,
560) -> Result<u64, String> {
561 let full_words = domain_bits / 32;
562 let tail_bits = domain_bits % 32;
563 let required_words = full_words + usize::from(tail_bits != 0);
564 if words.len() < required_words {
565 telemetry.record_truncated_frontier_sample();
566 }
567
568 let available_words = required_words.min(words.len());
569 if available_words == 0 {
570 return Ok(0);
571 }
572
573 if available_words < 64 {
576 return Ok(count_set_domain_bits(words, domain_bits, telemetry));
577 }
578
579 let words_u32 = u32::try_from(available_words).map_err(|_| {
580 "telemetry word count exceeds u32 dispatch metadata. Fix: shard the frontier before GPU telemetry dispatch."
581 .to_string()
582 })?;
583
584 crate::dispatch_decode::try_write_zero_words(
587 &mut scratch.telemetry_xor_words,
588 available_words,
589 "telemetry set bits staging",
590 )?;
591 for (dst, src) in scratch
592 .telemetry_xor_words
593 .iter_mut()
594 .zip(words.iter().copied())
595 {
596 *dst = src;
597 }
598 if tail_bits != 0 && available_words > full_words {
599 let mask = (1u32 << tail_bits) - 1;
600 scratch.telemetry_xor_words[full_words] &= mask;
601 }
602 crate::dispatch_decode::try_pack_u32_into(
603 &scratch.telemetry_xor_words[..available_words],
604 &mut scratch.telemetry_popcount_input_bytes,
605 "telemetry set bits input",
606 )?;
607
608 let popcount_outputs = dispatch_popcount_then_reduce(backend, config, words_u32, scratch)?;
609
610 let sum_bytes =
611 crate::dispatch_decode::only_output(&popcount_outputs, "telemetry reduce_sum", "sum")?;
612 let sum_u32 = crate::dispatch_decode::unpack_exact_u32_scalar(sum_bytes, "telemetry sum")?;
613 let sum_u64 = u64::from(sum_u32);
614 telemetry.record_sample(sum_u64);
615 Ok(sum_u64)
616}
617
618#[cfg(feature = "gpu-telemetry")]
629pub fn count_changed_domain_bits_gpu(
630 backend: &dyn vyre::VyreBackend,
631 config: &vyre::DispatchConfig,
632 current: &[u32],
633 next: &[u32],
634 domain_bits: usize,
635 telemetry: &mut FrontierDensityTelemetry,
636 scratch: &mut FixedPointScratch,
637) -> Result<u64, String> {
638 let full_words = domain_bits / 32;
639 let tail_bits = domain_bits % 32;
640 let required_words = full_words + usize::from(tail_bits != 0);
641 if current.len() < required_words || next.len() < required_words {
642 telemetry.record_truncated_frontier_sample();
643 }
644
645 let available_words = required_words.min(current.len()).min(next.len());
646 if available_words == 0 {
647 return Ok(0);
648 }
649
650 if available_words < 64 {
652 return Ok(count_changed_domain_bits(
653 current,
654 next,
655 domain_bits,
656 telemetry,
657 ));
658 }
659
660 let words_u32 = u32::try_from(available_words).map_err(|_| {
661 "telemetry word count exceeds u32 dispatch metadata. Fix: shard the frontier before GPU telemetry dispatch."
662 .to_string()
663 })?;
664
665 crate::dispatch_decode::try_write_zero_words(
667 &mut scratch.telemetry_xor_words,
668 available_words,
669 "telemetry changed bits staging",
670 )?;
671 for index in 0..available_words {
672 scratch.telemetry_xor_words[index] = current[index] ^ next[index];
673 }
674 if tail_bits != 0 && available_words > full_words {
675 let mask = (1u32 << tail_bits) - 1;
676 scratch.telemetry_xor_words[full_words] &= mask;
677 }
678 crate::dispatch_decode::try_pack_u32_into(
679 &scratch.telemetry_xor_words[..available_words],
680 &mut scratch.telemetry_popcount_input_bytes,
681 "telemetry changed bits input",
682 )?;
683
684 let popcount_outputs = dispatch_popcount_then_reduce(backend, config, words_u32, scratch)?;
685
686 let sum_bytes =
687 crate::dispatch_decode::only_output(&popcount_outputs, "telemetry reduce_sum", "sum")?;
688 let sum_u32 = crate::dispatch_decode::unpack_exact_u32_scalar(sum_bytes, "telemetry sum")?;
689 let sum_u64 = u64::from(sum_u32);
690 telemetry.record_sample(sum_u64);
691 Ok(sum_u64)
692}
693
694#[cfg(feature = "gpu-telemetry")]
695fn dispatch_popcount_then_reduce(
696 backend: &dyn vyre::VyreBackend,
697 config: &vyre::DispatchConfig,
698 words: u32,
699 scratch: &mut FixedPointScratch,
700) -> Result<Vec<Vec<u8>>, String> {
701 let popcount_byte_len = words
702 .checked_mul(4)
703 .ok_or_else(|| {
704 "telemetry popcount byte length overflows u32. Fix: shard the frontier before GPU telemetry dispatch."
705 .to_string()
706 })? as usize;
707
708 let popcount_program = match &scratch.telemetry_popcount_program {
710 Some((cached_words, _)) if *cached_words == words => None,
711 _ => Some(vyre_primitives::bitset::popcount::bitset_popcount(
712 "input",
713 "count_words",
714 words,
715 )),
716 };
717 if let Some(program) = popcount_program {
718 scratch.telemetry_popcount_program = Some((words, program));
719 }
720 let popcount_program = scratch
721 .telemetry_popcount_program
722 .as_ref()
723 .map(|(_, p)| p)
724 .ok_or_else(|| {
725 "telemetry popcount program missing after cache update. Fix: report this as an internal logic error."
726 .to_string()
727 })?;
728
729 crate::dispatch_decode::try_write_zero_bytes(
731 &mut scratch.telemetry_popcount_output_bytes,
732 popcount_byte_len,
733 "telemetry popcount output zero staging",
734 )?;
735
736 let popcount_inputs: Vec<Vec<u8>> = vec![
737 scratch.telemetry_popcount_input_bytes[..popcount_byte_len].to_vec(),
738 scratch.telemetry_popcount_output_bytes[..popcount_byte_len].to_vec(),
739 ];
740 let popcount_outputs = backend
741 .dispatch(popcount_program, &popcount_inputs, config)
742 .map_err(|error| {
743 format!(
744 "telemetry bitset_popcount dispatch failed: {error}. Fix: verify backend supports the bitset::popcount op and input buffers are sized correctly."
745 )
746 })?;
747
748 let popcount_result = crate::dispatch_decode::only_output(
750 &popcount_outputs,
751 "telemetry bitset_popcount",
752 "count_words",
753 )?;
754
755 let reduce_program = match &scratch.telemetry_reduce_program {
757 Some((cached_count, _)) if *cached_count == words => None,
758 _ => Some(vyre_primitives::reduce::sum::reduce_sum(
759 "values", "out", words,
760 )),
761 };
762 if let Some(program) = reduce_program {
763 scratch.telemetry_reduce_program = Some((words, program));
764 }
765 let reduce_program = scratch
766 .telemetry_reduce_program
767 .as_ref()
768 .map(|(_, p)| p)
769 .ok_or_else(|| {
770 "telemetry reduce program missing after cache update. Fix: report this as an internal logic error."
771 .to_string()
772 })?;
773
774 crate::dispatch_decode::try_write_zero_bytes(
776 &mut scratch.telemetry_sum_bytes,
777 std::mem::size_of::<u32>(),
778 "telemetry reduce sum output zero staging",
779 )?;
780
781 let reduce_inputs: Vec<Vec<u8>> = vec![
782 popcount_result.to_vec(),
783 scratch.telemetry_sum_bytes[..std::mem::size_of::<u32>()].to_vec(),
784 ];
785 backend
786 .dispatch(reduce_program, &reduce_inputs, config)
787 .map_err(|error| {
788 format!(
789 "telemetry reduce_sum dispatch failed: {error}. Fix: verify backend supports the reduce::sum op and input buffers are sized correctly."
790 )
791 })
792}
793
794#[inline]
795fn domain_bits_to_usize(domain_bits: u64, telemetry: &mut FrontierDensityTelemetry) -> usize {
796 match usize::try_from(domain_bits) {
797 Ok(bits) => bits,
798 Err(_) => {
799 domain_bits_overflow(telemetry)
801 }
802 }
803}
804
805#[cold]
806fn domain_bits_overflow(telemetry: &mut FrontierDensityTelemetry) -> usize {
807 telemetry.record_domain_overflow();
808 usize::MAX
809}
810
811#[inline]
812fn bounded_telemetry_add(left: u64, right: u64, telemetry: &mut FrontierDensityTelemetry) -> u64 {
813 match left.checked_add(right) {
814 Some(sum) => sum,
815 None => {
816 telemetry_overflow(telemetry)
818 }
819 }
820}
821
822#[cold]
823fn telemetry_overflow(telemetry: &mut FrontierDensityTelemetry) -> u64 {
824 telemetry.record_arithmetic_overflow();
825 u64::MAX
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 #[test]
833 fn frontier_telemetry_counts_available_words_instead_of_panicking() {
834 let mut telemetry = FrontierDensityTelemetry::begin(96);
835 assert_eq!(count_set_domain_bits(&[u32::MAX], 96, &mut telemetry), 32);
836 assert_eq!(
837 count_changed_domain_bits(&[u32::MAX], &[0], 96, &mut telemetry),
838 32
839 );
840 assert_eq!(telemetry.truncated_frontier_samples, 2);
841 }
842
843 #[test]
844 fn fixed_point_scratch_buffers_source_has_no_release_path_panic_telemetry_or_resize_growth() {
845 let source = include_str!("buffers.rs");
846 let production = source
847 .split("#[cfg(test)]\nmod tests")
848 .next()
849 .expect("fixed-point scratch buffer production source must precede tests");
850 assert!(
851 !production.contains(concat!("panic", "!("))
852 && !production.contains(".unwrap_or_else(")
853 && !production.contains(".resize(")
854 && !production.contains("eprintln!"),
855 "Fix: fixed-point scratch buffers must reserve/extend explicitly and frontier telemetry must not abort or write stderr on malformed decode lengths."
856 );
857 assert!(
858 production.contains("bounded_telemetry_add")
859 && production.contains("record_truncated_frontier_sample")
860 && production.contains("extend(std::iter::repeat_n(0, node_count))"),
861 "Fix: fixed-point scratch buffers must keep non-aborting structured telemetry and resize-free reverse CSR staging."
862 );
863 }
864
865 #[cfg(feature = "gpu-telemetry")]
866 #[test]
867 fn gpu_telemetry_matches_cpu_reference() {
868 let backend = vyre_driver_reference::CpuRefBackend;
869 let config = vyre::DispatchConfig::default();
870
871 for &bits in &[96u32, 1_000, 10_000] {
872 let words = ((bits + 31) / 32) as usize;
873 let data: Vec<u32> = (0..words).map(|i| i as u32).collect();
874 let next: Vec<u32> = (0..words).map(|i| !(i as u32)).collect();
875 let mut scratch = FixedPointScratch::with_capacities(words, 2, 2)
876 .expect("test scratch must allocate");
877
878 let cpu_set = count_set_domain_bits(
879 &data,
880 bits as usize,
881 &mut FrontierDensityTelemetry::default(),
882 );
883 let gpu_set = count_set_domain_bits_gpu(
884 &backend,
885 &config,
886 &data,
887 bits as usize,
888 &mut FrontierDensityTelemetry::default(),
889 &mut scratch,
890 )
891 .expect("gpu set bits must succeed");
892 assert_eq!(cpu_set, gpu_set, "count_set_domain_bits mismatch at {bits}");
893
894 let cpu_chg = count_changed_domain_bits(
895 &data,
896 &next,
897 bits as usize,
898 &mut FrontierDensityTelemetry::default(),
899 );
900 let gpu_chg = count_changed_domain_bits_gpu(
901 &backend,
902 &config,
903 &data,
904 &next,
905 bits as usize,
906 &mut FrontierDensityTelemetry::default(),
907 &mut scratch,
908 )
909 .expect("gpu changed bits must succeed");
910 assert_eq!(
911 cpu_chg, gpu_chg,
912 "count_changed_domain_bits mismatch at {bits}"
913 );
914 }
915 }
916
917 #[cfg(feature = "gpu-telemetry")]
918 #[test]
919 fn gpu_telemetry_fallback_for_small_frontiers() {
920 let backend = vyre_driver_reference::CpuRefBackend;
921 let config = vyre::DispatchConfig::default();
922 let data: Vec<u32> = vec![0b1010_1010; 31]; let next: Vec<u32> = vec![0b0101_0101; 31];
924 let mut scratch =
925 FixedPointScratch::with_capacities(31, 2, 2).expect("test scratch must allocate");
926
927 let result = count_set_domain_bits_gpu(
928 &backend,
929 &config,
930 &data,
931 992,
932 &mut FrontierDensityTelemetry::default(),
933 &mut scratch,
934 )
935 .expect("small frontier gpu telemetry must succeed");
936 assert_eq!(result, 4 * 31);
937
938 let result = count_changed_domain_bits_gpu(
939 &backend,
940 &config,
941 &data,
942 &next,
943 992,
944 &mut FrontierDensityTelemetry::default(),
945 &mut scratch,
946 )
947 .expect("small frontier gpu changed telemetry must succeed");
948 assert_eq!(result, 8 * 31);
949 }
950}