Xenova HF Staff commited on
Commit
9c596ce
·
verified ·
1 Parent(s): d50509e

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,16 +18,16 @@ See the [ONNX `Compress` spec](https://onnx.ai/onnx/operators/onnx__Compress.htm
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
- | --- | --- | --- | --- | --- | --- | --- |
23
- | `input` | `input` | `T` | — | — | Input tensor of rank `r >= 1` to select from. | required |
24
- | `condition` | `condition` | `C` | `1` | — | Rank-1 boolean mask indicating which slices or elements to select; may be shorter than the axis dimension, in which case trailing slices are discarded. | required |
25
 
26
  ## Outputs
27
 
28
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
29
- | --- | --- | --- | --- | --- | --- | --- |
30
- | `output` | `output` | `T` | derived | — | Selected slices with rank `r` when `axis` is specified, or rank 1 when the input is flattened. The selected dimension equals the number of true values in the inspected condition prefix. | required |
31
 
32
  ## Attributes
33
 
@@ -46,7 +46,7 @@ Attributes and default values (overridable per request):
46
 
47
  ## Files
48
 
49
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
50
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
51
  - [`test.json`](build/webgpu/test.json) — correctness cases
52
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -57,15 +57,18 @@ Attributes and default values (overridable per request):
57
 
58
  ## Use with `@huggingface/kernels`
59
 
60
- The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
 
 
61
 
62
- The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
63
 
64
- - `output`
65
 
66
- Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
67
 
68
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
69
 
70
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
71
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `input` | `T` | — | — | Input tensor of rank `r >= 1` to select from. | required |
24
+ | `condition` | `C` | `1` | — | Rank-1 boolean mask indicating which slices or elements to select; may be shorter than the axis dimension, in which case trailing slices are discarded. | required |
25
 
26
  ## Outputs
27
 
28
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
29
+ | --- | --- | --- | --- | --- | --- |
30
+ | `output` | `T` | derived | — | Selected slices with rank `r` when `axis` is specified, or rank 1 when the input is flattened. The selected dimension equals the number of true values in the inspected condition prefix. | required |
31
 
32
  ## Attributes
33
 
 
46
 
47
  ## Files
48
 
49
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
50
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
51
  - [`test.json`](build/webgpu/test.json) — correctness cases
52
  - [`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
+ Outputs with inferable metadata are allocated automatically. Explicit `outputs` entries request optional results or provide metadata that cannot be inferred from the supplied inputs and attributes.
65
 
66
+ This example supplies explicit metadata for:
67
 
68
+ - `output`
69
 
70
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
71
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
72
 
73
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
74
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Compress",
3
  "cases": [
4
  {
5
  "name": "compress-axis0-f32-64k-by-4",
@@ -99,6 +98,28 @@
99
  }
100
  ]
101
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
  ]
104
  }
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "compress-axis0-f32-64k-by-4",
 
98
  }
99
  ]
100
  }
101
+ },
102
+ {
103
+ "name": "compress-axis1-f32-short-condition-128-of-2048",
104
+ "preset": "edge",
105
+ "attrs": { "axis": 1 },
106
+ "inputs": {
107
+ "input": { "dtype": "float32", "shape": [4, 2048, 768], "dist": "normal", "seed": 915, "scale": 1 },
108
+ "condition": { "dtype": "bool", "shape": [128], "dist": "linearMod", "mod": 2 }
109
+ },
110
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 64, 768] } },
111
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(numel(shapes.output) * 2) * 4" }] }
112
+ },
113
+ {
114
+ "name": "compress-axis1-f32-full-condition-2048-control",
115
+ "preset": "edge",
116
+ "attrs": { "axis": 1 },
117
+ "inputs": {
118
+ "input": { "dtype": "float32", "shape": [4, 2048, 768], "dist": "normal", "seed": 915, "scale": 1 },
119
+ "condition": { "dtype": "bool", "shape": [2048], "dist": "linearMod", "mod": 2 }
120
+ },
121
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 1024, 768] } },
122
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(numel(shapes.output) * 2) * 4" }] }
123
  }
124
  ]
125
  }
build/webgpu/compress-scatter.wgsl.jinja CHANGED
@@ -4,31 +4,29 @@
4
  // lower bits gives an item's rank within its lane. The flattened route walks a
5
  // lane's ITEMS elements in order, so its output position advances without a
6
  // second lookup per element.
7
- {% if usesF16 %}
8
- enable f16;
9
- {% endif %}
10
  {{ env.wgsl.resourceDeclarations }}
11
 
12
  const WG: u32 = {{ workgroupSize }}u;
13
  const ITEMS: u32 = {{ scanItems }}u;
14
- {% if source.axisMode %}
15
  const BLOCK_ITEMS: u32 = WG * ITEMS;
16
  {% endif %}
17
 
18
  @compute @workgroup_size(WG)
19
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
20
- @builtin(num_workgroups) nwg: vec3<u32>,
21
  @builtin(local_invocation_id) lid: vec3<u32>) {
22
- {% if source.axisMode %}
23
- let input_index = (wg.x + wg.y * nwg.x) * WG + lid.x;
24
- if (input_index >= params.inputCount) {
25
- return;
26
- }
27
- let inner_index = input_index % params.inner;
28
- let axis_index = (input_index / params.inner) % params.axisDim;
29
- if (axis_index >= params.n) {
30
  return;
31
  }
 
 
 
 
32
  // ITEMS is a compile-time power of two, so these resolve to a shift and a mask.
33
  let lane = axis_index / ITEMS;
34
  let item = axis_index % ITEMS;
@@ -38,11 +36,10 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
38
  }
39
  let output_axis_index = blockPrefix[axis_index / BLOCK_ITEMS] + offsets[lane]
40
  + countOneBits(packed & ((1u << item) - 1u));
41
- let outer_index = input_index / (params.inner * params.axisDim);
42
  let output_index = (outer_index * params.outputAxisDim + output_axis_index) * params.inner + inner_index;
43
  output[output_index] = input[input_index];
44
  {% else %}
45
- let block = wg.x + wg.y * nwg.x;
46
  let lane = block * WG + lid.x;
47
  // base == block * BLOCK_ITEMS + tid * ITEMS, which is exactly lane * ITEMS.
48
  let base = lane * ITEMS;
 
4
  // lower bits gives an item's rank within its lane. The flattened route walks a
5
  // lane's ITEMS elements in order, so its output position advances without a
6
  // second lookup per element.
 
 
 
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
  const WG: u32 = {{ workgroupSize }}u;
10
  const ITEMS: u32 = {{ scanItems }}u;
11
+ {% if axisMode %}
12
  const BLOCK_ITEMS: u32 = WG * ITEMS;
13
  {% endif %}
14
 
15
  @compute @workgroup_size(WG)
16
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
 
17
  @builtin(local_invocation_id) lid: vec3<u32>) {
18
+ {% if axisMode %}
19
+ // Only the first `n` positions along the axis can be selected, so the grid
20
+ // covers (outer, n, inner) and the full-input address is rebuilt from it —
21
+ // the uninspected tail of the axis never costs an invocation.
22
+ let scan_index = (wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u) * WG + lid.x;
23
+ if (scan_index >= params.scanCount) {
 
 
24
  return;
25
  }
26
+ let inner_index = scan_index % params.inner;
27
+ let axis_index = (scan_index / params.inner) % params.n;
28
+ let outer_index = scan_index / (params.inner * params.n);
29
+ let input_index = (outer_index * params.axisDim + axis_index) * params.inner + inner_index;
30
  // ITEMS is a compile-time power of two, so these resolve to a shift and a mask.
31
  let lane = axis_index / ITEMS;
32
  let item = axis_index % ITEMS;
 
36
  }
37
  let output_axis_index = blockPrefix[axis_index / BLOCK_ITEMS] + offsets[lane]
38
  + countOneBits(packed & ((1u << item) - 1u));
 
39
  let output_index = (outer_index * params.outputAxisDim + output_axis_index) * params.inner + inner_index;
40
  output[output_index] = input[input_index];
41
  {% else %}
42
+ let block = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
43
  let lane = block * WG + lid.x;
44
  // base == block * BLOCK_ITEMS + tid * ITEMS, which is exactly lane * ITEMS.
45
  let base = lane * ITEMS;
build/webgpu/compress.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  @compute @workgroup_size(1)
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  @compute @workgroup_size(1)
build/webgpu/manifest.json CHANGED
@@ -2,43 +2,20 @@
2
  "domain": "ai.onnx",
3
  "name": "Compress",
4
  "sinceVersion": 11,
5
- "description": "Selects slices from the input tensor along an axis where the corresponding `condition` element is true. If `axis` is omitted, the input is flattened and elements are selected by position. The condition may be shorter than the selected dimension; values beyond its length are discarded. The data-dependent output extent must equal the number of true entries in the inspected prefix.",
6
- "inputs": [
7
- { "role": "input", "dtype": "T", "description": "Input tensor of rank `r >= 1` to select from." },
8
- {
9
- "role": "condition",
10
- "dtype": "C",
11
- "description": "Rank-1 boolean mask indicating which slices or elements to select; may be shorter than the axis dimension, in which case trailing slices are discarded.",
12
- "rank": 1
13
- }
14
- ],
15
- "outputs": [
16
- {
17
- "role": "output",
18
- "dtype": "T",
19
- "description": "Selected slices with rank `r` when `axis` is specified, or rank 1 when the input is flattened. The selected dimension equals the number of true values in the inspected condition prefix.",
20
- "rank": "ranks.input if has(attrs, \"axis\") else 1"
21
- }
22
- ],
23
- "attributeDescriptions": {
24
- "axis": "Axis along which to select slices; if omitted the input is flattened before selection. Negative values index from the end; accepted range is `[-r, r-1]`."
25
- },
26
  "typeConstraints": { "T": ["float32", "float16", "uint32", "int32", "int16", "uint8", "int8", "bool"], "C": ["bool"] },
27
- "args": {
28
- "input": { "kind": "tensor", "semantic": "input", "role": "input" },
29
- "condition": { "kind": "tensor", "semantic": "condition", "role": "input" },
30
- "output": { "kind": "tensor", "semantic": "output", "role": "output" }
31
- },
32
  "tunables": {
33
- "WORKGROUP_SIZE": 256,
34
- "SCAN_ITEMS_PER_THREAD": 16,
35
- "PARALLEL_MIN_ELEMENTS": 512,
36
- "MAX_PARALLEL_INPUT": 1073741823
37
  },
38
  "derive": {
39
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
40
  "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
41
- "foldedDispatchCapacity": "device.limits.maxComputeWorkgroupsPerDimension * device.limits.maxComputeWorkgroupsPerDimension",
42
  "axisParam": "attrs.axis if has(attrs, \"axis\") else 2147483647",
43
  "normalizedAxis": "axisParam if axisParam >= 0 else axisParam + ranks.input",
44
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
@@ -52,186 +29,33 @@
52
  "axisScanN": "min(dim(shapes.input, normalizedAxis), dim(shapes.condition, 0))",
53
  "axisScanBlocks": "ceilDiv(axisScanN, scanBlockItems)",
54
  "axisScanThreads": "axisScanBlocks * workgroupSize",
55
- "axisParallelFits": "workgroupStorageOk and axisScanThreads * 4 <= storageBufferLimit and axisScanBlocks * 4 <= storageBufferLimit and axisScanBlocks <= foldedDispatchCapacity and ceilDiv(numel(shapes.input), workgroupSize) <= foldedDispatchCapacity"
 
56
  },
57
- "constants": { "usesF16": "dtypes.T == \"f16\"", "scalar": "dtypes.T" },
58
- "bindingSets": {
59
- "scanBlocks": [
60
- {
61
- "name": "src",
62
- "arg": "condition",
63
- "semantic": "condition",
64
- "buffer": { "type": "read-only-storage" },
65
- "elementType": "u32"
66
- },
67
- { "name": "offsets", "semantic": "offsets", "buffer": { "type": "storage" }, "elementType": "u32" },
68
- { "name": "flags", "semantic": "flags", "buffer": { "type": "storage" }, "elementType": "u32" },
69
- { "name": "blockSums", "semantic": "blockSums", "buffer": { "type": "storage" }, "elementType": "u32" },
70
- {
71
- "name": "params",
72
- "semantic": "kernel.params",
73
- "buffer": { "type": "uniform" },
74
- "struct": { "name": "Params", "fields": [{ "name": "n", "type": "u32", "value": "scanN" }] }
75
- }
76
- ],
77
- "scanPrefix": [
78
- { "name": "blockSums", "semantic": "blockSums", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
79
- { "name": "blockPrefix", "semantic": "blockPrefix", "buffer": { "type": "storage" }, "elementType": "u32" },
80
- {
81
- "name": "params",
82
- "semantic": "kernel.params",
83
- "buffer": { "type": "uniform" },
84
- "struct": { "name": "Params", "fields": [{ "name": "numBlocks", "type": "u32", "value": "scanBlocks" }] }
85
- }
86
- ],
87
- "flatScatter": [
88
- {
89
- "name": "input",
90
- "arg": "input",
91
- "semantic": "input",
92
- "buffer": { "type": "read-only-storage" },
93
- "elementType": "$scalar"
94
- },
95
- { "name": "offsets", "semantic": "offsets", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
96
- { "name": "flags", "semantic": "flags", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
97
- {
98
- "name": "blockPrefix",
99
- "semantic": "blockPrefix",
100
- "buffer": { "type": "read-only-storage" },
101
- "elementType": "u32"
102
- },
103
- {
104
- "name": "output",
105
- "arg": "output",
106
- "semantic": "output",
107
- "buffer": { "type": "storage" },
108
- "elementType": "$scalar"
109
- },
110
- {
111
- "name": "params",
112
- "semantic": "kernel.params",
113
- "buffer": { "type": "uniform" },
114
- "struct": { "name": "Params", "fields": [{ "name": "n", "type": "u32", "value": "scanN" }] }
115
- }
116
- ],
117
- "axisScatter": [
118
- {
119
- "name": "input",
120
- "arg": "input",
121
- "semantic": "input",
122
- "buffer": { "type": "read-only-storage" },
123
- "elementType": "$scalar"
124
- },
125
- { "name": "offsets", "semantic": "offsets", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
126
- { "name": "flags", "semantic": "flags", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
127
- {
128
- "name": "blockPrefix",
129
- "semantic": "blockPrefix",
130
- "buffer": { "type": "read-only-storage" },
131
- "elementType": "u32"
132
- },
133
- {
134
- "name": "output",
135
- "arg": "output",
136
- "semantic": "output",
137
- "buffer": { "type": "storage" },
138
- "elementType": "$scalar"
139
- },
140
- {
141
- "name": "params",
142
- "semantic": "kernel.params",
143
- "buffer": { "type": "uniform" },
144
- "struct": {
145
- "name": "Params",
146
- "fields": [
147
- { "name": "inputCount", "type": "u32", "value": "numel(shapes.input)" },
148
- { "name": "inner", "type": "u32", "value": "inner(shapes.input, normalizedAxis)" },
149
- { "name": "axisDim", "type": "u32", "value": "dim(shapes.input, normalizedAxis)" },
150
- { "name": "n", "type": "u32", "value": "scanN" },
151
- { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.output, normalizedAxis)" }
152
- ]
153
- }
154
- }
155
- ],
156
- "flatSerial": [
157
- {
158
- "name": "input",
159
- "arg": "input",
160
- "semantic": "input",
161
- "buffer": { "type": "read-only-storage" },
162
- "elementType": "$scalar"
163
- },
164
- {
165
- "name": "condition",
166
- "arg": "condition",
167
- "semantic": "condition",
168
- "buffer": { "type": "read-only-storage" },
169
- "elementType": "u32"
170
- },
171
- {
172
- "name": "output",
173
- "arg": "output",
174
- "semantic": "output",
175
- "buffer": { "type": "storage" },
176
- "elementType": "$scalar"
177
- },
178
- {
179
- "name": "params",
180
- "semantic": "kernel.params",
181
- "buffer": { "type": "uniform" },
182
- "struct": {
183
- "name": "Params",
184
- "fields": [
185
- { "name": "inputCount", "type": "u32", "value": "numel(shapes.input)" },
186
- { "name": "conditionCount", "type": "u32", "value": "dim(shapes.condition, 0)" }
187
- ]
188
- }
189
- }
190
- ],
191
- "axisSerial": [
192
- {
193
- "name": "input",
194
- "arg": "input",
195
- "semantic": "input",
196
- "buffer": { "type": "read-only-storage" },
197
- "elementType": "$scalar"
198
- },
199
- {
200
- "name": "condition",
201
- "arg": "condition",
202
- "semantic": "condition",
203
- "buffer": { "type": "read-only-storage" },
204
- "elementType": "u32"
205
- },
206
- {
207
- "name": "output",
208
- "arg": "output",
209
- "semantic": "output",
210
- "buffer": { "type": "storage" },
211
- "elementType": "$scalar"
212
- },
213
- {
214
- "name": "params",
215
- "semantic": "kernel.params",
216
- "buffer": { "type": "uniform" },
217
- "struct": {
218
- "name": "Params",
219
- "fields": [
220
- { "name": "conditionCount", "type": "u32", "value": "dim(shapes.condition, 0)" },
221
- { "name": "outer", "type": "u32", "value": "outer(shapes.input, normalizedAxis)" },
222
- { "name": "axisDim", "type": "u32", "value": "dim(shapes.input, normalizedAxis)" },
223
- { "name": "inner", "type": "u32", "value": "inner(shapes.input, normalizedAxis)" },
224
- { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.output, normalizedAxis)" }
225
- ]
226
- }
227
- }
228
- ]
229
  },
230
  "variants": [
231
  {
232
  "id": "flatten_parallel_scan",
233
  "priority": 15,
234
- "when": ["normalizedAxis == 2147483647", "ranks.input >= 1", "ranks.condition == 1", "ranks.output == 1", "numel(shapes.output) <= numel(shapes.input)", "numel(shapes.input) >= tunables.PARALLEL_MIN_ELEMENTS", "numel(shapes.input) <= tunables.MAX_PARALLEL_INPUT", "dim(shapes.condition, 0) > 0", "flatParallelFits", "f16Ok(dtypes.T)"],
235
  "derive": { "scanN": "flatScanN", "scanBlocks": "flatScanBlocks", "scanThreads": "flatScanThreads" },
236
  "intermediates": [
237
  { "id": "offsets", "dtype": "uint32", "shape": "[scanThreads]" },
@@ -243,40 +67,39 @@
243
  {
244
  "id": "flag_scan",
245
  "name": "Compress.FlagBlockScan",
246
- "source": {
247
- "shader": "scan-flags-block-exclusive.wgsl.jinja",
248
- "inputs": {
249
- "predicate": "\"src[i] != 0u\"",
250
- "itemsPerThread": "scanItems",
251
- "useSubgroups": "device.features.has(\"subgroups\")"
252
- }
253
  },
254
- "bindings": "scanBlocks",
255
- "dispatch": { "workgroups": "scanBlocks" }
256
  },
257
  {
258
  "id": "block_prefix",
259
  "name": "Compress.BlockPrefixScan",
260
- "source": {
261
- "shader": "scan-block-prefix-u32.wgsl.jinja",
262
- "inputs": { "useSubgroups": "device.features.has(\"subgroups\")" }
263
- },
264
- "bindings": "scanPrefix",
265
  "dispatch": { "x": 1 }
266
  },
267
  {
268
  "id": "scatter",
269
  "name": "Compress.ParallelScatter",
270
- "source": { "shader": "compress-scatter.wgsl.jinja", "inputs": { "axisMode": false } },
271
- "bindings": "flatScatter",
272
- "dispatch": { "workgroups": "scanBlocks" }
 
273
  }
274
  ]
275
  },
276
  {
277
  "id": "axis_parallel_scan",
278
  "priority": 15,
279
- "when": ["normalizedAxis != 2147483647", "ranks.input >= 1", "ranks.condition == 1", "ranks.output == ranks.input", "outer(shapes.output, normalizedAxis) == outer(shapes.input, normalizedAxis)", "inner(shapes.output, normalizedAxis) == inner(shapes.input, normalizedAxis)", "dim(shapes.output, normalizedAxis) <= dim(shapes.input, normalizedAxis)", "numel(shapes.input) >= tunables.PARALLEL_MIN_ELEMENTS", "numel(shapes.input) <= tunables.MAX_PARALLEL_INPUT", "dim(shapes.condition, 0) > 0", "axisParallelFits", "f16Ok(dtypes.T)"],
280
  "derive": { "scanN": "axisScanN", "scanBlocks": "axisScanBlocks", "scanThreads": "axisScanThreads" },
281
  "intermediates": [
282
  { "id": "offsets", "dtype": "uint32", "shape": "[scanThreads]" },
@@ -288,60 +111,104 @@
288
  {
289
  "id": "flag_scan",
290
  "name": "Compress.FlagBlockScan",
291
- "source": {
292
- "shader": "scan-flags-block-exclusive.wgsl.jinja",
293
- "inputs": {
294
- "predicate": "\"src[i] != 0u\"",
295
- "itemsPerThread": "scanItems",
296
- "useSubgroups": "device.features.has(\"subgroups\")"
297
- }
298
  },
299
- "bindings": "scanBlocks",
300
- "dispatch": { "workgroups": "scanBlocks" }
301
  },
302
  {
303
  "id": "block_prefix",
304
  "name": "Compress.BlockPrefixScan",
305
- "source": {
306
- "shader": "scan-block-prefix-u32.wgsl.jinja",
307
- "inputs": { "useSubgroups": "device.features.has(\"subgroups\")" }
308
- },
309
- "bindings": "scanPrefix",
310
  "dispatch": { "x": 1 }
311
  },
312
  {
313
  "id": "scatter",
314
  "name": "Compress.ParallelScatterAxis",
315
- "source": { "shader": "compress-scatter.wgsl.jinja", "inputs": { "axisMode": true } },
316
- "bindings": "axisScatter",
317
- "dispatch": { "threads": "numel(shapes.input)", "workgroupSize": "workgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
  ]
320
  },
321
  {
322
  "id": "flatten_serial",
323
- "when": ["normalizedAxis == 2147483647", "ranks.condition == 1", "ranks.output == 1", "numel(shapes.output) <= numel(shapes.input)", "f16Ok(dtypes.T)"],
324
- "constants": { "axisMode": false },
325
  "passes": [
326
  {
327
  "id": "main",
328
  "name": "Compress",
329
  "shader": "compress.wgsl.jinja",
330
- "bindings": "flatSerial",
 
 
 
 
 
 
 
 
 
 
 
331
  "dispatch": { "x": 1 }
332
  }
333
  ]
334
  },
335
  {
336
  "id": "axis_serial",
337
- "when": ["normalizedAxis != 2147483647", "ranks.input >= 1", "ranks.condition == 1", "ranks.output == ranks.input", "outer(shapes.output, normalizedAxis) == outer(shapes.input, normalizedAxis)", "inner(shapes.output, normalizedAxis) == inner(shapes.input, normalizedAxis)", "dim(shapes.output, normalizedAxis) <= dim(shapes.input, normalizedAxis)", "f16Ok(dtypes.T)"],
338
- "constants": { "axisMode": true },
339
  "passes": [
340
  {
341
  "id": "main",
342
  "name": "Compress",
343
  "shader": "compress.wgsl.jinja",
344
- "bindings": "axisSerial",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  "dispatch": { "x": 1 }
346
  }
347
  ]
 
2
  "domain": "ai.onnx",
3
  "name": "Compress",
4
  "sinceVersion": 11,
5
+ "inputs": { "input": { "dtype": "T" }, "condition": { "dtype": "C", "rank": 1 } },
6
+ "outputs": { "output": { "dtype": "T", "rank": "ranks.input if has(attrs, \"axis\") else 1" } },
7
+ "attributes": { "axis": {} },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  "typeConstraints": { "T": ["float32", "float16", "uint32", "int32", "int16", "uint8", "int8", "bool"], "C": ["bool"] },
 
 
 
 
 
9
  "tunables": {
10
+ "WORKGROUP_SIZE": { "default": 256 },
11
+ "SCAN_ITEMS_PER_THREAD": { "default": 16 },
12
+ "PARALLEL_MIN_ELEMENTS": { "default": 512 },
13
+ "MAX_PARALLEL_INPUT": { "default": 1073741823 }
14
  },
15
  "derive": {
16
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
17
  "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
18
+ "foldedDispatchCapacity": "min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
19
  "axisParam": "attrs.axis if has(attrs, \"axis\") else 2147483647",
20
  "normalizedAxis": "axisParam if axisParam >= 0 else axisParam + ranks.input",
21
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
 
29
  "axisScanN": "min(dim(shapes.input, normalizedAxis), dim(shapes.condition, 0))",
30
  "axisScanBlocks": "ceilDiv(axisScanN, scanBlockItems)",
31
  "axisScanThreads": "axisScanBlocks * workgroupSize",
32
+ "axisScatterThreads": "outer(shapes.input, normalizedAxis) * axisScanN * inner(shapes.input, normalizedAxis)",
33
+ "axisParallelFits": "workgroupStorageOk and axisScanThreads * 4 <= storageBufferLimit and axisScanBlocks * 4 <= storageBufferLimit and axisScanBlocks <= foldedDispatchCapacity and ceilDiv(axisScatterThreads, workgroupSize) <= foldedDispatchCapacity"
34
  },
35
+ "when": ["ranks.condition == 1", "f16Ok(dtypes.T)"],
36
+ "bindings": {
37
+ "src": { "arg": "condition", "buffer": "read-only-storage", "elementType": "u32" },
38
+ "offsets": { "buffer": "storage", "elementType": "u32" },
39
+ "flags": { "buffer": "storage", "elementType": "u32" },
40
+ "blockSums": { "buffer": "storage", "elementType": "u32" },
41
+ "params": { "buffer": "uniform", "struct": [{ "name": "n", "type": "u32", "value": "scanN" }] },
42
+ "blockSums_2": { "name": "blockSums", "buffer": "read-only-storage", "elementType": "u32" },
43
+ "blockPrefix": { "buffer": "storage", "elementType": "u32" },
44
+ "params_2": {
45
+ "name": "params",
46
+ "buffer": "uniform",
47
+ "struct": [{ "name": "numBlocks", "type": "u32", "value": "scanBlocks" }]
48
+ },
49
+ "offsets_2": { "name": "offsets", "buffer": "read-only-storage", "elementType": "u32" },
50
+ "flags_2": { "name": "flags", "buffer": "read-only-storage", "elementType": "u32" },
51
+ "blockPrefix_2": { "name": "blockPrefix", "buffer": "read-only-storage", "elementType": "u32" },
52
+ "condition": { "buffer": "read-only-storage", "elementType": "u32" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  },
54
  "variants": [
55
  {
56
  "id": "flatten_parallel_scan",
57
  "priority": 15,
58
+ "when": ["normalizedAxis == 2147483647", "ranks.input >= 1", "ranks.output == 1", "numel(shapes.output) <= numel(shapes.input)", "numel(shapes.input) >= tunables.PARALLEL_MIN_ELEMENTS", "numel(shapes.input) <= tunables.MAX_PARALLEL_INPUT", "dim(shapes.condition, 0) > 0", "flatParallelFits"],
59
  "derive": { "scanN": "flatScanN", "scanBlocks": "flatScanBlocks", "scanThreads": "flatScanThreads" },
60
  "intermediates": [
61
  { "id": "offsets", "dtype": "uint32", "shape": "[scanThreads]" },
 
67
  {
68
  "id": "flag_scan",
69
  "name": "Compress.FlagBlockScan",
70
+ "shader": "scan-flags-block-exclusive.wgsl.jinja",
71
+ "subgroupCollectivesWidth": "portable",
72
+ "derive": {
73
+ "predicate": "\"src[i] != 0u\"",
74
+ "itemsPerThread": "scanItems",
75
+ "useSubgroups": "device.features.has(\"subgroups\")"
 
76
  },
77
+ "bindings": ["src", "offsets", "flags", "blockSums", "params"],
78
+ "dispatch": { "x": "min(scanBlocks, 65535)", "y": "ceilDiv(scanBlocks, 65535)", "z": 1 }
79
  },
80
  {
81
  "id": "block_prefix",
82
  "name": "Compress.BlockPrefixScan",
83
+ "shader": "scan-block-prefix-u32.wgsl.jinja",
84
+ "subgroupCollectivesWidth": "portable",
85
+ "derive": { "useSubgroups": "device.features.has(\"subgroups\")" },
86
+ "bindings": ["blockSums_2", "blockPrefix", "params_2"],
 
87
  "dispatch": { "x": 1 }
88
  },
89
  {
90
  "id": "scatter",
91
  "name": "Compress.ParallelScatter",
92
+ "shader": "compress-scatter.wgsl.jinja",
93
+ "derive": { "axisMode": false },
94
+ "bindings": ["input", "offsets_2", "flags_2", "blockPrefix_2", "output", "params"],
95
+ "dispatch": { "x": "min(scanBlocks, 65535)", "y": "ceilDiv(scanBlocks, 65535)", "z": 1 }
96
  }
97
  ]
98
  },
99
  {
100
  "id": "axis_parallel_scan",
101
  "priority": 15,
102
+ "when": ["normalizedAxis != 2147483647", "ranks.input >= 1", "ranks.output == ranks.input", "outer(shapes.output, normalizedAxis) == outer(shapes.input, normalizedAxis)", "inner(shapes.output, normalizedAxis) == inner(shapes.input, normalizedAxis)", "dim(shapes.output, normalizedAxis) <= dim(shapes.input, normalizedAxis)", "numel(shapes.input) >= tunables.PARALLEL_MIN_ELEMENTS", "numel(shapes.input) <= tunables.MAX_PARALLEL_INPUT", "dim(shapes.condition, 0) > 0", "axisParallelFits"],
103
  "derive": { "scanN": "axisScanN", "scanBlocks": "axisScanBlocks", "scanThreads": "axisScanThreads" },
104
  "intermediates": [
105
  { "id": "offsets", "dtype": "uint32", "shape": "[scanThreads]" },
 
111
  {
112
  "id": "flag_scan",
113
  "name": "Compress.FlagBlockScan",
114
+ "shader": "scan-flags-block-exclusive.wgsl.jinja",
115
+ "subgroupCollectivesWidth": "portable",
116
+ "derive": {
117
+ "predicate": "\"src[i] != 0u\"",
118
+ "itemsPerThread": "scanItems",
119
+ "useSubgroups": "device.features.has(\"subgroups\")"
 
120
  },
121
+ "bindings": ["src", "offsets", "flags", "blockSums", "params"],
122
+ "dispatch": { "x": "min(scanBlocks, 65535)", "y": "ceilDiv(scanBlocks, 65535)", "z": 1 }
123
  },
124
  {
125
  "id": "block_prefix",
126
  "name": "Compress.BlockPrefixScan",
127
+ "shader": "scan-block-prefix-u32.wgsl.jinja",
128
+ "subgroupCollectivesWidth": "portable",
129
+ "derive": { "useSubgroups": "device.features.has(\"subgroups\")" },
130
+ "bindings": ["blockSums_2", "blockPrefix", "params_2"],
 
131
  "dispatch": { "x": 1 }
132
  },
133
  {
134
  "id": "scatter",
135
  "name": "Compress.ParallelScatterAxis",
136
+ "shader": "compress-scatter.wgsl.jinja",
137
+ "derive": { "axisMode": true },
138
+ "bindings": [
139
+ "input",
140
+ "offsets_2",
141
+ "flags_2",
142
+ "blockPrefix_2",
143
+ "output",
144
+ {
145
+ "name": "params",
146
+ "struct": [
147
+ { "name": "scanCount", "type": "u32", "value": "axisScatterThreads" },
148
+ { "name": "inner", "type": "u32", "value": "inner(shapes.input, normalizedAxis)" },
149
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.input, normalizedAxis)" },
150
+ { "name": "n", "type": "u32", "value": "scanN" },
151
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.output, normalizedAxis)" }
152
+ ]
153
+ }
154
+ ],
155
+ "dispatch": {
156
+ "x": "min(ceilDiv((axisScatterThreads), (workgroupSize)), 65535)",
157
+ "y": "ceilDiv(ceilDiv((axisScatterThreads), (workgroupSize)), 65535)",
158
+ "z": 1
159
+ }
160
  }
161
  ]
162
  },
163
  {
164
  "id": "flatten_serial",
165
+ "when": ["normalizedAxis == 2147483647", "ranks.output == 1", "numel(shapes.output) <= numel(shapes.input)"],
166
+ "derive": { "axisMode": false },
167
  "passes": [
168
  {
169
  "id": "main",
170
  "name": "Compress",
171
  "shader": "compress.wgsl.jinja",
172
+ "bindings": [
173
+ "input",
174
+ "condition",
175
+ "output",
176
+ {
177
+ "name": "params",
178
+ "struct": [
179
+ { "name": "inputCount", "type": "u32", "value": "numel(shapes.input)" },
180
+ { "name": "conditionCount", "type": "u32", "value": "dim(shapes.condition, 0)" }
181
+ ]
182
+ }
183
+ ],
184
  "dispatch": { "x": 1 }
185
  }
186
  ]
187
  },
188
  {
189
  "id": "axis_serial",
190
+ "when": ["normalizedAxis != 2147483647", "ranks.input >= 1", "ranks.output == ranks.input", "outer(shapes.output, normalizedAxis) == outer(shapes.input, normalizedAxis)", "inner(shapes.output, normalizedAxis) == inner(shapes.input, normalizedAxis)", "dim(shapes.output, normalizedAxis) <= dim(shapes.input, normalizedAxis)"],
191
+ "derive": { "axisMode": true },
192
  "passes": [
193
  {
194
  "id": "main",
195
  "name": "Compress",
196
  "shader": "compress.wgsl.jinja",
197
+ "bindings": [
198
+ "input",
199
+ "condition",
200
+ "output",
201
+ {
202
+ "name": "params",
203
+ "struct": [
204
+ { "name": "conditionCount", "type": "u32", "value": "dim(shapes.condition, 0)" },
205
+ { "name": "outer", "type": "u32", "value": "outer(shapes.input, normalizedAxis)" },
206
+ { "name": "axisDim", "type": "u32", "value": "dim(shapes.input, normalizedAxis)" },
207
+ { "name": "inner", "type": "u32", "value": "inner(shapes.input, normalizedAxis)" },
208
+ { "name": "outputAxisDim", "type": "u32", "value": "dim(shapes.output, normalizedAxis)" }
209
+ ]
210
+ }
211
+ ],
212
  "dispatch": { "x": 1 }
213
  }
214
  ]
build/webgpu/metadata.json CHANGED
@@ -1,21 +1,29 @@
1
  {
2
  "name": "ai.onnx.Compress",
3
- "id": "_ai_onnx_compress_webgpu_cfbc4b5",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "eyLAKUGnwFcu+exPotgJl/ZViuzCu2hWDco3P8Cs9ps=",
11
- "compress-scatter.wgsl.jinja": "T6PIeYVTixXpDnRTaQB/iTAZ0LHYZecAJqUnWr3Ww0I=",
12
- "compress.wgsl.jinja": "2vtjJ6EHEsVl08tW1O1kOZTR0VuRxsw77mSMlYyuyyk=",
13
- "manifest.json": "DsMO6552aeUXGp8WmmmTQMetnVzS/JXRiiEk7Hh0mLM=",
14
- "scan-block-prefix-u32.wgsl.jinja": "UJRN3G2HczCxzkEaCkPruYSaNTw+dJJURbUiqmhTncU=",
15
- "scan-flags-block-exclusive.wgsl.jinja": "KiTJI6k5iFHfkj8xV5C/8g/BLcMqPewng9bBqRJXNT0=",
16
- "test.json": "MOnY26q5C+8aOvNgOQ3gWbu0CX7H1Huwg18qu4bot9M="
17
  }
18
  },
19
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
20
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.Compress" }
 
 
 
 
 
 
 
 
21
  }
 
1
  {
2
  "name": "ai.onnx.Compress",
3
+ "id": "_ai_onnx_compress_webgpu_ab772ca",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "+YEsjnASlQKDd+UVLHTmWVfYB68qJ7UzCl0HyYPytuA=",
11
+ "compress-scatter.wgsl.jinja": "/CDh8XVC6X8AL4VRCKYwsP4weVOSIA0jEggep0Opngw=",
12
+ "compress.wgsl.jinja": "H8cjLOeT5Nwk0Yxd0B/tjSTLtIV5OXbREitywwvQa2M=",
13
+ "manifest.json": "lhknKZZblyy6aWk5dGgODBL0X0i+KsP7etkPQbT7avk=",
14
+ "scan-block-prefix-u32.wgsl.jinja": "XxgrEAHcIrSr96cR8fvJxzmBGwSWaY2LNXsJM3ypxAI=",
15
+ "scan-flags-block-exclusive.wgsl.jinja": "xCIsrwQGNSLkCMtk1hS27aVhDGhdQOddrg86QCegerY=",
16
+ "test.json": "FroEGRUKQpjx/c0R8AMl6YB/dX5s3diS3YOCOZ4Fi90="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.0",
22
+ "variants": {
23
+ "flatten_parallel_scan": ["compress-scatter.wgsl.jinja", "scan-block-prefix-u32.wgsl.jinja", "scan-flags-block-exclusive.wgsl.jinja"],
24
+ "axis_parallel_scan": ["compress-scatter.wgsl.jinja", "scan-block-prefix-u32.wgsl.jinja", "scan-flags-block-exclusive.wgsl.jinja"],
25
+ "flatten_serial": ["compress.wgsl.jinja"],
26
+ "axis_serial": ["compress.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/scan-block-prefix-u32.wgsl.jinja CHANGED
@@ -3,24 +3,63 @@
3
  // scans each chunk with Hillis-Steele or subgroup collectives, and links chunks
4
  // with a running carry. blockPrefix[b] is therefore the number of set flags in
5
  // all blocks before b. The total count is the last prefix plus the last sum.
6
- {% if source.useSubgroups %}
7
  enable subgroups;
8
  {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  const WG: u32 = {{ workgroupSize }}u;
12
 
13
- {% if source.useSubgroups %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  var<workgroup> sgTotals: array<u32, WG>;
15
  {% else %}
16
  var<workgroup> wgScan: array<u32, WG>;
17
  {% endif %}
18
 
19
  @compute @workgroup_size(WG, 1, 1)
20
- fn main(@builtin(local_invocation_id) lid: vec3<u32>{% if source.useSubgroups %},
21
- @builtin(subgroup_invocation_id) sgLane: u32,
22
- @builtin(subgroup_size) sgSize: u32{% endif %}) {
 
 
 
 
 
 
 
23
  let tid = lid.x;
 
24
  var carry = 0u;
25
  let chunks = (params.numBlocks + WG - 1u) / WG;
26
  for (var c = 0u; c < chunks; c = c + 1u) {
@@ -29,14 +68,11 @@ fn main(@builtin(local_invocation_id) lid: vec3<u32>{% if source.useSubgroups %}
29
  if (j < params.numBlocks) {
30
  v = blockSums[j];
31
  }
32
- {% if source.useSubgroups %}
33
  let subgroupInclusive = subgroupInclusiveAdd(v);
34
  let subgroupTotal = subgroupAdd(v);
35
- let safeSg = max(sgSize, 1u);
36
- let subgroupId = tid / safeSg;
37
- let subgroupCount = (WG + safeSg - 1u) / safeSg;
38
- if (sgLane == 0u) {
39
- sgTotals[subgroupId] = subgroupTotal;
40
  }
41
  workgroupBarrier();
42
  var subgroupOffset = 0u;
@@ -44,7 +80,7 @@ fn main(@builtin(local_invocation_id) lid: vec3<u32>{% if source.useSubgroups %}
44
  for (var s = 0u; s < subgroupCount; s = s + 1u) {
45
  let total = sgTotals[s];
46
  chunkTotal = chunkTotal + total;
47
- if (s < subgroupId) {
48
  subgroupOffset = subgroupOffset + total;
49
  }
50
  }
 
3
  // scans each chunk with Hillis-Steele or subgroup collectives, and links chunks
4
  // with a running carry. blockPrefix[b] is therefore the number of set flags in
5
  // all blocks before b. The total count is the last prefix plus the last sum.
6
+ {% if useSubgroups %}
7
  enable subgroups;
8
  {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  const WG: u32 = {{ workgroupSize }}u;
12
 
13
+ {% if useSubgroups %}
14
+ {% set skipLogicalLaneCount = skipLogicalLaneCount is defined and skipLogicalLaneCount %}
15
+ var<workgroup> sgLaneClaims: atomic<u32>;
16
+
17
+ struct SubgroupLogicalLanes {
18
+ ltid: u32,
19
+ ord: u32,
20
+ rank: u32,
21
+ count: u32,
22
+ }
23
+
24
+ fn subgroup_logical_lanes() -> SubgroupLogicalLanes {
25
+ let rank = subgroupExclusiveAdd(1u);
26
+ let count = subgroupAdd(1u);
27
+ // Every lane performs the atomic so no collective follows a lane guard (a
28
+ // subgroup op after a closed `if (rank == 0u)` block is the reconvergence
29
+ // hazard the render gate flags): only the rank-0 lane adds its subgroup's
30
+ // claim, every other lane adds 0 and discards its snapshot. The rank-0 lane is
31
+ // the lowest active lane, which is the lane `subgroupBroadcastFirst` reads.
32
+ let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
33
+ let claim = subgroupBroadcastFirst(ticket);
34
+ return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
35
+ }
36
+ {% if not skipLogicalLaneCount %}
37
+
38
+ fn subgroup_logical_count() -> u32 {
39
+ return atomicLoad(&sgLaneClaims) >> 16u;
40
+ }
41
+ {% endif %}
42
+
43
+ // One partial per subgroup, addressed by the subgroup's logical ordinal, which
44
+ // is below the number of subgroups and therefore below WG at any width.
45
  var<workgroup> sgTotals: array<u32, WG>;
46
  {% else %}
47
  var<workgroup> wgScan: array<u32, WG>;
48
  {% endif %}
49
 
50
  @compute @workgroup_size(WG, 1, 1)
51
+ fn main({% if not useSubgroups %}@builtin(local_invocation_id) lid: vec3<u32>{% endif %}) {
52
+ {% if useSubgroups %}
53
+ // Logical lane coordinates replace local_invocation_index: `ltid` is the data
54
+ // position this invocation owns and `ord` its subgroup's carry ordinal, so the
55
+ // scan order is independent of how the device partitions the workgroup.
56
+ let L = subgroup_logical_lanes();
57
+ let tid = L.ltid;
58
+ workgroupBarrier();
59
+ let subgroupCount = subgroup_logical_count();
60
+ {% else %}
61
  let tid = lid.x;
62
+ {% endif %}
63
  var carry = 0u;
64
  let chunks = (params.numBlocks + WG - 1u) / WG;
65
  for (var c = 0u; c < chunks; c = c + 1u) {
 
68
  if (j < params.numBlocks) {
69
  v = blockSums[j];
70
  }
71
+ {% if useSubgroups %}
72
  let subgroupInclusive = subgroupInclusiveAdd(v);
73
  let subgroupTotal = subgroupAdd(v);
74
+ if (L.rank == 0u) {
75
+ sgTotals[L.ord] = subgroupTotal;
 
 
 
76
  }
77
  workgroupBarrier();
78
  var subgroupOffset = 0u;
 
80
  for (var s = 0u; s < subgroupCount; s = s + 1u) {
81
  let total = sgTotals[s];
82
  chunkTotal = chunkTotal + total;
83
+ if (s < L.ord) {
84
  subgroupOffset = subgroupOffset + total;
85
  }
86
  }
build/webgpu/scan-flags-block-exclusive.wgsl.jinja CHANGED
@@ -8,33 +8,68 @@
8
  // predicate mask, and blockSums stores the block population. After blockSums is
9
  // scanned, item k in lane t of block b lands at:
10
  // blockPrefix[b] + offsets[t] + countOneBits(flags[t] & ((1 << k) - 1)).
11
- {% if source.useSubgroups %}
12
  enable subgroups;
13
  {% endif %}
14
  {{ env.wgsl.resourceDeclarations }}
15
 
16
  const WG: u32 = {{ workgroupSize }}u;
17
- const ITEMS: u32 = {{ source.itemsPerThread }}u;
18
  const BLOCK_ITEMS: u32 = WG * ITEMS;
19
 
20
- {% if source.useSubgroups %}
21
- // Only the first ceil(WG / subgroup_size) entries are used. Keeping the array
22
- // device-independent lets one shader cover every legal subgroup width.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  var<workgroup> sgTotals: array<u32, WG>;
24
  {% else %}
25
  var<workgroup> wgScan: array<u32, WG>;
26
  {% endif %}
27
 
28
  @compute @workgroup_size(WG, 1, 1)
29
- fn main(@builtin(workgroup_id) wg: vec3<u32>,
30
- @builtin(num_workgroups) nwg: vec3<u32>,
31
- @builtin(local_invocation_id) lid: vec3<u32>{% if source.useSubgroups %},
32
- @builtin(subgroup_invocation_id) sgLane: u32,
33
- @builtin(subgroup_size) sgSize: u32{% endif %}) {
 
 
 
 
34
  let tid = lid.x;
35
- // Linearize the 2D dispatch so numBlocks can exceed the maxComputeWorkgroupsPerDimension grid cap: the
36
- // Dispatch folds blocks past maxComputeWorkgroupsPerDimension into y: block = x + y * gridWidth.
37
- let block = wg.x + wg.y * nwg.x;
 
38
  let base = block * BLOCK_ITEMS + tid * ITEMS;
39
 
40
  var packed = 0u;
@@ -43,7 +78,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
43
  let i = base + k;
44
  var flag = 0u;
45
  if (i < params.n) {
46
- if ({{ source.predicate }}) {
47
  flag = 1u;
48
  }
49
  }
@@ -51,19 +86,17 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
51
  laneCount = laneCount + flag;
52
  }
53
 
54
- {% if source.useSubgroups %}
55
  // Hardware scans each subgroup, then one shared-memory rendezvous links the
56
  // handful of subgroup totals. This replaces log2(WG) two-barrier sweeps.
57
  let subgroupInclusive = subgroupInclusiveAdd(laneCount);
58
  let subgroupTotal = subgroupAdd(laneCount);
59
- let safeSg = max(sgSize, 1u);
60
- let subgroupId = tid / safeSg;
61
- if (sgLane == 0u) {
62
- sgTotals[subgroupId] = subgroupTotal;
63
  }
64
  workgroupBarrier();
65
  var subgroupOffset = 0u;
66
- for (var s = 0u; s < subgroupId; s = s + 1u) {
67
  subgroupOffset = subgroupOffset + sgTotals[s];
68
  }
69
  let inclusive = subgroupOffset + subgroupInclusive;
 
8
  // predicate mask, and blockSums stores the block population. After blockSums is
9
  // scanned, item k in lane t of block b lands at:
10
  // blockPrefix[b] + offsets[t] + countOneBits(flags[t] & ((1 << k) - 1)).
11
+ {% if useSubgroups %}
12
  enable subgroups;
13
  {% endif %}
14
  {{ env.wgsl.resourceDeclarations }}
15
 
16
  const WG: u32 = {{ workgroupSize }}u;
17
+ const ITEMS: u32 = {{ itemsPerThread }}u;
18
  const BLOCK_ITEMS: u32 = WG * ITEMS;
19
 
20
+ {% if useSubgroups %}
21
+ {% set skipLogicalLaneCount = true %}
22
+ {% set skipLogicalLaneCount = skipLogicalLaneCount is defined and skipLogicalLaneCount %}
23
+ var<workgroup> sgLaneClaims: atomic<u32>;
24
+
25
+ struct SubgroupLogicalLanes {
26
+ ltid: u32,
27
+ ord: u32,
28
+ rank: u32,
29
+ count: u32,
30
+ }
31
+
32
+ fn subgroup_logical_lanes() -> SubgroupLogicalLanes {
33
+ let rank = subgroupExclusiveAdd(1u);
34
+ let count = subgroupAdd(1u);
35
+ // Every lane performs the atomic so no collective follows a lane guard (a
36
+ // subgroup op after a closed `if (rank == 0u)` block is the reconvergence
37
+ // hazard the render gate flags): only the rank-0 lane adds its subgroup's
38
+ // claim, every other lane adds 0 and discards its snapshot. The rank-0 lane is
39
+ // the lowest active lane, which is the lane `subgroupBroadcastFirst` reads.
40
+ let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
41
+ let claim = subgroupBroadcastFirst(ticket);
42
+ return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
43
+ }
44
+ {% if not skipLogicalLaneCount %}
45
+
46
+ fn subgroup_logical_count() -> u32 {
47
+ return atomicLoad(&sgLaneClaims) >> 16u;
48
+ }
49
+ {% endif %}
50
+
51
+ // One partial per subgroup, addressed by its logical ordinal. Only as many
52
+ // entries as there are subgroups are used; WG bounds that at every width.
53
  var<workgroup> sgTotals: array<u32, WG>;
54
  {% else %}
55
  var<workgroup> wgScan: array<u32, WG>;
56
  {% endif %}
57
 
58
  @compute @workgroup_size(WG, 1, 1)
59
+ fn main(@builtin(workgroup_id) wg: vec3<u32>{% if not useSubgroups %},
60
+ @builtin(local_invocation_id) lid: vec3<u32>{% endif %}) {
61
+ {% if useSubgroups %}
62
+ // Logical lane coordinates replace local_invocation_index: `ltid` is the data
63
+ // position this invocation owns and `ord` its subgroup's scan ordinal, so the
64
+ // block scan order is independent of how the device partitions the workgroup.
65
+ let L = subgroup_logical_lanes();
66
+ let tid = L.ltid;
67
+ {% else %}
68
  let tid = lid.x;
69
+ {% endif %}
70
+ // Linearize the 2D dispatch so numBlocks can exceed the per-axis dispatch fold width grid cap: the
71
+ // Dispatch folds blocks past the per-axis dispatch fold width into y: block = x + y * gridWidth.
72
+ let block = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
73
  let base = block * BLOCK_ITEMS + tid * ITEMS;
74
 
75
  var packed = 0u;
 
78
  let i = base + k;
79
  var flag = 0u;
80
  if (i < params.n) {
81
+ if ({{ predicate }}) {
82
  flag = 1u;
83
  }
84
  }
 
86
  laneCount = laneCount + flag;
87
  }
88
 
89
+ {% if useSubgroups %}
90
  // Hardware scans each subgroup, then one shared-memory rendezvous links the
91
  // handful of subgroup totals. This replaces log2(WG) two-barrier sweeps.
92
  let subgroupInclusive = subgroupInclusiveAdd(laneCount);
93
  let subgroupTotal = subgroupAdd(laneCount);
94
+ if (L.rank == 0u) {
95
+ sgTotals[L.ord] = subgroupTotal;
 
 
96
  }
97
  workgroupBarrier();
98
  var subgroupOffset = 0u;
99
+ for (var s = 0u; s < L.ord; s = s + 1u) {
100
  subgroupOffset = subgroupOffset + sgTotals[s];
101
  }
102
  let inclusive = subgroupOffset + subgroupInclusive;
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Compress",
3
  "cases": [
4
  {
5
  "name": "int16_axis_copy_boundaries",
@@ -173,7 +172,7 @@
173
  "provenance": {
174
  "source": "onnxruntime/test/providers/cpu/tensor/compress_op.test.cc",
175
  "test": "CompressTest.Compress_default_axis_issue_9247_cumulative_sum_overflow",
176
- "notes": "Extends ORT's >127-element cumulative-sum regression above 1024 elements so the WebGPU parallel prefix-scan path is exercised."
177
  },
178
  "inputs": {
179
  "input": { "dtype": "float32", "shape": [23, 50], "data": { "kind": "linspace", "start": 0.0, "end": 1149.0 } },
@@ -394,7 +393,7 @@
394
  {
395
  "name": "int8_payload_axis1_parallel_scatter",
396
  "provenance": {
397
- "notes": "int8 activation payload through the axis_parallel_scan scatter (numel 12288 > 1024). int8 stores as i32 per scalarType, so the $scalar copy path is exercised with the full s8 range incl -128/127. ORT CPU runs Compress for int8."
398
  },
399
  "attrs": { "axis": 1 },
400
  "inputs": {
@@ -425,7 +424,7 @@
425
  {
426
  "name": "int32_large_magnitude_above_2pow24_flatten",
427
  "provenance": {
428
- "notes": "int32 payload with magnitudes above 2^24 (incl ±2e9) through flatten_parallel_scan. Verifies the i32 scatter copy preserves values that would be lossy if carried through f32; comparison uses the int32 dtype so it stays exact. ORT CPU runs Compress for int32."
429
  },
430
  "inputs": {
431
  "input": {
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "int16_axis_copy_boundaries",
 
172
  "provenance": {
173
  "source": "onnxruntime/test/providers/cpu/tensor/compress_op.test.cc",
174
  "test": "CompressTest.Compress_default_axis_issue_9247_cumulative_sum_overflow",
175
+ "notes": "An all-true condition over more than 1,024 elements exercises the parallel prefix scan while preserving every input value."
176
  },
177
  "inputs": {
178
  "input": { "dtype": "float32", "shape": [23, 50], "data": { "kind": "linspace", "start": 0.0, "end": 1149.0 } },
 
393
  {
394
  "name": "int8_payload_axis1_parallel_scatter",
395
  "provenance": {
396
+ "notes": "A 12,288-element int8 activation tensor exercises axis-parallel scan and scatter. The package stores logical int8 values in int32 slots, and the cyclic payload includes the full signed-byte range from -128 to 127."
397
  },
398
  "attrs": { "axis": 1 },
399
  "inputs": {
 
424
  {
425
  "name": "int32_large_magnitude_above_2pow24_flatten",
426
  "provenance": {
427
+ "notes": "int32 payload with magnitudes above 2^24 (incl ±2e9) through flatten_parallel_scan. Verifies the i32 scatter copy preserves values that would be lossy if carried through f32; comparison uses the int32 dtype so it stays exact."
428
  },
429
  "inputs": {
430
  "input": {