Xenova HF Staff commited on
Commit
f430939
·
verified ·
1 Parent(s): bbc3807

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,17 +18,17 @@ See the [ONNX `DynamicQuantizeLinear` spec](https://onnx.ai/onnx/operators/onnx_
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
- | --- | --- | --- | --- | --- | --- | --- |
23
- | `x` | `x` | `T` | — | — | Float32 input tensor to quantize. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
28
- | --- | --- | --- | --- | --- | --- | --- |
29
- | `y` | `y` | `TQ` | same as `x` | same as `x` | Quantized output tensor; same shape as the input. | required |
30
- | `y_scale` | `y_scale` | `T` | `0` | `[]` | Per-tensor scale factor derived from the input min/max range; scalar. | required |
31
- | `y_zero_point` | `y_zero_point` | `TQ` | `0` | `[]` | Per-tensor zero point for the quantization; scalar. | required |
32
 
33
  ## Type constraints
34
 
@@ -37,9 +37,19 @@ See the [ONNX `DynamicQuantizeLinear` spec](https://onnx.ai/onnx/operators/onnx_
37
  | `T` | `float32` |
38
  | `TQ` | `uint8` |
39
 
 
 
 
 
 
 
 
 
 
 
40
  ## Files
41
 
42
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
43
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
44
  - [`test.json`](build/webgpu/test.json) — correctness cases
45
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -49,10 +59,14 @@ See the [ONNX `DynamicQuantizeLinear` spec](https://onnx.ai/onnx/operators/onnx_
49
 
50
  ## Use with `@huggingface/kernels`
51
 
52
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
53
- It then allocates the result tensors automatically.
 
 
 
54
 
55
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
56
 
57
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
58
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `T` | — | — | Float32 input tensor to quantize. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
28
+ | --- | --- | --- | --- | --- | --- |
29
+ | `y` | `TQ` | same as `x` | same as `x` | Quantized output tensor; same shape as the input. | required |
30
+ | `y_scale` | `T` | `0` | `[]` | Per-tensor scale factor derived from the input min/max range; scalar. | required |
31
+ | `y_zero_point` | `TQ` | `0` | `[]` | Per-tensor zero point for the quantization; scalar. | required |
32
 
33
  ## Type constraints
34
 
 
37
  | `T` | `float32` |
38
  | `TQ` | `uint8` |
39
 
40
+ ## Implementation variants
41
+
42
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
43
+
44
+ - `single_invocation` — Uses one invocation to find the range and quantize the tensor, avoiding partial buffers for small inputs. It also provides the fallback when the parallel reduction cannot satisfy device limits.
45
+ - `parallel_subgroup_reduce_vec4` — Reduces independent input blocks to min/max partials, combines them, and quantizes in a separate pass. The family uses packed reads when the input length is vec4-aligned.
46
+ - `parallel_subgroup_reduce` — Reduces independent input blocks to min/max partials, combines them, and quantizes in a separate pass. The family uses packed reads when the input length is vec4-aligned.
47
+ - `grid_stride_reduce_vec4` — Caps the number of min/max partials and grid-strides each workgroup across the input. This bounds scratch size and finalization work for large tensors.
48
+ - `grid_stride_reduce` — Caps the number of min/max partials and grid-strides each workgroup across the input. This bounds scratch size and finalization work for large tensors.
49
+
50
  ## Files
51
 
52
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
53
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
54
  - [`test.json`](build/webgpu/test.json) — correctness cases
55
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
59
 
60
  ## Use with `@huggingface/kernels`
61
 
62
+ ```sh
63
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
64
+ ```
65
+
66
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
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
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
70
 
71
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
72
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.DynamicQuantizeLinear",
3
  "cases": [
4
  {
5
  "name": "f32_64k",
@@ -12,8 +11,8 @@
12
  },
13
  "outputs": {
14
  "y": { "dtype": "uint8", "shape": [65536] },
15
- "y_scale": { "dtype": "float32", "shape": [1] },
16
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
17
  }
18
  },
19
  {
@@ -28,8 +27,8 @@
28
  },
29
  "outputs": {
30
  "y": { "dtype": "uint8", "shape": [2048, 2048] },
31
- "y_scale": { "dtype": "float32", "shape": [1] },
32
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
33
  },
34
  "bench": {
35
  "metrics": [
@@ -54,8 +53,8 @@
54
  },
55
  "outputs": {
56
  "y": { "dtype": "uint8", "shape": [4194303] },
57
- "y_scale": { "dtype": "float32", "shape": [1] },
58
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
59
  },
60
  "bench": {
61
  "primary": true,
@@ -81,8 +80,8 @@
81
  },
82
  "outputs": {
83
  "y": { "dtype": "uint8", "shape": [4194304] },
84
- "y_scale": { "dtype": "float32", "shape": [1] },
85
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
86
  },
87
  "bench": {
88
  "metrics": [
@@ -107,8 +106,8 @@
107
  },
108
  "outputs": {
109
  "y": { "dtype": "uint8", "shape": [2097153] },
110
- "y_scale": { "dtype": "float32", "shape": [1] },
111
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
112
  },
113
  "bench": {
114
  "metrics": [
@@ -133,8 +132,8 @@
133
  },
134
  "outputs": {
135
  "y": { "dtype": "uint8", "shape": [2097152] },
136
- "y_scale": { "dtype": "float32", "shape": [1] },
137
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
138
  },
139
  "bench": {
140
  "metrics": [
@@ -159,8 +158,8 @@
159
  },
160
  "outputs": {
161
  "y": { "dtype": "uint8", "shape": [262143] },
162
- "y_scale": { "dtype": "float32", "shape": [1] },
163
- "y_zero_point": { "dtype": "uint8", "shape": [1] }
164
  },
165
  "bench": {
166
  "metrics": [
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "f32_64k",
 
11
  },
12
  "outputs": {
13
  "y": { "dtype": "uint8", "shape": [65536] },
14
+ "y_scale": { "dtype": "float32", "shape": [] },
15
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
16
  }
17
  },
18
  {
 
27
  },
28
  "outputs": {
29
  "y": { "dtype": "uint8", "shape": [2048, 2048] },
30
+ "y_scale": { "dtype": "float32", "shape": [] },
31
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
32
  },
33
  "bench": {
34
  "metrics": [
 
53
  },
54
  "outputs": {
55
  "y": { "dtype": "uint8", "shape": [4194303] },
56
+ "y_scale": { "dtype": "float32", "shape": [] },
57
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
58
  },
59
  "bench": {
60
  "primary": true,
 
80
  },
81
  "outputs": {
82
  "y": { "dtype": "uint8", "shape": [4194304] },
83
+ "y_scale": { "dtype": "float32", "shape": [] },
84
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
85
  },
86
  "bench": {
87
  "metrics": [
 
106
  },
107
  "outputs": {
108
  "y": { "dtype": "uint8", "shape": [2097153] },
109
+ "y_scale": { "dtype": "float32", "shape": [] },
110
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
111
  },
112
  "bench": {
113
  "metrics": [
 
132
  },
133
  "outputs": {
134
  "y": { "dtype": "uint8", "shape": [2097152] },
135
+ "y_scale": { "dtype": "float32", "shape": [] },
136
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
137
  },
138
  "bench": {
139
  "metrics": [
 
158
  },
159
  "outputs": {
160
  "y": { "dtype": "uint8", "shape": [262143] },
161
+ "y_scale": { "dtype": "float32", "shape": [] },
162
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
163
  },
164
  "bench": {
165
  "metrics": [
build/webgpu/dynamic-quantize-linear-quantize.wgsl.jinja CHANGED
@@ -93,10 +93,9 @@ fn dynamic_quantize_exact_div_normal(numerator: f32, denominator: f32) -> f32 {
93
 
94
  let biased_exponent = quotient_exponent + 127;
95
  if (biased_exponent <= 0 || biased_exponent >= 255) {
96
- // DynamicQuantizeLinear's GPU-supported ranges are normal and finite.
97
- // Preserve the implementation-defined subnormal/overflow behavior outside
98
- // that contract; the integer quantizer never takes this branch near a
99
- // half-integer boundary.
100
  return numerator / denominator;
101
  }
102
  let result_bits = sign_bits
@@ -149,14 +148,13 @@ const EPT: u32 = {{ elemsPerThread }}u;
149
  {% endif %}
150
  @compute @workgroup_size(WG, 1, 1)
151
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
152
- @builtin(local_invocation_id) lid: vec3<u32>,
153
- @builtin(num_workgroups) nwg: vec3<u32>) {
154
  let tid = lid.x;
155
  let scale = y_scale[0];
156
  let zp_i32 = i32(y_zero_point[0]);
157
- // Fold the block grid across x/y at the device's per-axis workgroup limit.
158
  // Per-element guards discard the over-dispatched tail.
159
- let blk = wg.x + wg.y * nwg.x;
160
  {% if vec4 %}
161
  // The vec4 input load reads four scalars at once. Output storage still uses
162
  // one u32 element for each quantized value.
 
93
 
94
  let biased_exponent = quotient_exponent + 127;
95
  if (biased_exponent <= 0 || biased_exponent >= 255) {
96
+ // The exact normal-range reconstruction below cannot encode a subnormal or
97
+ // overflowed quotient. Use WGSL division for those exponent ranges; this
98
+ // branch is disjoint from the finite halfway cases handled below.
 
99
  return numerator / denominator;
100
  }
101
  let result_bits = sign_bits
 
148
  {% endif %}
149
  @compute @workgroup_size(WG, 1, 1)
150
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
151
+ @builtin(local_invocation_id) lid: vec3<u32>) {
 
152
  let tid = lid.x;
153
  let scale = y_scale[0];
154
  let zp_i32 = i32(y_zero_point[0]);
155
+ // Fold the block grid across x/y at a fixed per-axis workgroup width.
156
  // Per-element guards discard the over-dispatched tail.
157
+ let blk = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
158
  {% if vec4 %}
159
  // The vec4 input load reads four scalars at once. Output storage still uses
160
  // one u32 element for each quantized value.
build/webgpu/dynamic-quantize-linear-reduce.wgsl.jinja CHANGED
@@ -1,14 +1,17 @@
1
  // Pass 1 of parallel DynamicQuantizeLinear. Regular mode reduces one contiguous
2
  // WG * EPT block per workgroup. Grid-stride mode caps the partial count and has
3
  // every lane revisit the tensor at grid-sized strides. Both modes use the same
4
- // subgroup/tree combine and write one min/max pair per workgroup.
 
 
 
5
  //
6
  // The local min/max start at 0.0 because the ONNX DynamicQuantizeLinear range
7
  // always includes zero; out-of-range lanes contribute the same neutral value.
8
- // f32 min/max is order-independent, so the result is bit-identical to the serial
9
- // loop and between subgroup and workgroup-tree combines. Without subgroups, a
10
- // full workgroup tree combines the per-thread partials.
11
- {% set gridStride = source.gridStride if source.gridStride is defined else false %}
12
  {% if useSubgroups %}
13
  enable subgroups;
14
  {% endif %}
@@ -24,10 +27,11 @@ var<workgroup> wgMax: array<f32, WG>;
24
 
25
  @compute @workgroup_size(WG, 1, 1)
26
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
27
- @builtin(local_invocation_id) lid: vec3<u32>,
 
28
  @builtin(num_workgroups) nwg: vec3<u32>
 
29
  {%- if useSubgroups %},
30
- @builtin(subgroup_invocation_id) sgLid: u32,
31
  @builtin(subgroup_size) sgSize: u32
32
  {%- endif %}) {
33
  let tid = lid.x;
@@ -41,7 +45,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
41
  // per-dimension dispatch limit. The whole over-dispatched workgroup returns
42
  // before barriers or scratch stores.
43
  // blk/numBlocks are workgroup-uniform, so the return can't split the barrier.
44
- let blk = wg.x + wg.y * nwg.x;
45
  {% if vec4 %}
46
  let numBlocks = (params.count / 4u + WG - 1u) / WG;
47
  {% else %}
@@ -93,22 +97,39 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
93
  {% if useSubgroups %}
94
  let sgMin = subgroupMin(localMin);
95
  let sgMax = subgroupMax(localMax);
96
- let safeSg = max(sgSize, 1u);
97
- // Do not assume that the device subgroup size divides the workgroup size.
98
- let slotCount = max(1u, (WG + safeSg - 1u) / safeSg);
99
- if (sgLid == 0u) {
100
- let slot = min(tid / safeSg, WG - 1u);
101
- wgMin[slot] = sgMin;
102
- wgMax[slot] = sgMax;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
- workgroupBarrier();
105
  if (tid == 0u) {
106
- var totalMin = wgMin[0];
107
- var totalMax = wgMax[0];
108
- for (var i = 1u; i < slotCount; i = i + 1u) {
109
- totalMin = min(totalMin, wgMin[i]);
110
- totalMax = max(totalMax, wgMax[i]);
111
- }
112
  partial_min[blk] = totalMin;
113
  partial_max[blk] = totalMax;
114
  }
 
1
  // Pass 1 of parallel DynamicQuantizeLinear. Regular mode reduces one contiguous
2
  // WG * EPT block per workgroup. Grid-stride mode caps the partial count and has
3
  // every lane revisit the tensor at grid-sized strides. Both modes use the same
4
+ // subgroup/tree combine and write one min/max pair per workgroup. The subgroup
5
+ // combine publishes one shared-memory slot per invocation (its subgroup pair
6
+ // from the elected lane, the neutral 0.0 from every other lane) and every
7
+ // subgroup folds all of them.
8
  //
9
  // The local min/max start at 0.0 because the ONNX DynamicQuantizeLinear range
10
  // always includes zero; out-of-range lanes contribute the same neutral value.
11
+ // f32 min/max is order-independent, so subgroup and workgroup-tree combinations
12
+ // produce the same extrema. Without subgroups, a full workgroup tree combines
13
+ // the per-invocation partials.
14
+ {% set gridStride = gridStride if gridStride is defined else false %}
15
  {% if useSubgroups %}
16
  enable subgroups;
17
  {% endif %}
 
27
 
28
  @compute @workgroup_size(WG, 1, 1)
29
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
30
+ @builtin(local_invocation_id) lid: vec3<u32>
31
+ {%- if gridStride %},
32
  @builtin(num_workgroups) nwg: vec3<u32>
33
+ {%- endif %}
34
  {%- if useSubgroups %},
 
35
  @builtin(subgroup_size) sgSize: u32
36
  {%- endif %}) {
37
  let tid = lid.x;
 
45
  // per-dimension dispatch limit. The whole over-dispatched workgroup returns
46
  // before barriers or scratch stores.
47
  // blk/numBlocks are workgroup-uniform, so the return can't split the barrier.
48
+ let blk = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
49
  {% if vec4 %}
50
  let numBlocks = (params.count / 4u + WG - 1u) / WG;
51
  {% else %}
 
97
  {% if useSubgroups %}
98
  let sgMin = subgroupMin(localMin);
99
  let sgMax = subgroupMax(localMax);
100
+ // Cross-subgroup fold that assumes nothing about which invocations share a
101
+ // subgroup, how many subgroups there are, or which of a subgroup's lanes are
102
+ // active: every invocation owns the slot at its own index, the elected lane
103
+ // publishes its subgroup pair there and every other lane publishes 0.0, which
104
+ // is an exact identity here because every lane's local range already includes
105
+ // zero (so every published minimum is <= 0 and every maximum >= 0). Each
106
+ // subgroup then folds all WG slots — lane `rank`, its dense position among
107
+ // the active lanes, walks slots rank, rank + count, ... — and one more
108
+ // collective merges the lane partials, so every slot is merged exactly once
109
+ // at any legal width and partition. min/max is commutative and associative,
110
+ // so the fold order does not change the result.
111
+ var totalMin = sgMin;
112
+ var totalMax = sgMax;
113
+ // A one-subgroup workgroup is already fully reduced by the collectives above.
114
+ // The test reads the `subgroup_size` builtin, which is uniform; a collective's
115
+ // result is not uniform to WGSL's analysis and may not guard a barrier.
116
+ if (sgSize != WG) {
117
+ let rank = subgroupExclusiveAdd(1u);
118
+ let count = subgroupAdd(1u);
119
+ let leader = rank == 0u;
120
+ wgMin[tid] = select(0.0, sgMin, leader);
121
+ wgMax[tid] = select(0.0, sgMax, leader);
122
+ workgroupBarrier();
123
+ var foldMin = 0.0;
124
+ var foldMax = 0.0;
125
+ for (var i = rank; i < WG; i = i + count) {
126
+ foldMin = min(foldMin, wgMin[i]);
127
+ foldMax = max(foldMax, wgMax[i]);
128
+ }
129
+ totalMin = subgroupMin(foldMin);
130
+ totalMax = subgroupMax(foldMax);
131
  }
 
132
  if (tid == 0u) {
 
 
 
 
 
 
133
  partial_min[blk] = totalMin;
134
  partial_max[blk] = totalMax;
135
  }
build/webgpu/dynamic-quantize-linear.wgsl.jinja CHANGED
@@ -1,4 +1,4 @@
1
- {% if source.fromPartials %}
2
  // Parallel range finalization folds the guarded min/max partials and computes
3
  // the output scale and zero point. Each partial already includes zero in its range.
4
  {% else %}
@@ -95,10 +95,9 @@ fn dynamic_quantize_exact_div_normal(numerator: f32, denominator: f32) -> f32 {
95
 
96
  let biased_exponent = quotient_exponent + 127;
97
  if (biased_exponent <= 0 || biased_exponent >= 255) {
98
- // DynamicQuantizeLinear's GPU-supported ranges are normal and finite.
99
- // Preserve the implementation-defined subnormal/overflow behavior outside
100
- // that contract; the integer quantizer never takes this branch near a
101
- // half-integer boundary.
102
  return numerator / denominator;
103
  }
104
  let result_bits = sign_bits
@@ -147,7 +146,7 @@ fn round_dynamic_half_to_even(value: f32, scale: f32) -> i32 {
147
  @compute @workgroup_size(1)
148
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
149
  if (gid.x != 0u) { return; }
150
- {% if source.fromPartials %}
151
  var min_value = partial_min[0];
152
  var max_value = partial_max[0];
153
  for (var i = 1u; i < params.numPartials; i = i + 1u) {
@@ -169,7 +168,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
169
  let zp_i32 = clamp(round_dynamic_half_to_even(-min_value, scale), 0, 255);
170
  y_scale[0] = scale;
171
  y_zero_point[0] = u32(zp_i32);
172
- {% if not source.fromPartials %}
173
  for (var i = 0u; i < params.count; i = i + 1u) {
174
  let q = clamp(round_dynamic_half_to_even(x[i], scale) + zp_i32, 0, 255);
175
  y[i] = u32(q);
 
1
+ {% if fromPartials %}
2
  // Parallel range finalization folds the guarded min/max partials and computes
3
  // the output scale and zero point. Each partial already includes zero in its range.
4
  {% else %}
 
95
 
96
  let biased_exponent = quotient_exponent + 127;
97
  if (biased_exponent <= 0 || biased_exponent >= 255) {
98
+ // The exact normal-range reconstruction below cannot encode a subnormal or
99
+ // overflowed quotient. Use WGSL division for those exponent ranges; this
100
+ // branch is disjoint from the finite halfway cases handled below.
 
101
  return numerator / denominator;
102
  }
103
  let result_bits = sign_bits
 
146
  @compute @workgroup_size(1)
147
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
148
  if (gid.x != 0u) { return; }
149
+ {% if fromPartials %}
150
  var min_value = partial_min[0];
151
  var max_value = partial_max[0];
152
  for (var i = 1u; i < params.numPartials; i = i + 1u) {
 
168
  let zp_i32 = clamp(round_dynamic_half_to_even(-min_value, scale), 0, 255);
169
  y_scale[0] = scale;
170
  y_zero_point[0] = u32(zp_i32);
171
+ {% if not fromPartials %}
172
  for (var i = 0u; i < params.count; i = i + 1u) {
173
  let q = clamp(round_dynamic_half_to_even(x[i], scale) + zp_i32, 0, 255);
174
  y[i] = u32(q);
build/webgpu/manifest.json CHANGED
@@ -2,49 +2,19 @@
2
  "domain": "ai.onnx",
3
  "name": "DynamicQuantizeLinear",
4
  "sinceVersion": 11,
5
- "description": "Computes a per-tensor scale and zero point from the range of floating-point input `x`, extending the range to include zero, then quantizes each value to `uint8` as `saturate(round(x / y_scale) + y_zero_point)`. Uses round-to-nearest-even and clamps results to `[0, 255]`.",
6
- "inputs": [{ "role": "x", "dtype": "T", "description": "Float32 input tensor to quantize." }],
7
- "outputs": [
8
- {
9
- "role": "y",
10
- "dtype": "TQ",
11
- "rank": "ranks.x",
12
- "description": "Quantized output tensor; same shape as the input.",
13
- "shape": "shapes.x"
14
- },
15
- {
16
- "role": "y_scale",
17
- "dtype": "T",
18
- "rank": 0,
19
- "description": "Per-tensor scale factor derived from the input min/max range; scalar.",
20
- "shape": []
21
- },
22
- {
23
- "role": "y_zero_point",
24
- "dtype": "TQ",
25
- "rank": 0,
26
- "description": "Per-tensor zero point for the quantization; scalar.",
27
- "shape": []
28
- }
29
- ],
30
- "typeConstraints": { "T": ["float32"], "TQ": ["uint8"] },
31
- "args": {
32
- "x": { "kind": "tensor", "semantic": "x", "role": "input" },
33
- "y": { "kind": "tensor", "semantic": "y", "role": "output" },
34
- "y_scale": { "kind": "tensor", "semantic": "y_scale", "role": "output" },
35
- "y_zero_point": { "kind": "tensor", "semantic": "y_zero_point", "role": "output" }
36
  },
 
37
  "tunables": {
38
- "WORKGROUP_SIZE": 256,
39
- "ELEMENTS_PER_THREAD": 4,
40
- "GRID_STRIDE_MIN_ELEMENTS": 262144,
41
- "MAX_GRID_PARTIALS": 256,
42
- "SERIAL_MAX_ELEMENTS": 8192
43
- },
44
- "tunableDescriptions": {
45
- "GRID_STRIDE_MIN_ELEMENTS": "Sets the minimum input element count admitted to the bounded grid-stride reduction.",
46
- "MAX_GRID_PARTIALS": "Caps the number of min/max partials emitted by the grid-stride reduction, bounding scratch and finalization work.",
47
- "SERIAL_MAX_ELEMENTS": "Sets the input-size ceiling that prefers the one-invocation route; larger inputs use it only when the parallel reduction cannot fit."
48
  },
49
  "derive": {
50
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
@@ -58,182 +28,45 @@
58
  "parallelDeviceOk": "tunables.WORKGROUP_SIZE <= deviceWorkgroupCap and 8 * tunables.WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupStorageSize",
59
  "fullScratchFits": "4 * fullPartials <= storageBufferLimit",
60
  "gridScratchFits": "4 * gridPartials <= storageBufferLimit",
61
- "fullDispatchFits": "ceilDiv(fullPartials, device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension",
62
- "gridDispatchFits": "gridPartials <= device.limits.maxComputeWorkgroupsPerDimension",
63
  "parallelFullFits": "parallelDeviceOk and fullScratchFits and fullDispatchFits",
64
  "parallelGridFits": "parallelDeviceOk and gridScratchFits and gridDispatchFits and fullDispatchFits",
65
  "serialFallbackNeeded": "inputCount <= tunables.SERIAL_MAX_ELEMENTS or not parallelFullFits"
66
  },
67
- "bindingSets": {
68
- "reduce": [
69
- {
70
- "name": "x",
71
- "arg": "x",
72
- "semantic": "x",
73
- "buffer": { "type": "read-only-storage" },
74
- "elementType": "$inputElement"
75
- },
76
- { "name": "partial_min", "semantic": "partial_min", "buffer": { "type": "storage" }, "elementType": "f32" },
77
- { "name": "partial_max", "semantic": "partial_max", "buffer": { "type": "storage" }, "elementType": "f32" },
78
- {
79
- "name": "params",
80
- "semantic": "kernel.params",
81
- "buffer": { "type": "uniform" },
82
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] }
83
- }
84
- ],
85
- "finalizeFull": [
86
- {
87
- "name": "partial_min",
88
- "semantic": "partial_min",
89
- "buffer": { "type": "read-only-storage" },
90
- "elementType": "f32"
91
- },
92
- {
93
- "name": "partial_max",
94
- "semantic": "partial_max",
95
- "buffer": { "type": "read-only-storage" },
96
- "elementType": "f32"
97
- },
98
- {
99
- "name": "y_scale",
100
- "arg": "y_scale",
101
- "semantic": "y_scale",
102
- "buffer": { "type": "storage" },
103
- "elementType": "f32",
104
- "length": 1
105
- },
106
- {
107
- "name": "y_zero_point",
108
- "arg": "y_zero_point",
109
- "semantic": "y_zero_point",
110
- "buffer": { "type": "storage" },
111
- "elementType": "u32",
112
- "length": 1
113
- },
114
- {
115
- "name": "params",
116
- "semantic": "kernel.params",
117
- "buffer": { "type": "uniform" },
118
- "struct": { "name": "Params", "fields": [{ "name": "numPartials", "type": "u32", "value": "fullPartials" }] }
119
- }
120
- ],
121
- "finalizeGrid": [
122
- {
123
- "name": "partial_min",
124
- "semantic": "partial_min",
125
- "buffer": { "type": "read-only-storage" },
126
- "elementType": "f32"
127
- },
128
- {
129
- "name": "partial_max",
130
- "semantic": "partial_max",
131
- "buffer": { "type": "read-only-storage" },
132
- "elementType": "f32"
133
- },
134
- {
135
- "name": "y_scale",
136
- "arg": "y_scale",
137
- "semantic": "y_scale",
138
- "buffer": { "type": "storage" },
139
- "elementType": "f32",
140
- "length": 1
141
- },
142
- {
143
- "name": "y_zero_point",
144
- "arg": "y_zero_point",
145
- "semantic": "y_zero_point",
146
- "buffer": { "type": "storage" },
147
- "elementType": "u32",
148
- "length": 1
149
- },
150
- {
151
- "name": "params",
152
- "semantic": "kernel.params",
153
- "buffer": { "type": "uniform" },
154
- "struct": { "name": "Params", "fields": [{ "name": "numPartials", "type": "u32", "value": "gridPartials" }] }
155
- }
156
- ],
157
- "quantize": [
158
- {
159
- "name": "x",
160
- "arg": "x",
161
- "semantic": "x",
162
- "buffer": { "type": "read-only-storage" },
163
- "elementType": "$inputElement"
164
- },
165
- {
166
- "name": "y_scale",
167
- "arg": "y_scale",
168
- "semantic": "y_scale",
169
- "buffer": { "type": "read-only-storage" },
170
- "elementType": "f32",
171
- "length": 1
172
- },
173
- {
174
- "name": "y_zero_point",
175
- "arg": "y_zero_point",
176
- "semantic": "y_zero_point",
177
- "buffer": { "type": "read-only-storage" },
178
- "elementType": "u32",
179
- "length": 1
180
- },
181
- { "name": "y", "arg": "y", "semantic": "y", "buffer": { "type": "storage" }, "elementType": "u32" },
182
- {
183
- "name": "params",
184
- "semantic": "kernel.params",
185
- "buffer": { "type": "uniform" },
186
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] }
187
- }
188
- ],
189
- "serial": [
190
- { "name": "x", "arg": "x", "semantic": "x", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
191
- { "name": "y", "arg": "y", "semantic": "y", "buffer": { "type": "storage" }, "elementType": "u32" },
192
- {
193
- "name": "y_scale",
194
- "arg": "y_scale",
195
- "semantic": "y_scale",
196
- "buffer": { "type": "storage" },
197
- "elementType": "f32",
198
- "length": 1
199
- },
200
- {
201
- "name": "y_zero_point",
202
- "arg": "y_zero_point",
203
- "semantic": "y_zero_point",
204
- "buffer": { "type": "storage" },
205
- "elementType": "u32",
206
- "length": 1
207
- },
208
- {
209
- "name": "params",
210
- "semantic": "kernel.params",
211
- "buffer": { "type": "uniform" },
212
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] }
213
- }
214
- ]
215
  },
216
  "variants": [
217
  {
218
  "id": "single_invocation",
219
- "description": "Uses one invocation to find the range and quantize the tensor, avoiding partial buffers for small inputs. It also provides the fallback when the parallel reduction cannot satisfy device limits.",
220
  "when": ["serialContract", "serialFallbackNeeded"],
221
  "passes": [
222
  {
223
  "id": "main",
224
  "name": "DynamicQuantizeLinear",
225
- "source": { "shader": "dynamic-quantize-linear.wgsl.jinja", "inputs": { "fromPartials": false } },
226
- "bindings": "serial",
 
227
  "dispatch": { "x": 1 }
228
  }
229
  ]
230
  },
231
  {
232
  "id": "parallel_subgroup_reduce_vec4",
233
- "description": "Reduces independent input blocks to min/max partials, combines them, and quantizes in a separate pass. The family uses packed reads when the input length is vec4-aligned.",
234
  "priority": 11,
235
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 == 0", "parallelFullFits"],
236
- "constants": {
237
  "workgroupSize": "tunables.WORKGROUP_SIZE",
238
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
239
  "vec4": true,
@@ -249,34 +82,42 @@
249
  "id": "reduce",
250
  "name": "DynamicQuantizeLinear.ReduceMinMax",
251
  "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
252
- "bindings": "reduce",
253
- "dispatch": { "workgroups": "fullPartials" }
 
254
  },
255
  {
256
  "id": "finalize",
257
  "name": "DynamicQuantizeLinear.Finalize",
258
- "source": { "shader": "dynamic-quantize-linear.wgsl.jinja", "inputs": { "fromPartials": true } },
259
- "bindings": "finalizeFull",
 
 
 
 
 
 
 
260
  "dispatch": { "x": 1 }
261
  },
262
  {
263
  "id": "quantize",
264
  "name": "DynamicQuantizeLinear.Quantize",
265
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
266
- "bindings": "quantize",
267
  "dispatch": {
268
- "threads": "ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)",
269
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
270
  }
271
  }
272
  ]
273
  },
274
  {
275
  "id": "parallel_subgroup_reduce",
276
- "description": "Reduces independent input blocks to min/max partials, combines them, and quantizes in a separate pass. The family uses packed reads when the input length is vec4-aligned.",
277
  "priority": 10,
278
  "when": ["baseContract", "inputCount > 0", "true", "parallelFullFits"],
279
- "constants": {
280
  "workgroupSize": "tunables.WORKGROUP_SIZE",
281
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
282
  "vec4": false,
@@ -292,34 +133,42 @@
292
  "id": "reduce",
293
  "name": "DynamicQuantizeLinear.ReduceMinMax",
294
  "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
295
- "bindings": "reduce",
296
- "dispatch": { "workgroups": "fullPartials" }
 
297
  },
298
  {
299
  "id": "finalize",
300
  "name": "DynamicQuantizeLinear.Finalize",
301
- "source": { "shader": "dynamic-quantize-linear.wgsl.jinja", "inputs": { "fromPartials": true } },
302
- "bindings": "finalizeFull",
 
 
 
 
 
 
 
303
  "dispatch": { "x": 1 }
304
  },
305
  {
306
  "id": "quantize",
307
  "name": "DynamicQuantizeLinear.Quantize",
308
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
309
- "bindings": "quantize",
310
  "dispatch": {
311
- "threads": "ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)",
312
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
313
  }
314
  }
315
  ]
316
  },
317
  {
318
  "id": "grid_stride_reduce_vec4",
319
- "description": "Caps the number of min/max partials and grid-strides each workgroup across the input. This bounds scratch size and finalization work for large tensors.",
320
  "priority": 12,
321
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 == 0", "inputCount >= tunables.GRID_STRIDE_MIN_ELEMENTS", "parallelGridFits"],
322
- "constants": {
323
  "workgroupSize": "tunables.WORKGROUP_SIZE",
324
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
325
  "vec4": true,
@@ -334,35 +183,44 @@
334
  {
335
  "id": "reduce",
336
  "name": "DynamicQuantizeLinear.ReduceMinMax",
337
- "source": { "shader": "dynamic-quantize-linear-reduce.wgsl.jinja", "inputs": { "gridStride": true } },
338
- "bindings": "reduce",
 
 
339
  "dispatch": { "x": "gridPartials" }
340
  },
341
  {
342
  "id": "finalize",
343
  "name": "DynamicQuantizeLinear.Finalize",
344
- "source": { "shader": "dynamic-quantize-linear.wgsl.jinja", "inputs": { "fromPartials": true } },
345
- "bindings": "finalizeGrid",
 
 
 
 
 
 
 
346
  "dispatch": { "x": 1 }
347
  },
348
  {
349
  "id": "quantize",
350
  "name": "DynamicQuantizeLinear.Quantize",
351
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
352
- "bindings": "quantize",
353
  "dispatch": {
354
- "threads": "ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)",
355
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
356
  }
357
  }
358
  ]
359
  },
360
  {
361
  "id": "grid_stride_reduce",
362
- "description": "Caps the number of min/max partials and grid-strides each workgroup across the input. This bounds scratch size and finalization work for large tensors.",
363
  "priority": 12,
364
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 != 0", "inputCount >= tunables.GRID_STRIDE_MIN_ELEMENTS", "parallelGridFits"],
365
- "constants": {
366
  "workgroupSize": "tunables.WORKGROUP_SIZE",
367
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
368
  "vec4": false,
@@ -377,25 +235,35 @@
377
  {
378
  "id": "reduce",
379
  "name": "DynamicQuantizeLinear.ReduceMinMax",
380
- "source": { "shader": "dynamic-quantize-linear-reduce.wgsl.jinja", "inputs": { "gridStride": true } },
381
- "bindings": "reduce",
 
 
382
  "dispatch": { "x": "gridPartials" }
383
  },
384
  {
385
  "id": "finalize",
386
  "name": "DynamicQuantizeLinear.Finalize",
387
- "source": { "shader": "dynamic-quantize-linear.wgsl.jinja", "inputs": { "fromPartials": true } },
388
- "bindings": "finalizeGrid",
 
 
 
 
 
 
 
389
  "dispatch": { "x": 1 }
390
  },
391
  {
392
  "id": "quantize",
393
  "name": "DynamicQuantizeLinear.Quantize",
394
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
395
- "bindings": "quantize",
396
  "dispatch": {
397
- "threads": "ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)",
398
- "workgroupSize": "tunables.WORKGROUP_SIZE"
 
399
  }
400
  }
401
  ]
 
2
  "domain": "ai.onnx",
3
  "name": "DynamicQuantizeLinear",
4
  "sinceVersion": 11,
5
+ "inputs": { "x": { "dtype": "T" } },
6
+ "outputs": {
7
+ "y": { "dtype": "TQ", "rank": "ranks.x", "shape": "shapes.x" },
8
+ "y_scale": { "dtype": "T", "rank": 0, "shape": [] },
9
+ "y_zero_point": { "dtype": "TQ", "rank": 0, "shape": [] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  },
11
+ "typeConstraints": { "T": ["float32"], "TQ": ["uint8"] },
12
  "tunables": {
13
+ "WORKGROUP_SIZE": { "default": 256 },
14
+ "ELEMENTS_PER_THREAD": { "default": 4 },
15
+ "GRID_STRIDE_MIN_ELEMENTS": { "default": 262144 },
16
+ "MAX_GRID_PARTIALS": { "default": 256 },
17
+ "SERIAL_MAX_ELEMENTS": { "default": 8192 }
 
 
 
 
 
18
  },
19
  "derive": {
20
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
 
28
  "parallelDeviceOk": "tunables.WORKGROUP_SIZE <= deviceWorkgroupCap and 8 * tunables.WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupStorageSize",
29
  "fullScratchFits": "4 * fullPartials <= storageBufferLimit",
30
  "gridScratchFits": "4 * gridPartials <= storageBufferLimit",
31
+ "fullDispatchFits": "ceilDiv(fullPartials, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
32
+ "gridDispatchFits": "gridPartials <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
33
  "parallelFullFits": "parallelDeviceOk and fullScratchFits and fullDispatchFits",
34
  "parallelGridFits": "parallelDeviceOk and gridScratchFits and gridDispatchFits and fullDispatchFits",
35
  "serialFallbackNeeded": "inputCount <= tunables.SERIAL_MAX_ELEMENTS or not parallelFullFits"
36
  },
37
+ "bindings": {
38
+ "y": { "buffer": "storage", "elementType": "u32" },
39
+ "y_scale": { "buffer": "storage", "elementType": "f32", "length": 1 },
40
+ "y_zero_point": { "buffer": "storage", "elementType": "u32", "length": 1 },
41
+ "params": { "buffer": "uniform", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] },
42
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$inputElement" },
43
+ "partial_min": { "buffer": "storage", "elementType": "f32" },
44
+ "partial_max": { "buffer": "storage", "elementType": "f32" },
45
+ "partial_min_2": { "name": "partial_min", "buffer": "read-only-storage", "elementType": "f32" },
46
+ "partial_max_2": { "name": "partial_max", "buffer": "read-only-storage", "elementType": "f32" },
47
+ "y_scale_2": { "name": "y_scale", "buffer": "read-only-storage", "elementType": "f32", "length": 1 },
48
+ "y_zero_point_2": { "name": "y_zero_point", "buffer": "read-only-storage", "elementType": "u32", "length": 1 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  },
50
  "variants": [
51
  {
52
  "id": "single_invocation",
 
53
  "when": ["serialContract", "serialFallbackNeeded"],
54
  "passes": [
55
  {
56
  "id": "main",
57
  "name": "DynamicQuantizeLinear",
58
+ "shader": "dynamic-quantize-linear.wgsl.jinja",
59
+ "derive": { "fromPartials": false },
60
+ "bindings": [{ "arg": "x", "elementType": "f32" }, "y", "y_scale", "y_zero_point", "params"],
61
  "dispatch": { "x": 1 }
62
  }
63
  ]
64
  },
65
  {
66
  "id": "parallel_subgroup_reduce_vec4",
 
67
  "priority": 11,
68
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 == 0", "parallelFullFits"],
69
+ "derive": {
70
  "workgroupSize": "tunables.WORKGROUP_SIZE",
71
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
72
  "vec4": true,
 
82
  "id": "reduce",
83
  "name": "DynamicQuantizeLinear.ReduceMinMax",
84
  "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
85
+ "subgroupCollectivesWidth": "portable",
86
+ "bindings": ["x_2", "partial_min", "partial_max", "params"],
87
+ "dispatch": { "x": "min(fullPartials, 65535)", "y": "ceilDiv(fullPartials, 65535)", "z": 1 }
88
  },
89
  {
90
  "id": "finalize",
91
  "name": "DynamicQuantizeLinear.Finalize",
92
+ "shader": "dynamic-quantize-linear.wgsl.jinja",
93
+ "derive": { "fromPartials": true },
94
+ "bindings": [
95
+ "partial_min_2",
96
+ "partial_max_2",
97
+ "y_scale",
98
+ "y_zero_point",
99
+ { "name": "params", "struct": [{ "name": "numPartials", "type": "u32", "value": "fullPartials" }] }
100
+ ],
101
  "dispatch": { "x": 1 }
102
  },
103
  {
104
  "id": "quantize",
105
  "name": "DynamicQuantizeLinear.Quantize",
106
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
107
+ "bindings": ["x_2", "y_scale_2", "y_zero_point_2", "y", "params"],
108
  "dispatch": {
109
+ "x": "min(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
110
+ "y": "ceilDiv(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
111
+ "z": 1
112
  }
113
  }
114
  ]
115
  },
116
  {
117
  "id": "parallel_subgroup_reduce",
 
118
  "priority": 10,
119
  "when": ["baseContract", "inputCount > 0", "true", "parallelFullFits"],
120
+ "derive": {
121
  "workgroupSize": "tunables.WORKGROUP_SIZE",
122
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
123
  "vec4": false,
 
133
  "id": "reduce",
134
  "name": "DynamicQuantizeLinear.ReduceMinMax",
135
  "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
136
+ "subgroupCollectivesWidth": "portable",
137
+ "bindings": ["x_2", "partial_min", "partial_max", "params"],
138
+ "dispatch": { "x": "min(fullPartials, 65535)", "y": "ceilDiv(fullPartials, 65535)", "z": 1 }
139
  },
140
  {
141
  "id": "finalize",
142
  "name": "DynamicQuantizeLinear.Finalize",
143
+ "shader": "dynamic-quantize-linear.wgsl.jinja",
144
+ "derive": { "fromPartials": true },
145
+ "bindings": [
146
+ "partial_min_2",
147
+ "partial_max_2",
148
+ "y_scale",
149
+ "y_zero_point",
150
+ { "name": "params", "struct": [{ "name": "numPartials", "type": "u32", "value": "fullPartials" }] }
151
+ ],
152
  "dispatch": { "x": 1 }
153
  },
154
  {
155
  "id": "quantize",
156
  "name": "DynamicQuantizeLinear.Quantize",
157
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
158
+ "bindings": ["x_2", "y_scale_2", "y_zero_point_2", "y", "params"],
159
  "dispatch": {
160
+ "x": "min(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
161
+ "y": "ceilDiv(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
162
+ "z": 1
163
  }
164
  }
165
  ]
166
  },
167
  {
168
  "id": "grid_stride_reduce_vec4",
 
169
  "priority": 12,
170
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 == 0", "inputCount >= tunables.GRID_STRIDE_MIN_ELEMENTS", "parallelGridFits"],
171
+ "derive": {
172
  "workgroupSize": "tunables.WORKGROUP_SIZE",
173
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
174
  "vec4": true,
 
183
  {
184
  "id": "reduce",
185
  "name": "DynamicQuantizeLinear.ReduceMinMax",
186
+ "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
187
+ "subgroupCollectivesWidth": "portable",
188
+ "derive": { "gridStride": true },
189
+ "bindings": ["x_2", "partial_min", "partial_max", "params"],
190
  "dispatch": { "x": "gridPartials" }
191
  },
192
  {
193
  "id": "finalize",
194
  "name": "DynamicQuantizeLinear.Finalize",
195
+ "shader": "dynamic-quantize-linear.wgsl.jinja",
196
+ "derive": { "fromPartials": true },
197
+ "bindings": [
198
+ "partial_min_2",
199
+ "partial_max_2",
200
+ "y_scale",
201
+ "y_zero_point",
202
+ { "name": "params", "struct": [{ "name": "numPartials", "type": "u32", "value": "gridPartials" }] }
203
+ ],
204
  "dispatch": { "x": 1 }
205
  },
206
  {
207
  "id": "quantize",
208
  "name": "DynamicQuantizeLinear.Quantize",
209
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
210
+ "bindings": ["x_2", "y_scale_2", "y_zero_point_2", "y", "params"],
211
  "dispatch": {
212
+ "x": "min(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
213
+ "y": "ceilDiv(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
214
+ "z": 1
215
  }
216
  }
217
  ]
218
  },
219
  {
220
  "id": "grid_stride_reduce",
 
221
  "priority": 12,
222
  "when": ["baseContract", "inputCount > 0", "inputCount % 4 != 0", "inputCount >= tunables.GRID_STRIDE_MIN_ELEMENTS", "parallelGridFits"],
223
+ "derive": {
224
  "workgroupSize": "tunables.WORKGROUP_SIZE",
225
  "elemsPerThread": "tunables.ELEMENTS_PER_THREAD",
226
  "vec4": false,
 
235
  {
236
  "id": "reduce",
237
  "name": "DynamicQuantizeLinear.ReduceMinMax",
238
+ "shader": "dynamic-quantize-linear-reduce.wgsl.jinja",
239
+ "subgroupCollectivesWidth": "portable",
240
+ "derive": { "gridStride": true },
241
+ "bindings": ["x_2", "partial_min", "partial_max", "params"],
242
  "dispatch": { "x": "gridPartials" }
243
  },
244
  {
245
  "id": "finalize",
246
  "name": "DynamicQuantizeLinear.Finalize",
247
+ "shader": "dynamic-quantize-linear.wgsl.jinja",
248
+ "derive": { "fromPartials": true },
249
+ "bindings": [
250
+ "partial_min_2",
251
+ "partial_max_2",
252
+ "y_scale",
253
+ "y_zero_point",
254
+ { "name": "params", "struct": [{ "name": "numPartials", "type": "u32", "value": "gridPartials" }] }
255
+ ],
256
  "dispatch": { "x": 1 }
257
  },
258
  {
259
  "id": "quantize",
260
  "name": "DynamicQuantizeLinear.Quantize",
261
  "shader": "dynamic-quantize-linear-quantize.wgsl.jinja",
262
+ "bindings": ["x_2", "y_scale_2", "y_zero_point_2", "y", "params"],
263
  "dispatch": {
264
+ "x": "min(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
265
+ "y": "ceilDiv(ceilDiv((ceilDiv(inputCount, tunables.ELEMENTS_PER_THREAD)), (tunables.WORKGROUP_SIZE)), 65535)",
266
+ "z": 1
267
  }
268
  }
269
  ]
build/webgpu/metadata.json CHANGED
@@ -1,20 +1,29 @@
1
  {
2
  "name": "ai.onnx.DynamicQuantizeLinear",
3
- "id": "_ai_onnx_dynamicquantizelinear_webgpu_679d73b",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "ic1d0BkyDcNr1B1KJ9oJN/5mnB04zIaCIW6y43kaEgY=",
11
- "dynamic-quantize-linear-quantize.wgsl.jinja": "8J/MWQOE+FLfjP/5JCQvU7v0ncgxQcM79GMW6PtSPr8=",
12
- "dynamic-quantize-linear-reduce.wgsl.jinja": "hbPwzMCANALDD58/USzBkSqY18jQS952IABcmsOy69w=",
13
- "dynamic-quantize-linear.wgsl.jinja": "ccNGv53bI60K7evqWhmtmZi8ocQUp/zx9X9ICATi03M=",
14
- "manifest.json": "i7HEkBE8BwjpTprLea26Sqfyb5DM1CVD7V8uRXE0DLI=",
15
- "test.json": "I3ltf1DnGsVq5fWasOGlLYKfEDY9rtTLSV7xrLiU++M="
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.DynamicQuantizeLinear" }
 
 
 
 
 
 
 
 
 
20
  }
 
1
  {
2
  "name": "ai.onnx.DynamicQuantizeLinear",
3
+ "id": "_ai_onnx_dynamicquantizelinear_webgpu_c781458",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "XDg7UAukA6OblWGm+ypY2zEJvMbW7c2WSl5jcuO5zxU=",
11
+ "dynamic-quantize-linear-quantize.wgsl.jinja": "ISRZ+RP/JwuhPFFtM/mSTJ+qGU8ATQz9B8b1bnZuS18=",
12
+ "dynamic-quantize-linear-reduce.wgsl.jinja": "MbIpHB8DKZZo8wVxryLnQkF9UeI5+GDuPb67Wq5CwhA=",
13
+ "dynamic-quantize-linear.wgsl.jinja": "tGwmn/Rl6x4gkOe+YNfxsBhJ6TrZgeNFvOXCNfOFeq4=",
14
+ "manifest.json": "bndT+hJeO/n9kaxatO81kxGcbg9bdnNYv9uEEOY8SHw=",
15
+ "test.json": "moR9c5IlMwac/omRuy88wGQgWfM/KKQRuM9+oU68p1M="
16
  }
17
  },
18
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
19
+ "webgpu": {
20
+ "manifestSpec": "2.0",
21
+ "variants": {
22
+ "single_invocation": ["dynamic-quantize-linear.wgsl.jinja"],
23
+ "parallel_subgroup_reduce_vec4": ["dynamic-quantize-linear-quantize.wgsl.jinja", "dynamic-quantize-linear-reduce.wgsl.jinja", "dynamic-quantize-linear.wgsl.jinja"],
24
+ "parallel_subgroup_reduce": ["dynamic-quantize-linear-quantize.wgsl.jinja", "dynamic-quantize-linear-reduce.wgsl.jinja", "dynamic-quantize-linear.wgsl.jinja"],
25
+ "grid_stride_reduce_vec4": ["dynamic-quantize-linear-quantize.wgsl.jinja", "dynamic-quantize-linear-reduce.wgsl.jinja", "dynamic-quantize-linear.wgsl.jinja"],
26
+ "grid_stride_reduce": ["dynamic-quantize-linear-quantize.wgsl.jinja", "dynamic-quantize-linear-reduce.wgsl.jinja", "dynamic-quantize-linear.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.DynamicQuantizeLinear",
3
  "cases": [
4
  {
5
  "name": "mixed_sign_values",
@@ -278,7 +277,7 @@
278
  "provenance": {
279
  "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
280
  "test": "QuantizeLinearOpTest.DynamicQuantizeLinear_Max_Adjusted",
281
- "notes": "Rank-0 scalar companion for the all-negative max-adjusted path: max is adjusted to include zero, producing zero_point=255."
282
  },
283
  "inputs": { "x": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [-5.0] } } },
284
  "outputs": {
@@ -318,7 +317,7 @@
318
  {
319
  "name": "grid_stride_reduce_1m_mixed_sign",
320
  "provenance": {
321
- "notes": "numel = 1048576 >= 262144 selects grid_stride_reduce_vec4: the reduce dispatches a capped min(256, ceil(numel/1024))=256 workgroups, each grid-striding over the whole tensor and writing ONE partial, so the @workgroup_size(1) finalize folds only 256 partials (vs 4096 for the per-block reduce). Validates the grid-stride min/max is bit-identical to the serial reference (y_scale/y_zero_point exact) and that the capped 1D dispatch covers all elements."
322
  },
323
  "inputs": {
324
  "x": {
@@ -336,7 +335,7 @@
336
  {
337
  "name": "dispatch_cliff_blocks_65536",
338
  "provenance": {
339
- "notes": "numel = 67108864 = 65536*1024 > 65535*1024 forces the folded block dispatch (x=65535, y=2) in the reduce + quantize passes. Validates blk = wg.x + wg.y*nwg.x AND the new `blk >= numBlocks` reduce-pass early-return that protects the partial_min/max[blk] scratch store from an over-dispatched OOB write. linspace puts the global min/max at the first/last block (the last block lives in the wg.y=1 folded region), so a wrong fold corrupts the scale. Before the 2D fold the plan-time dispatch-limit guard throws."
340
  },
341
  "inputs": {
342
  "x": { "dtype": "float32", "shape": [67108864], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
@@ -365,7 +364,7 @@
365
  {
366
  "name": "non_vec4_large_finalize_fold_mixed_sign_corrected_division",
367
  "provenance": {
368
- "notes": "Regression for cross-backend f32 division: raw and fma-residual quotients disagreed with correctly-rounded ORT at half-integer boundaries on Metal and shifted the zero point on Intel D3D. The backend-independent integer significand fallback must keep this odd-sized grid-stride path bit-exact."
369
  },
370
  "inputs": {
371
  "x": {
@@ -416,7 +415,7 @@
416
  {
417
  "name": "symmetric_half_step_tie_vec4_boundary",
418
  "provenance": {
419
- "notes": "The scale 2/255 puts the zero-point quotient just below 127.5 after correctly-rounded f32 division. Intel D3D exposed an over-correction that changed zero_point 127 to 128."
420
  },
421
  "inputs": {
422
  "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [-1.0, -0.5, 0.5, 1.0] } }
@@ -430,7 +429,7 @@
430
  {
431
  "name": "intel_d3d_compensated_zero_point_regression",
432
  "provenance": {
433
- "notes": "Explicit golden companion to symmetric_half_step_tie_vec4_boundary. It catches backend excess-precision or residual-division compensation shifting the correctly-rounded zero point from 127 to 128."
434
  },
435
  "inputs": {
436
  "x": {
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "mixed_sign_values",
 
277
  "provenance": {
278
  "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
279
  "test": "QuantizeLinearOpTest.DynamicQuantizeLinear_Max_Adjusted",
280
+ "notes": "For an all-negative rank-0 input, the adjusted maximum includes zero and produces zero_point=255."
281
  },
282
  "inputs": { "x": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [-5.0] } } },
283
  "outputs": {
 
317
  {
318
  "name": "grid_stride_reduce_1m_mixed_sign",
319
  "provenance": {
320
+ "notes": "A 1,048,576-element input selects a grid-stride reduction capped at 256 workgroups, each producing one extrema partial. The final fold must include all elements and reproduce the exact global minimum, maximum, scale, and zero point."
321
  },
322
  "inputs": {
323
  "x": {
 
335
  {
336
  "name": "dispatch_cliff_blocks_65536",
337
  "provenance": {
338
+ "notes": "A 67,108,864-element input produces 65,536 reduction blocks and a folded 65,535-by-2 dispatch. A linspace places the extrema in the first and final blocks, making incorrect two-dimensional block reconstruction or a missing over-dispatch guard observable in the scale."
339
  },
340
  "inputs": {
341
  "x": { "dtype": "float32", "shape": [67108864], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
 
364
  {
365
  "name": "non_vec4_large_finalize_fold_mixed_sign_corrected_division",
366
  "provenance": {
367
+ "notes": "Half-integer quotient boundaries can change zero-point rounding when f32 division precision differs across backends. The integer-significand fallback must keep this odd-sized grid-stride result bit-exact."
368
  },
369
  "inputs": {
370
  "x": {
 
415
  {
416
  "name": "symmetric_half_step_tie_vec4_boundary",
417
  "provenance": {
418
+ "notes": "The scale 2/255 puts the zero-point quotient just below 127.5 after correctly-rounded f32 division. The zero point must remain 127; rounding correction must not raise it to 128."
419
  },
420
  "inputs": {
421
  "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [-1.0, -0.5, 0.5, 1.0] } }
 
429
  {
430
  "name": "intel_d3d_compensated_zero_point_regression",
431
  "provenance": {
432
+ "notes": "An explicit expected output at a symmetric half-step boundary requires the correctly rounded zero point 127 rather than 128."
433
  },
434
  "inputs": {
435
  "x": {