1use std::collections::{HashMap, HashSet};
18
19use ndarray::{Array2, ArrayView2, ArrayViewMut2};
20use rayon::prelude::*;
21
22use crate::accumulate::PerImageEval;
23use crate::dataset::{CategoryId, CocoDataset, CocoDetections, EvalDataset, ImageMeta};
24use crate::error::EvalError;
25use crate::evaluate::{
26 dt_top_indices_for_cell_into, evaluate_cell, gt_indices_for_cell, raw_dt_indices_for_cell,
27 CellBuffers, CellScratch, EvalGrid, EvalImageMeta, EvalKernel, EvaluateParams, KernelScratch,
28 COLLAPSED_CATEGORY_SENTINEL,
29};
30use crate::parity::ParityMode;
31
32#[cfg(feature = "bench-timings")]
34pub(crate) mod timings {
35 use crate::bench_counters::BenchCounterSet;
36
37 pub(super) const PAR_ITER_NS: usize = 0;
38 pub(super) const SERIAL_POST_NS: usize = 1;
39 pub(super) const N_CALLS: usize = 2;
40
41 pub(super) static COUNTERS: BenchCounterSet<3> = BenchCounterSet::new();
42
43 pub(crate) fn read_and_reset() -> (u64, u64, u64) {
44 let [p, s, n] = COUNTERS.read_and_reset();
45 (p, s, n)
46 }
47}
48
49pub fn evaluate_with_parallel<K: EvalKernel>(
56 gt: &CocoDataset,
57 dt: &CocoDetections,
58 params: EvaluateParams<'_>,
59 parity_mode: ParityMode,
60 kernel: &K,
61) -> Result<EvalGrid, EvalError> {
62 let mut images: Vec<&ImageMeta> = gt.images().iter().collect();
63 images.sort_unstable_by_key(|im| im.id.0);
64 let n_i = images.len();
65 let n_a = params.area_ranges.len();
66
67 let category_buckets: Vec<Option<CategoryId>> = if params.use_cats {
68 let mut cats: Vec<_> = gt.categories().iter().map(|c| c.id).collect();
69 cats.sort_unstable_by_key(|id| id.0);
70 cats.into_iter().map(Some).collect()
71 } else {
72 vec![None]
73 };
74 let n_k = category_buckets.len();
75
76 let federated_per_image: Vec<Option<(&HashSet<CategoryId>, &HashSet<CategoryId>)>> =
77 match (params.use_cats, gt.federated()) {
78 (true, Some(fed)) => images
79 .iter()
80 .map(|im| {
81 let neg = fed.neg_category_ids.get(&im.id)?;
82 let nel = fed.not_exhaustive_category_ids.get(&im.id)?;
83 Some((neg, nel))
84 })
85 .collect(),
86 _ => Vec::new(),
87 };
88
89 let strict_lvis_zero_area_filter =
90 matches!(parity_mode, ParityMode::Strict) && gt.federated().is_some();
91
92 let gt_anns = gt.annotations();
93 let dt_anns = dt.detections();
94
95 #[cfg(feature = "bench-timings")]
102 let t_par = std::time::Instant::now();
103
104 let total_slots = n_k * n_a * n_i;
105 let chunk_len = n_k * n_a;
106 let mut eval_imgs: Vec<Option<Box<PerImageEval>>> = vec![None; total_slots];
107 let mut eval_imgs_meta: Vec<Option<Box<EvalImageMeta>>> = vec![None; total_slots];
108
109 let retained_per_image: Vec<Vec<(usize, Array2<f64>)>> = eval_imgs
110 .par_chunks_mut(chunk_len)
111 .zip(eval_imgs_meta.par_chunks_mut(chunk_len))
112 .enumerate()
113 .map_init(
114 || {
115 (
116 CellScratch::new(),
117 KernelScratch::<K::Annotation>::default(),
118 )
119 },
120 |(scratch, kernel_scratch), (i, (eval_chunk, meta_chunk))| {
121 let mut per_image_retained: Vec<(usize, Array2<f64>)> = Vec::new();
122 for k in 0..n_k {
123 let eval_slice = &mut eval_chunk[k * n_a..(k + 1) * n_a];
124 let meta_slice = &mut meta_chunk[k * n_a..(k + 1) * n_a];
125 process_one_cell_into(
126 scratch,
127 kernel_scratch,
128 k,
129 i,
130 &images,
131 &category_buckets,
132 gt,
133 gt_anns,
134 dt,
135 dt_anns,
136 params,
137 parity_mode,
138 kernel,
139 &federated_per_image,
140 strict_lvis_zero_area_filter,
141 eval_slice,
142 meta_slice,
143 &mut per_image_retained,
144 )?;
145 }
146 Ok::<Vec<(usize, Array2<f64>)>, EvalError>(per_image_retained)
147 },
148 )
149 .collect::<Result<Vec<_>, _>>()?;
150
151 #[cfg(feature = "bench-timings")]
152 let par_ns = u64::try_from(t_par.elapsed().as_nanos()).unwrap_or(u64::MAX);
153
154 #[cfg(feature = "bench-timings")]
155 let t_post = std::time::Instant::now();
156
157 transpose_pair_image_major_to_canonical(
158 &mut eval_imgs,
159 &mut eval_imgs_meta,
160 CellLayout { n_k, n_a, n_i },
161 );
162
163 let mut retained_pairs: Vec<((usize, usize), Array2<f64>)> = Vec::new();
164 for (i, per_image) in retained_per_image.into_iter().enumerate() {
165 for (k, iou) in per_image {
166 retained_pairs.push(((k, i), iou));
167 }
168 }
169
170 let retained_ious_map: Option<HashMap<(usize, usize), Array2<f64>>> = if params.retain_iou {
174 retained_pairs.sort_by_key(|&(key, _)| key);
175 Some(retained_pairs.into_iter().collect())
176 } else {
177 None
178 };
179
180 let grid = EvalGrid {
181 eval_imgs,
182 eval_imgs_meta,
183 n_categories: n_k,
184 n_area_ranges: n_a,
185 n_images: n_i,
186 retained_ious: retained_ious_map.map(crate::tables::RetainedIous::from_map),
187 };
188 #[cfg(feature = "bench-timings")]
189 {
190 let post_ns = u64::try_from(t_post.elapsed().as_nanos()).unwrap_or(u64::MAX);
191 timings::COUNTERS.add(timings::PAR_ITER_NS, par_ns);
192 timings::COUNTERS.add(timings::SERIAL_POST_NS, post_ns);
193 timings::COUNTERS.bump(timings::N_CALLS);
194 }
195 Ok(grid)
196}
197
198#[allow(clippy::too_many_arguments)]
202fn process_one_cell_into<K: EvalKernel>(
203 scratch: &mut CellScratch,
204 kernel_scratch: &mut KernelScratch<K::Annotation>,
205 k: usize,
206 i: usize,
207 images: &[&ImageMeta],
208 category_buckets: &[Option<CategoryId>],
209 gt: &CocoDataset,
210 gt_anns: &[crate::dataset::CocoAnnotation],
211 dt: &CocoDetections,
212 dt_anns: &[crate::dataset::CocoDetection],
213 params: EvaluateParams<'_>,
214 parity_mode: ParityMode,
215 kernel: &K,
216 federated_per_image: &[Option<(&HashSet<CategoryId>, &HashSet<CategoryId>)>],
217 strict_lvis_zero_area_filter: bool,
218 eval_slice: &mut [Option<Box<PerImageEval>>],
219 meta_slice: &mut [Option<Box<EvalImageMeta>>],
220 retained_out: &mut Vec<(usize, Array2<f64>)>,
221) -> Result<(), EvalError> {
222 let cat = category_buckets[k];
223 let category_id = cat.map_or(COLLAPSED_CATEGORY_SENTINEL, |c| c.0);
224 let image = images[i];
225 let image_id = image.id;
226
227 let gt_indices_raw = gt_indices_for_cell(gt, image_id, cat);
228 let gt_indices_buf: Vec<usize>;
229 let gt_indices: &[usize] =
230 if strict_lvis_zero_area_filter && gt_indices_raw.iter().any(|&j| gt_anns[j].area <= 0.0) {
231 gt_indices_buf = gt_indices_raw
232 .iter()
233 .copied()
234 .filter(|&j| gt_anns[j].area > 0.0)
235 .collect();
236 >_indices_buf
237 } else {
238 gt_indices_raw
239 };
240 let raw_dt_indices = raw_dt_indices_for_cell(dt, image_id, cat);
241 if gt_indices.is_empty() && raw_dt_indices.is_empty() {
242 return Ok(());
243 }
244
245 let mut not_exhaustive_for_cell = false;
248 if let (Some(c), Some(Some((neg_set, nel_set)))) = (cat, federated_per_image.get(i)) {
249 if gt_indices.is_empty() && !neg_set.contains(&c) {
250 return Ok(());
251 }
252 not_exhaustive_for_cell = nel_set.contains(&c);
253 }
254
255 dt_top_indices_for_cell_into(
256 &mut scratch.dt_indices,
257 &mut scratch.dt_score_buf,
258 &mut scratch.dt_perm_buf,
259 dt_anns,
260 raw_dt_indices,
261 params.max_dets_per_image,
262 );
263
264 scratch.gt_areas.clear();
265 scratch
266 .gt_areas
267 .extend(gt_indices.iter().map(|&j| gt_anns[j].area));
268 scratch.gt_iscrowd.clear();
269 scratch
270 .gt_iscrowd
271 .extend(gt_indices.iter().map(|&j| gt_anns[j].is_crowd));
272 scratch.gt_base_ignore.clear();
273 scratch.gt_base_ignore.extend(
274 gt_indices.iter().map(|&j| {
275 gt_anns[j].effective_ignore(parity_mode) || kernel.extra_gt_ignore(>_anns[j])
276 }),
277 );
278 scratch.gt_ids.clear();
279 scratch
280 .gt_ids
281 .extend(gt_indices.iter().map(|&j| gt_anns[j].id.0));
282 scratch.dt_areas.clear();
283 scratch
284 .dt_areas
285 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].area));
286 scratch.dt_scores.clear();
287 scratch
288 .dt_scores
289 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].score));
290 scratch.dt_ids.clear();
291 scratch
292 .dt_ids
293 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].id.0));
294
295 #[cfg(feature = "bench-timings")]
296 {
297 crate::evaluate::build_anns_count::bump();
298 crate::evaluate::build_anns_count::bump();
299 }
300 kernel.build_gt_anns_into(&mut kernel_scratch.gt, gt_anns, gt_indices, image)?;
301 kernel.build_dt_anns_into(
302 &mut kernel_scratch.dt,
303 dt_anns,
304 &scratch.dt_indices,
305 image,
306 parity_mode,
307 )?;
308
309 let g = kernel_scratch.gt.len();
310 let d = kernel_scratch.dt.len();
311 scratch.iou_buf.clear();
312 scratch.iou_buf.resize(g * d, 0.0);
313 if g > 0 && d > 0 {
314 let mut iou_view =
315 ArrayViewMut2::from_shape((g, d), &mut scratch.iou_buf[..]).map_err(|e| {
316 EvalError::DimensionMismatch {
317 detail: format!("iou scratch view: {e}"),
318 }
319 })?;
320 kernel.compute(&kernel_scratch.gt, &kernel_scratch.dt, &mut iou_view)?;
321 }
322
323 let iou_view = ArrayView2::from_shape((g, d), &scratch.iou_buf[..]).map_err(|e| {
324 EvalError::DimensionMismatch {
325 detail: format!("iou scratch view: {e}"),
326 }
327 })?;
328 let buffers = CellBuffers {
329 image_id: image_id.0,
330 category_id,
331 max_det: params.max_dets_per_image,
332 gt_areas: &scratch.gt_areas,
333 gt_iscrowd: &scratch.gt_iscrowd,
334 gt_base_ignore: &scratch.gt_base_ignore,
335 gt_ids: &scratch.gt_ids,
336 dt_areas: &scratch.dt_areas,
337 dt_scores: &scratch.dt_scores,
338 dt_ids: &scratch.dt_ids,
339 iou: iou_view,
340 not_exhaustive: not_exhaustive_for_cell,
341 };
342
343 for (a, area) in params.area_ranges.iter().enumerate() {
344 let (cell, meta) = evaluate_cell(
345 &mut scratch.gt_ignore_buf,
346 &buffers,
347 area,
348 params.iou_thresholds,
349 parity_mode,
350 )?;
351 eval_slice[a] = Some(Box::new(cell));
352 meta_slice[a] = Some(Box::new(meta));
353 }
354
355 if params.retain_iou {
356 let cloned = Array2::from_shape_vec((g, d), scratch.iou_buf.clone()).map_err(|e| {
357 EvalError::DimensionMismatch {
358 detail: format!("retained iou clone: {e}"),
359 }
360 })?;
361 retained_out.push((k, cloned));
362 }
363
364 Ok(())
365}
366
367#[derive(Clone, Copy)]
370struct CellLayout {
371 n_k: usize,
372 n_a: usize,
373 n_i: usize,
374}
375
376fn transpose_pair_image_major_to_canonical<A, B>(
382 buf_a: &mut [Option<A>],
383 buf_b: &mut [Option<B>],
384 layout: CellLayout,
385) {
386 let total = layout.n_k * layout.n_a * layout.n_i;
387 debug_assert_eq!(buf_a.len(), total);
388 debug_assert_eq!(buf_b.len(), total);
389 if total <= 1 {
390 return;
391 }
392 let mut visited = vec![false; total];
393 for start in 0..total {
394 if visited[start] {
395 continue;
396 }
397 visited[start] = true;
398 let mut next = image_major_to_canonical(start, layout);
399 if next == start {
400 continue;
401 }
402 let mut held_a = buf_a[start].take();
403 let mut held_b = buf_b[start].take();
404 while next != start {
405 visited[next] = true;
406 std::mem::swap(&mut held_a, &mut buf_a[next]);
407 std::mem::swap(&mut held_b, &mut buf_b[next]);
408 next = image_major_to_canonical(next, layout);
409 }
410 buf_a[start] = held_a;
411 buf_b[start] = held_b;
412 }
413}
414
415#[inline]
418fn image_major_to_canonical(pos: usize, layout: CellLayout) -> usize {
419 let chunk = layout.n_k * layout.n_a;
420 let i = pos / chunk;
421 let r = pos % chunk;
422 let k = r / layout.n_a;
423 let a = r % layout.n_a;
424 k * layout.n_a * layout.n_i + a * layout.n_i + i
425}
426
427use crate::evaluate::{boundary_kernel, segm_kernel};
433use crate::similarity::{BboxIou, BoundaryGtCache, OksSimilarity, SegmGtCache};
434
435pub fn evaluate_bbox_parallel(
441 gt: &CocoDataset,
442 dt: &CocoDetections,
443 params: EvaluateParams<'_>,
444 parity_mode: ParityMode,
445) -> Result<EvalGrid, EvalError> {
446 evaluate_with_parallel(gt, dt, params, parity_mode, &BboxIou)
447}
448
449pub fn evaluate_segm_parallel(
455 gt: &CocoDataset,
456 dt: &CocoDetections,
457 params: EvaluateParams<'_>,
458 parity_mode: ParityMode,
459) -> Result<EvalGrid, EvalError> {
460 evaluate_with_parallel(gt, dt, params, parity_mode, &segm_kernel(None))
461}
462
463pub fn evaluate_segm_cached_parallel(
469 gt: &CocoDataset,
470 dt: &CocoDetections,
471 params: EvaluateParams<'_>,
472 parity_mode: ParityMode,
473 cache: &SegmGtCache,
474) -> Result<EvalGrid, EvalError> {
475 evaluate_with_parallel(gt, dt, params, parity_mode, &segm_kernel(Some(cache)))
476}
477
478pub fn evaluate_boundary_parallel(
490 gt: &CocoDataset,
491 dt: &CocoDetections,
492 params: EvaluateParams<'_>,
493 parity_mode: ParityMode,
494 dilation_ratio: f64,
495) -> Result<EvalGrid, EvalError> {
496 evaluate_with_parallel(
497 gt,
498 dt,
499 params,
500 parity_mode,
501 &boundary_kernel(dilation_ratio, None),
502 )
503}
504
505pub fn evaluate_boundary_cached_parallel(
511 gt: &CocoDataset,
512 dt: &CocoDetections,
513 params: EvaluateParams<'_>,
514 parity_mode: ParityMode,
515 dilation_ratio: f64,
516 cache: &BoundaryGtCache,
517) -> Result<EvalGrid, EvalError> {
518 cache.align_ratio(dilation_ratio);
519 evaluate_with_parallel(
520 gt,
521 dt,
522 params,
523 parity_mode,
524 &boundary_kernel(dilation_ratio, Some(cache)),
525 )
526}
527
528pub fn evaluate_keypoints_parallel(
534 gt: &CocoDataset,
535 dt: &CocoDetections,
536 params: EvaluateParams<'_>,
537 parity_mode: ParityMode,
538 sigmas: HashMap<i64, Vec<f64>>,
539) -> Result<EvalGrid, EvalError> {
540 evaluate_with_parallel(gt, dt, params, parity_mode, &OksSimilarity::new(sigmas))
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
550 use crate::accumulate::{accumulate, AccumulateParams};
551 use crate::dataset::{
552 AnnId, Bbox, CategoryId, CategoryMeta, CocoAnnotation, DetectionInput, ImageId, ImageMeta,
553 };
554 use crate::evaluate::{evaluate_with, AreaRange};
555 use crate::parity::{iou_thresholds, recall_thresholds};
556 use crate::similarity::BboxIou;
557
558 fn img(id: i64, w: u32, h: u32) -> ImageMeta {
559 ImageMeta {
560 id: ImageId(id),
561 width: w,
562 height: h,
563 file_name: None,
564 }
565 }
566
567 fn transpose_reference<T: Clone>(
569 image_major: &[Option<T>],
570 n_k: usize,
571 n_a: usize,
572 n_i: usize,
573 ) -> Vec<Option<T>> {
574 let mut out: Vec<Option<T>> = (0..n_k * n_a * n_i).map(|_| None).collect();
575 for i in 0..n_i {
576 for k in 0..n_k {
577 for a in 0..n_a {
578 let src = i * n_k * n_a + k * n_a + a;
579 let dst = k * n_a * n_i + a * n_i + i;
580 out[dst] = image_major[src].clone();
581 }
582 }
583 }
584 out
585 }
586
587 fn paired_fixture(
591 n_k: usize,
592 n_a: usize,
593 n_i: usize,
594 keep: impl Fn(usize) -> bool,
595 ) -> (Vec<Option<u32>>, Vec<Option<i64>>) {
596 let n = n_k * n_a * n_i;
597 let a: Vec<Option<u32>> = (0..n)
598 .map(|p| if keep(p) { Some(p as u32) } else { None })
599 .collect();
600 let b: Vec<Option<i64>> = (0..n)
601 .map(|p| if keep(p) { Some(-(p as i64)) } else { None })
602 .collect();
603 (a, b)
604 }
605
606 #[test]
607 fn inplace_transpose_matches_reference_dense() {
608 let layout = CellLayout {
610 n_k: 3,
611 n_a: 2,
612 n_i: 4,
613 };
614 let (mut a, mut b) = paired_fixture(layout.n_k, layout.n_a, layout.n_i, |_| true);
615 let expected_a = transpose_reference(&a, layout.n_k, layout.n_a, layout.n_i);
616 let expected_b = transpose_reference(&b, layout.n_k, layout.n_a, layout.n_i);
617 transpose_pair_image_major_to_canonical(&mut a, &mut b, layout);
618 assert_eq!(a, expected_a);
619 assert_eq!(b, expected_b);
620 }
621
622 #[test]
623 fn inplace_transpose_matches_reference_sparse() {
624 let layout = CellLayout {
627 n_k: 4,
628 n_a: 4,
629 n_i: 5,
630 };
631 let (mut a, mut b) = paired_fixture(layout.n_k, layout.n_a, layout.n_i, |p| p % 3 == 0);
632 let expected_a = transpose_reference(&a, layout.n_k, layout.n_a, layout.n_i);
633 let expected_b = transpose_reference(&b, layout.n_k, layout.n_a, layout.n_i);
634 transpose_pair_image_major_to_canonical(&mut a, &mut b, layout);
635 assert_eq!(a, expected_a);
636 assert_eq!(b, expected_b);
637 }
638
639 #[test]
640 fn inplace_transpose_handles_degenerate_shapes() {
641 for (n_k, n_a, n_i) in [(1, 4, 5), (3, 4, 1), (1, 1, 1)] {
644 let layout = CellLayout { n_k, n_a, n_i };
645 let (mut a, mut b) = paired_fixture(n_k, n_a, n_i, |_| true);
646 let expected_a = transpose_reference(&a, n_k, n_a, n_i);
647 let expected_b = transpose_reference(&b, n_k, n_a, n_i);
648 transpose_pair_image_major_to_canonical(&mut a, &mut b, layout);
649 assert_eq!(a, expected_a, "shape ({n_k}, {n_a}, {n_i})");
650 assert_eq!(b, expected_b, "shape ({n_k}, {n_a}, {n_i})");
651 }
652 }
653
654 fn cat(id: i64, name: &str) -> CategoryMeta {
655 CategoryMeta {
656 id: CategoryId(id),
657 name: name.into(),
658 supercategory: None,
659 }
660 }
661
662 fn ann(id: i64, image: i64, cat_id: i64, bbox: (f64, f64, f64, f64)) -> CocoAnnotation {
663 CocoAnnotation {
664 id: AnnId(id),
665 image_id: ImageId(image),
666 category_id: CategoryId(cat_id),
667 area: bbox.2 * bbox.3,
668 is_crowd: false,
669 ignore_flag: None,
670 bbox: Bbox {
671 x: bbox.0,
672 y: bbox.1,
673 w: bbox.2,
674 h: bbox.3,
675 },
676 segmentation: None,
677 keypoints: None,
678 num_keypoints: None,
679 }
680 }
681
682 fn dt_input(image: i64, cat_id: i64, score: f64, bbox: (f64, f64, f64, f64)) -> DetectionInput {
683 DetectionInput {
684 id: None,
685 image_id: ImageId(image),
686 category_id: CategoryId(cat_id),
687 score,
688 bbox: Bbox {
689 x: bbox.0,
690 y: bbox.1,
691 w: bbox.2,
692 h: bbox.3,
693 },
694 segmentation: None,
695 keypoints: None,
696 num_keypoints: None,
697 }
698 }
699
700 fn tiny_grid_inputs() -> (CocoDataset, CocoDetections) {
701 let images = vec![img(1, 100, 100), img(2, 100, 100), img(3, 100, 100)];
702 let cats = vec![cat(1, "a"), cat(2, "b")];
703 let mut anns = Vec::new();
704 let mut next_id = 1_i64;
705 for &im_id in &[1_i64, 2, 3] {
706 for &cat_id in &[1_i64, 2] {
707 anns.push(ann(next_id, im_id, cat_id, (10.0, 10.0, 20.0, 20.0)));
708 next_id += 1;
709 }
710 }
711 let gt = CocoDataset::from_parts(images, anns, cats).expect("dataset");
712 let dts = CocoDetections::from_inputs(vec![
713 dt_input(1, 1, 0.9, (12.0, 12.0, 18.0, 18.0)),
714 dt_input(2, 1, 0.8, (11.0, 11.0, 20.0, 20.0)),
715 dt_input(3, 2, 0.7, (15.0, 15.0, 10.0, 10.0)),
716 ])
717 .expect("detections");
718 (gt, dts)
719 }
720
721 #[test]
722 fn parallel_eval_imgs_bit_equals_sequential_bbox() {
723 let (gt, dt) = tiny_grid_inputs();
724 let area = AreaRange::coco_default();
725 let params = EvaluateParams {
726 iou_thresholds: iou_thresholds(),
727 area_ranges: &area,
728 max_dets_per_image: 100,
729 use_cats: true,
730 retain_iou: false,
731 };
732 let kernel = BboxIou;
733
734 let seq = evaluate_with(>, &dt, params, ParityMode::Strict, &kernel).expect("seq");
735
736 let pool = rayon::ThreadPoolBuilder::new()
739 .num_threads(4)
740 .build()
741 .expect("pool");
742 let par = pool
743 .install(|| evaluate_with_parallel(>, &dt, params, ParityMode::Strict, &kernel))
744 .expect("par");
745
746 assert_eq!(seq.n_categories, par.n_categories);
747 assert_eq!(seq.n_area_ranges, par.n_area_ranges);
748 assert_eq!(seq.n_images, par.n_images);
749 assert_eq!(seq.eval_imgs.len(), par.eval_imgs.len());
750 for (idx, (s, p)) in seq.eval_imgs.iter().zip(par.eval_imgs.iter()).enumerate() {
751 match (s, p) {
752 (None, None) => {}
753 (Some(s), Some(p)) => {
754 assert_eq!(s.dt_scores, p.dt_scores, "dt_scores mismatch at flat={idx}");
755 assert_eq!(s.gt_ignore, p.gt_ignore, "gt_ignore mismatch at flat={idx}");
756 assert_eq!(
757 s.dt_matched, p.dt_matched,
758 "dt_matched mismatch at flat={idx}"
759 );
760 assert_eq!(s.dt_ignore, p.dt_ignore, "dt_ignore mismatch at flat={idx}");
761 }
762 _ => panic!("Some/None mismatch at flat={idx}"),
763 }
764 }
765 }
766
767 #[test]
768 fn parallel_summary_bit_equals_sequential_across_thread_counts() {
769 let (gt, dt) = tiny_grid_inputs();
770 let area = AreaRange::coco_default();
771 let params = EvaluateParams {
772 iou_thresholds: iou_thresholds(),
773 area_ranges: &area,
774 max_dets_per_image: 100,
775 use_cats: true,
776 retain_iou: false,
777 };
778 let kernel = BboxIou;
779
780 let seq = evaluate_with(>, &dt, params, ParityMode::Strict, &kernel).expect("seq");
781
782 let acc_params = AccumulateParams {
783 iou_thresholds: iou_thresholds(),
784 recall_thresholds: recall_thresholds(),
785 max_dets: &[1, 10, 100],
786 n_categories: seq.n_categories,
787 n_area_ranges: seq.n_area_ranges,
788 n_images: seq.n_images,
789 };
790 let acc_seq = accumulate(&seq.eval_imgs, acc_params, ParityMode::Strict).expect("acc seq");
791
792 for n_threads in [2usize, 3, 4, 8] {
795 let pool = rayon::ThreadPoolBuilder::new()
796 .num_threads(n_threads)
797 .build()
798 .expect("pool");
799 let par = pool
800 .install(|| evaluate_with_parallel(>, &dt, params, ParityMode::Strict, &kernel))
801 .expect("par");
802 let acc_par =
803 accumulate(&par.eval_imgs, acc_params, ParityMode::Strict).expect("acc par");
804 assert_eq!(
805 acc_seq.precision, acc_par.precision,
806 "precision mismatch at n_threads={n_threads}"
807 );
808 assert_eq!(
809 acc_seq.recall, acc_par.recall,
810 "recall mismatch at n_threads={n_threads}"
811 );
812 assert_eq!(
813 acc_seq.scores, acc_par.scores,
814 "scores mismatch at n_threads={n_threads}"
815 );
816 }
817 }
818}