Xenova HF Staff commited on
Commit
9337f34
·
verified ·
1 Parent(s): 5d5e27b

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,82 @@
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.ScatterND
10
+
11
+ `ai.onnx` · standard ONNX operator · ONNX opset ≥ 18
12
+
13
+ ## Description
14
+
15
+ Copies `data` and then scatters values from `updates` into the copy at positions specified by `indices`, producing an output with the same shape as `data`. Each entry in `indices` is a k-tuple that addresses either a single element (when k equals the rank of `data`) or a slice; an optional `reduction` attribute replaces the default overwrite with an elementwise `add`, `mul`, `max`, or `min` accumulation.
16
+
17
+ See the [ONNX `ScatterND` spec](https://onnx.ai/onnx/operators/onnx__ScatterND.html) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `data` | `data` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | Source tensor of rank r >= 1 that is copied to form the base of the output. | required |
24
+ | `indices` | `indices` | `I` | `int32` | — | — | Logical int64 index tensor of rank q >= 1 whose last dimension gives signed k-tuples that address into `data`; WebGPU stores it as int32. | required |
25
+ | `updates` | `updates` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | Values to scatter into the output, of rank q + r - k - 1, matching the leading (q-1) index dimensions and the trailing (r-k) data slice dimensions. | required |
26
+
27
+ ## Outputs
28
+
29
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
30
+ | --- | --- | --- | --- | --- | --- | --- |
31
+ | `output` | `output` | `T` | same as `data` | same as `data` | Result tensor of rank r with the same shape as `data`, with selected positions overwritten or reduced by `updates`. | required |
32
+
33
+ ## Attributes
34
+
35
+ Default values (overridable per request):
36
+
37
+ | Attribute | Default | Description |
38
+ | --- | --- | --- |
39
+ | `reduction` | `"none"` | Reduction to apply when writing each update value: `none` (overwrite), `add`, `mul`, `max`, or `min`. |
40
+
41
+ ## Type constraints
42
+
43
+ | Variable | Allowed dtypes |
44
+ | --- | --- |
45
+ | `T` | `float32`, `float16`, `int32`, `uint32`, `int8`, `uint8`, `int16`, `bool` |
46
+ | `I` | `int64` |
47
+
48
+ ## Files
49
+
50
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
51
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
52
+ - [`test.json`](build/webgpu/test.json) — correctness cases
53
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
54
+ - [`datamove-scatter-row.wgsl.jinja`](build/webgpu/datamove-scatter-row.wgsl.jinja)
55
+ - [`scatter-f16-f32-convert.wgsl.jinja`](build/webgpu/scatter-f16-f32-convert.wgsl.jinja)
56
+ - [`scatter-flat-copy.wgsl.jinja`](build/webgpu/scatter-flat-copy.wgsl.jinja)
57
+ - [`scatter-narrow-wrap.wgsl.jinja`](build/webgpu/scatter-narrow-wrap.wgsl.jinja)
58
+ - [`scatter-nd-reduction-atomic.wgsl.jinja`](build/webgpu/scatter-nd-reduction-atomic.wgsl.jinja)
59
+ - [`scatter-nd-reduction-f16-packed-atomic.wgsl.jinja`](build/webgpu/scatter-nd-reduction-f16-packed-atomic.wgsl.jinja)
60
+ - [`scatter-nd-reduction-slice.wgsl.jinja`](build/webgpu/scatter-nd-reduction-slice.wgsl.jinja)
61
+ - [`scatter-nd.wgsl.jinja`](build/webgpu/scatter-nd.wgsl.jinja)
62
+ - [`scatter-scalar-add.wgsl.jinja`](build/webgpu/scatter-scalar-add.wgsl.jinja)
63
+
64
+ ## Use with `@huggingface/kernels`
65
+
66
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
67
+ It then allocates the result tensors automatically.
68
+
69
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
70
+
71
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
72
+
73
+ ```js
74
+ import { getKernel } from "@huggingface/kernels";
75
+
76
+ const kernel = await getKernel("webgpu-kernels/ai.onnx.ScatterND", { version: 1 });
77
+ const { output } = await kernel({
78
+ data: { data: dataData, shape: [2, 2] },
79
+ indices: { data: indicesData, shape: [2, 1] },
80
+ updates: { data: updatesData, shape: [2, 2] },
81
+ });
82
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.ScatterND",
3
+ "cases": [
4
+ {
5
+ "name": "row_updates",
6
+ "inputs": {
7
+ "data": { "dtype": "float32", "shape": [4096, 768] },
8
+ "indices": { "dtype": "int32", "shape": [128, 1] },
9
+ "updates": { "dtype": "float32", "shape": [128, 768] }
10
+ },
11
+ "outputs": { "output": { "dtype": "float32", "shape": [4096, 768] } }
12
+ },
13
+ {
14
+ "name": "row_updates_reduction_add",
15
+ "preset": "smoke",
16
+ "attrs": { "reduction": "add" },
17
+ "inputs": {
18
+ "data": { "dtype": "float32", "shape": [4096, 768], "dist": "normal", "seed": 1 },
19
+ "indices": { "dtype": "int32", "shape": [512, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
20
+ "updates": { "dtype": "float32", "shape": [512, 768], "dist": "normal", "seed": 2 }
21
+ },
22
+ "outputs": { "output": { "dtype": "float32", "shape": [4096, 768], "dist": "empty" } },
23
+ "bench": {
24
+ "primary": true,
25
+ "metrics": [
26
+ {
27
+ "type": "bandwidth",
28
+ "value": "4 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ {
34
+ "name": "row_updates_reduction_max",
35
+ "preset": "smoke",
36
+ "attrs": { "reduction": "max" },
37
+ "inputs": {
38
+ "data": { "dtype": "float32", "shape": [4096, 768], "dist": "normal", "seed": 1 },
39
+ "indices": { "dtype": "int32", "shape": [512, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
40
+ "updates": { "dtype": "float32", "shape": [512, 768], "dist": "normal", "seed": 2 }
41
+ },
42
+ "outputs": { "output": { "dtype": "float32", "shape": [4096, 768], "dist": "empty" } }
43
+ },
44
+ {
45
+ "name": "scalar_updates_reduction_add_contended_1m",
46
+ "preset": "stress",
47
+ "attrs": { "reduction": "add" },
48
+ "inputs": {
49
+ "data": { "dtype": "float32", "shape": [1024], "dist": "normal", "seed": 1 },
50
+ "indices": { "dtype": "int32", "shape": [1048576, 1], "dist": "randint", "min": 0, "max": 1024, "seed": 3 },
51
+ "updates": { "dtype": "float32", "shape": [1048576], "dist": "normal", "seed": 2 }
52
+ },
53
+ "outputs": { "output": { "dtype": "float32", "shape": [1024], "dist": "empty" } }
54
+ },
55
+ {
56
+ "name": "scalar_updates_reduction_add_single_slot_1m",
57
+ "preset": "stress",
58
+ "attrs": { "reduction": "add" },
59
+ "inputs": {
60
+ "data": { "dtype": "float32", "shape": [1024], "dist": "normal", "seed": 1 },
61
+ "indices": { "dtype": "int32", "shape": [1048576, 1], "dist": "constant", "value": 0 },
62
+ "updates": { "dtype": "float32", "shape": [1048576], "dist": "normal", "seed": 2 }
63
+ },
64
+ "outputs": { "output": { "dtype": "float32", "shape": [1024], "dist": "empty" } }
65
+ },
66
+ {
67
+ "name": "row_updates_dense_8192x1024",
68
+ "preset": "model",
69
+ "inputs": {
70
+ "data": { "dtype": "float32", "shape": [8192, 1024], "dist": "normal", "seed": 701 },
71
+ "indices": { "dtype": "int32", "shape": [8192, 1], "dist": "linearMod", "mod": 8192, "step": 7 },
72
+ "updates": { "dtype": "float32", "shape": [8192, 1024], "dist": "normal", "seed": 703 }
73
+ },
74
+ "outputs": { "output": { "dtype": "float32", "shape": [8192, 1024], "dist": "empty" } },
75
+ "bench": {
76
+ "primary": false,
77
+ "metrics": [
78
+ {
79
+ "type": "bandwidth",
80
+ "value": "4 * (2 * numel(shapes.data) + numel(shapes.updates)) + 4 * numel(shapes.indices)"
81
+ }
82
+ ]
83
+ }
84
+ },
85
+ {
86
+ "name": "f16_reduction_add_single_lane_serial_cliff",
87
+ "preset": "smoke",
88
+ "attrs": { "reduction": "add" },
89
+ "inputs": {
90
+ "data": { "dtype": "float16", "shape": [4096, 768], "dist": "normal", "seed": 1 },
91
+ "indices": { "dtype": "int32", "shape": [512, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
92
+ "updates": { "dtype": "float16", "shape": [512, 768], "dist": "normal", "seed": 2 }
93
+ },
94
+ "outputs": { "output": { "dtype": "float16", "shape": [4096, 768], "dist": "empty" } },
95
+ "bench": {
96
+ "metrics": [
97
+ {
98
+ "type": "bandwidth",
99
+ "value": "2 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
100
+ }
101
+ ]
102
+ }
103
+ },
104
+ {
105
+ "name": "f32_reduction_add_atomic_parallel_healthy",
106
+ "preset": "smoke",
107
+ "attrs": { "reduction": "add" },
108
+ "inputs": {
109
+ "data": { "dtype": "float32", "shape": [4096, 768], "dist": "normal", "seed": 1 },
110
+ "indices": { "dtype": "int32", "shape": [512, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
111
+ "updates": { "dtype": "float32", "shape": [512, 768], "dist": "normal", "seed": 2 }
112
+ },
113
+ "outputs": { "output": { "dtype": "float32", "shape": [4096, 768], "dist": "empty" } },
114
+ "bench": {
115
+ "metrics": [
116
+ {
117
+ "type": "bandwidth",
118
+ "value": "4 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
119
+ }
120
+ ]
121
+ }
122
+ },
123
+ {
124
+ "name": "int8_reduction_add_atomic_path_healthy",
125
+ "preset": "smoke",
126
+ "attrs": { "reduction": "add" },
127
+ "inputs": {
128
+ "data": { "dtype": "int8", "shape": [1024], "dist": "constant", "value": 0 },
129
+ "indices": { "dtype": "int32", "shape": [262144, 1], "dist": "randint", "min": 0, "max": 1024, "seed": 3 },
130
+ "updates": { "dtype": "int8", "shape": [262144], "dist": "constant", "value": 1 }
131
+ },
132
+ "outputs": { "output": { "dtype": "int8", "shape": [1024], "dist": "empty" } },
133
+ "bench": {
134
+ "metrics": [
135
+ { "type": "bandwidth", "value": "numel(shapes.data) + 3 * numel(shapes.updates) + 4 * numel(shapes.indices)" }
136
+ ]
137
+ }
138
+ },
139
+ {
140
+ "name": "f16_row_update_none_unaligned770_batch0_cliff",
141
+ "preset": "smoke",
142
+ "inputs": {
143
+ "data": { "dtype": "float16", "shape": [4096, 770], "dist": "normal", "seed": 11 },
144
+ "indices": { "dtype": "int32", "shape": [128, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
145
+ "updates": { "dtype": "float16", "shape": [128, 770], "dist": "normal", "seed": 12 }
146
+ },
147
+ "outputs": { "output": { "dtype": "float16", "shape": [4096, 770], "dist": "empty" } },
148
+ "bench": {
149
+ "metrics": [
150
+ {
151
+ "type": "bandwidth",
152
+ "value": "2 * (2 * numel(shapes.data) + numel(shapes.updates)) + 4 * numel(shapes.indices)"
153
+ }
154
+ ]
155
+ }
156
+ },
157
+ {
158
+ "name": "f16_row_update_none_aligned768_rowfast_healthy",
159
+ "preset": "smoke",
160
+ "inputs": {
161
+ "data": { "dtype": "float16", "shape": [4096, 768], "dist": "normal", "seed": 11 },
162
+ "indices": { "dtype": "int32", "shape": [128, 1], "dist": "linearMod", "mod": 4096, "step": 9 },
163
+ "updates": { "dtype": "float16", "shape": [128, 768], "dist": "normal", "seed": 12 }
164
+ },
165
+ "outputs": { "output": { "dtype": "float16", "shape": [4096, 768], "dist": "empty" } },
166
+ "bench": {
167
+ "metrics": [
168
+ {
169
+ "type": "bandwidth",
170
+ "value": "2 * (2 * numel(shapes.data) + numel(shapes.updates)) + 4 * numel(shapes.indices)"
171
+ }
172
+ ]
173
+ }
174
+ },
175
+ {
176
+ "name": "f16_reduction_add_scalar_updates_rank1_single_thread",
177
+ "preset": "stress",
178
+ "attrs": { "reduction": "add" },
179
+ "inputs": {
180
+ "data": { "dtype": "float16", "shape": [16384], "dist": "normal", "seed": 20 },
181
+ "indices": { "dtype": "int32", "shape": [16384, 1], "dist": "randint", "min": 0, "max": 16384, "seed": 21 },
182
+ "updates": { "dtype": "float16", "shape": [16384], "dist": "normal", "seed": 22 }
183
+ },
184
+ "outputs": { "output": { "dtype": "float16", "shape": [16384], "dist": "empty" } },
185
+ "bench": {
186
+ "metrics": [
187
+ {
188
+ "type": "bandwidth",
189
+ "value": "2 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
190
+ }
191
+ ]
192
+ }
193
+ },
194
+ {
195
+ "name": "f32_sparse_copy_dominates_tiny_scatter",
196
+ "preset": "stress",
197
+ "inputs": {
198
+ "data": { "dtype": "float32", "shape": [307200, 13], "dist": "normal", "seed": 30 },
199
+ "indices": { "dtype": "int32", "shape": [8, 1], "dist": "randint", "min": 0, "max": 307200, "seed": 31 },
200
+ "updates": { "dtype": "float32", "shape": [8, 13], "dist": "normal", "seed": 32 }
201
+ },
202
+ "outputs": { "output": { "dtype": "float32", "shape": [307200, 13], "dist": "empty" } },
203
+ "bench": {
204
+ "metrics": [
205
+ {
206
+ "type": "bandwidth",
207
+ "value": "4 * (2 * numel(shapes.data) + numel(shapes.updates)) + 4 * numel(shapes.indices)"
208
+ }
209
+ ]
210
+ }
211
+ },
212
+ {
213
+ "name": "scalar-loss-accumulation-add-4m-single-output-pathology",
214
+ "preset": "model",
215
+ "attrs": { "reduction": "add" },
216
+ "vars": { "count": 4194304, "dtype": "float32" },
217
+ "inputs": {
218
+ "data": { "dtype": "float32", "shape": [1], "dist": "constant", "value": 0 },
219
+ "indices": { "dtype": "int32", "shape": [4194304, 1], "dist": "constant", "value": 0 },
220
+ "updates": { "dtype": "float32", "shape": [4194304], "dist": "normal", "seed": 33, "scale": 0.001 }
221
+ },
222
+ "outputs": { "output": { "dtype": "float32", "shape": [1], "dist": "empty" } },
223
+ "bench": {
224
+ "metrics": [
225
+ { "type": "bandwidth", "value": "args.count * (dtypeBytes(args.dtype) + 4) + 2 * dtypeBytes(args.dtype)" }
226
+ ]
227
+ }
228
+ },
229
+ {
230
+ "name": "padding-token-hotspot-add-2m-vocab131072",
231
+ "preset": "stress",
232
+ "attrs": { "reduction": "add" },
233
+ "inputs": {
234
+ "data": { "dtype": "float32", "shape": [131072], "dist": "constant", "value": 0 },
235
+ "indices": { "dtype": "int32", "shape": [2097152, 1], "dist": "constant", "value": 0 },
236
+ "updates": { "dtype": "float32", "shape": [2097152], "dist": "constant", "value": 1 }
237
+ },
238
+ "outputs": { "output": { "dtype": "float32", "shape": [131072], "dist": "empty" } },
239
+ "bench": {
240
+ "primary": true,
241
+ "metrics": [
242
+ {
243
+ "type": "bandwidth",
244
+ "value": "4 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
245
+ }
246
+ ]
247
+ }
248
+ },
249
+ {
250
+ "name": "f16-sparse-router-gradient-65536-into16384",
251
+ "preset": "stress",
252
+ "attrs": { "reduction": "add" },
253
+ "inputs": {
254
+ "data": { "dtype": "float16", "shape": [16384], "dist": "constant", "value": 0 },
255
+ "indices": { "dtype": "int32", "shape": [65536, 1], "dist": "randint", "min": 0, "max": 16384, "seed": 41 },
256
+ "updates": { "dtype": "float16", "shape": [65536], "dist": "normal", "seed": 42, "scale": 0.01 }
257
+ },
258
+ "outputs": { "output": { "dtype": "float16", "shape": [16384], "dist": "empty" } },
259
+ "bench": {
260
+ "primary": true,
261
+ "metrics": [
262
+ {
263
+ "type": "bandwidth",
264
+ "value": "2 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
265
+ }
266
+ ]
267
+ }
268
+ },
269
+ {
270
+ "name": "f16-router-max-262144-into16384-single-column",
271
+ "preset": "stress",
272
+ "provenance": {
273
+ "notes": "Large f16 max reduction that exercises the serialized reduction_slice route; the atomic-add control distinguishes the non-add reduction gap."
274
+ },
275
+ "attrs": { "reduction": "max" },
276
+ "inputs": {
277
+ "data": { "dtype": "float16", "shape": [16384], "dist": "constant", "value": 0 },
278
+ "indices": { "dtype": "int32", "shape": [262144, 1], "dist": "randint", "min": 0, "max": 16384, "seed": 43 },
279
+ "updates": { "dtype": "float16", "shape": [262144], "dist": "normal", "seed": 44 }
280
+ },
281
+ "outputs": { "output": { "dtype": "float16", "shape": [16384], "dist": "empty" } },
282
+ "bench": {
283
+ "metrics": [
284
+ {
285
+ "type": "bandwidth",
286
+ "value": "2 * (2 * numel(shapes.data) + 3 * numel(shapes.updates)) + 4 * numel(shapes.indices)"
287
+ }
288
+ ]
289
+ }
290
+ }
291
+ ]
292
+ }
build/webgpu/datamove-scatter-row.wgsl.jinja ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // One workgroup per update row computes its normalized output offset and
2
+ // vec4-copies the contiguous inner slice from updates[row] into
3
+ // output[scatter_index]. This path handles indexDepth == 1 (scatter along
4
+ // data axis 0) with an inner size divisible by four. It performs no reduction;
5
+ // duplicate indices are last-write-wins, matching ONNX's nondeterministic
6
+ // duplicate-index semantics.
7
+ {% if usesF16 %}
8
+ enable f16;
9
+ {% endif %}
10
+ {{ env.wgsl.resourceDeclarations }}
11
+
12
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
13
+ const NUM_INDICES: u32 = {{ source.numIndices }}u;
14
+ const AXIS_DIM: u32 = {{ source.axisDim }}u;
15
+ const ROW_VECS: u32 = {{ source.rowVecs }}u;
16
+ const INVALID_INDEX: u32 = 0xffffffffu;
17
+
18
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
19
+ let dim_i = i32(dim);
20
+ var index = raw;
21
+ if (index < 0) {
22
+ index = index + dim_i;
23
+ }
24
+ if (index < 0 || index >= dim_i) {
25
+ return INVALID_INDEX;
26
+ }
27
+ return u32(index);
28
+ }
29
+
30
+
31
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
32
+ fn main(
33
+ @builtin(workgroup_id) wid: vec3<u32>,
34
+ @builtin(num_workgroups) nwg: vec3<u32>,
35
+ @builtin(local_invocation_id) lid: vec3<u32>
36
+ ) {
37
+ for (var row = wid.x; row < NUM_INDICES; row += nwg.x) {
38
+ let scatter_index = normalize_index(indices[row], AXIS_DIM);
39
+ // Out-of-range index: skip the row (ONNX leaves it to the copied `data`).
40
+ // The continue is uniform across the workgroup (all threads share `row`).
41
+ if (scatter_index == INVALID_INDEX) {
42
+ continue;
43
+ }
44
+ let src_base = row * ROW_VECS;
45
+ let dst_base = scatter_index * ROW_VECS;
46
+ for (var j = lid.x; j < ROW_VECS; j += WG) {
47
+ output[dst_base + j] = updates[src_base + j];
48
+ }
49
+ }
50
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "ai.onnx",
3
+ "name": "ScatterND",
4
+ "sinceVersion": 18,
5
+ "description": "Copies `data` and then scatters values from `updates` into the copy at positions specified by `indices`, producing an output with the same shape as `data`. Each entry in `indices` is a k-tuple that addresses either a single element (when k equals the rank of `data`) or a slice; an optional `reduction` attribute replaces the default overwrite with an elementwise `add`, `mul`, `max`, or `min` accumulation.",
6
+ "inputs": [
7
+ {
8
+ "role": "data",
9
+ "dtype": "T",
10
+ "description": "Source tensor of rank r >= 1 that is copied to form the base of the output."
11
+ },
12
+ {
13
+ "role": "indices",
14
+ "dtype": "I",
15
+ "description": "Logical int64 index tensor of rank q >= 1 whose last dimension gives signed k-tuples that address into `data`; WebGPU stores it as int32."
16
+ },
17
+ {
18
+ "role": "updates",
19
+ "dtype": "T",
20
+ "description": "Values to scatter into the output, of rank q + r - k - 1, matching the leading (q-1) index dimensions and the trailing (r-k) data slice dimensions."
21
+ }
22
+ ],
23
+ "outputs": [
24
+ {
25
+ "role": "output",
26
+ "dtype": "T",
27
+ "rank": "ranks.data",
28
+ "description": "Result tensor of rank r with the same shape as `data`, with selected positions overwritten or reduced by `updates`.",
29
+ "shape": "shapes.data"
30
+ }
31
+ ],
32
+ "attributes": { "reduction": "none" },
33
+ "attributeDescriptions": {
34
+ "reduction": "Reduction to apply when writing each update value: `none` (overwrite), `add`, `mul`, `max`, or `min`."
35
+ },
36
+ "attributeConstraints": { "reduction": { "values": ["none", "add", "mul", "max", "min"] } },
37
+ "typeConstraints": {
38
+ "T": ["float32", "float16", "int32", "uint32", "int8", "uint8", "int16", "bool"],
39
+ "I": ["int64"]
40
+ },
41
+ "args": {
42
+ "data": { "kind": "tensor", "semantic": "data", "role": "input" },
43
+ "indices": { "kind": "tensor", "semantic": "indices", "role": "indices", "dtype": "int32", "narrowing": "checked" },
44
+ "updates": { "kind": "tensor", "semantic": "updates", "role": "updates" },
45
+ "output": { "kind": "tensor", "semantic": "output", "role": "output" }
46
+ },
47
+ "tunables": {
48
+ "WORKGROUP_SIZE": 256,
49
+ "F16_ATOMIC_MIN_UPDATES": 4096,
50
+ "SCALAR_ADD_MIN_UPDATES": 256,
51
+ "PREAGG_ITEMS_PER_THREAD": 8,
52
+ "PREAGG_MIN_DUPLICATION": 8,
53
+ "PREAGG_SUBGROUP_ROUNDS": 2,
54
+ "SCALAR_ADD_ITEMS_PER_THREAD": 16
55
+ },
56
+ "derive": {
57
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
58
+ "foldedDispatchCapacity": "device.limits.maxComputeWorkgroupsPerDimension * device.limits.maxComputeWorkgroupsPerDimension",
59
+ "indexDepth": "dim(shapes.indices, ranks.indices - 1)",
60
+ "narrowIntData": "tensorDtypes.data == \"int8\" or tensorDtypes.data == \"uint8\" or tensorDtypes.data == \"int16\"",
61
+ "validLayout": "(dtypes.I == \"u32\" or dtypes.I == \"i32\") and ranks.data == ranks.output and ranks.data >= 1 and ranks.indices >= 2 and indexDepth <= ranks.data and ranks.updates == ranks.indices - 1 + ranks.data - indexDepth and numel(shapes.data) == numel(shapes.output)",
62
+ "workgroupLimit": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
63
+ "workgroupSize": "max(1, pow2ceil(workgroupLimit + 1) / 2)",
64
+ "foldedThreadCapacity": "foldedDispatchCapacity * workgroupSize",
65
+ "updateDispatchFits": "numel(shapes.updates) <= foldedThreadCapacity",
66
+ "outputDispatchFits": "numel(shapes.output) <= foldedThreadCapacity",
67
+ "f16AccumBytes": "numel(shapes.output) * dtypeBytes(\"uint32\")",
68
+ "f16AccumFits": "f16AccumBytes <= device.limits.maxStorageBufferBindingSize and f16AccumBytes <= device.limits.maxBufferSize",
69
+ "preaggStorageFits": "workgroupSize * dtypeBytes(\"float32\") + 16 <= device.limits.maxComputeWorkgroupStorageSize",
70
+ "scalarAddStorageFits": "workgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize"
71
+ },
72
+ "bindingSets": {
73
+ "copy": [
74
+ {
75
+ "name": "src",
76
+ "arg": "data",
77
+ "semantic": "data",
78
+ "buffer": { "type": "read-only-storage" },
79
+ "elementType": "$copyElementType"
80
+ },
81
+ {
82
+ "name": "dst",
83
+ "arg": "output",
84
+ "semantic": "output",
85
+ "buffer": { "type": "storage" },
86
+ "elementType": "$copyElementType"
87
+ }
88
+ ],
89
+ "rowVec4": [
90
+ {
91
+ "name": "indices",
92
+ "arg": "indices",
93
+ "semantic": "indices",
94
+ "buffer": { "type": "read-only-storage" },
95
+ "elementType": "$indexScalar"
96
+ },
97
+ {
98
+ "name": "updates",
99
+ "arg": "updates",
100
+ "semantic": "updates",
101
+ "buffer": { "type": "read-only-storage" },
102
+ "elementType": "$vectorScalar"
103
+ },
104
+ {
105
+ "name": "output",
106
+ "arg": "output",
107
+ "semantic": "output",
108
+ "buffer": { "type": "storage" },
109
+ "elementType": "$vectorScalar"
110
+ }
111
+ ],
112
+ "scatter": [
113
+ {
114
+ "name": "indices",
115
+ "arg": "indices",
116
+ "semantic": "indices",
117
+ "buffer": { "type": "read-only-storage" },
118
+ "elementType": "$indexScalar"
119
+ },
120
+ {
121
+ "name": "updates",
122
+ "arg": "updates",
123
+ "semantic": "updates",
124
+ "buffer": { "type": "read-only-storage" },
125
+ "elementType": "$updateElement"
126
+ },
127
+ {
128
+ "name": "output",
129
+ "arg": "output",
130
+ "semantic": "output",
131
+ "buffer": { "type": "storage" },
132
+ "elementType": "$outputElement"
133
+ },
134
+ {
135
+ "name": "params",
136
+ "semantic": "kernel.params",
137
+ "buffer": { "type": "uniform" },
138
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.updates)" }] }
139
+ }
140
+ ],
141
+ "f16WideInit": [
142
+ {
143
+ "name": "narrow",
144
+ "arg": "data",
145
+ "semantic": "data",
146
+ "buffer": { "type": "read-only-storage" },
147
+ "elementType": "f16"
148
+ },
149
+ { "name": "wide", "semantic": "accum", "buffer": { "type": "storage" }, "elementType": "u32" },
150
+ {
151
+ "name": "params",
152
+ "semantic": "kernel.params",
153
+ "buffer": { "type": "uniform" },
154
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
155
+ }
156
+ ],
157
+ "f16AtomicAccumulate": [
158
+ {
159
+ "name": "indices",
160
+ "arg": "indices",
161
+ "semantic": "indices",
162
+ "buffer": { "type": "read-only-storage" },
163
+ "elementType": "$indexScalar"
164
+ },
165
+ {
166
+ "name": "updates",
167
+ "arg": "updates",
168
+ "semantic": "updates",
169
+ "buffer": { "type": "read-only-storage" },
170
+ "elementType": "f16"
171
+ },
172
+ { "name": "output", "semantic": "accum", "buffer": { "type": "storage" }, "elementType": "atomic<u32>" },
173
+ {
174
+ "name": "params",
175
+ "semantic": "kernel.params",
176
+ "buffer": { "type": "uniform" },
177
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.updates)" }] }
178
+ }
179
+ ],
180
+ "f16NarrowFinalize": [
181
+ { "name": "wide", "semantic": "accum", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
182
+ { "name": "narrow", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f16" },
183
+ {
184
+ "name": "params",
185
+ "semantic": "kernel.params",
186
+ "buffer": { "type": "uniform" },
187
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
188
+ }
189
+ ],
190
+ "narrowWrap": [
191
+ { "name": "values", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "u32" },
192
+ {
193
+ "name": "params",
194
+ "semantic": "kernel.params",
195
+ "buffer": { "type": "uniform" },
196
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
197
+ }
198
+ ]
199
+ },
200
+ "variants": [
201
+ {
202
+ "id": "reduction_f16_atomic_f32",
203
+ "priority": 35,
204
+ "when": ["(attrs.reduction == \"add\" or attrs.reduction == \"min\" or attrs.reduction == \"max\")", "dtypes.T == \"f16\"", "validLayout", "indexDepth == ranks.data", "numel(shapes.updates) >= tunables.F16_ATOMIC_MIN_UPDATES", "f16Ok(dtypes.T)", "f16AccumFits", "outputDispatchFits", "updateDispatchFits"],
205
+ "constants": {
206
+ "scalar": "\"f32\"",
207
+ "indexScalar": "dtypes.I",
208
+ "usesF16": true,
209
+ "reductionOp": "attrs.reduction",
210
+ "useSubgroups": "device.features.has(\"subgroups\") and attrs.reduction == \"add\"",
211
+ "useWorkgroupPreagg": false,
212
+ "preaggRounds": "tunables.PREAGG_SUBGROUP_ROUNDS",
213
+ "preaggItemsPerThread": "tunables.PREAGG_ITEMS_PER_THREAD"
214
+ },
215
+ "intermediates": [{ "id": "accum", "dtype": "uint32", "shape": "[numel(shapes.output)]" }],
216
+ "passes": [
217
+ {
218
+ "id": "init",
219
+ "name": "ScatterND.F16InitF32",
220
+ "source": { "shader": "scatter-f16-f32-convert.wgsl.jinja", "inputs": { "toWide": true } },
221
+ "bindings": "f16WideInit",
222
+ "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "workgroupSize" }
223
+ },
224
+ {
225
+ "id": "scatter",
226
+ "name": "ScatterND.F16AtomicF32",
227
+ "subgroupCollectivesWidth": "portable",
228
+ "source": {
229
+ "shader": "scatter-nd-reduction-atomic.wgsl.jinja",
230
+ "inputs": {
231
+ "dataShape": "shapes.data",
232
+ "indicesShape": "shapes.indices",
233
+ "updatesShape": "shapes.updates",
234
+ "dataRank": "ranks.data",
235
+ "indicesRank": "ranks.indices",
236
+ "updatesRank": "ranks.updates",
237
+ "indexDepth": "indexDepth"
238
+ }
239
+ },
240
+ "bindings": "f16AtomicAccumulate",
241
+ "dispatch": { "threads": "numel(shapes.updates)", "workgroupSize": "workgroupSize" }
242
+ },
243
+ {
244
+ "id": "finalize",
245
+ "name": "ScatterND.F16FinalizeF32",
246
+ "source": { "shader": "scatter-f16-f32-convert.wgsl.jinja", "inputs": { "toWide": false } },
247
+ "bindings": "f16NarrowFinalize",
248
+ "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "workgroupSize" }
249
+ }
250
+ ]
251
+ },
252
+ {
253
+ "id": "scalar_output_add_parallel_reduce",
254
+ "priority": 30,
255
+ "when": ["attrs.reduction == \"add\"", "validLayout", "dtypes.T == \"f32\"", "ranks.data == 1", "ranks.output == 1", "numel(shapes.data) == 1", "numel(shapes.output) == 1", "ranks.indices == 2", "dim(shapes.indices, 1) == 1", "ranks.updates == 1", "dim(shapes.updates, 0) == dim(shapes.indices, 0)", "numel(shapes.updates) >= tunables.SCALAR_ADD_MIN_UPDATES", "scalarAddStorageFits"],
256
+ "constants": {
257
+ "scalar": "dtypes.T",
258
+ "indexScalar": "dtypes.I",
259
+ "copyElementType": "dtypes.T",
260
+ "updateElement": "dtypes.T",
261
+ "outputElement": "\"atomic<u32>\""
262
+ },
263
+ "passes": [
264
+ {
265
+ "id": "copy",
266
+ "name": "ScatterND.Copy",
267
+ "source": { "shader": "scatter-flat-copy.wgsl.jinja", "inputs": { "count": "numel(shapes.output)" } },
268
+ "bindings": "copy",
269
+ "dispatch": { "gridStride": "numel(shapes.output)", "workgroupSize": "workgroupSize" }
270
+ },
271
+ {
272
+ "id": "reduce",
273
+ "name": "ScatterND.ScalarAddParallelReduce",
274
+ "shader": "scatter-scalar-add.wgsl.jinja",
275
+ "bindings": "scatter",
276
+ "dispatch": {
277
+ "x": "min(ceilDiv(numel(shapes.updates), workgroupSize * tunables.SCALAR_ADD_ITEMS_PER_THREAD), device.limits.maxComputeWorkgroupsPerDimension)"
278
+ }
279
+ }
280
+ ]
281
+ },
282
+ {
283
+ "id": "reduction_narrow_wrap_atomic",
284
+ "priority": 26,
285
+ "when": ["(attrs.reduction == \"add\" or attrs.reduction == \"mul\")", "validLayout", "narrowIntData", "updateDispatchFits", "outputDispatchFits"],
286
+ "constants": {
287
+ "scalar": "\"u32\"",
288
+ "indexScalar": "dtypes.I",
289
+ "usesF16": false,
290
+ "reductionOp": "attrs.reduction",
291
+ "useSubgroups": "device.features.has(\"subgroups\")",
292
+ "useWorkgroupPreagg": false,
293
+ "preaggRounds": "tunables.PREAGG_SUBGROUP_ROUNDS",
294
+ "preaggItemsPerThread": "tunables.PREAGG_ITEMS_PER_THREAD",
295
+ "workgroupSize": "workgroupSize",
296
+ "narrowBits": "16 if tensorDtypes.data == \"int16\" else 8",
297
+ "narrowSigned": "tensorDtypes.data == \"int8\" or tensorDtypes.data == \"int16\"",
298
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
299
+ "updateElement": "\"u32\"",
300
+ "outputElement": "\"atomic<u32>\""
301
+ },
302
+ "passes": [
303
+ {
304
+ "id": "copy",
305
+ "name": "ScatterND.Copy",
306
+ "source": {
307
+ "shader": "scatter-flat-copy.wgsl.jinja",
308
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
309
+ },
310
+ "bindings": "copy",
311
+ "dispatch": {
312
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
313
+ "workgroupSize": "workgroupSize"
314
+ }
315
+ },
316
+ {
317
+ "id": "scatter",
318
+ "name": "ScatterND.NarrowWrapAtomic",
319
+ "subgroupCollectivesWidth": "portable",
320
+ "source": {
321
+ "shader": "scatter-nd-reduction-atomic.wgsl.jinja",
322
+ "inputs": {
323
+ "dataShape": "shapes.data",
324
+ "indicesShape": "shapes.indices",
325
+ "updatesShape": "shapes.updates",
326
+ "dataRank": "ranks.data",
327
+ "indicesRank": "ranks.indices",
328
+ "updatesRank": "ranks.updates",
329
+ "indexDepth": "indexDepth"
330
+ }
331
+ },
332
+ "bindings": "scatter",
333
+ "dispatch": { "threads": "numel(shapes.updates)", "workgroupSize": "workgroupSize" }
334
+ },
335
+ {
336
+ "id": "narrow",
337
+ "name": "ScatterND.NarrowWrapFinalize",
338
+ "shader": "scatter-narrow-wrap.wgsl.jinja",
339
+ "bindings": "narrowWrap",
340
+ "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "workgroupSize" }
341
+ }
342
+ ]
343
+ },
344
+ {
345
+ "id": "reduction_f16_packed_atomic_add",
346
+ "priority": 36,
347
+ "when": ["attrs.reduction == \"add\"", "dtypes.T == \"f16\"", "validLayout", "indexDepth < ranks.data", "numel(shapes.output) % 2 == 0", "numel(shapes.updates) >= tunables.F16_ATOMIC_MIN_UPDATES", "f16Ok(dtypes.T)", "outputDispatchFits", "updateDispatchFits"],
348
+ "constants": {
349
+ "indexScalar": "dtypes.I",
350
+ "usesF16": true,
351
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
352
+ "workgroupSize": "workgroupSize",
353
+ "updateElement": "\"f16\"",
354
+ "outputElement": "\"atomic<u32>\""
355
+ },
356
+ "passes": [
357
+ {
358
+ "id": "copy",
359
+ "name": "ScatterND.Copy",
360
+ "source": {
361
+ "shader": "scatter-flat-copy.wgsl.jinja",
362
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
363
+ },
364
+ "bindings": "copy",
365
+ "dispatch": {
366
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
367
+ "workgroupSize": "workgroupSize"
368
+ }
369
+ },
370
+ {
371
+ "id": "scatter",
372
+ "name": "ScatterND.F16PackedAtomicAdd",
373
+ "source": {
374
+ "shader": "scatter-nd-reduction-f16-packed-atomic.wgsl.jinja",
375
+ "inputs": {
376
+ "dataShape": "shapes.data",
377
+ "indicesShape": "shapes.indices",
378
+ "updatesShape": "shapes.updates",
379
+ "dataRank": "ranks.data",
380
+ "indicesRank": "ranks.indices",
381
+ "updatesRank": "ranks.updates",
382
+ "indexDepth": "indexDepth"
383
+ }
384
+ },
385
+ "bindings": "scatter",
386
+ "dispatch": { "threads": "numel(shapes.updates)", "workgroupSize": "workgroupSize" }
387
+ }
388
+ ]
389
+ },
390
+ {
391
+ "id": "reduction_atomic",
392
+ "priority": 25,
393
+ "when": ["(attrs.reduction == \"add\" or attrs.reduction == \"mul\" or attrs.reduction == \"min\" or attrs.reduction == \"max\")", "validLayout", "(tensorDtypes.data == \"float32\" or tensorDtypes.data == \"int32\" or tensorDtypes.data == \"uint32\" or (narrowIntData and (attrs.reduction == \"min\" or attrs.reduction == \"max\")))", "updateDispatchFits"],
394
+ "constants": {
395
+ "scalar": "dtypes.T",
396
+ "indexScalar": "dtypes.I",
397
+ "usesF16": "dtypes.T == \"f16\"",
398
+ "reductionOp": "attrs.reduction",
399
+ "useSubgroups": "device.features.has(\"subgroups\") and (attrs.reduction == \"add\" or attrs.reduction == \"mul\")",
400
+ "useWorkgroupPreagg": "attrs.reduction == \"add\" and dtypes.T == \"f32\" and indexDepth == ranks.data and numel(shapes.updates) >= numel(shapes.output) * tunables.PREAGG_MIN_DUPLICATION and preaggStorageFits",
401
+ "preaggRounds": "tunables.PREAGG_SUBGROUP_ROUNDS",
402
+ "workgroupSize": "workgroupSize",
403
+ "preaggItemsPerThread": "tunables.PREAGG_ITEMS_PER_THREAD",
404
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
405
+ "updateElement": "dtypes.T",
406
+ "outputElement": "\"atomic<i32>\" if dtypes.T == \"i32\" else \"atomic<u32>\""
407
+ },
408
+ "passes": [
409
+ {
410
+ "id": "copy",
411
+ "name": "ScatterND.Copy",
412
+ "source": {
413
+ "shader": "scatter-flat-copy.wgsl.jinja",
414
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
415
+ },
416
+ "bindings": "copy",
417
+ "dispatch": {
418
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
419
+ "workgroupSize": "workgroupSize"
420
+ }
421
+ },
422
+ {
423
+ "id": "scatter",
424
+ "name": "ScatterND.ReductionAtomic",
425
+ "subgroupCollectivesWidth": "portable",
426
+ "source": {
427
+ "shader": "scatter-nd-reduction-atomic.wgsl.jinja",
428
+ "inputs": {
429
+ "dataShape": "shapes.data",
430
+ "indicesShape": "shapes.indices",
431
+ "updatesShape": "shapes.updates",
432
+ "dataRank": "ranks.data",
433
+ "indicesRank": "ranks.indices",
434
+ "updatesRank": "ranks.updates",
435
+ "indexDepth": "indexDepth"
436
+ }
437
+ },
438
+ "bindings": "scatter",
439
+ "dispatch": {
440
+ "threads": "ceilDiv(numel(shapes.updates), tunables.PREAGG_ITEMS_PER_THREAD) if attrs.reduction == \"add\" and dtypes.T == \"f32\" and indexDepth == ranks.data and numel(shapes.updates) >= numel(shapes.output) * tunables.PREAGG_MIN_DUPLICATION and preaggStorageFits else numel(shapes.updates)",
441
+ "workgroupSize": "workgroupSize"
442
+ }
443
+ }
444
+ ]
445
+ },
446
+ {
447
+ "id": "reduction_slice",
448
+ "priority": 21,
449
+ "supersededBy": ["scalar_output_add_parallel_reduce"],
450
+ "when": ["(attrs.reduction == \"add\" or attrs.reduction == \"mul\" or attrs.reduction == \"min\" or attrs.reduction == \"max\")", "validLayout", "f16Ok(dtypes.T)"],
451
+ "constants": {
452
+ "scalar": "dtypes.T",
453
+ "indexScalar": "dtypes.I",
454
+ "usesF16": "dtypes.T == \"f16\"",
455
+ "reductionOp": "attrs.reduction",
456
+ "narrowBits": "8 if tensorDtypes.data == \"int8\" or tensorDtypes.data == \"uint8\" else (16 if tensorDtypes.data == \"int16\" else 0)",
457
+ "narrowSigned": "tensorDtypes.data == \"int8\" or tensorDtypes.data == \"int16\"",
458
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
459
+ "workgroupSize": "workgroupSize",
460
+ "updateElement": "dtypes.T",
461
+ "outputElement": "dtypes.T"
462
+ },
463
+ "passes": [
464
+ {
465
+ "id": "copy",
466
+ "name": "ScatterND.Copy",
467
+ "source": {
468
+ "shader": "scatter-flat-copy.wgsl.jinja",
469
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
470
+ },
471
+ "bindings": "copy",
472
+ "dispatch": {
473
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
474
+ "workgroupSize": "workgroupSize"
475
+ }
476
+ },
477
+ {
478
+ "id": "scatter",
479
+ "name": "ScatterND.ReductionSlice",
480
+ "source": {
481
+ "shader": "scatter-nd-reduction-slice.wgsl.jinja",
482
+ "inputs": {
483
+ "dataShape": "shapes.data",
484
+ "indicesShape": "shapes.indices",
485
+ "updatesShape": "shapes.updates",
486
+ "dataRank": "ranks.data",
487
+ "indicesRank": "ranks.indices",
488
+ "updatesRank": "ranks.updates",
489
+ "indexDepth": "indexDepth"
490
+ }
491
+ },
492
+ "bindings": "scatter",
493
+ "dispatch": {
494
+ "gridStride": "numel(shapes.updates) * indexDepth / numel(shapes.indices)",
495
+ "workgroupSize": "workgroupSize"
496
+ }
497
+ }
498
+ ]
499
+ },
500
+ {
501
+ "id": "row_fast_vec4",
502
+ "priority": 10,
503
+ "when": ["attrs.reduction == \"none\"", "validLayout", "f16Ok(dtypes.T)", "indexDepth == 1", "inner(shapes.data, 0) % 4 == 0", "numel(shapes.indices) > 0"],
504
+ "constants": {
505
+ "scalar": "dtypes.T",
506
+ "indexScalar": "dtypes.I",
507
+ "usesF16": "dtypes.T == \"f16\"",
508
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
509
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\""
510
+ },
511
+ "passes": [
512
+ {
513
+ "id": "copy",
514
+ "name": "ScatterND.Copy",
515
+ "source": {
516
+ "shader": "scatter-flat-copy.wgsl.jinja",
517
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
518
+ },
519
+ "bindings": "copy",
520
+ "dispatch": {
521
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
522
+ "workgroupSize": "workgroupSize"
523
+ },
524
+ "viewAlias": [{ "input": "src", "output": "dst" }]
525
+ },
526
+ {
527
+ "id": "scatter",
528
+ "name": "ScatterND.Scatter",
529
+ "source": {
530
+ "shader": "datamove-scatter-row.wgsl.jinja",
531
+ "inputs": {
532
+ "numIndices": "numel(shapes.indices)",
533
+ "axisDim": "dim(shapes.data, 0)",
534
+ "rowVecs": "inner(shapes.data, 0) / 4"
535
+ }
536
+ },
537
+ "bindings": "rowVec4",
538
+ "dispatch": { "gridStride": "numel(shapes.indices)" }
539
+ }
540
+ ]
541
+ },
542
+ {
543
+ "id": "batch0",
544
+ "priority": 0,
545
+ "when": ["attrs.reduction == \"none\"", "validLayout", "f16Ok(dtypes.T)", "updateDispatchFits"],
546
+ "constants": {
547
+ "scalar": "dtypes.T",
548
+ "indexScalar": "dtypes.I",
549
+ "usesF16": "dtypes.T == \"f16\"",
550
+ "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.output) % 4 == 0 else dtypes.T",
551
+ "workgroupSize": "workgroupSize",
552
+ "updateElement": "dtypes.T",
553
+ "outputElement": "dtypes.T"
554
+ },
555
+ "passes": [
556
+ {
557
+ "id": "copy",
558
+ "name": "ScatterND.Copy",
559
+ "source": {
560
+ "shader": "scatter-flat-copy.wgsl.jinja",
561
+ "inputs": { "count": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)" }
562
+ },
563
+ "bindings": "copy",
564
+ "dispatch": {
565
+ "gridStride": "numel(shapes.output) / 4 if numel(shapes.output) % 4 == 0 else numel(shapes.output)",
566
+ "workgroupSize": "workgroupSize"
567
+ },
568
+ "viewAlias": [{ "input": "src", "output": "dst" }]
569
+ },
570
+ {
571
+ "id": "scatter",
572
+ "name": "ScatterND.Scatter",
573
+ "source": {
574
+ "shader": "scatter-nd.wgsl.jinja",
575
+ "inputs": {
576
+ "dataShape": "shapes.data",
577
+ "indicesShape": "shapes.indices",
578
+ "updatesShape": "shapes.updates",
579
+ "dataRank": "ranks.data",
580
+ "indicesRank": "ranks.indices",
581
+ "updatesRank": "ranks.updates",
582
+ "indexDepth": "indexDepth"
583
+ }
584
+ },
585
+ "bindings": "scatter",
586
+ "dispatch": { "threads": "numel(shapes.updates)", "workgroupSize": "workgroupSize" }
587
+ }
588
+ ]
589
+ }
590
+ ]
591
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai.onnx.ScatterND",
3
+ "id": "_ai_onnx_scatternd_webgpu_3220727",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "GrY1TM5HIEywYLyjniLuwycr0czxfwYG/ac8PZ+IE9E=",
11
+ "datamove-scatter-row.wgsl.jinja": "nfTmOf2O7JCLw3aMpwqmEnUiLlraLVBklQLxkqhVUNg=",
12
+ "manifest.json": "ISRWqj8bAyNt1z7/35pzxXtQvaPRq4fRwm92neVyjgo=",
13
+ "scatter-f16-f32-convert.wgsl.jinja": "6ZiaoKGXqY9I7aiy2QY76t8N9f8tQTVed2GoDwBD9/o=",
14
+ "scatter-flat-copy.wgsl.jinja": "j95cywCwuvbfDJJxf9+SLt2Li3CvLllAesHpPykppFk=",
15
+ "scatter-narrow-wrap.wgsl.jinja": "skKLW560sgq8K0xFclFx8eEyk5tSDoPy11OgZhEQUfI=",
16
+ "scatter-nd-reduction-atomic.wgsl.jinja": "EEOc7xPQZcqWGQGN9sjbiHFaDFFm+FZqj0y3CsEtDog=",
17
+ "scatter-nd-reduction-f16-packed-atomic.wgsl.jinja": "B8AVBcP3R7qpgdMIZvwc1wFbZhanlnEib0kXY+4Ovj0=",
18
+ "scatter-nd-reduction-slice.wgsl.jinja": "N9MUXlG46j0ZO144klWLlT2tq8U8yeSkI8UIih6y2Q8=",
19
+ "scatter-nd.wgsl.jinja": "PJRmnpsn+b33y/t3VrpSqg1VuPA3Y1VZU1O9tl0eXWc=",
20
+ "scatter-scalar-add.wgsl.jinja": "GUpLZ/XGwu51w1vl/GEjAKdL3+iiWzSrcWqnb6qBwyQ=",
21
+ "test.json": "8D1gbC8s0AyoDrU6AG/OMKPiSSOe8uJnnZXOxy8uxwE="
22
+ }
23
+ },
24
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
25
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.ScatterND" }
26
+ }
build/webgpu/scatter-f16-f32-convert.wgsl.jinja ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable f16;
2
+ {{ env.wgsl.resourceDeclarations }}
3
+
4
+ // Narrow/wide conversion around f16 scatter reductions. `wide` stores f32
5
+ // payload bits in u32 words so the middle pass can bind the same allocation as
6
+ // atomic<u32> and perform portable float CAS operations.
7
+ @compute @workgroup_size({{ workgroupSize }})
8
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
9
+ @builtin(num_workgroups) nwg: vec3<u32>) {
10
+ let i = gid.x + gid.y * nwg.x * {{ workgroupSize }}u;
11
+ if (i >= params.count) { return; }
12
+ {% if source.toWide %}
13
+ wide[i] = bitcast<u32>(f32(narrow[i]));
14
+ {% else %}
15
+ narrow[i] = f16(bitcast<f32>(wide[i]));
16
+ {% endif %}
17
+ }
build/webgpu/scatter-flat-copy.wgsl.jinja ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Flat src -> dst copy for the scatter-family "materialize output from data"
2
+ // pass. The element width is owned by the binding element type: when
3
+ // numel(output) % 4 == 0 the bindings rebind as vec4<T> (128-bit loads).
4
+ // Tail-safe modes keep scalar bindings but copy a compile-time group of adjacent
5
+ // elements per invocation, guarding only the final partial group. A grid-stride
6
+ // loop lets the dispatch stay within maxComputeWorkgroupsPerDimension.
7
+ {% if usesF16 %}
8
+ enable f16;
9
+ {% endif %}
10
+ {{ env.wgsl.resourceDeclarations }}
11
+ {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
12
+
13
+ const COUNT: u32 = {{ source.count }}u;
14
+ const WG: u32 = {{ wg }}u;
15
+
16
+ @compute @workgroup_size({{ wg }})
17
+ fn main(
18
+ @builtin(global_invocation_id) gid: vec3<u32>,
19
+ @builtin(num_workgroups) nwg: vec3<u32>
20
+ ) {
21
+ let stride = nwg.x * WG;
22
+ for (var i = gid.x; i < COUNT; i += stride) {
23
+ dst[i] = src[i];
24
+ }
25
+ }
build/webgpu/scatter-narrow-wrap.wgsl.jinja ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% set mask = 255 if narrowBits == 8 else 65535 %}
3
+ {% set sign_bit = 128 if narrowBits == 8 else 32768 %}
4
+
5
+ // Truncates an atomically accumulated scatter result back to its narrow integer
6
+ // width, in place, after an add or mul reduction over int8/uint8/int16.
7
+ //
8
+ // Those types have no native atomic, so the reduction runs on the raw
9
+ // two's-complement bits held in u32. u32 add and mul are modulo 2^32 and
10
+ // truncation to the narrow width commutes with both, so narrowing once here is
11
+ // exactly the sequence of per-update narrowings the serial kernel performs --
12
+ // whatever order the atomics landed in.
13
+ //
14
+ // The values buffer stays bound as u32 for this pass, so the sign extension is
15
+ // written as a bit pattern rather than an i32 conversion.
16
+ const MASK: u32 = {{ mask }}u;
17
+ {% if narrowSigned %}
18
+ const SIGN_BIT: u32 = {{ sign_bit }}u;
19
+ {% endif %}
20
+
21
+ @compute @workgroup_size({{ workgroupSize }})
22
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
23
+ @builtin(num_workgroups) nwg: vec3<u32>) {
24
+ let i = gid.x + gid.y * nwg.x * {{ workgroupSize }}u;
25
+ if (i >= params.count) { return; }
26
+ let low = values[i] & MASK;
27
+ {% if narrowSigned %}
28
+ values[i] = select(low, low | ~MASK, low >= SIGN_BIT);
29
+ {% else %}
30
+ values[i] = low;
31
+ {% endif %}
32
+ }
build/webgpu/scatter-nd-reduction-atomic.wgsl.jinja ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Parallel ScatterND reduction: one thread per update and an atomic
2
+ // read-modify-write at the destination. Native integer atomics cover add/min/max;
3
+ // integer mul uses compare-exchange, f32 uses a CAS bitcast loop, and eligible
4
+ // f16 reductions use that f32 path through an upcast scratch before narrowing.
5
+ //
6
+ // Subgroup lanes targeting one destination may pre-aggregate CAS-based add/mul
7
+ // updates before an elected lane issues the atomic. Uniform match-loop control
8
+ // ensures every lane reaches each collective. Floating-point grouping and atomic
9
+ // update order can both change rounding. Without subgroups, an all-equal-key f32
10
+ // add workgroup reduces locally and emits one CAS; mixed keys use per-update CAS.
11
+ {% if usesF16 %}
12
+ enable f16;
13
+ {% endif %}
14
+ {% if useSubgroups %}
15
+ enable subgroups;
16
+ {% endif %}
17
+ {{ env.wgsl.resourceDeclarations }}
18
+
19
+ const WG: u32 = {{ workgroupSize }}u;
20
+ const INVALID_INDEX: u32 = 0xffffffffu;
21
+ {% if useWorkgroupPreagg %}
22
+
23
+ var<workgroup> wg_first_key: u32;
24
+ var<workgroup> wg_mismatch: atomic<u32>;
25
+ var<workgroup> wg_all_same: u32;
26
+ var<workgroup> wg_vals: array<f32, {{ workgroupSize }}>;
27
+
28
+ {% endif %}
29
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
30
+ let dim_i = i32(dim);
31
+ var index = raw;
32
+ if (index < 0) {
33
+ index = index + dim_i;
34
+ }
35
+ if (index < 0 || index >= dim_i) {
36
+ return INVALID_INDEX;
37
+ }
38
+ return u32(index);
39
+ }
40
+
41
+
42
+ fn indices_base(update_index: u32) -> u32 {
43
+ var rem = update_index;
44
+ var offset = 0u;
45
+ {% for axis in range(source.updatesRank) %}
46
+ {% set update_stride = namespace(value=1) %}
47
+ {% for j in range(axis + 1, source.updatesRank) %}
48
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
49
+ {% endfor %}
50
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
51
+ {% if axis < source.indicesRank - 1 %}
52
+ let coord{{ axis }} = rem / {{ update_stride_divisor }}u;
53
+ {% endif %}
54
+ rem = rem % {{ update_stride_divisor }}u;
55
+ {% if axis < source.indicesRank - 1 %}
56
+ {% set index_stride = namespace(value=source.indexDepth) %}
57
+ {% for j in range(axis + 1, source.indicesRank - 1) %}
58
+ {% set index_stride.value = index_stride.value * source.indicesShape[j] %}
59
+ {% endfor %}
60
+ offset = offset + coord{{ axis }} * {{ index_stride.value }}u;
61
+ {% endif %}
62
+ {% endfor %}
63
+ return offset;
64
+ }
65
+
66
+ fn output_offset(update_index: u32) -> u32 {
67
+ let index_base = indices_base(update_index);
68
+ var rem = update_index;
69
+ {% for axis in range(source.updatesRank) %}
70
+ {% set update_stride = namespace(value=1) %}
71
+ {% for j in range(axis + 1, source.updatesRank) %}
72
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
73
+ {% endfor %}
74
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
75
+ {% set suffixBegin = source.indicesRank - 1 %}
76
+ {% set suffixEnd = suffixBegin + source.dataRank - source.indexDepth %}
77
+ {% if axis >= suffixBegin and axis < suffixEnd %}
78
+ let ucoord{{ axis }} = rem / {{ update_stride_divisor }}u;
79
+ {% endif %}
80
+ rem = rem % {{ update_stride_divisor }}u;
81
+ {% endfor %}
82
+ var offset = 0u;
83
+ {% for axis in range(source.dataRank) %}
84
+ {% set data_stride = namespace(value=1) %}
85
+ {% for j in range(axis + 1, source.dataRank) %}
86
+ {% set data_stride.value = data_stride.value * source.dataShape[j] %}
87
+ {% endfor %}
88
+ {% if axis < source.indexDepth %}
89
+ let gathered{{ axis }} = normalize_index(indices[index_base + {{ axis }}u], {{ source.dataShape[axis] }}u);
90
+ if (gathered{{ axis }} == INVALID_INDEX) {
91
+ return INVALID_INDEX;
92
+ }
93
+ offset = offset + gathered{{ axis }} * {{ data_stride.value }}u;
94
+ {% else %}
95
+ {% set suffix_axis = (source.indicesRank - 1) + (axis - source.indexDepth) %}
96
+ offset = offset + ucoord{{ suffix_axis }} * {{ data_stride.value }}u;
97
+ {% endif %}
98
+ {% endfor %}
99
+ return offset;
100
+ }
101
+
102
+ {% if scalar == "f32" %}
103
+ // CAS-bitcast read-modify-write: output is bound as atomic<u32>; the f32
104
+ // payload is bitcast in-shader because WGSL has no float atomics.
105
+ fn apply_update(dst: u32, update: f32) {
106
+ var old_bits = atomicLoad(&output[dst]);
107
+ loop {
108
+ let current = bitcast<f32>(old_bits);
109
+ {% if reductionOp == "add" %}
110
+ let combined = current + update;
111
+ {% elif reductionOp == "mul" %}
112
+ let combined = current * update;
113
+ {% elif reductionOp == "min" %}
114
+ let combined = min(current, update);
115
+ {% else %}
116
+ let combined = max(current, update);
117
+ {% endif %}
118
+ let result = atomicCompareExchangeWeak(&output[dst], old_bits, bitcast<u32>(combined));
119
+ if (result.exchanged) {
120
+ break;
121
+ }
122
+ old_bits = result.old_value;
123
+ }
124
+ }
125
+ {% elif reductionOp == "mul" %}
126
+ // No atomicMul in WGSL: CAS loop on the native integer payload (wrapping
127
+ // integer multiply, same as the serial shader's current * update).
128
+ fn apply_update(dst: u32, update: {{ scalar }}) {
129
+ var old = atomicLoad(&output[dst]);
130
+ loop {
131
+ let result = atomicCompareExchangeWeak(&output[dst], old, old * update);
132
+ if (result.exchanged) {
133
+ break;
134
+ }
135
+ old = result.old_value;
136
+ }
137
+ }
138
+ {% else %}
139
+ fn apply_update(dst: u32, update: {{ scalar }}) {
140
+ {% if reductionOp == "add" %}
141
+ atomicAdd(&output[dst], update);
142
+ {% elif reductionOp == "min" %}
143
+ atomicMin(&output[dst], update);
144
+ {% else %}
145
+ atomicMax(&output[dst], update);
146
+ {% endif %}
147
+ }
148
+ {% endif %}
149
+ {% if useSubgroups %}
150
+
151
+ // Index of the lowest set bit across a 128-bit subgroup ballot. The ballot is
152
+ // subgroup-uniform, so every lane computes the same leader lane id.
153
+ fn ballot_first_lane(b: vec4<u32>) -> u32 {
154
+ if (b.x != 0u) { return firstTrailingBit(b.x); }
155
+ if (b.y != 0u) { return 32u + firstTrailingBit(b.y); }
156
+ if (b.z != 0u) { return 64u + firstTrailingBit(b.z); }
157
+ return 96u + firstTrailingBit(b.w);
158
+ }
159
+
160
+ // How many distinct output slots per subgroup are pre-aggregated before falling
161
+ // back to per-lane atomics for the remainder. A compile-time constant so the
162
+ // round loop has a uniform trip count (no data-dependent break): the subgroup
163
+ // builtins inside it are reached by every lane on every round, i.e. in
164
+ // subgroup-uniform control flow. ROUNDS covers the common contention shapes
165
+ // (few hot slots per subgroup, incl. the all-same-slot worst case in round 0);
166
+ // rare highly-scattered subgroups simply spill to the per-lane path.
167
+ const PREAGG_ROUNDS: u32 = {{ preaggRounds }}u;
168
+
169
+ // Warp-aggregated atomic: in each round, all still-pending lanes elect the
170
+ // smallest target slot (subgroupMin), the lanes hitting that slot combine their
171
+ // updates locally with subgroupAdd/subgroupMul, and one leader lane issues a
172
+ // single atomic for it. After PREAGG_ROUNDS rounds any lane still pending does a
173
+ // direct per-lane atomic. `valid` lanes whose dst is INVALID_INDEX never
174
+ // participate and never write.
175
+ //
176
+ // The algorithm is width-independent and makes no assumption about the subgroup
177
+ // width (subgroupMin/Add/Mul/Ballot reduce over whatever the compiled width is,
178
+ // and subgroupBallot is always a 128-bit vec4 regardless). `sg_size` only gates
179
+ // the cheap degenerate case: a 1-wide subgroup has no peers to combine, so it
180
+ // goes straight to the per-lane atomic and skips the reduction overhead.
181
+ fn aggregate_update(dst_in: u32, update_in: {{ scalar }}, valid_in: bool, sg_lane: u32, sg_size: u32) {
182
+ var pending = valid_in && (dst_in != INVALID_INDEX);
183
+ if (sg_size <= 1u) {
184
+ if (pending) {
185
+ apply_update(dst_in, update_in);
186
+ }
187
+ return;
188
+ }
189
+ for (var r = 0u; r < PREAGG_ROUNDS; r = r + 1u) {
190
+ // Elect a candidate dst: the smallest dst among still-pending lanes.
191
+ // Non-pending lanes contribute the sentinel so they cannot win.
192
+ let key = select(INVALID_INDEX, dst_in, pending);
193
+ let cand = subgroupMin(key);
194
+ let matched = pending && (dst_in == cand);
195
+ {% if reductionOp == "add" %}
196
+ let contrib = select({{ scalar }}(0), update_in, matched);
197
+ let total = subgroupAdd(contrib);
198
+ {% else %}
199
+ let contrib = select({{ scalar }}(1), update_in, matched);
200
+ let total = subgroupMul(contrib);
201
+ {% endif %}
202
+ // Elect the lowest matched lane as leader to
203
+ // issue the single atomic for `cand`. Ballot + firstTrailingBit is uniform.
204
+ // cand == INVALID_INDEX means no lane is pending this round -> skip.
205
+ let leader = ballot_first_lane(subgroupBallot(matched));
206
+ if (matched && sg_lane == leader && cand != INVALID_INDEX) {
207
+ apply_update(cand, total);
208
+ }
209
+ if (matched) {
210
+ pending = false;
211
+ }
212
+ }
213
+ // Spill: lanes that still have work after the fixed rounds fall back to a
214
+ // direct per-lane atomic (correct, just unaggregated for that remainder).
215
+ if (pending) {
216
+ apply_update(dst_in, update_in);
217
+ }
218
+ }
219
+ {% endif %}
220
+
221
+
222
+ @compute @workgroup_size(WG)
223
+ fn main(
224
+ @builtin(global_invocation_id) gid: vec3<u32>,
225
+ @builtin(num_workgroups) nwg: vec3<u32>
226
+ {%- if useSubgroups %},
227
+ @builtin(subgroup_invocation_id) sg_lane: u32,
228
+ @builtin(subgroup_size) sg_size: u32
229
+ {%- endif %}
230
+ {% if useWorkgroupPreagg %},
231
+ @builtin(local_invocation_id) lid: vec3<u32>
232
+ {%- endif %}
233
+ ) {
234
+ {% if useWorkgroupPreagg %}
235
+ // Coalesce several updates per lane, then test whether every live lane maps
236
+ // to the same valid destination. Equal-key workgroups reduce locally; mixed
237
+ // keys replay through the subgroup or direct-atomic path after the barriers.
238
+ const ITEMS: u32 = {{ preaggItemsPerThread }}u;
239
+ let thread = gid.y * (nwg.x * WG) + gid.x;
240
+ let totalThreads = nwg.x * nwg.y * WG;
241
+ if (lid.x == 0u) {
242
+ let firstIndex = thread - lid.x;
243
+ var firstKey = INVALID_INDEX;
244
+ if (firstIndex < params.count) { firstKey = output_offset(firstIndex); }
245
+ wg_first_key = firstKey;
246
+ atomicStore(&wg_mismatch, 0u);
247
+ }
248
+ let firstKey = workgroupUniformLoad(&wg_first_key);
249
+ var localSum = 0.0;
250
+ for (var item = 0u; item < ITEMS; item += 1u) {
251
+ let i = thread + item * totalThreads;
252
+ if (i < params.count) {
253
+ let dst = output_offset(i);
254
+ if (dst != INVALID_INDEX) {
255
+ if (dst == firstKey) {
256
+ localSum += updates[i];
257
+ } else {
258
+ atomicStore(&wg_mismatch, 1u);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ wg_vals[lid.x] = localSum;
264
+ workgroupBarrier();
265
+ if (lid.x == 0u) {
266
+ wg_all_same = select(0u, 1u, firstKey != INVALID_INDEX && atomicLoad(&wg_mismatch) == 0u);
267
+ }
268
+ let allSame = workgroupUniformLoad(&wg_all_same) != 0u;
269
+ if (allSame) {
270
+ var offset = WG / 2u;
271
+ loop {
272
+ if (lid.x < offset) {
273
+ wg_vals[lid.x] += wg_vals[lid.x + offset];
274
+ }
275
+ workgroupBarrier();
276
+ if (offset <= 1u) { break; }
277
+ offset /= 2u;
278
+ }
279
+ if (lid.x == 0u) {
280
+ apply_update(firstKey, wg_vals[0]);
281
+ }
282
+ } else {
283
+ for (var item = 0u; item < ITEMS; item += 1u) {
284
+ let i = thread + item * totalThreads;
285
+ let inRange = i < params.count;
286
+ var dst = INVALID_INDEX;
287
+ var upd = 0.0;
288
+ if (inRange) {
289
+ dst = output_offset(i);
290
+ upd = {{ scalar }}(updates[i]);
291
+ }
292
+ {% if useSubgroups %}
293
+ aggregate_update(dst, upd, inRange, sg_lane, sg_size);
294
+ {% else %}
295
+ if (inRange && dst != INVALID_INDEX) {
296
+ apply_update(dst, upd);
297
+ }
298
+ {% endif %}
299
+ }
300
+ }
301
+ {% elif useSubgroups %}
302
+ // The folded 2D dispatch assigns one index to every lane. Every lane calls
303
+ // `aggregate_update` once, keeping its subgroup builtins uniform; lanes past
304
+ // the count join as non-pending and neither read nor write.
305
+ let i = gid.y * (nwg.x * WG) + gid.x;
306
+ let in_range = i < params.count;
307
+ var dst = INVALID_INDEX;
308
+ var upd = {{ scalar }}(0);
309
+ if (in_range) {
310
+ dst = output_offset(i);
311
+ upd = {{ scalar }}(updates[i]);
312
+ }
313
+ aggregate_update(dst, upd, in_range, sg_lane, sg_size);
314
+ {% else %}
315
+ // Without subgroups, grid-stride over the folded 2D dispatch and issue one
316
+ // atomic for each valid update.
317
+ let row = nwg.x * WG;
318
+ let stride = row * nwg.y;
319
+ for (var i = gid.y * row + gid.x; i < params.count; i += stride) {
320
+ let dst = output_offset(i);
321
+ if (dst == INVALID_INDEX) {
322
+ continue;
323
+ }
324
+ apply_update(dst, {{ scalar }}(updates[i]));
325
+ }
326
+ {% endif %}
327
+ }
build/webgpu/scatter-nd-reduction-f16-packed-atomic.wgsl.jinja ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable f16;
2
+ {{ env.wgsl.resourceDeclarations }}
3
+ // Direct f16 reduction via CAS on the packed pair containing each output
4
+ // element. Updating either half preserves the other half from the successful
5
+ // observed word, so adjacent columns and duplicate indices remain race-free
6
+ // without widening the output into scratch storage.
7
+ // Add only: the one case that selects this shader gates on
8
+ // attrs.reduction == "add", so min/max never reach it. The general
9
+ // atomic and slice routes carry the other reductions.
10
+ //
11
+ // The same case requires numel(updates) >= F16_ATOMIC_MIN_UPDATES, so no
12
+ // update extent is zero and every stride below divides.
13
+
14
+ const WG: u32 = {{ workgroupSize }}u;
15
+ const INVALID_INDEX: u32 = 0xffffffffu;
16
+
17
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
18
+ let dim_i = i32(dim);
19
+ var index = raw;
20
+ if (index < 0) {
21
+ index = index + dim_i;
22
+ }
23
+ if (index < 0 || index >= dim_i) {
24
+ return INVALID_INDEX;
25
+ }
26
+ return u32(index);
27
+ }
28
+
29
+
30
+ fn indices_base(update_index: u32) -> u32 {
31
+ var rem = update_index;
32
+ var offset = 0u;
33
+ {% for axis in range(source.updatesRank) %}
34
+ {% set update_stride = namespace(value=1) %}
35
+ {% for j in range(axis + 1, source.updatesRank) %}
36
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
37
+ {% endfor %}
38
+ {% set update_stride_divisor = update_stride.value %}
39
+ {% if axis < source.indicesRank - 1 %}
40
+ let coord{{ axis }} = rem / {{ update_stride_divisor }}u;
41
+ {% endif %}
42
+ rem %= {{ update_stride_divisor }}u;
43
+ {% if axis < source.indicesRank - 1 %}
44
+ {% set index_stride = namespace(value=source.indexDepth) %}
45
+ {% for j in range(axis + 1, source.indicesRank - 1) %}
46
+ {% set index_stride.value = index_stride.value * source.indicesShape[j] %}
47
+ {% endfor %}
48
+ offset += coord{{ axis }} * {{ index_stride.value }}u;
49
+ {% endif %}
50
+ {% endfor %}
51
+ return offset;
52
+ }
53
+
54
+ fn output_offset(update_index: u32) -> u32 {
55
+ let index_base = indices_base(update_index);
56
+ var rem = update_index;
57
+ {% for axis in range(source.updatesRank) %}
58
+ {% set update_stride = namespace(value=1) %}
59
+ {% for j in range(axis + 1, source.updatesRank) %}
60
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
61
+ {% endfor %}
62
+ {% set update_stride_divisor = update_stride.value %}
63
+ {% set suffixBegin = source.indicesRank - 1 %}
64
+ {% set suffixEnd = suffixBegin + source.dataRank - source.indexDepth %}
65
+ {% if axis >= suffixBegin and axis < suffixEnd %}
66
+ let ucoord{{ axis }} = rem / {{ update_stride_divisor }}u;
67
+ {% endif %}
68
+ rem %= {{ update_stride_divisor }}u;
69
+ {% endfor %}
70
+ var offset = 0u;
71
+ {% for axis in range(source.dataRank) %}
72
+ {% set data_stride = namespace(value=1) %}
73
+ {% for j in range(axis + 1, source.dataRank) %}
74
+ {% set data_stride.value = data_stride.value * source.dataShape[j] %}
75
+ {% endfor %}
76
+ {% if axis < source.indexDepth %}
77
+ let gathered{{ axis }} = normalize_index(indices[index_base + {{ axis }}u], {{ source.dataShape[axis] }}u);
78
+ if (gathered{{ axis }} == INVALID_INDEX) {
79
+ return INVALID_INDEX;
80
+ }
81
+ offset += gathered{{ axis }} * {{ data_stride.value }}u;
82
+ {% else %}
83
+ {% set suffix_axis = (source.indicesRank - 1) + (axis - source.indexDepth) %}
84
+ offset += ucoord{{ suffix_axis }} * {{ data_stride.value }}u;
85
+ {% endif %}
86
+ {% endfor %}
87
+ return offset;
88
+ }
89
+
90
+ fn apply_update(dst: u32, update: f32) {
91
+ let word_index = dst >> 1u;
92
+ let high_half = (dst & 1u) != 0u;
93
+ var observed = atomicLoad(&output[word_index]);
94
+ loop {
95
+ var pair = unpack2x16float(observed);
96
+ let current = select(pair.x, pair.y, high_half);
97
+ let reduced = current + update;
98
+ if (high_half) {
99
+ pair.y = reduced;
100
+ } else {
101
+ pair.x = reduced;
102
+ }
103
+ let exchanged = atomicCompareExchangeWeak(&output[word_index], observed, pack2x16float(pair));
104
+ if (exchanged.exchanged) {
105
+ return;
106
+ }
107
+ observed = exchanged.old_value;
108
+ }
109
+ }
110
+
111
+ @compute @workgroup_size(WG, 1, 1)
112
+ fn main(
113
+ @builtin(global_invocation_id) gid: vec3<u32>,
114
+ @builtin(num_workgroups) nwg: vec3<u32>,
115
+ ) {
116
+ let row = nwg.x * WG;
117
+ let stride = row * nwg.y;
118
+ for (var i = gid.y * row + gid.x; i < params.count; i += stride) {
119
+ let dst = output_offset(i);
120
+ if (dst != INVALID_INDEX) {
121
+ apply_update(dst, f32(updates[i]));
122
+ }
123
+ }
124
+ }
build/webgpu/scatter-nd-reduction-slice.wgsl.jinja ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Column-parallel ScatterND reduction without atomics for f16/int8. Duplicate
2
+ // indices can collide only within one inner-width column: each update retains
3
+ // its trailing coordinate `w`, while indices remap only its leading slice.
4
+ // Different `w` values write disjoint cells, so columns run in parallel and the
5
+ // slice sweep within each column stays serial to order read-modify-write safely.
6
+ // A grid-stride loop covers any width.
7
+ // INNER_WIDTH = product(updatesShape[indicesRank-1:]) = data slice width.
8
+ {% if usesF16 %}
9
+ enable f16;
10
+ {% endif %}
11
+ {{ env.wgsl.resourceDeclarations }}
12
+
13
+ const WG: u32 = {{ workgroupSize }}u;
14
+ {% set inner_width = namespace(value=1) %}
15
+ {% for j in range(source.indicesRank - 1, source.updatesRank) %}
16
+ {% set inner_width.value = inner_width.value * source.updatesShape[j] %}
17
+ {% endfor %}
18
+ const INNER_WIDTH: u32 = {{ inner_width.value if inner_width.value != 0 else 1 }}u;
19
+ const INVALID_INDEX: u32 = 0xffffffffu;
20
+
21
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
22
+ let dim_i = i32(dim);
23
+ var index = raw;
24
+ if (index < 0) {
25
+ index = index + dim_i;
26
+ }
27
+ if (index < 0 || index >= dim_i) {
28
+ return INVALID_INDEX;
29
+ }
30
+ return u32(index);
31
+ }
32
+
33
+
34
+ fn indices_base(update_index: u32) -> u32 {
35
+ var rem = update_index;
36
+ var offset = 0u;
37
+ {% for axis in range(source.updatesRank) %}
38
+ {% set update_stride = namespace(value=1) %}
39
+ {% for j in range(axis + 1, source.updatesRank) %}
40
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
41
+ {% endfor %}
42
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
43
+ {% if axis < source.indicesRank - 1 %}
44
+ let coord{{ axis }} = rem / {{ update_stride_divisor }}u;
45
+ {% endif %}
46
+ rem = rem % {{ update_stride_divisor }}u;
47
+ {% if axis < source.indicesRank - 1 %}
48
+ {% set index_stride = namespace(value=source.indexDepth) %}
49
+ {% for j in range(axis + 1, source.indicesRank - 1) %}
50
+ {% set index_stride.value = index_stride.value * source.indicesShape[j] %}
51
+ {% endfor %}
52
+ offset = offset + coord{{ axis }} * {{ index_stride.value }}u;
53
+ {% endif %}
54
+ {% endfor %}
55
+ return offset;
56
+ }
57
+
58
+ fn output_offset(update_index: u32) -> u32 {
59
+ let index_base = indices_base(update_index);
60
+ var rem = update_index;
61
+ {% for axis in range(source.updatesRank) %}
62
+ {% set update_stride = namespace(value=1) %}
63
+ {% for j in range(axis + 1, source.updatesRank) %}
64
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
65
+ {% endfor %}
66
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
67
+ {% set suffixBegin = source.indicesRank - 1 %}
68
+ {% set suffixEnd = suffixBegin + source.dataRank - source.indexDepth %}
69
+ {% if axis >= suffixBegin and axis < suffixEnd %}
70
+ let ucoord{{ axis }} = rem / {{ update_stride_divisor }}u;
71
+ {% endif %}
72
+ rem = rem % {{ update_stride_divisor }}u;
73
+ {% endfor %}
74
+ var offset = 0u;
75
+ {% for axis in range(source.dataRank) %}
76
+ {% set data_stride = namespace(value=1) %}
77
+ {% for j in range(axis + 1, source.dataRank) %}
78
+ {% set data_stride.value = data_stride.value * source.dataShape[j] %}
79
+ {% endfor %}
80
+ {% if axis < source.indexDepth %}
81
+ let gathered{{ axis }} = normalize_index(indices[index_base + {{ axis }}u], {{ source.dataShape[axis] }}u);
82
+ if (gathered{{ axis }} == INVALID_INDEX) {
83
+ return INVALID_INDEX;
84
+ }
85
+ offset = offset + gathered{{ axis }} * {{ data_stride.value }}u;
86
+ {% else %}
87
+ {% set suffix_axis = (source.indicesRank - 1) + (axis - source.indexDepth) %}
88
+ offset = offset + ucoord{{ suffix_axis }} * {{ data_stride.value }}u;
89
+ {% endif %}
90
+ {% endfor %}
91
+ return offset;
92
+ }
93
+
94
+ @compute @workgroup_size(WG)
95
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
96
+ @builtin(num_workgroups) nwg: vec3<u32>) {
97
+ let num_slices = params.count / INNER_WIDTH;
98
+ let stride = nwg.x * WG;
99
+ // One thread per inner-width column; sweep slices for that column so
100
+ // duplicate-index collisions stay on a single lane (disjoint columns never race).
101
+ for (var w = gid.x; w < INNER_WIDTH; w = w + stride) {
102
+ for (var s = 0u; s < num_slices; s = s + 1u) {
103
+ let i = s * INNER_WIDTH + w;
104
+ let dst = output_offset(i);
105
+ if (dst == INVALID_INDEX) {
106
+ continue;
107
+ }
108
+ {% set scatterIndent = " " %}
109
+ {% if scalar == "i32" or scalar == "u32" %}
110
+ {{ scatterIndent }}let current = output[dst];
111
+ {{ scatterIndent }}let update = updates[i];
112
+ {% if reductionOp == "add" %}
113
+ {{ scatterIndent }}let combined = current + update;
114
+ {% elif reductionOp == "mul" %}
115
+ {{ scatterIndent }}let combined = current * update;
116
+ {% elif reductionOp == "min" %}
117
+ {{ scatterIndent }}output[dst] = min(current, update);
118
+ {% else %}
119
+ {{ scatterIndent }}output[dst] = max(current, update);
120
+ {% endif %}
121
+ {% if reductionOp == "add" or reductionOp == "mul" %}
122
+ {% if narrowBits == 8 and narrowSigned %}
123
+ {{ scatterIndent }}let low = combined & 255;
124
+ {{ scatterIndent }}output[dst] = select(low, low - 256, low > 127);
125
+ {% elif narrowBits == 16 and narrowSigned %}
126
+ {{ scatterIndent }}let low = combined & 65535;
127
+ {{ scatterIndent }}output[dst] = select(low, low - 65536, low > 32767);
128
+ {% elif narrowBits == 8 %}
129
+ {{ scatterIndent }}output[dst] = combined & 255u;
130
+ {% else %}
131
+ {{ scatterIndent }}output[dst] = combined;
132
+ {% endif %}
133
+ {% endif %}
134
+ {% else %}
135
+ {{ scatterIndent }}let current = f32(output[dst]);
136
+ {{ scatterIndent }}let update = f32(updates[i]);
137
+ {% if reductionOp == "add" %}
138
+ {{ scatterIndent }}output[dst] = {{ scalar }}(current + update);
139
+ {% elif reductionOp == "mul" %}
140
+ {{ scatterIndent }}output[dst] = {{ scalar }}(current * update);
141
+ {% elif reductionOp == "min" %}
142
+ {{ scatterIndent }}output[dst] = {{ scalar }}(min(current, update));
143
+ {% else %}
144
+ {{ scatterIndent }}output[dst] = {{ scalar }}(max(current, update));
145
+ {% endif %}
146
+ {% endif %}
147
+ }
148
+ }
149
+ }
build/webgpu/scatter-nd.wgsl.jinja ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ const INVALID_INDEX: u32 = 0xffffffffu;
7
+ const WG: u32 = {{ workgroupSize }}u;
8
+
9
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
10
+ let dim_i = i32(dim);
11
+ var index = raw;
12
+ if (index < 0) {
13
+ index = index + dim_i;
14
+ }
15
+ if (index < 0 || index >= dim_i) {
16
+ return INVALID_INDEX;
17
+ }
18
+ return u32(index);
19
+ }
20
+
21
+
22
+ fn indices_base(update_index: u32) -> u32 {
23
+ var rem = update_index;
24
+ var offset = 0u;
25
+ {% for axis in range(source.updatesRank) %}
26
+ {% set update_stride = namespace(value=1) %}
27
+ {% for j in range(axis + 1, source.updatesRank) %}
28
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
29
+ {% endfor %}
30
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
31
+ {% if axis < source.indicesRank - 1 %}
32
+ let coord{{ axis }} = rem / {{ update_stride_divisor }}u;
33
+ {% endif %}
34
+ rem = rem % {{ update_stride_divisor }}u;
35
+ {% if axis < source.indicesRank - 1 %}
36
+ {% set index_stride = namespace(value=source.indexDepth) %}
37
+ {% for j in range(axis + 1, source.indicesRank - 1) %}
38
+ {% set index_stride.value = index_stride.value * source.indicesShape[j] %}
39
+ {% endfor %}
40
+ offset = offset + coord{{ axis }} * {{ index_stride.value }}u;
41
+ {% endif %}
42
+ {% endfor %}
43
+ return offset;
44
+ }
45
+
46
+ fn output_offset(update_index: u32) -> u32 {
47
+ let index_base = indices_base(update_index);
48
+ var rem = update_index;
49
+ {% for axis in range(source.updatesRank) %}
50
+ {% set update_stride = namespace(value=1) %}
51
+ {% for j in range(axis + 1, source.updatesRank) %}
52
+ {% set update_stride.value = update_stride.value * source.updatesShape[j] %}
53
+ {% endfor %}
54
+ {% set update_stride_divisor = update_stride.value if update_stride.value != 0 else 1 %}
55
+ {% set suffixBegin = source.indicesRank - 1 %}
56
+ {% set suffixEnd = suffixBegin + source.dataRank - source.indexDepth %}
57
+ {% if axis >= suffixBegin and axis < suffixEnd %}
58
+ let ucoord{{ axis }} = rem / {{ update_stride_divisor }}u;
59
+ {% endif %}
60
+ rem = rem % {{ update_stride_divisor }}u;
61
+ {% endfor %}
62
+ var offset = 0u;
63
+ {% for axis in range(source.dataRank) %}
64
+ {% set data_stride = namespace(value=1) %}
65
+ {% for j in range(axis + 1, source.dataRank) %}
66
+ {% set data_stride.value = data_stride.value * source.dataShape[j] %}
67
+ {% endfor %}
68
+ {% if axis < source.indexDepth %}
69
+ let gathered{{ axis }} = normalize_index(indices[index_base + {{ axis }}u], {{ source.dataShape[axis] }}u);
70
+ if (gathered{{ axis }} == INVALID_INDEX) {
71
+ return INVALID_INDEX;
72
+ }
73
+ offset = offset + gathered{{ axis }} * {{ data_stride.value }}u;
74
+ {% else %}
75
+ {% set suffix_axis = (source.indicesRank - 1) + (axis - source.indexDepth) %}
76
+ offset = offset + ucoord{{ suffix_axis }} * {{ data_stride.value }}u;
77
+ {% endif %}
78
+ {% endfor %}
79
+ return offset;
80
+ }
81
+
82
+ @compute @workgroup_size(WG)
83
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
84
+ // 2D-folded flat index: gid.y carries the high bits past the device's
85
+ // per-dimension dispatch limit.
86
+ let i = gid.x + gid.y * nwg.x * WG;
87
+ if (i >= params.count) {
88
+ return;
89
+ }
90
+ let offset = output_offset(i);
91
+ if (offset != INVALID_INDEX) {
92
+ output[offset] = updates[i];
93
+ }
94
+ }
build/webgpu/scatter-scalar-add.wgsl.jinja ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Feature-independent high-contention ScatterND add for a one-element output.
2
+ // Each lane sums a grid-stride stream, each workgroup reduces locally, and one
3
+ // lane emits one f32 CAS. This replaces O(updates) globally serialized CAS
4
+ // operations with O(workgroups) CAS operations.
5
+ {{ env.wgsl.resourceDeclarations }}
6
+
7
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
8
+ const INVALID_INDEX: u32 = 0xffffffffu;
9
+ var<workgroup> partials: array<f32, {{ tunables.WORKGROUP_SIZE }}>;
10
+
11
+ fn normalize_index(raw: {{ indexScalar }}, dim: u32) -> u32 {
12
+ let dim_i = i32(dim);
13
+ var index = raw;
14
+ if (index < 0) {
15
+ index = index + dim_i;
16
+ }
17
+ if (index < 0 || index >= dim_i) {
18
+ return INVALID_INDEX;
19
+ }
20
+ return u32(index);
21
+ }
22
+
23
+
24
+ fn atomic_add_f32(update: f32) {
25
+ var old_bits = atomicLoad(&output[0]);
26
+ loop {
27
+ let result = atomicCompareExchangeWeak(
28
+ &output[0], old_bits, bitcast<u32>(bitcast<f32>(old_bits) + update)
29
+ );
30
+ if (result.exchanged) { break; }
31
+ old_bits = result.old_value;
32
+ }
33
+ }
34
+
35
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
36
+ fn main(
37
+ @builtin(global_invocation_id) gid: vec3<u32>,
38
+ @builtin(local_invocation_id) lid: vec3<u32>,
39
+ @builtin(num_workgroups) nwg: vec3<u32>
40
+ ) {
41
+ let total_threads = nwg.x * WG;
42
+ var sum = 0.0;
43
+ for (var i = gid.x; i < params.count; i += total_threads) {
44
+ if (normalize_index(indices[i], 1u) != INVALID_INDEX) {
45
+ sum += updates[i];
46
+ }
47
+ }
48
+ partials[lid.x] = sum;
49
+ workgroupBarrier();
50
+
51
+ var offset = WG / 2u;
52
+ loop {
53
+ if (lid.x < offset) {
54
+ partials[lid.x] += partials[lid.x + offset];
55
+ }
56
+ workgroupBarrier();
57
+ if (offset <= 1u) { break; }
58
+ offset /= 2u;
59
+ }
60
+ if (lid.x == 0u) {
61
+ atomic_add_f32(partials[0]);
62
+ }
63
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,1326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.ScatterND",
3
+ "fixtureArrays": {
4
+ "scatter_nd_data_4x4x4": [1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8],
5
+ "backend_row_slice_updates_input_updates": [5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4],
6
+ "ort_reduction_add_slice_duplicates_input_updates": [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144],
7
+ "f16_reduction_add_sparse_router_gradient_input_indices": [0, 17, 34, 51, 4, 21, 38, 55, 8, 25, 42, 59, 12, 29, 46, 63, 16, 33, 50, 3, 20, 37, 54, 7, 24, 41, 58, 11, 28, 45, 62, 15, 32, 49, 2, 19, 36, 53, 6, 23, 40, 57, 10, 27, 44, 61, 14, 31, 48, 1, 18, 35, 52, 5, 22, 39, 56, 9, 26, 43, 60, 13, 30, 47]
8
+ },
9
+ "cases": [
10
+ {
11
+ "name": "dispatch_cliff_batch0_row_update",
12
+ "inputs": {
13
+ "data": { "dtype": "float32", "shape": [1, 16777219], "data": { "kind": "constant", "value": 1.0 } },
14
+ "indices": { "dtype": "int32", "shape": [1, 1], "data": { "kind": "values", "values": [0] } },
15
+ "updates": { "dtype": "float32", "shape": [1, 16777219], "data": { "kind": "constant", "value": 7.0 } }
16
+ },
17
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 16777219], "tolerance": 0 } }
18
+ },
19
+ {
20
+ "name": "scalar_updates",
21
+ "inputs": {
22
+ "data": {
23
+ "dtype": "float32",
24
+ "shape": [3, 3],
25
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 10.0, 11.0, 12.0, 20.0, 21.0, 22.0] }
26
+ },
27
+ "indices": { "dtype": "int32", "shape": [2, 2], "data": { "kind": "values", "values": [0, 2, 2, 1] } },
28
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [99.0, 77.0] } }
29
+ },
30
+ "outputs": { "output": { "dtype": "float32", "shape": [3, 3] } }
31
+ },
32
+ {
33
+ "name": "row_updates_f16",
34
+ "inputs": {
35
+ "data": { "dtype": "float16", "shape": [3, 4] },
36
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [2, 0] } },
37
+ "updates": {
38
+ "dtype": "float16",
39
+ "shape": [2, 4],
40
+ "data": { "kind": "values", "values": [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0] }
41
+ }
42
+ },
43
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 4] } },
44
+ "tolerance": 0.001
45
+ },
46
+ {
47
+ "name": "rank5_indices_scalar_updates",
48
+ "inputs": {
49
+ "data": {
50
+ "dtype": "float32",
51
+ "shape": [2, 3],
52
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 10.0, 11.0, 12.0] }
53
+ },
54
+ "indices": { "dtype": "int32", "shape": [1, 1, 1, 1, 2], "data": { "kind": "values", "values": [1, 2] } },
55
+ "updates": { "dtype": "float32", "shape": [1, 1, 1, 1], "data": { "kind": "values", "values": [99.0] } }
56
+ },
57
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 3], "tolerance": 0.000001 } }
58
+ },
59
+ {
60
+ "name": "rank6_indices_scalar_updates",
61
+ "inputs": {
62
+ "data": {
63
+ "dtype": "float32",
64
+ "shape": [3, 3],
65
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 10.0, 11.0, 12.0, 20.0, 21.0, 22.0] }
66
+ },
67
+ "indices": {
68
+ "dtype": "int32",
69
+ "shape": [1, 1, 1, 1, 2, 2],
70
+ "data": { "kind": "values", "values": [0, 2, 2, 0] }
71
+ },
72
+ "updates": {
73
+ "dtype": "float32",
74
+ "shape": [1, 1, 1, 1, 2],
75
+ "data": { "kind": "values", "values": [99.0, 77.0] }
76
+ }
77
+ },
78
+ "outputs": { "output": { "dtype": "float32", "shape": [3, 3], "tolerance": 0.000001 } }
79
+ },
80
+ {
81
+ "name": "rank6_full_depth_scalar_updates",
82
+ "inputs": {
83
+ "data": {
84
+ "dtype": "float32",
85
+ "shape": [1, 2, 1, 2, 1, 3],
86
+ "data": {
87
+ "kind": "values",
88
+ "values": [0.0, 1.0, 2.0, 10.0, 11.0, 12.0, 100.0, 101.0, 102.0, 110.0, 111.0, 112.0]
89
+ }
90
+ },
91
+ "indices": {
92
+ "dtype": "int32",
93
+ "shape": [2, 6],
94
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 2, 0, 1, 0, 1, 0, 0] }
95
+ },
96
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [900.0, 901.0] } }
97
+ },
98
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 3], "tolerance": 0.000001 } }
99
+ },
100
+ {
101
+ "name": "reduction_add_duplicate_indices",
102
+ "attrs": { "reduction": "add" },
103
+ "inputs": {
104
+ "data": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [10.0, 20.0, 30.0] } },
105
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [1, 1, 2] } },
106
+ "updates": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [2.0, 3.0, 4.0] } }
107
+ },
108
+ "outputs": { "output": { "dtype": "float32", "shape": [3], "tolerance": 0.000001 } }
109
+ },
110
+ {
111
+ "name": "reduction_add_subnormal_duplicate_indices_gpu_gap",
112
+ "skipGpu": {
113
+ "category": "permanent",
114
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal values required by this fixture. Backend evidence: Metal flushes denormals to zero in the ALU, so this reduction's subnormal sum/product result is flushed on GPU; permanent FTZ limitation."
115
+ },
116
+ "provenance": {
117
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
118
+ "test": "ScatterNDOpTest.ScatterND_18_add",
119
+ "notes": "Duplicate-index add reduction with finite subnormal scalar updates; valid tiny updates must accumulate to nonzero subnormal outputs."
120
+ },
121
+ "attrs": { "reduction": "add" },
122
+ "inputs": {
123
+ "data": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0] } },
124
+ "indices": { "dtype": "int32", "shape": [4, 1], "data": { "kind": "values", "values": [1, 1, 3, 3] } },
125
+ "updates": {
126
+ "dtype": "float32",
127
+ "shape": [4],
128
+ "data": { "kind": "values", "values": [1e-40, 1e-40, -1e-40, -1e-40] }
129
+ }
130
+ },
131
+ "outputs": { "output": { "dtype": "float32", "shape": [4], "tolerance": 1e-44 } }
132
+ },
133
+ {
134
+ "name": "reduction_mul_normal_inputs_subnormal_product_gpu_gap",
135
+ "skipGpu": {
136
+ "category": "permanent",
137
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal values required by this fixture. Backend evidence: Metal flushes denormals to zero in the ALU, so this reduction's subnormal sum/product result is flushed on GPU; permanent FTZ limitation."
138
+ },
139
+ "provenance": {
140
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
141
+ "test": "ScatterNDOpTest.ScatterND_18_mul",
142
+ "notes": "Duplicate-index mul reduction where tiny normal updates produce a finite subnormal product."
143
+ },
144
+ "attrs": { "reduction": "mul" },
145
+ "inputs": {
146
+ "data": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.0, 1.0, 0.0, 0.0] } },
147
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [1, 1] } },
148
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [1e-20, 1e-20] } }
149
+ },
150
+ "outputs": { "output": { "dtype": "float32", "shape": [4], "tolerance": 1e-44 } }
151
+ },
152
+ {
153
+ "name": "rank6_indices_reduction_add_duplicate_indices",
154
+ "attrs": { "reduction": "add" },
155
+ "inputs": {
156
+ "data": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [10.0, 20.0, 30.0] } },
157
+ "indices": { "dtype": "int32", "shape": [1, 1, 1, 1, 3, 1], "data": { "kind": "values", "values": [1, 1, 2] } },
158
+ "updates": {
159
+ "dtype": "float32",
160
+ "shape": [1, 1, 1, 1, 3],
161
+ "data": { "kind": "values", "values": [2.0, 3.0, 4.0] }
162
+ }
163
+ },
164
+ "outputs": { "output": { "dtype": "float32", "shape": [3], "tolerance": 0.000001 } }
165
+ },
166
+ {
167
+ "name": "rank6_reduction_add_duplicate_indices",
168
+ "attrs": { "reduction": "add" },
169
+ "inputs": {
170
+ "data": {
171
+ "dtype": "float32",
172
+ "shape": [1, 1, 1, 1, 1, 4],
173
+ "data": { "kind": "values", "values": [0.0, 10.0, 20.0, 30.0] }
174
+ },
175
+ "indices": {
176
+ "dtype": "int32",
177
+ "shape": [3, 6],
178
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3] }
179
+ },
180
+ "updates": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [2.0, 3.0, 4.0] } }
181
+ },
182
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 1, 1, 1, 4], "tolerance": 0.000001 } }
183
+ },
184
+ {
185
+ "name": "backend_row_slice_updates",
186
+ "inputs": {
187
+ "data": {
188
+ "dtype": "float32",
189
+ "shape": [4, 4, 4],
190
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
191
+ },
192
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 2] } },
193
+ "updates": {
194
+ "dtype": "float32",
195
+ "shape": [2, 4, 4],
196
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
197
+ }
198
+ },
199
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4], "tolerance": 0.000001 } }
200
+ },
201
+ {
202
+ "name": "backend_row_slice_reduction_add_duplicate",
203
+ "attrs": { "reduction": "add" },
204
+ "inputs": {
205
+ "data": {
206
+ "dtype": "float32",
207
+ "shape": [4, 4, 4],
208
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
209
+ },
210
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
211
+ "updates": {
212
+ "dtype": "float32",
213
+ "shape": [2, 4, 4],
214
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
215
+ }
216
+ },
217
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4], "tolerance": 0.000001 } }
218
+ },
219
+ {
220
+ "name": "ort_slice_float_rank2",
221
+ "provenance": {
222
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
223
+ "test": "ScatterNDOpTest.ScatterND_slice_float_int64_t"
224
+ },
225
+ "inputs": {
226
+ "data": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [0.0, 0.1, 0.1, 0.1] } },
227
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [1, 0] } },
228
+ "updates": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [0.2, 0.3, 0.0, 0.1] } }
229
+ },
230
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.000001 } }
231
+ },
232
+ {
233
+ "name": "ort_3tensor_int16_slice_update",
234
+ "provenance": {
235
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
236
+ "test": "ScatterNDOpTest.ScatterND_3tensor_int64"
237
+ },
238
+ "inputs": {
239
+ "data": {
240
+ "dtype": "int16",
241
+ "shape": [2, 2, 2],
242
+ "data": { "kind": "values", "values": [0, 1, 2, 3, 0, 1, 2, 3] }
243
+ },
244
+ "indices": { "dtype": "int32", "shape": [1, 1], "data": { "kind": "values", "values": [1] } },
245
+ "updates": { "dtype": "int16", "shape": [1, 2, 2], "data": { "kind": "values", "values": [4, 5, 6, 7] } }
246
+ },
247
+ "outputs": { "output": { "dtype": "int16", "shape": [2, 2, 2], "tolerance": 0 } }
248
+ },
249
+ {
250
+ "name": "ort_batched_index_float_adapted",
251
+ "provenance": {
252
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
253
+ "test": "ScatterNDOpTest.ScatterND_batched_index_int64",
254
+ "notes": "ORT uses int64 data; this framework covers the same batched-index pattern with float32 data."
255
+ },
256
+ "inputs": {
257
+ "data": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [2.0, 3.0, 2.0, 3.0] } },
258
+ "indices": { "dtype": "int32", "shape": [2, 1, 2], "data": { "kind": "values", "values": [0, 0, 0, 1] } },
259
+ "updates": { "dtype": "float32", "shape": [2, 1], "data": { "kind": "values", "values": [0.0, 1.0] } }
260
+ },
261
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.000001 } }
262
+ },
263
+ {
264
+ "name": "ort_batched_index_bool",
265
+ "provenance": {
266
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
267
+ "test": "ScatterNDOpTest.ScatterND_batched_index_bool_int64"
268
+ },
269
+ "inputs": {
270
+ "data": { "dtype": "bool", "shape": [2, 2], "data": { "kind": "values", "values": [0, 1, 0, 1] } },
271
+ "indices": { "dtype": "int32", "shape": [2, 1, 2], "data": { "kind": "values", "values": [0, 0, 0, 1] } },
272
+ "updates": { "dtype": "bool", "shape": [2, 1], "data": { "kind": "values", "values": [1, 0] } }
273
+ },
274
+ "outputs": {
275
+ "output": {
276
+ "dtype": "bool",
277
+ "shape": [2, 2],
278
+ "data": { "kind": "values", "values": [1, 0, 0, 1] },
279
+ "tolerance": 0
280
+ }
281
+ }
282
+ },
283
+ {
284
+ "name": "ort_sliced_index_float_adapted",
285
+ "provenance": {
286
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
287
+ "test": "ScatterNDOpTest.ScatterND_sliced_index_int64",
288
+ "notes": "ORT uses int64 data; this framework covers the same sliced-index update pattern with float32 data."
289
+ },
290
+ "inputs": {
291
+ "data": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "constant", "value": 0.0 } },
292
+ "indices": { "dtype": "int32", "shape": [2, 1, 1], "data": { "kind": "values", "values": [1, 0] } },
293
+ "updates": {
294
+ "dtype": "float32",
295
+ "shape": [2, 1, 2],
296
+ "data": { "kind": "values", "values": [2.0, 3.0, 0.0, 1.0] }
297
+ }
298
+ },
299
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.000001 } }
300
+ },
301
+ {
302
+ "name": "ort_uint32_batched_index_depth2",
303
+ "provenance": {
304
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
305
+ "test": "ScatterNDOpTest.ScatterND_batched_3tensor_int64",
306
+ "notes": "Compact uint32 batched ScatterND fixture using the same depth-2 index pattern as ORT."
307
+ },
308
+ "inputs": {
309
+ "data": {
310
+ "dtype": "uint32",
311
+ "shape": [2, 2, 2],
312
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0, 0, 0] }
313
+ },
314
+ "indices": {
315
+ "dtype": "int32",
316
+ "shape": [2, 2, 2],
317
+ "data": { "kind": "values", "values": [0, 1, 1, 0, 0, 0, 1, 1] }
318
+ },
319
+ "updates": {
320
+ "dtype": "uint32",
321
+ "shape": [2, 2, 2],
322
+ "data": { "kind": "values", "values": [2, 3, 4, 5, 0, 1, 6, 7] }
323
+ }
324
+ },
325
+ "outputs": { "output": { "dtype": "uint32", "shape": [2, 2, 2], "tolerance": 0 } }
326
+ },
327
+ {
328
+ "name": "ort_int8_negative_indices_scalar_updates",
329
+ "provenance": {
330
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
331
+ "test": "ScatterNDOpTest.ScatterND_3tensor_int64",
332
+ "notes": "Compact signed-byte scalar-update fixture with negative indices and edge payload values."
333
+ },
334
+ "inputs": {
335
+ "data": {
336
+ "dtype": "int8",
337
+ "shape": [2, 2, 2],
338
+ "data": { "kind": "values", "values": [0, 0, 2, 3, 4, 0, 6, 7] }
339
+ },
340
+ "indices": { "dtype": "int32", "shape": [2, 3], "data": { "kind": "values", "values": [0, 0, 1, -1, 0, -1] } },
341
+ "updates": { "dtype": "int8", "shape": [2], "data": { "kind": "values", "values": [-128, 127] } }
342
+ },
343
+ "outputs": { "output": { "dtype": "int8", "shape": [2, 2, 2], "tolerance": 0 } }
344
+ },
345
+ {
346
+ "name": "empty_indices_zero_width_update_identity",
347
+ "provenance": {
348
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
349
+ "test": "ScatterNDOpTest.ScatterND_empty_indices",
350
+ "notes": "Valid empty-index ScatterND identity where both the data suffix and update suffix have zero width."
351
+ },
352
+ "inputs": {
353
+ "data": { "dtype": "float32", "shape": [2, 0], "data": { "kind": "values", "values": [] } },
354
+ "indices": { "dtype": "int32", "shape": [0, 1], "data": { "kind": "values", "values": [] } },
355
+ "updates": { "dtype": "float32", "shape": [0, 0], "data": { "kind": "values", "values": [] } }
356
+ },
357
+ "outputs": {
358
+ "output": { "dtype": "float32", "shape": [2, 0], "tolerance": 0, "data": { "kind": "values", "values": [] } }
359
+ }
360
+ },
361
+ {
362
+ "name": "empty_indices_zero_width_reduction_add_identity",
363
+ "provenance": {
364
+ "notes": "Reduction twin of the zero-width identity above. The slice route floors its inner width at one so a zero-width data suffix cannot divide by zero, and only a reduction case reaches that route."
365
+ },
366
+ "attrs": { "reduction": "add" },
367
+ "inputs": {
368
+ "data": { "dtype": "float32", "shape": [2, 0], "data": { "kind": "values", "values": [] } },
369
+ "indices": { "dtype": "int32", "shape": [0, 1], "data": { "kind": "values", "values": [] } },
370
+ "updates": { "dtype": "float32", "shape": [0, 0], "data": { "kind": "values", "values": [] } }
371
+ },
372
+ "outputs": {
373
+ "output": { "dtype": "float32", "shape": [2, 0], "tolerance": 0, "data": { "kind": "values", "values": [] } }
374
+ }
375
+ },
376
+ {
377
+ "name": "ort_empty_indices_identity",
378
+ "provenance": {
379
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
380
+ "test": "ScatterNDOpTest.ScatterND_empty_indices"
381
+ },
382
+ "inputs": {
383
+ "data": {
384
+ "dtype": "float32",
385
+ "shape": [2, 3],
386
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] }
387
+ },
388
+ "indices": { "dtype": "int32", "shape": [0, 1], "data": { "kind": "values", "values": [] } },
389
+ "updates": { "dtype": "float32", "shape": [0, 3], "data": { "kind": "values", "values": [] } }
390
+ },
391
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 3], "tolerance": 0 } }
392
+ },
393
+ {
394
+ "name": "ort_reduction_add_slice_duplicates",
395
+ "provenance": {
396
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
397
+ "test": "ScatterNDOpTest.ScatterND_18_add"
398
+ },
399
+ "attrs": { "reduction": "add" },
400
+ "inputs": {
401
+ "data": {
402
+ "dtype": "float32",
403
+ "shape": [2, 2, 3],
404
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
405
+ },
406
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 1, 0] } },
407
+ "updates": {
408
+ "dtype": "float32",
409
+ "shape": [3, 2, 3],
410
+ "data": {
411
+ "kind": "values",
412
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
413
+ }
414
+ }
415
+ },
416
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3], "tolerance": 0.01 } }
417
+ },
418
+ {
419
+ "name": "ort_reduction_mul_slice_duplicates",
420
+ "provenance": {
421
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
422
+ "test": "ScatterNDOpTest.ScatterND_18_mul"
423
+ },
424
+ "attrs": { "reduction": "mul" },
425
+ "inputs": {
426
+ "data": {
427
+ "dtype": "float32",
428
+ "shape": [2, 2, 3],
429
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
430
+ },
431
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 1, 0] } },
432
+ "updates": {
433
+ "dtype": "float32",
434
+ "shape": [3, 2, 3],
435
+ "data": {
436
+ "kind": "values",
437
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
438
+ }
439
+ }
440
+ },
441
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3], "tolerance": 0.01 } }
442
+ },
443
+ {
444
+ "name": "ort_reduction_min_slice_duplicates",
445
+ "provenance": {
446
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
447
+ "test": "ScatterNDOpTest.ScatterND_18_min"
448
+ },
449
+ "attrs": { "reduction": "min" },
450
+ "inputs": {
451
+ "data": {
452
+ "dtype": "float32",
453
+ "shape": [2, 2, 3],
454
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
455
+ },
456
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 1, 0] } },
457
+ "updates": {
458
+ "dtype": "float32",
459
+ "shape": [3, 2, 3],
460
+ "data": {
461
+ "kind": "values",
462
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
463
+ }
464
+ }
465
+ },
466
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3], "tolerance": 0.000001 } }
467
+ },
468
+ {
469
+ "name": "ort_reduction_max_slice_duplicates",
470
+ "provenance": {
471
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
472
+ "test": "ScatterNDOpTest.ScatterND_18_max"
473
+ },
474
+ "attrs": { "reduction": "max" },
475
+ "inputs": {
476
+ "data": {
477
+ "dtype": "float32",
478
+ "shape": [2, 2, 3],
479
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
480
+ },
481
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 1, 0] } },
482
+ "updates": {
483
+ "dtype": "float32",
484
+ "shape": [3, 2, 3],
485
+ "data": {
486
+ "kind": "values",
487
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
488
+ }
489
+ }
490
+ },
491
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3], "tolerance": 0.000001 } }
492
+ },
493
+ {
494
+ "name": "ort_int32_negative_indices_matrix",
495
+ "provenance": {
496
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
497
+ "test": "ScatterNDOpTest.ScatterND_matrice_int64_int64_neg_indices",
498
+ "notes": "ORT uses int64 data and indices; this framework covers the same negative-index pattern with float32 data and int32 index storage."
499
+ },
500
+ "inputs": {
501
+ "data": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [1.0, 1.0, 2.0, 2.0] } },
502
+ "indices": { "dtype": "int32", "shape": [2, 2], "data": { "kind": "values", "values": [0, 0, -1, -1] } },
503
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.0, 3.0] } }
504
+ },
505
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.000001 } }
506
+ },
507
+ {
508
+ "name": "ort_int32_negative_indices_slice_update_rank3",
509
+ "provenance": {
510
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
511
+ "test": "ScatterNDOpTest.ScatterND_3tensor_int64",
512
+ "notes": "ORT uses integer data; this framework covers the same negative-index slice update with float32 data and int32 index storage."
513
+ },
514
+ "inputs": {
515
+ "data": {
516
+ "dtype": "float32",
517
+ "shape": [2, 2, 2],
518
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] }
519
+ },
520
+ "indices": { "dtype": "int32", "shape": [2, 2], "data": { "kind": "values", "values": [0, -1, -1, 0] } },
521
+ "updates": {
522
+ "dtype": "float32",
523
+ "shape": [2, 2],
524
+ "data": { "kind": "values", "values": [20.0, 21.0, 30.0, 31.0] }
525
+ }
526
+ },
527
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 2], "tolerance": 0.000001 } }
528
+ },
529
+ {
530
+ "name": "ort_reduction_mul_rank6_slice_duplicates",
531
+ "provenance": {
532
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
533
+ "test": "ScatterNDOpTest.ScatterND_18_mul_long_shape",
534
+ "notes": "Same duplicate-index reduction pattern as ORT's long-shape case, constrained to this manifest's rank-6 limit."
535
+ },
536
+ "attrs": { "reduction": "mul" },
537
+ "inputs": {
538
+ "data": {
539
+ "dtype": "float32",
540
+ "shape": [2, 2, 3, 1, 1, 1],
541
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
542
+ },
543
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 1, 0] } },
544
+ "updates": {
545
+ "dtype": "float32",
546
+ "shape": [3, 2, 3, 1, 1, 1],
547
+ "data": {
548
+ "kind": "values",
549
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
550
+ }
551
+ }
552
+ },
553
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3, 1, 1, 1], "tolerance": 0.01 } }
554
+ },
555
+ {
556
+ "name": "ort_reduction_min_updates_win_duplicate_indices",
557
+ "provenance": {
558
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
559
+ "test": "ScatterNDOpTest.ScatterND_18_min",
560
+ "notes": "Uses ORT's reduction=min semantics with values chosen so both data and duplicate updates affect the answer."
561
+ },
562
+ "attrs": { "reduction": "min" },
563
+ "inputs": {
564
+ "data": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [5.0, -1.0, 3.0] } },
565
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
566
+ "updates": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [2.0, -4.0, 10.0] } }
567
+ },
568
+ "outputs": { "output": { "dtype": "float32", "shape": [3], "tolerance": 0.000001 } }
569
+ },
570
+ {
571
+ "name": "ort_reduction_max_updates_win_duplicate_indices",
572
+ "provenance": {
573
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
574
+ "test": "ScatterNDOpTest.ScatterND_18_max",
575
+ "notes": "Uses ORT's reduction=max semantics with values chosen so both data and duplicate updates affect the answer."
576
+ },
577
+ "attrs": { "reduction": "max" },
578
+ "inputs": {
579
+ "data": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-5.0, 1.0, 3.0] } },
580
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
581
+ "updates": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-2.0, 4.0, 2.0] } }
582
+ },
583
+ "outputs": { "output": { "dtype": "float32", "shape": [3], "tolerance": 0.000001 } }
584
+ },
585
+ {
586
+ "name": "onnx_backend_scatternd",
587
+ "inputs": {
588
+ "data": {
589
+ "dtype": "float32",
590
+ "shape": [4, 4, 4],
591
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
592
+ },
593
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 2] } },
594
+ "updates": {
595
+ "dtype": "float32",
596
+ "shape": [2, 4, 4],
597
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
598
+ }
599
+ },
600
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4] } },
601
+ "provenance": {
602
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_scatternd",
603
+ "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
604
+ }
605
+ },
606
+ {
607
+ "name": "onnx_backend_scatternd_add",
608
+ "attrs": { "reduction": "add" },
609
+ "inputs": {
610
+ "data": {
611
+ "dtype": "float32",
612
+ "shape": [4, 4, 4],
613
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
614
+ },
615
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
616
+ "updates": {
617
+ "dtype": "float32",
618
+ "shape": [2, 4, 4],
619
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
620
+ }
621
+ },
622
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4] } },
623
+ "provenance": {
624
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_scatternd_add",
625
+ "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
626
+ }
627
+ },
628
+ {
629
+ "name": "onnx_backend_scatternd_max",
630
+ "attrs": { "reduction": "max" },
631
+ "inputs": {
632
+ "data": {
633
+ "dtype": "float32",
634
+ "shape": [4, 4, 4],
635
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
636
+ },
637
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
638
+ "updates": {
639
+ "dtype": "float32",
640
+ "shape": [2, 4, 4],
641
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
642
+ }
643
+ },
644
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4] } },
645
+ "provenance": {
646
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_scatternd_max",
647
+ "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
648
+ }
649
+ },
650
+ {
651
+ "name": "onnx_backend_scatternd_min",
652
+ "attrs": { "reduction": "min" },
653
+ "inputs": {
654
+ "data": {
655
+ "dtype": "float32",
656
+ "shape": [4, 4, 4],
657
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
658
+ },
659
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
660
+ "updates": {
661
+ "dtype": "float32",
662
+ "shape": [2, 4, 4],
663
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
664
+ }
665
+ },
666
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4] } },
667
+ "provenance": {
668
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_scatternd_min",
669
+ "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
670
+ }
671
+ },
672
+ {
673
+ "name": "onnx_backend_scatternd_multiply",
674
+ "attrs": { "reduction": "mul" },
675
+ "inputs": {
676
+ "data": {
677
+ "dtype": "float32",
678
+ "shape": [4, 4, 4],
679
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/scatter_nd_data_4x4x4" } }
680
+ },
681
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
682
+ "updates": {
683
+ "dtype": "float32",
684
+ "shape": [2, 4, 4],
685
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/backend_row_slice_updates_input_updates" } }
686
+ }
687
+ },
688
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 4, 4] } },
689
+ "provenance": {
690
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_scatternd_multiply",
691
+ "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
692
+ }
693
+ },
694
+ {
695
+ "name": "ort_int32_slice_updates_negative_indices_exact_above_float24",
696
+ "provenance": {
697
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
698
+ "test": "ScatterNDOpTest.ScatterND_3tensor_int64",
699
+ "notes": "Native int32 projection of ORT's integer slice-update case with negative indices and values above f32's exact integer range."
700
+ },
701
+ "inputs": {
702
+ "data": {
703
+ "dtype": "int32",
704
+ "shape": [2, 2, 2],
705
+ "data": { "kind": "values", "values": [16777217, 1, 2, 3, 4, 5, -16777217, 7] }
706
+ },
707
+ "indices": { "dtype": "int32", "shape": [2, 2], "data": { "kind": "values", "values": [0, 1, -1, 0] } },
708
+ "updates": {
709
+ "dtype": "int32",
710
+ "shape": [2, 2],
711
+ "data": { "kind": "values", "values": [20000001, -20000001, 30000003, -30000003] }
712
+ }
713
+ },
714
+ "outputs": { "output": { "dtype": "int32", "shape": [2, 2, 2] } }
715
+ },
716
+ {
717
+ "name": "int32_reduction_add_exact_above_float24",
718
+ "provenance": {
719
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
720
+ "test": "ScatterNDOpTest.ScatterND_18_add",
721
+ "notes": "Integer projection of ORT's add-reduction coverage using values above f32's exact integer range."
722
+ },
723
+ "attrs": { "reduction": "add" },
724
+ "inputs": {
725
+ "data": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [0, 0] } },
726
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
727
+ "updates": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [16777217, 1] } }
728
+ },
729
+ "outputs": { "output": { "dtype": "int32", "shape": [2] } }
730
+ },
731
+ {
732
+ "name": "int32_reduction_min_duplicate_indices_negative",
733
+ "attrs": { "reduction": "min" },
734
+ "inputs": {
735
+ "data": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [5, -7, 3] } },
736
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
737
+ "updates": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [-2, 4, 10] } }
738
+ },
739
+ "outputs": { "output": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [-2, -7, 3] } } }
740
+ },
741
+ {
742
+ "name": "int32_reduction_max_duplicate_indices",
743
+ "attrs": { "reduction": "max" },
744
+ "inputs": {
745
+ "data": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [-5, -7, 3] } },
746
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
747
+ "updates": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [-2, 4, 1] } }
748
+ },
749
+ "outputs": { "output": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [4, -7, 3] } } }
750
+ },
751
+ {
752
+ "name": "int32_reduction_mul_duplicate_indices",
753
+ "attrs": { "reduction": "mul" },
754
+ "inputs": {
755
+ "data": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [2, 3, -1] } },
756
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
757
+ "updates": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [3, 5, -4] } }
758
+ },
759
+ "outputs": { "output": { "dtype": "int32", "shape": [3], "data": { "kind": "values", "values": [30, 3, 4] } } }
760
+ },
761
+ {
762
+ "name": "uint32_reduction_add_duplicate_indices",
763
+ "attrs": { "reduction": "add" },
764
+ "inputs": {
765
+ "data": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [1, 2] } },
766
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 0] } },
767
+ "updates": { "dtype": "uint32", "shape": [3], "data": { "kind": "values", "values": [2, 3, 4] } }
768
+ },
769
+ "outputs": { "output": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [10, 2] } } }
770
+ },
771
+ {
772
+ "name": "uint32_reduction_min_duplicate_indices",
773
+ "attrs": { "reduction": "min" },
774
+ "inputs": {
775
+ "data": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [8, 1] } },
776
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 1] } },
777
+ "updates": { "dtype": "uint32", "shape": [3], "data": { "kind": "values", "values": [5, 3, 9] } }
778
+ },
779
+ "outputs": { "output": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [3, 1] } } }
780
+ },
781
+ {
782
+ "name": "uint32_reduction_max_duplicate_indices",
783
+ "attrs": { "reduction": "max" },
784
+ "inputs": {
785
+ "data": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [1, 7] } },
786
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 1] } },
787
+ "updates": { "dtype": "uint32", "shape": [3], "data": { "kind": "values", "values": [5, 9, 2] } }
788
+ },
789
+ "outputs": { "output": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [9, 7] } } }
790
+ },
791
+ {
792
+ "name": "uint32_reduction_mul_duplicate_indices",
793
+ "attrs": { "reduction": "mul" },
794
+ "inputs": {
795
+ "data": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [3, 5] } },
796
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 0] } },
797
+ "updates": { "dtype": "uint32", "shape": [3], "data": { "kind": "values", "values": [2, 3, 4] } }
798
+ },
799
+ "outputs": { "output": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [72, 5] } } }
800
+ },
801
+ {
802
+ "name": "reduction_add_f32_high_contention_4096",
803
+ "attrs": { "reduction": "add" },
804
+ "inputs": {
805
+ "data": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.5, 0.0, 0.0, 0.0] } },
806
+ "indices": { "dtype": "int32", "shape": [4096, 1], "data": { "kind": "constant", "value": 0 } },
807
+ "updates": { "dtype": "float32", "shape": [4096], "data": { "kind": "constant", "value": 0.25 } }
808
+ },
809
+ "outputs": {
810
+ "output": {
811
+ "dtype": "float32",
812
+ "shape": [4],
813
+ "data": { "kind": "values", "values": [1024.5, 0.0, 0.0, 0.0] },
814
+ "tolerance": 0.000001
815
+ }
816
+ }
817
+ },
818
+ {
819
+ "name": "reduction_min_f32_high_contention_2048",
820
+ "attrs": { "reduction": "min" },
821
+ "inputs": {
822
+ "data": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [1000.0, -3.0] } },
823
+ "indices": { "dtype": "int32", "shape": [2048, 1], "data": { "kind": "constant", "value": 0 } },
824
+ "updates": {
825
+ "dtype": "float32",
826
+ "shape": [2048],
827
+ "data": { "kind": "cycle", "values": [9.0, 4.0, 7.0, 2.5, 8.0] }
828
+ }
829
+ },
830
+ "outputs": {
831
+ "output": {
832
+ "dtype": "float32",
833
+ "shape": [2],
834
+ "data": { "kind": "values", "values": [2.5, -3.0] },
835
+ "tolerance": 0.000001
836
+ }
837
+ }
838
+ },
839
+ {
840
+ "name": "ort_reduction_add_slice_negative_indices",
841
+ "provenance": {
842
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
843
+ "test": "ScatterNDOpTest.ScatterND_18_add",
844
+ "notes": "Same duplicate slice-add reduction as ORT, with signed negative indices that normalize to rows 0, 1, 0."
845
+ },
846
+ "attrs": { "reduction": "add" },
847
+ "inputs": {
848
+ "data": {
849
+ "dtype": "float32",
850
+ "shape": [2, 2, 3],
851
+ "data": { "kind": "values", "values": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] }
852
+ },
853
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [-2, -1, -2] } },
854
+ "updates": {
855
+ "dtype": "float32",
856
+ "shape": [3, 2, 3],
857
+ "data": {
858
+ "kind": "values",
859
+ "values": { "$ref": "#/fixtureArrays/ort_reduction_add_slice_duplicates_input_updates" }
860
+ }
861
+ }
862
+ },
863
+ "outputs": {
864
+ "output": {
865
+ "dtype": "float32",
866
+ "shape": [2, 2, 3],
867
+ "tolerance": 0.01,
868
+ "data": {
869
+ "kind": "values",
870
+ "values": [8194.1, 16388.1, 32776.1, 65552.1, 131104.1, 262208.1, 128.1, 256.1, 512.1, 1024.1, 2048.1, 4096.1]
871
+ }
872
+ }
873
+ }
874
+ },
875
+ {
876
+ "name": "reduction_mul_f32_high_contention_1024",
877
+ "attrs": { "reduction": "mul" },
878
+ "inputs": {
879
+ "data": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [7.0, 2.0] } },
880
+ "indices": { "dtype": "int32", "shape": [1024, 1], "data": { "kind": "constant", "value": 0 } },
881
+ "updates": { "dtype": "float32", "shape": [1024], "data": { "kind": "constant", "value": 1.0 } }
882
+ },
883
+ "outputs": {
884
+ "output": {
885
+ "dtype": "float32",
886
+ "shape": [2],
887
+ "data": { "kind": "values", "values": [7.0, 2.0] },
888
+ "tolerance": 0.000001
889
+ }
890
+ }
891
+ },
892
+ {
893
+ "name": "f16_reduction_add_row_slice_duplicate_indices",
894
+ "attrs": { "reduction": "add" },
895
+ "inputs": {
896
+ "data": {
897
+ "dtype": "float16",
898
+ "shape": [4, 4],
899
+ "data": {
900
+ "kind": "values",
901
+ "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
902
+ }
903
+ },
904
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
905
+ "updates": {
906
+ "dtype": "float16",
907
+ "shape": [3, 4],
908
+ "data": {
909
+ "kind": "values",
910
+ "values": [0.5, 1.5, 2.5, 3.5, 10.0, 20.0, 30.0, 40.0, 100.0, 200.0, 300.0, 400.0]
911
+ }
912
+ }
913
+ },
914
+ "outputs": { "output": { "dtype": "float16", "shape": [4, 4], "tolerance": 0.01 } }
915
+ },
916
+ {
917
+ "name": "f16_reduction_mul_scalar_updates_duplicate_indices",
918
+ "attrs": { "reduction": "mul" },
919
+ "inputs": {
920
+ "data": { "dtype": "float16", "shape": [4], "data": { "kind": "values", "values": [2.0, 1.0, 3.0, 1.0] } },
921
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
922
+ "updates": { "dtype": "float16", "shape": [3], "data": { "kind": "values", "values": [1.5, 2.0, 4.0] } }
923
+ },
924
+ "outputs": { "output": { "dtype": "float16", "shape": [4], "tolerance": 0.01 } }
925
+ },
926
+ {
927
+ "name": "f16_reduction_max_row_slice_duplicate_indices",
928
+ "attrs": { "reduction": "max" },
929
+ "inputs": {
930
+ "data": {
931
+ "dtype": "float16",
932
+ "shape": [3, 4],
933
+ "data": { "kind": "values", "values": [1.0, -1.0, 0.5, 2.0, -3.0, -3.0, -3.0, -3.0, 0.0, 0.0, 0.0, 0.0] }
934
+ },
935
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 1] } },
936
+ "updates": {
937
+ "dtype": "float16",
938
+ "shape": [3, 4],
939
+ "data": { "kind": "values", "values": [0.0, -2.0, 1.5, 1.0, -0.5, 4.0, -1.0, 3.0, -1.0, -2.0, -1.0, -2.0] }
940
+ }
941
+ },
942
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.001 } }
943
+ },
944
+ {
945
+ "name": "int8_reduction_add_scalar_updates_duplicate_indices_in_range",
946
+ "provenance": {
947
+ "source": "onnxruntime/test/providers/cpu/tensor/scatter_nd_op_test.cc",
948
+ "test": "ScatterNDOpTest.ScatterND_18_add",
949
+ "notes": "int8 add reduction with duplicate indices kept within int8 range (no overflow/wrap), exercising the untested serial f32-cast int8 reduction branch."
950
+ },
951
+ "attrs": { "reduction": "add" },
952
+ "inputs": {
953
+ "data": { "dtype": "int8", "shape": [4], "data": { "kind": "values", "values": [10, 0, -20, 0] } },
954
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 0, 2] } },
955
+ "updates": { "dtype": "int8", "shape": [3], "data": { "kind": "values", "values": [5, 7, -30] } }
956
+ },
957
+ "outputs": { "output": { "dtype": "int8", "shape": [4], "tolerance": 0 } }
958
+ },
959
+ {
960
+ "name": "batch0_non_vec4_aligned_inner_row",
961
+ "inputs": {
962
+ "data": { "dtype": "float32", "shape": [6, 7], "data": { "kind": "linspace", "start": 0.0, "end": 41.0 } },
963
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 4, 5] } },
964
+ "updates": {
965
+ "dtype": "float32",
966
+ "shape": [3, 7],
967
+ "data": {
968
+ "kind": "values",
969
+ "values": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 200.0, 201.0, 202.0, 203.0, 204.0, 205.0, 206.0, 300.0, 301.0, 302.0, 303.0, 304.0, 305.0, 306.0]
970
+ }
971
+ }
972
+ },
973
+ "outputs": { "output": { "dtype": "float32", "shape": [6, 7] } }
974
+ },
975
+ {
976
+ "name": "reduction_slice_f16_multi_outer_batch_duplicate_depth2",
977
+ "attrs": { "reduction": "add" },
978
+ "inputs": {
979
+ "data": {
980
+ "dtype": "float16",
981
+ "shape": [3, 4],
982
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0] }
983
+ },
984
+ "indices": {
985
+ "dtype": "int32",
986
+ "shape": [2, 2, 2],
987
+ "data": { "kind": "values", "values": [0, 1, 0, 2, 1, 0, 1, 3] }
988
+ },
989
+ "updates": {
990
+ "dtype": "float16",
991
+ "shape": [2, 2],
992
+ "data": { "kind": "values", "values": [10.0, 20.0, 30.0, 40.0] }
993
+ }
994
+ },
995
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.1 } }
996
+ },
997
+ {
998
+ "name": "row_fast_vec4_last_row_edge_large_axis_dim",
999
+ "inputs": {
1000
+ "data": { "dtype": "float32", "shape": [65537, 8], "data": { "kind": "constant", "value": 0.0 } },
1001
+ "indices": { "dtype": "int32", "shape": [3, 1], "data": { "kind": "values", "values": [0, 65536, 32768] } },
1002
+ "updates": {
1003
+ "dtype": "float32",
1004
+ "shape": [3, 8],
1005
+ "data": { "kind": "cycle", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] }
1006
+ }
1007
+ },
1008
+ "outputs": { "output": { "dtype": "float32", "shape": [65537, 8] } }
1009
+ },
1010
+ {
1011
+ "name": "reduction_add_single_hot_slot_many_updates",
1012
+ "provenance": {
1013
+ "notes": "Compact semantic sibling for the hot-slot atomic benchmark: many duplicate scalar updates target one output slot and must accumulate deterministically against the CPU reference."
1014
+ },
1015
+ "attrs": { "reduction": "add" },
1016
+ "inputs": {
1017
+ "data": {
1018
+ "dtype": "float32",
1019
+ "shape": [8],
1020
+ "data": { "kind": "values", "values": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0] }
1021
+ },
1022
+ "indices": {
1023
+ "dtype": "int32",
1024
+ "shape": [16, 1],
1025
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }
1026
+ },
1027
+ "updates": {
1028
+ "dtype": "float32",
1029
+ "shape": [16],
1030
+ "data": {
1031
+ "kind": "values",
1032
+ "values": [1.0, -2.0, 3.0, -4.0, 5.0, -6.0, 7.0, -8.0, 9.0, -10.0, 11.0, -12.0, 13.0, -14.0, 15.0, -16.0]
1033
+ }
1034
+ }
1035
+ },
1036
+ "outputs": { "output": { "dtype": "float32", "shape": [8], "tolerance": 0.000001 } }
1037
+ },
1038
+ {
1039
+ "name": "scalar_output_add_many_updates_parallel_reduce",
1040
+ "provenance": {
1041
+ "notes": "Correctness lock for scalar accumulation with int32 indices, where every valid index targets the sole output element."
1042
+ },
1043
+ "attrs": { "reduction": "add" },
1044
+ "inputs": {
1045
+ "data": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [3.5] } },
1046
+ "indices": { "dtype": "int32", "shape": [1024, 1], "data": { "kind": "constant", "value": 0 } },
1047
+ "updates": {
1048
+ "dtype": "float32",
1049
+ "shape": [1024],
1050
+ "data": { "kind": "cycle", "values": [0.25, -0.5, 1.0, -0.125, 0.0625, 0.75, -0.25, 0.5] }
1051
+ }
1052
+ },
1053
+ "outputs": { "output": { "dtype": "float32", "shape": [1], "tolerance": 0.001, "relTolerance": 0.0001 } }
1054
+ },
1055
+ {
1056
+ "name": "scalar_output_add_many_updates_parallel_reduce_i32_negative",
1057
+ "provenance": {
1058
+ "notes": "Negative indices on the same route. ONNX admits them and normalizeGatherIndex wraps them, so -1 addresses the sole output element exactly as 0 does; alternating the two makes a route that drops one of the spellings differ from the reference rather than merely halving the sum."
1059
+ },
1060
+ "attrs": { "reduction": "add" },
1061
+ "inputs": {
1062
+ "data": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [3.5] } },
1063
+ "indices": { "dtype": "int32", "shape": [1024, 1], "data": { "kind": "cycle", "values": [0, -1] } },
1064
+ "updates": {
1065
+ "dtype": "float32",
1066
+ "shape": [1024],
1067
+ "data": { "kind": "cycle", "values": [0.25, -0.5, 1.0, -0.125, 0.0625, 0.75, -0.25, 0.5] }
1068
+ }
1069
+ },
1070
+ "outputs": { "output": { "dtype": "float32", "shape": [1], "tolerance": 0.001, "relTolerance": 0.0001 } }
1071
+ },
1072
+ {
1073
+ "name": "reduction_add_padding_hotspot_many_updates",
1074
+ "provenance": {
1075
+ "notes": "Correctness sibling for sparse-routing and padding-token workloads where many updates target one hot key in a much larger output table."
1076
+ },
1077
+ "attrs": { "reduction": "add" },
1078
+ "inputs": {
1079
+ "data": { "dtype": "float32", "shape": [128], "data": { "kind": "constant", "value": 0.0 } },
1080
+ "indices": { "dtype": "int32", "shape": [2048, 1], "data": { "kind": "constant", "value": 0 } },
1081
+ "updates": { "dtype": "float32", "shape": [2048], "data": { "kind": "constant", "value": 1.0 } }
1082
+ },
1083
+ "outputs": { "output": { "dtype": "float32", "shape": [128], "tolerance": 0.001, "relTolerance": 0.0001 } }
1084
+ },
1085
+ {
1086
+ "name": "f16_reduction_add_sparse_router_gradient",
1087
+ "provenance": {
1088
+ "notes": "Correctness lock for f16 scalar sparse-gradient accumulation through the f32 atomic scratch path."
1089
+ },
1090
+ "attrs": { "reduction": "add" },
1091
+ "inputs": {
1092
+ "data": { "dtype": "float16", "shape": [64], "data": { "kind": "constant", "value": 0.0 } },
1093
+ "indices": {
1094
+ "dtype": "int32",
1095
+ "shape": [4096, 1],
1096
+ "data": {
1097
+ "kind": "cycle",
1098
+ "values": { "$ref": "#/fixtureArrays/f16_reduction_add_sparse_router_gradient_input_indices" }
1099
+ }
1100
+ },
1101
+ "updates": {
1102
+ "dtype": "float16",
1103
+ "shape": [4096],
1104
+ "data": { "kind": "fillFloat32", "sinStep": 0.031, "cosStep": 0.047, "scale": 0.01 }
1105
+ }
1106
+ },
1107
+ "outputs": { "output": { "dtype": "float16", "shape": [64], "tolerance": 0.1, "relTolerance": 0.02 } }
1108
+ },
1109
+ {
1110
+ "name": "f16_reduction_add_sparse_router_gradient_offset_scale_lock",
1111
+ "provenance": {
1112
+ "notes": "Scale lock for f16 reduction-add through atomic f32 scratch. Each of 64 slots receives 64 updates offset around +0.02, preventing zero-mean cancellation and making a lost, duplicated, or uniformly rescaled contribution observable. Tolerance covers the expected difference between per-add f16 rounding in the reference and one final f16 rounding after f32 scratch accumulation."
1113
+ },
1114
+ "attrs": { "reduction": "add" },
1115
+ "inputs": {
1116
+ "data": { "dtype": "float16", "shape": [64], "data": { "kind": "constant", "value": 0.0 } },
1117
+ "indices": {
1118
+ "dtype": "int32",
1119
+ "shape": [4096, 1],
1120
+ "data": {
1121
+ "kind": "cycle",
1122
+ "values": { "$ref": "#/fixtureArrays/f16_reduction_add_sparse_router_gradient_input_indices" }
1123
+ }
1124
+ },
1125
+ "updates": {
1126
+ "dtype": "float16",
1127
+ "shape": [4096],
1128
+ "data": { "kind": "fillFloat32", "sinStep": 0.031, "cosStep": 0.047, "scale": 0.01, "offset": 0.02 }
1129
+ }
1130
+ },
1131
+ "outputs": { "output": { "dtype": "float16", "shape": [64], "tolerance": 0.012, "relTolerance": 0.004 } }
1132
+ },
1133
+ {
1134
+ "name": "f16_reduction_add_row_slice_packed_atomic_route",
1135
+ "provenance": {
1136
+ "notes": "Correctness lock for direct packed-f16 CAS row-slice accumulation. Duplicate rows and adjacent columns contend on both halves of each output word."
1137
+ },
1138
+ "requires": { "features": ["shader-f16"] },
1139
+ "attrs": { "reduction": "add" },
1140
+ "inputs": {
1141
+ "data": { "dtype": "float16", "shape": [16, 4], "data": { "kind": "constant", "value": 0.0 } },
1142
+ "indices": {
1143
+ "dtype": "int32",
1144
+ "shape": [4096, 1],
1145
+ "data": { "kind": "cycle", "values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }
1146
+ },
1147
+ "updates": { "dtype": "float16", "shape": [4096, 4], "data": { "kind": "constant", "value": 1.0 } }
1148
+ },
1149
+ "outputs": { "output": { "dtype": "float16", "shape": [16, 4], "tolerance": 0.01 } }
1150
+ },
1151
+ {
1152
+ "name": "f16_reduction_add_rank3_batched_indices_packed_atomic",
1153
+ "provenance": {
1154
+ "notes": "Rank-3 batched indices through the direct packed-f16 CAS route, where indexDepth is less than rank(data). indices[2,8,1] pairs ascending rows in batch 0 with descending rows in batch 1, so dropping the interior index stride changes the update multiplicities. Data and updates are quarter-integers whose periods are coprime with both row and batch strides; every partial sum stays exactly representable in f16 regardless of atomic retirement order, allowing zero tolerance."
1155
+ },
1156
+ "requires": { "features": ["shader-f16"] },
1157
+ "attrs": { "reduction": "add" },
1158
+ "inputs": {
1159
+ "data": {
1160
+ "dtype": "float16",
1161
+ "shape": [8, 256],
1162
+ "data": { "kind": "cycle", "values": [-1.0, 0.5, -0.25, 2.0, 0.0, -1.5, 1.25] }
1163
+ },
1164
+ "indices": {
1165
+ "dtype": "int32",
1166
+ "shape": [2, 8, 1],
1167
+ "data": { "kind": "values", "values": [0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 0] }
1168
+ },
1169
+ "updates": {
1170
+ "dtype": "float16",
1171
+ "shape": [2, 8, 256],
1172
+ "data": { "kind": "cycle", "values": [0.25, 1.5, 0.75, 2.0, 1.25] }
1173
+ }
1174
+ },
1175
+ "outputs": { "output": { "dtype": "float16", "shape": [8, 256], "tolerance": 0 } }
1176
+ },
1177
+ {
1178
+ "name": "f16_reduction_max_full_depth_atomic_f32_route",
1179
+ "provenance": {
1180
+ "notes": "Correctness lock for the generalized full-index-depth f16 scratch-atomic route. Four thousand duplicate scalar updates contend on one destination, and max remains exact regardless of atomic order."
1181
+ },
1182
+ "attrs": { "reduction": "max" },
1183
+ "inputs": {
1184
+ "data": { "dtype": "float16", "shape": [16384], "data": { "kind": "constant", "value": 0.0 } },
1185
+ "indices": { "dtype": "int32", "shape": [4096, 1], "data": { "kind": "constant", "value": 7 } },
1186
+ "updates": { "dtype": "float16", "shape": [4096], "data": { "kind": "cycle", "values": [-3.0, 2.0, 1.0, 4.0] } }
1187
+ },
1188
+ "outputs": { "output": { "dtype": "float16", "shape": [16384], "tolerance": 0 } }
1189
+ },
1190
+ {
1191
+ "name": "rank7_scalar_updates",
1192
+ "inputs": {
1193
+ "data": { "dtype": "float32", "shape": [1, 1, 1, 1, 1, 2, 3], "data": { "kind": "constant", "value": 0.0 } },
1194
+ "indices": {
1195
+ "dtype": "int32",
1196
+ "shape": [2, 7],
1197
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0] }
1198
+ },
1199
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [9.0, 7.0] } }
1200
+ },
1201
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 1, 1, 1, 2, 3], "tolerance": 0 } }
1202
+ },
1203
+ {
1204
+ "name": "rank8_scalar_updates",
1205
+ "inputs": {
1206
+ "data": {
1207
+ "dtype": "float32",
1208
+ "shape": [1, 1, 1, 1, 1, 1, 2, 3],
1209
+ "data": { "kind": "linspace", "start": 1.0, "end": 6.0 }
1210
+ },
1211
+ "indices": {
1212
+ "dtype": "int32",
1213
+ "shape": [2, 8],
1214
+ "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 1] }
1215
+ },
1216
+ "updates": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [9.0, 7.0] } }
1217
+ },
1218
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 1, 1, 1, 1, 2, 3], "tolerance": 0 } }
1219
+ },
1220
+ {
1221
+ "name": "ort_int8_reduction_mul_wrap8",
1222
+ "provenance": {
1223
+ "source": "ONNX Runtime CPUExecutionProvider",
1224
+ "notes": "The existing INT8 route previously accumulated the physical i32 product without narrowing it."
1225
+ },
1226
+ "attrs": { "reduction": "mul" },
1227
+ "inputs": {
1228
+ "data": { "dtype": "int8", "shape": [2], "data": { "kind": "values", "values": [100, -7] } },
1229
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
1230
+ "updates": { "dtype": "int8", "shape": [2], "data": { "kind": "values", "values": [20, 20] } }
1231
+ },
1232
+ "outputs": {
1233
+ "output": { "dtype": "int8", "shape": [2], "tolerance": 0, "data": { "kind": "values", "values": [64, -7] } }
1234
+ }
1235
+ },
1236
+ {
1237
+ "name": "ort_uint8_reduction_add_wrap8",
1238
+ "provenance": {
1239
+ "source": "ONNX Runtime CPUExecutionProvider",
1240
+ "notes": "Covers standard UINT8 payload support and modulo-256 add reduction."
1241
+ },
1242
+ "attrs": { "reduction": "add" },
1243
+ "inputs": {
1244
+ "data": { "dtype": "uint8", "shape": [2], "data": { "kind": "values", "values": [250, 7] } },
1245
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
1246
+ "updates": { "dtype": "uint8", "shape": [2], "data": { "kind": "values", "values": [10, 10] } }
1247
+ },
1248
+ "outputs": {
1249
+ "output": { "dtype": "uint8", "shape": [2], "tolerance": 0, "data": { "kind": "values", "values": [14, 7] } }
1250
+ }
1251
+ },
1252
+ {
1253
+ "name": "ort_int16_reduction_mul_wrap16",
1254
+ "provenance": {
1255
+ "source": "ONNX Runtime CPUExecutionProvider",
1256
+ "notes": "Duplicate signed updates lock 16-bit two's-complement narrowing after multiplication."
1257
+ },
1258
+ "attrs": { "reduction": "mul" },
1259
+ "inputs": {
1260
+ "data": { "dtype": "int16", "shape": [2], "data": { "kind": "values", "values": [20000, -7] } },
1261
+ "indices": { "dtype": "int32", "shape": [2, 1], "data": { "kind": "values", "values": [0, 0] } },
1262
+ "updates": { "dtype": "int16", "shape": [2], "data": { "kind": "values", "values": [2, 2] } }
1263
+ },
1264
+ "outputs": {
1265
+ "output": {
1266
+ "dtype": "int16",
1267
+ "shape": [2],
1268
+ "tolerance": 0,
1269
+ "data": { "kind": "values", "values": [14464, -7] }
1270
+ }
1271
+ }
1272
+ },
1273
+ {
1274
+ "name": "int8_reduction_add_high_contention_wrap",
1275
+ "provenance": {
1276
+ "notes": "3000 updates over 4 slots, every slot wrapping several times. The parallel narrow-int route reduces on the raw two's-complement bits and narrows once at the end; this pins that against the reference's narrow-every-step arithmetic at a contention level a two-update fixture cannot reach. Index and update cycles share a period so each slot's total is distinct, which also catches a mispaired index/update lane."
1277
+ },
1278
+ "attrs": { "reduction": "add" },
1279
+ "inputs": {
1280
+ "data": { "dtype": "int8", "shape": [4], "data": { "kind": "values", "values": [7, -100, 0, 3] } },
1281
+ "indices": { "dtype": "int32", "shape": [3000, 1], "data": { "kind": "cycle", "values": [0, 1, 2, 3] } },
1282
+ "updates": { "dtype": "int8", "shape": [3000], "data": { "kind": "cycle", "values": [5, -3, 1, 2] } }
1283
+ },
1284
+ "outputs": { "output": { "dtype": "int8", "shape": [4], "tolerance": 0 } }
1285
+ },
1286
+ {
1287
+ "name": "int8_reduction_min_duplicate_indices",
1288
+ "provenance": {
1289
+ "notes": "Signed narrow min reduces through a native atomicMin on the i32-widened output. The update cycle length shares a factor with the index cycle so each slot sees a disjoint half of the values and the two slots reduce to different results, which a mispaired index/update lane would not reproduce; one slot is left untouched by any update."
1290
+ },
1291
+ "attrs": { "reduction": "min" },
1292
+ "inputs": {
1293
+ "data": { "dtype": "int8", "shape": [3], "data": { "kind": "values", "values": [100, -5, 42] } },
1294
+ "indices": { "dtype": "int32", "shape": [512, 1], "data": { "kind": "cycle", "values": [0, 1] } },
1295
+ "updates": { "dtype": "int8", "shape": [512], "data": { "kind": "cycle", "values": [9, -128, 7, 3] } }
1296
+ },
1297
+ "outputs": { "output": { "dtype": "int8", "shape": [3], "tolerance": 0 } }
1298
+ },
1299
+ {
1300
+ "name": "uint8_reduction_max_duplicate_indices",
1301
+ "provenance": {
1302
+ "notes": "Unsigned narrow max reduces through a native atomicMax on the u32-widened output. Each slot sees a disjoint half of the update cycle, so the two slots reduce to different results and one of them is decided by the data rather than by an update."
1303
+ },
1304
+ "attrs": { "reduction": "max" },
1305
+ "inputs": {
1306
+ "data": { "dtype": "uint8", "shape": [3], "data": { "kind": "values", "values": [0, 200, 17] } },
1307
+ "indices": { "dtype": "int32", "shape": [512, 1], "data": { "kind": "cycle", "values": [0, 1] } },
1308
+ "updates": { "dtype": "uint8", "shape": [512], "data": { "kind": "cycle", "values": [3, 255, 9, 1] } }
1309
+ },
1310
+ "outputs": { "output": { "dtype": "uint8", "shape": [3], "tolerance": 0 } }
1311
+ },
1312
+ {
1313
+ "name": "int16_reduction_max_duplicate_indices",
1314
+ "provenance": {
1315
+ "notes": "16-bit signed max over the same i32-widened atomic path as int8, at values no 8-bit fixture can represent."
1316
+ },
1317
+ "attrs": { "reduction": "max" },
1318
+ "inputs": {
1319
+ "data": { "dtype": "int16", "shape": [3], "data": { "kind": "values", "values": [-30000, 1, -7] } },
1320
+ "indices": { "dtype": "int32", "shape": [512, 1], "data": { "kind": "cycle", "values": [0, 1] } },
1321
+ "updates": { "dtype": "int16", "shape": [512], "data": { "kind": "cycle", "values": [-20000, 32767, -1, 5] } }
1322
+ },
1323
+ "outputs": { "output": { "dtype": "int16", "shape": [3], "tolerance": 0 } }
1324
+ }
1325
+ ]
1326
+ }