1use super::dispatcher::TransitionWidth;
8use super::segmentation;
9use crate::buffer::GpuBufferHandle;
10use crate::staging_reserve::reserve_vec_exact_for_len;
11use std::sync::Arc;
12use vyre_runtime::PipelineError;
13
14pub const FILE_METADATA_WORDS: usize = 4;
16pub const WORK_TRIPLE_WORDS: usize = 3;
18pub const HIT_RECORD_WORDS: usize = 4;
20pub const QUEUE_STATE_WORDS: usize = 6;
22pub const MAX_BATCH_WORK_ITEMS: usize = u32::MAX as usize;
24pub const MAX_BATCH_HIT_CAPACITY: u32 = 16 * 1024 * 1024;
26
27pub(crate) fn persistent_storage_binding_usage() -> wgpu::BufferUsages {
28 wgpu::BufferUsages::STORAGE
29 | wgpu::BufferUsages::COPY_SRC
30 | wgpu::BufferUsages::COPY_DST
31 | wgpu::BufferUsages::INDIRECT
32}
33
34pub mod queue_state_word {
36 pub const HEAD: usize = 0;
38 pub const QUEUE_LEN: usize = 1;
40 pub const HIT_HEAD: usize = 2;
42 pub const HIT_CAPACITY: usize = 3;
44 pub const DONE_COUNT: usize = 4;
46 pub const RULE_COUNT: usize = 5;
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct BatchFile {
57 pub path_hash: u64,
59 pub decoded_layer_index: u32,
61 pub bytes: Vec<u8>,
63}
64
65impl BatchFile {
66 #[must_use]
68 pub fn new(path_hash: u64, decoded_layer_index: u32, bytes: Vec<u8>) -> Self {
69 Self {
70 path_hash,
71 decoded_layer_index,
72 bytes,
73 }
74 }
75}
76
77#[repr(C)]
79#[derive(Debug, Clone, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
80pub struct FileMetadata {
81 pub path_hash_lo: u32,
83 pub path_hash_hi: u32,
85 pub size_bytes: u32,
87 pub decoded_layer_index: u32,
89}
90
91impl FileMetadata {
92 fn from_file(file: &BatchFile) -> Result<Self, PipelineError> {
93 Ok(Self {
94 path_hash_lo: file.path_hash as u32,
95 path_hash_hi: (file.path_hash >> 32) as u32,
96 size_bytes: u32::try_from(file.bytes.len()).map_err(|_| PipelineError::QueueFull {
97 queue: "submission",
98 fix: "file size exceeds u32::MAX; split the batch into smaller files before megakernel batching",
99 })?,
100 decoded_layer_index: file.decoded_layer_index,
101 })
102 }
103}
104
105#[repr(C)]
107#[derive(Debug, Clone, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
108pub struct WorkTriple {
109 pub file_idx: u32,
111 pub rule_idx: u32,
113 pub layer_idx: u32,
115}
116
117impl WorkTriple {
118 #[must_use]
120 pub const fn new(file_idx: u32, rule_idx: u32, layer_idx: u32) -> Self {
121 Self {
122 file_idx,
123 rule_idx,
124 layer_idx,
125 }
126 }
127}
128
129#[repr(C)]
131#[derive(Debug, Clone, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
132pub struct HitRecord {
133 pub file_idx: u32,
135 pub rule_idx: u32,
137 pub layer_idx: u32,
139 pub match_offset: u32,
141}
142
143#[derive(Clone)]
145pub struct FileBatch {
146 device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
147 file_metadata: Vec<FileMetadata>,
148 file_offsets: Vec<u32>,
149 haystack_words: Vec<u32>,
150 rule_count: u32,
151 queue_len: u32,
152 hit_capacity: u32,
153 haystack: GpuBufferHandle,
154 offsets: GpuBufferHandle,
155 metadata: GpuBufferHandle,
156 segments: GpuBufferHandle,
160 queue_state: GpuBufferHandle,
161 hit_ring: GpuBufferHandle,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub struct FileBatchRefreshReport {
167 pub bytes_uploaded: u64,
169 pub resident_allocations: u32,
172 pub reused_buffers: u32,
174 pub refreshed_buffers: u32,
176}
177
178impl std::fmt::Debug for FileBatch {
179 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 formatter
181 .debug_struct("FileBatch")
182 .field("file_count", &self.file_count())
183 .field("queue_len", &self.queue_len())
184 .field("haystack_bytes", &self.haystack.byte_len())
185 .field("hit_capacity", &self.hit_capacity)
186 .finish()
187 }
188}
189
190impl FileBatch {
191 pub fn upload(
198 device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
199 files: &[BatchFile],
200 rule_count: u32,
201 hit_capacity: u32,
202 ) -> Result<Self, PipelineError> {
203 validate_hit_capacity(hit_capacity)?;
204 let (device, queue) = &*device_queue;
205 validate_batch_shape(files, rule_count)?;
206 let mut file_metadata = Vec::new();
207 let mut file_offsets = Vec::new();
208 let mut haystack_words = Vec::new();
209 build_metadata_into(files, &mut file_metadata)?;
210 build_offsets_into(files, &mut file_offsets)?;
211 flatten_haystack_words_into(files, &mut haystack_words)?;
212 let (seg_len, overlap) = default_segmentation(file_metadata.len());
213 let segment_words = build_segment_table(&file_metadata, seg_len, overlap);
214 let queue_len = segment_queue_len(&segment_words, rule_count)?;
215 let queue_state_words = initial_queue_state(queue_len, hit_capacity, rule_count);
216
217 let haystack = GpuBufferHandle::upload(
218 device,
219 queue,
220 bytemuck::cast_slice(&haystack_words),
221 persistent_storage_binding_usage(),
222 )?;
223 let offsets = GpuBufferHandle::upload(
224 device,
225 queue,
226 bytemuck::cast_slice(&file_offsets),
227 persistent_storage_binding_usage(),
228 )?;
229 let metadata = GpuBufferHandle::upload(
230 device,
231 queue,
232 bytemuck::cast_slice(&file_metadata),
233 persistent_storage_binding_usage(),
234 )?;
235 let segments = GpuBufferHandle::upload(
236 device,
237 queue,
238 bytemuck::cast_slice(&segment_words),
239 persistent_storage_binding_usage(),
240 )?;
241 let queue_state = GpuBufferHandle::upload(
242 device,
243 queue,
244 bytemuck::cast_slice(&queue_state_words),
245 persistent_storage_binding_usage(),
246 )?;
247 let hit_ring_bytes = hit_ring_byte_len(hit_capacity)?;
248 let hit_ring =
249 GpuBufferHandle::alloc(device, hit_ring_bytes, persistent_storage_binding_usage())?;
250
251 Ok(Self {
252 device_queue,
253 file_metadata,
254 file_offsets,
255 haystack_words,
256 rule_count,
257 queue_len,
258 hit_capacity,
259 haystack,
260 offsets,
261 metadata,
262 segments,
263 queue_state,
264 hit_ring,
265 })
266 }
267
268 pub fn refresh(
276 &mut self,
277 files: &[BatchFile],
278 rule_count: u32,
279 hit_capacity: u32,
280 ) -> Result<(), PipelineError> {
281 self.refresh_with_report(files, rule_count, hit_capacity)
282 .map(|_| ())
283 }
284
285 pub fn refresh_with_report(
292 &mut self,
293 files: &[BatchFile],
294 rule_count: u32,
295 hit_capacity: u32,
296 ) -> Result<FileBatchRefreshReport, PipelineError> {
297 validate_hit_capacity(hit_capacity)?;
298 validate_batch_shape(files, rule_count)?;
299
300 build_metadata_into(files, &mut self.file_metadata)?;
301 build_offsets_into(files, &mut self.file_offsets)?;
302 flatten_haystack_words_into(files, &mut self.haystack_words)?;
303
304 let (seg_len, overlap) = default_segmentation(self.file_metadata.len());
305 let segment_words = build_segment_table(&self.file_metadata, seg_len, overlap);
306 let queue_len = segment_queue_len(&segment_words, rule_count)?;
307 self.rule_count = rule_count;
308 self.queue_len = queue_len;
309 self.hit_capacity = hit_capacity;
310 let queue_state_words = initial_queue_state(queue_len, hit_capacity, rule_count);
311 let (device, queue) = &*self.device_queue;
312 let mut report = FileBatchRefreshReport::default();
313 accumulate_refresh(
314 &mut report,
315 &mut self.haystack,
316 device,
317 queue,
318 bytemuck::cast_slice(&self.haystack_words),
319 persistent_storage_binding_usage(),
320 )?;
321 accumulate_refresh(
322 &mut report,
323 &mut self.offsets,
324 device,
325 queue,
326 bytemuck::cast_slice(&self.file_offsets),
327 persistent_storage_binding_usage(),
328 )?;
329 accumulate_refresh(
330 &mut report,
331 &mut self.metadata,
332 device,
333 queue,
334 bytemuck::cast_slice(&self.file_metadata),
335 persistent_storage_binding_usage(),
336 )?;
337 accumulate_refresh(
338 &mut report,
339 &mut self.segments,
340 device,
341 queue,
342 bytemuck::cast_slice(&segment_words),
343 persistent_storage_binding_usage(),
344 )?;
345 accumulate_refresh(
346 &mut report,
347 &mut self.queue_state,
348 device,
349 queue,
350 bytemuck::cast_slice(&queue_state_words),
351 persistent_storage_binding_usage(),
352 )?;
353 let hit_ring_bytes = hit_ring_byte_len(hit_capacity)?;
354 if self.hit_ring.allocation_len() < padded_write_len_u64(hit_ring_bytes)? {
355 self.hit_ring =
356 GpuBufferHandle::alloc(device, hit_ring_bytes, persistent_storage_binding_usage())?;
357 report.resident_allocations += 1;
358 report.refreshed_buffers += 1;
359 } else {
360 report.reused_buffers += 1;
361 }
362 Ok(report)
363 }
364
365 pub fn reset_queue_state(&self) -> Result<(), PipelineError> {
371 let (_, queue) = &*self.device_queue;
372 let words = initial_queue_state(self.queue_len, self.hit_capacity, self.rule_count);
373 queue.write_buffer(self.queue_state.buffer(), 0, bytemuck::cast_slice(&words));
374 Ok(())
375 }
376
377 pub fn set_segmentation(&mut self, seg_len: u32, overlap: u32) -> Result<(), PipelineError> {
397 if seg_len == 0 {
398 return Err(PipelineError::QueueFull {
399 queue: "submission",
400 fix: "segment owned-width (seg_len) must be > 0; pass u32::MAX for one segment per file",
401 });
402 }
403 let segment_words = build_segment_table(&self.file_metadata, seg_len, overlap);
404 let queue_len = segment_queue_len(&segment_words, self.rule_count)?;
405 self.queue_len = queue_len;
406 let queue_state_words = initial_queue_state(queue_len, self.hit_capacity, self.rule_count);
407 let (device, queue) = &*self.device_queue;
408 let mut report = FileBatchRefreshReport::default();
409 accumulate_refresh(
410 &mut report,
411 &mut self.segments,
412 device,
413 queue,
414 bytemuck::cast_slice(&segment_words),
415 persistent_storage_binding_usage(),
416 )?;
417 queue.write_buffer(
418 self.queue_state.buffer(),
419 0,
420 bytemuck::cast_slice(&queue_state_words),
421 );
422 Ok(())
423 }
424
425 #[must_use]
427 pub fn file_count(&self) -> usize {
428 self.file_metadata.len()
429 }
430
431 #[must_use]
433 pub const fn queue_len(&self) -> u32 {
434 self.queue_len
435 }
436
437 #[must_use]
439 pub const fn hit_capacity(&self) -> u32 {
440 self.hit_capacity
441 }
442
443 #[must_use]
445 pub fn device_queue(&self) -> Arc<(wgpu::Device, wgpu::Queue)> {
446 Arc::clone(&self.device_queue)
447 }
448
449 #[must_use]
451 pub const fn haystack(&self) -> &GpuBufferHandle {
452 &self.haystack
453 }
454
455 #[must_use]
457 pub const fn offsets(&self) -> &GpuBufferHandle {
458 &self.offsets
459 }
460
461 #[must_use]
463 pub const fn metadata(&self) -> &GpuBufferHandle {
464 &self.metadata
465 }
466
467 #[must_use]
471 pub const fn segments(&self) -> &GpuBufferHandle {
472 &self.segments
473 }
474
475 #[must_use]
477 pub const fn queue_state(&self) -> &GpuBufferHandle {
478 &self.queue_state
479 }
480
481 #[must_use]
483 pub const fn hit_ring(&self) -> &GpuBufferHandle {
484 &self.hit_ring
485 }
486
487 #[must_use]
489 pub fn host_metadata(&self) -> &[FileMetadata] {
490 &self.file_metadata
491 }
492
493 #[must_use]
495 pub fn host_offsets(&self) -> &[u32] {
496 &self.file_offsets
497 }
498
499 #[must_use]
504 pub fn host_work_items(&self) -> &[WorkTriple] {
505 &[]
506 }
507}
508
509#[derive(Clone)]
531pub struct CombinedBatch {
532 device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
533 file_metadata: Vec<FileMetadata>,
534 queue_len: u32,
535 hit_capacity: u32,
536 max_pattern_len: u32,
537 num_classes: u32,
541 transition_width: TransitionWidth,
544 haystack: GpuBufferHandle,
545 offsets: GpuBufferHandle,
546 metadata: GpuBufferHandle,
547 transitions: GpuBufferHandle,
550 output_offsets: GpuBufferHandle,
551 output_records: GpuBufferHandle,
552 class_maps: GpuBufferHandle,
554 segments: GpuBufferHandle,
555 queue_state: GpuBufferHandle,
556 hit_ring: GpuBufferHandle,
557}
558
559impl std::fmt::Debug for CombinedBatch {
560 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561 formatter
562 .debug_struct("CombinedBatch")
563 .field("file_count", &self.file_metadata.len())
564 .field("queue_len", &self.queue_len)
565 .field("hit_capacity", &self.hit_capacity)
566 .field("max_pattern_len", &self.max_pattern_len)
567 .finish()
568 }
569}
570
571impl CombinedBatch {
572 #[allow(clippy::too_many_arguments)]
607 pub fn upload(
608 device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
609 files: &[BatchFile],
610 transitions: &[u32],
611 output_offsets: &[u32],
612 output_records: &[u32],
613 state_count: u32,
614 max_pattern_len: u32,
615 seg_len: u32,
616 hit_capacity: u32,
617 ) -> Result<Self, PipelineError> {
618 Self::upload_with_transition_width(
619 device_queue,
620 files,
621 transitions,
622 output_offsets,
623 output_records,
624 state_count,
625 max_pattern_len,
626 seg_len,
627 hit_capacity,
628 TransitionWidth::Bits32,
629 )
630 }
631
632 #[allow(clippy::too_many_arguments)]
642 pub fn upload_with_transition_width(
643 device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
644 files: &[BatchFile],
645 transitions: &[u32],
646 output_offsets: &[u32],
647 output_records: &[u32],
648 state_count: u32,
649 max_pattern_len: u32,
650 seg_len: u32,
651 hit_capacity: u32,
652 transition_width: TransitionWidth,
653 ) -> Result<Self, PipelineError> {
654 validate_hit_capacity(hit_capacity)?;
655 validate_combined_automaton(transitions, output_offsets, output_records, state_count)?;
656 if seg_len == 0 {
657 return Err(PipelineError::QueueFull {
658 queue: "submission",
659 fix: "segment owned-width (seg_len) must be > 0; pass u32::MAX for one segment per file",
660 });
661 }
662 let (device, queue) = &*device_queue;
663 let mut file_metadata = Vec::new();
664 let mut file_offsets = Vec::new();
665 let mut haystack_words = Vec::new();
666 build_metadata_into(files, &mut file_metadata)?;
667 build_offsets_into(files, &mut file_offsets)?;
668 flatten_haystack_words_into(files, &mut haystack_words)?;
669 let segment_words = build_segment_table(&file_metadata, seg_len, max_pattern_len);
670 let queue_len = segment_queue_len(&segment_words, 1)?;
672 let queue_state_words = initial_queue_state(queue_len, hit_capacity, 1);
673
674 let mut class_map = Vec::new();
680 let num_classes = vyre_runtime::megakernel::rule_catalog::build_byte_class_map_for_table(
681 transitions,
682 state_count as usize,
683 &mut class_map,
684 );
685 let mut compressed_transitions = Vec::new();
686 vyre_runtime::megakernel::rule_catalog::compress_dense_transitions_into(
687 transitions,
688 state_count as usize,
689 &class_map,
690 num_classes,
691 &mut compressed_transitions,
692 );
693 let transition_table = match transition_width {
697 TransitionWidth::Bits32 => compressed_transitions,
698 TransitionWidth::Bits16 => {
699 let mut packed = Vec::new();
700 vyre_runtime::megakernel::rule_catalog::try_pack_u16_transitions_into(
701 &compressed_transitions,
702 &mut packed,
703 )?;
704 packed
705 }
706 };
707
708 let usage = persistent_storage_binding_usage();
709 let haystack =
710 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(&haystack_words), usage)?;
711 let offsets =
712 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(&file_offsets), usage)?;
713 let metadata =
714 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(&file_metadata), usage)?;
715 let transitions = GpuBufferHandle::upload(
716 device,
717 queue,
718 bytemuck::cast_slice(&transition_table),
719 usage,
720 )?;
721 let output_offsets =
722 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(output_offsets), usage)?;
723 let output_records =
724 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(output_records), usage)?;
725 let class_maps =
726 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(&class_map), usage)?;
727 let segments =
728 GpuBufferHandle::upload(device, queue, bytemuck::cast_slice(&segment_words), usage)?;
729 let queue_state = GpuBufferHandle::upload(
730 device,
731 queue,
732 bytemuck::cast_slice(&queue_state_words),
733 usage,
734 )?;
735 let hit_ring_bytes = hit_ring_byte_len(hit_capacity)?;
736 let hit_ring = GpuBufferHandle::alloc(device, hit_ring_bytes, usage)?;
737
738 Ok(Self {
739 device_queue,
740 file_metadata,
741 queue_len,
742 hit_capacity,
743 max_pattern_len,
744 num_classes,
745 transition_width,
746 haystack,
747 offsets,
748 metadata,
749 transitions,
750 output_offsets,
751 output_records,
752 class_maps,
753 segments,
754 queue_state,
755 hit_ring,
756 })
757 }
758
759 pub fn set_segmentation(&mut self, seg_len: u32) -> Result<(), PipelineError> {
768 if seg_len == 0 {
769 return Err(PipelineError::QueueFull {
770 queue: "submission",
771 fix: "segment owned-width (seg_len) must be > 0; pass u32::MAX for one segment per file",
772 });
773 }
774 let segment_words = build_segment_table(&self.file_metadata, seg_len, self.max_pattern_len);
775 let queue_len = segment_queue_len(&segment_words, 1)?;
776 self.queue_len = queue_len;
777 let queue_state_words = initial_queue_state(queue_len, self.hit_capacity, 1);
778 let (device, queue) = &*self.device_queue;
779 let mut report = FileBatchRefreshReport::default();
780 accumulate_refresh(
781 &mut report,
782 &mut self.segments,
783 device,
784 queue,
785 bytemuck::cast_slice(&segment_words),
786 persistent_storage_binding_usage(),
787 )?;
788 queue.write_buffer(
789 self.queue_state.buffer(),
790 0,
791 bytemuck::cast_slice(&queue_state_words),
792 );
793 Ok(())
794 }
795
796 pub fn reset_queue_state(&self) {
798 let (_, queue) = &*self.device_queue;
799 let words = initial_queue_state(self.queue_len, self.hit_capacity, 1);
800 queue.write_buffer(self.queue_state.buffer(), 0, bytemuck::cast_slice(&words));
801 }
802
803 #[must_use]
805 pub const fn queue_len(&self) -> u32 {
806 self.queue_len
807 }
808
809 #[must_use]
811 pub const fn hit_capacity(&self) -> u32 {
812 self.hit_capacity
813 }
814
815 #[must_use]
817 pub fn device_queue(&self) -> Arc<(wgpu::Device, wgpu::Queue)> {
818 Arc::clone(&self.device_queue)
819 }
820
821 #[must_use]
824 pub const fn num_classes(&self) -> u32 {
825 self.num_classes
826 }
827
828 #[must_use]
831 pub const fn transition_width(&self) -> TransitionWidth {
832 self.transition_width
833 }
834
835 #[must_use]
840 pub fn input_buffers(&self) -> [&GpuBufferHandle; 8] {
841 [
842 &self.offsets,
843 &self.metadata,
844 &self.haystack,
845 &self.transitions,
846 &self.output_offsets,
847 &self.output_records,
848 &self.class_maps,
849 &self.segments,
850 ]
851 }
852
853 #[must_use]
856 pub const fn output_buffers(&self) -> [&GpuBufferHandle; 2] {
857 [&self.queue_state, &self.hit_ring]
858 }
859
860 #[must_use]
862 pub const fn queue_state(&self) -> &GpuBufferHandle {
863 &self.queue_state
864 }
865
866 #[must_use]
868 pub const fn hit_ring(&self) -> &GpuBufferHandle {
869 &self.hit_ring
870 }
871}
872
873fn validate_combined_automaton(
882 transitions: &[u32],
883 output_offsets: &[u32],
884 output_records: &[u32],
885 state_count: u32,
886) -> Result<(), PipelineError> {
887 if state_count == 0 {
888 return Err(PipelineError::Backend(
889 "combined automaton has state_count 0. Fix: compile at least one pattern before upload."
890 .to_string(),
891 ));
892 }
893 let state_count_usize = state_count as usize;
894 let expected_transitions = state_count_usize.checked_mul(256).ok_or_else(|| {
895 PipelineError::Backend(
896 "combined transition table size (state_count * 256) overflowed usize. Fix: shard the pattern catalog."
897 .to_string(),
898 )
899 })?;
900 if transitions.len() != expected_transitions {
901 return Err(PipelineError::Backend(format!(
902 "combined transition table has {} words, expected state_count*256 = {expected_transitions}. Fix: pass the dense classic-AC transition table.",
903 transitions.len()
904 )));
905 }
906 if output_offsets.len() != state_count_usize + 1 {
907 return Err(PipelineError::Backend(format!(
908 "combined output_offsets has {} entries, expected state_count+1 = {}. Fix: pass the CSR row-pointer array.",
909 output_offsets.len(),
910 state_count_usize + 1
911 )));
912 }
913 let last_offset = *output_offsets.last().expect("non-empty: checked above") as usize;
914 if last_offset != output_records.len() {
915 return Err(PipelineError::Backend(format!(
916 "combined output_offsets terminator {last_offset} != output_records length {}. Fix: keep the CSR payload and row pointers consistent.",
917 output_records.len()
918 )));
919 }
920 Ok(())
921}
922
923fn accumulate_refresh(
924 report: &mut FileBatchRefreshReport,
925 handle: &mut GpuBufferHandle,
926 device: &wgpu::Device,
927 queue: &wgpu::Queue,
928 bytes: &[u8],
929 usage: wgpu::BufferUsages,
930) -> Result<(), PipelineError> {
931 let refreshed = upload_or_refresh(handle, device, queue, bytes, usage)?;
932 let padded_len = padded_write_len(bytes.len())?;
933 report.bytes_uploaded = report.bytes_uploaded.checked_add(padded_len).ok_or_else(|| {
934 PipelineError::Backend(
935 "batch refresh uploaded-byte accounting overflowed u64. Fix: shard the file batch before GPU upload."
936 .to_string(),
937 )
938 })?;
939 if refreshed {
940 report.resident_allocations += 1;
941 report.refreshed_buffers += 1;
942 } else {
943 report.reused_buffers += 1;
944 }
945 Ok(())
946}
947
948fn upload_or_refresh(
949 handle: &mut GpuBufferHandle,
950 device: &wgpu::Device,
951 queue: &wgpu::Queue,
952 bytes: &[u8],
953 usage: wgpu::BufferUsages,
954) -> Result<bool, PipelineError> {
955 let required_len = padded_write_len(bytes.len())?;
956 if handle.allocation_len() >= required_len
957 && handle
958 .usage()
959 .contains(usage | wgpu::BufferUsages::COPY_DST)
960 {
961 write_padded_prefix(queue, handle.buffer(), bytes)?;
962 Ok(false)
963 } else {
964 *handle = GpuBufferHandle::upload(device, queue, bytes, usage)?;
965 Ok(true)
966 }
967}
968
969fn padded_write_len(len: usize) -> Result<u64, PipelineError> {
970 if len == 0 {
971 return Ok(0);
972 }
973 let normalized = len.max(4);
974 let remainder = normalized % 4;
975 let padded = if remainder == 0 {
976 normalized
977 } else {
978 normalized.checked_add(4 - remainder).ok_or_else(|| {
979 PipelineError::Backend(
980 "refreshed batch buffer length overflows usize while padding to WGPU alignment. Fix: split the batch before upload.".to_string(),
981 )
982 })?
983 };
984 u64::try_from(padded).map_err(|source| {
985 PipelineError::Backend(format!(
986 "refreshed batch buffer length cannot fit u64: {source}. Fix: split the batch before upload."
987 ))
988 })
989}
990
991fn padded_write_len_u64(len: u64) -> Result<u64, PipelineError> {
992 if len == 0 {
993 return Ok(0);
994 }
995 let normalized = len.max(4);
996 let remainder = normalized % 4;
997 if remainder == 0 {
998 return Ok(normalized);
999 }
1000 normalized.checked_add(4 - remainder).ok_or_else(|| {
1001 PipelineError::Backend(
1002 "refreshed batch buffer length overflows u64 while padding to WGPU alignment. Fix: split the batch before upload.".to_string(),
1003 )
1004 })
1005}
1006
1007fn usize_to_u64(value: usize, label: &str) -> Result<u64, PipelineError> {
1008 u64::try_from(value).map_err(|source| {
1009 PipelineError::Backend(format!(
1010 "{label} cannot fit u64: {source}. Fix: split the batch before upload."
1011 ))
1012 })
1013}
1014
1015fn hit_ring_byte_len(hit_capacity: u32) -> Result<u64, PipelineError> {
1016 u64::from(hit_capacity)
1017 .checked_mul(usize_to_u64(HIT_RECORD_WORDS, "hit record word count")?)
1018 .and_then(|words| words.checked_mul(4))
1019 .ok_or_else(|| {
1020 PipelineError::Backend(
1021 "hit-ring allocation byte count overflowed u64. Fix: reduce hit_capacity or shard the batch."
1022 .to_string(),
1023 )
1024 })
1025}
1026
1027fn write_padded_prefix(
1028 queue: &wgpu::Queue,
1029 buffer: &wgpu::Buffer,
1030 bytes: &[u8],
1031) -> Result<(), PipelineError> {
1032 crate::padded_upload::write_padded_prefix(
1033 queue,
1034 buffer,
1035 bytes,
1036 "padded batch write tail offset",
1037 )
1038 .map_err(|source| PipelineError::Backend(source.to_string()))?;
1039 Ok(())
1040}
1041
1042fn validate_batch_shape(files: &[BatchFile], rule_count: u32) -> Result<(), PipelineError> {
1043 if u32::try_from(files.len()).is_err() {
1044 return Err(PipelineError::QueueFull {
1045 queue: "submission",
1046 fix: "file count exceeds u32::MAX; split the batch into smaller file shards",
1047 });
1048 }
1049 let mut total_bytes = 0u64;
1050 for file in files {
1051 if u32::try_from(file.bytes.len()).is_err() {
1052 return Err(PipelineError::QueueFull {
1053 queue: "submission",
1054 fix: "file size exceeds u32::MAX; split the batch into smaller files before megakernel batching",
1055 });
1056 }
1057 let file_len = usize_to_u64(file.bytes.len(), "batched file byte length")?;
1058 total_bytes = total_bytes
1059 .checked_add(file_len)
1060 .ok_or(PipelineError::QueueFull {
1061 queue: "submission",
1062 fix: "batched haystack length overflowed u64; split the batch into smaller shards",
1063 })?;
1064 }
1065 if total_bytes > u64::from(u32::MAX) {
1066 return Err(PipelineError::QueueFull {
1067 queue: "submission",
1068 fix: "batched haystack exceeds u32::MAX bytes; split the batch into smaller shards",
1069 });
1070 }
1071 validate_work_queue_shape(files.len(), rule_count)
1072}
1073
1074fn dense_queue_len(file_count: usize, rule_count: u32) -> Result<u32, PipelineError> {
1075 let rule_count = usize::try_from(rule_count).map_err(|_| PipelineError::QueueFull {
1076 queue: "submission",
1077 fix: "rule count cannot fit host usize; shard the rule set before dense queue planning",
1078 })?;
1079 let capacity = file_count
1080 .checked_mul(rule_count)
1081 .ok_or(PipelineError::QueueFull {
1082 queue: "submission",
1083 fix: "file_count * rule_count overflowed usize; split the batch or reduce the rule fanout",
1084 })?;
1085 if u32::try_from(capacity).is_err() {
1086 return Err(PipelineError::QueueFull {
1087 queue: "submission",
1088 fix: "work queue length exceeds u32::MAX; split the batch or reduce the rule fanout before allocation",
1089 });
1090 }
1091 if capacity > MAX_BATCH_WORK_ITEMS {
1092 return Err(PipelineError::QueueFull {
1093 queue: "submission",
1094 fix: "work queue length exceeds the device claim protocol; split the file batch or reduce the rule fanout",
1095 });
1096 }
1097 u32::try_from(capacity).map_err(|_| PipelineError::QueueFull {
1098 queue: "submission",
1099 fix: "work queue length exceeds u32::MAX; split the batch or reduce the rule fanout before allocation",
1100 })
1101}
1102
1103fn validate_work_queue_shape(file_count: usize, rule_count: u32) -> Result<(), PipelineError> {
1104 dense_queue_len(file_count, rule_count).map(|_| ())
1105}
1106
1107fn build_metadata_into(
1108 files: &[BatchFile],
1109 metadata: &mut Vec<FileMetadata>,
1110) -> Result<(), PipelineError> {
1111 reserve_batch_vec_len(metadata, files.len(), "file metadata records")?;
1112 if metadata.len() == files.len() {
1113 for (slot, file) in metadata.iter_mut().zip(files) {
1114 *slot = FileMetadata::from_file(file)?;
1115 }
1116 } else {
1117 metadata.clear();
1118 for file in files {
1119 metadata.push(FileMetadata::from_file(file)?);
1120 }
1121 }
1122 Ok(())
1123}
1124
1125#[cfg(test)]
1126fn build_offsets(files: &[BatchFile]) -> Result<Vec<u32>, PipelineError> {
1127 let mut offsets = Vec::new();
1128 build_offsets_into(files, &mut offsets)?;
1129 Ok(offsets)
1130}
1131
1132fn build_offsets_into(files: &[BatchFile], offsets: &mut Vec<u32>) -> Result<(), PipelineError> {
1133 let required = files.len().checked_add(1).ok_or(PipelineError::QueueFull {
1134 queue: "submission",
1135 fix: "file count overflows offset table length; split the batch before upload",
1136 })?;
1137 reserve_batch_vec_len(offsets, required, "file offset table")?;
1138 let stable_len = offsets.len() == required;
1139 if stable_len {
1140 offsets[0] = 0;
1141 } else {
1142 offsets.clear();
1143 offsets.push(0);
1144 }
1145 let mut total = 0u64;
1146 for (index, file) in files.iter().enumerate() {
1147 let file_len = usize_to_u64(file.bytes.len(), "batched file byte length")?;
1148 total = total
1149 .checked_add(file_len)
1150 .ok_or(PipelineError::QueueFull {
1151 queue: "submission",
1152 fix: "batched haystack length overflowed u64; split the batch into smaller shards",
1153 })?;
1154 let offset = u32::try_from(total).map_err(|_| PipelineError::QueueFull {
1155 queue: "submission",
1156 fix: "batched haystack exceeds u32::MAX bytes; split the batch into smaller shards",
1157 })?;
1158 if stable_len {
1159 offsets[index + 1] = offset;
1160 } else {
1161 offsets.push(offset);
1162 }
1163 }
1164 Ok(())
1165}
1166
1167#[cfg(test)]
1168fn flatten_haystack_words(files: &[BatchFile]) -> Result<Vec<u32>, PipelineError> {
1169 let mut words = Vec::new();
1170 flatten_haystack_words_into(files, &mut words)?;
1171 Ok(words)
1172}
1173
1174fn flatten_haystack_words_into(
1175 files: &[BatchFile],
1176 words: &mut Vec<u32>,
1177) -> Result<(), PipelineError> {
1178 let total = files.iter().try_fold(0usize, |acc, file| {
1179 acc.checked_add(file.bytes.len())
1180 .ok_or(PipelineError::QueueFull {
1181 queue: "submission",
1182 fix:
1183 "batched haystack length overflowed usize; split the batch into smaller shards",
1184 })
1185 })?;
1186 let target_words = total.div_ceil(4).max(1);
1187 reserve_batch_vec_len(words, target_words, "packed haystack words")?;
1188 let stable_len = words.len() == target_words;
1189 if stable_len {
1190 words.fill(0);
1191 } else {
1192 words.clear();
1193 }
1194 let mut word = 0u32;
1195 let mut shift = 0u32;
1196 let mut word_index = 0usize;
1197 for file in files {
1198 pack_bytes_into_words(
1199 &file.bytes,
1200 words,
1201 stable_len,
1202 &mut word_index,
1203 &mut word,
1204 &mut shift,
1205 );
1206 }
1207 if shift != 0 {
1208 write_packed_word(words, stable_len, &mut word_index, word);
1209 }
1210 if word_index == 0 {
1211 write_packed_word(words, stable_len, &mut word_index, 0);
1212 }
1213 Ok(())
1214}
1215
1216fn pack_bytes_into_words(
1217 bytes: &[u8],
1218 words: &mut Vec<u32>,
1219 stable_len: bool,
1220 word_index: &mut usize,
1221 word: &mut u32,
1222 shift: &mut u32,
1223) {
1224 let mut cursor = bytes;
1225 if *shift != 0 {
1226 while *shift != 0 && !cursor.is_empty() {
1227 *word |= u32::from(cursor[0]) << *shift;
1228 *shift += 8;
1229 cursor = &cursor[1..];
1230 if *shift == 32 {
1231 write_packed_word(words, stable_len, word_index, *word);
1232 *word = 0;
1233 *shift = 0;
1234 }
1235 }
1236 }
1237
1238 let mut chunks = cursor.chunks_exact(4);
1239 for chunk in &mut chunks {
1240 write_packed_word(
1241 words,
1242 stable_len,
1243 word_index,
1244 u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
1245 );
1246 }
1247 for byte in chunks.remainder() {
1248 *word |= u32::from(*byte) << *shift;
1249 *shift += 8;
1250 }
1251}
1252
1253fn write_packed_word(words: &mut Vec<u32>, stable_len: bool, word_index: &mut usize, word: u32) {
1254 if stable_len {
1255 words[*word_index] = word;
1256 } else {
1257 words.push(word);
1258 }
1259 *word_index += 1;
1260}
1261
1262#[cfg(test)]
1263fn derive_work_triple(
1264 file_metadata: &[FileMetadata],
1265 rule_count: u32,
1266 claim: u32,
1267) -> Result<WorkTriple, PipelineError> {
1268 let queue_len = dense_queue_len(file_metadata.len(), rule_count)?;
1269 if claim >= queue_len || rule_count == 0 {
1270 return Err(PipelineError::QueueFull {
1271 queue: "submission",
1272 fix: "claim exceeds the dense device-derived queue; keep queue length and rule count synchronized",
1273 });
1274 }
1275 let file_idx = claim / rule_count;
1276 let rule_idx = claim % rule_count;
1277 let file_idx_usize = usize::try_from(file_idx).map_err(|_| PipelineError::QueueFull {
1278 queue: "submission",
1279 fix: "derived file index cannot fit host usize; shard the batch before decoding work triples",
1280 })?;
1281 let metadata = file_metadata
1282 .get(file_idx_usize)
1283 .ok_or(PipelineError::QueueFull {
1284 queue: "submission",
1285 fix: "derived file index exceeds metadata length; keep queue length and metadata synchronized",
1286 })?;
1287 Ok(WorkTriple::new(
1288 file_idx,
1289 rule_idx,
1290 metadata.decoded_layer_index,
1291 ))
1292}
1293
1294fn validate_hit_capacity(hit_capacity: u32) -> Result<(), PipelineError> {
1295 if hit_capacity > MAX_BATCH_HIT_CAPACITY {
1296 return Err(PipelineError::QueueFull {
1297 queue: "submission",
1298 fix: "hit capacity exceeds the per-batch sparse ring cap; shard the batch or drain hits across multiple launches",
1299 });
1300 }
1301 Ok(())
1302}
1303
1304fn initial_queue_state(
1305 queue_len: u32,
1306 hit_capacity: u32,
1307 rule_count: u32,
1308) -> [u32; QUEUE_STATE_WORDS] {
1309 [0, queue_len, 0, hit_capacity, 0, rule_count]
1312}
1313
1314fn default_segmentation(_file_count: usize) -> (u32, u32) {
1322 (u32::MAX, 0)
1323}
1324
1325fn build_segment_table(file_metadata: &[FileMetadata], seg_len: u32, overlap: u32) -> Vec<u32> {
1329 let file_lens: Vec<u32> = file_metadata.iter().map(|meta| meta.size_bytes).collect();
1330 segmentation::segment_table(&file_lens, seg_len, overlap)
1331}
1332
1333fn segment_queue_len(segment_words: &[u32], rule_count: u32) -> Result<u32, PipelineError> {
1338 let segment_count = segment_words.len() / segmentation::SEGMENT_WORDS;
1339 dense_queue_len(segment_count, rule_count)
1340}
1341
1342fn reserve_batch_vec_len<T>(
1343 vec: &mut Vec<T>,
1344 target_len: usize,
1345 label: &'static str,
1346) -> Result<(), PipelineError> {
1347 reserve_vec_exact_for_len(
1348 vec,
1349 target_len,
1350 "megakernel FileBatch staging",
1351 label,
1352 "split the file batch or reduce rule fanout before upload",
1353 )
1354 .map_err(|error| PipelineError::Backend(error.to_string()))
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359 use super::*;
1360
1361 #[test]
1362 fn offsets_are_prefix_sums() {
1363 let files = vec![
1364 BatchFile::new(1, 0, b"ab".to_vec()),
1365 BatchFile::new(2, 3, b"cdef".to_vec()),
1366 ];
1367 assert_eq!(build_offsets(&files).unwrap(), vec![0, 2, 6]);
1368 }
1369
1370 #[test]
1371 fn haystack_flattening_preserves_cross_file_byte_order() {
1372 let files = vec![
1373 BatchFile::new(1, 0, vec![1, 2, 3]),
1374 BatchFile::new(2, 0, vec![4, 5, 6, 7, 8]),
1375 BatchFile::new(3, 0, vec![9]),
1376 ];
1377
1378 let words = flatten_haystack_words(&files).unwrap();
1379
1380 assert_eq!(
1381 words,
1382 vec![
1383 u32::from_le_bytes([1, 2, 3, 4]),
1384 u32::from_le_bytes([5, 6, 7, 8]),
1385 u32::from_le_bytes([9, 0, 0, 0]),
1386 ]
1387 );
1388 }
1389
1390 #[test]
1391 fn device_schedule_derives_files_x_rules_without_materialized_queue() {
1392 let metadata = vec![
1393 FileMetadata {
1394 path_hash_lo: 1,
1395 path_hash_hi: 0,
1396 size_bytes: 2,
1397 decoded_layer_index: 4,
1398 },
1399 FileMetadata {
1400 path_hash_lo: 2,
1401 path_hash_hi: 0,
1402 size_bytes: 3,
1403 decoded_layer_index: 9,
1404 },
1405 ];
1406 let queue = (0..dense_queue_len(metadata.len(), 2).unwrap())
1407 .map(|claim| derive_work_triple(&metadata, 2, claim).unwrap())
1408 .collect::<Vec<_>>();
1409 assert_eq!(
1410 queue,
1411 vec![
1412 WorkTriple::new(0, 0, 4),
1413 WorkTriple::new(0, 1, 4),
1414 WorkTriple::new(1, 0, 9),
1415 WorkTriple::new(1, 1, 9),
1416 ]
1417 );
1418 }
1419
1420 #[test]
1421 fn work_queue_rejects_u32_overflow_before_allocation() {
1422 let metadata = vec![
1423 FileMetadata {
1424 path_hash_lo: 1,
1425 path_hash_hi: 0,
1426 size_bytes: 1,
1427 decoded_layer_index: 0,
1428 },
1429 FileMetadata {
1430 path_hash_lo: 2,
1431 path_hash_hi: 0,
1432 size_bytes: 1,
1433 decoded_layer_index: 0,
1434 },
1435 ];
1436 let err = dense_queue_len(metadata.len(), u32::MAX).expect_err(
1437 "Fix: queue fanout exceeding u32 protocol must be rejected before allocation",
1438 );
1439 assert!(matches!(err, PipelineError::QueueFull { .. }));
1440 }
1441
1442 #[test]
1443 fn device_schedule_accepts_batches_above_legacy_host_queue_cap_without_allocating() {
1444 let metadata = vec![
1445 FileMetadata {
1446 path_hash_lo: 1,
1447 path_hash_hi: 0,
1448 size_bytes: 1,
1449 decoded_layer_index: 0,
1450 };
1451 2
1452 ];
1453 const LEGACY_HOST_WORK_QUEUE_CAP: usize = 16 * 1024 * 1024;
1454 let rule_count = u32::try_from(LEGACY_HOST_WORK_QUEUE_CAP / metadata.len() + 1).unwrap();
1455 let queue_len = dense_queue_len(metadata.len(), rule_count)
1456 .expect("Fix: device-derived scheduling must not retain the old host allocation cap");
1457 assert!(
1458 queue_len as usize > LEGACY_HOST_WORK_QUEUE_CAP,
1459 "dense scheduling must scale past the removed host Vec<WorkTriple> limit"
1460 );
1461 }
1462
1463 #[test]
1464 fn hit_capacity_rejects_allocation_cap() {
1465 let err = validate_hit_capacity(MAX_BATCH_HIT_CAPACITY + 1)
1466 .expect_err("oversized hit ring must reject before GPU allocation");
1467 assert!(matches!(err, PipelineError::QueueFull { .. }));
1468 }
1469
1470 #[test]
1471 fn refresh_reuses_host_and_gpu_storage_when_shape_fits() {
1472 let backend = crate::WgpuBackend::new().expect(
1473 "Fix: live WGPU backend required for FileBatch refresh reuse contract; missing GPU is a configuration bug.",
1474 );
1475 let first = vec![
1476 BatchFile::new(1, 0, b"abcdefgh".to_vec()),
1477 BatchFile::new(2, 1, b"ijklmnop".to_vec()),
1478 ];
1479 let second = vec![BatchFile::new(3, 2, b"xyz".to_vec())];
1480 let mut batch = FileBatch::upload(backend.device_queue(), &first, 4, 1024)
1481 .expect("Fix: initial FileBatch upload must succeed");
1482 let metadata_ptr = batch.file_metadata.as_ptr();
1483 let offsets_ptr = batch.file_offsets.as_ptr();
1484 let haystack_words_ptr = batch.haystack_words.as_ptr();
1485 let haystack_id = batch.haystack.allocation_identity();
1486 let offsets_id = batch.offsets.allocation_identity();
1487 let metadata_id = batch.metadata.allocation_identity();
1488 let queue_state_id = batch.queue_state.allocation_identity();
1489 let hit_ring_id = batch.hit_ring.allocation_identity();
1490
1491 let refresh_report = batch
1492 .refresh_with_report(&second, 2, 512)
1493 .expect("Fix: smaller FileBatch refresh must succeed in place");
1494
1495 assert_eq!(batch.file_metadata.as_ptr(), metadata_ptr);
1496 assert_eq!(batch.file_offsets.as_ptr(), offsets_ptr);
1497 assert_eq!(batch.haystack_words.as_ptr(), haystack_words_ptr);
1498 assert_eq!(batch.haystack.allocation_identity(), haystack_id);
1499 assert_eq!(batch.offsets.allocation_identity(), offsets_id);
1500 assert_eq!(batch.metadata.allocation_identity(), metadata_id);
1501 assert_eq!(batch.queue_state.allocation_identity(), queue_state_id);
1502 assert_eq!(batch.hit_ring.allocation_identity(), hit_ring_id);
1503 assert_eq!(batch.file_count(), 1);
1504 assert_eq!(batch.queue_len(), 2);
1505 assert!(
1506 batch.host_work_items().is_empty(),
1507 "dense megakernel batches must not retain file_count * rule_count host triples"
1508 );
1509 assert_eq!(batch.hit_capacity(), 512);
1510 assert_eq!(
1511 refresh_report.resident_allocations, 0,
1512 "smaller refresh must reuse every resident allocation"
1513 );
1514 assert_eq!(
1515 refresh_report.refreshed_buffers, 0,
1516 "smaller refresh must not replace resident input buffers"
1517 );
1518 assert_eq!(
1519 refresh_report.reused_buffers, 6,
1520 "refresh must account for five refreshed inputs (haystack, offsets, metadata, \
1521 segments, queue_state) plus hit ring reuse"
1522 );
1523 assert!(
1524 refresh_report.bytes_uploaded > 0,
1525 "refresh telemetry must report host-to-device logical prefix writes"
1526 );
1527
1528 let config = crate::megakernel::BatchDispatchConfig {
1529 workgroup_size_x: 64,
1530 worker_groups: 4,
1531 hit_capacity: 512,
1532 timeout: std::time::Duration::from_secs(10),
1533 ..Default::default()
1534 };
1535 let mut dispatcher = crate::megakernel::BatchDispatcher::new(backend, config)
1536 .expect("Fix: live batch dispatcher must compile after FileBatch refresh");
1537 let rules = vec![
1538 vyre_runtime::megakernel::BatchRuleProgram::new(0, vec![0; 256], vec![1], 1)
1539 .expect("Fix: accepting rule 0 must be valid"),
1540 vyre_runtime::megakernel::BatchRuleProgram::new(1, vec![0; 256], vec![1], 1)
1541 .expect("Fix: accepting rule 1 must be valid"),
1542 ];
1543 let mut hits = Vec::new();
1544 let report = dispatcher
1545 .dispatch_into(&batch, &rules, &mut hits)
1546 .expect("Fix: refreshed FileBatch must dispatch");
1547 assert_eq!(
1548 report.hit_count, 6,
1549 "refreshed batch must scan only the 3 refreshed bytes across 2 rules, not stale tail bytes"
1550 );
1551 }
1552
1553 #[test]
1554 fn set_segmentation_preserves_hits_vs_whole_file() {
1555 let backend = crate::WgpuBackend::new()
1564 .expect("Fix: live WGPU backend required for the segmentation GPU-parity contract.");
1565 let content: Vec<u8> = (0..600u32).map(|i| (i % 251) as u8).collect();
1566 let files = vec![BatchFile::new(7, 0, content)];
1567 let mut batch = FileBatch::upload(backend.device_queue(), &files, 1, 2048)
1568 .expect("Fix: FileBatch upload must succeed");
1569
1570 let config = crate::megakernel::BatchDispatchConfig {
1571 workgroup_size_x: 64,
1572 worker_groups: 8,
1573 hit_capacity: 2048,
1574 timeout: std::time::Duration::from_secs(10),
1575 ..Default::default()
1576 };
1577 let mut dispatcher = crate::megakernel::BatchDispatcher::new(backend, config)
1578 .expect("Fix: live batch dispatcher must compile");
1579 let rules =
1580 vec![
1581 vyre_runtime::megakernel::BatchRuleProgram::new(0, vec![0; 256], vec![1], 1)
1582 .expect("Fix: accept-every rule must be valid"),
1583 ];
1584
1585 assert_eq!(
1587 batch.queue_len(),
1588 1,
1589 "default geometry is one work item (1 file × 1 rule)"
1590 );
1591 let mut whole = Vec::new();
1592 dispatcher
1593 .dispatch_into(&batch, &rules, &mut whole)
1594 .expect("Fix: whole-file dispatch must succeed");
1595 let whole_set: std::collections::BTreeSet<(u32, u32)> =
1596 whole.iter().map(|h| (h.rule_idx, h.match_offset)).collect();
1597 assert_eq!(
1598 whole_set.len(),
1599 600,
1600 "accept-every rule hits every one of 600 offsets"
1601 );
1602 assert_eq!(*whole_set.iter().next().unwrap(), (0, 0));
1603 assert_eq!(*whole_set.iter().next_back().unwrap(), (0, 599));
1604
1605 batch
1607 .set_segmentation(128, 8)
1608 .expect("Fix: set_segmentation must succeed");
1609 assert_eq!(
1610 batch.queue_len(),
1611 5,
1612 "600 bytes at seg_len=128 ⇒ 5 segments × 1 rule = 5 work items"
1613 );
1614 let mut segmented = Vec::new();
1615 dispatcher
1616 .dispatch_into(&batch, &rules, &mut segmented)
1617 .expect("Fix: segmented dispatch must succeed");
1618 let segmented_set: std::collections::BTreeSet<(u32, u32)> = segmented
1619 .iter()
1620 .map(|h| (h.rule_idx, h.match_offset))
1621 .collect();
1622
1623 assert_eq!(
1624 segmented_set, whole_set,
1625 "segmented scan must produce the identical (rule, offset) hit set as the whole-file scan"
1626 );
1627 assert_eq!(
1629 segmented.len(),
1630 whole.len(),
1631 "segmented scan must not duplicate hits across window warm-up overlaps"
1632 );
1633 }
1634
1635 #[test]
1636 fn refresh_reused_buffers_write_only_padded_logical_prefix() {
1637 let src = include_str!("batch.rs");
1638 let production = src
1639 .split("\n#[cfg(test)]\nmod tests")
1640 .next()
1641 .expect("Fix: FileBatch production section should precede tests");
1642 let refresh_body = src
1643 .split("pub fn refresh(")
1644 .nth(1)
1645 .and_then(|tail| tail.split("pub fn reset_queue_state").next())
1646 .expect("Fix: FileBatch::refresh body must be discoverable");
1647 let reused_write_body = src
1648 .split("fn write_padded_prefix(")
1649 .nth(1)
1650 .and_then(|tail| tail.split("fn validate_batch_shape").next())
1651 .expect("Fix: write_padded_prefix body must be discoverable");
1652
1653 assert!(
1654 refresh_body.contains("accumulate_refresh"),
1655 "FileBatch::refresh must route resident inputs through telemetry-aware reusable buffer refresh"
1656 );
1657 assert!(
1658 refresh_body.contains("refresh_with_report"),
1659 "FileBatch::refresh must preserve the telemetry-capable refresh path"
1660 );
1661 assert!(
1662 reused_write_body.contains("crate::padded_upload::write_padded_prefix"),
1663 "reused FileBatch buffers must use the shared padded-prefix writer"
1664 );
1665 assert!(
1666 !reused_write_body.contains("allocation_len"),
1667 "reused FileBatch buffers must not zero-fill the full old allocation on smaller refreshes"
1668 );
1669 assert!(
1670 !production.contains("Vec::with_capacity"),
1671 "Fix: FileBatch upload/refresh staging must not use infallible capacity constructors."
1672 );
1673 assert!(
1674 !production.contains(".reserve_exact("),
1675 "Fix: FileBatch upload/refresh staging must route reservations through the shared fallible helper."
1676 );
1677 assert!(
1678 production.contains("reserve_batch_vec_len"),
1679 "Fix: FileBatch staging should have one shared target-length reservation adapter."
1680 );
1681 }
1682}