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

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,64 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # ai.onnx.DynamicQuantizeLinear
10
+
11
+ `ai.onnx` · standard ONNX operator · ONNX opset ≥ 11
12
+
13
+ ## Description
14
+
15
+ 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]`.
16
+
17
+ See the [ONNX `DynamicQuantizeLinear` spec](https://onnx.ai/onnx/operators/onnx__DynamicQuantizeLinear.html) for the reference semantics.
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
+
35
+ | Variable | Allowed dtypes |
36
+ | --- | --- |
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
46
+ - [`dynamic-quantize-linear-quantize.wgsl.jinja`](build/webgpu/dynamic-quantize-linear-quantize.wgsl.jinja)
47
+ - [`dynamic-quantize-linear-reduce.wgsl.jinja`](build/webgpu/dynamic-quantize-linear-reduce.wgsl.jinja)
48
+ - [`dynamic-quantize-linear.wgsl.jinja`](build/webgpu/dynamic-quantize-linear.wgsl.jinja)
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
+
59
+ ```js
60
+ import { getKernel } from "@huggingface/kernels";
61
+
62
+ const kernel = await getKernel("webgpu-kernels/ai.onnx.DynamicQuantizeLinear", { version: 1 });
63
+ const { y, y_scale, y_zero_point } = await kernel({ x: { data: xData, shape: [1] } });
64
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.DynamicQuantizeLinear",
3
+ "cases": [
4
+ {
5
+ "name": "f32_64k",
6
+ "inputs": {
7
+ "x": {
8
+ "dtype": "float32",
9
+ "shape": [65536],
10
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
11
+ }
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
+ {
20
+ "name": "f32_4m",
21
+ "preset": "smoke",
22
+ "inputs": {
23
+ "x": {
24
+ "dtype": "float32",
25
+ "shape": [2048, 2048],
26
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
27
+ }
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": [
36
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
37
+ {
38
+ "type": "bandwidth",
39
+ "name": "physical_3pass_storage",
40
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
41
+ }
42
+ ]
43
+ }
44
+ },
45
+ {
46
+ "name": "alignment_cliff_non_vec4_4194303",
47
+ "preset": "smoke",
48
+ "inputs": {
49
+ "x": {
50
+ "dtype": "float32",
51
+ "shape": [4194303],
52
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
53
+ }
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,
62
+ "metrics": [
63
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
64
+ {
65
+ "type": "bandwidth",
66
+ "name": "physical_3pass_storage",
67
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
68
+ }
69
+ ]
70
+ }
71
+ },
72
+ {
73
+ "name": "alignment_healthy_vec4_4194304",
74
+ "preset": "smoke",
75
+ "inputs": {
76
+ "x": {
77
+ "dtype": "float32",
78
+ "shape": [4194304],
79
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
80
+ }
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": [
89
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
90
+ {
91
+ "type": "bandwidth",
92
+ "name": "physical_3pass_storage",
93
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ {
99
+ "name": "serial_finalize_uncapped_non_vec4_2m",
100
+ "preset": "smoke",
101
+ "inputs": {
102
+ "x": {
103
+ "dtype": "float32",
104
+ "shape": [2097153],
105
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
106
+ }
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": [
115
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
116
+ {
117
+ "type": "bandwidth",
118
+ "name": "physical_3pass_storage",
119
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
120
+ }
121
+ ]
122
+ }
123
+ },
124
+ {
125
+ "name": "grid_stride_capped_finalize_2m",
126
+ "preset": "smoke",
127
+ "inputs": {
128
+ "x": {
129
+ "dtype": "float32",
130
+ "shape": [2097152],
131
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
132
+ }
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": [
141
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
142
+ {
143
+ "type": "bandwidth",
144
+ "name": "physical_3pass_storage",
145
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
146
+ }
147
+ ]
148
+ }
149
+ },
150
+ {
151
+ "name": "below_gridcap_threshold_non_vec4_262143",
152
+ "preset": "smoke",
153
+ "inputs": {
154
+ "x": {
155
+ "dtype": "float32",
156
+ "shape": [262143],
157
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
158
+ }
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": [
167
+ { "type": "bandwidth", "name": "useful_onnx_payload", "value": "5 * numel(shapes.x) + 5" },
168
+ {
169
+ "type": "bandwidth",
170
+ "name": "physical_3pass_storage",
171
+ "value": "12 * numel(shapes.x) + 16 * min(256, ceilDiv(numel(shapes.x), 1024)) + 16"
172
+ }
173
+ ]
174
+ }
175
+ }
176
+ ]
177
+ }
build/webgpu/dynamic-quantize-linear-quantize.wgsl.jinja ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 3 of parallel DynamicQuantizeLinear: elementwise quantization using the
2
+ // read-only y_scale/y_zero_point produced by the finalize pass. Each workgroup
3
+ // covers the same WG * EPT contiguous chunk as the reduction pass, so their
4
+ // dispatch counts match. Quantization applies round-to-even and saturates the
5
+ // result to the uint8 range using the common scale and zero point.
6
+ {{ env.wgsl.resourceDeclarations }}
7
+ // ONNX DynamicQuantizeLinear uses correctly rounded f32 division followed by
8
+ // round-half-to-even. Every execution path uses these helpers so their numerical
9
+ // behavior cannot drift apart.
10
+
11
+ // WGSL permits f32 division to differ from the correctly-rounded result by
12
+ // 2.5 ULP, and fma() inherits separate multiply/add accuracy rather than
13
+ // promising a fused residual. Reconstruct the correctly-rounded normal result
14
+ // with integer significand division when the quotient can affect an integer
15
+ // rounding boundary. This is backend-independent and uses only exact u32 ops.
16
+ fn dynamic_quantize_exact_div_normal(numerator: f32, denominator: f32) -> f32 {
17
+ if (numerator == 0.0) {
18
+ return numerator;
19
+ }
20
+
21
+ let numerator_bits = bitcast<u32>(numerator);
22
+ let denominator_bits = bitcast<u32>(denominator);
23
+ let sign_bits = (numerator_bits ^ denominator_bits) & 0x80000000u;
24
+
25
+ let numerator_abs = numerator_bits & 0x7fffffffu;
26
+ let denominator_abs = denominator_bits & 0x7fffffffu;
27
+ if (denominator_abs == 0u
28
+ || (numerator_abs & 0x7f800000u) == 0x7f800000u
29
+ || (denominator_abs & 0x7f800000u) == 0x7f800000u) {
30
+ return numerator / denominator;
31
+ }
32
+ var numerator_mantissa = numerator_abs & 0x007fffffu;
33
+ var denominator_mantissa = denominator_abs & 0x007fffffu;
34
+ let numerator_biased_exponent = (numerator_abs >> 23u) & 0xffu;
35
+ let denominator_biased_exponent = (denominator_abs >> 23u) & 0xffu;
36
+ var numerator_exponent: i32;
37
+ var denominator_exponent: i32;
38
+
39
+ if (numerator_biased_exponent == 0u) {
40
+ numerator_exponent = -126;
41
+ // Zero returned above. A non-zero subnormal reaches the implicit-bit
42
+ // position in at most 23 exact shifts.
43
+ while ((numerator_mantissa & 0x00800000u) == 0u) {
44
+ numerator_mantissa = numerator_mantissa << 1u;
45
+ numerator_exponent = numerator_exponent - 1;
46
+ }
47
+ } else {
48
+ numerator_mantissa = numerator_mantissa | 0x00800000u;
49
+ numerator_exponent = i32(numerator_biased_exponent) - 127;
50
+ }
51
+
52
+ if (denominator_biased_exponent == 0u) {
53
+ denominator_exponent = -126;
54
+ while ((denominator_mantissa & 0x00800000u) == 0u) {
55
+ denominator_mantissa = denominator_mantissa << 1u;
56
+ denominator_exponent = denominator_exponent - 1;
57
+ }
58
+ } else {
59
+ denominator_mantissa = denominator_mantissa | 0x00800000u;
60
+ denominator_exponent = i32(denominator_biased_exponent) - 127;
61
+ }
62
+
63
+ var quotient_exponent = numerator_exponent - denominator_exponent;
64
+ var remainder = numerator_mantissa;
65
+ if (remainder < denominator_mantissa) {
66
+ remainder = remainder << 1u;
67
+ quotient_exponent = quotient_exponent - 1;
68
+ }
69
+
70
+ // The normalized ratio is now in [1, 2). Emit its implicit bit followed by
71
+ // all 23 stored significand bits using exact binary long division.
72
+ var quotient_mantissa = 0x00800000u;
73
+ remainder = remainder - denominator_mantissa;
74
+ for (var digit = 0u; digit < 23u; digit = digit + 1u) {
75
+ remainder = remainder << 1u;
76
+ if (remainder >= denominator_mantissa) {
77
+ remainder = remainder - denominator_mantissa;
78
+ quotient_mantissa = quotient_mantissa | (1u << (22u - digit));
79
+ }
80
+ }
81
+
82
+ // Round the 24-bit significand to nearest, ties to even. remainder and its
83
+ // doubled value are below 2^25, so no u32 overflow is possible.
84
+ let twice_remainder = remainder << 1u;
85
+ if (twice_remainder > denominator_mantissa
86
+ || (twice_remainder == denominator_mantissa && (quotient_mantissa & 1u) != 0u)) {
87
+ quotient_mantissa = quotient_mantissa + 1u;
88
+ }
89
+ if (quotient_mantissa == 0x01000000u) {
90
+ quotient_mantissa = quotient_mantissa >> 1u;
91
+ quotient_exponent = quotient_exponent + 1;
92
+ }
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
103
+ | (u32(biased_exponent) << 23u)
104
+ | (quotient_mantissa & 0x007fffffu);
105
+ return bitcast<f32>(result_bits);
106
+ }
107
+
108
+ fn dynamic_quantize_division_may_cross_half(estimate: f32) -> bool {
109
+ let lower = floor(estimate);
110
+ let fraction = estimate - lower;
111
+ let magnitude = abs(estimate);
112
+ let magnitude_bits = bitcast<u32>(magnitude);
113
+ let adjacent = bitcast<f32>(magnitude_bits + 1u);
114
+ let ulp = adjacent - magnitude;
115
+ // Division is allowed 2.5 ULP error. Eight ULP also covers the factor-of-two
116
+ // ULP change when an estimate straddles the 0.5 exponent boundary.
117
+ return abs(fraction - 0.5) <= ulp * 8.0;
118
+ }
119
+
120
+ fn round_dynamic_half_to_even(value: f32, scale: f32) -> i32 {
121
+ let estimate = value / scale;
122
+ var scaled = estimate;
123
+ // `select` evaluates both value operands in WGSL; use control flow so the
124
+ // 23-bit software divide remains a rare boundary fallback, not O(23) work
125
+ // for every quantized element.
126
+ if (dynamic_quantize_division_may_cross_half(estimate)) {
127
+ scaled = dynamic_quantize_exact_div_normal(value, scale);
128
+ }
129
+ let lower = floor(scaled);
130
+ let fraction = scaled - lower;
131
+ if (fraction < 0.5) {
132
+ return i32(lower);
133
+ }
134
+ if (fraction > 0.5) {
135
+ return i32(lower + 1.0);
136
+ }
137
+
138
+ let upper = lower + 1.0;
139
+ let half_lower = floor(lower * 0.5);
140
+ let lower_is_even = (lower - half_lower * 2.0) == 0.0;
141
+ return i32(select(upper, lower, lower_is_even));
142
+ }
143
+
144
+
145
+ const WG: u32 = {{ workgroupSize }}u;
146
+ {% if not vec4 %}
147
+ const EPT: u32 = {{ elemsPerThread }}u;
148
+
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.
163
+ let count4 = params.count / 4u;
164
+ let idx4 = blk * WG + tid;
165
+ if (idx4 < count4) {
166
+ let v = x[idx4];
167
+ let o = idx4 * 4u;
168
+ y[o + 0u] = u32(clamp(round_dynamic_half_to_even(v.x, scale) + zp_i32, 0, 255));
169
+ y[o + 1u] = u32(clamp(round_dynamic_half_to_even(v.y, scale) + zp_i32, 0, 255));
170
+ y[o + 2u] = u32(clamp(round_dynamic_half_to_even(v.z, scale) + zp_i32, 0, 255));
171
+ y[o + 3u] = u32(clamp(round_dynamic_half_to_even(v.w, scale) + zp_i32, 0, 255));
172
+ }
173
+ {% else %}
174
+ let base = blk * WG * EPT;
175
+ for (var e = 0u; e < EPT; e = e + 1u) {
176
+ let idx = base + e * WG + tid;
177
+ if (idx < params.count) {
178
+ let q = clamp(round_dynamic_half_to_even(x[idx], scale) + zp_i32, 0, 255);
179
+ y[idx] = u32(q);
180
+ }
181
+ }
182
+ {% endif %}
183
+ }
build/webgpu/dynamic-quantize-linear-reduce.wgsl.jinja ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 %}
15
+ {{ env.wgsl.resourceDeclarations }}
16
+
17
+ const WG: u32 = {{ workgroupSize }}u;
18
+ {% if not gridStride and not vec4 %}
19
+ const EPT: u32 = {{ elemsPerThread }}u;
20
+
21
+ {% endif %}
22
+ var<workgroup> wgMin: array<f32, WG>;
23
+ 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;
34
+ {% if gridStride %}
35
+ let blk = wg.x;
36
+ // The capped x-only dispatch is the full logical grid. Each workgroup writes
37
+ // exactly one partial, matching finalize's numPartials uniform.
38
+ let gridThreads = nwg.x * WG;
39
+ {% else %}
40
+ // 2D-folded block index: wg.y carries the high bits past the device's
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 %}
48
+ let numBlocks = (params.count + WG * EPT - 1u) / (WG * EPT);
49
+ {% endif %}
50
+ if (blk >= numBlocks) { return; }
51
+ {% endif %}
52
+ var localMin = 0.0;
53
+ var localMax = 0.0;
54
+ {% if gridStride %}
55
+ {% if vec4 %}
56
+ // Adjacent lanes read adjacent vec4s on each grid-stride iteration.
57
+ let count4 = params.count / 4u;
58
+ for (var idx4 = blk * WG + tid; idx4 < count4; idx4 = idx4 + gridThreads) {
59
+ let v = x[idx4];
60
+ localMin = min(localMin, min(min(v.x, v.y), min(v.z, v.w)));
61
+ localMax = max(localMax, max(max(v.x, v.y), max(v.z, v.w)));
62
+ }
63
+ {% else %}
64
+ for (var idx = blk * WG + tid; idx < params.count; idx = idx + gridThreads) {
65
+ let value = x[idx];
66
+ localMin = min(localMin, value);
67
+ localMax = max(localMax, value);
68
+ }
69
+ {% endif %}
70
+ {% else %}
71
+ {% if vec4 %}
72
+ // Each thread reads one contiguous vec4; this path assumes params.count is
73
+ // divisible by 4.
74
+ let count4 = params.count / 4u;
75
+ let idx4 = blk * WG + tid;
76
+ if (idx4 < count4) {
77
+ let v = x[idx4];
78
+ localMin = min(localMin, min(min(v.x, v.y), min(v.z, v.w)));
79
+ localMax = max(localMax, max(max(v.x, v.y), max(v.z, v.w)));
80
+ }
81
+ {% else %}
82
+ let base = blk * WG * EPT;
83
+ for (var e = 0u; e < EPT; e = e + 1u) {
84
+ let idx = base + e * WG + tid;
85
+ if (idx < params.count) {
86
+ let v = x[idx];
87
+ localMin = min(localMin, v);
88
+ localMax = max(localMax, v);
89
+ }
90
+ }
91
+ {% endif %}
92
+ {% endif %}
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
+ }
115
+ {% else %}
116
+ // No-subgroups fallback: full workgroup-tree min/max over per-thread partials.
117
+ wgMin[tid] = localMin;
118
+ wgMax[tid] = localMax;
119
+ workgroupBarrier();
120
+ for (var step = WG >> 1u; step > 0u; step = step >> 1u) {
121
+ if (tid < step) {
122
+ wgMin[tid] = min(wgMin[tid], wgMin[tid + step]);
123
+ wgMax[tid] = max(wgMax[tid], wgMax[tid + step]);
124
+ }
125
+ workgroupBarrier();
126
+ }
127
+ if (tid == 0u) {
128
+ partial_min[blk] = wgMin[0];
129
+ partial_max[blk] = wgMax[0];
130
+ }
131
+ {% endif %}
132
+ }
build/webgpu/dynamic-quantize-linear.wgsl.jinja ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 %}
5
+ // The serial path computes the complete range, scale, zero point, and quantized
6
+ // output in one invocation.
7
+ {% endif %}
8
+ {{ env.wgsl.resourceDeclarations }}
9
+ // ONNX DynamicQuantizeLinear uses correctly rounded f32 division followed by
10
+ // round-half-to-even. Every execution path uses these helpers so their numerical
11
+ // behavior cannot drift apart.
12
+
13
+ // WGSL permits f32 division to differ from the correctly-rounded result by
14
+ // 2.5 ULP, and fma() inherits separate multiply/add accuracy rather than
15
+ // promising a fused residual. Reconstruct the correctly-rounded normal result
16
+ // with integer significand division when the quotient can affect an integer
17
+ // rounding boundary. This is backend-independent and uses only exact u32 ops.
18
+ fn dynamic_quantize_exact_div_normal(numerator: f32, denominator: f32) -> f32 {
19
+ if (numerator == 0.0) {
20
+ return numerator;
21
+ }
22
+
23
+ let numerator_bits = bitcast<u32>(numerator);
24
+ let denominator_bits = bitcast<u32>(denominator);
25
+ let sign_bits = (numerator_bits ^ denominator_bits) & 0x80000000u;
26
+
27
+ let numerator_abs = numerator_bits & 0x7fffffffu;
28
+ let denominator_abs = denominator_bits & 0x7fffffffu;
29
+ if (denominator_abs == 0u
30
+ || (numerator_abs & 0x7f800000u) == 0x7f800000u
31
+ || (denominator_abs & 0x7f800000u) == 0x7f800000u) {
32
+ return numerator / denominator;
33
+ }
34
+ var numerator_mantissa = numerator_abs & 0x007fffffu;
35
+ var denominator_mantissa = denominator_abs & 0x007fffffu;
36
+ let numerator_biased_exponent = (numerator_abs >> 23u) & 0xffu;
37
+ let denominator_biased_exponent = (denominator_abs >> 23u) & 0xffu;
38
+ var numerator_exponent: i32;
39
+ var denominator_exponent: i32;
40
+
41
+ if (numerator_biased_exponent == 0u) {
42
+ numerator_exponent = -126;
43
+ // Zero returned above. A non-zero subnormal reaches the implicit-bit
44
+ // position in at most 23 exact shifts.
45
+ while ((numerator_mantissa & 0x00800000u) == 0u) {
46
+ numerator_mantissa = numerator_mantissa << 1u;
47
+ numerator_exponent = numerator_exponent - 1;
48
+ }
49
+ } else {
50
+ numerator_mantissa = numerator_mantissa | 0x00800000u;
51
+ numerator_exponent = i32(numerator_biased_exponent) - 127;
52
+ }
53
+
54
+ if (denominator_biased_exponent == 0u) {
55
+ denominator_exponent = -126;
56
+ while ((denominator_mantissa & 0x00800000u) == 0u) {
57
+ denominator_mantissa = denominator_mantissa << 1u;
58
+ denominator_exponent = denominator_exponent - 1;
59
+ }
60
+ } else {
61
+ denominator_mantissa = denominator_mantissa | 0x00800000u;
62
+ denominator_exponent = i32(denominator_biased_exponent) - 127;
63
+ }
64
+
65
+ var quotient_exponent = numerator_exponent - denominator_exponent;
66
+ var remainder = numerator_mantissa;
67
+ if (remainder < denominator_mantissa) {
68
+ remainder = remainder << 1u;
69
+ quotient_exponent = quotient_exponent - 1;
70
+ }
71
+
72
+ // The normalized ratio is now in [1, 2). Emit its implicit bit followed by
73
+ // all 23 stored significand bits using exact binary long division.
74
+ var quotient_mantissa = 0x00800000u;
75
+ remainder = remainder - denominator_mantissa;
76
+ for (var digit = 0u; digit < 23u; digit = digit + 1u) {
77
+ remainder = remainder << 1u;
78
+ if (remainder >= denominator_mantissa) {
79
+ remainder = remainder - denominator_mantissa;
80
+ quotient_mantissa = quotient_mantissa | (1u << (22u - digit));
81
+ }
82
+ }
83
+
84
+ // Round the 24-bit significand to nearest, ties to even. remainder and its
85
+ // doubled value are below 2^25, so no u32 overflow is possible.
86
+ let twice_remainder = remainder << 1u;
87
+ if (twice_remainder > denominator_mantissa
88
+ || (twice_remainder == denominator_mantissa && (quotient_mantissa & 1u) != 0u)) {
89
+ quotient_mantissa = quotient_mantissa + 1u;
90
+ }
91
+ if (quotient_mantissa == 0x01000000u) {
92
+ quotient_mantissa = quotient_mantissa >> 1u;
93
+ quotient_exponent = quotient_exponent + 1;
94
+ }
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
105
+ | (u32(biased_exponent) << 23u)
106
+ | (quotient_mantissa & 0x007fffffu);
107
+ return bitcast<f32>(result_bits);
108
+ }
109
+
110
+ fn dynamic_quantize_division_may_cross_half(estimate: f32) -> bool {
111
+ let lower = floor(estimate);
112
+ let fraction = estimate - lower;
113
+ let magnitude = abs(estimate);
114
+ let magnitude_bits = bitcast<u32>(magnitude);
115
+ let adjacent = bitcast<f32>(magnitude_bits + 1u);
116
+ let ulp = adjacent - magnitude;
117
+ // Division is allowed 2.5 ULP error. Eight ULP also covers the factor-of-two
118
+ // ULP change when an estimate straddles the 0.5 exponent boundary.
119
+ return abs(fraction - 0.5) <= ulp * 8.0;
120
+ }
121
+
122
+ fn round_dynamic_half_to_even(value: f32, scale: f32) -> i32 {
123
+ let estimate = value / scale;
124
+ var scaled = estimate;
125
+ // `select` evaluates both value operands in WGSL; use control flow so the
126
+ // 23-bit software divide remains a rare boundary fallback, not O(23) work
127
+ // for every quantized element.
128
+ if (dynamic_quantize_division_may_cross_half(estimate)) {
129
+ scaled = dynamic_quantize_exact_div_normal(value, scale);
130
+ }
131
+ let lower = floor(scaled);
132
+ let fraction = scaled - lower;
133
+ if (fraction < 0.5) {
134
+ return i32(lower);
135
+ }
136
+ if (fraction > 0.5) {
137
+ return i32(lower + 1.0);
138
+ }
139
+
140
+ let upper = lower + 1.0;
141
+ let half_lower = floor(lower * 0.5);
142
+ let lower_is_even = (lower - half_lower * 2.0) == 0.0;
143
+ return i32(select(upper, lower, lower_is_even));
144
+ }
145
+
146
+
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) {
154
+ min_value = min(min_value, partial_min[i]);
155
+ max_value = max(max_value, partial_max[i]);
156
+ }
157
+ {% else %}
158
+ var min_value = 0.0;
159
+ var max_value = 0.0;
160
+ for (var i = 0u; i < params.count; i = i + 1u) {
161
+ min_value = min(min_value, x[i]);
162
+ max_value = max(max_value, x[i]);
163
+ }
164
+ {% endif %}
165
+ var scale = dynamic_quantize_exact_div_normal(max_value - min_value, 255.0);
166
+ if (scale == 0.0) {
167
+ scale = 1.0;
168
+ }
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);
176
+ }
177
+ {% endif %}
178
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
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)",
51
+ "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
52
+ "inputCount": "numel(shapes.x)",
53
+ "elementsPerBlock": "tunables.WORKGROUP_SIZE * tunables.ELEMENTS_PER_THREAD",
54
+ "fullPartials": "ceilDiv(inputCount, elementsPerBlock)",
55
+ "gridPartials": "min(tunables.MAX_GRID_PARTIALS, fullPartials)",
56
+ "baseContract": "ranks.x >= 1 and ranks.y == ranks.x and inputCount == numel(shapes.y) and ranks.y_scale == 0 and ranks.y_zero_point == 0",
57
+ "serialContract": "ranks.x >= 0 and ranks.y == ranks.x and inputCount == numel(shapes.y) and ranks.y_scale == 0 and ranks.y_zero_point == 0",
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,
240
+ "inputElement": "\"vec4<f32>\"",
241
+ "useSubgroups": "device.features.has(\"subgroups\")"
242
+ },
243
+ "intermediates": [
244
+ { "id": "partial_min", "dtype": "float32", "shape": "[fullPartials]" },
245
+ { "id": "partial_max", "dtype": "float32", "shape": "[fullPartials]" }
246
+ ],
247
+ "passes": [
248
+ {
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,
283
+ "inputElement": "\"f32\"",
284
+ "useSubgroups": "device.features.has(\"subgroups\")"
285
+ },
286
+ "intermediates": [
287
+ { "id": "partial_min", "dtype": "float32", "shape": "[fullPartials]" },
288
+ { "id": "partial_max", "dtype": "float32", "shape": "[fullPartials]" }
289
+ ],
290
+ "passes": [
291
+ {
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,
326
+ "inputElement": "\"vec4<f32>\"",
327
+ "useSubgroups": "device.features.has(\"subgroups\")"
328
+ },
329
+ "intermediates": [
330
+ { "id": "partial_min", "dtype": "float32", "shape": "[gridPartials]" },
331
+ { "id": "partial_max", "dtype": "float32", "shape": "[gridPartials]" }
332
+ ],
333
+ "passes": [
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,
369
+ "inputElement": "\"f32\"",
370
+ "useSubgroups": "device.features.has(\"subgroups\")"
371
+ },
372
+ "intermediates": [
373
+ { "id": "partial_min", "dtype": "float32", "shape": "[gridPartials]" },
374
+ { "id": "partial_max", "dtype": "float32", "shape": "[gridPartials]" }
375
+ ],
376
+ "passes": [
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
+ ]
402
+ }
403
+ ]
404
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.DynamicQuantizeLinear",
3
+ "cases": [
4
+ {
5
+ "name": "mixed_sign_values",
6
+ "inputs": {
7
+ "x": {
8
+ "dtype": "float32",
9
+ "shape": [6],
10
+ "data": { "kind": "values", "values": [-1.0, -0.5, 0.0, 0.5, 1.0, 2.0] }
11
+ }
12
+ },
13
+ "outputs": {
14
+ "y": { "dtype": "uint8", "shape": [6] },
15
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
16
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
17
+ }
18
+ },
19
+ {
20
+ "name": "ort_mixed_sign_rounding_formulation",
21
+ "provenance": {
22
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
23
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear"
24
+ },
25
+ "inputs": {
26
+ "x": {
27
+ "dtype": "float32",
28
+ "shape": [6],
29
+ "data": { "kind": "values", "values": [0.0, 2.0, -3.0, -2.5, 1.34, 0.5] }
30
+ }
31
+ },
32
+ "outputs": {
33
+ "y": { "dtype": "uint8", "shape": [6] },
34
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
35
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
36
+ }
37
+ },
38
+ {
39
+ "name": "all_positive_includes_zero_range",
40
+ "inputs": {
41
+ "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0] } }
42
+ },
43
+ "outputs": {
44
+ "y": { "dtype": "uint8", "shape": [4] },
45
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
46
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
47
+ }
48
+ },
49
+ {
50
+ "name": "constant_zero_scale_fallback",
51
+ "inputs": {
52
+ "x": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0] } }
53
+ },
54
+ "outputs": {
55
+ "y": { "dtype": "uint8", "shape": [4] },
56
+ "y_scale": { "dtype": "float32", "shape": [] },
57
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
58
+ }
59
+ },
60
+ {
61
+ "name": "subnormal_positive_range_preserves_scale_gpu_gap",
62
+ "skipGpu": {
63
+ "category": "permanent",
64
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal scale values required by this fixture. Backend evidence: DynamicQuantizeLinear computes scale=(max-min)/255 then x/scale; a subnormal input range yields a subnormal scale and Metal flushes denormals in floating-point division (-> NaN/0), so subnormal-range cases remain CPU-reference-only."
65
+ },
66
+ "provenance": {
67
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
68
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear",
69
+ "notes": "Tiny but valid positive range: y_scale is a float32 subnormal, so a backend must not flush it to zero and take the constant-input fallback."
70
+ },
71
+ "inputs": { "x": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.0, 1e-40] } } },
72
+ "outputs": {
73
+ "y": { "dtype": "uint8", "shape": [2], "tolerance": 0 },
74
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0 },
75
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
76
+ }
77
+ },
78
+ {
79
+ "name": "subnormal_negative_range_preserves_scale_gpu_gap",
80
+ "skipGpu": {
81
+ "category": "permanent",
82
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal scale values required by this fixture. Backend evidence: DynamicQuantizeLinear computes scale=(max-min)/255 then x/scale; a subnormal input range yields a subnormal scale and Metal flushes denormals in floating-point division (-> NaN/0), so subnormal-range cases remain CPU-reference-only."
83
+ },
84
+ "provenance": {
85
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
86
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear_Max_Adjusted",
87
+ "notes": "Tiny but valid negative range: ONNX adjusts the max to zero, producing a float32 subnormal scale and a high zero point rather than the constant-input fallback."
88
+ },
89
+ "inputs": { "x": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [-1e-40, 0.0] } } },
90
+ "outputs": {
91
+ "y": { "dtype": "uint8", "shape": [2], "tolerance": 0 },
92
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0 },
93
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
94
+ }
95
+ },
96
+ {
97
+ "name": "subnormal_mixed_sign_range_vec4_reduce_gpu_gap",
98
+ "skipGpu": {
99
+ "category": "permanent",
100
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal scale values required by this fixture. Backend evidence: DynamicQuantizeLinear computes scale=(max-min)/255 then x/scale; a subnormal input range yields a subnormal scale and Metal flushes denormals in floating-point division (-> NaN/0), so subnormal-range cases remain CPU-reference-only."
101
+ },
102
+ "provenance": {
103
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
104
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear",
105
+ "notes": "Tiny mixed-sign range with four elements exercises the vec4 reduce path: ONNX should keep a subnormal scale and nonzero zero point."
106
+ },
107
+ "inputs": {
108
+ "x": {
109
+ "dtype": "float32",
110
+ "shape": [4],
111
+ "data": { "kind": "values", "values": [-1e-40, -5e-41, 5e-41, 1e-40] }
112
+ }
113
+ },
114
+ "outputs": {
115
+ "y": { "dtype": "uint8", "shape": [4], "tolerance": 0 },
116
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0 },
117
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
118
+ }
119
+ },
120
+ {
121
+ "name": "min_adjusted_positive_2d_ort",
122
+ "provenance": {
123
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
124
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear_Min_Adjusted"
125
+ },
126
+ "inputs": {
127
+ "x": {
128
+ "dtype": "float32",
129
+ "shape": [3, 4],
130
+ "data": { "kind": "values", "values": [1.0, 2.1, 1.3, 2.5, 3.34, 4.0, 1.5, 2.6, 3.9, 4.0, 3.0, 2.345] }
131
+ }
132
+ },
133
+ "outputs": {
134
+ "y": { "dtype": "uint8", "shape": [3, 4] },
135
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
136
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
137
+ }
138
+ },
139
+ {
140
+ "name": "max_adjusted_negative_ort",
141
+ "provenance": {
142
+ "source": "onnxruntime/test/providers/cpu/tensor/dynamic_quantize_linear_test.cc",
143
+ "test": "QuantizeLinearOpTest.DynamicQuantizeLinear_Max_Adjusted"
144
+ },
145
+ "inputs": {
146
+ "x": {
147
+ "dtype": "float32",
148
+ "shape": [6],
149
+ "data": { "kind": "values", "values": [-1.0, -2.1, -1.3, -2.5, -3.34, -4.0] }
150
+ }
151
+ },
152
+ "outputs": {
153
+ "y": { "dtype": "uint8", "shape": [6] },
154
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
155
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
156
+ }
157
+ },
158
+ {
159
+ "name": "singleton_positive_adjusts_min_to_zero",
160
+ "inputs": { "x": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [5.0] } } },
161
+ "outputs": {
162
+ "y": { "dtype": "uint8", "shape": [1] },
163
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
164
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
165
+ }
166
+ },
167
+ {
168
+ "name": "onnx_backend_dynamicquantizelinear",
169
+ "provenance": { "source": "cmake/external/onnx/onnx/backend/test/data/node/test_dynamicquantizelinear" },
170
+ "inputs": {
171
+ "x": {
172
+ "dtype": "float32",
173
+ "shape": [6],
174
+ "data": { "kind": "values", "values": [0.0, 2.0, -3.0, -2.5, 1.340000033378601, 0.5] }
175
+ }
176
+ },
177
+ "outputs": {
178
+ "y": { "dtype": "uint8", "shape": [6], "tolerance": 0 },
179
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.00001 },
180
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
181
+ }
182
+ },
183
+ {
184
+ "name": "onnx_backend_dynamicquantizelinear_max_adjusted",
185
+ "provenance": {
186
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_dynamicquantizelinear_max_adjusted"
187
+ },
188
+ "inputs": {
189
+ "x": {
190
+ "dtype": "float32",
191
+ "shape": [6],
192
+ "data": {
193
+ "kind": "values",
194
+ "values": [-1.0, -2.0999999046325684, -1.2999999523162842, -2.5, -3.3399999141693115, -4.0]
195
+ }
196
+ }
197
+ },
198
+ "outputs": {
199
+ "y": { "dtype": "uint8", "shape": [6], "tolerance": 0 },
200
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.00001 },
201
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
202
+ }
203
+ },
204
+ {
205
+ "name": "onnx_backend_dynamicquantizelinear_min_adjusted",
206
+ "provenance": {
207
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_dynamicquantizelinear_min_adjusted"
208
+ },
209
+ "inputs": {
210
+ "x": {
211
+ "dtype": "float32",
212
+ "shape": [3, 4],
213
+ "data": {
214
+ "kind": "values",
215
+ "values": [1.0, 2.0999999046325684, 1.2999999523162842, 2.5, 3.3399999141693115, 4.0, 1.5, 2.5999999046325684, 3.9000000953674316, 4.0, 3.0, 2.3450000286102295]
216
+ }
217
+ }
218
+ },
219
+ "outputs": {
220
+ "y": { "dtype": "uint8", "shape": [3, 4], "tolerance": 0 },
221
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.00001 },
222
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
223
+ }
224
+ },
225
+ {
226
+ "name": "parallel_mixed_sign_5000",
227
+ "inputs": {
228
+ "x": {
229
+ "dtype": "float32",
230
+ "shape": [5000],
231
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
232
+ }
233
+ },
234
+ "outputs": {
235
+ "y": { "dtype": "uint8", "shape": [5000], "tolerance": 0 },
236
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
237
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
238
+ }
239
+ },
240
+ {
241
+ "name": "parallel_all_negative_1500_tail",
242
+ "inputs": {
243
+ "x": {
244
+ "dtype": "float32",
245
+ "shape": [1500],
246
+ "data": {
247
+ "kind": "cycle",
248
+ "values": [-1.0, -2.1, -1.3, -2.5, -3.34, -4.0, -0.25, -7.75, -0.001, -6.5, -2.75, -5.125, -3.5]
249
+ }
250
+ }
251
+ },
252
+ "outputs": {
253
+ "y": { "dtype": "uint8", "shape": [1500], "tolerance": 0 },
254
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
255
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
256
+ }
257
+ },
258
+ {
259
+ "name": "parallel_const_zero_1027_tail",
260
+ "inputs": { "x": { "dtype": "float32", "shape": [1027], "data": { "kind": "cycle", "values": [0.0] } } },
261
+ "outputs": {
262
+ "y": { "dtype": "uint8", "shape": [1027], "tolerance": 0 },
263
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0 },
264
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
265
+ }
266
+ },
267
+ {
268
+ "name": "scalar_rank0_positive",
269
+ "inputs": { "x": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [5.0] } } },
270
+ "outputs": {
271
+ "y": { "dtype": "uint8", "shape": [], "tolerance": 0 },
272
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
273
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
274
+ }
275
+ },
276
+ {
277
+ "name": "scalar_rank0_negative",
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": {
285
+ "y": { "dtype": "uint8", "shape": [], "tolerance": 0 },
286
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
287
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
288
+ }
289
+ },
290
+ {
291
+ "name": "empty_zero_dim",
292
+ "inputs": { "x": { "dtype": "float32", "shape": [0], "data": { "kind": "values", "values": [] } } },
293
+ "outputs": {
294
+ "y": { "dtype": "uint8", "shape": [0], "tolerance": 0 },
295
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.0001 },
296
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
297
+ }
298
+ },
299
+ {
300
+ "name": "ort_caseB_empty",
301
+ "inputs": { "x": { "dtype": "float32", "shape": [0], "data": { "kind": "values", "values": [] } } },
302
+ "outputs": {
303
+ "y": { "dtype": "uint8", "shape": [0], "data": { "kind": "values", "values": [] }, "tolerance": 0.001 },
304
+ "y_scale": {
305
+ "dtype": "float32",
306
+ "shape": [],
307
+ "data": { "kind": "values", "values": [1.0] },
308
+ "tolerance": 0.001
309
+ },
310
+ "y_zero_point": {
311
+ "dtype": "uint8",
312
+ "shape": [],
313
+ "data": { "kind": "values", "values": [0] },
314
+ "tolerance": 0.001
315
+ }
316
+ }
317
+ },
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": {
325
+ "dtype": "float32",
326
+ "shape": [1024, 1024],
327
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
328
+ }
329
+ },
330
+ "outputs": {
331
+ "y": { "dtype": "uint8", "shape": [1024, 1024], "tolerance": 0 },
332
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
333
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
334
+ }
335
+ },
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 } }
343
+ },
344
+ "outputs": {
345
+ "y": { "dtype": "uint8", "shape": [67108864], "tolerance": 1 },
346
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
347
+ "y_zero_point": { "dtype": "uint8", "shape": [] }
348
+ }
349
+ },
350
+ {
351
+ "name": "grid_stride_reduce_clean_int_262145",
352
+ "inputs": {
353
+ "x": {
354
+ "dtype": "float32",
355
+ "shape": [262145],
356
+ "data": { "kind": "cycle", "values": [0.0, 32.0, 64.0, 96.0, 128.0, 160.0, 192.0, 224.0, 255.0] }
357
+ }
358
+ },
359
+ "outputs": {
360
+ "y": { "dtype": "uint8", "shape": [262145], "tolerance": 0 },
361
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0 },
362
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
363
+ }
364
+ },
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": {
372
+ "dtype": "float32",
373
+ "shape": [4194305],
374
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
375
+ }
376
+ },
377
+ "outputs": {
378
+ "y": { "dtype": "uint8", "shape": [4194305], "tolerance": 0 },
379
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
380
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
381
+ }
382
+ },
383
+ {
384
+ "name": "per_token_activation_2d_vec4",
385
+ "inputs": {
386
+ "x": {
387
+ "dtype": "float32",
388
+ "shape": [3, 4096],
389
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.019, "scale": 2.5 }
390
+ }
391
+ },
392
+ "outputs": {
393
+ "y": { "dtype": "uint8", "shape": [3, 4096], "tolerance": 0 },
394
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
395
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
396
+ }
397
+ },
398
+ {
399
+ "name": "non_vec4_odd_hidden_bridge",
400
+ "inputs": {
401
+ "x": {
402
+ "dtype": "float32",
403
+ "shape": [2049],
404
+ "data": {
405
+ "kind": "cycle",
406
+ "values": [-1.0, -2.1, -1.3, 2.5, 3.34, 4.0, -0.25, 7.75, -0.001, 6.5, -2.75, 5.125, -3.5]
407
+ }
408
+ }
409
+ },
410
+ "outputs": {
411
+ "y": { "dtype": "uint8", "shape": [2049], "tolerance": 0 },
412
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
413
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
414
+ }
415
+ },
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] } }
423
+ },
424
+ "outputs": {
425
+ "y": { "dtype": "uint8", "shape": [4], "tolerance": 0 },
426
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
427
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
428
+ }
429
+ },
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": {
437
+ "dtype": "float32",
438
+ "shape": [8],
439
+ "data": { "kind": "values", "values": [-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0] }
440
+ }
441
+ },
442
+ "outputs": {
443
+ "y": {
444
+ "dtype": "uint8",
445
+ "shape": [8],
446
+ "data": { "kind": "values", "values": [0, 63, 95, 127, 159, 191, 223, 254] },
447
+ "tolerance": 0
448
+ },
449
+ "y_scale": {
450
+ "dtype": "float32",
451
+ "shape": [],
452
+ "data": { "kind": "values", "values": [0.01568627543747425] },
453
+ "tolerance": 0
454
+ },
455
+ "y_zero_point": { "dtype": "uint8", "shape": [], "data": { "kind": "values", "values": [127] }, "tolerance": 0 }
456
+ }
457
+ },
458
+ {
459
+ "name": "grid_stride_threshold_boundary_vec4_262144",
460
+ "inputs": {
461
+ "x": {
462
+ "dtype": "float32",
463
+ "shape": [262144],
464
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.017, "scale": 3.0 }
465
+ }
466
+ },
467
+ "outputs": {
468
+ "y": { "dtype": "uint8", "shape": [262144], "tolerance": 0 },
469
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
470
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
471
+ }
472
+ },
473
+ {
474
+ "name": "multidim_non_vec4_parallel_reduce_2d",
475
+ "inputs": {
476
+ "x": {
477
+ "dtype": "float32",
478
+ "shape": [17, 120],
479
+ "data": { "kind": "fillFloat32", "sinStep": 0.031, "cosStep": 0.023, "scale": 2.0 }
480
+ }
481
+ },
482
+ "outputs": {
483
+ "y": { "dtype": "uint8", "shape": [17, 120], "tolerance": 0 },
484
+ "y_scale": { "dtype": "float32", "shape": [], "tolerance": 0.000001 },
485
+ "y_zero_point": { "dtype": "uint8", "shape": [], "tolerance": 0 }
486
+ }
487
+ }
488
+ ]
489
+ }