Xenova HF Staff commited on
Commit
7a72eee
·
verified ·
1 Parent(s): fbb37c8

sync 91d990483a17

Browse files
README.md CHANGED
@@ -12,29 +12,29 @@ tags:
12
 
13
  ## Description
14
 
15
- Performs convolution on a quantized input using quantized weights, producing a quantized output. Inputs, weights, and output each carry their own scale and zero-point tensors (per-tensor or per-output-channel); an optional int32 bias must be pre-quantized with `scale = x_scale * w_scale` and zero point 0.
16
 
17
  See the [ONNX `QLinearConv` spec](https://onnx.ai/onnx/operators/onnx__QLinearConv.html) for the reference semantics.
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
- | --- | --- | --- | --- | --- | --- | --- |
23
- | `x` | `x` | `TX` | — | — | Quantized input data tensor shaped `(N x C x D1 x ... x Dn)`. | required |
24
- | `x_scale` | `x_scale` | `TF` | — | — | Per-tensor scale for input `x`. | required |
25
- | `x_zero_point` | `x_zero_point` | `TX` | — | — | Per-tensor zero point for input `x`. | required |
26
- | `w` | `w` | `TW` | — | — | Quantized weight tensor shaped `(M x C/group x k1 x ... x kn)`. | required |
27
- | `w_scale` | `w_scale` | `TF` | — | — | Scale for weights `w`; scalar for per-tensor or 1-D of length M for per-output-channel quantization. | required |
28
- | `w_zero_point` | `w_zero_point` | `TW` | — | — | Zero point for weights `w`; scalar or 1-D of length M matching `w_scale`. | required |
29
- | `y_scale` | `y_scale` | `TF` | — | — | Per-tensor scale for output `y`. | required |
30
- | `y_zero_point` | `y_zero_point` | `TY` | — | — | Per-tensor zero point for output `y`. | required |
31
- | `B` | `B` | `int32` | `1` | — | Optional 1-D bias of length `M`, pre-quantized with scale `x_scale * w_scale` and zero point 0. | optional |
32
 
33
  ## Outputs
34
 
35
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
36
- | --- | --- | --- | --- | --- | --- | --- |
37
- | `y` | `y` | `TY` | same as `x` | derived; see description | Quantized output tensor; shape is determined by kernel size, strides, and padding. | required |
38
 
39
  ## Attributes
40
 
@@ -43,8 +43,8 @@ Attributes and default values (overridable per request):
43
  | Attribute | Default | Description |
44
  | --- | --- | --- |
45
  | `auto_pad` | `"NOTSET"` | Automatic padding mode. `NOTSET` uses `pads`; `SAME_UPPER` and `SAME_LOWER` choose padding so each output spatial size is `ceil(input / stride)`; `VALID` uses no padding. |
46
- | `group` | `1` | Number of groups that input and output channels are split into; defaults to 1. |
47
  | `dilations` | — | Optional dilation factors, one positive integer per spatial axis. Omission means all ones. |
 
48
  | `kernel_shape` | — | Optional kernel shape, one positive integer per spatial axis. When present, it must match the spatial dimensions of the weight tensor; omission infers the shape from the weights. |
49
  | `pads` | — | Optional explicit padding in ONNX order `[begin_axis_0, ..., begin_axis_n, end_axis_0, ..., end_axis_n]`. Omission means all zeros; it cannot be combined with an automatic padding mode. |
50
  | `strides` | — | Optional stride factors, one positive integer per spatial axis. Omission means all ones. |
@@ -60,7 +60,7 @@ Attributes and default values (overridable per request):
60
 
61
  ## Files
62
 
63
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
64
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
65
  - [`test.json`](build/webgpu/test.json) — correctness cases
66
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -74,10 +74,14 @@ Attributes and default values (overridable per request):
74
 
75
  ## Use with `@huggingface/kernels`
76
 
77
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
78
- It then allocates the result tensors automatically.
 
 
 
79
 
80
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
81
 
82
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
83
 
 
12
 
13
  ## Description
14
 
15
+ Performs convolution on a quantized input using quantized weights, producing a quantized output. Input and output quantization use per-tensor scales and zero points; weight quantization may be per-tensor or per-output-channel; an optional int32 bias must be pre-quantized with `scale = x_scale * w_scale` and zero point 0.
16
 
17
  See the [ONNX `QLinearConv` spec](https://onnx.ai/onnx/operators/onnx__QLinearConv.html) for the reference semantics.
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `TX` | — | — | Quantized input data tensor shaped `(N x C x D1 x ... x Dn)`. | required |
24
+ | `x_scale` | `TF` | — | — | Per-tensor scale for input `x`. | required |
25
+ | `x_zero_point` | `TX` | — | — | Per-tensor zero point for input `x`. | required |
26
+ | `w` | `TW` | — | — | Quantized weight tensor shaped `(M x C/group x k1 x ... x kn)`. | required |
27
+ | `w_scale` | `TF` | — | — | Scale for weights `w`; scalar for per-tensor or 1-D of length M for per-output-channel quantization. | required |
28
+ | `w_zero_point` | `TW` | — | — | Zero point for weights `w`; scalar or 1-D of length M matching `w_scale`. | required |
29
+ | `y_scale` | `TF` | — | — | Per-tensor scale for output `y`. | required |
30
+ | `y_zero_point` | `TY` | — | — | Per-tensor zero point for output `y`. | required |
31
+ | `B` | `int32` | `1` | — | Optional 1-D bias of length `M`, pre-quantized with scale `x_scale * w_scale` and zero point 0. | optional |
32
 
33
  ## Outputs
34
 
35
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
36
+ | --- | --- | --- | --- | --- | --- |
37
+ | `y` | `TY` | same as `x` | derived | Quantized output tensor; shape is determined by kernel size, strides, and padding. | required |
38
 
39
  ## Attributes
40
 
 
43
  | Attribute | Default | Description |
44
  | --- | --- | --- |
45
  | `auto_pad` | `"NOTSET"` | Automatic padding mode. `NOTSET` uses `pads`; `SAME_UPPER` and `SAME_LOWER` choose padding so each output spatial size is `ceil(input / stride)`; `VALID` uses no padding. |
 
46
  | `dilations` | — | Optional dilation factors, one positive integer per spatial axis. Omission means all ones. |
47
+ | `group` | `1` | Number of groups that input and output channels are split into; defaults to 1. |
48
  | `kernel_shape` | — | Optional kernel shape, one positive integer per spatial axis. When present, it must match the spatial dimensions of the weight tensor; omission infers the shape from the weights. |
49
  | `pads` | — | Optional explicit padding in ONNX order `[begin_axis_0, ..., begin_axis_n, end_axis_0, ..., end_axis_n]`. Omission means all zeros; it cannot be combined with an automatic padding mode. |
50
  | `strides` | — | Optional stride factors, one positive integer per spatial axis. Omission means all ones. |
 
60
 
61
  ## Files
62
 
63
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
64
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
65
  - [`test.json`](build/webgpu/test.json) — correctness cases
66
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
74
 
75
  ## Use with `@huggingface/kernels`
76
 
77
+ ```sh
78
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
79
+ ```
80
+
81
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
82
 
83
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
84
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
85
 
86
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
87
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.QLinearConv",
3
  "tunableSpace": {
4
  "DP4A_DIRECT_SPATIAL_TILE": [8, 16],
5
  "DP4A_DIRECT_OC_TILE": [8, 16],
 
1
  {
 
2
  "tunableSpace": {
3
  "DP4A_DIRECT_SPATIAL_TILE": [8, 16],
4
  "DP4A_DIRECT_OC_TILE": [8, 16],
build/webgpu/conv-int-accumulate-spatial.wgsl.jinja CHANGED
@@ -1,37 +1,36 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
34
- {% set depthIndent = " " if source.spatialRank == 3 else "" %}
35
  {{ env.wgsl.resourceDeclarations }}
36
 
37
  fn read_x(index: u32) -> i32 {
@@ -76,13 +75,13 @@ fn read_w_zero({% if wZeroPerChannel %}oc: u32{% endif %}) -> i32 {
76
  }
77
 
78
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
79
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
80
  {{ flat_index_2d("index", guardInline=true) }}
81
  let ow = index % params.outW;
82
  var t = index / params.outW;
83
  let oh = t % params.outH;
84
  t = t / params.outH;
85
- {% if source.spatialRank == 3 %}
86
  let od = t % params.outD;
87
  t = t / params.outD;
88
  {% endif %}
@@ -94,28 +93,30 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
94
  var acc = 0i;
95
  for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
96
  let input_channel = group * params.inChannelsPerGroup + ic;
97
- {% if source.spatialRank == 3 %}
98
  for (var kd = 0u; kd < params.kernelD; kd = kd + 1u) {
99
  let id = i32(od * params.strideD + kd * params.dilationD) - params.padD;
100
  if (id < 0 || id >= i32(params.inD)) { continue; }
101
  {% endif %}
102
- {{ depthIndent }} for (var kh = 0u; kh < params.kernelH; kh = kh + 1u) {
103
- {{ depthIndent }} let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
104
- {{ depthIndent }} if (ih < 0 || ih >= i32(params.inH)) { continue; }
105
- {{ depthIndent }} for (var kw = 0u; kw < params.kernelW; kw = kw + 1u) {
106
- {{ depthIndent }} let iw = i32(ow * params.strideW + kw * params.dilationW) - params.padW;
107
- {{ depthIndent }} if (iw < 0 || iw >= i32(params.inW)) { continue; }
108
- {% if source.spatialRank == 3 %}
109
- {{ depthIndent }} let x_index = (((batch * params.inChannels + input_channel) * params.inD + u32(id)) * params.inH + u32(ih)) * params.inW + u32(iw);
110
- {{ depthIndent }} let w_index = (((oc * params.weightInChannels + ic) * params.kernelD + kd) * params.kernelH + kh) * params.kernelW + kw;
 
111
  {% else %}
112
- {{ depthIndent }} let x_index = ((batch * params.inChannels + input_channel) * params.inH + u32(ih)) * params.inW + u32(iw);
113
- {{ depthIndent }} let w_index = ((oc * params.weightInChannels + ic) * params.kernelH + kh) * params.kernelW + kw;
114
  {% endif %}
115
- {{ depthIndent }} acc = acc + (read_x(x_index) - xzp) * (read_w(w_index) - wzp);
116
- {{ depthIndent }} }
117
- {{ depthIndent }} }
118
- {% if source.spatialRank == 3 %}
 
119
  }
120
  {% endif %}
121
  }
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
 
34
  {{ env.wgsl.resourceDeclarations }}
35
 
36
  fn read_x(index: u32) -> i32 {
 
75
  }
76
 
77
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
78
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
79
  {{ flat_index_2d("index", guardInline=true) }}
80
  let ow = index % params.outW;
81
  var t = index / params.outW;
82
  let oh = t % params.outH;
83
  t = t / params.outH;
84
+ {% if spatialRankSpec == 3 %}
85
  let od = t % params.outD;
86
  t = t / params.outD;
87
  {% endif %}
 
93
  var acc = 0i;
94
  for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
95
  let input_channel = group * params.inChannelsPerGroup + ic;
96
+ {% if spatialRankSpec == 3 %}
97
  for (var kd = 0u; kd < params.kernelD; kd = kd + 1u) {
98
  let id = i32(od * params.strideD + kd * params.dilationD) - params.padD;
99
  if (id < 0 || id >= i32(params.inD)) { continue; }
100
  {% endif %}
101
+ {% filter indent(2 if spatialRankSpec == 3 else 0, true) %}
102
+ for (var kh = 0u; kh < params.kernelH; kh = kh + 1u) {
103
+ let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
104
+ if (ih < 0 || ih >= i32(params.inH)) { continue; }
105
+ for (var kw = 0u; kw < params.kernelW; kw = kw + 1u) {
106
+ let iw = i32(ow * params.strideW + kw * params.dilationW) - params.padW;
107
+ if (iw < 0 || iw >= i32(params.inW)) { continue; }
108
+ {% if spatialRankSpec == 3 %}
109
+ let x_index = (((batch * params.inChannels + input_channel) * params.inD + u32(id)) * params.inH + u32(ih)) * params.inW + u32(iw);
110
+ let w_index = (((oc * params.weightInChannels + ic) * params.kernelD + kd) * params.kernelH + kh) * params.kernelW + kw;
111
  {% else %}
112
+ let x_index = ((batch * params.inChannels + input_channel) * params.inH + u32(ih)) * params.inW + u32(iw);
113
+ let w_index = ((oc * params.weightInChannels + ic) * params.kernelH + kh) * params.kernelW + kw;
114
  {% endif %}
115
+ acc = acc + (read_x(x_index) - xzp) * (read_w(w_index) - wzp);
116
+ }
117
+ }
118
+ {% endfilter %}
119
+ {% if spatialRankSpec == 3 %}
120
  }
121
  {% endif %}
122
  }
build/webgpu/conv-int-im2col-spatial.wgsl.jinja CHANGED
@@ -5,27 +5,28 @@
5
  * Optional extra K rows contain the raw input zero point to align the DP4A
6
  * reduction. */
7
  {{ env.wgsl.resourceDeclarations }}
8
- {% if source.spatialRank == 3 %}
 
9
 
10
- const KERNEL_D: u32 = {{ source.kernelD }}u;
11
  {% endif %}
12
- const KERNEL_H: u32 = {{ source.kernelH }}u;
13
- const KERNEL_W: u32 = {{ source.kernelW }}u;
14
  const KERNEL_HW: u32 = KERNEL_H * KERNEL_W;
15
- {% if source.spatialRank == 3 %}
16
  const KSIZE: u32 = KERNEL_D * KERNEL_HW;
17
- const STRIDE_D: u32 = {{ source.strideD }}u;
18
- const DILATION_D: u32 = {{ source.dilationD }}u;
19
- const PAD_FRONT: i32 = {{ source.padFront }};
20
  {% else %}
21
  const KSIZE: u32 = KERNEL_HW;
22
  {% endif %}
23
- const STRIDE_H: u32 = {{ source.strideH }}u;
24
- const STRIDE_W: u32 = {{ source.strideW }}u;
25
- const DILATION_H: u32 = {{ source.dilationH }}u;
26
- const DILATION_W: u32 = {{ source.dilationW }}u;
27
- const PAD_TOP: i32 = {{ source.padTop }};
28
- const PAD_LEFT: i32 = {{ source.padLeft }};
29
 
30
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
31
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
@@ -35,7 +36,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
35
  }
36
  let k = gid.y;
37
  let batch = gid.z;
38
- {% if source.padK %}
39
  if (k >= params.sourceKRows) {
40
  cols[(batch * params.kRows + k) * params.outCount + pos] = x_zero_point[0];
41
  return;
@@ -43,7 +44,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
43
  {% endif %}
44
  let ic = k / KSIZE;
45
  let kq = k - ic * KSIZE;
46
- {% if source.spatialRank == 3 %}
47
  let kd = kq / KERNEL_HW;
48
  let khw = kq - kd * KERNEL_HW;
49
  let kh = khw / KERNEL_W;
@@ -63,7 +64,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
63
  let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
64
  let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
65
  var value: {{ bScalar }} = x_zero_point[0];
66
- {% if source.spatialRank == 3 %}
67
  if (
68
  id >= 0 && id < i32(params.inD)
69
  && ih >= 0 && ih < i32(params.inH)
 
5
  * Optional extra K rows contain the raw input zero point to align the DP4A
6
  * reduction. */
7
  {{ env.wgsl.resourceDeclarations }}
8
+ {% set padK = padK is defined and padK %}
9
+ {% if spatialRankSpec == 3 %}
10
 
11
+ const KERNEL_D: u32 = {{ kernelDSpec }}u;
12
  {% endif %}
13
+ const KERNEL_H: u32 = {{ kernelHSpec }}u;
14
+ const KERNEL_W: u32 = {{ kernelWSpec }}u;
15
  const KERNEL_HW: u32 = KERNEL_H * KERNEL_W;
16
+ {% if spatialRankSpec == 3 %}
17
  const KSIZE: u32 = KERNEL_D * KERNEL_HW;
18
+ const STRIDE_D: u32 = {{ strideDSpec }}u;
19
+ const DILATION_D: u32 = {{ dilationDSpec }}u;
20
+ const PAD_FRONT: i32 = {{ padFrontSpec }};
21
  {% else %}
22
  const KSIZE: u32 = KERNEL_HW;
23
  {% endif %}
24
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
25
+ const STRIDE_W: u32 = {{ strideWSpec }}u;
26
+ const DILATION_H: u32 = {{ dilationHSpec }}u;
27
+ const DILATION_W: u32 = {{ dilationWSpec }}u;
28
+ const PAD_TOP: i32 = {{ padTopSpec }};
29
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
30
 
31
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
32
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
 
36
  }
37
  let k = gid.y;
38
  let batch = gid.z;
39
+ {% if padK %}
40
  if (k >= params.sourceKRows) {
41
  cols[(batch * params.kRows + k) * params.outCount + pos] = x_zero_point[0];
42
  return;
 
44
  {% endif %}
45
  let ic = k / KSIZE;
46
  let kq = k - ic * KSIZE;
47
+ {% if spatialRankSpec == 3 %}
48
  let kd = kq / KERNEL_HW;
49
  let khw = kq - kd * KERNEL_HW;
50
  let kh = khw / KERNEL_W;
 
64
  let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
65
  let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
66
  var value: {{ bScalar }} = x_zero_point[0];
67
+ {% if spatialRankSpec == 3 %}
68
  if (
69
  id >= 0 && id < i32(params.inD)
70
  && ih >= 0 && ih < i32(params.inH)
build/webgpu/manifest.json CHANGED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json CHANGED
@@ -1,24 +1,46 @@
1
  {
2
  "name": "ai.onnx.QLinearConv",
3
- "id": "_ai_onnx_qlinearconv_webgpu_97f4388",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "7QcaEvhkU7vodLHPSkDnPEs+l3fyHE5M7iPreEs8EBs=",
11
- "conv-int-accumulate-spatial.wgsl.jinja": "+RLzIvFAtuFaDFjixU0amSlvCZ4sOlphzPpkf+7Klis=",
12
- "conv-int-im2col-spatial.wgsl.jinja": "fdcyh0E6YDzmlp/uWUnejeervSfidZul2GNQBrZUG+4=",
13
- "manifest.json": "GD1vHqnoBAsn2BJePTNWNRgtBJUcvPp6xTHgX0xEsLM=",
14
- "qlinear-conv-dp4a-direct-3x3.wgsl.jinja": "irlapLZkfGOdQZgLgddD34Djo8z+RSVCqUj0OrBH3Yw=",
15
- "qlinear-conv-nchw-x4.wgsl.jinja": "UZfNJ4L5YVesOPAv/9ugYz8tJOhujdafH/BtcnPjrKQ=",
16
- "qlinear-conv-requantize.wgsl.jinja": "w4e3cepxUD19iM8XgzpXUN7dyHCzzrxfrWjzrOaPps4=",
17
- "quant-dp4a-matmul.wgsl.jinja": "yzJ3cN0cvihhQ8P3joswjg12M0p4ZrLQgfxXq9epORo=",
18
- "quant-pad-k-matrices.wgsl.jinja": "UNPxeSYR99nYYGs6qmsHnO3/uHcDkdQB4jBZwZw2zmw=",
19
- "test.json": "Q1nMc5F1AYDSVoAvj5w8rpbPrOw+6hZhLvwo5wV9yJ4="
20
  }
21
  },
22
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
23
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.QLinearConv" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
 
1
  {
2
  "name": "ai.onnx.QLinearConv",
3
+ "id": "_ai_onnx_qlinearconv_webgpu_5dc8940",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "spUQwS0Tr+3Vdfkxd9WepuT6g6ezyeV6aMao4r7OsdY=",
11
+ "conv-int-accumulate-spatial.wgsl.jinja": "TkacF0/fJ1yShdyPJWePK/+8XJFaV+ZDKWT6XvHOCI8=",
12
+ "conv-int-im2col-spatial.wgsl.jinja": "3vFeBqBVtyYtxV1yteSQ3O7skFur08E3vDoVtdwcYAo=",
13
+ "manifest.json": "VInAZzgFiEXRlHexx+MCXfvzy6hnwIGSsbdY6pw6Nas=",
14
+ "qlinear-conv-dp4a-direct-3x3.wgsl.jinja": "xt3DqIwhlzomIiLMuMZ+8F0+Xqg7AWfGCbeZnNT2rLU=",
15
+ "qlinear-conv-nchw-x4.wgsl.jinja": "XT1MIv1iIEOhj0keQUkU5cau6Nc/CTJCAgu+PardWaQ=",
16
+ "qlinear-conv-requantize.wgsl.jinja": "JCGp/e1wWm22EQlsYPNreGtdualHITd5dmvA9kGYZCk=",
17
+ "quant-dp4a-matmul.wgsl.jinja": "x1yY2y4Wb6vlzMVeRw+DbEmfvJdPBHkMF8ATG34ZGis=",
18
+ "quant-pad-k-matrices.wgsl.jinja": "hsLWgSTbpXBnsIOyrIzsmhQzVyIYALaFvx62VN46KyY=",
19
+ "test.json": "z2krhaHYtSozWBIW2J1ZKnlpd1S7dDdTQwYq83sf/7g="
20
  }
21
  },
22
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
23
+ "webgpu": {
24
+ "manifestSpec": "2.0",
25
+ "variants": {
26
+ "dp4a_pointwise_1x1_padded_k_baseline": ["quant-dp4a-matmul.wgsl.jinja", "quant-pad-k-matrices.wgsl.jinja"],
27
+ "dp4a_im2col_nchw_padded_k_baseline": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja", "quant-pad-k-matrices.wgsl.jinja"],
28
+ "dp4a_direct_3x3_nchw_u8s8": ["qlinear-conv-dp4a-direct-3x3.wgsl.jinja"],
29
+ "dp4a_pointwise_1x1": ["quant-dp4a-matmul.wgsl.jinja"],
30
+ "nchw2d_feature_independent_x4": ["qlinear-conv-nchw-x4.wgsl.jinja"],
31
+ "ncdhw3d_per_channel_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
32
+ "ncw1d_scalar_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
33
+ "ncdhw3d_scalar_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
34
+ "nchw2d_scalar_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
35
+ "ncw1d_scalar_quantization_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
36
+ "ncdhw3d_scalar_quantization_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
37
+ "ncw1d_per_channel_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
38
+ "dp4a_im2col_ncdhw3d_scalar_quantization": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja"],
39
+ "dp4a_im2col_nchw_bias_quantization": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja"],
40
+ "dp4a_im2col_nchw_scalar_quantization": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja"],
41
+ "nchw2d_per_channel_quantization": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
42
+ "nchw2d_per_channel_quantization_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
43
+ "nchw2d_scalar_quantization_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"]
44
+ }
45
+ }
46
  }
build/webgpu/qlinear-conv-dp4a-direct-3x3.wgsl.jinja CHANGED
@@ -13,17 +13,17 @@ fn dot4_packed(a_word: u32, b_word: u32) -> i32 {
13
  }
14
 
15
 
16
- const IN_C: u32 = {{ source.inChannels }}u;
17
- const IN_H: u32 = {{ source.inHeight }}u;
18
- const IN_W: u32 = {{ source.inWidth }}u;
19
- const OUT_C: u32 = {{ source.outChannels }}u;
20
- const OUT_H: u32 = {{ source.outHeight }}u;
21
- const OUT_W: u32 = {{ source.outWidth }}u;
22
- const BATCH: u32 = {{ source.batch }}u;
23
-
24
- const SPATIAL_TILE: u32 = {{ source.spatialTile }}u;
25
- const OC_TILE: u32 = {{ source.ocTile }}u;
26
- const IC_TILE: u32 = {{ source.icTile }}u;
27
  const IC_WORDS: u32 = IC_TILE / 4u;
28
  const PATCH_EDGE: u32 = SPATIAL_TILE + 2u;
29
  const PATCH_AREA: u32 = PATCH_EDGE * PATCH_EDGE;
@@ -61,24 +61,24 @@ fn sum4_packed(v: u32) -> i32 {
61
  + signed_byte(v, 16u) + signed_byte(v, 24u);
62
  }
63
 
64
- fn read_w_zero({% if source.wZeroPerChannel %}oc: u32{% endif %}) -> i32 {
65
- {% if source.wZeroPerChannel %}
66
  return w_zero_point[oc];
67
  {% else %}
68
  return w_zero_point[0];
69
  {% endif %}
70
  }
71
 
72
- fn read_w_scale({% if source.wScalePerChannel %}oc: u32{% endif %}) -> f32 {
73
- {% if source.wScalePerChannel %}
74
  return w_scale[oc];
75
  {% else %}
76
  return w_scale[0];
77
  {% endif %}
78
  }
79
 
80
- fn requantize(acc: i32{% if source.wScalePerChannel %}, oc: u32{% endif %}) -> u32 {
81
- let scaled = f32(acc) * x_scale[0] * read_w_scale({% if source.wScalePerChannel %}oc{% endif %}) / y_scale[0];
82
  let q = clamp(
83
  i32(clamp(round(scaled), -2.0e9, 2.0e9)) + i32(y_zero_point[0]),
84
  0,
@@ -87,7 +87,7 @@ fn requantize(acc: i32{% if source.wScalePerChannel %}, oc: u32{% endif %}) -> u
87
  return u32(q);
88
  }
89
 
90
- @compute @workgroup_size({{ source.spatialTile }}, {{ source.spatialTile }}, 1)
91
  fn main(
92
  @builtin(workgroup_id) wg: vec3<u32>,
93
  @builtin(local_invocation_id) lid: vec3<u32>
@@ -107,7 +107,7 @@ fn main(
107
  }
108
  workgroupBarrier();
109
 
110
- {% for oc in range(source.ocTile) %}
111
  var acc{{ oc }} = 0i;
112
  {% endfor %}
113
  var sumX = 0i;
@@ -167,7 +167,7 @@ fn main(
167
  let tap = kh * 3u + kw;
168
  let xp = xTile[channelWord * PATCH_AREA + (lid.y + kh) * PATCH_EDGE + lid.x + kw];
169
  sumX += sum4_packed(xp);
170
- {% for oc in range(source.ocTile) %}
171
  acc{{ oc }} += dot4_packed(wTile[({{ oc }}u * IC_WORDS + channelWord) * 9u + tap], xp);
172
  {% endfor %}
173
  }
@@ -181,17 +181,17 @@ fn main(
181
  return;
182
  }
183
 
184
- {% for oc in range(source.ocTile) %}
185
  {
186
  let outOc = ocBase + {{ oc }}u;
187
  if (outOc < OUT_C) {
188
- let ca = -read_w_zero({% if source.wZeroPerChannel %}outOc{% endif %});
189
  let corrected = acc{{ oc }}
190
  + cb * atomicLoad(&wSums[{{ oc }}u])
191
  + ca * sumX
192
  + K * ca * cb;
193
  let outIndex = ((wg.z * OUT_C + outOc) * OUT_H + oh) * OUT_W + ow;
194
- y[outIndex] = requantize(corrected{% if source.wScalePerChannel %}, outOc{% endif %});
195
  }
196
  }
197
  {% endfor %}
 
13
  }
14
 
15
 
16
+ const IN_C: u32 = {{ inChannels }}u;
17
+ const IN_H: u32 = {{ inHeight }}u;
18
+ const IN_W: u32 = {{ inWidth }}u;
19
+ const OUT_C: u32 = {{ outChannels }}u;
20
+ const OUT_H: u32 = {{ outHeight }}u;
21
+ const OUT_W: u32 = {{ outWidth }}u;
22
+ const BATCH: u32 = {{ batch }}u;
23
+
24
+ const SPATIAL_TILE: u32 = {{ spatialTile }}u;
25
+ const OC_TILE: u32 = {{ ocTile }}u;
26
+ const IC_TILE: u32 = {{ icTile }}u;
27
  const IC_WORDS: u32 = IC_TILE / 4u;
28
  const PATCH_EDGE: u32 = SPATIAL_TILE + 2u;
29
  const PATCH_AREA: u32 = PATCH_EDGE * PATCH_EDGE;
 
61
  + signed_byte(v, 16u) + signed_byte(v, 24u);
62
  }
63
 
64
+ fn read_w_zero({% if wZeroPerChannel %}oc: u32{% endif %}) -> i32 {
65
+ {% if wZeroPerChannel %}
66
  return w_zero_point[oc];
67
  {% else %}
68
  return w_zero_point[0];
69
  {% endif %}
70
  }
71
 
72
+ fn read_w_scale({% if wScalePerChannel %}oc: u32{% endif %}) -> f32 {
73
+ {% if wScalePerChannel %}
74
  return w_scale[oc];
75
  {% else %}
76
  return w_scale[0];
77
  {% endif %}
78
  }
79
 
80
+ fn requantize(acc: i32{% if wScalePerChannel %}, oc: u32{% endif %}) -> u32 {
81
+ let scaled = f32(acc) * x_scale[0] * read_w_scale({% if wScalePerChannel %}oc{% endif %}) / y_scale[0];
82
  let q = clamp(
83
  i32(clamp(round(scaled), -2.0e9, 2.0e9)) + i32(y_zero_point[0]),
84
  0,
 
87
  return u32(q);
88
  }
89
 
90
+ @compute @workgroup_size({{ spatialTile }}, {{ spatialTile }}, 1)
91
  fn main(
92
  @builtin(workgroup_id) wg: vec3<u32>,
93
  @builtin(local_invocation_id) lid: vec3<u32>
 
107
  }
108
  workgroupBarrier();
109
 
110
+ {% for oc in range(ocTile) %}
111
  var acc{{ oc }} = 0i;
112
  {% endfor %}
113
  var sumX = 0i;
 
167
  let tap = kh * 3u + kw;
168
  let xp = xTile[channelWord * PATCH_AREA + (lid.y + kh) * PATCH_EDGE + lid.x + kw];
169
  sumX += sum4_packed(xp);
170
+ {% for oc in range(ocTile) %}
171
  acc{{ oc }} += dot4_packed(wTile[({{ oc }}u * IC_WORDS + channelWord) * 9u + tap], xp);
172
  {% endfor %}
173
  }
 
181
  return;
182
  }
183
 
184
+ {% for oc in range(ocTile) %}
185
  {
186
  let outOc = ocBase + {{ oc }}u;
187
  if (outOc < OUT_C) {
188
+ let ca = -read_w_zero({% if wZeroPerChannel %}outOc{% endif %});
189
  let corrected = acc{{ oc }}
190
  + cb * atomicLoad(&wSums[{{ oc }}u])
191
  + ca * sumX
192
  + K * ca * cb;
193
  let outIndex = ((wg.z * OUT_C + outOc) * OUT_H + oh) * OUT_W + ow;
194
+ y[outIndex] = requantize(corrected{% if wScalePerChannel %}, outOc{% endif %});
195
  }
196
  }
197
  {% endfor %}
build/webgpu/qlinear-conv-nchw-x4.wgsl.jinja CHANGED
@@ -1,30 +1,30 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
@@ -97,7 +97,7 @@ fn write_y(i: u32, value: i32) {
97
  }
98
 
99
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
100
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
101
  {{ flat_index_2d("flat", "", note="") }}
102
  let groups_per_row = (params.outW + 3u) / 4u;
103
  let oc_blocks = (params.outChannels + {{ ocTile - 1 }}u) / {{ ocTile }}u;
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
 
97
  }
98
 
99
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
100
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
101
  {{ flat_index_2d("flat", "", note="") }}
102
  let groups_per_row = (params.outW + 3u) / 4u;
103
  let oc_blocks = (params.outChannels + {{ ocTile - 1 }}u) / {{ ocTile }}u;
build/webgpu/qlinear-conv-requantize.wgsl.jinja CHANGED
@@ -1,30 +1,30 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
@@ -50,7 +50,7 @@ fn write_y(index: u32, value: i32) {
50
  }
51
 
52
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
53
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
54
  {{ flat_index_2d(guardInline=true) }}
55
  {% if hasBias or wScalePerChannel %}
56
  let oc = (i / params.outImageSize) % params.outChannels;
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
 
50
  }
51
 
52
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
53
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
54
  {{ flat_index_2d(guardInline=true) }}
55
  {% if hasBias or wScalePerChannel %}
56
  let oc = (i / params.outImageSize) % params.outChannels;
build/webgpu/quant-dp4a-matmul.wgsl.jinja CHANGED
@@ -1,24 +1,36 @@
1
- {% if requant != "none" and scaleScalar == "f16" %}
2
- enable f16;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
 
6
  const TILE_M: u32 = {{ tileM }}u;
7
  const TILE_N: u32 = {{ tileN }}u;
8
  const KT_WORDS: u32 = {{ ktWords }}u;
9
  const A_TILE_WORDS: u32 = TILE_M * KT_WORDS;
10
- {% set GEMV_UNROLL = gemvKUnroll if gemvKUnroll is defined else 4 %}
11
- {% if tileM == 1 %}
12
- const GEMV_K_UNROLL: u32 = {{ GEMV_UNROLL }}u;
13
- {% endif %}
14
 
15
- {% set needsASum = hasBZero or bUnsigned %}
16
- {% set needsBSum = hasAZero or aUnsigned %}
17
  var<workgroup> a_tile: array<u32, A_TILE_WORDS>;
18
- {% if needsASum %}
19
  var<workgroup> a_row_sum: array<atomic<i32>, TILE_M>;
20
 
21
- {% endif %}
22
  // Pack 4 widened A values into one u32 (byte 0 = lowest k).
23
  fn pack_a(v: vec4<{{ aScalar }}>) -> u32 {
24
  {% if aUnsigned %}
@@ -28,7 +40,6 @@ fn pack_a(v: vec4<{{ aScalar }}>) -> u32 {
28
  return (u32(v.x) & 0xFFu) | ((u32(v.y) & 0xFFu) << 8u) | ((u32(v.z) & 0xFFu) << 16u) | ((u32(v.w) & 0xFFu) << 24u);
29
  {% endif %}
30
  }
31
- {% if needsASum %}
32
 
33
  // Sum of the 4 bias-shifted A values of one packed word.
34
  fn sum4_a(v: vec4<{{ aScalar }}>) -> i32 {
@@ -39,7 +50,6 @@ fn sum4_a(v: vec4<{{ aScalar }}>) -> i32 {
39
  {% endif %}
40
  }
41
 
42
- {% endif %}
43
  fn pack_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> u32 {
44
  {% if bUnsigned %}
45
  return ((b0 ^ 0x80u) & 0xFFu) | (((b1 ^ 0x80u) & 0xFFu) << 8u) | (((b2 ^ 0x80u) & 0xFFu) << 16u) | (((b3 ^ 0x80u) & 0xFFu) << 24u);
@@ -54,7 +64,6 @@ fn dot4_packed(a_word: u32, b_word: u32) -> i32 {
54
  return dot4I8Packed(a_word, b_word);
55
  }
56
 
57
- {% if needsBSum %}
58
 
59
  fn sum4_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> i32 {
60
  {% if bUnsigned %}
@@ -64,8 +73,6 @@ fn sum4_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScala
64
  {% endif %}
65
  }
66
 
67
- {% endif %}
68
- {% if hasAZero %}
69
  fn read_a_zero({% if aZeroPerRow %}row: u32{% endif %}) -> i32 {
70
  {% if aZeroPerRow %}
71
  {% if aUnsigned %}
@@ -82,25 +89,14 @@ fn read_a_zero({% if aZeroPerRow %}row: u32{% endif %}) -> i32 {
82
  {% endif %}
83
  }
84
 
85
- {% endif %}
86
- {% if hasBZero %}
87
- fn read_b_zero({% if bZeroPerColumn %}col: u32{% endif %}) -> i32 {
88
- {% if bZeroPerColumn %}
89
- {% if bUnsigned %}
90
- return i32(b_zero_point[col]);
91
- {% else %}
92
- return b_zero_point[col];
93
- {% endif %}
94
- {% else %}
95
  {% if bUnsigned %}
96
  return i32(b_zero_point[0]);
97
  {% else %}
98
  return b_zero_point[0];
99
  {% endif %}
100
- {% endif %}
101
  }
102
 
103
- {% endif %}
104
  {% if requant != "none" %}
105
  fn read_y_zero() -> i32 {
106
  {% if yUnsigned %}
@@ -131,31 +127,15 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
131
  let k_words_total = params.K / 4u;
132
  {% endif %}
133
 
134
- {% if needsASum %}
135
  if (tid < TILE_M) {
136
  atomicStore(&a_row_sum[tid], 0);
137
  }
138
  workgroupBarrier();
139
 
140
- {% endif %}
141
- {% if tileM == 1 %}
142
- var r0: i32 = 0;
143
- {% for u in range(1, GEMV_UNROLL) %}
144
- var r0_{{ u }}: i32 = 0;
145
- {% endfor %}
146
- {% else %}
147
  {% for m in range(tileM) %}
148
  var r{{ m }}: i32 = 0;
149
  {% endfor %}
150
- {% endif %}
151
- {% if needsBSum %}
152
  var sum_b: i32 = 0;
153
- {% if tileM == 1 %}
154
- {% for u in range(1, GEMV_UNROLL) %}
155
- var sum_b_{{ u }}: i32 = 0;
156
- {% endfor %}
157
- {% endif %}
158
- {% endif %}
159
 
160
  for (var kw0 = 0u; kw0 < k_words_total; kw0 = kw0 + KT_WORDS) {
161
  let words = min(KT_WORDS, k_words_total - kw0);
@@ -179,49 +159,13 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
179
  let v = a[a_base4 + src_m * k_words_total + kw0 + ki];
180
  {% endif %}
181
  packed = pack_a(v);
182
- {% if needsASum %}
183
  atomicAdd(&a_row_sum[mi], sum4_a(v));
184
- {% endif %}
185
  }
186
  a_tile[mi * KT_WORDS + ki] = packed;
187
  }
188
  workgroupBarrier();
189
 
190
  if (n_valid) {
191
- {% if tileM == 1 %}
192
- // M=1 has only one output accumulator, so a scalar K loop forms a long
193
- // dependent DP4A chain. Interleave several words while preserving the
194
- // coalesced mapping of neighboring threads to neighboring B columns.
195
- let unrolled_words = words - words % GEMV_K_UNROLL;
196
- for (var kk = 0u; kk < unrolled_words; kk += GEMV_K_UNROLL) {
197
- {% for u in range(GEMV_UNROLL) %}
198
- let k4_{{ u }} = (kw0 + kk + {{ u }}u) * 4u;
199
- let b_row_{{ u }} = b_base + k4_{{ u }} * params.N + col;
200
- let b0_{{ u }} = b[b_row_{{ u }}];
201
- let b1_{{ u }} = b[b_row_{{ u }} + params.N];
202
- let b2_{{ u }} = b[b_row_{{ u }} + 2u * params.N];
203
- let b3_{{ u }} = b[b_row_{{ u }} + 3u * params.N];
204
- {% if needsBSum %}
205
- sum_b{{ "" if u == 0 else "_" ~ u }} += sum4_b(b0_{{ u }}, b1_{{ u }}, b2_{{ u }}, b3_{{ u }});
206
- {% endif %}
207
- let bp_{{ u }} = pack_b(b0_{{ u }}, b1_{{ u }}, b2_{{ u }}, b3_{{ u }});
208
- r0{{ "" if u == 0 else "_" ~ u }} += dot4_packed(a_tile[kk + {{ u }}u], bp_{{ u }});
209
- {% endfor %}
210
- }
211
- for (var kk = unrolled_words; kk < words; kk++) {
212
- let k4 = (kw0 + kk) * 4u;
213
- let b_row = b_base + k4 * params.N + col;
214
- let b0 = b[b_row];
215
- let b1 = b[b_row + params.N];
216
- let b2 = b[b_row + 2u * params.N];
217
- let b3 = b[b_row + 3u * params.N];
218
- {% if needsBSum %}
219
- sum_b += sum4_b(b0, b1, b2, b3);
220
- {% endif %}
221
- let bp = pack_b(b0, b1, b2, b3);
222
- r0 += dot4_packed(a_tile[kk], bp);
223
- }
224
- {% else %}
225
  for (var kk = 0u; kk < words; kk = kk + 1u) {
226
  let k4 = (kw0 + kk) * 4u;
227
  let b_row = b_base + k4 * params.N + col;
@@ -229,15 +173,12 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
229
  let b1 = b[b_row + params.N];
230
  let b2 = b[b_row + 2u * params.N];
231
  let b3 = b[b_row + 3u * params.N];
232
- {% if needsBSum %}
233
  sum_b = sum_b + sum4_b(b0, b1, b2, b3);
234
- {% endif %}
235
  let bp = pack_b(b0, b1, b2, b3);
236
  {% for m in range(tileM) %}
237
  r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS + kk], bp);
238
  {% endfor %}
239
  }
240
- {% endif %}
241
  }
242
  workgroupBarrier();
243
  }
@@ -262,9 +203,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
262
  let a3: {{ aScalar }} = {{ "128u" if aUnsigned else "0" }};
263
  let v = vec4<{{ aScalar }}>(a0, a1, a2, a3);
264
  packed = pack_a(v);
265
- {% if needsASum %}
266
  atomicAdd(&a_row_sum[tid], sum4_a(v));
267
- {% endif %}
268
  }
269
  a_tile[tid * KT_WORDS] = packed;
270
  }
@@ -279,9 +218,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
279
  if (tail_lanes > 1u) { b1 = b[b_row + params.N]; }
280
  if (tail_lanes > 2u) { b2 = b[b_row + 2u * params.N]; }
281
  let b3: {{ bScalar }} = {{ "128u" if bUnsigned else "0" }};
282
- {% if needsBSum %}
283
  sum_b = sum_b + sum4_b(b0, b1, b2, b3);
284
- {% endif %}
285
  let bp = pack_b(b0, b1, b2, b3);
286
  {% for m in range(tileM) %}
287
  r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS], bp);
@@ -296,14 +233,11 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
296
  }
297
 
298
  // Fold bias + zero-point corrections (exact in modular i32 arithmetic).
299
- let cb: i32 = {{ "128" if bUnsigned else "0" }}{% if hasBZero %} - read_b_zero({% if bZeroPerColumn %}col{% endif %}){% endif %};
300
  {% if not aZeroPerRow %}
301
- let ca: i32 = {{ "128" if aUnsigned else "0" }}{% if hasAZero %} - read_a_zero(){% endif %};
302
  let kcc: i32 = i32(params.K) * ca * cb;
303
  {% endif %}
304
- {% if needsBSum and tileM == 1 %}
305
- let sum_b_total = sum_b{% for u in range(1, GEMV_UNROLL) %} + sum_b_{{ u }}{% endfor %};
306
- {% endif %}
307
 
308
  {% for m in range(tileM) %}
309
  {
@@ -313,13 +247,9 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
313
  let ca: i32 = {{ "128" if aUnsigned else "0" }} - read_a_zero(mr);
314
  let kcc: i32 = i32(params.K) * ca * cb;
315
  {% endif %}
316
- var acc = r{{ m }}{% if tileM == 1 %}{% for u in range(1, GEMV_UNROLL) %} + r0_{{ u }}{% endfor %}{% endif %};
317
- {% if needsASum %}
318
  acc = acc + cb * atomicLoad(&a_row_sum[{{ m }}u]);
319
- {% endif %}
320
- {% if needsBSum %}
321
- acc = acc + ca * {% if tileM == 1 %}sum_b_total{% else %}sum_b{% endif %};
322
- {% endif %}
323
  acc = acc + kcc;
324
  {% if hasBias %}
325
  acc = acc + bias[mr];
@@ -327,7 +257,8 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
327
  {% if requant == "none" %}
328
  y[y_base + mr * params.N + col] = acc;
329
  {% else %}
330
- let scaled = f32(acc) * f32(a_scale[0]) * f32(b_scale[{% if requant == "per_column" %}col{% elif requant == "per_row" %}mr{% else %}0u{% endif %}]) / f32(y_scale[0]);
 
331
  // Clamp the rounded float into an i32-representable range before the cast
332
  // so a huge-finite / Inf requantized value saturates (the final clamp pins
333
  // it to [qMin,qMax]) instead of invoking undefined i32(huge_float).
 
1
+ {% set yUnsigned = yUnsigned is defined and yUnsigned %}
2
+ {% set qMin = qMin | default(0 if yUnsigned else 0 - 128) %}
3
+ {% set qMax = qMax | default(255 if yUnsigned else 127) %}
4
+ {% if not env.device.wgslLanguageFeatures.has("packed_4x8_integer_dot_product") %}
5
+ // Runtime source-compatible fallbacks for the packed 4x8 integer built-ins used
6
+ // by quantized kernels. Native implementations remain unshadowed when the
7
+ // language feature is present. The user functions retain the built-ins'
8
+ // @must_use contract, but cannot be @const; every call is runtime.
9
+ // Inputs are function parameters, so replacing a built-in call with one of
10
+ // these same-name functions preserves WGSL's single evaluation of each
11
+ // argument. Byte extraction is unsigned. Signed lanes use the exact
12
+ // i32(byte ^ 0x80u) - 128i sign extension, avoiding signed-left-shift overflow
13
+ // while preserving every 8-bit two's-complement value.
14
+ @must_use fn dot4I8Packed(e1: u32, e2: u32) -> i32 {
15
+ let shifts = vec4<u32>(0u, 8u, 16u, 24u);
16
+ let a_bytes = (vec4<u32>(e1) >> shifts) & vec4<u32>(0xffu);
17
+ let b_bytes = (vec4<u32>(e2) >> shifts) & vec4<u32>(0xffu);
18
+ let a = vec4<i32>(a_bytes ^ vec4<u32>(0x80u)) - vec4<i32>(128i);
19
+ let b = vec4<i32>(b_bytes ^ vec4<u32>(0x80u)) - vec4<i32>(128i);
20
+ return dot(a, b);
21
+ }
22
  {% endif %}
23
  {{ env.wgsl.resourceDeclarations }}
24
 
25
+
26
  const TILE_M: u32 = {{ tileM }}u;
27
  const TILE_N: u32 = {{ tileN }}u;
28
  const KT_WORDS: u32 = {{ ktWords }}u;
29
  const A_TILE_WORDS: u32 = TILE_M * KT_WORDS;
 
 
 
 
30
 
 
 
31
  var<workgroup> a_tile: array<u32, A_TILE_WORDS>;
 
32
  var<workgroup> a_row_sum: array<atomic<i32>, TILE_M>;
33
 
 
34
  // Pack 4 widened A values into one u32 (byte 0 = lowest k).
35
  fn pack_a(v: vec4<{{ aScalar }}>) -> u32 {
36
  {% if aUnsigned %}
 
40
  return (u32(v.x) & 0xFFu) | ((u32(v.y) & 0xFFu) << 8u) | ((u32(v.z) & 0xFFu) << 16u) | ((u32(v.w) & 0xFFu) << 24u);
41
  {% endif %}
42
  }
 
43
 
44
  // Sum of the 4 bias-shifted A values of one packed word.
45
  fn sum4_a(v: vec4<{{ aScalar }}>) -> i32 {
 
50
  {% endif %}
51
  }
52
 
 
53
  fn pack_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> u32 {
54
  {% if bUnsigned %}
55
  return ((b0 ^ 0x80u) & 0xFFu) | (((b1 ^ 0x80u) & 0xFFu) << 8u) | (((b2 ^ 0x80u) & 0xFFu) << 16u) | (((b3 ^ 0x80u) & 0xFFu) << 24u);
 
64
  return dot4I8Packed(a_word, b_word);
65
  }
66
 
 
67
 
68
  fn sum4_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> i32 {
69
  {% if bUnsigned %}
 
73
  {% endif %}
74
  }
75
 
 
 
76
  fn read_a_zero({% if aZeroPerRow %}row: u32{% endif %}) -> i32 {
77
  {% if aZeroPerRow %}
78
  {% if aUnsigned %}
 
89
  {% endif %}
90
  }
91
 
92
+ fn read_b_zero() -> i32 {
 
 
 
 
 
 
 
 
 
93
  {% if bUnsigned %}
94
  return i32(b_zero_point[0]);
95
  {% else %}
96
  return b_zero_point[0];
97
  {% endif %}
 
98
  }
99
 
 
100
  {% if requant != "none" %}
101
  fn read_y_zero() -> i32 {
102
  {% if yUnsigned %}
 
127
  let k_words_total = params.K / 4u;
128
  {% endif %}
129
 
 
130
  if (tid < TILE_M) {
131
  atomicStore(&a_row_sum[tid], 0);
132
  }
133
  workgroupBarrier();
134
 
 
 
 
 
 
 
 
135
  {% for m in range(tileM) %}
136
  var r{{ m }}: i32 = 0;
137
  {% endfor %}
 
 
138
  var sum_b: i32 = 0;
 
 
 
 
 
 
139
 
140
  for (var kw0 = 0u; kw0 < k_words_total; kw0 = kw0 + KT_WORDS) {
141
  let words = min(KT_WORDS, k_words_total - kw0);
 
159
  let v = a[a_base4 + src_m * k_words_total + kw0 + ki];
160
  {% endif %}
161
  packed = pack_a(v);
 
162
  atomicAdd(&a_row_sum[mi], sum4_a(v));
 
163
  }
164
  a_tile[mi * KT_WORDS + ki] = packed;
165
  }
166
  workgroupBarrier();
167
 
168
  if (n_valid) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  for (var kk = 0u; kk < words; kk = kk + 1u) {
170
  let k4 = (kw0 + kk) * 4u;
171
  let b_row = b_base + k4 * params.N + col;
 
173
  let b1 = b[b_row + params.N];
174
  let b2 = b[b_row + 2u * params.N];
175
  let b3 = b[b_row + 3u * params.N];
 
176
  sum_b = sum_b + sum4_b(b0, b1, b2, b3);
 
177
  let bp = pack_b(b0, b1, b2, b3);
178
  {% for m in range(tileM) %}
179
  r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS + kk], bp);
180
  {% endfor %}
181
  }
 
182
  }
183
  workgroupBarrier();
184
  }
 
203
  let a3: {{ aScalar }} = {{ "128u" if aUnsigned else "0" }};
204
  let v = vec4<{{ aScalar }}>(a0, a1, a2, a3);
205
  packed = pack_a(v);
 
206
  atomicAdd(&a_row_sum[tid], sum4_a(v));
 
207
  }
208
  a_tile[tid * KT_WORDS] = packed;
209
  }
 
218
  if (tail_lanes > 1u) { b1 = b[b_row + params.N]; }
219
  if (tail_lanes > 2u) { b2 = b[b_row + 2u * params.N]; }
220
  let b3: {{ bScalar }} = {{ "128u" if bUnsigned else "0" }};
 
221
  sum_b = sum_b + sum4_b(b0, b1, b2, b3);
 
222
  let bp = pack_b(b0, b1, b2, b3);
223
  {% for m in range(tileM) %}
224
  r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS], bp);
 
233
  }
234
 
235
  // Fold bias + zero-point corrections (exact in modular i32 arithmetic).
236
+ let cb: i32 = {{ "128" if bUnsigned else "0" }} - read_b_zero();
237
  {% if not aZeroPerRow %}
238
+ let ca: i32 = {{ "128" if aUnsigned else "0" }} - read_a_zero();
239
  let kcc: i32 = i32(params.K) * ca * cb;
240
  {% endif %}
 
 
 
241
 
242
  {% for m in range(tileM) %}
243
  {
 
247
  let ca: i32 = {{ "128" if aUnsigned else "0" }} - read_a_zero(mr);
248
  let kcc: i32 = i32(params.K) * ca * cb;
249
  {% endif %}
250
+ var acc = r{{ m }};
 
251
  acc = acc + cb * atomicLoad(&a_row_sum[{{ m }}u]);
252
+ acc = acc + ca * sum_b;
 
 
 
253
  acc = acc + kcc;
254
  {% if hasBias %}
255
  acc = acc + bias[mr];
 
257
  {% if requant == "none" %}
258
  y[y_base + mr * params.N + col] = acc;
259
  {% else %}
260
+ {% set scale_index = "col" if requant == "per_column" else ("mr" if requant == "per_row" else "0u") %}
261
+ let scaled = f32(acc) * f32(a_scale[0]) * f32(b_scale[{{ scale_index }}]) / f32(y_scale[0]);
262
  // Clamp the rounded float into an i32-representable range before the cast
263
  // so a huge-finite / Inf requantized value saturates (the final clamp pins
264
  // it to [qMin,qMax]) instead of invoking undefined i32(huge_float).
build/webgpu/quant-pad-k-matrices.wgsl.jinja CHANGED
@@ -16,9 +16,8 @@ fn read_a_zero({% if aZeroPerRow %}row: u32{% endif %}) -> i32 {
16
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
17
  fn main(
18
  @builtin(global_invocation_id) gid: vec3<u32>,
19
- @builtin(num_workgroups) nwg: vec3<u32>,
20
  ) {
21
- let flat = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
22
 
23
  let a_count = params.M * params.KPadded;
24
  if (flat < a_count) {
 
16
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
17
  fn main(
18
  @builtin(global_invocation_id) gid: vec3<u32>,
 
19
  ) {
20
+ let flat = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
21
 
22
  let a_count = params.M * params.KPadded;
23
  if (flat < a_count) {
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.QLinearConv",
3
  "fixtureArrays": {
4
  "same_upper_stride2_autopad_input_x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
5
  "backend_qlinearconv_pointwise_u8_input_x": [255, 174, 162, 25, 203, 168, 58, 15, 59, 237, 95, 129, 0, 64, 56, 242, 153, 221, 168, 12, 166, 232, 178, 186, 195, 237, 162, 237, 188, 39, 124, 77, 80, 102, 43, 127, 230, 21, 83, 41, 40, 134, 255, 154, 92, 141, 42, 148, 247],
@@ -83,7 +82,7 @@
83
  "x": {
84
  "dtype": "int8",
85
  "shape": [1, 1, 3, 3],
86
- "data": { "kind": "values", "values": [-4, -3, -2, -1, 0, 1, 2, 3, 4] }
87
  },
88
  "x_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.5] } },
89
  "x_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [0] } },
@@ -94,7 +93,10 @@
94
  "y_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [-3] } }
95
  },
96
  "outputs": { "y": { "dtype": "int8", "shape": [1, 1, 2, 2] } },
97
- "attrs": {}
 
 
 
98
  },
99
  {
100
  "name": "int8_input_uint8_output_pairwise_route",
@@ -881,7 +883,7 @@
881
  "provenance": {
882
  "source": "onnxruntime/test/providers/cpu/nn/qlinearconv_op_test.cc",
883
  "test": "QLinearConvTest.Conv3D_S8S8",
884
- "notes": "Signed companion for compact 3D QLinearConv: int8 input, int8 weights, and int8 output."
885
  },
886
  "attrs": { "strides": [1, 1, 1] },
887
  "inputs": {
@@ -1229,15 +1231,18 @@
1229
  "w": {
1230
  "dtype": "int8",
1231
  "shape": [64, 16, 3, 3],
1232
- "data": { "kind": "cycle", "values": [2, -1, 3, -2, 1, 4, -3, 2, 0] }
1233
  },
1234
  "w_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.02] } },
1235
  "w_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "cycle", "values": [1] } },
1236
- "y_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.5] } },
1237
- "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "cycle", "values": [10] } }
1238
  },
1239
  "outputs": { "y": { "dtype": "uint8", "shape": [1, 64, 22, 22] } },
1240
- "attrs": {}
 
 
 
1241
  },
1242
  {
1243
  "name": "im2col_dp4a_requant_huge_finite_saturates_3x3",
@@ -1327,7 +1332,7 @@
1327
  "name": "conv3d_depth_dilation2_compact",
1328
  "provenance": {
1329
  "source": "ONNX QLinearConv-10 volumetric dilation semantics",
1330
- "notes": "Covers a non-default depth dilation. Existing 3D fixtures kept dilationD=1 and no 3D performance fixture existed."
1331
  },
1332
  "attrs": { "strides": [1, 1, 1], "dilations": [2, 1, 1] },
1333
  "inputs": {
@@ -1579,7 +1584,7 @@
1579
  "name": "same_lower_stride2_autopad",
1580
  "provenance": {
1581
  "source": "ONNX Runtime QLinearConv-10 CPUExecutionProvider",
1582
- "notes": "ONNX auto_pad SAME_LOWER puts the odd padding element at the START of the axis, so unlike its SAME_UPPER twin this case has a non-zero leading pad and fails if the kernel is handed the explicit pad attributes instead of the derived ones."
1583
  },
1584
  "attrs": { "auto_pad": "SAME_LOWER", "strides": [2, 2] },
1585
  "inputs": {
@@ -1597,6 +1602,94 @@
1597
  "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } }
1598
  },
1599
  "outputs": { "y": { "dtype": "uint8", "shape": [1, 1, 2, 2], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1600
  }
1601
  ]
1602
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "same_upper_stride2_autopad_input_x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
4
  "backend_qlinearconv_pointwise_u8_input_x": [255, 174, 162, 25, 203, 168, 58, 15, 59, 237, 95, 129, 0, 64, 56, 242, 153, 221, 168, 12, 166, 232, 178, 186, 195, 237, 162, 237, 188, 39, 124, 77, 80, 102, 43, 127, 230, 21, 83, 41, 40, 134, 255, 154, 92, 141, 42, 148, 247],
 
82
  "x": {
83
  "dtype": "int8",
84
  "shape": [1, 1, 3, 3],
85
+ "data": { "kind": "values", "values": [-4, 3, -2, 1, 0, -1, 2, -3, 4] }
86
  },
87
  "x_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.5] } },
88
  "x_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [0] } },
 
93
  "y_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [-3] } }
94
  },
95
  "outputs": { "y": { "dtype": "int8", "shape": [1, 1, 2, 2] } },
96
+ "attrs": {},
97
+ "provenance": {
98
+ "notes": "Signed output storage with a nonzero output zero point. The input is not a ramp, so the 2x2 kernel gives each output position its own response."
99
+ }
100
  },
101
  {
102
  "name": "int8_input_uint8_output_pairwise_route",
 
883
  "provenance": {
884
  "source": "onnxruntime/test/providers/cpu/nn/qlinearconv_op_test.cc",
885
  "test": "QLinearConvTest.Conv3D_S8S8",
886
+ "notes": "A compact three-dimensional QLinearConv uses int8 input, weights, and output."
887
  },
888
  "attrs": { "strides": [1, 1, 1] },
889
  "inputs": {
 
1231
  "w": {
1232
  "dtype": "int8",
1233
  "shape": [64, 16, 3, 3],
1234
+ "data": { "kind": "cycle", "values": [2, -1, 3, -2, 1, 4, -3, 2, 0, -4] }
1235
  },
1236
  "w_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.02] } },
1237
  "w_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "cycle", "values": [1] } },
1238
+ "y_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.002] } },
1239
+ "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "cycle", "values": [128] } }
1240
  },
1241
  "outputs": { "y": { "dtype": "uint8", "shape": [1, 64, 22, 22] } },
1242
+ "attrs": {},
1243
+ "provenance": {
1244
+ "notes": "The dot4 im2col route over 16 input channels. A ten-value weight cycle does not tile the nine taps of a 3x3 kernel, so every output channel carries its own filter, and the output scale resolves the accumulator instead of quantizing every position onto the zero point."
1245
+ }
1246
  },
1247
  {
1248
  "name": "im2col_dp4a_requant_huge_finite_saturates_3x3",
 
1332
  "name": "conv3d_depth_dilation2_compact",
1333
  "provenance": {
1334
  "source": "ONNX QLinearConv-10 volumetric dilation semantics",
1335
+ "notes": "A three-dimensional quantized convolution uses a nondefault depth dilation."
1336
  },
1337
  "attrs": { "strides": [1, 1, 1], "dilations": [2, 1, 1] },
1338
  "inputs": {
 
1584
  "name": "same_lower_stride2_autopad",
1585
  "provenance": {
1586
  "source": "ONNX Runtime QLinearConv-10 CPUExecutionProvider",
1587
+ "notes": "ONNX auto_pad SAME_LOWER places the odd padding element at the start of the axis. The nonzero leading pad verifies that execution uses the derived padding rather than unrelated explicit pad attributes."
1588
  },
1589
  "attrs": { "auto_pad": "SAME_LOWER", "strides": [2, 2] },
1590
  "inputs": {
 
1602
  "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } }
1603
  },
1604
  "outputs": { "y": { "dtype": "uint8", "shape": [1, 1, 2, 2], "tolerance": 0 } }
1605
+ },
1606
+ {
1607
+ "name": "ncw1d_scalar_bias",
1608
+ "provenance": {
1609
+ "source": "ONNX Operators.md, QLinearConv",
1610
+ "notes": "A one-dimensional quantized convolution carrying a bias, which is the ordinary audio form. The bias is non-zero so the case proves it is applied, not merely that a route exists."
1611
+ },
1612
+ "attrs": { "kernel_shape": [2], "pads": [0, 0] },
1613
+ "inputs": {
1614
+ "x": { "dtype": "uint8", "shape": [1, 1, 4], "data": { "kind": "values", "values": [1, 2, 3, 4] } },
1615
+ "x_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1616
+ "x_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1617
+ "w": { "dtype": "int8", "shape": [1, 1, 2], "data": { "kind": "values", "values": [1, 1] } },
1618
+ "w_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1619
+ "w_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1620
+ "y_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1621
+ "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1622
+ "B": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [10] } }
1623
+ },
1624
+ "outputs": {
1625
+ "y": {
1626
+ "dtype": "uint8",
1627
+ "shape": [1, 1, 3],
1628
+ "data": { "kind": "values", "values": [13, 15, 17] },
1629
+ "tolerance": 0
1630
+ }
1631
+ }
1632
+ },
1633
+ {
1634
+ "name": "ncdhw3d_scalar_bias",
1635
+ "provenance": {
1636
+ "source": "ONNX Operators.md, QLinearConv",
1637
+ "notes": "A three-dimensional quantized convolution includes a nonzero bias."
1638
+ },
1639
+ "attrs": { "kernel_shape": [2, 2, 2], "pads": [0, 0, 0, 0, 0, 0] },
1640
+ "inputs": {
1641
+ "x": {
1642
+ "dtype": "uint8",
1643
+ "shape": [1, 1, 2, 2, 2],
1644
+ "data": { "kind": "values", "values": [1, 2, 3, 4, 5, 6, 7, 8] }
1645
+ },
1646
+ "x_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1647
+ "x_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1648
+ "w": {
1649
+ "dtype": "int8",
1650
+ "shape": [1, 1, 2, 2, 2],
1651
+ "data": { "kind": "values", "values": [1, 1, 1, 1, 1, 1, 1, 1] }
1652
+ },
1653
+ "w_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1654
+ "w_zero_point": { "dtype": "int8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1655
+ "y_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1656
+ "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1657
+ "B": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4] } }
1658
+ },
1659
+ "outputs": {
1660
+ "y": {
1661
+ "dtype": "uint8",
1662
+ "shape": [1, 1, 1, 1, 1],
1663
+ "data": { "kind": "values", "values": [40] },
1664
+ "tolerance": 0
1665
+ }
1666
+ }
1667
+ },
1668
+ {
1669
+ "name": "ncw1d_per_channel_weight_quantization",
1670
+ "provenance": {
1671
+ "source": "ONNX Operators.md, QLinearConv",
1672
+ "notes": "One-dimensional convolution uses weights quantized independently per output channel."
1673
+ },
1674
+ "attrs": { "kernel_shape": [2], "pads": [0, 0] },
1675
+ "inputs": {
1676
+ "x": { "dtype": "uint8", "shape": [1, 1, 4], "data": { "kind": "values", "values": [1, 2, 3, 4] } },
1677
+ "x_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1678
+ "x_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } },
1679
+ "w": { "dtype": "int8", "shape": [2, 1, 2], "data": { "kind": "values", "values": [1, 1, 2, -1] } },
1680
+ "w_scale": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [1.0, 1.0] } },
1681
+ "w_zero_point": { "dtype": "int8", "shape": [2], "data": { "kind": "values", "values": [0, 0] } },
1682
+ "y_scale": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [1.0] } },
1683
+ "y_zero_point": { "dtype": "uint8", "shape": [1], "data": { "kind": "values", "values": [0] } }
1684
+ },
1685
+ "outputs": {
1686
+ "y": {
1687
+ "dtype": "uint8",
1688
+ "shape": [1, 2, 3],
1689
+ "data": { "kind": "values", "values": [3, 5, 7, 0, 1, 2] },
1690
+ "tolerance": 0
1691
+ }
1692
+ }
1693
  }
1694
  ]
1695
  }