sync c928d21e6cc1
Browse files- README.md +1 -0
- build/webgpu/bench.json +9 -1
- build/webgpu/manifest.json +160 -2
- build/webgpu/metadata.json +9 -8
- build/webgpu/nms-bitmask.wgsl.jinja +291 -0
- build/webgpu/nms-groups-compact.wgsl.jinja +17 -20
- build/webgpu/nms-groups-select.wgsl.jinja +6 -0
- build/webgpu/non-max-suppression.wgsl.jinja +6 -0
- build/webgpu/test.json +111 -0
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": "
|
| 4 |
"version": 1,
|
| 5 |
"license": "Apache-2.0",
|
| 6 |
"backend": { "type": "webgpu" },
|
| 7 |
"digest": {
|
| 8 |
"algorithm": "sha256",
|
| 9 |
"files": {
|
| 10 |
-
"bench.json": "
|
| 11 |
-
"manifest.json": "
|
| 12 |
-
"nms-
|
| 13 |
-
"nms-groups-
|
| 14 |
-
"
|
| 15 |
-
"
|
|
|
|
| 16 |
}
|
| 17 |
},
|
| 18 |
-
"provenance": { "kernel": { "sha": "
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
}
|
| 36 |
}
|
| 37 |
-
|
| 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 |
}
|