Xenova HF Staff commited on
Commit
569bc8d
·
verified ·
1 Parent(s): 630ffd4

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,92 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # ai.onnx.Unique
10
+
11
+ `ai.onnx` · standard ONNX operator · ONNX opset ≥ 11
12
+
13
+ ## Description
14
+
15
+ Finds unique values or subtensors along an optional `axis`. Without an axis, `X` is flattened; results are sorted or retain first-occurrence order. Sub-32-bit integers and booleans use lossless widened 32-bit storage. Metadata outputs remain logical int64 but use lossless uint32 storage because all values are bounded by an addressable tensor extent. Callers supply exact data-dependent output shapes. ONNX-permitted uint16, 64-bit, string, and complex inputs remain unsupported because the runtime lacks matching WebGPU storage.
16
+
17
+ See the [ONNX `Unique` spec](https://onnx.ai/onnx/operators/onnx__Unique.html) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `X` | `x` | `T` | — | — | The N-D input tensor from which unique values or subtensors are extracted. When `axis` is omitted, tensors of any rank are flattened in row-major order. | required |
24
+
25
+ ## Outputs
26
+
27
+ | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
28
+ | --- | --- | --- | --- | --- | --- | --- | --- |
29
+ | `Y` | `y` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | derived | — | Tensor containing all unique values or subtensors of X, sorted or in first-occurrence order. | required |
30
+ | `indices` | `indices` | `I` | `uint32` | `1` | — | Optional logical int64 indices of each `Y` value or slice's first occurrence in `X`; stored as bounded uint32 values by WebGPU. | optional |
31
+ | `inverse_indices` | `inverse_indices` | `I` | `uint32` | `1` | — | Optional logical int64 mapping from each flattened input value, or each input-axis slice, to its corresponding index in `Y`; stored as bounded uint32 values by WebGPU. | optional |
32
+ | `counts` | `counts` | `I` | `uint32` | `1` | — | Optional logical int64 occurrence count for each unique value or slice in `Y`; stored as bounded uint32 values by WebGPU. | optional |
33
+
34
+ ## Attributes
35
+
36
+ Attributes and default values (overridable per request):
37
+
38
+ | Attribute | Default | Description |
39
+ | --- | --- | --- |
40
+ | `sorted` | `1` | Whether to sort unique elements in ascending order before output; 1 (default) sorts, 0 retains first-occurrence order. |
41
+ | `axis` | — | Optional axis along which unique subtensors are identified. Negative values count from the back; when omitted, the input is flattened. |
42
+
43
+ ## Type constraints
44
+
45
+ | Variable | Allowed dtypes |
46
+ | --- | --- |
47
+ | `T` | `float32`, `float16`, `uint32`, `int32`, `int16`, `uint8`, `int8`, `bool` |
48
+ | `I` | `int64` |
49
+
50
+ ## Files
51
+
52
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
53
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
54
+ - [`test.json`](build/webgpu/test.json) — correctness cases
55
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
56
+ - [`unique-axis-compact-sort.wgsl.jinja`](build/webgpu/unique-axis-compact-sort.wgsl.jinja)
57
+ - [`unique-axis-dedup.wgsl.jinja`](build/webgpu/unique-axis-dedup.wgsl.jinja)
58
+ - [`unique-axis-hash.wgsl.jinja`](build/webgpu/unique-axis-hash.wgsl.jinja)
59
+ - [`unique-axis-scatter.wgsl.jinja`](build/webgpu/unique-axis-scatter.wgsl.jinja)
60
+ - [`unique-axis.wgsl.jinja`](build/webgpu/unique-axis.wgsl.jinja)
61
+ - [`unique-compact-sort.wgsl.jinja`](build/webgpu/unique-compact-sort.wgsl.jinja)
62
+ - [`unique-dedup.wgsl.jinja`](build/webgpu/unique-dedup.wgsl.jinja)
63
+ - [`unique-hash-build.wgsl.jinja`](build/webgpu/unique-hash-build.wgsl.jinja)
64
+ - [`unique-hash-collect.wgsl.jinja`](build/webgpu/unique-hash-collect.wgsl.jinja)
65
+ - [`unique-hash-init.wgsl.jinja`](build/webgpu/unique-hash-init.wgsl.jinja)
66
+ - [`unique-hash-mark.wgsl.jinja`](build/webgpu/unique-hash-mark.wgsl.jinja)
67
+ - [`unique-hash-sort-collected-key-only.wgsl.jinja`](build/webgpu/unique-hash-sort-collected-key-only.wgsl.jinja)
68
+ - [`unique.wgsl.jinja`](build/webgpu/unique.wgsl.jinja)
69
+
70
+ ## Use with `@huggingface/kernels`
71
+
72
+ The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
73
+
74
+ The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
75
+
76
+ - `y`
77
+
78
+ Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
79
+
80
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
81
+
82
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
83
+
84
+ ```js
85
+ import { getKernel } from "@huggingface/kernels";
86
+
87
+ const kernel = await getKernel("webgpu-kernels/ai.onnx.Unique", { version: 1 });
88
+ // Explicit destinations request optional results or supply metadata that cannot be inferred.
89
+ const { y } = await kernel({ x: { data: xData, shape: [1] } }, {
90
+ outputs: { y: { shape: [1], dtype: "float32" } },
91
+ });
92
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.Unique",
3
+ "cases": [
4
+ {
5
+ "name": "int32-64k-moderate-distinct-sorted",
6
+ "preset": "smoke",
7
+ "inputs": { "x": { "dtype": "int32", "shape": [65536], "dist": "linearMod", "mod": 512 } },
8
+ "outputs": { "y": { "dtype": "int32", "shape": [512] } },
9
+ "bench": {
10
+ "primary": true,
11
+ "metrics": [
12
+ {
13
+ "type": "bandwidth",
14
+ "name": "hash-init-build-collect-sort traffic",
15
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
16
+ }
17
+ ]
18
+ }
19
+ },
20
+ {
21
+ "name": "int32-1024-all-equal",
22
+ "inputs": { "x": { "dtype": "int32", "shape": [1024], "data": { "kind": "constant", "value": 7 } } },
23
+ "outputs": { "y": { "dtype": "int32", "shape": [1] } }
24
+ },
25
+ {
26
+ "name": "int32-256k-1024-distinct-sorted",
27
+ "preset": "smoke",
28
+ "inputs": { "x": { "dtype": "int32", "shape": [262144], "dist": "linearMod", "mod": 1024 } },
29
+ "outputs": { "y": { "dtype": "int32", "shape": [1024] } },
30
+ "bench": {
31
+ "primary": true,
32
+ "metrics": [
33
+ {
34
+ "type": "bandwidth",
35
+ "name": "hash-init-build-collect-sort traffic",
36
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
37
+ }
38
+ ]
39
+ }
40
+ },
41
+ {
42
+ "name": "int32-8k-64-distinct-sorted",
43
+ "preset": "smoke",
44
+ "inputs": {
45
+ "x": {
46
+ "dtype": "int32",
47
+ "shape": [8192],
48
+ "data": {
49
+ "kind": "cycle",
50
+ "values": [37, -5, 12, 99, -73, 0, 41, 8, -21, 64, 3, -90, 55, 17, -2, 76, 29, -48, 83, 6, -33, 92, 14, -67, 50, 22, -9, 70, 35, -58, 88, 1, -26, 95, 19, -81, 46, 27, -14, 61, 33, -44, 79, 10, -39, 53, 24, -86, 100, -100, 11, -11, 13, -13, 47, -47, 59, -59, 71, -71, 89, -89, 97, -97]
51
+ }
52
+ }
53
+ },
54
+ "outputs": { "y": { "dtype": "int32", "shape": [64] } },
55
+ "bench": {
56
+ "primary": true,
57
+ "metrics": [
58
+ {
59
+ "type": "bandwidth",
60
+ "name": "hash-init-build-collect-sort traffic",
61
+ "value": "12 * pow2ceil(2 * numel(shapes.x)) + 32 * numel(shapes.x) + 16 * numel(shapes.y)"
62
+ }
63
+ ]
64
+ }
65
+ },
66
+ {
67
+ "name": "float32-262144-8-distinct-serial",
68
+ "preset": "smoke",
69
+ "inputs": {
70
+ "x": {
71
+ "dtype": "float32",
72
+ "shape": [262144],
73
+ "data": { "kind": "cycle", "values": [-4.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 8.0] }
74
+ }
75
+ },
76
+ "outputs": { "y": { "dtype": "float32", "shape": [8] } },
77
+ "bench": {
78
+ "primary": true,
79
+ "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + numel(shapes.y))" }]
80
+ }
81
+ },
82
+ {
83
+ "name": "int32-262144-900-distinct-hash",
84
+ "preset": "smoke",
85
+ "inputs": { "x": { "dtype": "int32", "shape": [262144], "dist": "linearMod", "mod": 900 } },
86
+ "outputs": { "y": { "dtype": "int32", "shape": [900] } },
87
+ "bench": {
88
+ "primary": true,
89
+ "metrics": [
90
+ {
91
+ "type": "bandwidth",
92
+ "name": "hash-init-build-collect-sort traffic",
93
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ {
99
+ "name": "int32-262144-2049-distinct-large-output-hash",
100
+ "preset": "smoke",
101
+ "inputs": { "x": { "dtype": "int32", "shape": [262144], "dist": "linearMod", "mod": 2049 } },
102
+ "outputs": { "y": { "dtype": "int32", "shape": [2049] } },
103
+ "bench": {
104
+ "primary": true,
105
+ "metrics": [
106
+ {
107
+ "type": "bandwidth",
108
+ "name": "hash-init-build-collect-sort traffic",
109
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
110
+ }
111
+ ]
112
+ }
113
+ },
114
+ {
115
+ "name": "axis0-f32-4096x256-all-distinct",
116
+ "preset": "smoke",
117
+ "attrs": { "axis": 0, "sorted": 1 },
118
+ "inputs": {
119
+ "x": { "dtype": "float32", "shape": [4096, 256], "dist": "uniform", "seed": 1531, "scale": 8, "offset": 0 }
120
+ },
121
+ "outputs": { "y": { "dtype": "float32", "shape": [4096, 256] } },
122
+ "bench": {
123
+ "primary": true,
124
+ "metrics": [
125
+ {
126
+ "type": "bandwidth",
127
+ "name": "axis-hash-sort-scatter traffic",
128
+ "value": "4 * numel(shapes.x) + 12 * numel(shapes.y) + 32 * dim(shapes.x, 0) + 8 * pow2ceil(2 * dim(shapes.x, 0)) + 8 * dim(shapes.y, 0) + 888 * pow2ceil(dim(shapes.y, 0))"
129
+ }
130
+ ]
131
+ }
132
+ },
133
+ {
134
+ "name": "axis_hash_split_scatter_unsorted_zero_fills_tail",
135
+ "preset": "smoke",
136
+ "attrs": { "axis": 0, "sorted": 0 },
137
+ "inputs": {
138
+ "x": { "dtype": "int32", "shape": [2048, 1], "data": { "kind": "cycle", "values": [9, -2, 7, 9, 42, -2] } }
139
+ },
140
+ "outputs": { "y": { "dtype": "int32", "shape": [4, 1] } },
141
+ "bench": {
142
+ "primary": true,
143
+ "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + numel(shapes.y))" }]
144
+ }
145
+ },
146
+ {
147
+ "name": "axis_hash_int32_inner2_duplicate_rows_sorted",
148
+ "preset": "smoke",
149
+ "attrs": { "axis": 0, "sorted": 1 },
150
+ "inputs": {
151
+ "x": { "dtype": "int32", "shape": [2048, 2], "data": { "kind": "cycle", "values": [2, 1, 0, 3, 2, 1, -1, 4] } }
152
+ },
153
+ "outputs": { "y": { "dtype": "int32", "shape": [3, 2] } },
154
+ "bench": {
155
+ "primary": true,
156
+ "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + numel(shapes.y) + dim(shapes.x, 0))" }]
157
+ }
158
+ },
159
+ {
160
+ "name": "axis0_large_y_int32_unsorted_2100_distinct",
161
+ "preset": "smoke",
162
+ "attrs": { "axis": 0, "sorted": 0 },
163
+ "inputs": {
164
+ "x": { "dtype": "int32", "shape": [2100, 1], "data": { "kind": "linspace", "start": -4000, "end": 4000 } }
165
+ },
166
+ "outputs": { "y": { "dtype": "int32", "shape": [2100, 1] } },
167
+ "bench": {
168
+ "primary": true,
169
+ "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + numel(shapes.y) + dim(shapes.x, 0))" }]
170
+ }
171
+ },
172
+ {
173
+ "name": "int32-16k-4096-distinct-large-output-hash",
174
+ "preset": "stress",
175
+ "inputs": { "x": { "dtype": "int32", "shape": [16384], "dist": "linearMod", "mod": 4096 } },
176
+ "outputs": { "y": { "dtype": "int32", "shape": [4096] } },
177
+ "bench": {
178
+ "primary": true,
179
+ "metrics": [
180
+ {
181
+ "type": "bandwidth",
182
+ "name": "hash-init-build-collect-sort traffic",
183
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
184
+ }
185
+ ]
186
+ }
187
+ },
188
+ {
189
+ "name": "axis0-f32-3000x8-all-distinct",
190
+ "preset": "stress",
191
+ "attrs": { "axis": 0, "sorted": 1 },
192
+ "inputs": {
193
+ "x": { "dtype": "float32", "shape": [3000, 8], "dist": "uniform", "seed": 2029, "scale": 64, "offset": 0 }
194
+ },
195
+ "outputs": { "y": { "dtype": "float32", "shape": [3000, 8] } },
196
+ "bench": { "primary": true, "metrics": [{ "type": "bandwidth", "value": "4 * numel(shapes.x)" }] }
197
+ },
198
+ {
199
+ "name": "axis0-f32-9000x1-storage-order",
200
+ "preset": "stress",
201
+ "attrs": { "axis": 0, "sorted": 1 },
202
+ "inputs": {
203
+ "x": { "dtype": "float32", "shape": [9000, 1], "data": { "kind": "linspace", "start": -4500.0, "end": 4499.0 } }
204
+ },
205
+ "outputs": { "y": { "dtype": "float32", "shape": [9000, 1] } },
206
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + dim(shapes.y, 0))" }] }
207
+ },
208
+ {
209
+ "name": "int32-262144-Y4096-token-vocab-sorted",
210
+ "preset": "stress",
211
+ "inputs": { "x": { "dtype": "int32", "shape": [262144], "dist": "linearMod", "mod": 4096 } },
212
+ "outputs": { "y": { "dtype": "int32", "shape": [4096] } },
213
+ "bench": {
214
+ "primary": true,
215
+ "metrics": [
216
+ {
217
+ "type": "bandwidth",
218
+ "name": "hash-init-build-collect-sort traffic",
219
+ "value": "8 * pow2ceil(2 * numel(shapes.x)) + 12 * numel(shapes.x) + 12 * numel(shapes.y)"
220
+ }
221
+ ]
222
+ }
223
+ },
224
+ {
225
+ "name": "int32-8192-all-distinct-unsorted",
226
+ "preset": "stress",
227
+ "attrs": { "sorted": 0 },
228
+ "inputs": { "x": { "dtype": "int32", "shape": [8192], "dist": "linearMod", "mod": 8192 } },
229
+ "outputs": { "y": { "dtype": "int32", "shape": [8192] } },
230
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "4 * (numel(shapes.x) + numel(shapes.y))" }] }
231
+ }
232
+ ]
233
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,1714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "ai.onnx",
3
+ "name": "Unique",
4
+ "sinceVersion": 11,
5
+ "description": "Finds unique values or subtensors along an optional `axis`. Without an axis, `X` is flattened; results are sorted or retain first-occurrence order. Sub-32-bit integers and booleans use lossless widened 32-bit storage. Metadata outputs remain logical int64 but use lossless uint32 storage because all values are bounded by an addressable tensor extent. Callers supply exact data-dependent output shapes. ONNX-permitted uint16, 64-bit, string, and complex inputs remain unsupported because the runtime lacks matching WebGPU storage.",
6
+ "inputs": [
7
+ {
8
+ "role": "X",
9
+ "dtype": "T",
10
+ "description": "The N-D input tensor from which unique values or subtensors are extracted. When `axis` is omitted, tensors of any rank are flattened in row-major order."
11
+ }
12
+ ],
13
+ "outputs": [
14
+ {
15
+ "role": "Y",
16
+ "dtype": "T",
17
+ "description": "Tensor containing all unique values or subtensors of X, sorted or in first-occurrence order.",
18
+ "rank": "ranks.X if has(attrs, \"axis\") else 1"
19
+ },
20
+ {
21
+ "role": "indices",
22
+ "dtype": "I",
23
+ "rank": 1,
24
+ "optional": true,
25
+ "description": "Optional logical int64 indices of each `Y` value or slice's first occurrence in `X`; stored as bounded uint32 values by WebGPU."
26
+ },
27
+ {
28
+ "role": "inverse_indices",
29
+ "dtype": "I",
30
+ "rank": 1,
31
+ "optional": true,
32
+ "description": "Optional logical int64 mapping from each flattened input value, or each input-axis slice, to its corresponding index in `Y`; stored as bounded uint32 values by WebGPU."
33
+ },
34
+ {
35
+ "role": "counts",
36
+ "dtype": "I",
37
+ "rank": 1,
38
+ "optional": true,
39
+ "description": "Optional logical int64 occurrence count for each unique value or slice in `Y`; stored as bounded uint32 values by WebGPU."
40
+ }
41
+ ],
42
+ "attributes": { "sorted": 1 },
43
+ "attributeDescriptions": {
44
+ "sorted": "Whether to sort unique elements in ascending order before output; 1 (default) sorts, 0 retains first-occurrence order.",
45
+ "axis": "Optional axis along which unique subtensors are identified. Negative values count from the back; when omitted, the input is flattened."
46
+ },
47
+ "attributeConstraints": { "sorted": { "values": [0, 1] } },
48
+ "typeConstraints": {
49
+ "T": ["float32", "float16", "uint32", "int32", "int16", "uint8", "int8", "bool"],
50
+ "I": ["int64"]
51
+ },
52
+ "args": {
53
+ "x": { "kind": "tensor", "semantic": "X", "role": "input" },
54
+ "y": { "kind": "tensor", "semantic": "Y", "role": "output" },
55
+ "indices": { "kind": "tensor", "semantic": "indices", "role": "output", "dtype": "uint32", "required": false },
56
+ "inverse_indices": {
57
+ "kind": "tensor",
58
+ "semantic": "inverse_indices",
59
+ "role": "output",
60
+ "dtype": "uint32",
61
+ "required": false
62
+ },
63
+ "counts": { "kind": "tensor", "semantic": "counts", "role": "output", "dtype": "uint32", "required": false }
64
+ },
65
+ "tunables": {
66
+ "WORKGROUP_SIZE": 256,
67
+ "AXIS_SERIAL_WG": 512,
68
+ "HASH_MIN_INPUT": 32768,
69
+ "HASH_MIN_I32_INPUT": 16384,
70
+ "LOCAL_SORT_CROSSOVER": 2048,
71
+ "HASH_SORT_MAX_CAPACITY": 4096,
72
+ "MAX_PARALLEL_INPUT": 1048576,
73
+ "GLOBAL_MAX_CAPACITY": 65536
74
+ },
75
+ "derive": {
76
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
77
+ "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
78
+ "axisPresent": "has(attrs, \"axis\")",
79
+ "normalizedAxis": "attrs.axis + ranks.X if axisPresent and attrs.axis < 0 else (attrs.axis if axisPresent else 0)",
80
+ "metadataAbsent": "not present.indices and not present.inverse_indices and not present.counts",
81
+ "metadataShapeContract": "(not present.indices or (ranks.indices == 1 and dim(shapes.indices, 0) == (dim(shapes.Y, normalizedAxis) if axisPresent else numel(shapes.Y)) and tensorDtypes.indices == \"uint32\")) and (not present.inverse_indices or (ranks.inverse_indices == 1 and dim(shapes.inverse_indices, 0) == (dim(shapes.X, normalizedAxis) if axisPresent else numel(shapes.X)) and tensorDtypes.inverse_indices == \"uint32\")) and (not present.counts or (ranks.counts == 1 and dim(shapes.counts, 0) == (dim(shapes.Y, normalizedAxis) if axisPresent else numel(shapes.Y)) and tensorDtypes.counts == \"uint32\"))",
82
+ "flatLayout": "not axisPresent and f16Ok(dtypes.T) and ranks.Y == 1 and numel(shapes.Y) <= numel(shapes.X)",
83
+ "axisLayout": "axisPresent and f16Ok(dtypes.T) and ranks.X >= 1 and attrs.axis + ranks.X >= 0 and attrs.axis < ranks.X and ranks.Y == ranks.X and sameShape(prefix(shapes.Y, normalizedAxis), prefix(shapes.X, normalizedAxis)) and sameShape(suffix(shapes.Y, normalizedAxis + 1), suffix(shapes.X, normalizedAxis + 1)) and dim(shapes.Y, normalizedAxis) <= dim(shapes.X, normalizedAxis)",
84
+ "flatContract": "flatLayout and metadataAbsent",
85
+ "axisContract": "axisLayout and metadataAbsent",
86
+ "flatMetadataContract": "flatLayout and not metadataAbsent and metadataShapeContract",
87
+ "axisMetadataContract": "axisLayout and not metadataAbsent and metadataShapeContract",
88
+ "flatScanBlocks": "ceilDiv(numel(shapes.X), tunables.WORKGROUP_SIZE)",
89
+ "flatTableSize": "pow2ceil(2 * numel(shapes.X))",
90
+ "flatTableBlocks": "ceilDiv(flatTableSize, tunables.WORKGROUP_SIZE)",
91
+ "flatTableVec4Blocks": "ceilDiv(flatTableSize, 4 * tunables.WORKGROUP_SIZE)",
92
+ "axisDim": "dim(shapes.X, normalizedAxis) if axisPresent else 0",
93
+ "axisOutputDim": "dim(shapes.Y, normalizedAxis) if axisPresent else 0",
94
+ "axisScanBlocks": "ceilDiv(axisDim, tunables.WORKGROUP_SIZE)",
95
+ "axisTableSize": "pow2ceil(2 * axisDim)",
96
+ "axisTableBlocks": "ceilDiv(axisTableSize, tunables.WORKGROUP_SIZE)",
97
+ "flatLocalStorageBytes": "(12 * pow2ceil(numel(shapes.Y)) if attrs.sorted != 0 else 4 * numel(shapes.Y)) + 4 * tunables.WORKGROUP_SIZE + 4",
98
+ "axisLocalStorageBytes": "(12 * pow2ceil(axisOutputDim) if attrs.sorted != 0 else 4 * pow2ceil(axisOutputDim)) + 4 * tunables.WORKGROUP_SIZE + 4",
99
+ "flatLocalSortFits": "flatLocalStorageBytes <= device.limits.maxComputeWorkgroupStorageSize",
100
+ "axisLocalSortFits": "axisLocalStorageBytes <= device.limits.maxComputeWorkgroupStorageSize",
101
+ "hashKeySortFits": "4 * pow2ceil(numel(shapes.Y)) <= device.limits.maxComputeWorkgroupStorageSize",
102
+ "flatScanScratchFits": "4 * numel(shapes.X) <= storageBufferLimit",
103
+ "flatHashScratchFits": "4 * flatTableSize <= storageBufferLimit",
104
+ "flatOutputScratchFits": "4 * numel(shapes.Y) <= storageBufferLimit",
105
+ "flatGlobalSortScratchFits": "4 * pow2ceil(min(numel(shapes.X), numel(shapes.Y))) <= storageBufferLimit",
106
+ "flatScanDispatchFits": "flatScanBlocks <= device.limits.maxComputeWorkgroupsPerDimension",
107
+ "flatHashDispatchFits": "flatScanDispatchFits and flatTableBlocks <= device.limits.maxComputeWorkgroupsPerDimension",
108
+ "axisFlagScratchFits": "4 * axisDim <= storageBufferLimit",
109
+ "axisHashScratchFits": "4 * axisTableSize <= storageBufferLimit",
110
+ "axisSortScratchFits": "4 * pow2ceil(axisOutputDim) <= storageBufferLimit",
111
+ "axisSerialWg": "max(1, min(tunables.AXIS_SERIAL_WG, deviceWorkgroupCap))",
112
+ "axisOrderScratchFits": "4 * max(1, axisOutputDim) <= storageBufferLimit",
113
+ "axisHeadCacheBytes": "4 * max(1, axisOutputDim) + 64",
114
+ "axisHeadCacheSlots": "max(1, axisOutputDim) if axisHeadCacheBytes <= device.limits.maxComputeWorkgroupStorageSize else 0",
115
+ "axisScanDispatchFits": "axisScanBlocks <= device.limits.maxComputeWorkgroupsPerDimension",
116
+ "axisHashDispatchFits": "axisScanDispatchFits and axisTableBlocks <= device.limits.maxComputeWorkgroupsPerDimension",
117
+ "narrowPortableExecution": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize < device.adapterInfo.subgroupMaxSize and device.adapterInfo.subgroupMaxSize <= 16",
118
+ "integerInput": "dtypes.T != \"f16\" and dtypes.T != \"f32\"",
119
+ "axisHashAvailable": "integerInput and axisOutputDim > 0 and axisOutputDim <= tunables.GLOBAL_MAX_CAPACITY and axisDim >= tunables.LOCAL_SORT_CROSSOVER and axisDim <= tunables.GLOBAL_MAX_CAPACITY and axisFlagScratchFits and axisHashScratchFits and axisSortScratchFits and axisHashDispatchFits",
120
+ "axisLocalAvailable": "integerInput and axisOutputDim > 0 and axisOutputDim <= tunables.LOCAL_SORT_CROSSOVER and axisLocalSortFits and axisFlagScratchFits and axisScanDispatchFits",
121
+ "axisGlobalAvailable": "integerInput and axisOutputDim > tunables.LOCAL_SORT_CROSSOVER and axisOutputDim <= tunables.GLOBAL_MAX_CAPACITY and axisFlagScratchFits and axisSortScratchFits and axisScanDispatchFits",
122
+ "axisParallelAvailable": "axisHashAvailable or axisLocalAvailable or axisGlobalAvailable",
123
+ "axisSerialSmallShape": "axisDim <= tunables.WORKGROUP_SIZE",
124
+ "flatHashSortedAvailable": "attrs.sorted != 0 and integerInput and (numel(shapes.X) >= tunables.HASH_MIN_INPUT or (dtypes.T == \"i32\" and numel(shapes.X) >= tunables.HASH_MIN_I32_INPUT and numel(shapes.Y) > tunables.LOCAL_SORT_CROSSOVER)) and numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT and numel(shapes.Y) > 0 and numel(shapes.Y) <= tunables.HASH_SORT_MAX_CAPACITY and hashKeySortFits and flatHashScratchFits and flatOutputScratchFits and flatHashDispatchFits",
125
+ "flatHashAvailable": "integerInput and numel(shapes.X) >= tunables.HASH_MIN_INPUT and numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT and numel(shapes.Y) > 0 and numel(shapes.Y) <= tunables.LOCAL_SORT_CROSSOVER and flatLocalSortFits and flatScanScratchFits and flatHashScratchFits and flatHashDispatchFits",
126
+ "flatLocalAvailable": "integerInput and numel(shapes.X) > 0 and numel(shapes.X) < tunables.HASH_MIN_INPUT and numel(shapes.Y) > 0 and numel(shapes.Y) <= tunables.LOCAL_SORT_CROSSOVER and flatLocalSortFits and flatScanScratchFits and flatScanDispatchFits",
127
+ "flatGlobalAvailable": "integerInput and numel(shapes.X) > 0 and numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT and numel(shapes.Y) > tunables.LOCAL_SORT_CROSSOVER and min(numel(shapes.X), numel(shapes.Y)) <= tunables.GLOBAL_MAX_CAPACITY and flatScanScratchFits and flatGlobalSortScratchFits and flatScanDispatchFits",
128
+ "flatParallelAvailable": "flatHashSortedAvailable or flatHashAvailable or flatLocalAvailable or flatGlobalAvailable",
129
+ "flatSerialFallbackNeeded": "not flatParallelAvailable"
130
+ },
131
+ "constants": {
132
+ "scalar": "dtypes.T",
133
+ "usesF16": "dtypes.T == \"f16\"",
134
+ "isFloat": "dtypes.T == \"f16\" or dtypes.T == \"f32\"",
135
+ "isUnsigned": "dtypes.T == \"u32\""
136
+ },
137
+ "bindingSets": {
138
+ "flatMetadataIndices": [
139
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
140
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
141
+ {
142
+ "name": "indices",
143
+ "arg": "indices",
144
+ "semantic": "indices",
145
+ "buffer": { "type": "storage" },
146
+ "elementType": "u32"
147
+ },
148
+ {
149
+ "name": "params",
150
+ "semantic": "kernel.params",
151
+ "buffer": { "type": "uniform" },
152
+ "struct": {
153
+ "name": "Params",
154
+ "fields": [
155
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
156
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
157
+ ]
158
+ }
159
+ }
160
+ ],
161
+ "flatMetadataInverse": [
162
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
163
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
164
+ {
165
+ "name": "inverse_indices",
166
+ "arg": "inverse_indices",
167
+ "semantic": "inverse_indices",
168
+ "buffer": { "type": "storage" },
169
+ "elementType": "u32"
170
+ },
171
+ {
172
+ "name": "params",
173
+ "semantic": "kernel.params",
174
+ "buffer": { "type": "uniform" },
175
+ "struct": {
176
+ "name": "Params",
177
+ "fields": [
178
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
179
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
180
+ ]
181
+ }
182
+ }
183
+ ],
184
+ "flatMetadataCounts": [
185
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
186
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
187
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
188
+ {
189
+ "name": "params",
190
+ "semantic": "kernel.params",
191
+ "buffer": { "type": "uniform" },
192
+ "struct": {
193
+ "name": "Params",
194
+ "fields": [
195
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
196
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
197
+ ]
198
+ }
199
+ }
200
+ ],
201
+ "flatMetadataIndicesInverse": [
202
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
203
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
204
+ {
205
+ "name": "indices",
206
+ "arg": "indices",
207
+ "semantic": "indices",
208
+ "buffer": { "type": "storage" },
209
+ "elementType": "u32"
210
+ },
211
+ {
212
+ "name": "inverse_indices",
213
+ "arg": "inverse_indices",
214
+ "semantic": "inverse_indices",
215
+ "buffer": { "type": "storage" },
216
+ "elementType": "u32"
217
+ },
218
+ {
219
+ "name": "params",
220
+ "semantic": "kernel.params",
221
+ "buffer": { "type": "uniform" },
222
+ "struct": {
223
+ "name": "Params",
224
+ "fields": [
225
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
226
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
227
+ ]
228
+ }
229
+ }
230
+ ],
231
+ "flatMetadataIndicesCounts": [
232
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
233
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
234
+ {
235
+ "name": "indices",
236
+ "arg": "indices",
237
+ "semantic": "indices",
238
+ "buffer": { "type": "storage" },
239
+ "elementType": "u32"
240
+ },
241
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
242
+ {
243
+ "name": "params",
244
+ "semantic": "kernel.params",
245
+ "buffer": { "type": "uniform" },
246
+ "struct": {
247
+ "name": "Params",
248
+ "fields": [
249
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
250
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
251
+ ]
252
+ }
253
+ }
254
+ ],
255
+ "flatMetadataInverseCounts": [
256
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
257
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
258
+ {
259
+ "name": "inverse_indices",
260
+ "arg": "inverse_indices",
261
+ "semantic": "inverse_indices",
262
+ "buffer": { "type": "storage" },
263
+ "elementType": "u32"
264
+ },
265
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
266
+ {
267
+ "name": "params",
268
+ "semantic": "kernel.params",
269
+ "buffer": { "type": "uniform" },
270
+ "struct": {
271
+ "name": "Params",
272
+ "fields": [
273
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
274
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
275
+ ]
276
+ }
277
+ }
278
+ ],
279
+ "flatMetadataAll": [
280
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
281
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
282
+ {
283
+ "name": "indices",
284
+ "arg": "indices",
285
+ "semantic": "indices",
286
+ "buffer": { "type": "storage" },
287
+ "elementType": "u32"
288
+ },
289
+ {
290
+ "name": "inverse_indices",
291
+ "arg": "inverse_indices",
292
+ "semantic": "inverse_indices",
293
+ "buffer": { "type": "storage" },
294
+ "elementType": "u32"
295
+ },
296
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
297
+ {
298
+ "name": "params",
299
+ "semantic": "kernel.params",
300
+ "buffer": { "type": "uniform" },
301
+ "struct": {
302
+ "name": "Params",
303
+ "fields": [
304
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
305
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
306
+ ]
307
+ }
308
+ }
309
+ ],
310
+ "axisMetadataIndices": [
311
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
312
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
313
+ {
314
+ "name": "indices",
315
+ "arg": "indices",
316
+ "semantic": "indices",
317
+ "buffer": { "type": "storage" },
318
+ "elementType": "u32"
319
+ },
320
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
321
+ {
322
+ "name": "params",
323
+ "semantic": "kernel.params",
324
+ "buffer": { "type": "uniform" },
325
+ "struct": {
326
+ "name": "Params",
327
+ "fields": [
328
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
329
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
330
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
331
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
332
+ ]
333
+ }
334
+ }
335
+ ],
336
+ "axisMetadataInverse": [
337
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
338
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
339
+ {
340
+ "name": "inverse_indices",
341
+ "arg": "inverse_indices",
342
+ "semantic": "inverse_indices",
343
+ "buffer": { "type": "storage" },
344
+ "elementType": "u32"
345
+ },
346
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
347
+ {
348
+ "name": "params",
349
+ "semantic": "kernel.params",
350
+ "buffer": { "type": "uniform" },
351
+ "struct": {
352
+ "name": "Params",
353
+ "fields": [
354
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
355
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
356
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
357
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
358
+ ]
359
+ }
360
+ }
361
+ ],
362
+ "axisMetadataCounts": [
363
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
364
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
365
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
366
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
367
+ {
368
+ "name": "params",
369
+ "semantic": "kernel.params",
370
+ "buffer": { "type": "uniform" },
371
+ "struct": {
372
+ "name": "Params",
373
+ "fields": [
374
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
375
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
376
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
377
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
378
+ ]
379
+ }
380
+ }
381
+ ],
382
+ "axisMetadataIndicesInverse": [
383
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
384
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
385
+ {
386
+ "name": "indices",
387
+ "arg": "indices",
388
+ "semantic": "indices",
389
+ "buffer": { "type": "storage" },
390
+ "elementType": "u32"
391
+ },
392
+ {
393
+ "name": "inverse_indices",
394
+ "arg": "inverse_indices",
395
+ "semantic": "inverse_indices",
396
+ "buffer": { "type": "storage" },
397
+ "elementType": "u32"
398
+ },
399
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
400
+ {
401
+ "name": "params",
402
+ "semantic": "kernel.params",
403
+ "buffer": { "type": "uniform" },
404
+ "struct": {
405
+ "name": "Params",
406
+ "fields": [
407
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
408
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
409
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
410
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
411
+ ]
412
+ }
413
+ }
414
+ ],
415
+ "axisMetadataIndicesCounts": [
416
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
417
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
418
+ {
419
+ "name": "indices",
420
+ "arg": "indices",
421
+ "semantic": "indices",
422
+ "buffer": { "type": "storage" },
423
+ "elementType": "u32"
424
+ },
425
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
426
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
427
+ {
428
+ "name": "params",
429
+ "semantic": "kernel.params",
430
+ "buffer": { "type": "uniform" },
431
+ "struct": {
432
+ "name": "Params",
433
+ "fields": [
434
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
435
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
436
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
437
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
438
+ ]
439
+ }
440
+ }
441
+ ],
442
+ "axisMetadataInverseCounts": [
443
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
444
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
445
+ {
446
+ "name": "inverse_indices",
447
+ "arg": "inverse_indices",
448
+ "semantic": "inverse_indices",
449
+ "buffer": { "type": "storage" },
450
+ "elementType": "u32"
451
+ },
452
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
453
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
454
+ {
455
+ "name": "params",
456
+ "semantic": "kernel.params",
457
+ "buffer": { "type": "uniform" },
458
+ "struct": {
459
+ "name": "Params",
460
+ "fields": [
461
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
462
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
463
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
464
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
465
+ ]
466
+ }
467
+ }
468
+ ],
469
+ "axisMetadataAll": [
470
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
471
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
472
+ {
473
+ "name": "indices",
474
+ "arg": "indices",
475
+ "semantic": "indices",
476
+ "buffer": { "type": "storage" },
477
+ "elementType": "u32"
478
+ },
479
+ {
480
+ "name": "inverse_indices",
481
+ "arg": "inverse_indices",
482
+ "semantic": "inverse_indices",
483
+ "buffer": { "type": "storage" },
484
+ "elementType": "u32"
485
+ },
486
+ { "name": "counts", "arg": "counts", "semantic": "counts", "buffer": { "type": "storage" }, "elementType": "u32" },
487
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
488
+ {
489
+ "name": "params",
490
+ "semantic": "kernel.params",
491
+ "buffer": { "type": "uniform" },
492
+ "struct": {
493
+ "name": "Params",
494
+ "fields": [
495
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
496
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
497
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
498
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
499
+ ]
500
+ }
501
+ }
502
+ ],
503
+ "hashBuild": [
504
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
505
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
506
+ { "name": "tableIdx", "semantic": "tableIdx", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
507
+ {
508
+ "name": "special",
509
+ "semantic": "special",
510
+ "buffer": { "type": "storage" },
511
+ "elementType": "atomic<u32>",
512
+ "length": 1
513
+ },
514
+ {
515
+ "name": "params",
516
+ "semantic": "kernel.params",
517
+ "buffer": { "type": "uniform" },
518
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
519
+ }
520
+ ],
521
+ "hashBuildKeyOnly": [
522
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
523
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
524
+ {
525
+ "name": "special",
526
+ "semantic": "special",
527
+ "buffer": { "type": "storage" },
528
+ "elementType": "atomic<u32>",
529
+ "length": 1
530
+ },
531
+ {
532
+ "name": "params",
533
+ "semantic": "kernel.params",
534
+ "buffer": { "type": "uniform" },
535
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
536
+ }
537
+ ],
538
+ "dedup": [
539
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
540
+ { "name": "flags", "semantic": "flags", "buffer": { "type": "storage" }, "elementType": "u32" },
541
+ {
542
+ "name": "params",
543
+ "semantic": "kernel.params",
544
+ "buffer": { "type": "uniform" },
545
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
546
+ }
547
+ ],
548
+ "compact": [
549
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
550
+ { "name": "flags", "semantic": "flags", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
551
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
552
+ {
553
+ "name": "params",
554
+ "semantic": "kernel.params",
555
+ "buffer": { "type": "uniform" },
556
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
557
+ }
558
+ ],
559
+ "axisDedup": [
560
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
561
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "storage" }, "elementType": "u32" },
562
+ {
563
+ "name": "params",
564
+ "semantic": "kernel.params",
565
+ "buffer": { "type": "uniform" },
566
+ "struct": {
567
+ "name": "Params",
568
+ "fields": [
569
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
570
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
571
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" }
572
+ ]
573
+ }
574
+ }
575
+ ],
576
+ "axisCompactGlobal": [
577
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
578
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
579
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "storage" }, "elementType": "u32" },
580
+ { "name": "sortPad", "semantic": "sortPad", "buffer": { "type": "storage" }, "elementType": "u32" },
581
+ { "name": "sortKey", "semantic": "sortKey", "buffer": { "type": "storage" }, "elementType": "u32" },
582
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
583
+ {
584
+ "name": "params",
585
+ "semantic": "kernel.params",
586
+ "buffer": { "type": "uniform" },
587
+ "struct": {
588
+ "name": "Params",
589
+ "fields": [
590
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
591
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
592
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
593
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
594
+ ]
595
+ }
596
+ }
597
+ ],
598
+ "axisCompactGlobalUnsorted": [
599
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
600
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
601
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "storage" }, "elementType": "u32" },
602
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
603
+ {
604
+ "name": "params",
605
+ "semantic": "kernel.params",
606
+ "buffer": { "type": "uniform" },
607
+ "struct": {
608
+ "name": "Params",
609
+ "fields": [
610
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
611
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
612
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
613
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
614
+ ]
615
+ }
616
+ }
617
+ ],
618
+ "axisCompactGlobalScratch": [
619
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
620
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
621
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "storage" }, "elementType": "u32" },
622
+ { "name": "sortPad", "semantic": "sortPad", "buffer": { "type": "storage" }, "elementType": "u32" },
623
+ { "name": "sortKey", "semantic": "sortKey", "buffer": { "type": "storage" }, "elementType": "u32" },
624
+ { "name": "count", "semantic": "count", "buffer": { "type": "storage" }, "elementType": "u32", "length": 1 },
625
+ {
626
+ "name": "params",
627
+ "semantic": "kernel.params",
628
+ "buffer": { "type": "uniform" },
629
+ "struct": {
630
+ "name": "Params",
631
+ "fields": [
632
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
633
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
634
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" }
635
+ ]
636
+ }
637
+ }
638
+ ],
639
+ "axisCompactGlobalScratchUnsortedBase": [
640
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
641
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "storage" }, "elementType": "u32" },
642
+ { "name": "count", "semantic": "count", "buffer": { "type": "storage" }, "elementType": "u32", "length": 1 }
643
+ ],
644
+ "axisCompactGlobalScratchUnsorted": [
645
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
646
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "storage" }, "elementType": "u32" },
647
+ { "name": "count", "semantic": "count", "buffer": { "type": "storage" }, "elementType": "u32", "length": 1 },
648
+ {
649
+ "name": "params",
650
+ "semantic": "kernel.params",
651
+ "buffer": { "type": "uniform" },
652
+ "struct": {
653
+ "name": "Params",
654
+ "fields": [{ "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" }]
655
+ }
656
+ }
657
+ ],
658
+ "axisScatterGlobal": [
659
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
660
+ { "name": "slots", "semantic": "slots", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
661
+ {
662
+ "name": "count",
663
+ "semantic": "count",
664
+ "buffer": { "type": "read-only-storage" },
665
+ "elementType": "u32",
666
+ "length": 1
667
+ },
668
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" }
669
+ ],
670
+ "hashInitKeyOnlyWithCount": [
671
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "storage" }, "elementType": "vec4<u32>" },
672
+ {
673
+ "name": "special",
674
+ "semantic": "special",
675
+ "buffer": { "type": "storage" },
676
+ "elementType": "atomic<u32>",
677
+ "length": 1
678
+ },
679
+ {
680
+ "name": "count",
681
+ "semantic": "count",
682
+ "buffer": { "type": "storage" },
683
+ "elementType": "atomic<u32>",
684
+ "length": 1
685
+ }
686
+ ],
687
+ "hashCollectKeyOnly": [
688
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
689
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
690
+ {
691
+ "name": "special",
692
+ "semantic": "special",
693
+ "buffer": { "type": "storage" },
694
+ "elementType": "atomic<u32>",
695
+ "length": 1
696
+ },
697
+ { "name": "compactedBits", "semantic": "compactedBits", "buffer": { "type": "storage" }, "elementType": "u32" },
698
+ {
699
+ "name": "count",
700
+ "semantic": "count",
701
+ "buffer": { "type": "storage" },
702
+ "elementType": "atomic<u32>",
703
+ "length": 1
704
+ }
705
+ ],
706
+ "collectedSort": [
707
+ {
708
+ "name": "compactedBits",
709
+ "semantic": "compactedBits",
710
+ "buffer": { "type": "read-only-storage" },
711
+ "elementType": "u32"
712
+ },
713
+ {
714
+ "name": "count",
715
+ "semantic": "count",
716
+ "buffer": { "type": "read-only-storage" },
717
+ "elementType": "u32",
718
+ "length": 1
719
+ },
720
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" }
721
+ ],
722
+ "hashInit": [
723
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
724
+ { "name": "tableIdx", "semantic": "tableIdx", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
725
+ {
726
+ "name": "special",
727
+ "semantic": "special",
728
+ "buffer": { "type": "storage" },
729
+ "elementType": "atomic<u32>",
730
+ "length": 1
731
+ }
732
+ ],
733
+ "hashMark": [
734
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
735
+ { "name": "flags", "semantic": "flags", "buffer": { "type": "storage" }, "elementType": "u32" },
736
+ { "name": "tableKey", "semantic": "tableKey", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
737
+ { "name": "tableIdx", "semantic": "tableIdx", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
738
+ {
739
+ "name": "special",
740
+ "semantic": "special",
741
+ "buffer": { "type": "storage" },
742
+ "elementType": "atomic<u32>",
743
+ "length": 1
744
+ },
745
+ {
746
+ "name": "params",
747
+ "semantic": "kernel.params",
748
+ "buffer": { "type": "uniform" },
749
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
750
+ }
751
+ ],
752
+ "flatGlobalCompact": [
753
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
754
+ { "name": "flags", "semantic": "flags", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
755
+ { "name": "sortVal", "semantic": "sortVal", "buffer": { "type": "storage" }, "elementType": "u32" },
756
+ { "name": "sortKey", "semantic": "sortKey", "buffer": { "type": "storage" }, "elementType": "u32" },
757
+ { "name": "sortPad", "semantic": "sortPad", "buffer": { "type": "storage" }, "elementType": "u32" },
758
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
759
+ {
760
+ "name": "params",
761
+ "semantic": "kernel.params",
762
+ "buffer": { "type": "uniform" },
763
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
764
+ }
765
+ ],
766
+ "flatGlobalCompactUnsorted": [
767
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
768
+ { "name": "flags", "semantic": "flags", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
769
+ { "name": "sortVal", "semantic": "sortVal", "buffer": { "type": "storage" }, "elementType": "u32" },
770
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
771
+ {
772
+ "name": "params",
773
+ "semantic": "kernel.params",
774
+ "buffer": { "type": "uniform" },
775
+ "struct": { "name": "Params", "fields": [{ "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" }] }
776
+ }
777
+ ],
778
+ "flatSerial": [
779
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
780
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
781
+ {
782
+ "name": "params",
783
+ "semantic": "kernel.params",
784
+ "buffer": { "type": "uniform" },
785
+ "struct": {
786
+ "name": "Params",
787
+ "fields": [
788
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.X)" },
789
+ { "name": "capacity", "type": "u32", "value": "numel(shapes.Y)" }
790
+ ]
791
+ }
792
+ }
793
+ ],
794
+ "axisHashInit": [
795
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
796
+ { "name": "sliceHash", "semantic": "sliceHash", "buffer": { "type": "storage" }, "elementType": "u32" },
797
+ { "name": "hashSlot", "semantic": "hashSlot", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
798
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "storage" }, "elementType": "u32" },
799
+ {
800
+ "name": "params",
801
+ "semantic": "kernel.params",
802
+ "buffer": { "type": "uniform" },
803
+ "struct": {
804
+ "name": "Params",
805
+ "fields": [
806
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
807
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
808
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" }
809
+ ]
810
+ }
811
+ }
812
+ ],
813
+ "axisHashBuild": [
814
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
815
+ { "name": "sliceHash", "semantic": "sliceHash", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
816
+ { "name": "hashSlot", "semantic": "hashSlot", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
817
+ {
818
+ "name": "params",
819
+ "semantic": "kernel.params",
820
+ "buffer": { "type": "uniform" },
821
+ "struct": {
822
+ "name": "Params",
823
+ "fields": [
824
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
825
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
826
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" }
827
+ ]
828
+ }
829
+ }
830
+ ],
831
+ "axisHashMark": [
832
+ { "name": "hashSlot", "semantic": "hashSlot", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
833
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "storage" }, "elementType": "u32" }
834
+ ],
835
+ "axisCompactLocal": [
836
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
837
+ { "name": "firstFlag", "semantic": "firstFlag", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
838
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
839
+ {
840
+ "name": "params",
841
+ "semantic": "kernel.params",
842
+ "buffer": { "type": "uniform" },
843
+ "struct": {
844
+ "name": "Params",
845
+ "fields": [
846
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
847
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
848
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
849
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
850
+ ]
851
+ }
852
+ }
853
+ ],
854
+ "axisSerial": [
855
+ { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
856
+ { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
857
+ { "name": "order", "semantic": "order", "buffer": { "type": "storage" }, "elementType": "u32" },
858
+ {
859
+ "name": "params",
860
+ "semantic": "kernel.params",
861
+ "buffer": { "type": "uniform" },
862
+ "struct": {
863
+ "name": "Params",
864
+ "fields": [
865
+ { "name": "outer", "type": "u32", "value": "outer(shapes.X, normalizedAxis)" },
866
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.X, normalizedAxis)" },
867
+ { "name": "inner", "type": "u32", "value": "inner(shapes.X, normalizedAxis)" },
868
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.Y, normalizedAxis)" }
869
+ ]
870
+ }
871
+ }
872
+ ]
873
+ },
874
+ "variants": [
875
+ {
876
+ "id": "hash_dedup_sorted_collect",
877
+ "priority": 30,
878
+ "when": ["flatContract", "attrs.sorted != 0", "integerInput", "(numel(shapes.X) >= tunables.HASH_MIN_INPUT or (dtypes.T == \"i32\" and numel(shapes.X) >= tunables.HASH_MIN_I32_INPUT and numel(shapes.Y) > tunables.LOCAL_SORT_CROSSOVER))", "numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT", "numel(shapes.Y) > 0", "numel(shapes.Y) <= tunables.HASH_SORT_MAX_CAPACITY", "hashKeySortFits", "flatHashScratchFits", "flatOutputScratchFits", "flatHashDispatchFits"],
879
+ "derive": {
880
+ "scanBlocks": "flatScanBlocks",
881
+ "tableSize": "flatTableSize",
882
+ "tableBlocks": "flatTableBlocks",
883
+ "tableVec4Blocks": "flatTableVec4Blocks"
884
+ },
885
+ "intermediates": [
886
+ { "id": "tableKey", "dtype": "uint32", "shape": "[tableSize]" },
887
+ { "id": "special", "dtype": "uint32", "shape": "[1]" },
888
+ { "id": "compactedBits", "dtype": "uint32", "shape": "[numel(shapes.Y)]" },
889
+ { "id": "count", "dtype": "uint32", "shape": "[1]" }
890
+ ],
891
+ "passes": [
892
+ {
893
+ "id": "init",
894
+ "name": "Unique.HashInitCollect",
895
+ "source": {
896
+ "shader": "unique-hash-init.wgsl.jinja",
897
+ "inputs": { "tableSize": "tableSize", "keyOnlyVec4": true }
898
+ },
899
+ "bindings": "hashInitKeyOnlyWithCount",
900
+ "dispatch": { "x": "tableVec4Blocks" }
901
+ },
902
+ {
903
+ "id": "build",
904
+ "name": "Unique.HashBuild",
905
+ "source": {
906
+ "shader": "unique-hash-build.wgsl.jinja",
907
+ "inputs": { "tableSize": "tableSize", "keyOnly": true }
908
+ },
909
+ "bindings": "hashBuildKeyOnly",
910
+ "dispatch": { "x": "scanBlocks" }
911
+ },
912
+ {
913
+ "id": "collect",
914
+ "name": "Unique.HashCollect",
915
+ "source": {
916
+ "shader": "unique-hash-collect.wgsl.jinja",
917
+ "inputs": {
918
+ "tableSize": "tableSize",
919
+ "capacity": "numel(shapes.Y)",
920
+ "useSubgroups": "device.features.has(\"subgroups\")"
921
+ }
922
+ },
923
+ "bindings": "hashCollectKeyOnly",
924
+ "dispatch": { "x": "tableBlocks" }
925
+ },
926
+ {
927
+ "id": "sort",
928
+ "name": "Unique.HashSortCollected",
929
+ "source": {
930
+ "shader": "unique-hash-sort-collected-key-only.wgsl.jinja",
931
+ "inputs": { "capacity": "numel(shapes.Y)", "sortN": "pow2ceil(numel(shapes.Y))" }
932
+ },
933
+ "bindings": "collectedSort",
934
+ "dispatch": { "x": 1 }
935
+ }
936
+ ]
937
+ },
938
+ {
939
+ "id": "hash_dedup",
940
+ "priority": 20,
941
+ "when": ["flatContract", "integerInput", "numel(shapes.X) >= tunables.HASH_MIN_INPUT", "numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT", "numel(shapes.Y) > 0", "numel(shapes.Y) <= tunables.LOCAL_SORT_CROSSOVER", "flatLocalSortFits", "flatScanScratchFits", "flatHashScratchFits", "flatHashDispatchFits"],
942
+ "derive": {
943
+ "scanN": "numel(shapes.X)",
944
+ "scanBlocks": "flatScanBlocks",
945
+ "tableSize": "flatTableSize",
946
+ "tableBlocks": "flatTableBlocks"
947
+ },
948
+ "constants": { "sorted": "attrs.sorted != 0" },
949
+ "intermediates": [
950
+ { "id": "flags", "dtype": "uint32", "shape": "[scanN]" },
951
+ { "id": "tableKey", "dtype": "uint32", "shape": "[tableSize]" },
952
+ { "id": "tableIdx", "dtype": "uint32", "shape": "[tableSize]" },
953
+ { "id": "special", "dtype": "uint32", "shape": "[1]" }
954
+ ],
955
+ "passes": [
956
+ {
957
+ "id": "init",
958
+ "name": "Unique.HashInit",
959
+ "source": {
960
+ "shader": "unique-hash-init.wgsl.jinja",
961
+ "inputs": { "tableSize": "tableSize", "keyOnlyVec4": false }
962
+ },
963
+ "bindings": "hashInit",
964
+ "dispatch": { "x": "tableBlocks" }
965
+ },
966
+ {
967
+ "id": "build",
968
+ "name": "Unique.HashBuild",
969
+ "source": {
970
+ "shader": "unique-hash-build.wgsl.jinja",
971
+ "inputs": { "tableSize": "tableSize", "keyOnly": false }
972
+ },
973
+ "bindings": "hashBuild",
974
+ "dispatch": { "x": "scanBlocks" }
975
+ },
976
+ {
977
+ "id": "mark",
978
+ "name": "Unique.HashMark",
979
+ "source": { "shader": "unique-hash-mark.wgsl.jinja", "inputs": { "tableSize": "tableSize" } },
980
+ "bindings": "hashMark",
981
+ "dispatch": { "x": "scanBlocks" }
982
+ },
983
+ {
984
+ "id": "compact",
985
+ "name": "Unique.CompactSort",
986
+ "source": {
987
+ "shader": "unique-compact-sort.wgsl.jinja",
988
+ "inputs": { "capacity": "numel(shapes.Y)", "sortN": "pow2ceil(numel(shapes.Y))", "globalScratch": false }
989
+ },
990
+ "bindings": "compact",
991
+ "dispatch": { "x": 1 }
992
+ }
993
+ ]
994
+ },
995
+ {
996
+ "id": "rank1_bounded",
997
+ "when": ["flatContract", "flatSerialFallbackNeeded"],
998
+ "constants": { "sorted": "attrs.sorted != 0" },
999
+ "passes": [
1000
+ {
1001
+ "id": "main",
1002
+ "name": "Unique",
1003
+ "shader": "unique.wgsl.jinja",
1004
+ "bindings": "flatSerial",
1005
+ "dispatch": { "x": 1 }
1006
+ }
1007
+ ]
1008
+ },
1009
+ {
1010
+ "id": "axis_serial",
1011
+ "when": ["axisContract", "axisOrderScratchFits", "not axisParallelAvailable or axisSerialSmallShape"],
1012
+ "constants": { "sorted": "attrs.sorted != 0", "axisSerialWg": "axisSerialWg" },
1013
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1014
+ "passes": [
1015
+ {
1016
+ "id": "main",
1017
+ "name": "Unique.Axis",
1018
+ "source": {
1019
+ "shader": "unique-axis.wgsl.jinja",
1020
+ "inputs": { "capacity": "max(1, dim(shapes.Y, normalizedAxis))", "headCacheSlots": "axisHeadCacheSlots" }
1021
+ },
1022
+ "bindings": "axisSerial",
1023
+ "dispatch": { "x": 1 }
1024
+ }
1025
+ ]
1026
+ },
1027
+ {
1028
+ "id": "axis_hash_bounded",
1029
+ "priority": 20,
1030
+ "demoteWhen": ["narrowPortableExecution and (axisLocalAvailable or axisGlobalAvailable)"],
1031
+ "when": ["axisContract", "axisHashAvailable", "attrs.sorted != 0"],
1032
+ "derive": {
1033
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1034
+ "tableSize": "axisTableSize",
1035
+ "axisBlocks": "axisScanBlocks",
1036
+ "tableBlocks": "axisTableBlocks",
1037
+ "sortN": "pow2ceil(dim(shapes.Y, normalizedAxis))"
1038
+ },
1039
+ "constants": { "sorted": true },
1040
+ "intermediates": [
1041
+ { "id": "sliceHash", "dtype": "uint32", "shape": "[axisDim]" },
1042
+ { "id": "hashSlot", "dtype": "uint32", "shape": "[tableSize]" },
1043
+ { "id": "firstFlag", "dtype": "uint32", "shape": "[axisDim]" },
1044
+ { "id": "slots", "dtype": "uint32", "shape": "[sortN]" },
1045
+ { "id": "sortPad", "dtype": "uint32", "shape": "[sortN]" },
1046
+ { "id": "sortKey", "dtype": "uint32", "shape": "[sortN]" },
1047
+ { "id": "count", "dtype": "uint32", "shape": "[1]" }
1048
+ ],
1049
+ "passes": [
1050
+ {
1051
+ "id": "hash_init",
1052
+ "name": "Unique.AxisHashInit",
1053
+ "source": {
1054
+ "shader": "unique-axis-hash.wgsl.jinja",
1055
+ "inputs": { "stage": "\"init\"", "tableSize": "tableSize" }
1056
+ },
1057
+ "bindings": "axisHashInit",
1058
+ "dispatch": { "x": "tableBlocks" }
1059
+ },
1060
+ {
1061
+ "id": "hash_build",
1062
+ "name": "Unique.AxisHashBuild",
1063
+ "source": {
1064
+ "shader": "unique-axis-hash.wgsl.jinja",
1065
+ "inputs": { "stage": "\"build\"", "tableSize": "tableSize" }
1066
+ },
1067
+ "bindings": "axisHashBuild",
1068
+ "dispatch": { "x": "axisBlocks" }
1069
+ },
1070
+ {
1071
+ "id": "hash_mark",
1072
+ "name": "Unique.AxisHashMark",
1073
+ "source": {
1074
+ "shader": "unique-axis-hash.wgsl.jinja",
1075
+ "inputs": { "stage": "\"mark\"", "tableSize": "tableSize" }
1076
+ },
1077
+ "bindings": "axisHashMark",
1078
+ "dispatch": { "x": "tableBlocks" }
1079
+ },
1080
+ {
1081
+ "id": "compact",
1082
+ "name": "Unique.AxisCompactSortGlobal",
1083
+ "source": {
1084
+ "shader": "unique-axis-compact-sort.wgsl.jinja",
1085
+ "inputs": {
1086
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1087
+ "sortN": "sortN",
1088
+ "compactOnly": true,
1089
+ "globalScratch": true,
1090
+ "emitCount": true
1091
+ }
1092
+ },
1093
+ "bindings": "axisCompactGlobalScratch",
1094
+ "dispatch": { "x": 1 }
1095
+ },
1096
+ {
1097
+ "id": "scatter",
1098
+ "name": "Unique.AxisScatter",
1099
+ "source": {
1100
+ "shader": "unique-axis-scatter.wgsl.jinja",
1101
+ "inputs": {
1102
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1103
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1104
+ "inner": "max(1, inner(shapes.X, normalizedAxis))",
1105
+ "outputAxisDim": "dim(shapes.Y, normalizedAxis)",
1106
+ "totalOut": "numel(shapes.Y)",
1107
+ "staticShape": true
1108
+ }
1109
+ },
1110
+ "bindings": "axisScatterGlobal",
1111
+ "dispatch": { "gridStride": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
1112
+ }
1113
+ ]
1114
+ },
1115
+ {
1116
+ "id": "axis_hash_bounded_unsorted",
1117
+ "priority": 20,
1118
+ "demoteWhen": ["narrowPortableExecution and (axisLocalAvailable or axisGlobalAvailable)"],
1119
+ "when": ["axisContract", "axisHashAvailable", "attrs.sorted == 0"],
1120
+ "derive": {
1121
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1122
+ "tableSize": "axisTableSize",
1123
+ "axisBlocks": "axisScanBlocks",
1124
+ "tableBlocks": "axisTableBlocks",
1125
+ "sortN": "pow2ceil(dim(shapes.Y, normalizedAxis))"
1126
+ },
1127
+ "constants": { "sorted": false },
1128
+ "intermediates": [
1129
+ { "id": "sliceHash", "dtype": "uint32", "shape": "[axisDim]" },
1130
+ { "id": "hashSlot", "dtype": "uint32", "shape": "[tableSize]" },
1131
+ { "id": "firstFlag", "dtype": "uint32", "shape": "[axisDim]" },
1132
+ { "id": "slots", "dtype": "uint32", "shape": "[dim(shapes.Y, normalizedAxis)]" },
1133
+ { "id": "count", "dtype": "uint32", "shape": "[1]" }
1134
+ ],
1135
+ "passes": [
1136
+ {
1137
+ "id": "hash_init",
1138
+ "name": "Unique.AxisHashInit",
1139
+ "source": {
1140
+ "shader": "unique-axis-hash.wgsl.jinja",
1141
+ "inputs": { "stage": "\"init\"", "tableSize": "tableSize" }
1142
+ },
1143
+ "bindings": "axisHashInit",
1144
+ "dispatch": { "x": "tableBlocks" }
1145
+ },
1146
+ {
1147
+ "id": "hash_build",
1148
+ "name": "Unique.AxisHashBuild",
1149
+ "source": {
1150
+ "shader": "unique-axis-hash.wgsl.jinja",
1151
+ "inputs": { "stage": "\"build\"", "tableSize": "tableSize" }
1152
+ },
1153
+ "bindings": "axisHashBuild",
1154
+ "dispatch": { "x": "axisBlocks" }
1155
+ },
1156
+ {
1157
+ "id": "hash_mark",
1158
+ "name": "Unique.AxisHashMark",
1159
+ "source": {
1160
+ "shader": "unique-axis-hash.wgsl.jinja",
1161
+ "inputs": { "stage": "\"mark\"", "tableSize": "tableSize" }
1162
+ },
1163
+ "bindings": "axisHashMark",
1164
+ "dispatch": { "x": "tableBlocks" }
1165
+ },
1166
+ {
1167
+ "id": "compact",
1168
+ "name": "Unique.AxisCompactGlobal",
1169
+ "source": {
1170
+ "shader": "unique-axis-compact-sort.wgsl.jinja",
1171
+ "inputs": {
1172
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1173
+ "sortN": "sortN",
1174
+ "compactOnly": true,
1175
+ "globalScratch": true,
1176
+ "emitCount": true
1177
+ }
1178
+ },
1179
+ "bindings": "axisCompactGlobalScratchUnsorted",
1180
+ "dispatch": { "x": 1 }
1181
+ },
1182
+ {
1183
+ "id": "scatter",
1184
+ "name": "Unique.AxisScatter",
1185
+ "source": {
1186
+ "shader": "unique-axis-scatter.wgsl.jinja",
1187
+ "inputs": {
1188
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1189
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1190
+ "inner": "max(1, inner(shapes.X, normalizedAxis))",
1191
+ "outputAxisDim": "dim(shapes.Y, normalizedAxis)",
1192
+ "totalOut": "numel(shapes.Y)",
1193
+ "staticShape": true
1194
+ }
1195
+ },
1196
+ "bindings": "axisScatterGlobal",
1197
+ "dispatch": { "gridStride": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
1198
+ }
1199
+ ]
1200
+ },
1201
+ {
1202
+ "id": "parallel_dedup",
1203
+ "priority": 10,
1204
+ "when": ["flatContract", "integerInput", "numel(shapes.X) > 0", "numel(shapes.X) < tunables.HASH_MIN_INPUT", "numel(shapes.Y) > 0", "numel(shapes.Y) <= tunables.LOCAL_SORT_CROSSOVER", "flatLocalSortFits", "flatScanScratchFits", "flatScanDispatchFits", "true"],
1205
+ "derive": { "scanN": "numel(shapes.X)", "scanBlocks": "flatScanBlocks" },
1206
+ "constants": { "sorted": "attrs.sorted != 0" },
1207
+ "intermediates": [{ "id": "flags", "dtype": "uint32", "shape": "[scanN]" }],
1208
+ "passes": [
1209
+ {
1210
+ "id": "dedup",
1211
+ "name": "Unique.Dedup",
1212
+ "shader": "unique-dedup.wgsl.jinja",
1213
+ "bindings": "dedup",
1214
+ "dispatch": { "x": "scanBlocks" }
1215
+ },
1216
+ {
1217
+ "id": "compact",
1218
+ "name": "Unique.CompactSort",
1219
+ "source": {
1220
+ "shader": "unique-compact-sort.wgsl.jinja",
1221
+ "inputs": { "capacity": "numel(shapes.Y)", "sortN": "pow2ceil(numel(shapes.Y))", "globalScratch": false }
1222
+ },
1223
+ "bindings": "compact",
1224
+ "dispatch": { "x": 1 }
1225
+ }
1226
+ ]
1227
+ },
1228
+ {
1229
+ "id": "parallel_dedup_large_y",
1230
+ "priority": 5,
1231
+ "when": ["flatContract", "integerInput", "numel(shapes.X) > 0", "numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT", "numel(shapes.Y) > tunables.LOCAL_SORT_CROSSOVER", "min(numel(shapes.X), numel(shapes.Y)) <= tunables.GLOBAL_MAX_CAPACITY", "flatScanScratchFits", "flatGlobalSortScratchFits", "flatScanDispatchFits", "attrs.sorted != 0"],
1232
+ "derive": {
1233
+ "scanN": "numel(shapes.X)",
1234
+ "scanBlocks": "flatScanBlocks",
1235
+ "activeCap": "min(numel(shapes.X), numel(shapes.Y))",
1236
+ "sortN": "pow2ceil(activeCap)"
1237
+ },
1238
+ "constants": { "sorted": true },
1239
+ "intermediates": [
1240
+ { "id": "flags", "dtype": "uint32", "shape": "[scanN]" },
1241
+ { "id": "sortVal", "dtype": "uint32", "shape": "[sortN]" },
1242
+ { "id": "sortKey", "dtype": "uint32", "shape": "[sortN]" },
1243
+ { "id": "sortPad", "dtype": "uint32", "shape": "[sortN]" }
1244
+ ],
1245
+ "passes": [
1246
+ {
1247
+ "id": "dedup",
1248
+ "name": "Unique.Dedup",
1249
+ "shader": "unique-dedup.wgsl.jinja",
1250
+ "bindings": "dedup",
1251
+ "dispatch": { "x": "scanBlocks" }
1252
+ },
1253
+ {
1254
+ "id": "compactsort",
1255
+ "name": "Unique.CompactSortGlobal",
1256
+ "source": {
1257
+ "shader": "unique-compact-sort.wgsl.jinja",
1258
+ "inputs": { "capacity": "numel(shapes.Y)", "sortN": "sortN", "globalScratch": true }
1259
+ },
1260
+ "bindings": "flatGlobalCompact",
1261
+ "dispatch": { "x": 1 }
1262
+ }
1263
+ ]
1264
+ },
1265
+ {
1266
+ "id": "parallel_dedup_large_y_unsorted",
1267
+ "priority": 5,
1268
+ "when": ["flatContract", "integerInput", "numel(shapes.X) > 0", "numel(shapes.X) <= tunables.MAX_PARALLEL_INPUT", "numel(shapes.Y) > tunables.LOCAL_SORT_CROSSOVER", "min(numel(shapes.X), numel(shapes.Y)) <= tunables.GLOBAL_MAX_CAPACITY", "flatScanScratchFits", "flatGlobalSortScratchFits", "flatScanDispatchFits", "attrs.sorted == 0"],
1269
+ "derive": {
1270
+ "scanN": "numel(shapes.X)",
1271
+ "scanBlocks": "flatScanBlocks",
1272
+ "activeCap": "min(numel(shapes.X), numel(shapes.Y))",
1273
+ "sortN": "pow2ceil(activeCap)"
1274
+ },
1275
+ "constants": { "sorted": false },
1276
+ "intermediates": [
1277
+ { "id": "flags", "dtype": "uint32", "shape": "[scanN]" },
1278
+ { "id": "sortVal", "dtype": "uint32", "shape": "[sortN]" }
1279
+ ],
1280
+ "passes": [
1281
+ {
1282
+ "id": "dedup",
1283
+ "name": "Unique.Dedup",
1284
+ "shader": "unique-dedup.wgsl.jinja",
1285
+ "bindings": "dedup",
1286
+ "dispatch": { "x": "scanBlocks" }
1287
+ },
1288
+ {
1289
+ "id": "compactsort",
1290
+ "name": "Unique.CompactGlobal",
1291
+ "source": {
1292
+ "shader": "unique-compact-sort.wgsl.jinja",
1293
+ "inputs": { "capacity": "numel(shapes.Y)", "sortN": "sortN", "globalScratch": true }
1294
+ },
1295
+ "bindings": "flatGlobalCompactUnsorted",
1296
+ "dispatch": { "x": 1 }
1297
+ }
1298
+ ]
1299
+ },
1300
+ {
1301
+ "id": "axis_bounded",
1302
+ "priority": 10,
1303
+ "when": ["axisContract", "axisLocalAvailable", "true"],
1304
+ "derive": { "axisDim": "dim(shapes.X, normalizedAxis)", "dedupBlocks": "axisScanBlocks" },
1305
+ "constants": { "sorted": "attrs.sorted != 0" },
1306
+ "intermediates": [{ "id": "firstFlag", "dtype": "uint32", "shape": "[axisDim]" }],
1307
+ "passes": [
1308
+ {
1309
+ "id": "dedup",
1310
+ "name": "Unique.AxisDedup",
1311
+ "shader": "unique-axis-dedup.wgsl.jinja",
1312
+ "bindings": "axisDedup",
1313
+ "dispatch": { "x": "dedupBlocks" }
1314
+ },
1315
+ {
1316
+ "id": "compact",
1317
+ "name": "Unique.AxisCompactSort",
1318
+ "source": {
1319
+ "shader": "unique-axis-compact-sort.wgsl.jinja",
1320
+ "inputs": {
1321
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1322
+ "sortN": "pow2ceil(dim(shapes.Y, normalizedAxis))",
1323
+ "compactOnly": false,
1324
+ "globalScratch": false
1325
+ }
1326
+ },
1327
+ "bindings": "axisCompactLocal",
1328
+ "dispatch": { "x": 1 }
1329
+ }
1330
+ ]
1331
+ },
1332
+ {
1333
+ "id": "axis_bounded_large_sorted",
1334
+ "priority": 5,
1335
+ "when": ["axisContract", "axisGlobalAvailable", "attrs.sorted != 0"],
1336
+ "derive": {
1337
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1338
+ "dedupBlocks": "axisScanBlocks",
1339
+ "sortN": "pow2ceil(dim(shapes.Y, normalizedAxis))"
1340
+ },
1341
+ "constants": { "sorted": true },
1342
+ "intermediates": [
1343
+ { "id": "firstFlag", "dtype": "uint32", "shape": "[axisDim]" },
1344
+ { "id": "slots", "dtype": "uint32", "shape": "[sortN]" },
1345
+ { "id": "sortPad", "dtype": "uint32", "shape": "[sortN]" },
1346
+ { "id": "sortKey", "dtype": "uint32", "shape": "[sortN]" }
1347
+ ],
1348
+ "passes": [
1349
+ {
1350
+ "id": "dedup",
1351
+ "name": "Unique.AxisDedup",
1352
+ "shader": "unique-axis-dedup.wgsl.jinja",
1353
+ "bindings": "axisDedup",
1354
+ "dispatch": { "x": "dedupBlocks" }
1355
+ },
1356
+ {
1357
+ "id": "compact",
1358
+ "name": "Unique.AxisCompactSortGlobal",
1359
+ "source": {
1360
+ "shader": "unique-axis-compact-sort.wgsl.jinja",
1361
+ "inputs": {
1362
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1363
+ "sortN": "sortN",
1364
+ "compactOnly": false,
1365
+ "globalScratch": true
1366
+ }
1367
+ },
1368
+ "bindings": "axisCompactGlobal",
1369
+ "dispatch": { "x": 1 }
1370
+ }
1371
+ ]
1372
+ },
1373
+ {
1374
+ "id": "axis_bounded_large",
1375
+ "priority": 5,
1376
+ "when": ["axisContract", "axisGlobalAvailable", "attrs.sorted == 0"],
1377
+ "derive": {
1378
+ "axisDim": "dim(shapes.X, normalizedAxis)",
1379
+ "dedupBlocks": "axisScanBlocks",
1380
+ "sortN": "pow2ceil(dim(shapes.Y, normalizedAxis))"
1381
+ },
1382
+ "constants": { "sorted": false },
1383
+ "intermediates": [
1384
+ { "id": "firstFlag", "dtype": "uint32", "shape": "[axisDim]" },
1385
+ { "id": "slots", "dtype": "uint32", "shape": "[sortN]" }
1386
+ ],
1387
+ "passes": [
1388
+ {
1389
+ "id": "dedup",
1390
+ "name": "Unique.AxisDedup",
1391
+ "shader": "unique-axis-dedup.wgsl.jinja",
1392
+ "bindings": "axisDedup",
1393
+ "dispatch": { "x": "dedupBlocks" }
1394
+ },
1395
+ {
1396
+ "id": "compact",
1397
+ "name": "Unique.AxisCompactGlobal",
1398
+ "source": {
1399
+ "shader": "unique-axis-compact-sort.wgsl.jinja",
1400
+ "inputs": {
1401
+ "capacity": "dim(shapes.Y, normalizedAxis)",
1402
+ "sortN": "sortN",
1403
+ "compactOnly": false,
1404
+ "globalScratch": true
1405
+ }
1406
+ },
1407
+ "bindings": "axisCompactGlobalUnsorted",
1408
+ "dispatch": { "x": 1 }
1409
+ }
1410
+ ]
1411
+ },
1412
+ {
1413
+ "id": "flat_metadata_indices",
1414
+ "priority": 40,
1415
+ "when": ["flatMetadataContract", "present.indices", "not present.inverse_indices", "not present.counts"],
1416
+ "constants": { "sorted": "attrs.sorted != 0" },
1417
+ "passes": [
1418
+ {
1419
+ "id": "main",
1420
+ "name": "Unique.Metadata",
1421
+ "source": {
1422
+ "shader": "unique.wgsl.jinja",
1423
+ "inputs": { "hasIndices": true, "hasInverseIndices": false, "hasCounts": false }
1424
+ },
1425
+ "bindings": "flatMetadataIndices",
1426
+ "dispatch": { "x": 1 }
1427
+ }
1428
+ ]
1429
+ },
1430
+ {
1431
+ "id": "axis_metadata_indices",
1432
+ "priority": 40,
1433
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "present.indices", "not present.inverse_indices", "not present.counts"],
1434
+ "constants": { "sorted": "attrs.sorted != 0" },
1435
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1436
+ "passes": [
1437
+ {
1438
+ "id": "main",
1439
+ "name": "Unique.AxisMetadata",
1440
+ "source": {
1441
+ "shader": "unique-axis.wgsl.jinja",
1442
+ "inputs": {
1443
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1444
+ "hasIndices": true,
1445
+ "hasInverseIndices": false,
1446
+ "hasCounts": false,
1447
+ "headCacheSlots": "axisHeadCacheSlots"
1448
+ }
1449
+ },
1450
+ "bindings": "axisMetadataIndices",
1451
+ "dispatch": { "x": 1 }
1452
+ }
1453
+ ]
1454
+ },
1455
+ {
1456
+ "id": "flat_metadata_inverse",
1457
+ "priority": 40,
1458
+ "when": ["flatMetadataContract", "not present.indices", "present.inverse_indices", "not present.counts"],
1459
+ "constants": { "sorted": "attrs.sorted != 0" },
1460
+ "passes": [
1461
+ {
1462
+ "id": "main",
1463
+ "name": "Unique.Metadata",
1464
+ "source": {
1465
+ "shader": "unique.wgsl.jinja",
1466
+ "inputs": { "hasIndices": false, "hasInverseIndices": true, "hasCounts": false }
1467
+ },
1468
+ "bindings": "flatMetadataInverse",
1469
+ "dispatch": { "x": 1 }
1470
+ }
1471
+ ]
1472
+ },
1473
+ {
1474
+ "id": "axis_metadata_inverse",
1475
+ "priority": 40,
1476
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "not present.indices", "present.inverse_indices", "not present.counts"],
1477
+ "constants": { "sorted": "attrs.sorted != 0" },
1478
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1479
+ "passes": [
1480
+ {
1481
+ "id": "main",
1482
+ "name": "Unique.AxisMetadata",
1483
+ "source": {
1484
+ "shader": "unique-axis.wgsl.jinja",
1485
+ "inputs": {
1486
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1487
+ "hasIndices": false,
1488
+ "hasInverseIndices": true,
1489
+ "hasCounts": false,
1490
+ "headCacheSlots": "axisHeadCacheSlots"
1491
+ }
1492
+ },
1493
+ "bindings": "axisMetadataInverse",
1494
+ "dispatch": { "x": 1 }
1495
+ }
1496
+ ]
1497
+ },
1498
+ {
1499
+ "id": "flat_metadata_counts",
1500
+ "priority": 40,
1501
+ "when": ["flatMetadataContract", "not present.indices", "not present.inverse_indices", "present.counts"],
1502
+ "constants": { "sorted": "attrs.sorted != 0" },
1503
+ "passes": [
1504
+ {
1505
+ "id": "main",
1506
+ "name": "Unique.Metadata",
1507
+ "source": {
1508
+ "shader": "unique.wgsl.jinja",
1509
+ "inputs": { "hasIndices": false, "hasInverseIndices": false, "hasCounts": true }
1510
+ },
1511
+ "bindings": "flatMetadataCounts",
1512
+ "dispatch": { "x": 1 }
1513
+ }
1514
+ ]
1515
+ },
1516
+ {
1517
+ "id": "axis_metadata_counts",
1518
+ "priority": 40,
1519
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "not present.indices", "not present.inverse_indices", "present.counts"],
1520
+ "constants": { "sorted": "attrs.sorted != 0" },
1521
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1522
+ "passes": [
1523
+ {
1524
+ "id": "main",
1525
+ "name": "Unique.AxisMetadata",
1526
+ "source": {
1527
+ "shader": "unique-axis.wgsl.jinja",
1528
+ "inputs": {
1529
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1530
+ "hasIndices": false,
1531
+ "hasInverseIndices": false,
1532
+ "hasCounts": true,
1533
+ "headCacheSlots": "axisHeadCacheSlots"
1534
+ }
1535
+ },
1536
+ "bindings": "axisMetadataCounts",
1537
+ "dispatch": { "x": 1 }
1538
+ }
1539
+ ]
1540
+ },
1541
+ {
1542
+ "id": "flat_metadata_indices_inverse",
1543
+ "priority": 40,
1544
+ "when": ["flatMetadataContract", "present.indices", "present.inverse_indices", "not present.counts"],
1545
+ "constants": { "sorted": "attrs.sorted != 0" },
1546
+ "passes": [
1547
+ {
1548
+ "id": "main",
1549
+ "name": "Unique.Metadata",
1550
+ "source": {
1551
+ "shader": "unique.wgsl.jinja",
1552
+ "inputs": { "hasIndices": true, "hasInverseIndices": true, "hasCounts": false }
1553
+ },
1554
+ "bindings": "flatMetadataIndicesInverse",
1555
+ "dispatch": { "x": 1 }
1556
+ }
1557
+ ]
1558
+ },
1559
+ {
1560
+ "id": "axis_metadata_indices_inverse",
1561
+ "priority": 40,
1562
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "present.indices", "present.inverse_indices", "not present.counts"],
1563
+ "constants": { "sorted": "attrs.sorted != 0" },
1564
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1565
+ "passes": [
1566
+ {
1567
+ "id": "main",
1568
+ "name": "Unique.AxisMetadata",
1569
+ "source": {
1570
+ "shader": "unique-axis.wgsl.jinja",
1571
+ "inputs": {
1572
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1573
+ "hasIndices": true,
1574
+ "hasInverseIndices": true,
1575
+ "hasCounts": false,
1576
+ "headCacheSlots": "axisHeadCacheSlots"
1577
+ }
1578
+ },
1579
+ "bindings": "axisMetadataIndicesInverse",
1580
+ "dispatch": { "x": 1 }
1581
+ }
1582
+ ]
1583
+ },
1584
+ {
1585
+ "id": "flat_metadata_indices_counts",
1586
+ "priority": 40,
1587
+ "when": ["flatMetadataContract", "present.indices", "not present.inverse_indices", "present.counts"],
1588
+ "constants": { "sorted": "attrs.sorted != 0" },
1589
+ "passes": [
1590
+ {
1591
+ "id": "main",
1592
+ "name": "Unique.Metadata",
1593
+ "source": {
1594
+ "shader": "unique.wgsl.jinja",
1595
+ "inputs": { "hasIndices": true, "hasInverseIndices": false, "hasCounts": true }
1596
+ },
1597
+ "bindings": "flatMetadataIndicesCounts",
1598
+ "dispatch": { "x": 1 }
1599
+ }
1600
+ ]
1601
+ },
1602
+ {
1603
+ "id": "axis_metadata_indices_counts",
1604
+ "priority": 40,
1605
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "present.indices", "not present.inverse_indices", "present.counts"],
1606
+ "constants": { "sorted": "attrs.sorted != 0" },
1607
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1608
+ "passes": [
1609
+ {
1610
+ "id": "main",
1611
+ "name": "Unique.AxisMetadata",
1612
+ "source": {
1613
+ "shader": "unique-axis.wgsl.jinja",
1614
+ "inputs": {
1615
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1616
+ "hasIndices": true,
1617
+ "hasInverseIndices": false,
1618
+ "hasCounts": true,
1619
+ "headCacheSlots": "axisHeadCacheSlots"
1620
+ }
1621
+ },
1622
+ "bindings": "axisMetadataIndicesCounts",
1623
+ "dispatch": { "x": 1 }
1624
+ }
1625
+ ]
1626
+ },
1627
+ {
1628
+ "id": "flat_metadata_inverse_counts",
1629
+ "priority": 40,
1630
+ "when": ["flatMetadataContract", "not present.indices", "present.inverse_indices", "present.counts"],
1631
+ "constants": { "sorted": "attrs.sorted != 0" },
1632
+ "passes": [
1633
+ {
1634
+ "id": "main",
1635
+ "name": "Unique.Metadata",
1636
+ "source": {
1637
+ "shader": "unique.wgsl.jinja",
1638
+ "inputs": { "hasIndices": false, "hasInverseIndices": true, "hasCounts": true }
1639
+ },
1640
+ "bindings": "flatMetadataInverseCounts",
1641
+ "dispatch": { "x": 1 }
1642
+ }
1643
+ ]
1644
+ },
1645
+ {
1646
+ "id": "axis_metadata_inverse_counts",
1647
+ "priority": 40,
1648
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "not present.indices", "present.inverse_indices", "present.counts"],
1649
+ "constants": { "sorted": "attrs.sorted != 0" },
1650
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1651
+ "passes": [
1652
+ {
1653
+ "id": "main",
1654
+ "name": "Unique.AxisMetadata",
1655
+ "source": {
1656
+ "shader": "unique-axis.wgsl.jinja",
1657
+ "inputs": {
1658
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1659
+ "hasIndices": false,
1660
+ "hasInverseIndices": true,
1661
+ "hasCounts": true,
1662
+ "headCacheSlots": "axisHeadCacheSlots"
1663
+ }
1664
+ },
1665
+ "bindings": "axisMetadataInverseCounts",
1666
+ "dispatch": { "x": 1 }
1667
+ }
1668
+ ]
1669
+ },
1670
+ {
1671
+ "id": "flat_metadata_all",
1672
+ "priority": 40,
1673
+ "when": ["flatMetadataContract", "present.indices", "present.inverse_indices", "present.counts"],
1674
+ "constants": { "sorted": "attrs.sorted != 0" },
1675
+ "passes": [
1676
+ {
1677
+ "id": "main",
1678
+ "name": "Unique.Metadata",
1679
+ "source": {
1680
+ "shader": "unique.wgsl.jinja",
1681
+ "inputs": { "hasIndices": true, "hasInverseIndices": true, "hasCounts": true }
1682
+ },
1683
+ "bindings": "flatMetadataAll",
1684
+ "dispatch": { "x": 1 }
1685
+ }
1686
+ ]
1687
+ },
1688
+ {
1689
+ "id": "axis_metadata_all",
1690
+ "priority": 40,
1691
+ "when": ["axisMetadataContract", "axisOrderScratchFits", "present.indices", "present.inverse_indices", "present.counts"],
1692
+ "constants": { "sorted": "attrs.sorted != 0" },
1693
+ "intermediates": [{ "id": "order", "dtype": "uint32", "shape": "[max(1, axisOutputDim)]" }],
1694
+ "passes": [
1695
+ {
1696
+ "id": "main",
1697
+ "name": "Unique.AxisMetadata",
1698
+ "source": {
1699
+ "shader": "unique-axis.wgsl.jinja",
1700
+ "inputs": {
1701
+ "capacity": "max(1, dim(shapes.Y, normalizedAxis))",
1702
+ "hasIndices": true,
1703
+ "hasInverseIndices": true,
1704
+ "hasCounts": true,
1705
+ "headCacheSlots": "axisHeadCacheSlots"
1706
+ }
1707
+ },
1708
+ "bindings": "axisMetadataAll",
1709
+ "dispatch": { "x": 1 }
1710
+ }
1711
+ ]
1712
+ }
1713
+ ]
1714
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai.onnx.Unique",
3
+ "id": "_ai_onnx_unique_webgpu_0d47131",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "uWuMe2FMuN7vfuafXZShcUDGe/C8rp2oVHckKukc+pQ=",
11
+ "manifest.json": "b9zSmspb5Oo2oSITmFfU4X/mYZnv0Bm1Uu8caVO1jEg=",
12
+ "test.json": "dcDsfDw6xJR+OIbeTcptXvo7dV21lPAiDnElSLafb0Q=",
13
+ "unique-axis-compact-sort.wgsl.jinja": "RAgXG0xLG/Yw3PwOPrrJvXOOMINPBQ/REenbvq73Ak0=",
14
+ "unique-axis-dedup.wgsl.jinja": "O1EWypziZGw+vSmIZWyYo7IBihHTAEEYrUtUSdA+sk4=",
15
+ "unique-axis-hash.wgsl.jinja": "tLAuHVQcTX2Q+/gIbwupvPWe+85TP9W4oyxAvc1QLcY=",
16
+ "unique-axis-scatter.wgsl.jinja": "54tyC3ECbMR48qFXnBQ2el6+tMdOipaM+l91vySKeL8=",
17
+ "unique-axis.wgsl.jinja": "fjQxtH8MjP43bhdPOcxya+xVerIi30R4cgs7URqc71s=",
18
+ "unique-compact-sort.wgsl.jinja": "jeh9pkkZIYSv2jQA/StxvAaQTVp/sI0eUsekeYSnkvo=",
19
+ "unique-dedup.wgsl.jinja": "muTd580L/rfaL/p5NjOwMPp2F3ALTR9S7SzVYl9r+wQ=",
20
+ "unique-hash-build.wgsl.jinja": "HkomLl6SIyeWaOI0dRrXbl2chUwav16ClFwxZ+EDZzY=",
21
+ "unique-hash-collect.wgsl.jinja": "5+RKxUzJlrcqJ3Z/RfyznHl43WSoVKJSwGlQ/KzwnI4=",
22
+ "unique-hash-init.wgsl.jinja": "d4tz0KAxmNaRn4ZvRQemr+7wk4GwRZY9tLQguTuIcGo=",
23
+ "unique-hash-mark.wgsl.jinja": "87mwSXE1BpY/3zHxtaOhLpm4IZAF8hSX6bLvr6GfncE=",
24
+ "unique-hash-sort-collected-key-only.wgsl.jinja": "Rpuf6QQZwx2XvogNtJoTS5/vsNHbA721YwBqwWt0SLQ=",
25
+ "unique.wgsl.jinja": "J4xqWcvx4Ca4uW4ZbJ1xG2pfZbvL3cdnYU33fgHg1eo="
26
+ }
27
+ },
28
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
29
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.Unique" }
30
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,1530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.Unique",
3
+ "fixtureArrays": {
4
+ "ort_axis1_unsorted_f32_input_x": [1, 1, 0, 1, 2, 1, 0, 1, 1, 1, 0, 1, 2, 1, 0, 1],
5
+ "serial_sorted_dense_duplicates_int32_input_x": [37, -5, 12, 99, -73, 0, 41, 8, -21, 64, 3, -90, 55, 17, -2, 76, 29, -48, 83, 6, -33, 92, 14, -67, 50, 22, -9, 70, 35, -58, 88, 1, -26, 95, 19, -81, 46, 27, -14, 61, 33, -44, 79, 10, -39, 53, 24, -86]
6
+ },
7
+ "cases": [
8
+ {
9
+ "name": "sorted_int32",
10
+ "inputs": {
11
+ "x": { "dtype": "int32", "shape": [8], "data": { "kind": "values", "values": [3, 1, 3, 2, 1, -1, 2, 4] } }
12
+ },
13
+ "outputs": { "y": { "dtype": "int32", "shape": [5] } }
14
+ },
15
+ {
16
+ "name": "first_seen_uint8_overflow",
17
+ "attrs": { "sorted": 0 },
18
+ "inputs": {
19
+ "x": { "dtype": "uint8", "shape": [7], "data": { "kind": "values", "values": [5, 6, 5, 7, 8, 9, 10] } }
20
+ },
21
+ "outputs": { "y": { "dtype": "uint8", "shape": [6] } }
22
+ },
23
+ {
24
+ "name": "sorted_f32",
25
+ "inputs": {
26
+ "x": {
27
+ "dtype": "float32",
28
+ "shape": [6],
29
+ "data": { "kind": "values", "values": [2.5, -1.0, 2.5, 0.0, -1.0, 4.0] }
30
+ }
31
+ },
32
+ "outputs": { "y": { "dtype": "float32", "shape": [4] } }
33
+ },
34
+ {
35
+ "name": "f32_subnormal_distinct_from_zero",
36
+ "provenance": {
37
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
38
+ "test": "Unique.Flatten_Unsorted",
39
+ "notes": "Subnormal finite values are distinct from zero and preserve first-seen order when sorted=0."
40
+ },
41
+ "attrs": { "sorted": 0 },
42
+ "inputs": {
43
+ "x": {
44
+ "dtype": "float32",
45
+ "shape": [5],
46
+ "data": { "kind": "values", "values": [0.0, 1e-40, 0.0, -1e-40, 1e-40] }
47
+ }
48
+ },
49
+ "outputs": {
50
+ "y": {
51
+ "dtype": "float32",
52
+ "shape": [3],
53
+ "tolerance": 0,
54
+ "data": { "kind": "values", "values": [0.0, 1e-40, -1e-40] }
55
+ }
56
+ }
57
+ },
58
+ {
59
+ "name": "f32_sorted_subnormal_distinct_from_zero",
60
+ "provenance": {
61
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
62
+ "test": "Unique.Flatten_Sorted",
63
+ "notes": "Sorted Unique must keep negative subnormal, zero, and positive subnormal as distinct ordered buckets."
64
+ },
65
+ "attrs": { "sorted": 1 },
66
+ "inputs": {
67
+ "x": {
68
+ "dtype": "float32",
69
+ "shape": [5],
70
+ "data": { "kind": "values", "values": [0.0, 1e-40, -1e-40, 0.0, 1e-40] }
71
+ }
72
+ },
73
+ "outputs": {
74
+ "y": {
75
+ "dtype": "float32",
76
+ "shape": [3],
77
+ "tolerance": 0,
78
+ "data": { "kind": "values", "values": [-1e-40, 0.0, 1e-40] }
79
+ }
80
+ }
81
+ },
82
+ {
83
+ "name": "ort_unsorted_nan_equivalence",
84
+ "provenance": {
85
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
86
+ "test": "Unique.Flatten_Unsorted",
87
+ "notes": "Additional ORT CPU probe: ordered-map lower_bound makes NaN comparator-equivalent to its candidate bucket at insertion, so later finite values still deduplicate normally."
88
+ },
89
+ "attrs": { "sorted": 0 },
90
+ "inputs": {
91
+ "x": { "dtype": "float32", "shape": [5], "data": { "kind": "values", "values": [2.0, "NaN", 2.0, "NaN", 3.0] } }
92
+ },
93
+ "outputs": {
94
+ "y": { "dtype": "float32", "shape": [2], "tolerance": 0, "data": { "kind": "values", "values": [2.0, 3.0] } }
95
+ }
96
+ },
97
+ {
98
+ "name": "ort_backend_unsorted_first_seen_f32",
99
+ "provenance": {
100
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
101
+ "test": "Unique.Flatten_Unsorted_MatchOnnxTest"
102
+ },
103
+ "attrs": { "sorted": 0 },
104
+ "inputs": {
105
+ "x": {
106
+ "dtype": "float32",
107
+ "shape": [6],
108
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
109
+ }
110
+ },
111
+ "outputs": { "y": { "dtype": "float32", "shape": [4], "tolerance": 0 } }
112
+ },
113
+ {
114
+ "name": "ort_flatten_unsorted_f32",
115
+ "provenance": {
116
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
117
+ "test": "Unique.Flatten_Unsorted"
118
+ },
119
+ "attrs": { "sorted": 0 },
120
+ "inputs": {
121
+ "x": {
122
+ "dtype": "float32",
123
+ "shape": [6],
124
+ "data": { "kind": "values", "values": [1.0, 4.0, 1.0, 2.0, 2.0, 0.0] }
125
+ }
126
+ },
127
+ "outputs": { "y": { "dtype": "float32", "shape": [4], "tolerance": 0 } }
128
+ },
129
+ {
130
+ "name": "ort_flatten_sorted_f32",
131
+ "provenance": {
132
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
133
+ "test": "Unique.Flatten_Sorted"
134
+ },
135
+ "attrs": { "sorted": 1 },
136
+ "inputs": {
137
+ "x": {
138
+ "dtype": "float32",
139
+ "shape": [6],
140
+ "data": { "kind": "values", "values": [1.0, 4.0, 1.0, 2.0, 2.0, 0.0] }
141
+ }
142
+ },
143
+ "outputs": { "y": { "dtype": "float32", "shape": [4], "tolerance": 0 } }
144
+ },
145
+ {
146
+ "name": "ort_empty_input_f32",
147
+ "provenance": { "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc", "test": "Unique.EmptyInput" },
148
+ "inputs": { "x": { "dtype": "float32", "shape": [0], "data": { "kind": "values", "values": [] } } },
149
+ "outputs": { "y": { "dtype": "float32", "shape": [0], "tolerance": 0 } }
150
+ },
151
+ {
152
+ "name": "length1_f32_backend",
153
+ "inputs": { "x": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0] } } },
154
+ "outputs": { "y": { "dtype": "float32", "shape": [1], "tolerance": 0 } }
155
+ },
156
+ {
157
+ "name": "all_duplicates_exact_output",
158
+ "attrs": { "sorted": 0 },
159
+ "inputs": {
160
+ "x": {
161
+ "dtype": "float32",
162
+ "shape": [5],
163
+ "data": { "kind": "values", "values": [-7.0, -7.0, -7.0, -7.0, -7.0] }
164
+ }
165
+ },
166
+ "outputs": { "y": { "dtype": "float32", "shape": [1], "tolerance": 0 } }
167
+ },
168
+ {
169
+ "name": "int8_signed_sorted_order",
170
+ "inputs": {
171
+ "x": {
172
+ "dtype": "int8",
173
+ "shape": [8],
174
+ "data": { "kind": "values", "values": [3, -1, -128, 3, 127, -1, 0, -128] }
175
+ }
176
+ },
177
+ "outputs": { "y": { "dtype": "int8", "shape": [5], "tolerance": 0 } }
178
+ },
179
+ {
180
+ "name": "ort_no_optional_output_int8_sorted",
181
+ "provenance": {
182
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
183
+ "test": "Unique.NoOptionalOutput",
184
+ "notes": "The fixture requests only Y, the standard required output, with its exact data-dependent shape."
185
+ },
186
+ "inputs": {
187
+ "x": { "dtype": "int8", "shape": [8], "data": { "kind": "values", "values": [1, 4, -1, 2, 2, 0, -1, 4] } }
188
+ },
189
+ "outputs": { "y": { "dtype": "int8", "shape": [5], "tolerance": 0 } }
190
+ },
191
+ {
192
+ "name": "ort_axis0_unsorted_f32",
193
+ "provenance": {
194
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
195
+ "test": "Unique.Axis0_Unsorted",
196
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
197
+ },
198
+ "attrs": { "axis": 0, "sorted": 0 },
199
+ "inputs": {
200
+ "x": {
201
+ "dtype": "float32",
202
+ "shape": [4, 2],
203
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0] }
204
+ }
205
+ },
206
+ "outputs": {
207
+ "y": {
208
+ "dtype": "float32",
209
+ "shape": [3, 2],
210
+ "tolerance": 0,
211
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 1.0, 1.0, 0.0] }
212
+ }
213
+ }
214
+ },
215
+ {
216
+ "name": "ort_axis0_sorted_f32",
217
+ "provenance": {
218
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
219
+ "test": "Unique.Axis0_Sorted",
220
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
221
+ },
222
+ "attrs": { "axis": 0, "sorted": 1 },
223
+ "inputs": {
224
+ "x": {
225
+ "dtype": "float32",
226
+ "shape": [4, 2],
227
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0] }
228
+ }
229
+ },
230
+ "outputs": {
231
+ "y": {
232
+ "dtype": "float32",
233
+ "shape": [3, 2],
234
+ "tolerance": 0,
235
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 0.0, 1.0, 1.0] }
236
+ }
237
+ }
238
+ },
239
+ {
240
+ "name": "ort_axis1_unsorted_f32",
241
+ "provenance": {
242
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
243
+ "test": "Unique.Axis1_Unsorted",
244
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
245
+ },
246
+ "attrs": { "axis": 1, "sorted": 0 },
247
+ "inputs": {
248
+ "x": {
249
+ "dtype": "float32",
250
+ "shape": [2, 4, 2],
251
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_axis1_unsorted_f32_input_x" } }
252
+ }
253
+ },
254
+ "outputs": {
255
+ "y": {
256
+ "dtype": "float32",
257
+ "shape": [2, 3, 2],
258
+ "tolerance": 0,
259
+ "data": { "kind": "values", "values": [1.0, 1.0, 0.0, 1.0, 2.0, 1.0, 1.0, 1.0, 0.0, 1.0, 2.0, 1.0] }
260
+ }
261
+ }
262
+ },
263
+ {
264
+ "name": "ort_axis1_sorted_f32",
265
+ "provenance": {
266
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
267
+ "test": "Unique.Axis1_Sorted",
268
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
269
+ },
270
+ "attrs": { "axis": 1, "sorted": 1 },
271
+ "inputs": {
272
+ "x": {
273
+ "dtype": "float32",
274
+ "shape": [2, 4, 2],
275
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_axis1_unsorted_f32_input_x" } }
276
+ }
277
+ },
278
+ "outputs": {
279
+ "y": {
280
+ "dtype": "float32",
281
+ "shape": [2, 3, 2],
282
+ "tolerance": 0,
283
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0] }
284
+ }
285
+ }
286
+ },
287
+ {
288
+ "name": "ort_axis2_unsorted_f32",
289
+ "provenance": {
290
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
291
+ "test": "Unique.Axis2_Unsorted",
292
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
293
+ },
294
+ "attrs": { "axis": 2, "sorted": 0 },
295
+ "inputs": {
296
+ "x": {
297
+ "dtype": "float32",
298
+ "shape": [2, 2, 4],
299
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_axis1_unsorted_f32_input_x" } }
300
+ }
301
+ },
302
+ "outputs": {
303
+ "y": {
304
+ "dtype": "float32",
305
+ "shape": [2, 2, 3],
306
+ "tolerance": 0,
307
+ "data": { "kind": "values", "values": [1.0, 1.0, 0.0, 2.0, 1.0, 0.0, 1.0, 1.0, 0.0, 2.0, 1.0, 0.0] }
308
+ }
309
+ }
310
+ },
311
+ {
312
+ "name": "ort_axis2_sorted_f32",
313
+ "provenance": {
314
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
315
+ "test": "Unique.Axis2_Sorted",
316
+ "notes": "The fixture supplies the exact data-dependent Y shape and requests no optional metadata outputs."
317
+ },
318
+ "attrs": { "axis": 2, "sorted": 1 },
319
+ "inputs": {
320
+ "x": {
321
+ "dtype": "float32",
322
+ "shape": [2, 2, 4],
323
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_axis1_unsorted_f32_input_x" } }
324
+ }
325
+ },
326
+ "outputs": {
327
+ "y": {
328
+ "dtype": "float32",
329
+ "shape": [2, 2, 3],
330
+ "tolerance": 0,
331
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 0.0, 1.0, 2.0, 0.0, 1.0, 1.0, 0.0, 1.0, 2.0] }
332
+ }
333
+ }
334
+ },
335
+ {
336
+ "name": "ort_negative_axis_last_sorted_f32",
337
+ "provenance": {
338
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
339
+ "test": "Unique.Axis2_Sorted",
340
+ "notes": "Same semantic case as ORT's positive axis=2 coverage, expressed with the ONNX-valid negative last-axis spelling and an exact data-dependent Y shape."
341
+ },
342
+ "attrs": { "axis": -1, "sorted": 1 },
343
+ "inputs": {
344
+ "x": {
345
+ "dtype": "float32",
346
+ "shape": [2, 2, 4],
347
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_axis1_unsorted_f32_input_x" } }
348
+ }
349
+ },
350
+ "outputs": {
351
+ "y": {
352
+ "dtype": "float32",
353
+ "shape": [2, 2, 3],
354
+ "tolerance": 0,
355
+ "data": { "kind": "values", "values": [0.0, 1.0, 1.0, 0.0, 1.0, 2.0, 0.0, 1.0, 1.0, 0.0, 1.0, 2.0] }
356
+ }
357
+ }
358
+ },
359
+ {
360
+ "name": "onnx_backend_unique_length_1",
361
+ "attrs": { "sorted": 1 },
362
+ "inputs": { "x": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [0] } } },
363
+ "outputs": { "y": { "dtype": "int32", "shape": [1] } },
364
+ "provenance": {
365
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_unique_length_1",
366
+ "notes": "The fixture supplies the exact data-dependent Y shape. Optional ONNX int64 metadata outputs are omitted here; requested metadata is represented as uint32 where values are representable."
367
+ }
368
+ },
369
+ {
370
+ "name": "onnx_backend_unique_not_sorted_without_axis",
371
+ "attrs": { "sorted": 0 },
372
+ "inputs": {
373
+ "x": {
374
+ "dtype": "float32",
375
+ "shape": [6],
376
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
377
+ }
378
+ },
379
+ "outputs": { "y": { "dtype": "float32", "shape": [4] } },
380
+ "provenance": {
381
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_unique_not_sorted_without_axis",
382
+ "notes": "The fixture supplies the exact data-dependent Y shape. Optional ONNX int64 metadata outputs are omitted here; requested metadata is represented as uint32 where values are representable."
383
+ }
384
+ },
385
+ {
386
+ "name": "onnx_backend_unique_sorted_without_axis",
387
+ "attrs": { "sorted": 1 },
388
+ "inputs": {
389
+ "x": {
390
+ "dtype": "float32",
391
+ "shape": [6],
392
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
393
+ }
394
+ },
395
+ "outputs": { "y": { "dtype": "float32", "shape": [4] } },
396
+ "provenance": {
397
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_unique_sorted_without_axis",
398
+ "notes": "The fixture supplies the exact data-dependent Y shape. Optional ONNX int64 metadata outputs are omitted here; requested metadata is represented as uint32 where values are representable."
399
+ }
400
+ },
401
+ {
402
+ "name": "serial_sorted_dense_duplicates_int32",
403
+ "provenance": {
404
+ "notes": "2048 elements cycling 48 distinct values exercise the deduplication early exit and sorted exchange sort with an exact 48-element result."
405
+ },
406
+ "inputs": {
407
+ "x": {
408
+ "dtype": "int32",
409
+ "shape": [2048],
410
+ "data": {
411
+ "kind": "cycle",
412
+ "values": { "$ref": "#/fixtureArrays/serial_sorted_dense_duplicates_int32_input_x" }
413
+ }
414
+ }
415
+ },
416
+ "outputs": { "y": { "dtype": "int32", "shape": [48], "tolerance": 0 } }
417
+ },
418
+ {
419
+ "name": "serial_first_seen_order_f32_1024",
420
+ "attrs": { "sorted": 0 },
421
+ "provenance": {
422
+ "notes": "First-seen order is preserved over 1024 elements with 32 distinct values and an exact 32-element result."
423
+ },
424
+ "inputs": {
425
+ "x": {
426
+ "dtype": "float32",
427
+ "shape": [1024],
428
+ "data": {
429
+ "kind": "cycle",
430
+ "values": [0.5, -1.25, 3.0, 7.75, -0.5, 2.25, 9.0, -4.5, 1.5, 6.25, -8.0, 0.25, 5.5, -2.75, 4.0, 8.5, -6.25, 1.75, 7.25, -3.5, 2.5, 9.75, -0.75, 5.25, -7.5, 3.25, 6.75, -1.5, 4.75, 8.25, -5.75, 0.75]
431
+ }
432
+ }
433
+ },
434
+ "outputs": { "y": { "dtype": "float32", "shape": [32], "tolerance": 0 } }
435
+ },
436
+ {
437
+ "name": "serial_f32_subnormal_distinct_from_zero",
438
+ "attrs": { "sorted": 0 },
439
+ "provenance": {
440
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
441
+ "test": "Unique.Flatten_Unsorted",
442
+ "notes": "Large-input companion to the scalar subnormal case: the exact serial float comparator keeps finite subnormal values distinct from zero and emits three first-seen buckets."
443
+ },
444
+ "inputs": {
445
+ "x": { "dtype": "float32", "shape": [4096], "data": { "kind": "cycle", "values": [0.0, 1e-40, 0.0, -1e-40] } }
446
+ },
447
+ "outputs": {
448
+ "y": {
449
+ "dtype": "float32",
450
+ "shape": [3],
451
+ "tolerance": 0,
452
+ "data": { "kind": "values", "values": [0.0, 1e-40, -1e-40] }
453
+ }
454
+ }
455
+ },
456
+ {
457
+ "name": "serial_sorted_f32_32k_subnormal",
458
+ "attrs": { "sorted": 1 },
459
+ "provenance": {
460
+ "notes": "32768 f32 elements cycling six distinct values, including positive and negative subnormals, exercise the exact ordered-map float path at scale. Subnormals remain bit-distinct from zero and from each other, and sorted output uses the IEEE total-order key."
461
+ },
462
+ "inputs": {
463
+ "x": {
464
+ "dtype": "float32",
465
+ "shape": [32768],
466
+ "data": { "kind": "cycle", "values": [0.0, 1e-40, -1e-40, 2.5, -3.5, 7.0] }
467
+ }
468
+ },
469
+ "outputs": { "y": { "dtype": "float32", "shape": [6], "tolerance": 0 } }
470
+ },
471
+ {
472
+ "name": "serial_unsorted_first_seen_f32_32k_subnormal",
473
+ "attrs": { "sorted": 0 },
474
+ "provenance": {
475
+ "notes": "First-seen-order companion for the exact serial float path: comparator-order buckets are reordered by representative input index for sorted=0."
476
+ },
477
+ "inputs": {
478
+ "x": {
479
+ "dtype": "float32",
480
+ "shape": [32768],
481
+ "data": { "kind": "cycle", "values": [0.0, 1e-40, -1e-40, 2.5, -3.5, 7.0] }
482
+ }
483
+ },
484
+ "outputs": { "y": { "dtype": "float32", "shape": [6], "tolerance": 0 } }
485
+ },
486
+ {
487
+ "name": "serial_f32_32k_nan_lower_bound",
488
+ "attrs": { "sorted": 0 },
489
+ "provenance": {
490
+ "source": "onnxruntime/test/providers/cpu/tensor/unique_op_test.cc",
491
+ "test": "Unique.Flatten_Unsorted",
492
+ "notes": "NaN equivalence on the exact ordered-map float path at large input size: each NaN resolves to the lower-bound bucket headed by 2 and forms no new bucket."
493
+ },
494
+ "inputs": {
495
+ "x": {
496
+ "dtype": "float32",
497
+ "shape": [32768],
498
+ "data": { "kind": "cycle", "values": [2.0, "NaN", 3.0, "NaN", 5.0] }
499
+ }
500
+ },
501
+ "outputs": {
502
+ "y": {
503
+ "dtype": "float32",
504
+ "shape": [3],
505
+ "tolerance": 0,
506
+ "data": { "kind": "values", "values": [2.0, 3.0, 5.0] }
507
+ }
508
+ }
509
+ },
510
+ {
511
+ "name": "hash_sorted_int32_32k",
512
+ "attrs": { "sorted": 1 },
513
+ "provenance": {
514
+ "notes": "32768 int32 elements cycling 48 distinct values: exercises the hash-set parallel dedup (numel >= 32768 floor) for the sorted flatten path. Bit-exact vs the CPU reference."
515
+ },
516
+ "inputs": {
517
+ "x": {
518
+ "dtype": "int32",
519
+ "shape": [32768],
520
+ "data": {
521
+ "kind": "cycle",
522
+ "values": { "$ref": "#/fixtureArrays/serial_sorted_dense_duplicates_int32_input_x" }
523
+ }
524
+ }
525
+ },
526
+ "outputs": { "y": { "dtype": "int32", "shape": [48], "tolerance": 0 } }
527
+ },
528
+ {
529
+ "name": "hash_unsorted_first_seen_int32_32k",
530
+ "attrs": { "sorted": 0 },
531
+ "provenance": {
532
+ "notes": "32768 int32 elements cycling 48 distinct values, first-seen order: the hash dedup folds in atomicMin(index), so the compacted order matches the appearance order of the parallel/serial paths exactly."
533
+ },
534
+ "inputs": {
535
+ "x": {
536
+ "dtype": "int32",
537
+ "shape": [32768],
538
+ "data": {
539
+ "kind": "cycle",
540
+ "values": { "$ref": "#/fixtureArrays/serial_sorted_dense_duplicates_int32_input_x" }
541
+ }
542
+ }
543
+ },
544
+ "outputs": { "y": { "dtype": "int32", "shape": [48], "tolerance": 0 } }
545
+ },
546
+ {
547
+ "name": "hash_sentinel_value_minus_one_int32_64k",
548
+ "attrs": { "sorted": 1 },
549
+ "provenance": {
550
+ "notes": "65536 int32 elements whose distinct set includes -1 (bitcast<u32> == 0xffffffff, the hash table's EMPTY sentinel). Exercises the dedicated `special` min-index slot the hash dedup uses for the one value that cannot be a hash key."
551
+ },
552
+ "inputs": {
553
+ "x": {
554
+ "dtype": "int32",
555
+ "shape": [65536],
556
+ "data": {
557
+ "kind": "cycle",
558
+ "values": [-1, 7, -1, 3, 100, -50, -1, 42, 7, 3, -2147483648, 2147483647, 0, -1, 13, 100]
559
+ }
560
+ }
561
+ },
562
+ "outputs": { "y": { "dtype": "int32", "shape": [10], "tolerance": 0 } }
563
+ },
564
+ {
565
+ "name": "hash_uint8_sorted_32k",
566
+ "attrs": { "sorted": 1 },
567
+ "provenance": {
568
+ "notes": "32768 uint8 elements cycling 12 distinct values: exercises the hash dedup's unsigned-key path (dtypes.T == u32 carried width) at the >= 32768 floor."
569
+ },
570
+ "inputs": {
571
+ "x": {
572
+ "dtype": "uint8",
573
+ "shape": [32768],
574
+ "data": { "kind": "cycle", "values": [5, 200, 17, 5, 255, 0, 128, 17, 64, 200, 3, 250, 0, 5] }
575
+ }
576
+ },
577
+ "outputs": { "y": { "dtype": "uint8", "shape": [9], "tolerance": 0 } }
578
+ },
579
+ {
580
+ "name": "hash_uint32_few_distinct_64k",
581
+ "attrs": { "sorted": 1 },
582
+ "provenance": {
583
+ "notes": "65536 uint32 elements with only 6 distinct values (heavy duplicates) including 0xffffffff (the EMPTY sentinel): few-distinct stress for the hash dedup atomicMin contention plus the unsigned special-slot path."
584
+ },
585
+ "inputs": {
586
+ "x": {
587
+ "dtype": "uint32",
588
+ "shape": [65536],
589
+ "data": { "kind": "cycle", "values": [4294967295, 0, 7, 4294967295, 1000000, 42, 7, 0] }
590
+ }
591
+ },
592
+ "outputs": { "y": { "dtype": "uint32", "shape": [5], "tolerance": 0 } }
593
+ },
594
+ {
595
+ "name": "axis0_f32_subnormal_collapse_unsorted",
596
+ "attrs": { "axis": 0, "sorted": 0 },
597
+ "provenance": {
598
+ "notes": "Spec-valid axis-mode deduplication over a [N,2] tensor. Raw-bit float comparisons keep finite subnormals distinct from zero even on FTZ GPUs; the pinned output is cross-checked with ORT."
599
+ },
600
+ "inputs": {
601
+ "x": {
602
+ "dtype": "float32",
603
+ "shape": [5, 2],
604
+ "data": { "kind": "values", "values": [0.0, 5.0, 1e-40, 5.0, -1e-40, 5.0, 0.0, 5.0, 2.0, 7.0] }
605
+ }
606
+ },
607
+ "outputs": {
608
+ "y": {
609
+ "dtype": "float32",
610
+ "shape": [4, 2],
611
+ "tolerance": 0,
612
+ "data": { "kind": "values", "values": [0.0, 5.0, 1e-40, 5.0, -1e-40, 5.0, 2.0, 7.0] }
613
+ }
614
+ }
615
+ },
616
+ {
617
+ "name": "axis0_f32_subnormal_collapse_sorted",
618
+ "attrs": { "axis": 0, "sorted": 1 },
619
+ "provenance": {
620
+ "notes": "Sorted companion to the subnormal axis case. Raw-bit equality and total-order keys preserve -1e-40 < 0 < 1e-40 on FTZ GPUs; the pinned output is cross-checked with ORT."
621
+ },
622
+ "inputs": {
623
+ "x": {
624
+ "dtype": "float32",
625
+ "shape": [5, 2],
626
+ "data": { "kind": "values", "values": [0.0, 5.0, 1e-40, 5.0, -1e-40, 5.0, 0.0, 5.0, 2.0, 7.0] }
627
+ }
628
+ },
629
+ "outputs": {
630
+ "y": {
631
+ "dtype": "float32",
632
+ "shape": [4, 2],
633
+ "tolerance": 0,
634
+ "data": { "kind": "values", "values": [-1e-40, 5.0, 0.0, 5.0, 1e-40, 5.0, 2.0, 7.0] }
635
+ }
636
+ }
637
+ },
638
+ {
639
+ "name": "hash_int8_signed_negatives_32k",
640
+ "attrs": { "sorted": 1 },
641
+ "provenance": {
642
+ "notes": "Exercises hash deduplication at 32768 elements with 12 signed int8 values, including -128, 127, and other negatives. The TypeScript reference is the oracle because ORT CPU does not implement Unique for int8."
643
+ },
644
+ "inputs": {
645
+ "x": {
646
+ "dtype": "int8",
647
+ "shape": [32768],
648
+ "data": { "kind": "cycle", "values": [5, -1, -128, 3, 127, -1, 0, -128, 42, -64, 100, -100, 7, 17] }
649
+ }
650
+ },
651
+ "outputs": { "y": { "dtype": "int8", "shape": [12], "tolerance": 0 } }
652
+ },
653
+ {
654
+ "name": "parallel_uint32_sentinel_first_seen_4k",
655
+ "attrs": { "sorted": 0 },
656
+ "provenance": {
657
+ "notes": "Exercises grid-parallel deduplication below the hash threshold with 0xffffffff present in first-seen order. The TypeScript reference is the oracle because ORT CPU does not implement Unique for uint32."
658
+ },
659
+ "inputs": {
660
+ "x": {
661
+ "dtype": "uint32",
662
+ "shape": [4096],
663
+ "data": { "kind": "cycle", "values": [4294967295, 0, 7, 1000000, 4294967295, 42, 0, 13, 7, 999] }
664
+ }
665
+ },
666
+ "outputs": { "y": { "dtype": "uint32", "shape": [7], "tolerance": 0 } }
667
+ },
668
+ {
669
+ "name": "flat_f32_all_distinct_4096",
670
+ "provenance": {
671
+ "notes": "4096 fully distinct values exercise the exact large-output serial float path and its ordered representative set."
672
+ },
673
+ "attrs": { "sorted": 1 },
674
+ "inputs": {
675
+ "x": { "dtype": "float32", "shape": [4096], "data": { "kind": "linspace", "start": -4096.0, "end": 4095.0 } }
676
+ },
677
+ "outputs": { "y": { "dtype": "float32", "shape": [4096], "tolerance": 0 } }
678
+ },
679
+ {
680
+ "name": "axis0_f32_all_distinct_2500",
681
+ "provenance": {
682
+ "notes": "2500 fully distinct scalar rows exercise exact axis-mode output and bit-preserving slice comparison at a large axis size."
683
+ },
684
+ "attrs": { "axis": 0, "sorted": 1 },
685
+ "inputs": {
686
+ "x": { "dtype": "float32", "shape": [2500, 1], "data": { "kind": "linspace", "start": -5000.0, "end": 4999.0 } }
687
+ },
688
+ "outputs": { "y": { "dtype": "float32", "shape": [2500, 1], "tolerance": 0 } }
689
+ },
690
+ {
691
+ "name": "serial_f32_32k_signed_zero_single_bucket",
692
+ "provenance": {
693
+ "notes": "32768 f32 elements containing both +0.0 and -0.0 plus finite values verify that exact serial equality canonicalizes signed zero into one bucket, matching ORT."
694
+ },
695
+ "attrs": { "sorted": 1 },
696
+ "inputs": {
697
+ "x": {
698
+ "dtype": "float32",
699
+ "shape": [32768],
700
+ "data": { "kind": "cycle", "values": [0.0, 0.0, 2.5, -3.5, 7.0, 0.0, 0.0] }
701
+ }
702
+ },
703
+ "outputs": { "y": { "dtype": "float32", "shape": [4], "tolerance": 0 } }
704
+ },
705
+ {
706
+ "name": "large_y_int32_sorted_2501_distinct",
707
+ "provenance": {
708
+ "notes": "6000 int32 values deduplicate to an exact 2501-element sorted result, exercising the large-output global-scratch sort and signed integer order across zero."
709
+ },
710
+ "attrs": { "sorted": 1 },
711
+ "inputs": {
712
+ "x": { "dtype": "int32", "shape": [6000], "data": { "kind": "linspace", "start": -1200, "end": 1300 } }
713
+ },
714
+ "outputs": { "y": { "dtype": "int32", "shape": [2501] } }
715
+ },
716
+ {
717
+ "name": "large_y_int32_unsorted_8192_distinct",
718
+ "provenance": {
719
+ "notes": "8192 distinct int32 values exercise the large-output unsorted compaction path without allocating or touching bitonic-sort scratch."
720
+ },
721
+ "attrs": { "sorted": 0 },
722
+ "inputs": {
723
+ "x": { "dtype": "int32", "shape": [8192], "data": { "kind": "linspace", "start": -4096, "end": 4095 } }
724
+ },
725
+ "outputs": { "y": { "dtype": "int32", "shape": [8192] } }
726
+ },
727
+ {
728
+ "name": "f32_sorted_padding_heavy",
729
+ "provenance": {
730
+ "notes": "A padding-heavy sorted float case with about 40 distinct values verifies max-key padding and raw-bit float ordering, including negatives and signed zero."
731
+ },
732
+ "attrs": { "sorted": 1 },
733
+ "inputs": {
734
+ "x": {
735
+ "dtype": "float32",
736
+ "shape": [400],
737
+ "data": {
738
+ "kind": "cycle",
739
+ "values": [3.5, -1.0, 0.0, 2.5, -7.25, 100.0, -100.0, 0.5, -0.5, 42.0, -42.0, 1.0, -1.5, 88.75, -88.75, 6.0, -6.0, 13.5, -13.5, 21.0, -21.0, 7.0, -7.0, 55.5, -55.5, 9.0, -9.0, 64.25, -64.25, 4.0, -4.0, 17.0, -17.0, 30.0, -30.0, 2.0, -2.0, 11.0, -11.0, 99.0]
740
+ }
741
+ }
742
+ },
743
+ "outputs": { "y": { "dtype": "float32", "shape": [40] } }
744
+ },
745
+ {
746
+ "name": "uint32_sorted_unsigned_order",
747
+ "provenance": {
748
+ "notes": "The exact 30-element result spans values above 2^31 through 0xffffffff and verifies unsigned ordering rather than a signed interpretation of raw bits."
749
+ },
750
+ "attrs": { "sorted": 1 },
751
+ "inputs": {
752
+ "x": {
753
+ "dtype": "uint32",
754
+ "shape": [300],
755
+ "data": {
756
+ "kind": "cycle",
757
+ "values": [10, 4000000000, 5, 2147483648, 0, 3000000000, 100, 2147483647, 42, 4294967295, 7, 1, 2500000000, 99, 2147483649, 3, 500, 4000000001, 8, 2, 123456, 4294967294, 55, 2147483650, 9, 777, 3500000000, 6, 4, 1000000]
758
+ }
759
+ }
760
+ },
761
+ "outputs": { "y": { "dtype": "uint32", "shape": [30] } }
762
+ },
763
+ {
764
+ "name": "int32_unsorted_first_occurrence",
765
+ "provenance": {
766
+ "notes": "The exact 30-element unsorted result preserves first-occurrence order while round-tripping signed int32 values through raw-bit scratch."
767
+ },
768
+ "attrs": { "sorted": 0 },
769
+ "inputs": {
770
+ "x": {
771
+ "dtype": "int32",
772
+ "shape": [300],
773
+ "data": {
774
+ "kind": "cycle",
775
+ "values": [37, -5, 12, 99, -73, 0, 41, 8, -21, 64, 3, -90, 55, 17, -2, 76, 29, -48, 83, 6, -33, 92, 14, -67, 50, 22, -9, 70, 35, -58]
776
+ }
777
+ }
778
+ },
779
+ "outputs": { "y": { "dtype": "int32", "shape": [30] } }
780
+ },
781
+ {
782
+ "name": "axis0_large_y_f32_sorted_2200_distinct",
783
+ "provenance": {
784
+ "notes": "The exact serial float axis path handles 2200 distinct scalar slices with its representative order in global storage. This keeps float behavior aligned with ORT's non-transitive NaN comparator without exceeding workgroup-storage limits."
785
+ },
786
+ "attrs": { "axis": 0, "sorted": 1 },
787
+ "inputs": {
788
+ "x": { "dtype": "float32", "shape": [2200, 1], "data": { "kind": "linspace", "start": -500.0, "end": 500.0 } }
789
+ },
790
+ "outputs": { "y": { "dtype": "float32", "shape": [2200, 1] } }
791
+ },
792
+ {
793
+ "name": "axis0_large_y_int32_unsorted_2100_distinct",
794
+ "provenance": {
795
+ "notes": "axis=0 output axis dim 2100 > 2048 with sorted=0 selects axis_bounded_large's no-bitonic branch: parallel compaction gathers the first-occurrence slice indices in appearance order and scatters them from GLOBAL slots scratch, skipping the sort network. inner=1 so flat-unique == axis-unique (reference exact). Validates the unsorted large-axis path at multi-chunk scale against the single-lane unique-axis kernel."
796
+ },
797
+ "attrs": { "axis": 0, "sorted": 0 },
798
+ "inputs": {
799
+ "x": { "dtype": "int32", "shape": [2100, 1], "data": { "kind": "linspace", "start": -4000, "end": 4000 } }
800
+ },
801
+ "outputs": { "y": { "dtype": "int32", "shape": [2100, 1] } }
802
+ },
803
+ {
804
+ "name": "axis_hash_split_scatter_sorted_all_distinct",
805
+ "provenance": {
806
+ "notes": "The hash-backed axis path compacts, sorts, and scatters all 4096 distinct scalar rows into the exact output shape."
807
+ },
808
+ "attrs": { "axis": 0, "sorted": 1 },
809
+ "inputs": {
810
+ "x": { "dtype": "int32", "shape": [4096, 1], "data": { "kind": "linspace", "start": -2048, "end": 2047 } }
811
+ },
812
+ "outputs": { "y": { "dtype": "int32", "shape": [4096, 1], "tolerance": 0 } }
813
+ },
814
+ {
815
+ "name": "axis_hash_split_scatter_unsorted_four_distinct",
816
+ "provenance": {
817
+ "notes": "Unsorted companion for grid-parallel axis scatter: four distinct scalar rows are emitted in first-occurrence order into an exact four-row output."
818
+ },
819
+ "attrs": { "axis": 0, "sorted": 0 },
820
+ "inputs": {
821
+ "x": { "dtype": "int32", "shape": [2048, 1], "data": { "kind": "cycle", "values": [9, -2, 7, 9, 42, -2] } }
822
+ },
823
+ "outputs": { "y": { "dtype": "int32", "shape": [4, 1], "tolerance": 0 } }
824
+ },
825
+ {
826
+ "name": "axis_hash_int32_inner2_duplicate_rows_sorted",
827
+ "provenance": {
828
+ "notes": "Routes the >=2048-axis exact hash dedup with vector-valued slices. Repeated rows must share one hash bucket, first-occurrence representatives must survive, and sorted output remains lexicographic over the complete two-element slice. Pinned values avoid relying on the flattened TypeScript Unique reference for axis semantics."
829
+ },
830
+ "attrs": { "axis": 0, "sorted": 1 },
831
+ "inputs": {
832
+ "x": { "dtype": "int32", "shape": [2048, 2], "data": { "kind": "cycle", "values": [2, 1, 0, 3, 2, 1, -1, 4] } }
833
+ },
834
+ "outputs": {
835
+ "y": {
836
+ "dtype": "int32",
837
+ "shape": [3, 2],
838
+ "tolerance": 0,
839
+ "data": { "kind": "values", "values": [-1, 4, 0, 3, 2, 1] }
840
+ }
841
+ }
842
+ },
843
+ {
844
+ "name": "hash_int32_sorted_16_distinct",
845
+ "provenance": {
846
+ "notes": "A 32768-element input with 16 distinct signed values exercises hash-backed compaction and sorting with an exact result."
847
+ },
848
+ "attrs": { "sorted": 1 },
849
+ "inputs": {
850
+ "x": {
851
+ "dtype": "int32",
852
+ "shape": [32768],
853
+ "data": { "kind": "cycle", "values": [37, -5, 12, 99, -73, 0, 41, 8, -21, 64, 3, -90, 55, 17, -2, 76] }
854
+ }
855
+ },
856
+ "outputs": { "y": { "dtype": "int32", "shape": [16] } }
857
+ },
858
+ {
859
+ "name": "hash_int32_sorted_16k_4096_distinct",
860
+ "provenance": {
861
+ "notes": "A 16K-element input with exactly 4096 distinct integers exercises the narrow int32 large-output hash threshold and signed sorting."
862
+ },
863
+ "attrs": { "sorted": 1 },
864
+ "inputs": { "x": { "dtype": "int32", "shape": [16384], "data": { "kind": "linspace", "start": 0, "end": 4095 } } },
865
+ "outputs": { "y": { "dtype": "int32", "shape": [4096] } }
866
+ },
867
+ {
868
+ "name": "exact_output_33_unsorted_f32_serial",
869
+ "provenance": {
870
+ "notes": "Thirty-three distinct float values exercise exact-output first-occurrence ordering on the serial float path."
871
+ },
872
+ "attrs": { "sorted": 0 },
873
+ "inputs": {
874
+ "x": { "dtype": "float32", "shape": [33], "data": { "kind": "linspace", "start": -16.0, "end": 16.0 } }
875
+ },
876
+ "outputs": { "y": { "dtype": "float32", "shape": [33], "tolerance": 0 } }
877
+ },
878
+ {
879
+ "name": "rank7_axis_last",
880
+ "attrs": { "axis": 6, "sorted": 1 },
881
+ "inputs": {
882
+ "x": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 4], "data": { "kind": "constant", "value": 3.0 } }
883
+ },
884
+ "outputs": { "y": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 1], "tolerance": 0 } }
885
+ },
886
+ {
887
+ "name": "axis0_y1025_storage_order_serial",
888
+ "provenance": {
889
+ "notes": "A 1025-row exact float output exercises axis_serial with storage-backed representative order, so its capacity is independent of maxComputeWorkgroupStorageSize."
890
+ },
891
+ "attrs": { "axis": 0, "sorted": 1 },
892
+ "inputs": {
893
+ "x": { "dtype": "float32", "shape": [1025, 1], "data": { "kind": "linspace", "start": -1024.0, "end": 1024.0 } }
894
+ },
895
+ "outputs": { "y": { "dtype": "float32", "shape": [1025, 1], "tolerance": 0 } }
896
+ },
897
+ {
898
+ "name": "axis0_rows65537_over_hash_ceiling_large_y",
899
+ "provenance": {
900
+ "notes": "65537 rows exceed the axis hash-table ceiling, while exactly 2049 distinct rows keep the output above the local crossover; this selects the global axis fallback with an exact result."
901
+ },
902
+ "attrs": { "axis": 0, "sorted": 1 },
903
+ "inputs": {
904
+ "x": { "dtype": "int32", "shape": [65537, 1], "data": { "kind": "linspace", "start": 0, "end": 2048 } }
905
+ },
906
+ "outputs": { "y": { "dtype": "int32", "shape": [2049, 1], "tolerance": 0 } }
907
+ },
908
+ {
909
+ "name": "rank8_axis_last",
910
+ "attrs": { "axis": 7, "sorted": 1 },
911
+ "inputs": {
912
+ "x": {
913
+ "dtype": "float32",
914
+ "shape": [1, 2, 1, 2, 1, 2, 2, 4],
915
+ "data": { "kind": "cycle", "values": [1.0, 2.0, 3.0, 1.0] }
916
+ }
917
+ },
918
+ "outputs": { "y": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 2, 3], "tolerance": 0 } }
919
+ },
920
+ {
921
+ "name": "axis0_uint32_sorted_order",
922
+ "provenance": {
923
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
924
+ "notes": "Exercises unsigned slice ordering through the bounded parallel axis compaction path, including values above int32 range."
925
+ },
926
+ "attrs": { "axis": 0, "sorted": 1 },
927
+ "inputs": {
928
+ "x": {
929
+ "dtype": "uint32",
930
+ "shape": [6, 1],
931
+ "data": { "kind": "values", "values": [4000000000, 5, 2147483648, 5, 0, 4000000000] }
932
+ }
933
+ },
934
+ "outputs": {
935
+ "y": {
936
+ "dtype": "uint32",
937
+ "shape": [4, 1],
938
+ "tolerance": 0,
939
+ "data": { "kind": "values", "values": [0, 5, 2147483648, 4000000000] }
940
+ }
941
+ }
942
+ },
943
+ {
944
+ "name": "axis_empty_y_only",
945
+ "provenance": {
946
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
947
+ "notes": "An empty selected axis has zero unique slices and therefore a legal zero-sized Y axis."
948
+ },
949
+ "attrs": { "axis": 0, "sorted": 1 },
950
+ "inputs": { "x": { "dtype": "int32", "shape": [0, 2], "data": { "kind": "values", "values": [] } } },
951
+ "outputs": { "y": { "dtype": "int32", "shape": [0, 2], "tolerance": 0 } }
952
+ },
953
+ {
954
+ "name": "axis_empty_all_metadata",
955
+ "provenance": {
956
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
957
+ "notes": "Zero-length axis mode requests every standard optional metadata output with its exact empty shape."
958
+ },
959
+ "attrs": { "axis": 1, "sorted": 0 },
960
+ "inputs": { "x": { "dtype": "int32", "shape": [2, 0, 3], "data": { "kind": "values", "values": [] } } },
961
+ "outputs": {
962
+ "y": { "dtype": "int32", "shape": [2, 0, 3], "tolerance": 0 },
963
+ "indices": { "dtype": "uint32", "shape": [0], "tolerance": 0, "data": { "kind": "values", "values": [] } },
964
+ "inverse_indices": {
965
+ "dtype": "uint32",
966
+ "shape": [0],
967
+ "tolerance": 0,
968
+ "data": { "kind": "values", "values": [] }
969
+ },
970
+ "counts": { "dtype": "uint32", "shape": [0], "tolerance": 0, "data": { "kind": "values", "values": [] } }
971
+ }
972
+ },
973
+ {
974
+ "name": "axis_nan_ordered_map_unsorted_all_metadata",
975
+ "provenance": {
976
+ "source": "onnxruntime/core/providers/cpu/tensor/unique.cc",
977
+ "notes": "ORT's ordered slice comparator stops at the first unequal coordinate. A NaN there makes the key equivalent to the lower-bound bucket and ignores the remaining suffix."
978
+ },
979
+ "attrs": { "axis": 0, "sorted": 0 },
980
+ "inputs": {
981
+ "x": {
982
+ "dtype": "float32",
983
+ "shape": [4, 2],
984
+ "data": { "kind": "values", "values": [5.0, 0.0, 1.0, 0.0, "NaN", 9.0, 3.0, 0.0] }
985
+ }
986
+ },
987
+ "outputs": {
988
+ "y": {
989
+ "dtype": "float32",
990
+ "shape": [3, 2],
991
+ "tolerance": 0,
992
+ "data": { "kind": "values", "values": [5.0, 0.0, 1.0, 0.0, 3.0, 0.0] }
993
+ },
994
+ "indices": {
995
+ "dtype": "uint32",
996
+ "shape": [3],
997
+ "tolerance": 0,
998
+ "data": { "kind": "values", "values": [0, 1, 3] }
999
+ },
1000
+ "inverse_indices": {
1001
+ "dtype": "uint32",
1002
+ "shape": [4],
1003
+ "tolerance": 0,
1004
+ "data": { "kind": "values", "values": [0, 1, 1, 2] }
1005
+ },
1006
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [1, 2, 1] } }
1007
+ }
1008
+ },
1009
+ {
1010
+ "name": "axis_nan_ordered_map_sorted_all_metadata",
1011
+ "provenance": {
1012
+ "source": "onnxruntime/core/providers/cpu/tensor/unique.cc",
1013
+ "notes": "The same NaN lower-bound equivalence is retained while sorted output follows the ordered map's comparator order."
1014
+ },
1015
+ "attrs": { "axis": 0, "sorted": 1 },
1016
+ "inputs": {
1017
+ "x": {
1018
+ "dtype": "float32",
1019
+ "shape": [4, 2],
1020
+ "data": { "kind": "values", "values": [5.0, 0.0, 1.0, 0.0, "NaN", 9.0, 3.0, 0.0] }
1021
+ }
1022
+ },
1023
+ "outputs": {
1024
+ "y": {
1025
+ "dtype": "float32",
1026
+ "shape": [3, 2],
1027
+ "tolerance": 0,
1028
+ "data": { "kind": "values", "values": [1.0, 0.0, 3.0, 0.0, 5.0, 0.0] }
1029
+ },
1030
+ "indices": {
1031
+ "dtype": "uint32",
1032
+ "shape": [3],
1033
+ "tolerance": 0,
1034
+ "data": { "kind": "values", "values": [1, 3, 0] }
1035
+ },
1036
+ "inverse_indices": {
1037
+ "dtype": "uint32",
1038
+ "shape": [4],
1039
+ "tolerance": 0,
1040
+ "data": { "kind": "values", "values": [2, 0, 0, 1] }
1041
+ },
1042
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [2, 1, 1] } }
1043
+ }
1044
+ },
1045
+ {
1046
+ "name": "flat_nan_ordered_map_unsorted_all_metadata",
1047
+ "provenance": {
1048
+ "source": "onnxruntime/core/providers/cpu/tensor/unique.cc",
1049
+ "notes": "Flat Unique uses the same stateful lower_bound rule: NaN maps to the smallest bucket present at its insertion point, not unconditionally to output bucket zero."
1050
+ },
1051
+ "attrs": { "sorted": 0 },
1052
+ "inputs": {
1053
+ "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [5.0, 1.0, "NaN", 3.0] } }
1054
+ },
1055
+ "outputs": {
1056
+ "y": {
1057
+ "dtype": "float32",
1058
+ "shape": [3],
1059
+ "tolerance": 0,
1060
+ "data": { "kind": "values", "values": [5.0, 1.0, 3.0] }
1061
+ },
1062
+ "indices": {
1063
+ "dtype": "uint32",
1064
+ "shape": [3],
1065
+ "tolerance": 0,
1066
+ "data": { "kind": "values", "values": [0, 1, 3] }
1067
+ },
1068
+ "inverse_indices": {
1069
+ "dtype": "uint32",
1070
+ "shape": [4],
1071
+ "tolerance": 0,
1072
+ "data": { "kind": "values", "values": [0, 1, 1, 2] }
1073
+ },
1074
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [1, 2, 1] } }
1075
+ }
1076
+ },
1077
+ {
1078
+ "name": "flat_nan_ordered_map_sorted_all_metadata",
1079
+ "provenance": {
1080
+ "source": "onnxruntime/core/providers/cpu/tensor/unique.cc",
1081
+ "notes": "Sorted flat output keeps the ordered-map bucket accounting while emitting comparator order."
1082
+ },
1083
+ "attrs": { "sorted": 1 },
1084
+ "inputs": {
1085
+ "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [5.0, 1.0, "NaN", 3.0] } }
1086
+ },
1087
+ "outputs": {
1088
+ "y": {
1089
+ "dtype": "float32",
1090
+ "shape": [3],
1091
+ "tolerance": 0,
1092
+ "data": { "kind": "values", "values": [1.0, 3.0, 5.0] }
1093
+ },
1094
+ "indices": {
1095
+ "dtype": "uint32",
1096
+ "shape": [3],
1097
+ "tolerance": 0,
1098
+ "data": { "kind": "values", "values": [1, 3, 0] }
1099
+ },
1100
+ "inverse_indices": {
1101
+ "dtype": "uint32",
1102
+ "shape": [4],
1103
+ "tolerance": 0,
1104
+ "data": { "kind": "values", "values": [2, 0, 0, 1] }
1105
+ },
1106
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [2, 1, 1] } }
1107
+ }
1108
+ },
1109
+ {
1110
+ "name": "flat_nan_ordered_map_insertion_state_all_metadata",
1111
+ "provenance": {
1112
+ "source": "onnxruntime/core/providers/cpu/tensor/unique.cc",
1113
+ "notes": "With NaN inserted before the later minimum, lower_bound associates it with 5 rather than retroactively moving it to the later 1 bucket."
1114
+ },
1115
+ "attrs": { "sorted": 0 },
1116
+ "inputs": {
1117
+ "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [5.0, "NaN", 1.0, 3.0] } }
1118
+ },
1119
+ "outputs": {
1120
+ "y": {
1121
+ "dtype": "float32",
1122
+ "shape": [3],
1123
+ "tolerance": 0,
1124
+ "data": { "kind": "values", "values": [5.0, 1.0, 3.0] }
1125
+ },
1126
+ "indices": {
1127
+ "dtype": "uint32",
1128
+ "shape": [3],
1129
+ "tolerance": 0,
1130
+ "data": { "kind": "values", "values": [0, 2, 3] }
1131
+ },
1132
+ "inverse_indices": {
1133
+ "dtype": "uint32",
1134
+ "shape": [4],
1135
+ "tolerance": 0,
1136
+ "data": { "kind": "values", "values": [0, 0, 1, 2] }
1137
+ },
1138
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [2, 1, 1] } }
1139
+ }
1140
+ },
1141
+ {
1142
+ "name": "metadata_flat_all_int32",
1143
+ "provenance": {
1144
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1145
+ "notes": "Exercises every optional output on the non-float flattened comparator path."
1146
+ },
1147
+ "attrs": { "sorted": 1 },
1148
+ "inputs": { "x": { "dtype": "int32", "shape": [5], "data": { "kind": "values", "values": [3, 1, 3, 2, 1] } } },
1149
+ "outputs": {
1150
+ "y": { "dtype": "int32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [1, 2, 3] } },
1151
+ "indices": {
1152
+ "dtype": "uint32",
1153
+ "shape": [3],
1154
+ "tolerance": 0,
1155
+ "data": { "kind": "values", "values": [1, 3, 0] }
1156
+ },
1157
+ "inverse_indices": {
1158
+ "dtype": "uint32",
1159
+ "shape": [5],
1160
+ "tolerance": 0,
1161
+ "data": { "kind": "values", "values": [2, 0, 2, 1, 0] }
1162
+ },
1163
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0, "data": { "kind": "values", "values": [2, 1, 2] } }
1164
+ }
1165
+ },
1166
+ {
1167
+ "name": "metadata_flat_indices_nd_input",
1168
+ "provenance": {
1169
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1170
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1171
+ },
1172
+ "attrs": { "sorted": 0 },
1173
+ "inputs": {
1174
+ "x": {
1175
+ "dtype": "float32",
1176
+ "shape": [2, 3],
1177
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1178
+ }
1179
+ },
1180
+ "outputs": {
1181
+ "y": { "dtype": "float32", "shape": [4] },
1182
+ "indices": { "dtype": "uint32", "shape": [4], "tolerance": 0 }
1183
+ }
1184
+ },
1185
+ {
1186
+ "name": "metadata_axis_negative_indices",
1187
+ "provenance": {
1188
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1189
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1190
+ },
1191
+ "attrs": { "axis": -2, "sorted": 1 },
1192
+ "inputs": {
1193
+ "x": {
1194
+ "dtype": "float32",
1195
+ "shape": [3, 3],
1196
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1197
+ }
1198
+ },
1199
+ "outputs": {
1200
+ "y": { "dtype": "float32", "shape": [2, 3] },
1201
+ "indices": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1202
+ }
1203
+ },
1204
+ {
1205
+ "name": "metadata_flat_inverse_nd_input",
1206
+ "provenance": {
1207
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1208
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1209
+ },
1210
+ "attrs": { "sorted": 0 },
1211
+ "inputs": {
1212
+ "x": {
1213
+ "dtype": "float32",
1214
+ "shape": [2, 3],
1215
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1216
+ }
1217
+ },
1218
+ "outputs": {
1219
+ "y": { "dtype": "float32", "shape": [4] },
1220
+ "inverse_indices": { "dtype": "uint32", "shape": [6], "tolerance": 0 }
1221
+ }
1222
+ },
1223
+ {
1224
+ "name": "metadata_axis_negative_inverse",
1225
+ "provenance": {
1226
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1227
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1228
+ },
1229
+ "attrs": { "axis": -2, "sorted": 1 },
1230
+ "inputs": {
1231
+ "x": {
1232
+ "dtype": "float32",
1233
+ "shape": [3, 3],
1234
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1235
+ }
1236
+ },
1237
+ "outputs": {
1238
+ "y": { "dtype": "float32", "shape": [2, 3] },
1239
+ "inverse_indices": { "dtype": "uint32", "shape": [3], "tolerance": 0 }
1240
+ }
1241
+ },
1242
+ {
1243
+ "name": "metadata_flat_counts_nd_input",
1244
+ "provenance": {
1245
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1246
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1247
+ },
1248
+ "attrs": { "sorted": 0 },
1249
+ "inputs": {
1250
+ "x": {
1251
+ "dtype": "float32",
1252
+ "shape": [2, 3],
1253
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1254
+ }
1255
+ },
1256
+ "outputs": {
1257
+ "y": { "dtype": "float32", "shape": [4] },
1258
+ "counts": { "dtype": "uint32", "shape": [4], "tolerance": 0 }
1259
+ }
1260
+ },
1261
+ {
1262
+ "name": "metadata_axis_negative_counts",
1263
+ "provenance": {
1264
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1265
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1266
+ },
1267
+ "attrs": { "axis": -2, "sorted": 1 },
1268
+ "inputs": {
1269
+ "x": {
1270
+ "dtype": "float32",
1271
+ "shape": [3, 3],
1272
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1273
+ }
1274
+ },
1275
+ "outputs": {
1276
+ "y": { "dtype": "float32", "shape": [2, 3] },
1277
+ "counts": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1278
+ }
1279
+ },
1280
+ {
1281
+ "name": "metadata_flat_indices_inverse_nd_input",
1282
+ "provenance": {
1283
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1284
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1285
+ },
1286
+ "attrs": { "sorted": 0 },
1287
+ "inputs": {
1288
+ "x": {
1289
+ "dtype": "float32",
1290
+ "shape": [2, 3],
1291
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1292
+ }
1293
+ },
1294
+ "outputs": {
1295
+ "y": { "dtype": "float32", "shape": [4] },
1296
+ "indices": { "dtype": "uint32", "shape": [4], "tolerance": 0 },
1297
+ "inverse_indices": { "dtype": "uint32", "shape": [6], "tolerance": 0 }
1298
+ }
1299
+ },
1300
+ {
1301
+ "name": "metadata_axis_negative_indices_inverse",
1302
+ "provenance": {
1303
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1304
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1305
+ },
1306
+ "attrs": { "axis": -2, "sorted": 1 },
1307
+ "inputs": {
1308
+ "x": {
1309
+ "dtype": "float32",
1310
+ "shape": [3, 3],
1311
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1312
+ }
1313
+ },
1314
+ "outputs": {
1315
+ "y": { "dtype": "float32", "shape": [2, 3] },
1316
+ "indices": { "dtype": "uint32", "shape": [2], "tolerance": 0 },
1317
+ "inverse_indices": { "dtype": "uint32", "shape": [3], "tolerance": 0 }
1318
+ }
1319
+ },
1320
+ {
1321
+ "name": "metadata_flat_indices_counts_nd_input",
1322
+ "provenance": {
1323
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1324
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1325
+ },
1326
+ "attrs": { "sorted": 0 },
1327
+ "inputs": {
1328
+ "x": {
1329
+ "dtype": "float32",
1330
+ "shape": [2, 3],
1331
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1332
+ }
1333
+ },
1334
+ "outputs": {
1335
+ "y": { "dtype": "float32", "shape": [4] },
1336
+ "indices": { "dtype": "uint32", "shape": [4], "tolerance": 0 },
1337
+ "counts": { "dtype": "uint32", "shape": [4], "tolerance": 0 }
1338
+ }
1339
+ },
1340
+ {
1341
+ "name": "metadata_axis_negative_indices_counts",
1342
+ "provenance": {
1343
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1344
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1345
+ },
1346
+ "attrs": { "axis": -2, "sorted": 1 },
1347
+ "inputs": {
1348
+ "x": {
1349
+ "dtype": "float32",
1350
+ "shape": [3, 3],
1351
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1352
+ }
1353
+ },
1354
+ "outputs": {
1355
+ "y": { "dtype": "float32", "shape": [2, 3] },
1356
+ "indices": { "dtype": "uint32", "shape": [2], "tolerance": 0 },
1357
+ "counts": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1358
+ }
1359
+ },
1360
+ {
1361
+ "name": "metadata_flat_inverse_counts_nd_input",
1362
+ "provenance": {
1363
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1364
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1365
+ },
1366
+ "attrs": { "sorted": 0 },
1367
+ "inputs": {
1368
+ "x": {
1369
+ "dtype": "float32",
1370
+ "shape": [2, 3],
1371
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1372
+ }
1373
+ },
1374
+ "outputs": {
1375
+ "y": { "dtype": "float32", "shape": [4] },
1376
+ "inverse_indices": { "dtype": "uint32", "shape": [6], "tolerance": 0 },
1377
+ "counts": { "dtype": "uint32", "shape": [4], "tolerance": 0 }
1378
+ }
1379
+ },
1380
+ {
1381
+ "name": "metadata_axis_negative_inverse_counts",
1382
+ "provenance": {
1383
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1384
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1385
+ },
1386
+ "attrs": { "axis": -2, "sorted": 1 },
1387
+ "inputs": {
1388
+ "x": {
1389
+ "dtype": "float32",
1390
+ "shape": [3, 3],
1391
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1392
+ }
1393
+ },
1394
+ "outputs": {
1395
+ "y": { "dtype": "float32", "shape": [2, 3] },
1396
+ "inverse_indices": { "dtype": "uint32", "shape": [3], "tolerance": 0 },
1397
+ "counts": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1398
+ }
1399
+ },
1400
+ {
1401
+ "name": "metadata_flat_all_nd_input",
1402
+ "provenance": {
1403
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1404
+ "notes": "Exercises the requested standard optional output combination while flattening an N-D input."
1405
+ },
1406
+ "attrs": { "sorted": 0 },
1407
+ "inputs": {
1408
+ "x": {
1409
+ "dtype": "float32",
1410
+ "shape": [2, 3],
1411
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 4.0, 3.0] }
1412
+ }
1413
+ },
1414
+ "outputs": {
1415
+ "y": { "dtype": "float32", "shape": [4] },
1416
+ "indices": { "dtype": "uint32", "shape": [4], "tolerance": 0 },
1417
+ "inverse_indices": { "dtype": "uint32", "shape": [6], "tolerance": 0 },
1418
+ "counts": { "dtype": "uint32", "shape": [4], "tolerance": 0 }
1419
+ }
1420
+ },
1421
+ {
1422
+ "name": "metadata_axis_negative_all",
1423
+ "provenance": {
1424
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1425
+ "notes": "Exercises the requested standard optional output combination and normalizes axis=-2 to axis 0 for a rank-2 input."
1426
+ },
1427
+ "attrs": { "axis": -2, "sorted": 1 },
1428
+ "inputs": {
1429
+ "x": {
1430
+ "dtype": "float32",
1431
+ "shape": [3, 3],
1432
+ "data": { "kind": "values", "values": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 3.0, 4.0] }
1433
+ }
1434
+ },
1435
+ "outputs": {
1436
+ "y": { "dtype": "float32", "shape": [2, 3] },
1437
+ "indices": { "dtype": "uint32", "shape": [2], "tolerance": 0 },
1438
+ "inverse_indices": { "dtype": "uint32", "shape": [3], "tolerance": 0 },
1439
+ "counts": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1440
+ }
1441
+ },
1442
+ {
1443
+ "name": "float16_sorted_subnormal_and_signed_zero",
1444
+ "provenance": {
1445
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1446
+ "notes": "Covers the standard float16 type with bit-preserving subnormal ordering and signed-zero equality."
1447
+ },
1448
+ "attrs": { "sorted": 1 },
1449
+ "inputs": {
1450
+ "x": {
1451
+ "dtype": "float16",
1452
+ "shape": [5],
1453
+ "data": { "kind": "values", "values": [0.0, -0.0, 5.960464477539063e-8, -5.960464477539063e-8, 0.0] }
1454
+ }
1455
+ },
1456
+ "outputs": {
1457
+ "y": {
1458
+ "dtype": "float16",
1459
+ "shape": [3],
1460
+ "tolerance": 0,
1461
+ "data": { "kind": "values", "values": [-5.960464477539063e-8, 0.0, 5.960464477539063e-8] }
1462
+ }
1463
+ }
1464
+ },
1465
+ {
1466
+ "name": "float16_nan_ordered_map_all_metadata",
1467
+ "provenance": {
1468
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1469
+ "notes": "Exercises float16 NaN lower-bound equivalence and every standard optional metadata output."
1470
+ },
1471
+ "attrs": { "sorted": 0 },
1472
+ "inputs": {
1473
+ "x": { "dtype": "float16", "shape": [4], "data": { "kind": "values", "values": [5.0, "NaN", 1.0, 3.0] } }
1474
+ },
1475
+ "outputs": {
1476
+ "y": { "dtype": "float16", "shape": [3], "tolerance": 0 },
1477
+ "indices": { "dtype": "uint32", "shape": [3], "tolerance": 0 },
1478
+ "inverse_indices": { "dtype": "uint32", "shape": [4], "tolerance": 0 },
1479
+ "counts": { "dtype": "uint32", "shape": [3], "tolerance": 0 }
1480
+ }
1481
+ },
1482
+ {
1483
+ "name": "float16_axis_unsorted_duplicate_rows",
1484
+ "provenance": {
1485
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1486
+ "notes": "Covers standard float16 axis-mode deduplication and first-occurrence output order."
1487
+ },
1488
+ "attrs": { "axis": 0, "sorted": 0 },
1489
+ "inputs": {
1490
+ "x": {
1491
+ "dtype": "float16",
1492
+ "shape": [3, 2],
1493
+ "data": { "kind": "values", "values": [2.0, 1.0, 1.0, 3.0, 2.0, 1.0] }
1494
+ }
1495
+ },
1496
+ "outputs": { "y": { "dtype": "float16", "shape": [2, 2], "tolerance": 0 } }
1497
+ },
1498
+ {
1499
+ "name": "int16_sorted_extremes",
1500
+ "provenance": {
1501
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1502
+ "notes": "Covers the standard int16 type at both representable extremes."
1503
+ },
1504
+ "attrs": { "sorted": 1 },
1505
+ "inputs": {
1506
+ "x": {
1507
+ "dtype": "int16",
1508
+ "shape": [6],
1509
+ "data": { "kind": "values", "values": [32767, -32768, -1, 0, 32767, -32768] }
1510
+ }
1511
+ },
1512
+ "outputs": { "y": { "dtype": "int16", "shape": [4], "tolerance": 0 } }
1513
+ },
1514
+ {
1515
+ "name": "bool_unsorted_all_metadata",
1516
+ "provenance": {
1517
+ "source": "https://onnx.ai/onnx/operators/onnx__Unique.html",
1518
+ "notes": "Covers the standard bool type, first-occurrence order, and every optional metadata output."
1519
+ },
1520
+ "attrs": { "sorted": 0 },
1521
+ "inputs": { "x": { "dtype": "bool", "shape": [5], "data": { "kind": "values", "values": [1, 0, 1, 1, 0] } } },
1522
+ "outputs": {
1523
+ "y": { "dtype": "bool", "shape": [2], "tolerance": 0 },
1524
+ "indices": { "dtype": "uint32", "shape": [2], "tolerance": 0 },
1525
+ "inverse_indices": { "dtype": "uint32", "shape": [5], "tolerance": 0 },
1526
+ "counts": { "dtype": "uint32", "shape": [2], "tolerance": 0 }
1527
+ }
1528
+ }
1529
+ ]
1530
+ }
build/webgpu/unique-axis-compact-sort.wgsl.jinja ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% set emitCount = source.emitCount | default(false) %}
3
+
4
+ // Compact every first-occurrence slice index, optionally bitonic-sort them,
5
+ // then scatter the exact result. Small results keep slots in workgroup memory;
6
+ // larger results use storage-buffer scratch with storage barriers.
7
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
8
+ const CAP: u32 = {{ source.capacity }}u;
9
+ {% if sorted or not source.globalScratch %}
10
+ const SORT_N: u32 = {{ source.sortN }}u;
11
+
12
+ {% endif %}
13
+ {% if sorted %}
14
+ fn less_value(a: {{ scalar }}, b: {{ scalar }}) -> bool {
15
+ return a < b;
16
+ }
17
+
18
+ {% endif %}
19
+ {% if sorted or not source.compactOnly %}
20
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
21
+ return x[(o * params.axisDim + k) * params.inner + n];
22
+ }
23
+
24
+ {% endif %}
25
+ {% if sorted %}
26
+ // True iff slice a < slice b lexicographically over (outer, inner) element order.
27
+ // The serial insertion sort uses the same comparator.
28
+ fn slice_less(a: u32, b: u32) -> bool {
29
+ for (var o = 0u; o < params.outer; o = o + 1u) {
30
+ for (var n = 0u; n < params.inner; n = n + 1u) {
31
+ let va = slice_at(o, a, n);
32
+ let vb = slice_at(o, b, n);
33
+ if (less_value(va, vb)) { return true; }
34
+ if (less_value(vb, va)) { return false; }
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ // Cache the first element's monotonic integer-order key alongside each slot so
41
+ // the bitonic network normally compares only threadgroup u32s; equal keys still
42
+ // use the exact full-slice comparator.
43
+ fn slice_primary_key(k: u32) -> u32 {
44
+ if (params.outer == 0u || params.inner == 0u) { return 0u; }
45
+ {% if isUnsigned %}
46
+ return u32(slice_at(0u, k, 0u));
47
+ {% else %}
48
+ return bitcast<u32>(slice_at(0u, k, 0u)) ^ 0x80000000u;
49
+ {% endif %}
50
+ }
51
+
52
+ {% endif %}
53
+ {% if not source.compactOnly %}
54
+ // Keep exact-result axis scatter identical across static- and dynamic-shape
55
+ // dispatch strategies. The zero branch is a defensive guard for invalid shapes.
56
+ fn unique_axis_zero_value() -> {{ scalar }} {
57
+ return {{ scalar }}(0);
58
+ }
59
+
60
+ fn unique_axis_scatter_element(g: u32, written: u32) {
61
+ let n = g % params.inner;
62
+ let tmp = g / params.inner;
63
+ let p = tmp % params.outputAxisDim;
64
+ let o = tmp / params.outputAxisDim;
65
+ if (p < written) {
66
+ y[g] = slice_at(o, slots[p], n);
67
+ } else {
68
+ y[g] = unique_axis_zero_value();
69
+ }
70
+ }
71
+
72
+
73
+ {% endif %}
74
+ var<workgroup> wgScan: array<u32, WG>;
75
+ {% if not source.globalScratch %}
76
+ var<workgroup> slots: array<u32, SORT_N>; // compacted (then sorted) slice indices
77
+ {% endif %}
78
+ var<workgroup> wgCarry: u32;
79
+ {% if sorted and not source.globalScratch %}
80
+ var<workgroup> sortPad: array<u32, SORT_N>; // 1 = padding slot, sorts after every real
81
+ var<workgroup> sortKey: array<u32, SORT_N>; // first-element total-order key
82
+
83
+ {% endif %}
84
+ @compute @workgroup_size(WG)
85
+ fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
86
+ let tid = lid.x;
87
+ if (tid == 0u) { wgCarry = 0u; }
88
+ workgroupBarrier();
89
+
90
+ // Parallel compaction: chunked Hillis-Steele exclusive scan of firstFlag, gathering
91
+ // the axis-index k of each first-occurrence slice into slots[appearance-rank].
92
+ let chunks = (params.axisDim + WG - 1u) / WG;
93
+ for (var c = 0u; c < chunks; c = c + 1u) {
94
+ let k = c * WG + tid;
95
+ var f = 0u;
96
+ if (k < params.axisDim) {
97
+ f = firstFlag[k];
98
+ }
99
+ wgScan[tid] = f;
100
+ workgroupBarrier();
101
+ var stride = 1u;
102
+ loop {
103
+ if (stride >= WG) { break; }
104
+ var add = 0u;
105
+ if (tid >= stride) {
106
+ add = wgScan[tid - stride];
107
+ }
108
+ workgroupBarrier();
109
+ wgScan[tid] = wgScan[tid] + add;
110
+ workgroupBarrier();
111
+ stride = stride * 2u;
112
+ }
113
+ let excl = wgScan[tid] - f;
114
+ let pos = wgCarry + excl;
115
+ if (k < params.axisDim && f == 1u && pos < CAP) {
116
+ slots[pos] = k;
117
+ }
118
+ workgroupBarrier();
119
+ if (tid == 0u) {
120
+ wgCarry = wgCarry + wgScan[WG - 1u];
121
+ }
122
+ workgroupBarrier();
123
+ }
124
+
125
+ let total = wgCarry;
126
+ {% if sorted or not source.compactOnly %}
127
+ let written = min(total, CAP);
128
+ {% endif %}
129
+ {% if emitCount %}
130
+ if (tid == 0u) {
131
+ count[0] = total;
132
+ }
133
+ {% endif %}
134
+ {% if source.globalScratch %}
135
+ storageBarrier();
136
+
137
+ {% endif %}
138
+ {% if sorted %}
139
+ // Pad [written, SORT_N) so padding sorts strictly after every real slice, then
140
+ // bitonic-sort slots by slice_less (real slices are distinct, so it is a total order).
141
+ for (var k = tid; k < SORT_N; k = k + WG) {
142
+ let isPad = select(0u, 1u, k >= written);
143
+ sortPad[k] = isPad;
144
+ if (isPad == 1u) {
145
+ slots[k] = 0u;
146
+ sortKey[k] = 0u;
147
+ } else {
148
+ sortKey[k] = slice_primary_key(slots[k]);
149
+ }
150
+ }
151
+ {% if source.globalScratch %}
152
+ storageBarrier();
153
+ {% else %}
154
+ workgroupBarrier();
155
+ {% endif %}
156
+
157
+ var size = 2u;
158
+ loop {
159
+ if (size > SORT_N) { break; }
160
+ var stride = size / 2u;
161
+ loop {
162
+ if (stride == 0u) { break; }
163
+ for (var k = tid; k < SORT_N; k = k + WG) {
164
+ let partner = k ^ stride;
165
+ if (partner > k) {
166
+ let ascending = (k & size) == 0u;
167
+ let pk = sortPad[k];
168
+ let pp = sortPad[partner];
169
+ // k should precede partner under (padding-flag asc, slice order asc)
170
+ var kBeforeP: bool;
171
+ if (pk != pp) {
172
+ kBeforeP = pk < pp;
173
+ } else if (pk == 1u) {
174
+ kBeforeP = true;
175
+ } else {
176
+ let kk = sortKey[k];
177
+ let kp = sortKey[partner];
178
+ if (kk != kp) {
179
+ kBeforeP = kk < kp;
180
+ } else {
181
+ kBeforeP = !slice_less(slots[partner], slots[k]);
182
+ }
183
+ }
184
+ let needSwap = select(kBeforeP, !kBeforeP, ascending);
185
+ if (needSwap) {
186
+ let tSlot = slots[k];
187
+ slots[k] = slots[partner];
188
+ slots[partner] = tSlot;
189
+ let tPad = sortPad[k];
190
+ sortPad[k] = sortPad[partner];
191
+ sortPad[partner] = tPad;
192
+ let tKey = sortKey[k];
193
+ sortKey[k] = sortKey[partner];
194
+ sortKey[partner] = tKey;
195
+ }
196
+ }
197
+ }
198
+ {% if source.globalScratch %}
199
+ storageBarrier();
200
+ {% else %}
201
+ workgroupBarrier();
202
+ {% endif %}
203
+ stride = stride / 2u;
204
+ }
205
+ size = size * 2u;
206
+ }
207
+ {% endif %}
208
+ {% if not source.compactOnly %}
209
+ {% if source.globalScratch %}
210
+ storageBarrier();
211
+ {% else %}
212
+ workgroupBarrier();
213
+ {% endif %}
214
+
215
+ // Scatter the distinct slices into the exact Y allocation: one lane per
216
+ // output element, flat index g = (o*outputAxisDim + p)*inner + n.
217
+ let totalOut = params.outer * params.outputAxisDim * params.inner;
218
+ for (var g = tid; g < totalOut; g = g + WG) {
219
+ unique_axis_scatter_element(g, written);
220
+ }
221
+ {% endif %}
222
+ }
build/webgpu/unique-axis-dedup.wgsl.jinja ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Pass 1 of parallel axis-Unique. One thread per slice k writes
4
+ // firstFlag[k] = 1 iff no earlier slice j < k is element-wise equal to k.
5
+ //
6
+ // A "slice" along the axis is the vector x[(o*axisDim + k)*inner + n] over all
7
+ // (o in outer, n in inner). This parallel path compares only integer storage
8
+ // values.
9
+ //
10
+ // The quadratic scan's common case only needs to prove that two slices differ.
11
+ // Loading the first element of every candidate directly from X made that scan a
12
+ // large, cache-hostile gather (the axis stride can be kilobytes). Instead, every
13
+ // workgroup stages a chunk of small sampled slice fingerprints in threadgroup
14
+ // memory. A fingerprint mismatch proves inequality; collisions fall through to
15
+ // the exact element-wise comparator, so this is an optimization only and cannot
16
+ // change Unique semantics.
17
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
18
+ return x[(o * params.axisDim + k) * params.inner + n];
19
+ }
20
+
21
+ fn value_key(value: {{ scalar }}) -> u32 {
22
+ // Integer conversion is injective for the 8/32-bit integer storage types.
23
+ return u32(value);
24
+ }
25
+
26
+ fn mix_key(hash: u32, value: u32) -> u32 {
27
+ var h = (hash ^ value) * 0x9e3779b1u;
28
+ h = h ^ (h >> 16u);
29
+ return h;
30
+ }
31
+
32
+ fn slice_key(k: u32) -> u32 {
33
+ let elements = params.outer * params.inner;
34
+ if (elements == 0u) { return 0u; }
35
+
36
+ // Four evenly-spaced samples make the prefilter useful for structured rows
37
+ // whose leading value is shared, while keeping the staging pass tiny.
38
+ let last = elements - 1u;
39
+ var hash = 0x811c9dc5u;
40
+ for (var sample = 0u; sample < 4u; sample = sample + 1u) {
41
+ let logical = (sample * last) / 3u;
42
+ let o = logical / params.inner;
43
+ let n = logical % params.inner;
44
+ hash = mix_key(hash, value_key(slice_at(o, k, n)));
45
+ }
46
+ return hash;
47
+ }
48
+
49
+ fn slice_eq(a: u32, b: u32) -> bool {
50
+ for (var o = 0u; o < params.outer; o = o + 1u) {
51
+ for (var n = 0u; n < params.inner; n = n + 1u) {
52
+ if (slice_at(o, a, n) != slice_at(o, b, n)) { return false; }
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+
58
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
59
+ var<workgroup> keyCache: array<u32, WG>;
60
+
61
+ @compute @workgroup_size(WG)
62
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
63
+ @builtin(workgroup_id) wid: vec3<u32>,
64
+ @builtin(local_invocation_id) lid: vec3<u32>) {
65
+ let k = gid.x;
66
+ let laneValid = k < params.axisDim;
67
+ var kKey = 0u;
68
+ if (laneValid) {
69
+ kKey = slice_key(k);
70
+ }
71
+
72
+ var seen = false;
73
+ // Workgroup b only owns k in chunk b, so no lane can need a j from a later
74
+ // chunk. Keeping this loop bound workgroup-uniform preserves legal barriers.
75
+ let chunks = min(wid.x + 1u, (params.axisDim + WG - 1u) / WG);
76
+ for (var chunk = 0u; chunk < chunks; chunk = chunk + 1u) {
77
+ let chunkBase = chunk * WG;
78
+ let loadK = chunkBase + lid.x;
79
+ var loadedKey = 0u;
80
+ if (loadK < params.axisDim) {
81
+ loadedKey = slice_key(loadK);
82
+ }
83
+ keyCache[lid.x] = loadedKey;
84
+ workgroupBarrier();
85
+
86
+ if (laneValid && !seen) {
87
+ if (chunkBase < k) {
88
+ let end = min(WG, k - chunkBase);
89
+ for (var offset = 0u; offset < end; offset = offset + 1u) {
90
+ let j = chunkBase + offset;
91
+ if (keyCache[offset] == kKey && slice_eq(j, k)) {
92
+ seen = true;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ // Every lane must finish reading this chunk before the next one overwrites it.
99
+ workgroupBarrier();
100
+ }
101
+
102
+ if (laneValid) {
103
+ firstFlag[k] = select(1u, 0u, seen);
104
+ }
105
+ }
build/webgpu/unique-axis-hash.wgsl.jinja ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Hash-backed axis Unique front end. Initialization computes one full-slice
2
+ // hash per axis index and clears the open-addressed table. Build inserts
3
+ // exact-equality buckets and atomically retains the earliest representative.
4
+ // Mark converts occupied buckets into first-occurrence flags for compaction.
5
+ // Hash collisions always use the exact slice comparator, so hashes never
6
+ // affect semantics.
7
+ {{ env.wgsl.resourceDeclarations }}
8
+
9
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
10
+ const TABLE_SIZE: u32 = {{ source.tableSize }}u;
11
+ {% if source.stage == "build" %}
12
+ const TABLE_MASK: u32 = TABLE_SIZE - 1u;
13
+ {% endif %}
14
+ const EMPTY: u32 = 0xffffffffu;
15
+
16
+ {% if source.stage == "init" %}
17
+ fn value_key(value: {{ scalar }}) -> u32 {
18
+ return u32(value);
19
+ }
20
+
21
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
22
+ return x[(o * params.axisDim + k) * params.inner + n];
23
+ }
24
+
25
+ fn slice_hash(k: u32) -> u32 {
26
+ // FNV-1a over the complete slice. The axis index is deliberately excluded:
27
+ // equal slices must hash identically regardless of where they occur.
28
+ var hash = 0x811c9dc5u;
29
+ for (var o = 0u; o < params.outer; o += 1u) {
30
+ for (var n = 0u; n < params.inner; n += 1u) {
31
+ hash = (hash ^ value_key(slice_at(o, k, n))) * 0x01000193u;
32
+ }
33
+ }
34
+ // One final avalanche reduces clustering for short structured slices.
35
+ hash ^= hash >> 16u;
36
+ hash *= 0x7feb352du;
37
+ hash ^= hash >> 15u;
38
+ return hash;
39
+ }
40
+
41
+ @compute @workgroup_size(WG)
42
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
43
+ let i = gid.x;
44
+ if (i < TABLE_SIZE) {
45
+ atomicStore(&hashSlot[i], EMPTY);
46
+ }
47
+ if (i < params.axisDim) {
48
+ firstFlag[i] = 0u;
49
+ sliceHash[i] = slice_hash(i);
50
+ }
51
+ }
52
+ {% elif source.stage == "build" %}
53
+
54
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
55
+ return x[(o * params.axisDim + k) * params.inner + n];
56
+ }
57
+
58
+ fn slice_eq(a: u32, b: u32) -> bool {
59
+ for (var o = 0u; o < params.outer; o += 1u) {
60
+ for (var n = 0u; n < params.inner; n += 1u) {
61
+ if (slice_at(o, a, n) != slice_at(o, b, n)) { return false; }
62
+ }
63
+ }
64
+ return true;
65
+ }
66
+
67
+ @compute @workgroup_size(WG)
68
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
69
+ let k = gid.x;
70
+ if (k >= params.axisDim) { return; }
71
+ let key = sliceHash[k];
72
+ var slot = key & TABLE_MASK;
73
+ for (var probe = 0u; probe < TABLE_SIZE; probe += 1u) {
74
+ var representative = atomicLoad(&hashSlot[slot]);
75
+ if (representative == EMPTY) {
76
+ let inserted = atomicCompareExchangeWeak(&hashSlot[slot], EMPTY, k);
77
+ if (inserted.exchanged) { return; }
78
+ representative = inserted.old_value;
79
+ // compare-exchange may fail spuriously while the slot is still empty.
80
+ if (representative == EMPTY) { continue; }
81
+ }
82
+ if (sliceHash[representative] == key && slice_eq(representative, k)) {
83
+ atomicMin(&hashSlot[slot], k);
84
+ return;
85
+ }
86
+ slot = (slot + 1u) & TABLE_MASK;
87
+ }
88
+ }
89
+ {% else %}
90
+ @compute @workgroup_size(WG)
91
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
92
+ let slot = gid.x;
93
+ if (slot >= TABLE_SIZE) { return; }
94
+ let representative = atomicLoad(&hashSlot[slot]);
95
+ if (representative != EMPTY) {
96
+ firstFlag[representative] = 1u;
97
+ }
98
+ }
99
+ {% endif %}
build/webgpu/unique-axis-scatter.wgsl.jinja ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Grid-parallel output stage for axis Unique. Compact/sort must remain a single
4
+ // globally synchronized workgroup, but copying its selected slice indices into a
5
+ // wide Y does not: distribute that bandwidth-heavy tail across the device.
6
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
7
+ const CAP: u32 = {{ source.capacity }}u;
8
+ const AXIS_DIM: u32 = {{ source.axisDim }}u;
9
+ const INNER: u32 = {{ source.inner }}u;
10
+ const OUTPUT_AXIS_DIM: u32 = {{ source.outputAxisDim }}u;
11
+ const TOTAL_OUT: u32 = {{ source.totalOut }}u;
12
+
13
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
14
+ return x[(o * AXIS_DIM + k) * INNER + n];
15
+ }
16
+
17
+ // Keep exact-result axis scatter identical across static- and dynamic-shape
18
+ // dispatch strategies. The zero branch is a defensive guard for invalid shapes.
19
+ fn unique_axis_zero_value() -> {{ scalar }} {
20
+ return {{ scalar }}(0);
21
+ }
22
+
23
+ fn unique_axis_scatter_element(g: u32, written: u32) {
24
+ let n = g % INNER;
25
+ let tmp = g / INNER;
26
+ let p = tmp % OUTPUT_AXIS_DIM;
27
+ let o = tmp / OUTPUT_AXIS_DIM;
28
+ if (p < written) {
29
+ y[g] = slice_at(o, slots[p], n);
30
+ } else {
31
+ y[g] = unique_axis_zero_value();
32
+ }
33
+ }
34
+
35
+
36
+ @compute @workgroup_size(WG)
37
+ fn main(
38
+ @builtin(global_invocation_id) gid: vec3<u32>,
39
+ @builtin(num_workgroups) nwg: vec3<u32>
40
+ ) {
41
+ let written = min(count[0], CAP);
42
+ let stride = nwg.x * WG;
43
+ for (var g = gid.x; g < TOTAL_OUT; g += stride) {
44
+ unique_axis_scatter_element(g, written);
45
+ }
46
+ }
build/webgpu/unique-axis.wgsl.jinja ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // IEEE total-order comparators for Unique. Floating-point equality and ordering
2
+ // use raw bits because GPUs may flush subnormals in float comparisons, which
3
+ // would otherwise collapse distinct values. Equality canonicalizes -0 to +0;
4
+ // ordering uses a monotonic u32 key over the real line. Integers use == and <.
5
+ {% macro float_bits_def() %}
6
+ fn float_bits(v: {{ scalar }}) -> u32 {
7
+ {% if usesF16 %}
8
+ // WGSL has no scalar u16 type. Packing v into the low component preserves
9
+ // its binary16 representation while producing a bitcast-compatible 32 bits.
10
+ return bitcast<u32>(vec2<f16>(v, 0.0h)) & 0xffffu;
11
+ {% else %}
12
+ return bitcast<u32>(v);
13
+ {% endif %}
14
+ }
15
+ {%- endmacro -%}
16
+ {% macro eq_value_def() %}
17
+ fn eq_value(a: {{ scalar }}, b: {{ scalar }}) -> bool {
18
+ {% if isFloat %}
19
+ var ba = float_bits(a);
20
+ var bb = float_bits(b);
21
+ {% if usesF16 %}
22
+ if (ba == 0x8000u) { ba = 0u; } // -0 -> +0
23
+ if (bb == 0x8000u) { bb = 0u; }
24
+ {% else %}
25
+ if (ba == 0x80000000u) { ba = 0u; } // -0 -> +0
26
+ if (bb == 0x80000000u) { bb = 0u; }
27
+ {% endif %}
28
+ return ba == bb;
29
+ {% else %}
30
+ return a == b;
31
+ {% endif %}
32
+ }
33
+ {%- endmacro -%}
34
+ {%- macro less_value_def() %}
35
+ fn less_value(a: {{ scalar }}, b: {{ scalar }}) -> bool {
36
+ {% if isFloat %}
37
+ let ba = float_bits(a);
38
+ let bb = float_bits(b);
39
+ {% if usesF16 %}
40
+ let ka = select(ba | 0x8000u, (~ba) & 0xffffu, (ba & 0x8000u) != 0u);
41
+ let kb = select(bb | 0x8000u, (~bb) & 0xffffu, (bb & 0x8000u) != 0u);
42
+ {% else %}
43
+ let ka = select(ba | 0x80000000u, ~ba, (ba & 0x80000000u) != 0u);
44
+ let kb = select(bb | 0x80000000u, ~bb, (bb & 0x80000000u) != 0u);
45
+ {% endif %}
46
+ return ka < kb;
47
+ {% else %}
48
+ return a < b;
49
+ {% endif %}
50
+ }
51
+ {%- endmacro -%}
52
+ {%- macro zero_value_def() %}
53
+ fn zero_value() -> {{ scalar }} {
54
+ return {{ scalar }}(0);
55
+ }
56
+ {%- endmacro %}
57
+
58
+ {% set emitIndices = source.hasIndices | default(false) %}
59
+ {% set emitInverseIndices = source.hasInverseIndices | default(false) %}
60
+ {% set emitCounts = source.hasCounts | default(false) %}
61
+ {% if usesF16 %}
62
+ enable f16;
63
+ {% endif %}
64
+ {{ env.wgsl.resourceDeclarations }}
65
+
66
+ // Axis-mode Unique views the input as outer x axisDim x inner. Two axis slices
67
+ // are equal iff every (outer, inner) element matches. The first occurrence of
68
+ // each distinct slice is retained, optionally sorted lexicographically, then
69
+ // scattered into the data-dependent output allocation.
70
+ //
71
+ // Float equality and ordering use the same bit-preserving rules as the other
72
+ // Unique kernels, including canonicalized signed zero and FTZ-safe subnormals.
73
+
74
+ const CAP: u32 = {{ source.capacity }}u;
75
+ // The walk is one sequential step per slice and every step carries several
76
+ // workgroup barriers, so this width is barrier latency, not throughput: the
77
+ // head test settles in one round on real data and the chunked scan and shift
78
+ // both adapt to any width. Tuned separately from the op-wide WORKGROUP_SIZE.
79
+ const WG: u32 = {{ axisSerialWg }}u;
80
+ {% if isFloat %}
81
+
82
+ // Sentinel for the cooperative lower-bound reduction: no lane in the current
83
+ // chunk found a candidate whose comparison fails. It is above every reachable
84
+ // slice index because a chunk only tests indices below `written <= CAP`.
85
+ const NO_HIT: u32 = 0xffffffffu;
86
+ var<workgroup> lowerBoundHit: atomic<u32>;
87
+ var<workgroup> hitIndex: u32;
88
+ var<workgroup> diffAt: atomic<u32>;
89
+ var<workgroup> cmpOut: u32;
90
+ {% endif %}
91
+ {% set headCache = isFloat and (source.headCacheSlots | default(0)) > 0 %}
92
+ {% set floatSliceLess = isFloat and (emitInverseIndices or emitCounts) %}
93
+ {% set headOf = "headCache[candidate]" if headCache else "head_bits(order[candidate])" %}
94
+ {% set signBit = "0x8000u" if usesF16 else "0x80000000u" %}
95
+ {% set keyMask = "0xffffu" if usesF16 else "0xffffffffu" %}
96
+ {% set expMask = "0x7c00u" if usesF16 else "0x7f800000u" %}
97
+ {% set mantMask = "0x03ffu" if usesF16 else "0x007fffffu" %}
98
+ {% if isFloat %}
99
+
100
+ // `is_nan_bits` and `less_value` over a bit pattern that has already been
101
+ // loaded, so the comparators below can hoist one `float_bits` per value.
102
+ fn is_nan_head(b: u32) -> bool {
103
+ return (b & {{ expMask }}) == {{ expMask }} && (b & {{ mantMask }}) != 0u;
104
+ }
105
+
106
+ // Same monotonic key as `less_value`. Folding -0 to +0 moves its key by one
107
+ // step onto +0's, and no other value's key lies between them, so ordering
108
+ // against every other value is unchanged.
109
+ fn less_head(ba: u32, bb: u32) -> bool {
110
+ let ka = select(ba | {{ signBit }}, (~ba) & {{ keyMask }}, (ba & {{ signBit }}) != 0u);
111
+ let kb = select(bb | {{ signBit }}, (~bb) & {{ keyMask }}, (bb & {{ signBit }}) != 0u);
112
+ return ka < kb;
113
+ }
114
+ {% endif %}
115
+ {% if headCache %}
116
+
117
+ // Leading element of each written representative, kept beside `order`. The scan
118
+ // below compares one slice against every representative, and reading those
119
+ // leading elements out of X is a gather whose stride is the slice width, which
120
+ // can be a kilobyte, so every candidate lands on its own cache line. Widening
121
+ // the workgroup makes that worse rather than better, which is the tell: the
122
+ // limit is cache capacity, not latency. Caching the leading elements densely
123
+ // means the common case, where two slices already differ at element zero, never
124
+ // touches X. The integer dedup path solves the same problem the same way, with
125
+ // sampled slice fingerprints.
126
+ var<workgroup> headCache: array<u32, {{ source.headCacheSlots }}>;
127
+ {% endif %}
128
+ {% if isFloat %}
129
+
130
+ // Canonicalized leading-element bits: -0 folds to +0 exactly as `eq_value`
131
+ // does, so equal head bits mean the leading elements compare equal.
132
+ fn head_bits(k: u32) -> u32 {
133
+ if (params.outer == 0u || params.inner == 0u) { return 0u; }
134
+ let b = float_bits(slice_at(0u, k, 0u));
135
+ return select(b, 0u, b == {{ signBit }});
136
+ }
137
+ {% endif %}
138
+
139
+ {% if isFloat %}
140
+ // Whether `cand` could be the lower bound, judged from leading elements alone:
141
+ // exactly `!slice_less(cand, k)` when the leading elements differ, and
142
+ // optimistically true when they match, which `slice_cmp_coop` then settles.
143
+ // Over-reporting only ever lowers the candidate, and a candidate that fails the
144
+ // exact test is skipped and the scan resumes after it -- so the index this
145
+ // converges on is the first truly qualifying one, which is what the serial
146
+ // scan stops at.
147
+ fn head_qualifies(hc: u32, hk: u32) -> bool {
148
+ if (is_nan_head(hc) || is_nan_head(hk)) { return true; }
149
+ if (hc == hk) { return true; }
150
+ return !less_head(hc, hk);
151
+ }
152
+
153
+ // Three-way slice comparison across the whole workgroup: 0 when the slices are
154
+ // equivalent (identical, or decided at a coordinate holding a NaN, which
155
+ // `slice_less` reports as neither-less), 1 when a < b, 2 when b < a.
156
+ //
157
+ // The serial `slice_less` walks a slice one element at a time on a single lane,
158
+ // and its exit is data dependent, so a pair of EQUAL slices -- what a duplicate
159
+ // input produces, and the reason this comparison exists -- runs the full slice
160
+ // length at one memory latency per element, and that walk dominates the kernel.
161
+ // Here every lane takes one coordinate and the first difference falls out of a
162
+ // min-reduction, so an equal pair costs one round instead of one per element.
163
+ fn slice_cmp_coop(a: u32, b: u32, lane: u32) -> u32 {
164
+ if (lane == 0u) { atomicStore(&diffAt, NO_HIT); }
165
+ workgroupBarrier();
166
+ let total = params.outer * params.inner;
167
+ for (var c = lane; c < total; c = c + WG) {
168
+ // A difference already found below this coordinate settles the comparison;
169
+ // this lane's remaining coordinates are all above it.
170
+ if (atomicLoad(&diffAt) < c) { break; }
171
+ let o = c / params.inner;
172
+ let n = c - o * params.inner;
173
+ let ba = float_bits(slice_at(o, a, n));
174
+ let bb = float_bits(slice_at(o, b, n));
175
+ if (is_nan_head(ba) || is_nan_head(bb)
176
+ || select(ba, 0u, ba == {{ signBit }}) != select(bb, 0u, bb == {{ signBit }})) {
177
+ atomicMin(&diffAt, c);
178
+ }
179
+ }
180
+ workgroupBarrier();
181
+ if (lane == 0u) {
182
+ let c = atomicLoad(&diffAt);
183
+ var verdict = 0u;
184
+ if (c != NO_HIT) {
185
+ let o = c / params.inner;
186
+ let n = c - o * params.inner;
187
+ let ba = float_bits(slice_at(o, a, n));
188
+ let bb = float_bits(slice_at(o, b, n));
189
+ if (!(is_nan_head(ba) || is_nan_head(bb))) {
190
+ verdict = select(2u, 1u,
191
+ less_head(select(ba, 0u, ba == {{ signBit }}), select(bb, 0u, bb == {{ signBit }})));
192
+ }
193
+ }
194
+ cmpOut = verdict;
195
+ }
196
+ return workgroupUniformLoad(&cmpOut);
197
+ }
198
+ {% endif %}
199
+
200
+ fn slice_at(o: u32, k: u32, n: u32) -> {{ scalar }} {
201
+ return x[(o * params.axisDim + k) * params.inner + n];
202
+ }
203
+
204
+ // FTZ-safe element comparators use raw IEEE bits because float ALUs may flush
205
+ // subnormals and collapse distinct rows. Signed zero is canonicalized before
206
+ // applying an IEEE total-order key; non-float types use native ==/<.
207
+ {% if isFloat %}
208
+ {{ float_bits_def() }}
209
+
210
+ {% endif %}
211
+ {% if not isFloat %}
212
+ {{ eq_value_def() }}
213
+ {% if sorted %}
214
+
215
+ {{ less_value_def() }}
216
+ {% endif %}
217
+ {% endif %}
218
+ {% if floatSliceLess %}
219
+
220
+ // Lexicographic comparison stops at the first unequal coordinate. If either
221
+ // value there is NaN, neither slice is less and ordered insertion treats the
222
+ // keys as equivalent.
223
+ fn slice_less(a: u32, b: u32) -> bool {
224
+ for (var o = 0u; o < params.outer; o = o + 1u) {
225
+ let baseA = (o * params.axisDim + a) * params.inner;
226
+ let baseB = (o * params.axisDim + b) * params.inner;
227
+ for (var n = 0u; n < params.inner; n = n + 1u) {
228
+ // Same three tests as `is_nan_bits` / `eq_value` / `less_value`, over one
229
+ // pair of bit patterns instead of six independent `float_bits` calls --
230
+ // which on f16 is a vec2 pack and mask each time. Two equal slices, the
231
+ // case a duplicate input hits, walk to the end of the slice, so this loop
232
+ // body runs once per element and was the kernel's dominant cost.
233
+ let ba = float_bits(x[baseA + n]);
234
+ let bb = float_bits(x[baseB + n]);
235
+ if (is_nan_head(ba) || is_nan_head(bb)) { return false; }
236
+ // -0 folds to +0 exactly as `eq_value` does.
237
+ let ca = select(ba, 0u, ba == {{ signBit }});
238
+ let cb = select(bb, 0u, bb == {{ signBit }});
239
+ if (ca == cb) { continue; }
240
+ return less_head(ca, cb);
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+ {% elif sorted and not isFloat %}
246
+
247
+ // true iff slice a < slice b lexicographically over (outer, inner) order.
248
+ fn slice_less(a: u32, b: u32) -> bool {
249
+ for (var o = 0u; o < params.outer; o = o + 1u) {
250
+ for (var n = 0u; n < params.inner; n = n + 1u) {
251
+ let va = slice_at(o, a, n);
252
+ let vb = slice_at(o, b, n);
253
+ if (less_value(va, vb)) { return true; }
254
+ if (less_value(vb, va)) { return false; }
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+
260
+ {% endif %}
261
+ {% if not isFloat %}
262
+ // true iff slice a equals slice b element-wise.
263
+ fn slice_eq(a: u32, b: u32) -> bool {
264
+ for (var o = 0u; o < params.outer; o = o + 1u) {
265
+ for (var n = 0u; n < params.inner; n = n + 1u) {
266
+ if (!eq_value(slice_at(o, a, n), slice_at(o, b, n))) { return false; }
267
+ }
268
+ }
269
+ return true;
270
+ }
271
+ {% endif %}
272
+
273
+ {{ zero_value_def() }}
274
+ {% if sorted and not isFloat %}
275
+
276
+ fn sort_order(written: u32) {
277
+ for (var a = 1u; a < written; a = a + 1u) {
278
+ let key = order[a];
279
+ var b = a;
280
+ loop {
281
+ if (b == 0u) { break; }
282
+ if (!slice_less(key, order[b - 1u])) { break; }
283
+ order[b] = order[b - 1u];
284
+ b = b - 1u;
285
+ }
286
+ order[b] = key;
287
+ }
288
+ }
289
+
290
+ {% endif %}
291
+ {% if isFloat and not sorted %}
292
+ // The map is built in comparator order. Unsorted ONNX output instead uses
293
+ // bucket insertion order, which is ascending representative input index.
294
+ fn sort_by_first_occurrence(written: u32) {
295
+ for (var a = 1u; a < written; a = a + 1u) {
296
+ let key = order[a];
297
+ var b = a;
298
+ loop {
299
+ if (b == 0u || order[b - 1u] < key) { break; }
300
+ order[b] = order[b - 1u];
301
+ b = b - 1u;
302
+ }
303
+ order[b] = key;
304
+ }
305
+ }
306
+ {% endif %}
307
+
308
+ {% if isFloat and (emitInverseIndices or emitCounts) %}
309
+ // Find the bucket selected when input slice k was inserted. Representatives
310
+ // with a later first occurrence did not exist yet and must not influence the
311
+ // lower_bound result; this preserves stateful NaN equivalence after output is
312
+ // reordered.
313
+ fn find_bucket(k: u32, written: u32) -> u32 {
314
+ var candidate = written;
315
+ for (var p = 0u; p < written; p = p + 1u) {
316
+ let representative = order[p];
317
+ if (representative > k || slice_less(representative, k)) { continue; }
318
+ if (candidate == written || slice_less(representative, order[candidate])) {
319
+ candidate = p;
320
+ }
321
+ }
322
+ if (candidate < written && !slice_less(k, order[candidate])) {
323
+ return candidate;
324
+ }
325
+ return 0u;
326
+ }
327
+
328
+ {% endif %}
329
+ @compute @workgroup_size(WG)
330
+ fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
331
+ {% if isFloat %}
332
+ // Reproduce ordered lower-bound insertion. NaN makes the comparator
333
+ // non-transitive, so pairwise equality and conventional hash tables cannot
334
+ // preserve its stateful bucket behavior.
335
+ //
336
+ // Every representative written so far is a lower-bound candidate, so the scan
337
+ // over them is the whole cost, and each step is a strided pair of slice loads
338
+ // that pays full memory latency. Scanning on one lane left the rest of the
339
+ // workgroup idle and issued those loads one at a time. Here the workgroup
340
+ // tests a chunk of candidates at once and keeps the SMALLEST index whose
341
+ // comparison fails -- the index the serial scan stops at, since it stops at
342
+ // the first failure too. Testing the remainder of a chunk is redundant, never
343
+ // observable: `slice_less` reads x and writes nothing.
344
+ //
345
+ // Both inner loops are barrier-free, so their bounds may depend on the live
346
+ // count; every barrier sits at the top level of this loop, whose bound comes
347
+ // from the uniform buffer, which is what WGSL's uniformity analysis requires.
348
+ var written = 0u;
349
+ for (var k = 0u; k < params.axisDim; k = k + 1u) {
350
+ let headK = head_bits(k);
351
+ // Walk the representatives for the first one that is not less than this
352
+ // slice. The head-only test is exact whenever the leading elements differ,
353
+ // so on real data this settles in a single round; a leading-element tie is
354
+ // resolved by one exact comparison, and the scan resumes past it only if
355
+ // that representative really does sort first.
356
+ var searchFrom = 0u;
357
+ var lowerBound = written;
358
+ var equivalent = false;
359
+ loop {
360
+ if (lid.x == 0u) { atomicStore(&lowerBoundHit, NO_HIT); }
361
+ workgroupBarrier();
362
+ for (var base = searchFrom; base < written; base = base + WG) {
363
+ // Chunks ascend, so an index already recorded is smaller than anything
364
+ // this chunk holds. Reading the atomic without a barrier can only see
365
+ // the hit late, which costs a redundant chunk and never a wrong bound.
366
+ if (atomicLoad(&lowerBoundHit) != NO_HIT) { break; }
367
+ let candidate = base + lid.x;
368
+ if (candidate >= searchFrom && candidate < written
369
+ && head_qualifies({{ headOf }}, headK)) {
370
+ atomicMin(&lowerBoundHit, candidate);
371
+ }
372
+ }
373
+ workgroupBarrier();
374
+ // Republished so the analysis accepts it as uniform: the exact comparison
375
+ // below carries barriers, so everything gating it has to be uniform.
376
+ if (lid.x == 0u) { hitIndex = atomicLoad(&lowerBoundHit); }
377
+ let hit = workgroupUniformLoad(&hitIndex);
378
+ if (hit == NO_HIT) {
379
+ // Nothing qualifies, so the serial scan would have run off the end.
380
+ lowerBound = written;
381
+ equivalent = false;
382
+ break;
383
+ }
384
+ let verdict = slice_cmp_coop(k, order[hit], lid.x);
385
+ if (verdict == 0u) { lowerBound = hit; equivalent = true; break; }
386
+ if (verdict == 1u) { lowerBound = hit; equivalent = false; break; }
387
+ // This representative sorts before the slice after all, so it is not the
388
+ // lower bound; resume the scan above it.
389
+ searchFrom = hit + 1u;
390
+ }
391
+ let insert = !equivalent && written < CAP;
392
+ // Open a slot at `lowerBound` by moving [lowerBound, written) up one place.
393
+ // Splitting that range into one contiguous run per lane keeps every move
394
+ // private except at a run's low end, which the run below overwrites -- so
395
+ // each lane preloads that single element and the whole shift needs one
396
+ // barrier instead of one per chunk.
397
+ let span = select(0u, written - lowerBound, insert);
398
+ let perLane = (span + WG - 1u) / WG;
399
+ let runLo = lowerBound + min(span, lid.x * perLane);
400
+ let runHi = lowerBound + min(span, (lid.x + 1u) * perLane);
401
+ var carry = 0u;
402
+ {% if headCache %}
403
+ var carryHead = 0u;
404
+ {% endif %}
405
+ if (runLo < runHi) {
406
+ carry = order[runLo];
407
+ {% if headCache %}
408
+ carryHead = headCache[runLo];
409
+ {% endif %}
410
+ }
411
+ storageBarrier();
412
+ {% if headCache %}
413
+ workgroupBarrier();
414
+ {% endif %}
415
+ if (runLo < runHi) {
416
+ // Top-down within the run, so a slot is read before it is written.
417
+ var p = runHi - 1u;
418
+ loop {
419
+ if (p <= runLo) { break; }
420
+ order[p + 1u] = order[p];
421
+ {% if headCache %}
422
+ headCache[p + 1u] = headCache[p];
423
+ {% endif %}
424
+ p = p - 1u;
425
+ }
426
+ order[runLo + 1u] = carry;
427
+ {% if headCache %}
428
+ headCache[runLo + 1u] = carryHead;
429
+ {% endif %}
430
+ }
431
+ if (insert && lid.x == 0u) {
432
+ order[lowerBound] = k;
433
+ {% if headCache %}
434
+ headCache[lowerBound] = headK;
435
+ {% endif %}
436
+ }
437
+ storageBarrier();
438
+ {% if headCache %}
439
+ workgroupBarrier();
440
+ {% endif %}
441
+ if (insert) { written = written + 1u; }
442
+ }
443
+ {% if not sorted %}
444
+ if (lid.x == 0u) {
445
+ sort_by_first_occurrence(written);
446
+ }
447
+ {% endif %}
448
+ {% else %}
449
+ if (params.axisDim <= CAP) {
450
+ // When every input slice can be represented in the order scratch, test
451
+ // first-occurrence status independently. Duplicate-heavy inputs usually
452
+ // exit after only a handful of comparisons, and all lanes participate.
453
+ for (var k = lid.x; k < params.axisDim; k = k + WG) {
454
+ var seen = false;
455
+ for (var j = 0u; j < k; j = j + 1u) {
456
+ if (slice_eq(j, k)) { seen = true; break; }
457
+ }
458
+ order[k] = select(1u, 0u, seen);
459
+ }
460
+ storageBarrier();
461
+
462
+ if (lid.x == 0u) {
463
+ var written = 0u;
464
+ for (var k = 0u; k < params.axisDim; k = k + 1u) {
465
+ let first = order[k];
466
+ if (first != 0u) {
467
+ // Compacting in increasing k order preserves first-occurrence order.
468
+ // `written <= k`, so this never overwrites an unread flag.
469
+ order[written] = k;
470
+ written = written + 1u;
471
+ }
472
+ }
473
+ {% if sorted %}
474
+
475
+ sort_order(written);
476
+
477
+ {% endif %}
478
+ }
479
+ } else if (lid.x == 0u) {
480
+ // When axisDim exceeds the exact output capacity, discover slices serially
481
+ // while retaining one representative for every distinct result.
482
+ var written = 0u;
483
+ for (var k = 0u; k < params.axisDim; k = k + 1u) {
484
+ var seen = false;
485
+ for (var p = 0u; p < written; p = p + 1u) {
486
+ if (slice_eq(order[p], k)) { seen = true; break; }
487
+ }
488
+ if (!seen) {
489
+ if (written < CAP) {
490
+ order[written] = k;
491
+ written = written + 1u;
492
+ }
493
+ }
494
+ }
495
+ {% if sorted %}
496
+
497
+ sort_order(written);
498
+
499
+ {% endif %}
500
+ }
501
+ {% endif %}
502
+ // `order` is storage-backed so legal data-dependent capacities are not
503
+ // constrained by maxComputeWorkgroupStorageSize.
504
+ storageBarrier();
505
+ // The public contract requires the caller-provided Y axis dimension to equal
506
+ // the exact unique count, so it is also the cross-lane written count.
507
+ let writtenCount = params.outputAxisDim;
508
+
509
+ // Scattering the selected slices is independent once `order` is ready, so
510
+ // spread both data and metadata stores across the workgroup.
511
+ let totalOut = params.outer * params.outputAxisDim * params.inner;
512
+ for (var out_i = lid.x; out_i < totalOut; out_i = out_i + WG) {
513
+ let n = out_i % params.inner;
514
+ let axisOuter = out_i / params.inner;
515
+ let p = axisOuter % params.outputAxisDim;
516
+ let o = axisOuter / params.outputAxisDim;
517
+ if (p < writtenCount) {
518
+ y[out_i] = slice_at(o, order[p], n);
519
+ } else {
520
+ y[out_i] = zero_value();
521
+ }
522
+ }
523
+ {% if emitIndices %}
524
+ for (var p = lid.x; p < writtenCount; p = p + WG) {
525
+ indices[p] = order[p];
526
+ }
527
+ {% endif %}
528
+ {% if emitInverseIndices %}
529
+ for (var k = lid.x; k < params.axisDim; k = k + WG) {
530
+ {% if isFloat %}
531
+ inverse_indices[k] = find_bucket(k, writtenCount);
532
+ {% else %}
533
+ var uniqueIndex = 0u;
534
+ for (var p = 0u; p < writtenCount; p = p + 1u) {
535
+ if (slice_eq(order[p], k)) {
536
+ uniqueIndex = p;
537
+ break;
538
+ }
539
+ }
540
+ inverse_indices[k] = uniqueIndex;
541
+ {% endif %}
542
+ }
543
+ {% endif %}
544
+ {% if emitCounts %}
545
+ for (var p = lid.x; p < writtenCount; p = p + WG) {
546
+ var occurrenceCount = 0u;
547
+ for (var k = 0u; k < params.axisDim; k = k + 1u) {
548
+ {% if isFloat %}
549
+ if (find_bucket(k, writtenCount) == p) {
550
+ {% else %}
551
+ if (slice_eq(order[p], k)) {
552
+ {% endif %}
553
+ occurrenceCount = occurrenceCount + 1u;
554
+ }
555
+ }
556
+ counts[p] = occurrenceCount;
557
+ }
558
+ {% endif %}
559
+ }
build/webgpu/unique-compact-sort.wgsl.jinja ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Pass 2 of parallel Unique: compact flagged first occurrences in appearance
4
+ // order, optionally bitonic-sort them, and write the exact result. Small results
5
+ // keep scratch in workgroup memory, while larger results use storage buffers
6
+ // selected from device limits.
7
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
8
+ const CAP: u32 = {{ source.capacity }}u;
9
+ {% if sorted %}
10
+ // The power-of-two padded tail sorts after every real value.
11
+ const SORT_N: u32 = {{ source.sortN }}u;
12
+
13
+ {% endif %}
14
+ var<workgroup> wgScan: array<u32, WG>;
15
+ var<workgroup> wgCarry: u32;
16
+ {% if not source.globalScratch %}
17
+ {% if sorted %}
18
+ var<workgroup> sortKey: array<u32, SORT_N>;
19
+ var<workgroup> sortVal: array<{{ scalar }}, SORT_N>;
20
+ var<workgroup> sortPad: array<u32, SORT_N>; // 1 = padding, 0 = real unique
21
+ {% else %}
22
+ var<workgroup> compacted: array<{{ scalar }}, CAP>;
23
+ {% endif %}
24
+
25
+ {% endif %}
26
+ fn zero_value() -> {{ scalar }} {
27
+ return {{ scalar }}(0);
28
+ }
29
+ {% if sorted %}
30
+
31
+ // Monotonic key matching unsigned or signed integer order. Global scratch
32
+ // already stores raw bits.
33
+ fn sort_key(v: {% if source.globalScratch %}u32{% else %}{{ scalar }}{% endif %}) -> u32 {
34
+ {% if source.globalScratch %}
35
+ let b = v;
36
+ {% else %}
37
+ let b = bitcast<u32>(v);
38
+ {% endif %}
39
+ {% if isUnsigned %}
40
+ return b;
41
+ {% else %}
42
+ return b ^ 0x80000000u;
43
+ {% endif %}
44
+ }
45
+
46
+ {% endif %}
47
+ {% macro scratch_barrier() %}
48
+ {% if source.globalScratch %}storageBarrier();{% else %}workgroupBarrier();{% endif %}
49
+ {% endmacro %}
50
+
51
+ @compute @workgroup_size(WG)
52
+ fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
53
+ let tid = lid.x;
54
+ if (tid == 0u) {
55
+ wgCarry = 0u;
56
+ }
57
+ workgroupBarrier();
58
+
59
+ // Chunked Hillis-Steele exclusive scan maps flags to appearance-order ranks.
60
+ let chunks = (params.inputCount + WG - 1u) / WG;
61
+ for (var c = 0u; c < chunks; c = c + 1u) {
62
+ let i = c * WG + tid;
63
+ var f = 0u;
64
+ if (i < params.inputCount) {
65
+ f = flags[i];
66
+ }
67
+ wgScan[tid] = f;
68
+ workgroupBarrier();
69
+ var stride = 1u;
70
+ loop {
71
+ if (stride >= WG) { break; }
72
+ var add = 0u;
73
+ if (tid >= stride) {
74
+ add = wgScan[tid - stride];
75
+ }
76
+ workgroupBarrier();
77
+ wgScan[tid] = wgScan[tid] + add;
78
+ workgroupBarrier();
79
+ stride = stride * 2u;
80
+ }
81
+ let excl = wgScan[tid] - f;
82
+ let pos = wgCarry + excl;
83
+ if (i < params.inputCount && f == 1u && pos < CAP) {
84
+ {% if source.globalScratch %}
85
+ sortVal[pos] = bitcast<u32>(x[i]);
86
+ {% elif sorted %}
87
+ sortVal[pos] = x[i];
88
+ {% else %}
89
+ compacted[pos] = x[i];
90
+ {% endif %}
91
+ }
92
+ workgroupBarrier();
93
+ if (tid == 0u) {
94
+ wgCarry = wgCarry + wgScan[WG - 1u];
95
+ }
96
+ workgroupBarrier();
97
+ }
98
+
99
+ let total = wgCarry;
100
+ let written = min(total, CAP);
101
+ {% if source.globalScratch %}
102
+ storageBarrier();
103
+
104
+ {% endif %}
105
+ {% if sorted %}
106
+ // Padding flag is the primary key, so even a real maximum-valued key stays
107
+ // before padding. Unique values are distinct, making the bit key a total order.
108
+ for (var k = tid; k < SORT_N; k = k + WG) {
109
+ let isPad = select(0u, 1u, k >= written);
110
+ sortPad[k] = isPad;
111
+ if (isPad == 0u) {
112
+ sortKey[k] = sort_key(sortVal[k]);
113
+ } else {
114
+ {% if source.globalScratch %}
115
+ sortVal[k] = 0u;
116
+ {% else %}
117
+ sortVal[k] = zero_value();
118
+ {% endif %}
119
+ sortKey[k] = 0xffffffffu;
120
+ }
121
+ }
122
+ {{ scratch_barrier() }}
123
+
124
+ // Batcher bitonic network. Each lane owns one side of a compare-exchange.
125
+ var size = 2u;
126
+ loop {
127
+ if (size > SORT_N) { break; }
128
+ var stride = size / 2u;
129
+ loop {
130
+ if (stride == 0u) { break; }
131
+ for (var k = tid; k < SORT_N; k = k + WG) {
132
+ let partner = k ^ stride;
133
+ if (partner > k) {
134
+ let ascending = (k & size) == 0u;
135
+ let pk = sortPad[k];
136
+ let pp = sortPad[partner];
137
+ let kk = sortKey[k];
138
+ let kp = sortKey[partner];
139
+ let kBeforeP = (pk < pp) || (pk == pp && kk <= kp);
140
+ let needSwap = select(kBeforeP, !kBeforeP, ascending);
141
+ if (needSwap) {
142
+ let tKey = sortKey[k];
143
+ sortKey[k] = sortKey[partner];
144
+ sortKey[partner] = tKey;
145
+ let tVal = sortVal[k];
146
+ sortVal[k] = sortVal[partner];
147
+ sortVal[partner] = tVal;
148
+ let tPad = sortPad[k];
149
+ sortPad[k] = sortPad[partner];
150
+ sortPad[partner] = tPad;
151
+ }
152
+ }
153
+ }
154
+ {{ scratch_barrier() }}
155
+ stride = stride / 2u;
156
+ }
157
+ size = size * 2u;
158
+ }
159
+
160
+ {% endif %}
161
+ {% if not source.globalScratch %}
162
+ workgroupBarrier();
163
+ {% endif %}
164
+ for (var k = tid; k < CAP; k = k + WG) {
165
+ if (k < written) {
166
+ {% if source.globalScratch %}
167
+ y[k] = bitcast<{{ scalar }}>(sortVal[k]);
168
+ {% elif sorted %}
169
+ y[k] = sortVal[k];
170
+ {% else %}
171
+ y[k] = compacted[k];
172
+ {% endif %}
173
+ } else {
174
+ y[k] = zero_value();
175
+ }
176
+ }
177
+ }
build/webgpu/unique-dedup.wgsl.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Pass 1 of parallel Unique: grid-parallel first-occurrence test. One thread per
4
+ // input element, dispatched across all cores (ceil(n/WG) workgroups), so the
5
+ // O(n^2) "have I seen this value before" scan runs in parallel rather than on a
6
+ // single lane. Each workgroup cooperatively stages earlier values in a shared
7
+ // tile: WG lanes consume each global load instead of independently rereading
8
+ // the same prefix. flags[i] = 1 iff x[i] is the first occurrence of its value.
9
+ //
10
+ // This parallel equality path handles integer storage values only.
11
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
12
+ var<workgroup> valueTile: array<{{ scalar }}, WG>;
13
+
14
+ @compute @workgroup_size(WG)
15
+ fn main(
16
+ @builtin(global_invocation_id) gid: vec3<u32>,
17
+ @builtin(local_invocation_id) lid: vec3<u32>,
18
+ @builtin(workgroup_id) workgroup: vec3<u32>,
19
+ ) {
20
+ let i = gid.x;
21
+ let laneActive = i < params.inputCount;
22
+ var value: {{ scalar }};
23
+ if (laneActive) {
24
+ value = x[i];
25
+ }
26
+ var seen = !laneActive;
27
+ // Every lane executes every barrier, including the inactive tail lanes of the
28
+ // final workgroup. groupEnd is uniform within a workgroup, so the tile loop is
29
+ // uniform as well. The current workgroup's tile is the last one any lane needs.
30
+ let groupEnd = min((workgroup.x + 1u) * WG, params.inputCount);
31
+ for (var tileBase = 0u; tileBase < groupEnd; tileBase += WG) {
32
+ let sourceIndex = tileBase + lid.x;
33
+ if (sourceIndex < params.inputCount) {
34
+ valueTile[lid.x] = x[sourceIndex];
35
+ }
36
+ workgroupBarrier();
37
+
38
+ if (laneActive && !seen) {
39
+ for (var k = 0u; k < WG; k += 1u) {
40
+ let earlier = tileBase + k;
41
+ if (earlier >= i) { break; }
42
+ if (valueTile[k] == value) {
43
+ seen = true;
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ workgroupBarrier();
49
+ }
50
+
51
+ if (laneActive) {
52
+ flags[i] = select(0u, 1u, !seen);
53
+ }
54
+ }
build/webgpu/unique-hash-build.wgsl.jinja ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // One thread per input element inserts its value into an open-addressing hash
4
+ // table. Unsorted Unique also folds in each value's minimum input index. Sorted
5
+ // Unique only needs the keys themselves; its final pass sorts their values, so
6
+ // omitting tableIdx removes one atomic operation and one table-sized buffer.
7
+ //
8
+ // Race-freedom: the claim-and-publish of a slot's key is a single atomic
9
+ // compareExchange (EMPTY -> key). There is no window where a slot is claimed but
10
+ // its key is unpublished, so a concurrent probe either sees EMPTY (slot still
11
+ // free, keep this slot via the CAS) or sees the final key value. Linear probing
12
+ // on collision; the table is sized to >= 2*inputCount, so an empty slot always
13
+ // exists and the probe terminates. Raw u32 value bits form the key. Integer
14
+ // 0xffffffff cannot occupy a table slot because it is the EMPTY sentinel, so
15
+ // its minimum index uses `special`.
16
+ // Open-addressing table constants and key/hash helpers.
17
+ const EMPTY: u32 = 0xffffffffu;
18
+ const MASK: u32 = {{ source.tableSize }}u - 1u;
19
+
20
+ fn key_bits(v: {{ scalar }}) -> u32 {
21
+ {% if isUnsigned %}
22
+ return v;
23
+ {% else %}
24
+ return bitcast<u32>(v);
25
+ {% endif %}
26
+ }
27
+ // Integer hash (Wang-style mix) to spread keys across the table.
28
+ fn hash_key(k: u32) -> u32 {
29
+ var x = k;
30
+ x = x ^ (x >> 16u);
31
+ x = x * 0x7feb352du;
32
+ x = x ^ (x >> 15u);
33
+ x = x * 0x846ca68bu;
34
+ x = x ^ (x >> 16u);
35
+ return x;
36
+ }
37
+
38
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
39
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
40
+ let i = gid.x;
41
+ if (i >= params.inputCount) {
42
+ return;
43
+ }
44
+ let k = key_bits(x[i]);
45
+ if (k == EMPTY) {
46
+ atomicMin(&special[0], i);
47
+ return;
48
+ }
49
+ var h = hash_key(k) & MASK;
50
+ loop {
51
+ let res = atomicCompareExchangeWeak(&tableKey[h], EMPTY, k);
52
+ if (res.exchanged || res.old_value == k) {
53
+ {% if not source.keyOnly %}
54
+ // This slot now holds k (we just placed it, or it already held k).
55
+ atomicMin(&tableIdx[h], i);
56
+ {% endif %}
57
+ break;
58
+ }
59
+ h = (h + 1u) & MASK;
60
+ }
61
+ }
build/webgpu/unique-hash-collect.wgsl.jinja ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if source.useSubgroups %}
2
+ enable subgroups;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ // Sorted Unique does not need appearance order before its final sort. Collect
7
+ // occupied hash keys directly and reserve output positions once per workgroup.
8
+ // This avoids serializing every distinct value through one global atomic.
9
+ const EMPTY: u32 = 0xffffffffu;
10
+ const TABLE_SIZE: u32 = {{ source.tableSize }}u;
11
+ const CAP: u32 = {{ source.capacity }}u;
12
+ {% if source.useSubgroups %}
13
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
14
+
15
+ {% endif %}
16
+ var<workgroup> groupBase: u32;
17
+ {% if source.useSubgroups %}
18
+ // The first subgroup lane writes its total. Lane zero converts these totals to
19
+ // subgroup offsets and performs the workgroup's sole global count reservation.
20
+ var<workgroup> subgroupOffsets: array<u32, WG>;
21
+ {% else %}
22
+ // Without subgroup operations, a workgroup-local atomic provides the same
23
+ // compaction. Its contention never leaves shared memory and is bounded by WG.
24
+ var<workgroup> localCount: atomic<u32>;
25
+ {% endif %}
26
+
27
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
28
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
29
+ @builtin(local_invocation_id) lid: vec3<u32>{% if source.useSubgroups %},
30
+ @builtin(subgroup_invocation_id) sgLane: u32,
31
+ @builtin(subgroup_size) sgSize: u32{% endif %}) {
32
+ let h = gid.x;
33
+ var itemCount = 0u;
34
+ var item0 = 0u;
35
+ var item1 = 0u;
36
+
37
+ if (h < TABLE_SIZE) {
38
+ let key = tableKey[h];
39
+ if (key != EMPTY) {
40
+ item0 = key;
41
+ itemCount = 1u;
42
+ }
43
+ if (h == 0u) {
44
+ let specialIndex = atomicLoad(&special[0]);
45
+ if (specialIndex != EMPTY) {
46
+ let specialBits = bitcast<u32>(x[specialIndex]);
47
+ if (itemCount == 0u) {
48
+ item0 = specialBits;
49
+ } else {
50
+ item1 = specialBits;
51
+ }
52
+ itemCount += 1u;
53
+ }
54
+ }
55
+ }
56
+
57
+ {% if source.useSubgroups %}
58
+ let subgroupPrefix = subgroupExclusiveAdd(itemCount);
59
+ let subgroupTotal = subgroupAdd(itemCount);
60
+ let subgroupId = lid.x / sgSize;
61
+ let subgroupCount = (WG + sgSize - 1u) / sgSize;
62
+ if (sgLane == 0u) {
63
+ subgroupOffsets[subgroupId] = subgroupTotal;
64
+ }
65
+ workgroupBarrier();
66
+ if (lid.x == 0u) {
67
+ var running = 0u;
68
+ for (var s = 0u; s < subgroupCount; s += 1u) {
69
+ let subgroupItems = subgroupOffsets[s];
70
+ subgroupOffsets[s] = running;
71
+ running += subgroupItems;
72
+ }
73
+ groupBase = 0u;
74
+ if (running != 0u) {
75
+ groupBase = atomicAdd(&count[0], running);
76
+ }
77
+ }
78
+ workgroupBarrier();
79
+ let localPrefix = subgroupOffsets[subgroupId] + subgroupPrefix;
80
+ {% else %}
81
+ if (lid.x == 0u) {
82
+ atomicStore(&localCount, 0u);
83
+ }
84
+ workgroupBarrier();
85
+ let localPrefix = atomicAdd(&localCount, itemCount);
86
+ workgroupBarrier();
87
+ if (lid.x == 0u) {
88
+ let workgroupItems = atomicLoad(&localCount);
89
+ groupBase = 0u;
90
+ if (workgroupItems != 0u) {
91
+ groupBase = atomicAdd(&count[0], workgroupItems);
92
+ }
93
+ }
94
+ workgroupBarrier();
95
+ {% endif %}
96
+
97
+ let outputIndex = groupBase + localPrefix;
98
+ if (itemCount >= 1u && outputIndex < CAP) {
99
+ compactedBits[outputIndex] = item0;
100
+ }
101
+ if (itemCount >= 2u && outputIndex + 1u < CAP) {
102
+ compactedBits[outputIndex + 1u] = item1;
103
+ }
104
+ }
build/webgpu/unique-hash-init.wgsl.jinja ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Initialize the Unique hash table. Sorted collection only needs the key table,
4
+ // so that path clears four keys per invocation with ordinary vector stores.
5
+ // The first-occurrence path keeps both atomic tables because its build pass
6
+ // folds each key's minimum input index into tableIdx, and it leaves the output
7
+ // counter alone -- only the sorted-collect path counts from zero here.
8
+ const EMPTY: u32 = 0xffffffffu;
9
+ const TABLE_SIZE: u32 = {{ source.tableSize }}u;
10
+
11
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
12
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
13
+ {% if source.keyOnlyVec4 %}
14
+ let h4 = gid.x;
15
+ if (h4 >= TABLE_SIZE / 4u) {
16
+ return;
17
+ }
18
+ tableKey[h4] = vec4<u32>(EMPTY);
19
+ if (h4 == 0u) {
20
+ atomicStore(&special[0], EMPTY);
21
+ atomicStore(&count[0], 0u);
22
+ }
23
+ {% else %}
24
+ let h = gid.x;
25
+ if (h >= TABLE_SIZE) {
26
+ return;
27
+ }
28
+ atomicStore(&tableKey[h], EMPTY);
29
+ atomicStore(&tableIdx[h], EMPTY);
30
+ if (h == 0u) {
31
+ atomicStore(&special[0], EMPTY);
32
+ }
33
+ {% endif %}
34
+ }
build/webgpu/unique-hash-mark.wgsl.jinja ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Pass 3 of the hash-set parallel Unique (integer dtypes only). One thread per
4
+ // input element re-probes the (now fully built) hash table for its value and
5
+ // reads the stored minimum index. flags[i] = 1 iff i is that minimum, i.e. i is
6
+ // the first occurrence of its value — exactly the predicate the O(n^2) scan
7
+ // produced, but in O(n) probes. The slot is guaranteed to exist (this value was
8
+ // inserted in the build pass), so the probe always finds a matching key before
9
+ // hitting an EMPTY slot.
10
+ // Open-addressing table constants and key/hash helpers.
11
+ const EMPTY: u32 = 0xffffffffu;
12
+ const MASK: u32 = {{ source.tableSize }}u - 1u;
13
+
14
+ fn key_bits(v: {{ scalar }}) -> u32 {
15
+ {% if isUnsigned %}
16
+ return v;
17
+ {% else %}
18
+ return bitcast<u32>(v);
19
+ {% endif %}
20
+ }
21
+ fn hash_key(k: u32) -> u32 {
22
+ var x = k;
23
+ x = x ^ (x >> 16u);
24
+ x = x * 0x7feb352du;
25
+ x = x ^ (x >> 15u);
26
+ x = x * 0x846ca68bu;
27
+ x = x ^ (x >> 16u);
28
+ return x;
29
+ }
30
+
31
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
32
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
33
+ let i = gid.x;
34
+ if (i >= params.inputCount) {
35
+ return;
36
+ }
37
+ let k = key_bits(x[i]);
38
+ if (k == EMPTY) {
39
+ flags[i] = select(0u, 1u, atomicLoad(&special[0]) == i);
40
+ return;
41
+ }
42
+ var h = hash_key(k) & MASK;
43
+ loop {
44
+ let stored = atomicLoad(&tableKey[h]);
45
+ if (stored == k) {
46
+ flags[i] = select(0u, 1u, atomicLoad(&tableIdx[h]) == i);
47
+ return;
48
+ }
49
+ // stored == EMPTY would mean the value is absent, which is impossible here
50
+ // (it was inserted in the build pass); keep probing on any other key.
51
+ h = (h + 1u) & MASK;
52
+ }
53
+ }
build/webgpu/unique-hash-sort-collected-key-only.wgsl.jinja ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
4
+ const CAP: u32 = {{ source.capacity }}u;
5
+ const SORT_N: u32 = {{ source.sortN }}u;
6
+
7
+ // The monotonic key transform is invertible, so the workgroup sort only needs
8
+ // one u32 per slot. Padding uses the maximum key; if a real maximum key exists,
9
+ // it decodes to the same value, so no separate padding flag/value arrays are
10
+ // needed. SORT_N is specialized so this array fits the device's workgroup-
11
+ // storage limit.
12
+ var<workgroup> keys: array<u32, SORT_N>;
13
+
14
+ fn key_from_bits(bits: u32) -> u32 {
15
+ {% if isUnsigned %}
16
+ return bits;
17
+ {% else %}
18
+ return bits ^ 0x80000000u;
19
+ {% endif %}
20
+ }
21
+
22
+ fn bits_from_key(key: u32) -> u32 {
23
+ {% if isUnsigned %}
24
+ return key;
25
+ {% else %}
26
+ return key ^ 0x80000000u;
27
+ {% endif %}
28
+ }
29
+
30
+ fn zero_value() -> {{ scalar }} {
31
+ return {{ scalar }}(0);
32
+ }
33
+
34
+ @compute @workgroup_size(WG)
35
+ fn main(@builtin(local_invocation_id) local: vec3<u32>) {
36
+ let tid = local.x;
37
+ let written = min(count[0], CAP);
38
+ for (var index = tid; index < SORT_N; index = index + WG) {
39
+ var key = 0xffffffffu;
40
+ if (index < written) {
41
+ key = key_from_bits(compactedBits[index]);
42
+ }
43
+ keys[index] = key;
44
+ }
45
+ workgroupBarrier();
46
+
47
+ var size = 2u;
48
+ loop {
49
+ if (size > SORT_N) {
50
+ break;
51
+ }
52
+ var stride = size / 2u;
53
+ loop {
54
+ if (stride == 0u) {
55
+ break;
56
+ }
57
+ for (var index = tid; index < SORT_N; index = index + WG) {
58
+ let partner = index ^ stride;
59
+ if (partner > index) {
60
+ let a = keys[index];
61
+ let b = keys[partner];
62
+ let ascending = (index & size) == 0u;
63
+ let swap = select((a < b), (a > b), ascending);
64
+ if (swap) {
65
+ keys[index] = b;
66
+ keys[partner] = a;
67
+ }
68
+ }
69
+ }
70
+ workgroupBarrier();
71
+ stride = stride / 2u;
72
+ }
73
+ size = size * 2u;
74
+ }
75
+
76
+ for (var index = tid; index < CAP; index = index + WG) {
77
+ if (index < written) {
78
+ y[index] = bitcast<{{ scalar }}>(bits_from_key(keys[index]));
79
+ } else {
80
+ y[index] = zero_value();
81
+ }
82
+ }
83
+ }
build/webgpu/unique.wgsl.jinja ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // IEEE total-order comparators for Unique. Floating-point equality and ordering
2
+ // use raw bits because GPUs may flush subnormals in float comparisons, which
3
+ // would otherwise collapse distinct values. Equality canonicalizes -0 to +0;
4
+ // ordering uses a monotonic u32 key over the real line. Integers use == and <.
5
+ {% macro float_bits_def() %}
6
+ fn float_bits(v: {{ scalar }}) -> u32 {
7
+ {% if usesF16 %}
8
+ // WGSL has no scalar u16 type. Packing v into the low component preserves
9
+ // its binary16 representation while producing a bitcast-compatible 32 bits.
10
+ return bitcast<u32>(vec2<f16>(v, 0.0h)) & 0xffffu;
11
+ {% else %}
12
+ return bitcast<u32>(v);
13
+ {% endif %}
14
+ }
15
+ {%- endmacro -%}
16
+ {% macro eq_value_def() %}
17
+ fn eq_value(a: {{ scalar }}, b: {{ scalar }}) -> bool {
18
+ {% if isFloat %}
19
+ var ba = float_bits(a);
20
+ var bb = float_bits(b);
21
+ {% if usesF16 %}
22
+ if (ba == 0x8000u) { ba = 0u; } // -0 -> +0
23
+ if (bb == 0x8000u) { bb = 0u; }
24
+ {% else %}
25
+ if (ba == 0x80000000u) { ba = 0u; } // -0 -> +0
26
+ if (bb == 0x80000000u) { bb = 0u; }
27
+ {% endif %}
28
+ return ba == bb;
29
+ {% else %}
30
+ return a == b;
31
+ {% endif %}
32
+ }
33
+ {%- endmacro -%}
34
+ {%- macro less_value_def() %}
35
+ fn less_value(a: {{ scalar }}, b: {{ scalar }}) -> bool {
36
+ {% if isFloat %}
37
+ let ba = float_bits(a);
38
+ let bb = float_bits(b);
39
+ {% if usesF16 %}
40
+ let ka = select(ba | 0x8000u, (~ba) & 0xffffu, (ba & 0x8000u) != 0u);
41
+ let kb = select(bb | 0x8000u, (~bb) & 0xffffu, (bb & 0x8000u) != 0u);
42
+ {% else %}
43
+ let ka = select(ba | 0x80000000u, ~ba, (ba & 0x80000000u) != 0u);
44
+ let kb = select(bb | 0x80000000u, ~bb, (bb & 0x80000000u) != 0u);
45
+ {% endif %}
46
+ return ka < kb;
47
+ {% else %}
48
+ return a < b;
49
+ {% endif %}
50
+ }
51
+ {%- endmacro -%}
52
+ {%- macro zero_value_def() %}
53
+ fn zero_value() -> {{ scalar }} {
54
+ return {{ scalar }}(0);
55
+ }
56
+ {%- endmacro -%}
57
+ {%- macro is_nan_bits_def() %}
58
+ fn is_nan_bits(v: {{ scalar }}) -> bool {
59
+ let b = float_bits(v);
60
+ {% if usesF16 %}
61
+ return (b & 0x7c00u) == 0x7c00u && (b & 0x03ffu) != 0u;
62
+ {% else %}
63
+ return (b & 0x7f800000u) == 0x7f800000u && (b & 0x007fffffu) != 0u;
64
+ {% endif %}
65
+ }
66
+ {%- endmacro %}
67
+
68
+ {% set emitIndices = source.hasIndices | default(false) %}
69
+ {% set emitInverseIndices = source.hasInverseIndices | default(false) %}
70
+ {% set emitCounts = source.hasCounts | default(false) %}
71
+ {% if usesF16 %}
72
+ enable f16;
73
+ {% endif %}
74
+ {{ env.wgsl.resourceDeclarations }}
75
+
76
+ // Floating-point equality/order use the raw IEEE bit pattern, not the float
77
+ // ALU: GPUs may flush subnormals to zero in comparisons and collapse distinct
78
+ // values. Equality canonicalizes -0 to +0; ordering uses an IEEE total-order
79
+ // key. Integers use ==/< with their native signedness.
80
+ {% if isFloat %}
81
+ {{ float_bits_def() }}
82
+
83
+ {% endif %}
84
+ {{ eq_value_def() }}
85
+
86
+ {{ less_value_def() }}
87
+
88
+ {{ zero_value_def() }}
89
+ {% if isFloat %}
90
+
91
+ {{ is_nan_bits_def() }}
92
+
93
+ {% endif %}
94
+ // NaN is unequal to every value, but neither comparison is less, so ordered
95
+ // insertion treats it as equivalent to the current candidate. The candidate
96
+ // therefore depends on insertion history.
97
+ fn ordered_less(a: {{ scalar }}, b: {{ scalar }}) -> bool {
98
+ {% if isFloat %}
99
+ if (is_nan_bits(a) || is_nan_bits(b)) { return false; }
100
+ {% endif %}
101
+ if (eq_value(a, b)) { return false; }
102
+ return less_value(a, b);
103
+ }
104
+
105
+ {% if emitIndices or emitInverseIndices or emitCounts %}
106
+ fn representative_input_index(output_i: u32) -> u32 {
107
+ let representative = y[output_i];
108
+ for (var input_i = 0u; input_i < params.inputCount; input_i = input_i + 1u) {
109
+ if (eq_value(x[input_i], representative)) { return input_i; }
110
+ }
111
+ return params.inputCount;
112
+ }
113
+ {% endif %}
114
+
115
+ {% if emitInverseIndices or emitCounts %}
116
+ // Resolve the bucket selected when input_i was inserted. A representative that
117
+ // first appeared later did not exist at that point and cannot change the
118
+ // stateful lower_bound result.
119
+ fn find_bucket(input_i: u32, written: u32) -> u32 {
120
+ let value = x[input_i];
121
+ var candidate = written;
122
+ for (var output_i = 0u; output_i < written; output_i = output_i + 1u) {
123
+ if (representative_input_index(output_i) > input_i || ordered_less(y[output_i], value)) { continue; }
124
+ if (candidate == written || ordered_less(y[output_i], y[candidate])) {
125
+ candidate = output_i;
126
+ }
127
+ }
128
+ if (candidate < written && !ordered_less(value, y[candidate])) {
129
+ return candidate;
130
+ }
131
+ return 0u;
132
+ }
133
+
134
+ {% endif %}
135
+ @compute @workgroup_size(1)
136
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
137
+ if (gid.x != 0u) { return; }
138
+ var written = 0u;
139
+ for (var i = 0u; i < params.inputCount; i = i + 1u) {
140
+ let value = x[i];
141
+ var candidate = written;
142
+ for (var output_i = 0u; output_i < written; output_i = output_i + 1u) {
143
+ if (!ordered_less(y[output_i], value)) {
144
+ if (candidate == written || ordered_less(y[output_i], y[candidate])) {
145
+ candidate = output_i;
146
+ }
147
+ }
148
+ }
149
+ let seen = candidate < written && !ordered_less(value, y[candidate]);
150
+ if (!seen) {
151
+ if (written < params.capacity) {
152
+ y[written] = value;
153
+ written = written + 1u;
154
+ }
155
+ }
156
+ }
157
+ {% if sorted %}
158
+ for (var i = 0u; i < written; i = i + 1u) {
159
+ for (var j = i + 1u; j < written; j = j + 1u) {
160
+ if (ordered_less(y[j], y[i])) {
161
+ let tmp = y[i];
162
+ y[i] = y[j];
163
+ y[j] = tmp;
164
+ }
165
+ }
166
+ }
167
+ {% endif %}
168
+ for (var i = written; i < params.capacity; i = i + 1u) {
169
+ y[i] = zero_value();
170
+ }
171
+ {% if emitIndices %}
172
+ for (var unique_i = 0u; unique_i < params.capacity; unique_i = unique_i + 1u) {
173
+ indices[unique_i] = representative_input_index(unique_i);
174
+ }
175
+ {% endif %}
176
+ {% if emitCounts %}
177
+ for (var unique_i = 0u; unique_i < params.capacity; unique_i = unique_i + 1u) {
178
+ counts[unique_i] = 0u;
179
+ }
180
+ {% endif %}
181
+ {% if emitInverseIndices or emitCounts %}
182
+ // Resolve metadata against final Y order while retaining the prefix state
183
+ // that selected each NaN-equivalent bucket.
184
+ for (var input_i = 0u; input_i < params.inputCount; input_i = input_i + 1u) {
185
+ let unique_i = find_bucket(input_i, written);
186
+ {% if emitInverseIndices %}
187
+ inverse_indices[input_i] = unique_i;
188
+ {% endif %}
189
+ {% if emitCounts %}
190
+ counts[unique_i] = counts[unique_i] + 1u;
191
+ {% endif %}
192
+ }
193
+ {% endif %}
194
+ }