Xenova HF Staff commited on
Commit
57ccde0
·
verified ·
1 Parent(s): 723a9b2

sync c928d21e6cc1

Browse files
README.md CHANGED
@@ -54,6 +54,7 @@ Default values (overridable per request):
54
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
  - [`test.json`](build/webgpu/test.json) — correctness cases
56
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
57
  - [`nms-groups-compact.wgsl.jinja`](build/webgpu/nms-groups-compact.wgsl.jinja)
58
  - [`nms-groups-select.wgsl.jinja`](build/webgpu/nms-groups-select.wgsl.jinja)
59
  - [`non-max-suppression.wgsl.jinja`](build/webgpu/non-max-suppression.wgsl.jinja)
 
54
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
  - [`test.json`](build/webgpu/test.json) — correctness cases
56
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
57
+ - [`nms-bitmask.wgsl.jinja`](build/webgpu/nms-bitmask.wgsl.jinja)
58
  - [`nms-groups-compact.wgsl.jinja`](build/webgpu/nms-groups-compact.wgsl.jinja)
59
  - [`nms-groups-select.wgsl.jinja`](build/webgpu/nms-groups-select.wgsl.jinja)
60
  - [`non-max-suppression.wgsl.jinja`](build/webgpu/non-max-suppression.wgsl.jinja)
build/webgpu/bench.json CHANGED
@@ -10,7 +10,15 @@
10
  "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "constant", "value": 0.5 } },
11
  "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "constant", "value": 0.1 } }
12
  },
13
- "outputs": { "selected_indices": { "dtype": "uint32", "shape": [10, 3] } }
 
 
 
 
 
 
 
 
14
  },
15
  {
16
  "name": "nms-1class-2048-boxes",
 
10
  "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "constant", "value": 0.5 } },
11
  "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "constant", "value": 0.1 } }
12
  },
13
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [10, 3] } },
14
+ "bench": {
15
+ "metrics": [
16
+ {
17
+ "type": "gflops",
18
+ "value": "7 * dim(shapes.boxes, 0) * dim(shapes.scores, 1) * dim(shapes.boxes, 1) * dim(shapes.boxes, 1)"
19
+ }
20
+ ]
21
+ }
22
  },
23
  {
24
  "name": "nms-1class-2048-boxes",
build/webgpu/manifest.json CHANGED
@@ -67,14 +67,19 @@
67
  "score_threshold": { "kind": "tensor", "semantic": "score_threshold", "role": "input", "required": false },
68
  "selected_indices": { "kind": "tensor", "semantic": "selected_indices", "role": "output", "dtype": "uint32" }
69
  },
70
- "tunables": { "WORKGROUP_SIZE": 256 },
71
  "derive": {
72
  "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
73
  "validLayout": "ranks.boxes == 3 and ranks.scores == 3 and ranks.selected_indices == 2 and dim(shapes.boxes, 0) == dim(shapes.scores, 0) and dim(shapes.boxes, 1) == dim(shapes.scores, 2) and dim(shapes.boxes, 2) == 4 and dim(shapes.selected_indices, 1) == 3 and dim(shapes.selected_indices, 0) <= dim(shapes.boxes, 0) * dim(shapes.scores, 1) * dim(shapes.boxes, 1) and (present.max_output_boxes_per_class or dim(shapes.selected_indices, 0) == 0) and (not present.max_output_boxes_per_class or (ranks.max_output_boxes_per_class == 0 and numel(shapes.max_output_boxes_per_class) == 1)) and (not present.iou_threshold or (ranks.iou_threshold == 0 and numel(shapes.iou_threshold) == 1)) and (not present.score_threshold or (ranks.score_threshold == 0 and numel(shapes.score_threshold) == 1))",
74
  "numGroups": "dim(shapes.boxes, 0) * dim(shapes.scores, 1)",
75
  "maxKeep": "max(1, dim(shapes.boxes, 1))",
76
  "parallelDispatchFits": "numGroups <= device.limits.maxComputeWorkgroupsPerDimension",
77
- "parallelScratchFits": "numGroups * dim(shapes.boxes, 1) * 4 <= storageBufferLimit and numGroups * maxKeep * 4 <= storageBufferLimit and numGroups * 4 <= storageBufferLimit"
 
 
 
 
 
78
  },
79
  "constants": { "workgroupSize": "tunables.WORKGROUP_SIZE" },
80
  "bindingSets": {
@@ -562,9 +567,162 @@
562
  ]
563
  }
564
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  ]
566
  },
567
  "variants": [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  {
569
  "id": "groups_parallel",
570
  "priority": 25,
 
67
  "score_threshold": { "kind": "tensor", "semantic": "score_threshold", "role": "input", "required": false },
68
  "selected_indices": { "kind": "tensor", "semantic": "selected_indices", "role": "output", "dtype": "uint32" }
69
  },
70
+ "tunables": { "WORKGROUP_SIZE": 256, "NMS_BITMASK_MAX_GROUPS": 4, "NMS_BITMASK_MIN_BOXES": 2048 },
71
  "derive": {
72
  "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
73
  "validLayout": "ranks.boxes == 3 and ranks.scores == 3 and ranks.selected_indices == 2 and dim(shapes.boxes, 0) == dim(shapes.scores, 0) and dim(shapes.boxes, 1) == dim(shapes.scores, 2) and dim(shapes.boxes, 2) == 4 and dim(shapes.selected_indices, 1) == 3 and dim(shapes.selected_indices, 0) <= dim(shapes.boxes, 0) * dim(shapes.scores, 1) * dim(shapes.boxes, 1) and (present.max_output_boxes_per_class or dim(shapes.selected_indices, 0) == 0) and (not present.max_output_boxes_per_class or (ranks.max_output_boxes_per_class == 0 and numel(shapes.max_output_boxes_per_class) == 1)) and (not present.iou_threshold or (ranks.iou_threshold == 0 and numel(shapes.iou_threshold) == 1)) and (not present.score_threshold or (ranks.score_threshold == 0 and numel(shapes.score_threshold) == 1))",
74
  "numGroups": "dim(shapes.boxes, 0) * dim(shapes.scores, 1)",
75
  "maxKeep": "max(1, dim(shapes.boxes, 1))",
76
  "parallelDispatchFits": "numGroups <= device.limits.maxComputeWorkgroupsPerDimension",
77
+ "parallelScratchFits": "numGroups * dim(shapes.boxes, 1) * 4 <= storageBufferLimit and numGroups * maxKeep * 4 <= storageBufferLimit and numGroups * 4 <= storageBufferLimit",
78
+ "bitmaskSortN": "pow2ceil(dim(shapes.boxes, 1))",
79
+ "bitmaskMaskWords": "ceilDiv(dim(shapes.boxes, 1), 32)",
80
+ "bitmaskPairBlocks": "ceilDiv(numGroups * dim(shapes.boxes, 1) * bitmaskMaskWords, tunables.WORKGROUP_SIZE)",
81
+ "bitmaskDispatchFits": "numGroups <= device.limits.maxComputeWorkgroupsPerDimension and bitmaskPairBlocks <= device.limits.maxComputeWorkgroupsPerDimension",
82
+ "bitmaskScratchFits": "4 * numGroups * bitmaskSortN <= storageBufferLimit and 4 * numGroups * bitmaskSortN * bitmaskMaskWords <= storageBufferLimit and numGroups * maxKeep * 4 <= storageBufferLimit and numGroups * 4 <= storageBufferLimit"
83
  },
84
  "constants": { "workgroupSize": "tunables.WORKGROUP_SIZE" },
85
  "bindingSets": {
 
567
  ]
568
  }
569
  }
570
+ ],
571
+ "bitmaskSort": [
572
+ {
573
+ "name": "scores",
574
+ "arg": "scores",
575
+ "semantic": "scores",
576
+ "buffer": { "type": "read-only-storage" },
577
+ "elementType": "f32"
578
+ },
579
+ {
580
+ "name": "score_threshold",
581
+ "arg": "score_threshold",
582
+ "semantic": "score_threshold",
583
+ "buffer": { "type": "read-only-storage" },
584
+ "elementType": "f32",
585
+ "length": 1
586
+ },
587
+ { "name": "sortKey", "semantic": "sortKey", "buffer": { "type": "storage" }, "elementType": "u32" },
588
+ { "name": "sortIdx", "semantic": "sortIdx", "buffer": { "type": "storage" }, "elementType": "u32" },
589
+ {
590
+ "name": "params",
591
+ "semantic": "kernel.params",
592
+ "buffer": { "type": "uniform" },
593
+ "struct": {
594
+ "name": "Params",
595
+ "fields": [
596
+ { "name": "boxesPerBatch", "type": "u32", "value": "dim(shapes.boxes, 1)" },
597
+ { "name": "classes", "type": "u32", "value": "dim(shapes.scores, 1)" },
598
+ { "name": "centerPointBox", "type": "u32", "value": "attrs.center_point_box" }
599
+ ]
600
+ }
601
+ }
602
+ ],
603
+ "bitmaskPairs": [
604
+ {
605
+ "name": "boxes",
606
+ "arg": "boxes",
607
+ "semantic": "boxes",
608
+ "buffer": { "type": "read-only-storage" },
609
+ "elementType": "f32"
610
+ },
611
+ {
612
+ "name": "iou_threshold",
613
+ "arg": "iou_threshold",
614
+ "semantic": "iou_threshold",
615
+ "buffer": { "type": "read-only-storage" },
616
+ "elementType": "f32",
617
+ "length": 1
618
+ },
619
+ { "name": "sortIdx", "semantic": "sortIdx", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
620
+ { "name": "pairMask", "semantic": "pairMask", "buffer": { "type": "storage" }, "elementType": "u32" },
621
+ {
622
+ "name": "params",
623
+ "semantic": "kernel.params",
624
+ "buffer": { "type": "uniform" },
625
+ "struct": {
626
+ "name": "Params",
627
+ "fields": [
628
+ { "name": "boxesPerBatch", "type": "u32", "value": "dim(shapes.boxes, 1)" },
629
+ { "name": "classes", "type": "u32", "value": "dim(shapes.scores, 1)" },
630
+ { "name": "centerPointBox", "type": "u32", "value": "attrs.center_point_box" }
631
+ ]
632
+ }
633
+ }
634
+ ],
635
+ "bitmaskWalk": [
636
+ {
637
+ "name": "max_output_boxes_per_class",
638
+ "arg": "max_output_boxes_per_class",
639
+ "semantic": "max_output_boxes_per_class",
640
+ "buffer": { "type": "read-only-storage" },
641
+ "elementType": "u32",
642
+ "length": 1
643
+ },
644
+ { "name": "sortIdx", "semantic": "sortIdx", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
645
+ { "name": "pairMask", "semantic": "pairMask", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
646
+ { "name": "groupSel", "semantic": "groupSel", "buffer": { "type": "storage" }, "elementType": "u32" },
647
+ { "name": "groupCount", "semantic": "groupCount", "buffer": { "type": "storage" }, "elementType": "u32" },
648
+ {
649
+ "name": "params",
650
+ "semantic": "kernel.params",
651
+ "buffer": { "type": "uniform" },
652
+ "struct": {
653
+ "name": "Params",
654
+ "fields": [
655
+ { "name": "boxesPerBatch", "type": "u32", "value": "dim(shapes.boxes, 1)" },
656
+ { "name": "classes", "type": "u32", "value": "dim(shapes.scores, 1)" },
657
+ { "name": "centerPointBox", "type": "u32", "value": "attrs.center_point_box" }
658
+ ]
659
+ }
660
+ }
661
  ]
662
  },
663
  "variants": [
664
+ {
665
+ "id": "bitmask_low_occupancy",
666
+ "priority": 30,
667
+ "description": "Few `(batch, class)` groups cannot occupy the device with one greedy workgroup each, so a sorted-candidate pass, an all-pairs `IoU` bitmask pass, and a mask-replay walk reproduce greedy suppression with pair parallelism; the shared compact pass lays out `selected_indices` unchanged.",
668
+ "when": ["validLayout", "present.max_output_boxes_per_class", "present.iou_threshold", "present.score_threshold", "dim(shapes.selected_indices, 0) > 0", "dim(shapes.boxes, 1) >= tunables.NMS_BITMASK_MIN_BOXES", "numGroups <= tunables.NMS_BITMASK_MAX_GROUPS", "bitmaskDispatchFits", "bitmaskScratchFits"],
669
+ "intermediates": [
670
+ { "id": "sortKey", "dtype": "uint32", "shape": "[numGroups * bitmaskSortN]" },
671
+ { "id": "sortIdx", "dtype": "uint32", "shape": "[numGroups * bitmaskSortN]" },
672
+ { "id": "pairMask", "dtype": "uint32", "shape": "[numGroups * bitmaskSortN * bitmaskMaskWords]" },
673
+ { "id": "groupSel", "dtype": "uint32", "shape": "[numGroups * maxKeep]" },
674
+ { "id": "groupCount", "dtype": "uint32", "shape": "[numGroups]" }
675
+ ],
676
+ "passes": [
677
+ {
678
+ "id": "sort",
679
+ "name": "NonMaxSuppression.BitmaskSort",
680
+ "source": {
681
+ "shader": "nms-bitmask.wgsl.jinja",
682
+ "inputs": { "stage": "\"sort\"", "sortN": "bitmaskSortN", "maskWords": "bitmaskMaskWords" }
683
+ },
684
+ "bindings": "bitmaskSort",
685
+ "dispatch": { "x": "numGroups" }
686
+ },
687
+ {
688
+ "id": "pairs",
689
+ "name": "NonMaxSuppression.BitmaskPairs",
690
+ "source": {
691
+ "shader": "nms-bitmask.wgsl.jinja",
692
+ "inputs": {
693
+ "stage": "\"pairs\"",
694
+ "sortN": "bitmaskSortN",
695
+ "maskWords": "bitmaskMaskWords",
696
+ "numGroups": "numGroups"
697
+ }
698
+ },
699
+ "bindings": "bitmaskPairs",
700
+ "dispatch": { "x": "bitmaskPairBlocks" }
701
+ },
702
+ {
703
+ "id": "walk",
704
+ "name": "NonMaxSuppression.BitmaskWalk",
705
+ "source": {
706
+ "shader": "nms-bitmask.wgsl.jinja",
707
+ "inputs": {
708
+ "stage": "\"walk\"",
709
+ "sortN": "bitmaskSortN",
710
+ "maskWords": "bitmaskMaskWords",
711
+ "maxKeep": "maxKeep"
712
+ }
713
+ },
714
+ "bindings": "bitmaskWalk",
715
+ "dispatch": { "x": "numGroups" }
716
+ },
717
+ {
718
+ "id": "compact",
719
+ "name": "NonMaxSuppression.GroupCompact",
720
+ "source": { "shader": "nms-groups-compact.wgsl.jinja", "inputs": { "maxKeep": "maxKeep" } },
721
+ "bindings": "parallelCompact",
722
+ "dispatch": { "x": 1 }
723
+ }
724
+ ]
725
+ },
726
  {
727
  "id": "groups_parallel",
728
  "priority": 25,
build/webgpu/metadata.json CHANGED
@@ -1,20 +1,21 @@
1
  {
2
  "name": "ai.onnx.NonMaxSuppression",
3
- "id": "_ai_onnx_nonmaxsuppression_webgpu_d44f6ed",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "QfNxa5siC+dAWXaBHejTokSn3gllrgjOelQ0UNQlxEE=",
11
- "manifest.json": "vZuoGvM3nYg4zijLh8n62me7YOGuyRElAeZQelssago=",
12
- "nms-groups-compact.wgsl.jinja": "4z2+LMPnLoFRYfgH4bMN9D9eQw/besrivDzeM81JI5c=",
13
- "nms-groups-select.wgsl.jinja": "OKea1RdhUe0+cDFuFGIrx32rOxnRbUxgDJA+0FHIHt8=",
14
- "non-max-suppression.wgsl.jinja": "D4xiNb1cSvy5W1TjKi3jp4zA/H6besnr0zfsh3OAk+Q=",
15
- "test.json": "ehZ2AurQHFMhpSAmDim7zkCfdJL/IdB6VaCVCakwAMY="
 
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.NonMaxSuppression" }
20
  }
 
1
  {
2
  "name": "ai.onnx.NonMaxSuppression",
3
+ "id": "_ai_onnx_nonmaxsuppression_webgpu_7b023d1",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "J+izPem2HPnKTPaigtwj4dAVhN+zxO/L0S0a9ISi3Lc=",
11
+ "manifest.json": "xCHgSH5g6FOnX97P1CWWiuhGBA+HUT8uAlffDx4Lpv4=",
12
+ "nms-bitmask.wgsl.jinja": "1DpFLx6M5cSkG++rOrmF5ynIQv5cBYwBISiLy4ZnXqo=",
13
+ "nms-groups-compact.wgsl.jinja": "sxMP2Rsg3WNsZrnjS+sqK1DLcror9laJ056kifG1vtg=",
14
+ "nms-groups-select.wgsl.jinja": "mGgrizfXZ55mCm6nyWcUl7HnC2HI8dk0tsDobi6WNcs=",
15
+ "non-max-suppression.wgsl.jinja": "OGGmipiIE3O4HpLoXqz43q+/HFVuordD5tnL7VHEVzg=",
16
+ "test.json": "P5MoWNVu+EMZ9bWzzCYkFL3h1iXUidoFdd+ITSBNYiM="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
20
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.NonMaxSuppression" }
21
  }
build/webgpu/nms-bitmask.wgsl.jinja ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Bitmask NonMaxSuppression for low group counts. One workgroup per (batch,
2
+ // class) group cannot occupy the device when there are few groups, so the
3
+ // greedy loop's IoU work moves to a fully parallel pair pass instead:
4
+ //
5
+ // sort one workgroup per group orders candidate boxes by (score desc,
6
+ // index asc) in storage scratch, exactly the order the greedy
7
+ // argmax visits survivors;
8
+ // pairs every (rank i, 32-rank word) thread stores which higher ranks
9
+ // box i suppresses, so IoU cost is independent of group count;
10
+ // walk one workgroup per group replays the greedy scan over precomputed
11
+ // words: a rank is kept iff no earlier kept rank suppressed it,
12
+ // which reproduces greedy selection exactly.
13
+ //
14
+ // Scores compare by monotone IEEE key, so NaN scores are dropped up front --
15
+ // the sweep kernel can never choose them either. The kept list lands in the
16
+ // same `groupSel`/`groupCount` layout the shared compact pass consumes.
17
+ {{ env.wgsl.resourceDeclarations }}
18
+
19
+ const WG: u32 = {{ workgroupSize }}u;
20
+ const SORT_N: u32 = {{ source.sortN }}u;
21
+ {% if source.stage != "sort" %}
22
+ const MASK_WORDS: u32 = {{ source.maskWords }}u;
23
+ {% endif %}
24
+ const INVALID: u32 = 0xffffffffu;
25
+ {% if source.stage == "pairs" %}
26
+ const NUM_GROUPS: u32 = {{ source.numGroups }}u;
27
+ {% endif %}
28
+ {% if source.stage == "walk" %}
29
+ const MAX_KEEP: u32 = {{ source.maxKeep }}u;
30
+ {% endif %}
31
+ {% if source.stage != "walk" %}
32
+
33
+ {% set needIndexedIou = false %}
34
+ {% set needScoreAt = source.stage == "sort" %}
35
+ {% set needBoxIou = source.stage == "pairs" %}
36
+ {% set needScoreAt = needScoreAt is not defined or needScoreAt %}
37
+ {% set needBoxIou = needBoxIou is not defined or needBoxIou %}
38
+ {% if needBoxIou %}
39
+ fn box_offset(batch: u32, box: u32) -> u32 {
40
+ return (batch * params.boxesPerBatch + box) * 4u;
41
+ }
42
+
43
+ {% endif %}{% if needScoreAt %}
44
+ fn score_at(batch: u32, class_id: u32, box: u32) -> f32 {
45
+ return scores[(batch * params.classes + class_id) * params.boxesPerBatch + box];
46
+ }
47
+ {% endif %}{% if needBoxIou %}
48
+
49
+ fn box_corners(batch: u32, box: u32) -> vec4<f32> {
50
+ let offset = box_offset(batch, box);
51
+ let v0 = boxes[offset];
52
+ let v1 = boxes[offset + 1u];
53
+ let v2 = boxes[offset + 2u];
54
+ let v3 = boxes[offset + 3u];
55
+ if (params.centerPointBox != 0u) {
56
+ let x_center = v0;
57
+ let y_center = v1;
58
+ let half_w = v2 * 0.5;
59
+ let half_h = v3 * 0.5;
60
+ return vec4<f32>(y_center - half_h, x_center - half_w, y_center + half_h, x_center + half_w);
61
+ }
62
+ return vec4<f32>(v0, v1, v2, v3);
63
+ }
64
+
65
+ fn iou_with_box(batch: u32, a: u32, b_box: vec4<f32>) -> f32 {
66
+ let a_box = box_corners(batch, a);
67
+ let ay1_raw = a_box.x;
68
+ let ax1_raw = a_box.y;
69
+ let ay2_raw = a_box.z;
70
+ let ax2_raw = a_box.w;
71
+ let by1_raw = b_box.x;
72
+ let bx1_raw = b_box.y;
73
+ let by2_raw = b_box.z;
74
+ let bx2_raw = b_box.w;
75
+ let ay1 = min(ay1_raw, ay2_raw);
76
+ let ax1 = min(ax1_raw, ax2_raw);
77
+ let ay2 = max(ay1_raw, ay2_raw);
78
+ let ax2 = max(ax1_raw, ax2_raw);
79
+ let by1 = min(by1_raw, by2_raw);
80
+ let bx1 = min(bx1_raw, bx2_raw);
81
+ let by2 = max(by1_raw, by2_raw);
82
+ let bx2 = max(bx1_raw, bx2_raw);
83
+ let inter_y1 = max(ay1, by1);
84
+ let inter_x1 = max(ax1, bx1);
85
+ let inter_y2 = min(ay2, by2);
86
+ let inter_x2 = min(ax2, bx2);
87
+ let inter = max(0.0, inter_y2 - inter_y1) * max(0.0, inter_x2 - inter_x1);
88
+ let area_a = max(0.0, ay2 - ay1) * max(0.0, ax2 - ax1);
89
+ let area_b = max(0.0, by2 - by1) * max(0.0, bx2 - bx1);
90
+ let denom = area_a + area_b - inter;
91
+ if (denom <= 0.0) { return 0.0; }
92
+ return inter / denom;
93
+ }
94
+ {% if needIndexedIou %}
95
+
96
+ fn iou(batch: u32, a: u32, b: u32) -> f32 {
97
+ return iou_with_box(batch, a, box_corners(batch, b));
98
+ }
99
+ {% endif %}{% endif %}
100
+
101
+ {% endif %}
102
+ {% if source.stage == "sort" %}
103
+
104
+ @compute @workgroup_size(WG, 1, 1)
105
+ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
106
+ let tid = lid.x;
107
+ let group = wid.x;
108
+ let batch = group / params.classes;
109
+ let class_id = group % params.classes;
110
+ let base = group * SORT_N;
111
+ let score_threshold_value = score_threshold[0];
112
+
113
+ // Candidate keys: ~(monotone IEEE fold) sorts ascending keys as descending
114
+ // scores; sub-threshold, NaN, and padding slots share the maximum key and an
115
+ // INVALID index, so they sort strictly after every candidate.
116
+ for (var r = tid; r < SORT_N; r = r + WG) {
117
+ var key = 0xffffffffu;
118
+ var idx = INVALID;
119
+ if (r < params.boxesPerBatch) {
120
+ let s = score_at(batch, class_id, r);
121
+ let b = bitcast<u32>(s);
122
+ let is_nan = (b & 0x7f800000u) == 0x7f800000u && (b & 0x007fffffu) != 0u;
123
+ if (!is_nan && s > score_threshold_value) {
124
+ let folded = select(b | 0x80000000u, ~b, (b & 0x80000000u) != 0u);
125
+ key = ~folded;
126
+ idx = r;
127
+ }
128
+ }
129
+ sortKey[base + r] = key;
130
+ sortIdx[base + r] = idx;
131
+ }
132
+ storageBarrier();
133
+
134
+ // Bitonic network over this group's slice. Groups touch disjoint ranges, so
135
+ // the storage barriers only order this workgroup's own lanes.
136
+ var size = 2u;
137
+ loop {
138
+ if (size > SORT_N) { break; }
139
+ var stride = size / 2u;
140
+ loop {
141
+ if (stride == 0u) { break; }
142
+ for (var k = tid; k < SORT_N; k = k + WG) {
143
+ let partner = k ^ stride;
144
+ if (partner > k) {
145
+ let ka = sortKey[base + k];
146
+ let kb = sortKey[base + partner];
147
+ let ia = sortIdx[base + k];
148
+ let ib = sortIdx[base + partner];
149
+ let ascending = (k & size) == 0u;
150
+ let kBeforeP = ka < kb || (ka == kb && ia <= ib);
151
+ let needSwap = select(kBeforeP, !kBeforeP, ascending);
152
+ if (needSwap) {
153
+ sortKey[base + k] = kb;
154
+ sortKey[base + partner] = ka;
155
+ sortIdx[base + k] = ib;
156
+ sortIdx[base + partner] = ia;
157
+ }
158
+ }
159
+ }
160
+ storageBarrier();
161
+ stride = stride / 2u;
162
+ }
163
+ size = size * 2u;
164
+ }
165
+ }
166
+ {% elif source.stage == "pairs" %}
167
+
168
+ @compute @workgroup_size(WG, 1, 1)
169
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
170
+ let rowWords = params.boxesPerBatch * MASK_WORDS;
171
+ let total = NUM_GROUPS * rowWords;
172
+ let t = gid.x;
173
+ if (t >= total) { return; }
174
+ let group = t / rowWords;
175
+ let rem = t % rowWords;
176
+ let i = rem / MASK_WORDS;
177
+ let word = rem % MASK_WORDS;
178
+ let base = group * SORT_N;
179
+ let batch = group / params.classes;
180
+
181
+ var bits = 0u;
182
+ let idx_i = sortIdx[base + i];
183
+ if (idx_i != INVALID) {
184
+ let corners_i = box_corners(batch, idx_i);
185
+ let iou_threshold_value = iou_threshold[0];
186
+ let j_start = word * 32u;
187
+ for (var b = 0u; b < 32u; b = b + 1u) {
188
+ let j = j_start + b;
189
+ if (j > i && j < params.boxesPerBatch) {
190
+ let idx_j = sortIdx[base + j];
191
+ if (idx_j != INVALID && iou_with_box(batch, idx_j, corners_i) > iou_threshold_value) {
192
+ bits = bits | (1u << b);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ pairMask[(base + i) * MASK_WORDS + word] = bits;
198
+ }
199
+ {% else %}
200
+
201
+ const WINDOW_WORDS: u32 = WG / 32u;
202
+
203
+ var<workgroup> running: array<atomic<u32>, MASK_WORDS>;
204
+ var<workgroup> admissible: array<u32, WG>;
205
+ var<workgroup> windowSlices: array<u32, WG * WINDOW_WORDS>;
206
+ var<workgroup> picks: array<u32, WG>;
207
+ var<workgroup> wgPickCount: u32;
208
+
209
+ @compute @workgroup_size(WG, 1, 1)
210
+ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
211
+ let tid = lid.x;
212
+ let group = wid.x;
213
+ let base = group * SORT_N;
214
+ let selBase = group * MAX_KEEP;
215
+ let max_per_class = min(max_output_boxes_per_class[0], MAX_KEEP);
216
+
217
+ for (var w = tid; w < MASK_WORDS; w = w + WG) {
218
+ atomicStore(&running[w], 0u);
219
+ }
220
+
221
+ // Replay greedy selection one WG-rank window at a time. Lanes pre-test their
222
+ // rank against the running words (stable between windows), one lane resolves
223
+ // the window serially through the pair-mask window slice -- the only place
224
+ // rank order matters -- and the kept rows fold into the running words in a
225
+ // single barrier-free pass with each lane owning its words.
226
+ var kept = 0u;
227
+ var window_base = 0u;
228
+ loop {
229
+ if (kept >= max_per_class || window_base >= params.boxesPerBatch) { break; }
230
+ workgroupBarrier();
231
+ let r = window_base + tid;
232
+ var open = 0u;
233
+ if (r < params.boxesPerBatch) {
234
+ let idx_r = sortIdx[base + r];
235
+ if (idx_r != INVALID && (atomicLoad(&running[r / 32u]) & (1u << (r % 32u))) == 0u) {
236
+ open = 1u;
237
+ }
238
+ }
239
+ admissible[tid] = open;
240
+ // Stage every window rank's pair-mask window slice cooperatively, so the
241
+ // serial resolution below reads workgroup memory instead of chasing
242
+ // dependent global loads.
243
+ let sliceBase = (base + window_base) * MASK_WORDS + window_base / 32u;
244
+ for (var i = tid; i < WG * WINDOW_WORDS; i = i + WG) {
245
+ let t = i / WINDOW_WORDS;
246
+ let w = i % WINDOW_WORDS;
247
+ var words = 0u;
248
+ if (window_base + t < params.boxesPerBatch) {
249
+ words = pairMask[sliceBase + t * MASK_WORDS + w];
250
+ }
251
+ windowSlices[t * WINDOW_WORDS + w] = words;
252
+ }
253
+ workgroupBarrier();
254
+
255
+ if (tid == 0u) {
256
+ // Serial window resolution: a window rank is kept iff it survives the
257
+ // running mask (admissible) and no earlier kept rank of this same
258
+ // window suppresses it, read straight from its pair-mask row's slice.
259
+ var window_mask: array<u32, WINDOW_WORDS>;
260
+ for (var w = 0u; w < WINDOW_WORDS; w = w + 1u) { window_mask[w] = 0u; }
261
+ var found = 0u;
262
+ for (var t = 0u; t < WG && kept + found < max_per_class; t = t + 1u) {
263
+ if (admissible[t] == 0u) { continue; }
264
+ if ((window_mask[t / 32u] & (1u << (t % 32u))) != 0u) { continue; }
265
+ let pick = window_base + t;
266
+ picks[found] = pick;
267
+ groupSel[selBase + kept + found] = sortIdx[base + pick];
268
+ found = found + 1u;
269
+ for (var w = 0u; w < WINDOW_WORDS; w = w + 1u) {
270
+ window_mask[w] = window_mask[w] | windowSlices[t * WINDOW_WORDS + w];
271
+ }
272
+ }
273
+ wgPickCount = found;
274
+ }
275
+ let found = workgroupUniformLoad(&wgPickCount);
276
+ kept = kept + found;
277
+
278
+ // Fold every kept row of this window into the running words over the
279
+ // whole flat (row, word) grid: independent loads pipeline across lanes,
280
+ // and workgroup atomicOr makes concurrent same-word folds safe.
281
+ for (var i = tid; i < found * MASK_WORDS; i = i + WG) {
282
+ let k = i / MASK_WORDS;
283
+ let w = i % MASK_WORDS;
284
+ atomicOr(&running[w], pairMask[(base + picks[k]) * MASK_WORDS + w]);
285
+ }
286
+ window_base = window_base + WG;
287
+ }
288
+
289
+ if (tid == 0u) { groupCount[group] = kept; }
290
+ }
291
+ {% endif %}
build/webgpu/nms-groups-compact.wgsl.jinja CHANGED
@@ -9,34 +9,31 @@
9
  const WG: u32 = {{ workgroupSize }}u;
10
  const MAX_KEEP: u32 = {{ source.maxKeep }}u;
11
 
12
- var<workgroup> wgTotal: u32;
13
-
14
  @compute @workgroup_size(WG, 1, 1)
15
  fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
16
  let tid = lid.x;
17
  let numGroups = params.batches * params.classes;
18
 
19
- if (tid == 0u) {
20
- var total = 0u;
21
- for (var group = 0u; group < numGroups; group = group + 1u) {
22
- let batch = group / params.classes;
23
- let class_id = group % params.classes;
24
- let selBase = group * MAX_KEEP;
25
- let n = groupCount[group];
26
- for (var k = 0u; k < n; k = k + 1u) {
27
- if (total < params.capacity) {
28
- let box = groupSel[selBase + k];
29
- let row = total * 3u;
30
- selected_indices[row] = batch;
31
- selected_indices[row + 1u] = class_id;
32
- selected_indices[row + 2u] = box;
33
- }
34
- total = total + 1u;
35
  }
36
  }
37
- wgTotal = total;
38
  }
39
- let total = workgroupUniformLoad(&wgTotal);
40
 
41
  // Initialize any defensive tail rows to (0,0,0).
42
  let written = min(total, params.capacity);
 
9
  const WG: u32 = {{ workgroupSize }}u;
10
  const MAX_KEEP: u32 = {{ source.maxKeep }}u;
11
 
 
 
12
  @compute @workgroup_size(WG, 1, 1)
13
  fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
14
  let tid = lid.x;
15
  let numGroups = params.batches * params.classes;
16
 
17
+ // Every lane walks the same group-count prefix (uniform, cache-served), so
18
+ // the running offset needs no shared state while each group's rows copy in
19
+ // parallel WG-wide strides.
20
+ var total = 0u;
21
+ for (var group = 0u; group < numGroups; group = group + 1u) {
22
+ let batch = group / params.classes;
23
+ let class_id = group % params.classes;
24
+ let selBase = group * MAX_KEEP;
25
+ let n = groupCount[group];
26
+ for (var k = tid; k < n; k = k + WG) {
27
+ if (total + k < params.capacity) {
28
+ let box = groupSel[selBase + k];
29
+ let row = (total + k) * 3u;
30
+ selected_indices[row] = batch;
31
+ selected_indices[row + 1u] = class_id;
32
+ selected_indices[row + 2u] = box;
33
  }
34
  }
35
+ total = total + n;
36
  }
 
37
 
38
  // Initialize any defensive tail rows to (0,0,0).
39
  let written = min(total, params.capacity);
build/webgpu/nms-groups-select.wgsl.jinja CHANGED
@@ -16,13 +16,18 @@ const WG: u32 = {{ workgroupSize }}u;
16
  const MAX_KEEP: u32 = {{ source.maxKeep }}u;
17
  const INVALID: u32 = 0xffffffffu;
18
 
 
 
 
19
  fn box_offset(batch: u32, box: u32) -> u32 {
20
  return (batch * params.boxesPerBatch + box) * 4u;
21
  }
22
 
 
23
  fn score_at(batch: u32, class_id: u32, box: u32) -> f32 {
24
  return scores[(batch * params.classes + class_id) * params.boxesPerBatch + box];
25
  }
 
26
 
27
  fn box_corners(batch: u32, box: u32) -> vec4<f32> {
28
  let offset = box_offset(batch, box);
@@ -69,6 +74,7 @@ fn iou_with_box(batch: u32, a: u32, b_box: vec4<f32>) -> f32 {
69
  if (denom <= 0.0) { return 0.0; }
70
  return inter / denom;
71
  }
 
72
 
73
 
74
  var<workgroup> wgScore: array<f32, WG>;
 
16
  const MAX_KEEP: u32 = {{ source.maxKeep }}u;
17
  const INVALID: u32 = 0xffffffffu;
18
 
19
+ {% set needScoreAt = needScoreAt is not defined or needScoreAt %}
20
+ {% set needBoxIou = needBoxIou is not defined or needBoxIou %}
21
+ {% if needBoxIou %}
22
  fn box_offset(batch: u32, box: u32) -> u32 {
23
  return (batch * params.boxesPerBatch + box) * 4u;
24
  }
25
 
26
+ {% endif %}{% if needScoreAt %}
27
  fn score_at(batch: u32, class_id: u32, box: u32) -> f32 {
28
  return scores[(batch * params.classes + class_id) * params.boxesPerBatch + box];
29
  }
30
+ {% endif %}{% if needBoxIou %}
31
 
32
  fn box_corners(batch: u32, box: u32) -> vec4<f32> {
33
  let offset = box_offset(batch, box);
 
74
  if (denom <= 0.0) { return 0.0; }
75
  return inter / denom;
76
  }
77
+ {% endif %}
78
 
79
 
80
  var<workgroup> wgScore: array<f32, WG>;
build/webgpu/non-max-suppression.wgsl.jinja CHANGED
@@ -1,12 +1,17 @@
1
  {{ env.wgsl.resourceDeclarations }}
2
 
 
 
 
3
  fn box_offset(batch: u32, box: u32) -> u32 {
4
  return (batch * params.boxesPerBatch + box) * 4u;
5
  }
6
 
 
7
  fn score_at(batch: u32, class_id: u32, box: u32) -> f32 {
8
  return scores[(batch * params.classes + class_id) * params.boxesPerBatch + box];
9
  }
 
10
 
11
  fn box_corners(batch: u32, box: u32) -> vec4<f32> {
12
  let offset = box_offset(batch, box);
@@ -57,6 +62,7 @@ fn iou_with_box(batch: u32, a: u32, b_box: vec4<f32>) -> f32 {
57
  fn iou(batch: u32, a: u32, b: u32) -> f32 {
58
  return iou_with_box(batch, a, box_corners(batch, b));
59
  }
 
60
 
61
 
62
  fn selected_box(row: u32) -> u32 {
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
+ {% set needScoreAt = needScoreAt is not defined or needScoreAt %}
4
+ {% set needBoxIou = needBoxIou is not defined or needBoxIou %}
5
+ {% if needBoxIou %}
6
  fn box_offset(batch: u32, box: u32) -> u32 {
7
  return (batch * params.boxesPerBatch + box) * 4u;
8
  }
9
 
10
+ {% endif %}{% if needScoreAt %}
11
  fn score_at(batch: u32, class_id: u32, box: u32) -> f32 {
12
  return scores[(batch * params.classes + class_id) * params.boxesPerBatch + box];
13
  }
14
+ {% endif %}{% if needBoxIou %}
15
 
16
  fn box_corners(batch: u32, box: u32) -> vec4<f32> {
17
  let offset = box_offset(batch, box);
 
62
  fn iou(batch: u32, a: u32, b: u32) -> f32 {
63
  return iou_with_box(batch, a, box_corners(batch, b));
64
  }
65
+ {% endif %}
66
 
67
 
68
  fn selected_box(row: u32) -> u32 {
build/webgpu/test.json CHANGED
@@ -1174,6 +1174,117 @@
1174
  "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } }
1175
  },
1176
  "outputs": { "selected_indices": { "dtype": "uint32", "shape": [0, 3], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
  }
1178
  ]
1179
  }
 
1174
  "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } }
1175
  },
1176
  "outputs": { "selected_indices": { "dtype": "uint32", "shape": [0, 3], "tolerance": 0 } }
1177
+ },
1178
+ {
1179
+ "name": "bitmask_1class_2048_grid_sorted_keep128",
1180
+ "provenance": {
1181
+ "notes": "2048 disjoint linspace boxes with ascending scores route to the bitmask variant; zero overlaps make selection the top-128 scores, so the sorted walk must emit indices in descending index order."
1182
+ },
1183
+ "attrs": { "center_point_box": 0 },
1184
+ "inputs": {
1185
+ "boxes": {
1186
+ "dtype": "float32",
1187
+ "shape": [1, 2048, 4],
1188
+ "data": { "kind": "linspace", "start": 0.0, "end": 8191.0 }
1189
+ },
1190
+ "scores": {
1191
+ "dtype": "float32",
1192
+ "shape": [1, 1, 2048],
1193
+ "data": { "kind": "linspace", "start": 0.1, "end": 1.0 }
1194
+ },
1195
+ "max_output_boxes_per_class": { "dtype": "uint32", "shape": [], "data": { "kind": "values", "values": [128] } },
1196
+ "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } },
1197
+ "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.05] } }
1198
+ },
1199
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [128, 3], "tolerance": 0 } }
1200
+ },
1201
+ {
1202
+ "name": "bitmask_1class_4096_two_positions_collapse",
1203
+ "provenance": {
1204
+ "notes": "4096 boxes cycling two identical positions collapse under the pair mask to the best-scored box of each position; descending scores make those boxes 0 and 1."
1205
+ },
1206
+ "attrs": { "center_point_box": 0 },
1207
+ "inputs": {
1208
+ "boxes": {
1209
+ "dtype": "float32",
1210
+ "shape": [1, 4096, 4],
1211
+ "data": { "kind": "cycle", "values": [0.0, 0.0, 10.0, 10.0, 100.0, 100.0, 110.0, 110.0] }
1212
+ },
1213
+ "scores": {
1214
+ "dtype": "float32",
1215
+ "shape": [1, 1, 4096],
1216
+ "data": { "kind": "linspace", "start": 1.0, "end": 0.001 }
1217
+ },
1218
+ "max_output_boxes_per_class": { "dtype": "uint32", "shape": [], "data": { "kind": "values", "values": [4096] } },
1219
+ "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } },
1220
+ "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.0] } }
1221
+ },
1222
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [2, 3], "tolerance": 0 } }
1223
+ },
1224
+ {
1225
+ "name": "bitmask_2batch_2048_two_positions",
1226
+ "provenance": {
1227
+ "notes": "Two batches of 2048 two-position boxes give two groups on the bitmask route; each batch keeps its own pair of survivors, exercising per-group sort, mask, and walk offsets."
1228
+ },
1229
+ "attrs": { "center_point_box": 0 },
1230
+ "inputs": {
1231
+ "boxes": {
1232
+ "dtype": "float32",
1233
+ "shape": [2, 2048, 4],
1234
+ "data": { "kind": "cycle", "values": [0.0, 0.0, 10.0, 10.0, 100.0, 100.0, 110.0, 110.0] }
1235
+ },
1236
+ "scores": {
1237
+ "dtype": "float32",
1238
+ "shape": [2, 1, 2048],
1239
+ "data": { "kind": "linspace", "start": 1.0, "end": 0.001 }
1240
+ },
1241
+ "max_output_boxes_per_class": { "dtype": "uint32", "shape": [], "data": { "kind": "values", "values": [2048] } },
1242
+ "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } },
1243
+ "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.0] } }
1244
+ },
1245
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [4, 3], "tolerance": 0 } }
1246
+ },
1247
+ {
1248
+ "name": "bitmask_1class_2048_all_tied_scores_keep64",
1249
+ "provenance": {
1250
+ "notes": "All 2048 disjoint boxes share one score, so ordering is decided purely by the index tie-break; the walk must keep boxes 0..63 exactly."
1251
+ },
1252
+ "attrs": { "center_point_box": 0 },
1253
+ "inputs": {
1254
+ "boxes": {
1255
+ "dtype": "float32",
1256
+ "shape": [1, 2048, 4],
1257
+ "data": { "kind": "linspace", "start": 0.0, "end": 8191.0 }
1258
+ },
1259
+ "scores": { "dtype": "float32", "shape": [1, 1, 2048], "data": { "kind": "cycle", "values": [0.75] } },
1260
+ "max_output_boxes_per_class": { "dtype": "uint32", "shape": [], "data": { "kind": "values", "values": [64] } },
1261
+ "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } },
1262
+ "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.0] } }
1263
+ },
1264
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [64, 3], "tolerance": 0 } }
1265
+ },
1266
+ {
1267
+ "name": "bitmask_1class_2048_threshold_half_keep32",
1268
+ "provenance": {
1269
+ "notes": "A 0.55 score threshold admits just under half of 2048 disjoint linspace-scored boxes and `max_output_boxes_per_class` clamps the kept prefix to 32 on the bitmask route."
1270
+ },
1271
+ "attrs": { "center_point_box": 0 },
1272
+ "inputs": {
1273
+ "boxes": {
1274
+ "dtype": "float32",
1275
+ "shape": [1, 2048, 4],
1276
+ "data": { "kind": "linspace", "start": 0.0, "end": 8191.0 }
1277
+ },
1278
+ "scores": {
1279
+ "dtype": "float32",
1280
+ "shape": [1, 1, 2048],
1281
+ "data": { "kind": "linspace", "start": 0.1, "end": 1.0 }
1282
+ },
1283
+ "max_output_boxes_per_class": { "dtype": "uint32", "shape": [], "data": { "kind": "values", "values": [32] } },
1284
+ "iou_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.5] } },
1285
+ "score_threshold": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [0.55] } }
1286
+ },
1287
+ "outputs": { "selected_indices": { "dtype": "uint32", "shape": [32, 3], "tolerance": 0 } }
1288
  }
1289
  ]
1290
  }