Xenova HF Staff commited on
Commit
3d754b3
·
verified ·
1 Parent(s): 50c1187

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,16 +18,16 @@ See the [ONNX `Upsample` spec](https://onnx.ai/onnx/operators/onnx__Upsample.htm
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `X` | `x` | `T` | — | — | Input tensor to upsample. | required |
24
- | `scales` | `scales` | `S` | `1` | — | Per-dimension scale factors, one value per input dimension. | required |
25
 
26
  ## Outputs
27
 
28
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
29
  | --- | --- | --- | --- | --- | --- | --- |
30
- | `Y` | `y` | `T` | same as `X` | — | Upsampled output tensor; each dimension is `floor(input_dimension * scale)`. | required |
31
 
32
  ## Attributes
33
 
@@ -46,7 +46,7 @@ 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 @@ 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
- - `y`
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 | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `X` | `T` | — | — | Input tensor to upsample. | required |
24
+ | `scales` | | `S` | `1` | — | Per-dimension scale factors, one value per input dimension. | required |
25
 
26
  ## Outputs
27
 
28
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
29
  | --- | --- | --- | --- | --- | --- | --- |
30
+ | `y` | `Y` | `T` | same as `x` | — | Upsampled output tensor; each dimension is `floor(input_dimension * scale)`. | 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
+ - `y`
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.Upsample",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
build/webgpu/manifest.json CHANGED
@@ -2,222 +2,195 @@
2
  "domain": "ai.onnx",
3
  "name": "Upsample",
4
  "sinceVersion": 9,
5
- "description": "Upsamples the input by applying a per-dimension scale factor; each output dimension equals `floor(input_dimension * scale)`. Deprecated in favor of Resize; supports `nearest` and `linear` interpolation modes.",
6
- "inputs": [
7
- { "role": "X", "dtype": "T", "description": "Input tensor to upsample." },
8
- {
9
- "role": "scales",
10
- "dtype": "S",
11
- "description": "Per-dimension scale factors, one value per input dimension.",
12
- "rank": 1
13
- }
14
- ],
15
- "outputs": [
16
- {
17
- "role": "Y",
18
- "dtype": "T",
19
- "description": "Upsampled output tensor; each dimension is `floor(input_dimension * scale)`.",
20
- "rank": "ranks.X"
21
- }
22
- ],
23
- "attributes": { "mode": "nearest" },
24
- "attributeDescriptions": {
25
- "mode": "Interpolation algorithm to use when mapping output coordinates back to input values; either `\"nearest\"` or `\"linear\"`."
26
- },
27
  "attributeConstraints": { "mode": { "values": ["nearest", "linear"] } },
28
  "typeConstraints": { "T": ["float32", "float16", "int32", "int8", "uint8"], "S": ["float32"] },
29
- "args": {
30
- "x": { "kind": "tensor", "semantic": "X", "role": "input" },
31
- "scales": { "kind": "tensor", "semantic": "scales", "role": "input" },
32
- "y": { "kind": "tensor", "semantic": "Y", "role": "output" }
33
- },
34
- "tunables": { "WORKGROUP_SIZE": 256 },
35
- "bindingSets": {
36
- "static": [
37
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
38
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$outputElement" }
39
- ],
40
- "genericScales": [
41
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
42
- {
43
- "name": "scales",
44
- "arg": "scales",
45
- "semantic": "scales",
46
- "buffer": { "type": "read-only-storage" },
47
- "elementType": "f32"
48
- },
49
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" }
50
- ],
51
- "nchw": [
52
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
53
- {
54
- "name": "scales",
55
- "arg": "scales",
56
- "semantic": "scales",
57
- "buffer": { "type": "read-only-storage" },
58
- "elementType": "f32"
59
- },
60
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$outputElement" },
61
- {
62
- "name": "params",
63
- "semantic": "kernel.params",
64
- "buffer": { "type": "uniform" },
65
- "struct": {
66
- "name": "Params",
67
- "fields": [
68
- { "name": "count", "type": "u32", "value": "numel(shapes.Y)" },
69
- { "name": "C", "type": "u32", "value": "dim(shapes.X, 1)" },
70
- { "name": "inH", "type": "u32", "value": "dim(shapes.X, 2)" },
71
- { "name": "inW", "type": "u32", "value": "dim(shapes.X, 3)" },
72
- { "name": "outH", "type": "u32", "value": "dim(shapes.Y, 2)" },
73
- { "name": "outW", "type": "u32", "value": "dim(shapes.Y, 3)" }
74
- ]
75
- }
76
- }
77
- ]
78
  },
79
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
80
  "variants": [
81
  {
82
  "id": "nearest_integer_scale_vec4",
83
  "priority": 21,
84
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "numel(shapes.Y) > 0", "attrs.mode == \"nearest\"", "(dim(shapes.Y, 2) == dim(shapes.X, 2) * 2 or dim(shapes.Y, 2) == dim(shapes.X, 2) * 3 or dim(shapes.Y, 2) == dim(shapes.X, 2) * 4)", "(dim(shapes.Y, 3) == dim(shapes.X, 3) * 2 or dim(shapes.Y, 3) == dim(shapes.X, 3) * 3 or dim(shapes.Y, 3) == dim(shapes.X, 3) * 4)", "dim(shapes.Y, 3) % 4 == 0", "f16Ok(dtypes.T)"],
85
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
86
  "passes": [
87
  {
88
  "id": "main",
89
  "name": "Upsample.nearestIntegerScaleVec4",
90
- "source": {
91
- "shader": "resize-nearest-integer-scale.wgsl.jinja",
92
- "inputs": { "xShape": "shapes.X", "yShape": "shapes.Y", "vec4": true }
93
- },
94
- "bindings": "static",
95
- "dispatch": { "gridStride": "numel(shapes.Y) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
96
  }
97
  ]
98
  },
99
  {
100
  "id": "nearest_integer_scale",
101
  "priority": 20,
102
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "numel(shapes.Y) > 0", "attrs.mode == \"nearest\"", "(dim(shapes.Y, 2) == dim(shapes.X, 2) * 2 or dim(shapes.Y, 2) == dim(shapes.X, 2) * 3 or dim(shapes.Y, 2) == dim(shapes.X, 2) * 4)", "(dim(shapes.Y, 3) == dim(shapes.X, 3) * 2 or dim(shapes.Y, 3) == dim(shapes.X, 3) * 3 or dim(shapes.Y, 3) == dim(shapes.X, 3) * 4)", "f16Ok(dtypes.T)"],
103
- "constants": { "outputElement": "dtypes.T" },
104
  "passes": [
105
  {
106
  "id": "main",
107
  "name": "Upsample.nearestIntegerScale",
108
- "source": {
109
- "shader": "resize-nearest-integer-scale.wgsl.jinja",
110
- "inputs": { "xShape": "shapes.X", "yShape": "shapes.Y", "vec4": false }
111
- },
112
- "bindings": "static",
113
- "dispatch": { "gridStride": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
114
  }
115
  ]
116
  },
117
  {
118
  "id": "linear_2x_stencil_vec4",
119
  "priority": 20,
120
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "numel(shapes.Y) > 0", "attrs.mode == \"linear\"", "dim(shapes.Y, 2) == dim(shapes.X, 2) * 2", "dim(shapes.Y, 3) == dim(shapes.X, 3) * 2", "dim(shapes.Y, 3) % 4 == 0", "f16Ok(dtypes.T)"],
121
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
122
  "passes": [
123
  {
124
  "id": "main",
125
  "name": "Upsample.linear2xStencilVec4",
126
- "source": {
127
- "shader": "resize-linear-2x-stencil.wgsl.jinja",
128
- "inputs": { "xShape": "shapes.X", "yShape": "shapes.Y", "coordinateMode": "\"asymmetric\"", "vec4": true }
 
 
 
 
129
  },
130
- "bindings": "static",
131
- "dispatch": { "gridStride": "numel(shapes.Y) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
132
  }
133
  ]
134
  },
135
  {
136
  "id": "linear_2x_stencil",
137
  "priority": 19,
138
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "numel(shapes.Y) > 0", "attrs.mode == \"linear\"", "dim(shapes.Y, 2) == dim(shapes.X, 2) * 2", "dim(shapes.Y, 3) == dim(shapes.X, 3) * 2", "f16Ok(dtypes.T)"],
139
- "constants": { "outputElement": "dtypes.T" },
140
  "passes": [
141
  {
142
  "id": "main",
143
  "name": "Upsample.linear2xStencil",
144
- "source": {
145
- "shader": "resize-linear-2x-stencil.wgsl.jinja",
146
- "inputs": { "xShape": "shapes.X", "yShape": "shapes.Y", "coordinateMode": "\"asymmetric\"" }
 
 
 
147
  },
148
- "bindings": "static",
149
- "dispatch": { "gridStride": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
150
  }
151
  ]
152
  },
153
  {
154
  "id": "nchw_rank4_vec4",
155
  "priority": 1,
156
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "dim(shapes.Y, 3) % 4 == 0", "f16Ok(dtypes.T)"],
157
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
158
  "passes": [
159
  {
160
  "id": "main",
161
  "name": "Upsample.Vec4",
162
- "source": {
163
- "shader": "resize-coord-transform.wgsl.jinja",
164
- "inputs": {
165
- "mode": "attrs.mode",
166
- "coordinateMode": "\"asymmetric\"",
167
- "nearestMode": "\"floor\"",
168
- "vec4": true,
169
- "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
170
- "scalesBuffer": true
171
- }
172
  },
173
- "bindings": "nchw",
174
- "dispatch": { "threads": "numel(shapes.Y) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
175
  }
176
  ]
177
  },
178
  {
179
  "id": "nchw_rank4",
180
- "when": ["ranks.X == 4", "ranks.Y == 4", "dim(shapes.X, 0) == dim(shapes.Y, 0)", "dim(shapes.X, 1) == dim(shapes.Y, 1)", "f16Ok(dtypes.T)"],
181
- "constants": { "outputElement": "dtypes.T" },
182
  "passes": [
183
  {
184
  "id": "main",
185
  "name": "Upsample",
186
- "source": {
187
- "shader": "resize-coord-transform.wgsl.jinja",
188
- "inputs": {
189
- "mode": "attrs.mode",
190
- "coordinateMode": "\"asymmetric\"",
191
- "nearestMode": "\"floor\"",
192
- "scalesBuffer": true
193
- }
194
  },
195
- "bindings": "nchw",
196
- "dispatch": { "threads": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
197
  }
198
  ]
199
  },
200
  {
201
  "id": "generic_rank_n",
202
  "priority": 0,
203
- "when": ["ranks.X == ranks.Y", "ranks.X >= 1", "numel(shapes.Y) > 0", "(ranks.X != 4 or dim(shapes.X, 0) != dim(shapes.Y, 0) or dim(shapes.X, 1) != dim(shapes.Y, 1))", "f16Ok(dtypes.T)"],
204
  "passes": [
205
  {
206
  "id": "main",
207
  "name": "Upsample.Generic",
208
- "source": {
209
- "shader": "resize-generic.wgsl.jinja",
210
- "inputs": {
211
- "xShape": "shapes.X",
212
- "yShape": "shapes.Y",
213
- "mode": "attrs.mode",
214
- "coordinateMode": "\"asymmetric\"",
215
- "nearestMode": "\"floor\"",
216
- "scalesBuffer": true
217
- }
218
  },
219
- "bindings": "genericScales",
220
- "dispatch": { "gridStride": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
221
  }
222
  ]
223
  }
 
2
  "domain": "ai.onnx",
3
  "name": "Upsample",
4
  "sinceVersion": 9,
5
+ "inputs": { "x": { "onnx": "X", "dtype": "T" }, "scales": { "dtype": "S", "rank": 1 } },
6
+ "outputs": { "y": { "onnx": "Y", "dtype": "T", "rank": "ranks.x" } },
7
+ "attributes": { "mode": { "default": "nearest" } },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  "attributeConstraints": { "mode": { "values": ["nearest", "linear"] } },
9
  "typeConstraints": { "T": ["float32", "float16", "int32", "int8", "uint8"], "S": ["float32"] },
10
+ "tunables": { "WORKGROUP_SIZE": { "default": 256 } },
11
+ "derive": { "scalar": "dtypes.T" },
12
+ "when": ["f16Ok(dtypes.T)"],
13
+ "bindings": {
14
+ "y": { "buffer": "storage", "elementType": "$outputElement" },
15
+ "scales": { "buffer": "read-only-storage", "elementType": "f32" },
16
+ "params": {
17
+ "buffer": "uniform",
18
+ "struct": [
19
+ { "name": "count", "type": "u32", "value": "numel(shapes.y)" },
20
+ { "name": "C", "type": "u32", "value": "dim(shapes.x, 1)" },
21
+ { "name": "inH", "type": "u32", "value": "dim(shapes.x, 2)" },
22
+ { "name": "inW", "type": "u32", "value": "dim(shapes.x, 3)" },
23
+ { "name": "outH", "type": "u32", "value": "dim(shapes.y, 2)" },
24
+ { "name": "outW", "type": "u32", "value": "dim(shapes.y, 3)" }
25
+ ]
26
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  },
 
28
  "variants": [
29
  {
30
  "id": "nearest_integer_scale_vec4",
31
  "priority": 21,
32
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)", "numel(shapes.y) > 0", "attrs.mode == \"nearest\"", "(dim(shapes.y, 2) == dim(shapes.x, 2) * 2 or dim(shapes.y, 2) == dim(shapes.x, 2) * 3 or dim(shapes.y, 2) == dim(shapes.x, 2) * 4)", "(dim(shapes.y, 3) == dim(shapes.x, 3) * 2 or dim(shapes.y, 3) == dim(shapes.x, 3) * 3 or dim(shapes.y, 3) == dim(shapes.x, 3) * 4)", "dim(shapes.y, 3) % 4 == 0"],
33
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
34
  "passes": [
35
  {
36
  "id": "main",
37
  "name": "Upsample.nearestIntegerScaleVec4",
38
+ "shader": "resize-nearest-integer-scale.wgsl.jinja",
39
+ "derive": { "xShape": "shapes.x", "yShape": "shapes.y", "vec4": true },
40
+ "bindings": ["x", "y"],
41
+ "dispatch": {
42
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
43
+ "y": 1,
44
+ "z": 1
45
+ }
46
  }
47
  ]
48
  },
49
  {
50
  "id": "nearest_integer_scale",
51
  "priority": 20,
52
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)", "numel(shapes.y) > 0", "attrs.mode == \"nearest\"", "(dim(shapes.y, 2) == dim(shapes.x, 2) * 2 or dim(shapes.y, 2) == dim(shapes.x, 2) * 3 or dim(shapes.y, 2) == dim(shapes.x, 2) * 4)", "(dim(shapes.y, 3) == dim(shapes.x, 3) * 2 or dim(shapes.y, 3) == dim(shapes.x, 3) * 3 or dim(shapes.y, 3) == dim(shapes.x, 3) * 4)"],
53
+ "derive": { "outputElement": "dtypes.T" },
54
  "passes": [
55
  {
56
  "id": "main",
57
  "name": "Upsample.nearestIntegerScale",
58
+ "shader": "resize-nearest-integer-scale.wgsl.jinja",
59
+ "derive": { "xShape": "shapes.x", "yShape": "shapes.y", "vec4": false },
60
+ "bindings": ["x", "y"],
61
+ "dispatch": {
62
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
63
+ "y": 1,
64
+ "z": 1
65
+ }
66
  }
67
  ]
68
  },
69
  {
70
  "id": "linear_2x_stencil_vec4",
71
  "priority": 20,
72
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)", "numel(shapes.y) > 0", "attrs.mode == \"linear\"", "dim(shapes.y, 2) == dim(shapes.x, 2) * 2", "dim(shapes.y, 3) == dim(shapes.x, 3) * 2", "dim(shapes.y, 3) % 4 == 0"],
73
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
74
  "passes": [
75
  {
76
  "id": "main",
77
  "name": "Upsample.linear2xStencilVec4",
78
+ "shader": "resize-linear-2x-stencil.wgsl.jinja",
79
+ "derive": {
80
+ "xShape": "shapes.x",
81
+ "yShape": "shapes.y",
82
+ "coordinateMode": "\"asymmetric\"",
83
+ "vec4": true,
84
+ "scalesBuffer": true
85
  },
86
+ "bindings": ["x", "scales", "y"],
87
+ "dispatch": {
88
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
89
+ "y": 1,
90
+ "z": 1
91
+ }
92
  }
93
  ]
94
  },
95
  {
96
  "id": "linear_2x_stencil",
97
  "priority": 19,
98
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)", "numel(shapes.y) > 0", "attrs.mode == \"linear\"", "dim(shapes.y, 2) == dim(shapes.x, 2) * 2", "dim(shapes.y, 3) == dim(shapes.x, 3) * 2"],
99
+ "derive": { "outputElement": "dtypes.T" },
100
  "passes": [
101
  {
102
  "id": "main",
103
  "name": "Upsample.linear2xStencil",
104
+ "shader": "resize-linear-2x-stencil.wgsl.jinja",
105
+ "derive": {
106
+ "xShape": "shapes.x",
107
+ "yShape": "shapes.y",
108
+ "coordinateMode": "\"asymmetric\"",
109
+ "scalesBuffer": true
110
  },
111
+ "bindings": ["x", "scales", "y"],
112
+ "dispatch": {
113
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
114
+ "y": 1,
115
+ "z": 1
116
+ }
117
  }
118
  ]
119
  },
120
  {
121
  "id": "nchw_rank4_vec4",
122
  "priority": 1,
123
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)", "dim(shapes.y, 3) % 4 == 0"],
124
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"", "outputElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
125
  "passes": [
126
  {
127
  "id": "main",
128
  "name": "Upsample.Vec4",
129
+ "shader": "resize-coord-transform.wgsl.jinja",
130
+ "derive": {
131
+ "modeSpec": "attrs.mode",
132
+ "coordinateMode": "\"asymmetric\"",
133
+ "nearestMode": "\"floor\"",
134
+ "vec4": true,
135
+ "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
136
+ "scalesBuffer": true
 
 
137
  },
138
+ "bindings": ["x", "scales", "y", "params"],
139
+ "dispatch": {
140
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
141
+ "y": "ceilDiv(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
142
+ "z": 1
143
+ }
144
  }
145
  ]
146
  },
147
  {
148
  "id": "nchw_rank4",
149
+ "when": ["ranks.x == 4", "ranks.y == 4", "dim(shapes.x, 0) == dim(shapes.y, 0)", "dim(shapes.x, 1) == dim(shapes.y, 1)"],
150
+ "derive": { "outputElement": "dtypes.T" },
151
  "passes": [
152
  {
153
  "id": "main",
154
  "name": "Upsample",
155
+ "shader": "resize-coord-transform.wgsl.jinja",
156
+ "derive": {
157
+ "modeSpec": "attrs.mode",
158
+ "coordinateMode": "\"asymmetric\"",
159
+ "nearestMode": "\"floor\"",
160
+ "scalesBuffer": true
 
 
161
  },
162
+ "bindings": ["x", "scales", "y", "params"],
163
+ "dispatch": {
164
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), 65535)",
165
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), 65535)",
166
+ "z": 1
167
+ }
168
  }
169
  ]
170
  },
171
  {
172
  "id": "generic_rank_n",
173
  "priority": 0,
174
+ "when": ["ranks.x == ranks.y", "ranks.x >= 1", "numel(shapes.y) > 0", "(ranks.x != 4 or dim(shapes.x, 0) != dim(shapes.y, 0) or dim(shapes.x, 1) != dim(shapes.y, 1))"],
175
  "passes": [
176
  {
177
  "id": "main",
178
  "name": "Upsample.Generic",
179
+ "shader": "resize-generic.wgsl.jinja",
180
+ "derive": {
181
+ "xShape": "shapes.x",
182
+ "yShape": "shapes.y",
183
+ "modeSpec": "attrs.mode",
184
+ "coordinateMode": "\"asymmetric\"",
185
+ "nearestMode": "\"floor\"",
186
+ "scalesBuffer": true
 
 
187
  },
188
+ "bindings": ["x", "scales", { "arg": "y" }],
189
+ "dispatch": {
190
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
191
+ "y": 1,
192
+ "z": 1
193
+ }
194
  }
195
  ]
196
  }
build/webgpu/metadata.json CHANGED
@@ -1,21 +1,32 @@
1
  {
2
  "name": "ai.onnx.Upsample",
3
- "id": "_ai_onnx_upsample_webgpu_a452be7",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "XMODCq/PrRSKwq5XRxOvlQoOhjCxnml+3+c9dAA0vv8=",
11
- "manifest.json": "RXmICJOLSLjz7QW9zMGE5gJgKMAeqkER3fq+e7O64K0=",
12
- "resize-coord-transform.wgsl.jinja": "+HABv5jMRtHp8VOXFY1Wi/ZjTIFxOJe6dExMJFAZ7H0=",
13
- "resize-generic.wgsl.jinja": "JYlXh0pPD1wW2uCSm9KhS+rJqNimezHaq4sKb+iqv0M=",
14
- "resize-linear-2x-stencil.wgsl.jinja": "cyI43wkZQdrjNUiaXRP2bfvLBuy04VvQQ5rFTuYXMd0=",
15
- "resize-nearest-integer-scale.wgsl.jinja": "fgry1tCB+VAmoiWgqHWsy3EL2G47mtvtZhVPP27Z0Fg=",
16
- "test.json": "FWRXvZ7UwTNUb3+n9G8xf4jp53QFrImqWPe3z0yUcqo="
17
  }
18
  },
19
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
20
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.Upsample" }
 
 
 
 
 
 
 
 
 
 
 
21
  }
 
1
  {
2
  "name": "ai.onnx.Upsample",
3
+ "id": "_ai_onnx_upsample_webgpu_226c6a3",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "F6Vym+nah84vMiKJZvh+qKarMEzp84955NrGH18TWBs=",
11
+ "manifest.json": "ZZF7ZLeywO+hHftIgE5IeP4cufl+b3GKDYU9AV/inS4=",
12
+ "resize-coord-transform.wgsl.jinja": "WzhlhFxNz877wX29wcns2y+IJTC5g/IzYX5NEH27n6s=",
13
+ "resize-generic.wgsl.jinja": "bAUivK8i1ccgtOFXGB7Y9kTjyRfql5TUb+sikbvOMPk=",
14
+ "resize-linear-2x-stencil.wgsl.jinja": "r98ldVQUx6+bQnoqw9Fjt4JtygJqj5yEKXAv2DOJ5ic=",
15
+ "resize-nearest-integer-scale.wgsl.jinja": "GnmGPu4MyJrir+dzp3ucQ/ADVW9mJDcU1czn6L/RPF8=",
16
+ "test.json": "Cmo87nuUyMyjpettO/Nks1mu7Z00DCEnYyaHf5fQNsI="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.0",
22
+ "variants": {
23
+ "nearest_integer_scale_vec4": ["resize-nearest-integer-scale.wgsl.jinja"],
24
+ "nearest_integer_scale": ["resize-nearest-integer-scale.wgsl.jinja"],
25
+ "linear_2x_stencil_vec4": ["resize-linear-2x-stencil.wgsl.jinja"],
26
+ "linear_2x_stencil": ["resize-linear-2x-stencil.wgsl.jinja"],
27
+ "nchw_rank4_vec4": ["resize-coord-transform.wgsl.jinja"],
28
+ "nchw_rank4": ["resize-coord-transform.wgsl.jinja"],
29
+ "generic_rank_n": ["resize-generic.wgsl.jinja"]
30
+ }
31
+ }
32
  }
build/webgpu/resize-coord-transform.wgsl.jinja CHANGED
@@ -3,9 +3,8 @@
3
  // Nearest-index rounding is load-bearing for ONNX compatibility:
4
  // round_prefer_ceil ties toward positive infinity, while round_prefer_floor
5
  // uses its specified epsilon.
6
- {% if usesF16 %}
7
- enable f16;
8
- {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
11
 
@@ -19,7 +18,7 @@ fn source_coord(out_coord: u32, in_size: u32, out_size: u32, explicit_scale: f32
19
  /* Backend-stable nearest-neighbor coordinate selection. Inferred align-corners,
20
  * asymmetric, and half-pixel transforms use exact integer ratios. This matters
21
  * at nearest-mode ties: reassociating an f32 division into a reciprocal
22
- * multiply can move an exact 0.5 to either side of the boundary. */{% if source.mode == "nearest" %}
23
  fn nearest_index(v: f32) -> i32 {
24
  return i32(floor(v));
25
  }
@@ -67,7 +66,7 @@ fn input_at(base: u32, h: i32, w: i32) -> f32 {
67
  return f32(x[base + hh * params.inW + ww]);
68
  }
69
 
70
- {% if source.mode == "linear" %}
71
  // Clamp before choosing neighbors. At an edge both neighbors coincide and
72
  // receive half weight; the four-term order is observable for integer outputs.
73
  fn bilinear_sample(base: u32, source_y: f32, source_x: f32) -> f32 {
@@ -92,20 +91,24 @@ fn bilinear_sample(base: u32, source_y: f32, source_x: f32) -> f32 {
92
  {% endif %}
93
 
94
  @compute @workgroup_size({{ wg }})
95
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
96
  // 2D-folded flat index: gid.y carries the high bits past the
97
- // maxComputeWorkgroupsPerDimension limit.
98
- let flat = gid.x + gid.y * nwg.x * {{ wg }}u;
99
- // Upsample: the per-axis scales are a runtime input tensor, unknown at plan
100
- // build time, so read the true spatial scales (NCHW axes 2/3) directly instead
101
- // of inferring out/in (floor(in*scale) loses the fractional part when the scale
102
- // is non-integer, e.g. in=2 scale=2.333 -> out=4 -> inferred 2.0 != 2.333).
103
  let scaleH_val = f32(scales[2]);
104
  let scaleW_val = f32(scales[3]);
105
- {% if source.vec4 %}
106
- // One thread owns 4 contiguous output columns (outW % 4 == 0, so a 4-group
107
- // never crosses a row): all 4 lanes share oh/c/n and the source row coord sy;
108
- // only sx differs per lane. One 128-bit vec4 store. Bit-identical to scalar.
 
 
 
 
109
  let count4 = params.count / 4u;
110
  if (flat >= count4) { return; }
111
  let i = flat * 4u;
@@ -115,14 +118,49 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
115
  let c = (row / params.outH) % params.C;
116
  let n = row / (params.outH * params.C);
117
  let base = (n * params.C + c) * params.inH * params.inW;
118
- {% if source.mode != "nearest" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  let sy = source_coord(oh, params.inH, params.outH, scaleH_val);
120
  let sx0 = source_coord(ow0, params.inW, params.outW, scaleW_val);
121
  let sx1 = source_coord(ow0 + 1u, params.inW, params.outW, scaleW_val);
122
  let sx2 = source_coord(ow0 + 2u, params.inW, params.outW, scaleW_val);
123
  let sx3 = source_coord(ow0 + 3u, params.inW, params.outW, scaleW_val);
124
  {% endif %}
125
- {% if source.mode == "nearest" %}
126
  let ih = nearest_coord_index(oh, params.inH, params.outH, scaleH_val);
127
  let r = vec4<f32>(
128
  input_at(base, ih, nearest_coord_index(ow0, params.inW, params.outW, scaleW_val)),
@@ -136,7 +174,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
136
  bilinear_sample(base, sy, sx2),
137
  bilinear_sample(base, sy, sx3));
138
  {% endif %}
139
- y[flat] = {{ source.vecType }}(r);
 
 
 
 
140
  {% else %}
141
  let i = flat;
142
  if (i >= params.count) { return; }
@@ -145,11 +187,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
145
  let c = (i / (params.outW * params.outH)) % params.C;
146
  let n = i / (params.outW * params.outH * params.C);
147
  let base = (n * params.C + c) * params.inH * params.inW;
148
- {% if source.mode != "nearest" %}
149
  let sy = source_coord(oh, params.inH, params.outH, scaleH_val);
150
  let sx = source_coord(ow, params.inW, params.outW, scaleW_val);
151
  {% endif %}
152
- {% if source.mode == "nearest" %}
153
  let ih = nearest_coord_index(oh, params.inH, params.outH, scaleH_val);
154
  let iw = nearest_coord_index(ow, params.inW, params.outW, scaleW_val);
155
  let result = input_at(base, ih, iw);
 
3
  // Nearest-index rounding is load-bearing for ONNX compatibility:
4
  // round_prefer_ceil ties toward positive infinity, while round_prefer_floor
5
  // uses its specified epsilon.
6
+ {% set scalesBuffer = scalesBuffer is defined and scalesBuffer %}
7
+ {% set scalarX4 = scalarX4 is defined and scalarX4 %}
 
8
  {{ env.wgsl.resourceDeclarations }}
9
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
10
 
 
18
  /* Backend-stable nearest-neighbor coordinate selection. Inferred align-corners,
19
  * asymmetric, and half-pixel transforms use exact integer ratios. This matters
20
  * at nearest-mode ties: reassociating an f32 division into a reciprocal
21
+ * multiply can move an exact 0.5 to either side of the boundary. */{% if modeSpec == "nearest" %}
22
  fn nearest_index(v: f32) -> i32 {
23
  return i32(floor(v));
24
  }
 
66
  return f32(x[base + hh * params.inW + ww]);
67
  }
68
 
69
+ {% if modeSpec == "linear" %}
70
  // Clamp before choosing neighbors. At an edge both neighbors coincide and
71
  // receive half weight; the four-term order is observable for integer outputs.
72
  fn bilinear_sample(base: u32, source_y: f32, source_x: f32) -> f32 {
 
91
  {% endif %}
92
 
93
  @compute @workgroup_size({{ wg }})
94
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
95
  // 2D-folded flat index: gid.y carries the high bits past the
96
+ // per-axis dispatch fold width.
97
+ let flat = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wg }}u;
98
+ {% if scalesBuffer %}
99
+ // Read spatial scales from NCHW axes 2 and 3. Inferring a scale from the
100
+ // rounded output extent would discard its fractional part; for example,
101
+ // input 2 scaled by 2.333 has output extent 4, whose extent ratio is 2.0.
102
  let scaleH_val = f32(scales[2]);
103
  let scaleW_val = f32(scales[3]);
104
+ {% else %}
105
+ let scaleH_val = params.scaleH;
106
+ let scaleW_val = params.scaleW;
107
+ {% endif %}
108
+ {% if vec4 %}
109
+ // One invocation owns four contiguous output columns. Because outW is
110
+ // divisible by four, the group never crosses a row; all lanes share n, c,
111
+ // oh, and source row coordinate sy, while sx differs per lane.
112
  let count4 = params.count / 4u;
113
  if (flat >= count4) { return; }
114
  let i = flat * 4u;
 
118
  let c = (row / params.outH) % params.C;
119
  let n = row / (params.outH * params.C);
120
  let base = (n * params.C + c) * params.inH * params.inW;
121
+ {% if modeSpec != "nearest" %}
122
+ let sy = source_coord(oh, params.inH, params.outH, scaleH_val);
123
+ let sx0 = source_coord(ow0, params.inW, params.outW, scaleW_val);
124
+ let sx1 = source_coord(ow0 + 1u, params.inW, params.outW, scaleW_val);
125
+ let sx2 = source_coord(ow0 + 2u, params.inW, params.outW, scaleW_val);
126
+ let sx3 = source_coord(ow0 + 3u, params.inW, params.outW, scaleW_val);
127
+ {% endif %}
128
+ {% if modeSpec == "nearest" %}
129
+ let ih = nearest_coord_index(oh, params.inH, params.outH, scaleH_val);
130
+ let r = vec4<f32>(
131
+ input_at(base, ih, nearest_coord_index(ow0, params.inW, params.outW, scaleW_val)),
132
+ input_at(base, ih, nearest_coord_index(ow0 + 1u, params.inW, params.outW, scaleW_val)),
133
+ input_at(base, ih, nearest_coord_index(ow0 + 2u, params.inW, params.outW, scaleW_val)),
134
+ input_at(base, ih, nearest_coord_index(ow0 + 3u, params.inW, params.outW, scaleW_val)));
135
+ {% else %}
136
+ let r = vec4<f32>(
137
+ bilinear_sample(base, sy, sx0),
138
+ bilinear_sample(base, sy, sx1),
139
+ bilinear_sample(base, sy, sx2),
140
+ bilinear_sample(base, sy, sx3));
141
+ {% endif %}
142
+ y[flat] = {{ vecType }}(r);
143
+ {% elif scalarX4 %}
144
+ // Each invocation computes four row-local columns without a vec4 storage
145
+ // binding. The final one to three columns use guarded scalar stores.
146
+ let groups_per_row = (params.outW + 3u) / 4u;
147
+ let group_count = (params.count / params.outW) * groups_per_row;
148
+ if (flat >= group_count) { return; }
149
+ let group_in_row = flat % groups_per_row;
150
+ let row = flat / groups_per_row;
151
+ let ow0 = group_in_row * 4u;
152
+ let oh = row % params.outH;
153
+ let c = (row / params.outH) % params.C;
154
+ let n = row / (params.outH * params.C);
155
+ let base = (n * params.C + c) * params.inH * params.inW;
156
+ {% if modeSpec != "nearest" %}
157
  let sy = source_coord(oh, params.inH, params.outH, scaleH_val);
158
  let sx0 = source_coord(ow0, params.inW, params.outW, scaleW_val);
159
  let sx1 = source_coord(ow0 + 1u, params.inW, params.outW, scaleW_val);
160
  let sx2 = source_coord(ow0 + 2u, params.inW, params.outW, scaleW_val);
161
  let sx3 = source_coord(ow0 + 3u, params.inW, params.outW, scaleW_val);
162
  {% endif %}
163
+ {% if modeSpec == "nearest" %}
164
  let ih = nearest_coord_index(oh, params.inH, params.outH, scaleH_val);
165
  let r = vec4<f32>(
166
  input_at(base, ih, nearest_coord_index(ow0, params.inW, params.outW, scaleW_val)),
 
174
  bilinear_sample(base, sy, sx2),
175
  bilinear_sample(base, sy, sx3));
176
  {% endif %}
177
+ let i = row * params.outW + ow0;
178
+ y[i] = {{ scalar }}(r.x);
179
+ if (ow0 + 1u < params.outW) { y[i + 1u] = {{ scalar }}(r.y); }
180
+ if (ow0 + 2u < params.outW) { y[i + 2u] = {{ scalar }}(r.z); }
181
+ if (ow0 + 3u < params.outW) { y[i + 3u] = {{ scalar }}(r.w); }
182
  {% else %}
183
  let i = flat;
184
  if (i >= params.count) { return; }
 
187
  let c = (i / (params.outW * params.outH)) % params.C;
188
  let n = i / (params.outW * params.outH * params.C);
189
  let base = (n * params.C + c) * params.inH * params.inW;
190
+ {% if modeSpec != "nearest" %}
191
  let sy = source_coord(oh, params.inH, params.outH, scaleH_val);
192
  let sx = source_coord(ow, params.inW, params.outW, scaleW_val);
193
  {% endif %}
194
+ {% if modeSpec == "nearest" %}
195
  let ih = nearest_coord_index(oh, params.inH, params.outH, scaleH_val);
196
  let iw = nearest_coord_index(ow, params.inW, params.outW, scaleW_val);
197
  let result = input_at(base, ih, iw);
build/webgpu/resize-generic.wgsl.jinja CHANGED
@@ -1,18 +1,13 @@
1
- // Rank-generic Resize for arbitrary ranks and resized axes, including
2
- // tf_crop_and_resize with an ROI and extrapolation value. The per-axis
3
- // coordinate transform follows the rank-specialized paths so half_pixel,
4
- // pytorch_half_pixel, asymmetric, and align_corners stay
5
- // consistent. Linear interpolation runs only over resized axes; passthrough
6
- // axes contribute one tap.
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
- {%- set xs = source.xShape %}
13
- {% set ys = source.yShape %}
14
  {% set r = xs | length -%}
15
- {% set usesExplicitScale = source.coordinateMode not in ["align_corners", "tf_crop_and_resize"] %}
16
 
17
  {% set resolvedScales = namespace(values = []) %}
18
  {% for a in range(r) %}
@@ -36,10 +31,9 @@ enable f16;
36
 
37
  const WG: u32 = {{ wg }}u;
38
 
39
- // Runtime form used by the shared exact-nearest helper. The generic kernel's
40
- // interpolation paths keep their baked expressions below; nearest uses this
41
- // function only when an exact inferred-ratio path is unavailable.
42
- {% if source.mode == "nearest" and k > 0 and (source.roi is not defined or (source.roi | length) == 0) and source.coordinateMode != "align_corners" %}
43
 
44
  fn source_coord(out_coord: u32, in_size: u32, out_size: u32, explicit_scale: f32) -> f32 {
45
  return select(f32(out_coord) * f32(in_size) / f32(out_size), f32(out_coord) / explicit_scale, explicit_scale > 0.0);
@@ -57,10 +51,10 @@ fn source_coord(out_coord: u32, in_size: u32, out_size: u32, explicit_scale: f32
57
  (f32({{ oc }}) / scales[{{ a }}])
58
  {% endmacro %}
59
 
60
- {% if source.mode == "cubic" %}
61
  // Keys cubic convolution kernel with coefficient coeffA.
62
  fn cubic_weight(s_in: f32) -> f32 {
63
- let a = f32({{ source.coeffA }});
64
  let s = abs(s_in);
65
  let s2 = s * s;
66
  let s3 = s2 * s;
@@ -70,15 +64,15 @@ fn cubic_weight(s_in: f32) -> f32 {
70
  }
71
  {% endif %}
72
 
73
- {% if source.mode == "nearest" and k > 0 %}
74
  /* Backend-stable nearest-neighbor coordinate selection. Inferred align-corners,
75
  * asymmetric, and half-pixel transforms use exact integer ratios. This matters
76
  * at nearest-mode ties: reassociating an f32 division into a reciprocal
77
- * multiply can move an exact 0.5 to either side of the boundary. */{% set ratioMode = source.coordinateMode in ["align_corners", "asymmetric"] %}
78
- {% set inferredHalfPixelMode = source.coordinateMode in ["pytorch_half_pixel", "half_pixel", "half_pixel_symmetric"] %}
79
- {% set noRoi = source.roi is not defined or (source.roi | length) == 0 %}
80
- {% set usesExplicitScale = source.coordinateMode not in ["align_corners", "tf_crop_and_resize"] %}
81
- {% if source.mode == "nearest" %}
82
  fn nearest_index(v: f32) -> i32 {
83
  return i32(floor(v));
84
  }
@@ -177,11 +171,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
177
  {% endfor %}
178
  // Per-axis source coordinates.
179
  {% for a in range(r) %}
180
- {% if a in resized.list and (source.mode != "nearest" or (source.roi is defined and source.roi)) %}
181
  let s{{ a }} = {{ src_coord(a, out_coord(a)) }};
182
  {% endif %}
183
  {% endfor %}
184
- {% if source.mode == "nearest" %}
185
  {% for a in range(r) %}
186
  {% if a in resized.list %}
187
  {% set explicitScale = "f32(scales[" ~ a ~ "])" %}
@@ -191,7 +185,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
191
  {% endif %}
192
  {% endfor %}
193
  y[i] = {{ scalar }}(f32(x[0u{% for a in range(r) %} + idx{{ a }} * {{ istride(a) }}u{% endfor %}]));
194
- {% elif source.mode == "cubic" %}
195
  // Separable Keys cubic: 4 taps per resized axis (offsets -1..2), passthrough
196
  // axes contribute a single tap. 4^k taps total.
197
  {% for a in resized.list %}
 
1
+ {% set coeffA = coeffA | default(0) %}
2
+ // Rank-generic interpolation for arbitrary ranks and resized axes. Scales are
3
+ // read from the input buffer, and linear interpolation runs only over resized
4
+ // axes; passthrough axes contribute one tap.
 
 
 
 
 
5
  {{ env.wgsl.resourceDeclarations }}
6
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
7
+ {%- set xs = xShape %}
8
+ {% set ys = yShape %}
9
  {% set r = xs | length -%}
10
+ {% set usesExplicitScale = coordinateMode not in ["align_corners", "tf_crop_and_resize"] %}
11
 
12
  {% set resolvedScales = namespace(values = []) %}
13
  {% for a in range(r) %}
 
31
 
32
  const WG: u32 = {{ wg }}u;
33
 
34
+ // Runtime coordinate form for nearest interpolation when an exact
35
+ // inferred-ratio expression is unavailable.
36
+ {% if modeSpec == "nearest" and k > 0 and (roi is not defined or (roi | length) == 0) and coordinateMode != "align_corners" %}
 
37
 
38
  fn source_coord(out_coord: u32, in_size: u32, out_size: u32, explicit_scale: f32) -> f32 {
39
  return select(f32(out_coord) * f32(in_size) / f32(out_size), f32(out_coord) / explicit_scale, explicit_scale > 0.0);
 
51
  (f32({{ oc }}) / scales[{{ a }}])
52
  {% endmacro %}
53
 
54
+ {% if modeSpec == "cubic" %}
55
  // Keys cubic convolution kernel with coefficient coeffA.
56
  fn cubic_weight(s_in: f32) -> f32 {
57
+ let a = f32({{ coeffA }});
58
  let s = abs(s_in);
59
  let s2 = s * s;
60
  let s3 = s2 * s;
 
64
  }
65
  {% endif %}
66
 
67
+ {% if modeSpec == "nearest" and k > 0 %}
68
  /* Backend-stable nearest-neighbor coordinate selection. Inferred align-corners,
69
  * asymmetric, and half-pixel transforms use exact integer ratios. This matters
70
  * at nearest-mode ties: reassociating an f32 division into a reciprocal
71
+ * multiply can move an exact 0.5 to either side of the boundary. */{% set ratioMode = coordinateMode in ["align_corners", "asymmetric"] %}
72
+ {% set inferredHalfPixelMode = coordinateMode in ["pytorch_half_pixel", "half_pixel", "half_pixel_symmetric"] %}
73
+ {% set noRoi = roi is not defined or (roi | length) == 0 %}
74
+ {% set usesExplicitScale = coordinateMode not in ["align_corners", "tf_crop_and_resize"] %}
75
+ {% if modeSpec == "nearest" %}
76
  fn nearest_index(v: f32) -> i32 {
77
  return i32(floor(v));
78
  }
 
171
  {% endfor %}
172
  // Per-axis source coordinates.
173
  {% for a in range(r) %}
174
+ {% if a in resized.list and (modeSpec != "nearest" or (roi is defined and roi)) %}
175
  let s{{ a }} = {{ src_coord(a, out_coord(a)) }};
176
  {% endif %}
177
  {% endfor %}
178
+ {% if modeSpec == "nearest" %}
179
  {% for a in range(r) %}
180
  {% if a in resized.list %}
181
  {% set explicitScale = "f32(scales[" ~ a ~ "])" %}
 
185
  {% endif %}
186
  {% endfor %}
187
  y[i] = {{ scalar }}(f32(x[0u{% for a in range(r) %} + idx{{ a }} * {{ istride(a) }}u{% endfor %}]));
188
+ {% elif modeSpec == "cubic" %}
189
  // Separable Keys cubic: 4 taps per resized axis (offsets -1..2), passthrough
190
  // axes contribute a single tap. 4^k taps total.
191
  {% for a in resized.list %}
build/webgpu/resize-linear-2x-stencil.wgsl.jinja CHANGED
@@ -1,35 +1,55 @@
1
- // NCHW bilinear Resize with an exact 2x upscale on both spatial axes. The
 
2
  // interpolation weights collapse to fixed per-parity stencils, replacing
3
  // per-element floating-point coordinate divisions with integer parity tests
4
  // and constants.
5
  //
6
- // For half_pixel and pytorch_half_pixel, src = o / 2 - 0.25: even outputs use
7
- // floor q - 1 with fraction 0.75, and odd outputs use floor q with fraction
8
- // 0.25. For asymmetric mode, src = o / 2: even outputs use fraction 0.0 and odd
9
- // outputs use 0.5. align_corners remains on the generic fallback because its
10
- // position-dependent scale does not reduce to fixed stencils.
11
  //
12
  // Input coordinates clamp at the edges, including the q - 1 and q + 1 border
13
- // taps. The f32 weight construction is bit-identical to the generic fallback.
14
- {% if usesF16 %}
15
- enable f16;
16
- {% endif %}
17
  {{ env.wgsl.resourceDeclarations }}
18
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
19
 
20
  const WG: u32 = {{ wg }}u;
21
- const IN_H: u32 = {{ source.xShape[2] }}u;
22
- const IN_W: u32 = {{ source.xShape[3] }}u;
23
- const IN_HW: u32 = {{ source.xShape[2] * source.xShape[3] }}u;
24
- const OUT_H: u32 = {{ source.yShape[2] }}u;
25
- const OUT_W: u32 = {{ source.yShape[3] }}u;
26
- const COUNT: u32 = {{ source.yShape[0] * source.yShape[1] * source.yShape[2] * source.yShape[3] }}u;
27
 
28
  fn input_at(base: u32, h: i32, w: i32) -> f32 {
29
  let hh = u32(clamp(h, 0, i32(IN_H) - 1));
30
  let ww = u32(clamp(w, 0, i32(IN_W) - 1));
31
  return f32(x[base + hh * IN_W + ww]);
32
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  @compute @workgroup_size({{ wg }})
35
  fn main(
@@ -37,13 +57,16 @@ fn main(
37
  @builtin(num_workgroups) nwg: vec3<u32>
38
  ) {
39
  let stride = nwg.x * WG;
40
- {% if source.vec4 %}
 
 
 
41
  // Vec4 path: one thread owns 4 contiguous output columns (OUT_W % 4 == 0, so
42
  // a 4-group never crosses a row). All 4 lanes share the same oh -> same source
43
  // rows (y0,y1) and the same 4-column source window (qx0-1 .. qx0+2); load that
44
  // window once, compute the 4 bilinear results with the baked per-lane parity
45
- // weights, and emit a single 128-bit vec4 store. Bit-identical to the scalar
46
- // path (same input_at, same (1-w) split, same f32 operation order).
47
  let OUT_W4 = OUT_W / 4u;
48
  let COUNT4 = COUNT / 4u;
49
  for (var v = gid.x; v < COUNT4; v += stride) {
@@ -53,6 +76,16 @@ fn main(
53
  let nc = tt / OUT_H;
54
  let base = nc * IN_HW;
55
  let ow0 = ow4 * 4u;
 
 
 
 
 
 
 
 
 
 
56
  let qx0 = i32(ow0 / 2u);
57
  let y0 = i32(oh / 2u);
58
  let wy = select(0.0, 0.5, (oh & 1u) == 1u);
@@ -77,6 +110,12 @@ fn main(
77
  let oh = t % OUT_H;
78
  let nc = t / OUT_H;
79
  let base = nc * IN_HW;
 
 
 
 
 
 
80
  let qy = i32(oh / 2u);
81
  let qx = i32(ow / 2u);
82
  let oddY = (oh & 1u) == 1u;
 
1
+ {% set scalesBuffer = scalesBuffer if scalesBuffer is defined else false %}
2
+ // NCHW bilinear 2x upscale on both spatial axes. The
3
  // interpolation weights collapse to fixed per-parity stencils, replacing
4
  // per-element floating-point coordinate divisions with integer parity tests
5
  // and constants.
6
  //
7
+ // With asymmetric coordinates, src = o / 2: even outputs use fraction 0.0 and
8
+ // odd outputs use fraction 0.5.
 
 
 
9
  //
10
  // Input coordinates clamp at the edges, including the q - 1 and q + 1 border
11
+ // taps. Interpolation weights are constructed in f32 before the output cast.
 
 
 
12
  {{ env.wgsl.resourceDeclarations }}
13
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
14
 
15
  const WG: u32 = {{ wg }}u;
16
+ const IN_H: u32 = {{ xShape[2] }}u;
17
+ const IN_W: u32 = {{ xShape[3] }}u;
18
+ const IN_HW: u32 = {{ xShape[2] * xShape[3] }}u;
19
+ const OUT_H: u32 = {{ yShape[2] }}u;
20
+ const OUT_W: u32 = {{ yShape[3] }}u;
21
+ const COUNT: u32 = {{ yShape[0] * yShape[1] * yShape[2] * yShape[3] }}u;
22
 
23
  fn input_at(base: u32, h: i32, w: i32) -> f32 {
24
  let hh = u32(clamp(h, 0, i32(IN_H) - 1));
25
  let ww = u32(clamp(w, 0, i32(IN_W) - 1));
26
  return f32(x[base + hh * IN_W + ww]);
27
  }
28
+ {% if scalesBuffer %}
29
+
30
+ // The output shape condition floor(in * scale) == 2 * in also admits scales
31
+ // slightly above 2. Read the spatial scales: exactly 2 uses the stencils;
32
+ // otherwise sample asymmetric coordinates with source = output / scale.
33
+ fn general_sample(base: u32, oh: u32, ow: u32) -> f32 {
34
+ let sy = clamp(f32(oh) / f32(scales[2]), 0.0, f32(IN_H - 1u));
35
+ let sx = clamp(f32(ow) / f32(scales[3]), 0.0, f32(IN_W - 1u));
36
+ let y0 = i32(floor(sy));
37
+ let x0 = i32(floor(sx));
38
+ let y1 = min(y0 + 1, i32(IN_H) - 1);
39
+ let x1 = min(x0 + 1, i32(IN_W) - 1);
40
+ let same_y = y0 == y1;
41
+ let same_x = x0 == x1;
42
+ let wy0 = select(abs(sy - f32(y1)), 0.5, same_y);
43
+ let wy1 = select(abs(sy - f32(y0)), 0.5, same_y);
44
+ let wx0 = select(abs(sx - f32(x1)), 0.5, same_x);
45
+ let wx1 = select(abs(sx - f32(x0)), 0.5, same_x);
46
+ let v00 = input_at(base, y0, x0);
47
+ let v01 = input_at(base, y0, x1);
48
+ let v10 = input_at(base, y1, x0);
49
+ let v11 = input_at(base, y1, x1);
50
+ return wx0 * wy0 * v00 + wx1 * wy0 * v01 + wx0 * wy1 * v10 + wx1 * wy1 * v11;
51
+ }
52
+ {% endif %}
53
 
54
  @compute @workgroup_size({{ wg }})
55
  fn main(
 
57
  @builtin(num_workgroups) nwg: vec3<u32>
58
  ) {
59
  let stride = nwg.x * WG;
60
+ {% if scalesBuffer %}
61
+ let exact2x = f32(scales[2]) == 2.0 && f32(scales[3]) == 2.0;
62
+ {% endif %}
63
+ {% if vec4 %}
64
  // Vec4 path: one thread owns 4 contiguous output columns (OUT_W % 4 == 0, so
65
  // a 4-group never crosses a row). All 4 lanes share the same oh -> same source
66
  // rows (y0,y1) and the same 4-column source window (qx0-1 .. qx0+2); load that
67
  // window once, compute the 4 bilinear results with the baked per-lane parity
68
+ // weights, and emit a single vec4 store. Each component uses the same
69
+ // input_at calls, weight split, and f32 operation order.
70
  let OUT_W4 = OUT_W / 4u;
71
  let COUNT4 = COUNT / 4u;
72
  for (var v = gid.x; v < COUNT4; v += stride) {
 
76
  let nc = tt / OUT_H;
77
  let base = nc * IN_HW;
78
  let ow0 = ow4 * 4u;
79
+ {% if scalesBuffer %}
80
+ if (!exact2x) {
81
+ y[v] = vec4<{{ scalar }}>(vec4<f32>(
82
+ general_sample(base, oh, ow0),
83
+ general_sample(base, oh, ow0 + 1u),
84
+ general_sample(base, oh, ow0 + 2u),
85
+ general_sample(base, oh, ow0 + 3u)));
86
+ continue;
87
+ }
88
+ {% endif %}
89
  let qx0 = i32(ow0 / 2u);
90
  let y0 = i32(oh / 2u);
91
  let wy = select(0.0, 0.5, (oh & 1u) == 1u);
 
110
  let oh = t % OUT_H;
111
  let nc = t / OUT_H;
112
  let base = nc * IN_HW;
113
+ {% if scalesBuffer %}
114
+ if (!exact2x) {
115
+ y[i] = {{ scalar }}(general_sample(base, oh, ow));
116
+ continue;
117
+ }
118
+ {% endif %}
119
  let qy = i32(oh / 2u);
120
  let qx = i32(ow / 2u);
121
  let oddY = (oh & 1u) == 1u;
build/webgpu/resize-nearest-integer-scale.wgsl.jinja CHANGED
@@ -7,25 +7,22 @@
7
  // ow = q*s + r gives an offset whose magnitude is strictly below one half, so
8
  // ties cannot occur and every nearest rounding rule returns q. Downsampling and
9
  // other coordinate-mode combinations require the generic coordinate transform.
10
- {% if usesF16 %}
11
- enable f16;
12
- {% endif %}
13
  {{ env.wgsl.resourceDeclarations }}
14
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
15
- {%- set inH = source.xShape[2] %}
16
- {% set inW = source.xShape[3] %}
17
- {% set outH = source.yShape[2] %}
18
- {% set outW = source.yShape[3] %}
19
  {% set scaleW = ((outW / inW) | int) %}
20
- {% set rows = source.yShape[0] * source.yShape[1] * outH %}
21
  const WG: u32 = {{ wg }}u;
22
  const IN_W: u32 = {{ inW }}u;
23
  const IN_HW: u32 = {{ inH * inW }}u;
24
  const OUT_H: u32 = {{ outH }}u;
25
  const SCALE_H: u32 = {{ ((outH / inH) | int) }}u;
26
- {% if not source.vec4 or scaleW == 3 %}const SCALE_W: u32 = {{ scaleW }}u;
27
  {% endif %}
28
- {% if source.vec4 %}
29
  const OUT_W_VECS: u32 = {{ ((outW / 4) | int) }}u;
30
  const COUNT: u32 = {{ (((rows * outW) / 4) | int) }}u;
31
  {% else %}
@@ -39,7 +36,7 @@ fn main(
39
  @builtin(num_workgroups) nwg: vec3<u32>
40
  ) {
41
  let stride = nwg.x * WG;
42
- {% if source.vec4 %}
43
  for (var v = gid.x; v < COUNT; v += stride) {
44
  let ow0 = (v % OUT_W_VECS) * 4u;
45
  let t = v / OUT_W_VECS;
 
7
  // ow = q*s + r gives an offset whose magnitude is strictly below one half, so
8
  // ties cannot occur and every nearest rounding rule returns q. Downsampling and
9
  // other coordinate-mode combinations require the generic coordinate transform.
 
 
 
10
  {{ env.wgsl.resourceDeclarations }}
11
  {% set wg = workgroupSize if workgroupSize is defined else tunables.WORKGROUP_SIZE %}
12
+ {%- set inH = xShape[2] %}
13
+ {% set inW = xShape[3] %}
14
+ {% set outH = yShape[2] %}
15
+ {% set outW = yShape[3] %}
16
  {% set scaleW = ((outW / inW) | int) %}
17
+ {% set rows = yShape[0] * yShape[1] * outH %}
18
  const WG: u32 = {{ wg }}u;
19
  const IN_W: u32 = {{ inW }}u;
20
  const IN_HW: u32 = {{ inH * inW }}u;
21
  const OUT_H: u32 = {{ outH }}u;
22
  const SCALE_H: u32 = {{ ((outH / inH) | int) }}u;
23
+ {% if not vec4 or scaleW == 3 %}const SCALE_W: u32 = {{ scaleW }}u;
24
  {% endif %}
25
+ {% if vec4 %}
26
  const OUT_W_VECS: u32 = {{ ((outW / 4) | int) }}u;
27
  const COUNT: u32 = {{ (((rows * outW) / 4) | int) }}u;
28
  {% else %}
 
36
  @builtin(num_workgroups) nwg: vec3<u32>
37
  ) {
38
  let stride = nwg.x * WG;
39
+ {% if vec4 %}
40
  for (var v = gid.x; v < COUNT; v += stride) {
41
  let ow0 = (v % OUT_W_VECS) * 4u;
42
  let t = v / OUT_W_VECS;
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Upsample",
3
  "cases": [
4
  {
5
  "name": "nearest_2x_f32",
@@ -472,6 +471,38 @@
472
  }
473
  },
474
  "outputs": { "y": { "dtype": "float32", "shape": [1, 1, 2, 1, 2, 1, 2, 6], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  }
476
  ]
477
  }
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "nearest_2x_f32",
 
471
  }
472
  },
473
  "outputs": { "y": { "dtype": "float32", "shape": [1, 1, 2, 1, 2, 1, 2, 6], "tolerance": 0 } }
474
+ },
475
+ {
476
+ "name": "linear_scale_2p1_not_exact_2x_f32",
477
+ "provenance": {
478
+ "notes": "scales 2.1 on a 3x3 input gives floor(3 * 2.1) = 6 outputs per axis, the same shape as an exact 2x upsample, but the asymmetric source coordinate is o / 2.1, not o / 2. A 2x stencil chosen from the shapes alone would compute the wrong interpolation weights."
479
+ },
480
+ "attrs": { "mode": "linear" },
481
+ "inputs": {
482
+ "x": {
483
+ "dtype": "float32",
484
+ "shape": [1, 1, 3, 3],
485
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] }
486
+ },
487
+ "scales": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 1.0, 2.1, 2.1] } }
488
+ },
489
+ "outputs": { "y": { "dtype": "float32", "shape": [1, 1, 6, 6], "tolerance": 0.000001 } }
490
+ },
491
+ {
492
+ "name": "linear_scale_2p1_not_exact_2x_vec4_f32",
493
+ "provenance": {
494
+ "notes": "A 3-by-2 input with scales 2.1 produces a 6-by-4 vector-aligned output while retaining source coordinates based on 2.1 rather than 2."
495
+ },
496
+ "attrs": { "mode": "linear" },
497
+ "inputs": {
498
+ "x": {
499
+ "dtype": "float32",
500
+ "shape": [1, 1, 3, 2],
501
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] }
502
+ },
503
+ "scales": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 1.0, 2.1, 2.1] } }
504
+ },
505
+ "outputs": { "y": { "dtype": "float32", "shape": [1, 1, 6, 4], "tolerance": 0.000001 } }
506
  }
507
  ]
508
  }