Xenova HF Staff commited on
Commit
fca5e7d
·
verified ·
1 Parent(s): a0b9936

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,17 +18,17 @@ See the [ONNX `TensorScatter` spec](https://onnx.ai/onnx/operators/onnx__TensorS
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- | --- |
23
- | `past_cache` | `past` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | Existing cache tensor with shape `(batch_size, ..., max_sequence_length, ...)`. | required |
24
- | `update` | `update` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | New values to scatter in, with the same shape as `past_cache` except the sequence dimension equals `sequence_length`. | required |
25
- | `write_indices` | `writeIndices` | `I` | `uint32` | `1` | — | Logical int64 per-sample write offset into the cache sequence dimension; shape `(batch_size,)`, stored as uint32 by WebGPU, and assumed all zeros if absent. | optional |
26
 
27
  ## Outputs
28
 
29
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
30
  | --- | --- | --- | --- | --- | --- | --- |
31
- | `present_cache` | `present` | `T` | same as `past_cache` | same as `past_cache` | Updated cache; same shape as `past_cache`. | required |
32
 
33
  ## Attributes
34
 
@@ -48,7 +48,7 @@ Default values (overridable per request):
48
 
49
  ## Files
50
 
51
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -57,10 +57,14 @@ Default values (overridable per request):
57
 
58
  ## Use with `@huggingface/kernels`
59
 
60
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
61
- It then allocates the result tensors automatically.
 
 
 
62
 
63
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
64
 
65
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
66
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `past` | `past_cache` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | Existing cache tensor with shape `(batch_size, ..., max_sequence_length, ...)`. | required |
24
+ | `update` | | `T` | runtime-selected; narrow integers and bool use 32-bit slots | — | — | New values to scatter in, with the same shape as `past_cache` except the sequence dimension equals `sequence_length`. | required |
25
+ | `writeIndices` | `write_indices` | `I` | `uint32` | `1` | — | Logical int64 per-sample write offset into the cache sequence dimension; shape `(batch_size,)`, stored as uint32 by WebGPU, and assumed all zeros if absent. | optional |
26
 
27
  ## Outputs
28
 
29
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
30
  | --- | --- | --- | --- | --- | --- | --- |
31
+ | `present` | `present_cache` | `T` | same as `past` | same as `past` | Updated cache; same shape as `past_cache`. | required |
32
 
33
  ## Attributes
34
 
 
48
 
49
  ## Files
50
 
51
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
57
 
58
  ## Use with `@huggingface/kernels`
59
 
60
+ ```sh
61
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
62
+ ```
63
+
64
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
65
 
66
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
67
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
68
 
69
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
70
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.TensorScatter",
3
  "cases": [
4
  {
5
  "name": "decode-kv-update-f32",
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "decode-kv-update-f32",
build/webgpu/manifest.json CHANGED
@@ -2,135 +2,39 @@
2
  "domain": "ai.onnx",
3
  "name": "TensorScatter",
4
  "sinceVersion": 24,
5
- "description": "Functionally updates a KV cache tensor by scattering an `update` tensor into the `past_cache` along a sequence axis, producing `present_cache` with the same shape. Each batch sample's update is written at the offset given by `write_indices` (zero if omitted), either linearly or in wrap-around `circular` fashion.",
6
- "inputs": [
7
- {
8
- "role": "past_cache",
9
- "dtype": "T",
10
- "description": "Existing cache tensor with shape `(batch_size, ..., max_sequence_length, ...)`."
11
- },
12
- {
13
- "role": "update",
14
- "dtype": "T",
15
- "description": "New values to scatter in, with the same shape as `past_cache` except the sequence dimension equals `sequence_length`."
16
- },
17
- {
18
- "role": "write_indices",
19
  "dtype": "I",
20
  "rank": 1,
21
  "optional": true,
22
- "description": "Logical int64 per-sample write offset into the cache sequence dimension; shape `(batch_size,)`, stored as uint32 by WebGPU, and assumed all zeros if absent."
23
- }
24
- ],
25
- "outputs": [
26
- {
27
- "role": "present_cache",
28
- "dtype": "T",
29
- "rank": "ranks.past_cache",
30
- "description": "Updated cache; same shape as `past_cache`.",
31
- "shape": "shapes.past_cache"
32
  }
33
- ],
34
- "attributes": { "axis": -2, "mode": "linear" },
35
- "attributeDescriptions": {
36
- "axis": "Sequence dimension of `past_cache` and `update`; cannot be 0 (the batch dimension). Default is `-2`.",
37
- "mode": "Write mode: `linear` requires `write_indices + sequence_length <= max_sequence_length`; `circular` wraps the write index modulo `max_sequence_length`."
38
  },
 
 
39
  "attributeConstraints": { "mode": { "values": ["linear", "circular"] } },
40
  "typeConstraints": {
41
  "T": ["float32", "float16", "int32", "int16", "int8", "uint32", "uint8", "bool"],
42
  "I": ["int64"]
43
  },
44
- "args": {
45
- "past": { "kind": "tensor", "semantic": "past_cache", "role": "input" },
46
- "update": { "kind": "tensor", "semantic": "update", "role": "input" },
47
- "writeIndices": {
48
- "kind": "tensor",
49
- "semantic": "write_indices",
50
- "role": "indices",
51
- "dtype": "uint32",
52
- "narrowing": "checked",
53
- "required": false
54
- },
55
- "present": { "kind": "tensor", "semantic": "present_cache", "role": "output" }
56
- },
57
- "tunables": { "WORKGROUP_SIZE": 256 },
58
- "derive": {},
59
- "bindingSets": {
60
- "copyCache": [
61
- {
62
- "name": "src",
63
- "arg": "past",
64
- "semantic": "past_cache",
65
- "buffer": { "type": "read-only-storage" },
66
- "elementType": "$copyElementType"
67
- },
68
- {
69
- "name": "dst",
70
- "arg": "present",
71
- "semantic": "present_cache",
72
- "buffer": { "type": "storage" },
73
- "elementType": "$copyElementType"
74
- }
75
- ],
76
- "contiguousUpdate": [
77
- {
78
- "name": "update",
79
- "arg": "update",
80
- "semantic": "update",
81
- "buffer": { "type": "read-only-storage" },
82
- "elementType": "$scalar"
83
- },
84
- {
85
- "name": "present",
86
- "arg": "present",
87
- "semantic": "present_cache",
88
- "buffer": { "type": "storage" },
89
- "elementType": "$scalar"
90
- },
91
- {
92
- "name": "params",
93
- "semantic": "kernel.params",
94
- "buffer": { "type": "uniform" },
95
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.update)" }] }
96
- }
97
- ],
98
- "indexedUpdate": [
99
- {
100
- "name": "update",
101
- "arg": "update",
102
- "semantic": "update",
103
- "buffer": { "type": "read-only-storage" },
104
- "elementType": "$scalar"
105
- },
106
- {
107
- "name": "writeIndices",
108
- "arg": "writeIndices",
109
- "semantic": "write_indices",
110
- "buffer": { "type": "read-only-storage" },
111
- "elementType": "u32"
112
- },
113
- {
114
- "name": "present",
115
- "arg": "present",
116
- "semantic": "present_cache",
117
- "buffer": { "type": "storage" },
118
- "elementType": "$scalar"
119
- },
120
- {
121
- "name": "params",
122
- "semantic": "kernel.params",
123
- "buffer": { "type": "uniform" },
124
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.update)" }] }
125
- }
126
- ]
127
  },
128
  "variants": [
129
  {
130
  "id": "no_write_indices",
131
  "priority": 0,
132
- "when": ["not present.writeIndices", "ranks.past == ranks.update", "ranks.past == ranks.present", "ranks.past >= 2", "(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) >= 1", "(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) < ranks.past", "numel(shapes.past) == numel(shapes.present)", "f16Ok(dtypes.T)"],
133
- "constants": {
134
  "scalar": "dtypes.T",
135
  "usesF16": "dtypes.T == \"f16\"",
136
  "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.present) % 4 == 0 else dtypes.T"
@@ -139,46 +43,50 @@
139
  {
140
  "id": "copy",
141
  "name": "TensorScatter.Copy",
142
- "source": {
143
- "shader": "scatter-flat-copy.wgsl.jinja",
144
- "inputs": {
145
- "count": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
146
- "copyWidth": "16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4)",
147
- "elementCount": "numel(shapes.present)",
148
- "scalarTailSafe": "numel(shapes.present) % 4 != 0"
149
- }
150
  },
151
- "bindings": "copyCache",
 
 
 
152
  "dispatch": {
153
- "gridStride": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
154
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
155
  },
156
  "viewAlias": [{ "input": "src", "output": "dst" }]
157
  },
158
  {
159
  "id": "scatter",
160
  "name": "TensorScatter.Scatter",
161
- "source": {
162
- "shader": "tensor-scatter.wgsl.jinja",
163
- "inputs": {
164
- "pastShape": "shapes.past",
165
- "updateShape": "shapes.update",
166
- "rank": "ranks.past",
167
- "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past",
168
- "circular": "attrs.mode == \"circular\"",
169
- "hasIndices": "false"
170
- }
171
  },
172
- "bindings": "contiguousUpdate",
173
- "dispatch": { "threads": "numel(shapes.update)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
174
  }
175
  ]
176
  },
177
  {
178
  "id": "with_write_indices",
179
  "priority": 20,
180
- "when": ["present.writeIndices", "dtypes.I == \"u32\"", "ranks.past == ranks.update", "ranks.past == ranks.present", "ranks.past >= 2", "(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) >= 1", "(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) < ranks.past", "numel(shapes.past) == numel(shapes.present)", "f16Ok(dtypes.T)"],
181
- "constants": {
182
  "scalar": "dtypes.T",
183
  "usesF16": "dtypes.T == \"f16\"",
184
  "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.present) % 4 == 0 else dtypes.T"
@@ -187,38 +95,42 @@
187
  {
188
  "id": "copy",
189
  "name": "TensorScatter.Copy",
190
- "source": {
191
- "shader": "scatter-flat-copy.wgsl.jinja",
192
- "inputs": {
193
- "count": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
194
- "copyWidth": "16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4)",
195
- "elementCount": "numel(shapes.present)",
196
- "scalarTailSafe": "numel(shapes.present) % 4 != 0"
197
- }
198
  },
199
- "bindings": "copyCache",
 
 
 
200
  "dispatch": {
201
- "gridStride": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
202
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
203
  },
204
  "viewAlias": [{ "input": "src", "output": "dst" }]
205
  },
206
  {
207
  "id": "scatter",
208
  "name": "TensorScatter.Scatter",
209
- "source": {
210
- "shader": "tensor-scatter.wgsl.jinja",
211
- "inputs": {
212
- "pastShape": "shapes.past",
213
- "updateShape": "shapes.update",
214
- "rank": "ranks.past",
215
- "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past",
216
- "circular": "attrs.mode == \"circular\"",
217
- "hasIndices": "true"
218
- }
219
  },
220
- "bindings": "indexedUpdate",
221
- "dispatch": { "threads": "numel(shapes.update)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
222
  }
223
  ]
224
  }
 
2
  "domain": "ai.onnx",
3
  "name": "TensorScatter",
4
  "sinceVersion": 24,
5
+ "inputs": {
6
+ "past": { "onnx": "past_cache", "dtype": "T" },
7
+ "update": { "dtype": "T" },
8
+ "writeIndices": {
9
+ "onnx": "write_indices",
 
 
 
 
 
 
 
 
 
10
  "dtype": "I",
11
  "rank": 1,
12
  "optional": true,
13
+ "storage": "uint32",
14
+ "narrowing": "checked"
 
 
 
 
 
 
 
 
15
  }
 
 
 
 
 
16
  },
17
+ "outputs": { "present": { "onnx": "present_cache", "dtype": "T", "rank": "ranks.past", "shape": "shapes.past" } },
18
+ "attributes": { "axis": { "default": -2 }, "mode": { "default": "linear" } },
19
  "attributeConstraints": { "mode": { "values": ["linear", "circular"] } },
20
  "typeConstraints": {
21
  "T": ["float32", "float16", "int32", "int16", "int8", "uint32", "uint8", "bool"],
22
  "I": ["int64"]
23
  },
24
+ "tunables": { "WORKGROUP_SIZE": { "default": 256 } },
25
+ "when": ["(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) < ranks.past", "(attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past) >= 1", "f16Ok(dtypes.T)", "numel(shapes.past) == numel(shapes.present)", "ranks.past == ranks.present", "ranks.past == ranks.update", "ranks.past >= 2"],
26
+ "bindings": {
27
+ "update": { "buffer": "read-only-storage", "elementType": "$scalar" },
28
+ "present": { "buffer": "storage", "elementType": "$scalar" },
29
+ "params": { "buffer": "uniform", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.update)" }] },
30
+ "writeIndices": { "buffer": "read-only-storage", "elementType": "u32" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  },
32
  "variants": [
33
  {
34
  "id": "no_write_indices",
35
  "priority": 0,
36
+ "when": ["not present.writeIndices"],
37
+ "derive": {
38
  "scalar": "dtypes.T",
39
  "usesF16": "dtypes.T == \"f16\"",
40
  "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.present) % 4 == 0 else dtypes.T"
 
43
  {
44
  "id": "copy",
45
  "name": "TensorScatter.Copy",
46
+ "shader": "scatter-flat-copy.wgsl.jinja",
47
+ "derive": {
48
+ "count": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
49
+ "copyWidth": "16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4)",
50
+ "elementCount": "numel(shapes.present)",
51
+ "scalarTailSafe": "numel(shapes.present) % 4 != 0"
 
 
52
  },
53
+ "bindings": [
54
+ { "arg": "past", "name": "src", "elementType": "$copyElementType" },
55
+ { "arg": "present", "name": "dst", "elementType": "$copyElementType" }
56
+ ],
57
  "dispatch": {
58
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))), (tunables.WORKGROUP_SIZE)), 65535)",
59
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))), (tunables.WORKGROUP_SIZE)), 65535)",
60
+ "z": 1
61
  },
62
  "viewAlias": [{ "input": "src", "output": "dst" }]
63
  },
64
  {
65
  "id": "scatter",
66
  "name": "TensorScatter.Scatter",
67
+ "shader": "tensor-scatter.wgsl.jinja",
68
+ "derive": {
69
+ "pastShape": "shapes.past",
70
+ "updateShape": "shapes.update",
71
+ "rank": "ranks.past",
72
+ "axisSpec": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past",
73
+ "circular": "attrs.mode == \"circular\"",
74
+ "hasIndices": "false"
 
 
75
  },
76
+ "bindings": ["update", "present", "params"],
77
+ "dispatch": {
78
+ "x": "min(ceilDiv((numel(shapes.update)), (tunables.WORKGROUP_SIZE)), 65535)",
79
+ "y": "ceilDiv(ceilDiv((numel(shapes.update)), (tunables.WORKGROUP_SIZE)), 65535)",
80
+ "z": 1
81
+ }
82
  }
83
  ]
84
  },
85
  {
86
  "id": "with_write_indices",
87
  "priority": 20,
88
+ "when": ["present.writeIndices", "dtypes.I == \"u32\""],
89
+ "derive": {
90
  "scalar": "dtypes.T",
91
  "usesF16": "dtypes.T == \"f16\"",
92
  "copyElementType": "(\"vec4<\" ~ dtypes.T ~ \">\") if numel(shapes.present) % 4 == 0 else dtypes.T"
 
95
  {
96
  "id": "copy",
97
  "name": "TensorScatter.Copy",
98
+ "shader": "scatter-flat-copy.wgsl.jinja",
99
+ "derive": {
100
+ "count": "ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))",
101
+ "copyWidth": "16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4)",
102
+ "elementCount": "numel(shapes.present)",
103
+ "scalarTailSafe": "numel(shapes.present) % 4 != 0"
 
 
104
  },
105
+ "bindings": [
106
+ { "arg": "past", "name": "src", "elementType": "$copyElementType" },
107
+ { "arg": "present", "name": "dst", "elementType": "$copyElementType" }
108
+ ],
109
  "dispatch": {
110
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))), (tunables.WORKGROUP_SIZE)), 65535)",
111
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.present), 16 if dtypes.T == \"f16\" and numel(shapes.present) % 4 != 0 else (8 if numel(shapes.present) % 4 != 0 else 4))), (tunables.WORKGROUP_SIZE)), 65535)",
112
+ "z": 1
113
  },
114
  "viewAlias": [{ "input": "src", "output": "dst" }]
115
  },
116
  {
117
  "id": "scatter",
118
  "name": "TensorScatter.Scatter",
119
+ "shader": "tensor-scatter.wgsl.jinja",
120
+ "derive": {
121
+ "pastShape": "shapes.past",
122
+ "updateShape": "shapes.update",
123
+ "rank": "ranks.past",
124
+ "axisSpec": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.past",
125
+ "circular": "attrs.mode == \"circular\"",
126
+ "hasIndices": "true"
 
 
127
  },
128
+ "bindings": ["update", "writeIndices", "present", "params"],
129
+ "dispatch": {
130
+ "x": "min(ceilDiv((numel(shapes.update)), (tunables.WORKGROUP_SIZE)), 65535)",
131
+ "y": "ceilDiv(ceilDiv((numel(shapes.update)), (tunables.WORKGROUP_SIZE)), 65535)",
132
+ "z": 1
133
+ }
134
  }
135
  ]
136
  }
build/webgpu/metadata.json CHANGED
@@ -1,19 +1,25 @@
1
  {
2
  "name": "ai.onnx.TensorScatter",
3
- "id": "_ai_onnx_tensorscatter_webgpu_d5b6d1c",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "27NgakY5cT0I8768syqpQg8+XMBWBQIPNUfS8FN4GHM=",
11
- "manifest.json": "hIT+BgOyanYiB3FJJjnjjheedXZaLszhHIe7buQVVAM=",
12
- "scatter-flat-copy.wgsl.jinja": "TcsxU7ecJvkki1Bo9K7kjXnovGgU7IKumZI6bDsxwUM=",
13
- "tensor-scatter.wgsl.jinja": "Ys/UVlAuBJYHUJ0DwrUbTj/T2+2JBokOnwPIqMO7P6U=",
14
- "test.json": "AiLV0oDQiO121/e1IpqXRfz1PNTcC9khxqUrBdw3/yY="
15
  }
16
  },
17
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
18
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.TensorScatter" }
 
 
 
 
 
 
19
  }
 
1
  {
2
  "name": "ai.onnx.TensorScatter",
3
+ "id": "_ai_onnx_tensorscatter_webgpu_fae7a8d",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "8pIi4/PPMqyk12pePykM6uwawjM0N6tsexFwiHFqySw=",
11
+ "manifest.json": "RY7RXbSuGDdCHSr8e4qqS5wG4oCULwzNV5C9/ToI6Ak=",
12
+ "scatter-flat-copy.wgsl.jinja": "mXcfAXV6Nxe+Y+cvem8Wwwt0BeqRQXaiCNhQoiynX0M=",
13
+ "tensor-scatter.wgsl.jinja": "cDtR3E7EOm41/YKXMPKfPP3YCakYj/pbU85wt2obnY0=",
14
+ "test.json": "H/VHqdsOkO3A3dM1LSKrAmk5XO2vlrDyry0EXKm/2X4="
15
  }
16
  },
17
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
18
+ "webgpu": {
19
+ "manifestSpec": "2.0",
20
+ "variants": {
21
+ "no_write_indices": ["scatter-flat-copy.wgsl.jinja", "tensor-scatter.wgsl.jinja"],
22
+ "with_write_indices": ["scatter-flat-copy.wgsl.jinja", "tensor-scatter.wgsl.jinja"]
23
+ }
24
+ }
25
  }
build/webgpu/scatter-flat-copy.wgsl.jinja CHANGED
@@ -2,46 +2,46 @@
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
- {% if source.scalarTailSafe %}
24
- let base = i * {{ source.copyWidth }}u;
25
- dst[base] = src[base];
26
- if (base + 1u < {{ source.elementCount }}u) { dst[base + 1u] = src[base + 1u]; }
27
- if (base + 2u < {{ source.elementCount }}u) { dst[base + 2u] = src[base + 2u]; }
28
- if (base + 3u < {{ source.elementCount }}u) { dst[base + 3u] = src[base + 3u]; }
29
- if (base + 4u < {{ source.elementCount }}u) { dst[base + 4u] = src[base + 4u]; }
30
- if (base + 5u < {{ source.elementCount }}u) { dst[base + 5u] = src[base + 5u]; }
31
- if (base + 6u < {{ source.elementCount }}u) { dst[base + 6u] = src[base + 6u]; }
32
- if (base + 7u < {{ source.elementCount }}u) { dst[base + 7u] = src[base + 7u]; }
33
- {% if source.copyWidth == 16 %}
34
- if (base + 8u < {{ source.elementCount }}u) { dst[base + 8u] = src[base + 8u]; }
35
- if (base + 9u < {{ source.elementCount }}u) { dst[base + 9u] = src[base + 9u]; }
36
- if (base + 10u < {{ source.elementCount }}u) { dst[base + 10u] = src[base + 10u]; }
37
- if (base + 11u < {{ source.elementCount }}u) { dst[base + 11u] = src[base + 11u]; }
38
- if (base + 12u < {{ source.elementCount }}u) { dst[base + 12u] = src[base + 12u]; }
39
- if (base + 13u < {{ source.elementCount }}u) { dst[base + 13u] = src[base + 13u]; }
40
- if (base + 14u < {{ source.elementCount }}u) { dst[base + 14u] = src[base + 14u]; }
41
- if (base + 15u < {{ source.elementCount }}u) { dst[base + 15u] = src[base + 15u]; }
42
  {% endif %}
43
  {% else %}
44
- dst[i] = src[i];
45
  {% endif %}
46
- }
47
  }
 
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.
6
+ {% set scalarTailSafe = scalarTailSafe is defined and scalarTailSafe %}
7
+ {% set copyWidth = copyWidth | default(0) %}
8
+ {% set elementCount = elementCount | default(0) %}
9
  {% if usesF16 %}
10
  enable f16;
11
  {% endif %}
12
  {{ env.wgsl.resourceDeclarations }}
13
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
14
 
15
+ const COUNT: u32 = {{ count }}u;
16
  const WG: u32 = {{ wg }}u;
17
 
18
  @compute @workgroup_size({{ wg }})
19
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
20
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
21
+ if (i >= COUNT) {
22
+ return;
23
+ }
24
+ {% if scalarTailSafe %}
25
+ let base = i * {{ copyWidth }}u;
26
+ dst[base] = src[base];
27
+ if (base + 1u < {{ elementCount }}u) { dst[base + 1u] = src[base + 1u]; }
28
+ if (base + 2u < {{ elementCount }}u) { dst[base + 2u] = src[base + 2u]; }
29
+ if (base + 3u < {{ elementCount }}u) { dst[base + 3u] = src[base + 3u]; }
30
+ if (base + 4u < {{ elementCount }}u) { dst[base + 4u] = src[base + 4u]; }
31
+ if (base + 5u < {{ elementCount }}u) { dst[base + 5u] = src[base + 5u]; }
32
+ if (base + 6u < {{ elementCount }}u) { dst[base + 6u] = src[base + 6u]; }
33
+ if (base + 7u < {{ elementCount }}u) { dst[base + 7u] = src[base + 7u]; }
34
+ {% if copyWidth == 16 %}
35
+ if (base + 8u < {{ elementCount }}u) { dst[base + 8u] = src[base + 8u]; }
36
+ if (base + 9u < {{ elementCount }}u) { dst[base + 9u] = src[base + 9u]; }
37
+ if (base + 10u < {{ elementCount }}u) { dst[base + 10u] = src[base + 10u]; }
38
+ if (base + 11u < {{ elementCount }}u) { dst[base + 11u] = src[base + 11u]; }
39
+ if (base + 12u < {{ elementCount }}u) { dst[base + 12u] = src[base + 12u]; }
40
+ if (base + 13u < {{ elementCount }}u) { dst[base + 13u] = src[base + 13u]; }
41
+ if (base + 14u < {{ elementCount }}u) { dst[base + 14u] = src[base + 14u]; }
42
+ if (base + 15u < {{ elementCount }}u) { dst[base + 15u] = src[base + 15u]; }
 
43
  {% endif %}
44
  {% else %}
45
+ dst[i] = src[i];
46
  {% endif %}
 
47
  }
build/webgpu/tensor-scatter.wgsl.jinja CHANGED
@@ -1,63 +1,60 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
34
- {% if usesF16 %}
35
- enable f16;
36
- {% endif %}
37
  {{ env.wgsl.resourceDeclarations }}
38
 
39
- {% set max_seq = source.pastShape[source.axis] %}
40
- {% set seq_len = source.updateShape[source.axis] %}
41
  {% set dim0_stride = namespace(value=1) %}
42
- {% for j in range(1, source.rank) %}{% set dim0_stride.value = dim0_stride.value * source.updateShape[j] %}{% endfor %}
43
  {% set axis_stride = namespace(value=1) %}
44
- {% for j in range(source.axis + 1, source.rank) %}{% set axis_stride.value = axis_stride.value * source.updateShape[j] %}{% endfor %}
45
 
46
  // Map a flat index into the `update` tensor onto a flat index into `present`,
47
  // using `cache_axis` as the sequence-axis coordinate.
48
  fn present_offset(update_index: u32, cache_axis: u32) -> u32 {
49
  var rem = update_index;
50
  var offset = 0u;
51
- {% for d in range(source.rank) %}
52
  {% set u_stride = namespace(value=1) %}
53
- {% for j in range(d + 1, source.rank) %}{% set u_stride.value = u_stride.value * source.updateShape[j] %}{% endfor %}
54
  {% set p_stride = namespace(value=1) %}
55
- {% for j in range(d + 1, source.rank) %}{% set p_stride.value = p_stride.value * source.pastShape[j] %}{% endfor %}
56
- {% if d != source.axis %}
57
  let coord{{ d }} = rem / {{ max(u_stride.value, 1) }}u;
58
  {% endif %}
59
  rem = rem % {{ max(u_stride.value, 1) }}u;
60
- {% if d == source.axis %}
61
  offset = offset + cache_axis * {{ p_stride.value }}u;
62
  {% else %}
63
  offset = offset + coord{{ d }} * {{ p_stride.value }}u;
@@ -67,21 +64,33 @@ fn present_offset(update_index: u32, cache_axis: u32) -> u32 {
67
  }
68
 
69
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
70
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
71
  {{ flat_index_2d("j") }}
72
  // batch_idx is the leading (dim-0) coordinate; s is the coordinate along the
73
  // sequence axis. write_indices is indexed per batch sample.
74
- {% if source.hasIndices %}
75
  let batch_idx = j / {{ max(dim0_stride.value, 1) }}u;
76
  {% endif %}
77
  let s = (j / {{ max(axis_stride.value, 1) }}u) % {{ max(seq_len, 1) }}u;
78
- {% if source.hasIndices %}
79
  let w = writeIndices[batch_idx];
80
  {% else %}
81
  let w = 0u;
82
  {% endif %}
83
- {% if source.circular %}
84
- let cache_axis = (w + s) % {{ max_seq }}u;
 
 
 
 
 
 
 
 
 
 
 
 
85
  {% else %}
86
  let cache_axis = w + s;
87
  {% endif %}
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
 
 
 
34
  {{ env.wgsl.resourceDeclarations }}
35
 
36
+ {% set max_seq = pastShape[axisSpec] %}
37
+ {% set seq_len = updateShape[axisSpec] %}
38
  {% set dim0_stride = namespace(value=1) %}
39
+ {% for j in range(1, rank) %}{% set dim0_stride.value = dim0_stride.value * updateShape[j] %}{% endfor %}
40
  {% set axis_stride = namespace(value=1) %}
41
+ {% for j in range(axisSpec + 1, rank) %}{% set axis_stride.value = axis_stride.value * updateShape[j] %}{% endfor %}
42
 
43
  // Map a flat index into the `update` tensor onto a flat index into `present`,
44
  // using `cache_axis` as the sequence-axis coordinate.
45
  fn present_offset(update_index: u32, cache_axis: u32) -> u32 {
46
  var rem = update_index;
47
  var offset = 0u;
48
+ {% for d in range(rank) %}
49
  {% set u_stride = namespace(value=1) %}
50
+ {% for j in range(d + 1, rank) %}{% set u_stride.value = u_stride.value * updateShape[j] %}{% endfor %}
51
  {% set p_stride = namespace(value=1) %}
52
+ {% for j in range(d + 1, rank) %}{% set p_stride.value = p_stride.value * pastShape[j] %}{% endfor %}
53
+ {% if d != axisSpec %}
54
  let coord{{ d }} = rem / {{ max(u_stride.value, 1) }}u;
55
  {% endif %}
56
  rem = rem % {{ max(u_stride.value, 1) }}u;
57
+ {% if d == axisSpec %}
58
  offset = offset + cache_axis * {{ p_stride.value }}u;
59
  {% else %}
60
  offset = offset + coord{{ d }} * {{ p_stride.value }}u;
 
64
  }
65
 
66
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
67
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
68
  {{ flat_index_2d("j") }}
69
  // batch_idx is the leading (dim-0) coordinate; s is the coordinate along the
70
  // sequence axis. write_indices is indexed per batch sample.
71
+ {% if hasIndices %}
72
  let batch_idx = j / {{ max(dim0_stride.value, 1) }}u;
73
  {% endif %}
74
  let s = (j / {{ max(axis_stride.value, 1) }}u) % {{ max(seq_len, 1) }}u;
75
+ {% if hasIndices %}
76
  let w = writeIndices[batch_idx];
77
  {% else %}
78
  let w = 0u;
79
  {% endif %}
80
+ {% if circular %}
81
+ {% if seq_len > max_seq %}
82
+ // More updates than cache slots: positions wrap onto each other and the
83
+ // operator's sequential order makes the LAST update to a slot win. Only the
84
+ // final max_seq updates can be that last writer; earlier ones stay silent so
85
+ // the result does not depend on invocation scheduling.
86
+ if (s + {{ max_seq }}u < {{ seq_len }}u) {
87
+ return;
88
+ }
89
+ {% endif %}
90
+ // Reduce both terms before adding them: a write index near the u32 ceiling
91
+ // makes the sum wrap, and a wrapped sum lands on a different slot than the
92
+ // one the operator names unless the capacity happens to divide 2^32.
93
+ let cache_axis = (w % {{ max_seq }}u + s % {{ max_seq }}u) % {{ max_seq }}u;
94
  {% else %}
95
  let cache_axis = w + s;
96
  {% endif %}
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.TensorScatter",
3
  "fixtureArrays": {
4
  "ort_linear_4d_spec_example_input_past": [1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 4, 3, 2, 1, 0],
5
  "ort_linear_3d_default_axis_input_past": [1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1],
@@ -45,7 +44,7 @@
45
  {
46
  "name": "decode_kv_update_f32_axis_minus2_compact",
47
  "provenance": {
48
- "notes": "Compact sibling for the decode-style TensorScatter benchmark; preserves axis=-2, per-batch write indices, full-cache copy, and one-token update rows."
49
  },
50
  "attrs": { "mode": "linear", "axis": -2 },
51
  "inputs": {
@@ -436,7 +435,7 @@
436
  "name": "ort_standard_int16_linear_extremes",
437
  "provenance": {
438
  "source": "onnxruntime/core/providers/cpu/llm/tensorscatter.cc",
439
- "notes": "ONNX TensorScatter-24 permits int16; both signed extrema guard the runtime's widened i32 copy-and-overwrite route."
440
  },
441
  "attrs": { "axis": 2, "mode": "linear" },
442
  "inputs": {
@@ -520,6 +519,54 @@
520
  "writeIndices": { "dtype": "uint32", "shape": [1], "data": { "kind": "values", "values": [1] } }
521
  },
522
  "outputs": { "present": { "dtype": "bool", "shape": [1, 1, 4], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  }
524
  ]
525
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_linear_4d_spec_example_input_past": [1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 4, 3, 2, 1, 0],
4
  "ort_linear_3d_default_axis_input_past": [1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 5, 4, 3, 2, 1],
 
44
  {
45
  "name": "decode_kv_update_f32_axis_minus2_compact",
46
  "provenance": {
47
+ "notes": "A compact decode-style TensorScatter uses axis=-2, per-batch write indices, a full-cache copy, and one-token update rows."
48
  },
49
  "attrs": { "mode": "linear", "axis": -2 },
50
  "inputs": {
 
435
  "name": "ort_standard_int16_linear_extremes",
436
  "provenance": {
437
  "source": "onnxruntime/core/providers/cpu/llm/tensorscatter.cc",
438
+ "notes": "ONNX TensorScatter-24 permits int16; both signed extrema verify logical int16 values stored in i32 slots during copy and overwrite."
439
  },
440
  "attrs": { "axis": 2, "mode": "linear" },
441
  "inputs": {
 
519
  "writeIndices": { "dtype": "uint32", "shape": [1], "data": { "kind": "values", "values": [1] } }
520
  },
521
  "outputs": { "present": { "dtype": "bool", "shape": [1, 1, 4], "tolerance": 0 } }
522
+ },
523
+ {
524
+ "name": "circular_update_longer_than_capacity_last_write_wins",
525
+ "provenance": {
526
+ "notes": "Six update rows enter a four-slot circular cache from write index 3: rows 0 and 4 both land on slot 3, while rows 1 and 5 land on slot 0. Sequence order makes the later row determine each collision, producing [[60,61],[30,31],[40,41],[50,51]] regardless of GPU scheduling."
527
+ },
528
+ "attrs": { "mode": "circular" },
529
+ "inputs": {
530
+ "past": { "dtype": "float32", "shape": [1, 4, 2], "data": { "kind": "constant", "value": 0.0 } },
531
+ "update": {
532
+ "dtype": "float32",
533
+ "shape": [1, 6, 2],
534
+ "data": {
535
+ "kind": "values",
536
+ "values": [10.0, 11.0, 20.0, 21.0, 30.0, 31.0, 40.0, 41.0, 50.0, 51.0, 60.0, 61.0]
537
+ }
538
+ },
539
+ "writeIndices": { "dtype": "uint32", "shape": [1], "data": { "kind": "values", "values": [3] } }
540
+ },
541
+ "outputs": { "present": { "dtype": "float32", "shape": [1, 4, 2], "tolerance": 0 } }
542
+ },
543
+ {
544
+ "name": "circular_write_index_at_u32_ceiling",
545
+ "provenance": {
546
+ "notes": "A circular cache reduces the write index modulo the capacity, and the sum of index and offset was formed before the reduction. At a write index near the u32 ceiling that sum wraps, so the second update lands on the slot the first one already took instead of the next one, and the two race. Slots 0 and 1 here are what int64 arithmetic names."
547
+ },
548
+ "attrs": { "mode": "circular" },
549
+ "inputs": {
550
+ "past": {
551
+ "dtype": "float32",
552
+ "shape": [1, 3, 2],
553
+ "data": { "kind": "values", "values": [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0] }
554
+ },
555
+ "update": {
556
+ "dtype": "float32",
557
+ "shape": [1, 2, 2],
558
+ "data": { "kind": "values", "values": [10.0, 11.0, 20.0, 21.0] }
559
+ },
560
+ "writeIndices": { "dtype": "uint32", "shape": [1], "data": { "kind": "values", "values": [4294967295] } }
561
+ },
562
+ "outputs": {
563
+ "present": {
564
+ "dtype": "float32",
565
+ "shape": [1, 3, 2],
566
+ "data": { "kind": "values", "values": [10.0, 11.0, 20.0, 21.0, -5.0, -6.0] },
567
+ "tolerance": 0
568
+ }
569
+ }
570
  }
571
  ]
572
  }