Xenova HF Staff commited on
Commit
3eb68da
·
verified ·
1 Parent(s): 7d7921b

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -1,3 +1,187 @@
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
+ # com.microsoft.QLinearConv
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Convolution on a quantized input with quantized weights, producing a quantized output, in either tensor layout. `channels_last = 0` reads `X` as `(N, C, D1..Dn)` and writes `Y` as `(N, M, D1..Dn)`; `channels_last = 1` reads `(N, D1..Dn, C)` and writes `(N, D1..Dn, M)`. The weight stays `(M, C/group, k1..kn)` in both layouts. Input and output quantization are per-tensor; weight quantization is per-tensor or per-output-channel. The optional int32 bias is pre-quantized with `scale = x_scale * w_scale` and zero point 0. Spatial ranks 1 to 3 are implemented.
16
+
17
+ See the [ONNX Runtime `QLinearConv` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.QLinearConv) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `TX` | — | — | Quantized input tensor, `(N, C, D1 x ... x Dn)` when `channels_last` is 0 and `(N, D1 x ... x Dn, C)` when it is 1. | 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, C/group, k1 x ... x kn)` in both layouts; `channels_last` never transposes the weight. | 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; the channel axis follows `channels_last` and the spatial sizes follow the kernel, strides, dilations and padding. | required |
38
+
39
+ ## Attributes
40
+
41
+ Attributes and default values (overridable per request):
42
+
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
+ | `channels_last` | `0` | Tensor layout convention for `x` and `y`: `0` places the channel axis at index 1, `1` places it last. The weight layout is unaffected. Defaults to 0. |
47
+ | `dilations` | — | Optional dilation factors, one positive integer per spatial axis. Omission means all ones. |
48
+ | `group` | `1` | Number of groups that input and output channels are split into; defaults to 1. |
49
+ | `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. |
50
+ | `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. |
51
+ | `strides` | — | Optional stride factors, one positive integer per spatial axis. Omission means all ones. |
52
+
53
+ ## Type constraints
54
+
55
+ | Variable | Allowed dtypes |
56
+ | --- | --- |
57
+ | `TX` | `uint8`, `int8` |
58
+ | `TW` | `uint8`, `int8` |
59
+ | `TY` | `uint8`, `int8` |
60
+ | `TF` | `float32` |
61
+
62
+ ## Implementation variants
63
+
64
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
65
+
66
+ - `nchw2d_columns_x4` — Channel-first 2-D route without integer dot-product support: one invocation folds four adjacent output columns into a vec4 accumulator for up to four output channels, so each weight load serves sixteen products.
67
+ - `dp4a_pointwise_nchw` — Channel-first 1x1 route: the convolution is exactly one integer GEMM whose A operand is the weight and whose B operand is the activation image, so the shared packed integer dot-product core runs with no materialized column matrix.
68
+ - `matrix_pointwise_nchw` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
69
+ - `dp4a_im2col_nchw` — Channel-first route that materializes the column matrix in `(ic, kh, kw)` order and hands it to the shared packed integer dot-product core as the B operand, with the weight as A.
70
+ - `matrix_im2col_nchw` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
71
+ - `dp4a_im2col_nchw_bias` — Channel-first route that materializes the column matrix in `(ic, kh, kw)` order and hands it to the shared packed integer dot-product core as the B operand, with the weight as A. A pre-quantized int32 bias is added per output channel before requantization.
72
+ - `matrix_im2col_nchw_bias` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
73
+ - `dp4a_pointwise_nhwc` — Channels-last 1x1 route: the activation is already the GEMM's row-major A operand, so only the weight is reordered into K-major `(kh, kw, ic)` columns for the shared packed integer dot-product core, whose output rows land as the channels-last image.
74
+ - `matrix_pointwise_nhwc` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
75
+ - `dp4a_pointwise_nhwc_bias` — Channels-last 1x1 route: the activation is already the GEMM's row-major A operand, so only the weight is reordered into K-major `(kh, kw, ic)` columns for the shared packed integer dot-product core, whose output rows land as the channels-last image. A pre-quantized int32 bias is added per output channel before requantization.
76
+ - `matrix_pointwise_nhwc_bias` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
77
+ - `dp4a_im2col_nhwc` — Channels-last general route: the activation is already the GEMM's row-major A operand, so only the weight is reordered into K-major `(kh, kw, ic)` columns for the shared packed integer dot-product core, whose output rows land as the channels-last image.
78
+ - `matrix_im2col_nhwc` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
79
+ - `dp4a_im2col_nhwc_bias` — Channels-last general route: the activation is already the GEMM's row-major A operand, so only the weight is reordered into K-major `(kh, kw, ic)` columns for the shared packed integer dot-product core, whose output rows land as the channels-last image. A pre-quantized int32 bias is added per output channel before requantization.
80
+ - `matrix_im2col_nhwc_bias` — Exact subgroup-matrix convolution using the same operand preparation. Bounded f32 partial dots preserve byte-integer products; int32 partial accumulation, optional bias and compensated requantization preserve output quantization. Small grids and short contractions use the packed integer kernels.
81
+ - `precast_f16_pointwise_nchw_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
82
+ - `portable_f16_pointwise_nchw_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
83
+ - `precast_f16_pointwise_nchw_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
84
+ - `portable_f16_pointwise_nchw_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
85
+ - `precast_f16_im2col_nchw_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
86
+ - `portable_f16_im2col_nchw_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
87
+ - `precast_f16_im2col_nchw_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
88
+ - `portable_f16_im2col_nchw_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
89
+ - `precast_f16_im2col_nchw_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
90
+ - `portable_f16_im2col_nchw_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
91
+ - `precast_f16_im2col_nchw_bias_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
92
+ - `portable_f16_im2col_nchw_bias_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
93
+ - `precast_f16_pointwise_nhwc_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
94
+ - `portable_f16_pointwise_nhwc_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
95
+ - `precast_f16_pointwise_nhwc_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
96
+ - `portable_f16_pointwise_nhwc_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
97
+ - `precast_f16_pointwise_nhwc_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
98
+ - `portable_f16_pointwise_nhwc_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
99
+ - `precast_f16_pointwise_nhwc_bias_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
100
+ - `portable_f16_pointwise_nhwc_bias_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
101
+ - `precast_f16_im2col_nhwc_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
102
+ - `portable_f16_im2col_nhwc_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
103
+ - `precast_f16_im2col_nhwc_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
104
+ - `portable_f16_im2col_nhwc_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
105
+ - `precast_f16_im2col_nhwc_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
106
+ - `portable_f16_im2col_nhwc_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
107
+ - `precast_f16_im2col_nhwc_bias_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
108
+ - `portable_f16_im2col_nhwc_bias_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
109
+ - `precast_f32_pointwise_nchw_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
110
+ - `portable_f32_pointwise_nchw_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
111
+ - `precast_f32_pointwise_nchw_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
112
+ - `portable_f32_pointwise_nchw_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
113
+ - `precast_f32_im2col_nchw_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
114
+ - `portable_f32_im2col_nchw_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
115
+ - `precast_f32_im2col_nchw_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
116
+ - `portable_f32_im2col_nchw_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
117
+ - `precast_f32_im2col_nchw_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
118
+ - `portable_f32_im2col_nchw_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
119
+ - `precast_f32_im2col_nchw_bias_row` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
120
+ - `portable_f32_im2col_nchw_bias_row` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
121
+ - `precast_f32_pointwise_nhwc_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
122
+ - `portable_f32_pointwise_nhwc_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
123
+ - `precast_f32_pointwise_nhwc_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
124
+ - `portable_f32_pointwise_nhwc_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
125
+ - `precast_f32_pointwise_nhwc_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
126
+ - `portable_f32_pointwise_nhwc_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
127
+ - `precast_f32_pointwise_nhwc_bias_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
128
+ - `portable_f32_pointwise_nhwc_bias_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
129
+ - `precast_f32_im2col_nhwc_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
130
+ - `portable_f32_im2col_nhwc_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
131
+ - `precast_f32_im2col_nhwc_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
132
+ - `portable_f32_im2col_nhwc_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
133
+ - `precast_f32_im2col_nhwc_bias_tensor` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
134
+ - `portable_f32_im2col_nhwc_bias_tensor` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
135
+ - `precast_f32_im2col_nhwc_bias_column` — Fuse spatial sampling, weight layout and exact byte centering into padded matrix operands. Both component tiers use bounded `f32` partial dots and `int32` totals before compensated requantization; `f16` exactly represents `[-255, 255]`. Tiles follow device limits, and small workloads use the other routes.
136
+ - `portable_f32_im2col_nhwc_bias_column` — Reuse fused exact byte preparation with a portable vector GEMM. Each workgroup computes a device-bounded tile with bounded exact f32 partial dots, int32 totals, optional per-channel bias and compensated requantization. Exact f16 operand storage is used when supported; f32 requires no optional arithmetic features.
137
+
138
+ ## Device requirements
139
+
140
+ Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
141
+
142
+ ## Files
143
+
144
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
145
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
146
+ - [`test.json`](build/webgpu/test.json) — correctness cases
147
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
148
+ - [`conv-int-accumulate-spatial.wgsl.jinja`](build/webgpu/conv-int-accumulate-spatial.wgsl.jinja)
149
+ - [`conv-int-im2col-spatial.wgsl.jinja`](build/webgpu/conv-int-im2col-spatial.wgsl.jinja)
150
+ - [`qlinear-conv-im2col-nhwc.wgsl.jinja`](build/webgpu/qlinear-conv-im2col-nhwc.wgsl.jinja)
151
+ - [`qlinear-conv-nchw-x4.wgsl.jinja`](build/webgpu/qlinear-conv-nchw-x4.wgsl.jinja)
152
+ - [`qlinear-conv-nhwc-oc4.wgsl.jinja`](build/webgpu/qlinear-conv-nhwc-oc4.wgsl.jinja)
153
+ - [`qlinear-conv-requantize.wgsl.jinja`](build/webgpu/qlinear-conv-requantize.wgsl.jinja)
154
+ - [`quant-dp4a-matmul.wgsl.jinja`](build/webgpu/quant-dp4a-matmul.wgsl.jinja)
155
+ - [`quant-exact-matrix.wgsl.jinja`](build/webgpu/quant-exact-matrix.wgsl.jinja)
156
+ - [`quant-exact-portable.wgsl.jinja`](build/webgpu/quant-exact-portable.wgsl.jinja)
157
+ - [`quant-exact-prepare.wgsl.jinja`](build/webgpu/quant-exact-prepare.wgsl.jinja)
158
+ - [`quant-weight-k-major.wgsl.jinja`](build/webgpu/quant-weight-k-major.wgsl.jinja)
159
+
160
+ ## Use with `@huggingface/kernels`
161
+
162
+ ```sh
163
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
164
+ ```
165
+
166
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
167
+
168
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
169
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
170
+
171
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
172
+
173
+ ```js
174
+ import { getKernel } from "@huggingface/kernels";
175
+
176
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.QLinearConv", { version: 1 });
177
+ const { y } = await kernel({
178
+ x: { data: xData, shape: [1, 1, 1, 1] },
179
+ x_scale: { data: x_scaleData, shape: [1] },
180
+ x_zero_point: { data: x_zero_pointData, shape: [1] },
181
+ w: { data: wData, shape: [2, 1, 1, 1] },
182
+ w_scale: { data: w_scaleData, shape: [1] },
183
+ w_zero_point: { data: w_zero_pointData, shape: [1] },
184
+ y_scale: { data: y_scaleData, shape: [1] },
185
+ y_zero_point: { data: y_zero_pointData, shape: [1] },
186
+ });
187
+ ```
build/webgpu/bench.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/conv-int-accumulate-spatial.wgsl.jinja ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_scalar_zero(fn_name, buffer_name, is_unsigned, present=true) %}
2
+ fn {{ fn_name }}() -> i32 {
3
+ {% if present %}
4
+ {% if is_unsigned %}
5
+ return i32({{ buffer_name }}[0]);
6
+ {% else %}
7
+ return {{ buffer_name }}[0];
8
+ {% endif %}
9
+ {% else %}
10
+ return 0;
11
+ {% endif %}
12
+ }{% endmacro %}
13
+ {% macro quant_read_column_zero(fn_name, buffer_name, is_unsigned, present=true, per_column=false, param_name="col", return_type="i32") %}
14
+ fn {{ fn_name }}({% if per_column %}{{ param_name }}: u32{% endif %}) -> {{ return_type }} {
15
+ {% if present %}
16
+ {% if per_column %}
17
+ {% if is_unsigned %}
18
+ return i32({{ buffer_name }}[{{ param_name }}]);
19
+ {% else %}
20
+ return {{ buffer_name }}[{{ param_name }}];
21
+ {% endif %}
22
+ {% else %}
23
+ {% if is_unsigned %}
24
+ return i32({{ buffer_name }}[0]);
25
+ {% else %}
26
+ return {{ buffer_name }}[0];
27
+ {% endif %}
28
+ {% endif %}
29
+ {% else %}
30
+ return 0;
31
+ {% endif %}
32
+ }{% endmacro %}
33
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
34
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
35
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
36
+ // per-axis workgroup fold width.
37
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
38
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
39
+ {{ env.wgsl.resourceDeclarations }}
40
+
41
+ fn read_x(index: u32) -> i32 {
42
+ {% if xUnsigned %}
43
+ return i32(x[index]);
44
+ {% else %}
45
+ return x[index];
46
+ {% endif %}
47
+ }
48
+
49
+ fn read_w(index: u32) -> i32 {
50
+ {% if wUnsigned %}
51
+ return i32(w[index]);
52
+ {% else %}
53
+ return w[index];
54
+ {% endif %}
55
+ }
56
+
57
+ {{ quant_read_scalar_zero("read_x_zero", "x_zero_point", xUnsigned, not (xZeroOmitted is defined and xZeroOmitted)) }}
58
+
59
+ // A per-output-channel weight zero point applies at every spatial rank.
60
+ {{ quant_read_column_zero("read_w_zero", "w_zero_point", wUnsigned, not (wZeroOmitted is defined and wZeroOmitted), wZeroPerChannel, "oc") }}
61
+
62
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
63
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
64
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, "index", guardInline=true) }}
65
+ {% if channelsLast is defined and channelsLast %}
66
+ let oc = index % params.outChannels;
67
+ var t = index / params.outChannels;
68
+ let ow = t % params.outW;
69
+ t = t / params.outW;
70
+ let oh = t % params.outH;
71
+ t = t / params.outH;
72
+ {% if spatialRankSpec == 3 %}
73
+ let od = t % params.outD;
74
+ t = t / params.outD;
75
+ {% endif %}
76
+ let batch = t;
77
+ {% else %}
78
+ let ow = index % params.outW;
79
+ var t = index / params.outW;
80
+ let oh = t % params.outH;
81
+ t = t / params.outH;
82
+ {% if spatialRankSpec == 3 %}
83
+ let od = t % params.outD;
84
+ t = t / params.outD;
85
+ {% endif %}
86
+ let oc = t % params.outChannels;
87
+ let batch = t / params.outChannels;
88
+ {% endif %}
89
+ let group = oc / params.outChannelsPerGroup;
90
+ let xzp = read_x_zero();
91
+ let wzp = read_w_zero({% if wZeroPerChannel %}oc{% endif %});
92
+ var acc = 0i;
93
+ for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
94
+ let input_channel = group * params.inChannelsPerGroup + ic;
95
+ {% if spatialRankSpec == 3 %}
96
+ for (var kd = 0u; kd < params.kernelD; kd = kd + 1u) {
97
+ let id = i32(od * params.strideD + kd * params.dilationD) - params.padD;
98
+ if (id < 0 || id >= i32(params.inD)) { continue; }
99
+ {% endif %}
100
+ {% filter indent(2 if spatialRankSpec == 3 else 0, true) %}
101
+ for (var kh = 0u; kh < params.kernelH; kh = kh + 1u) {
102
+ let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
103
+ if (ih < 0 || ih >= i32(params.inH)) { continue; }
104
+ for (var kw = 0u; kw < params.kernelW; kw = kw + 1u) {
105
+ let iw = i32(ow * params.strideW + kw * params.dilationW) - params.padW;
106
+ if (iw < 0 || iw >= i32(params.inW)) { continue; }
107
+ {% if spatialRankSpec == 3 %}
108
+ {% if channelsLast is defined and channelsLast %}
109
+ let x_index = (((batch * params.inD + u32(id)) * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + input_channel;
110
+ {% else %}
111
+ let x_index = (((batch * params.inChannels + input_channel) * params.inD + u32(id)) * params.inH + u32(ih)) * params.inW + u32(iw);
112
+ {% endif %}
113
+ let w_index = (((oc * params.weightInChannels + ic) * params.kernelD + kd) * params.kernelH + kh) * params.kernelW + kw;
114
+ {% else %}
115
+ {% if channelsLast is defined and channelsLast %}
116
+ let x_index = ((batch * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + input_channel;
117
+ {% else %}
118
+ let x_index = ((batch * params.inChannels + input_channel) * params.inH + u32(ih)) * params.inW + u32(iw);
119
+ {% endif %}
120
+ let w_index = ((oc * params.weightInChannels + ic) * params.kernelH + kh) * params.kernelW + kw;
121
+ {% endif %}
122
+ acc = acc + (read_x(x_index) - xzp) * (read_w(w_index) - wzp);
123
+ }
124
+ }
125
+ {% endfilter %}
126
+ {% if spatialRankSpec == 3 %}
127
+ }
128
+ {% endif %}
129
+ }
130
+ y[index] = acc;
131
+ }
build/webgpu/conv-int-im2col-spatial.wgsl.jinja ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Widened-int8 im2col for convolution DP4A paths. The materialized K order is
2
+ * OIHW for 2D input and OIDHW for 3D input. Spatial padding is filled with the
3
+ * input zero point, so padded taps contribute exactly zero after centering.
4
+ * Adjacent x invocations cover adjacent output positions for coalesced writes.
5
+ * Optional extra K rows contain the raw input zero point to align the DP4A
6
+ * reduction. */
7
+ {{ env.wgsl.resourceDeclarations }}
8
+ {% macro spatial_constants() %}
9
+ const KERNEL_H: u32 = {{ kernelHSpec }}u;
10
+ const KERNEL_W: u32 = {{ kernelWSpec }}u;
11
+ const KERNEL_HW: u32 = KERNEL_H * KERNEL_W;
12
+ const KSIZE: u32 = KERNEL_HW;
13
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
14
+ const STRIDE_W: u32 = {{ strideWSpec }}u;
15
+ const DILATION_H: u32 = {{ dilationHSpec }}u;
16
+ const DILATION_W: u32 = {{ dilationWSpec }}u;
17
+ const PAD_TOP: i32 = {{ padTopSpec }};
18
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
19
+ {% endmacro %}
20
+ {% macro spatial_sample(value="value", input="x", zero="x_zero_point", sampleK="k", sampleScalar="auto") %}
21
+ let ic = {{ sampleK }} / KSIZE;
22
+ let kq = {{ sampleK }} - ic * KSIZE;
23
+ let kh = kq / KERNEL_W;
24
+ let kw = kq - kh * KERNEL_W;
25
+ let oh = pos / params.outW;
26
+ let ow = pos - oh * params.outW;
27
+ let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
28
+ let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
29
+ var {{ value }}: {{ bScalar if sampleScalar == "auto" else sampleScalar }} = {{ zero }}[0];
30
+ if (ih >= 0 && ih < i32(params.inH) && iw >= 0 && iw < i32(params.inW)) {
31
+ {{ value }} = {{ input }}[{% if sampleChannelsLast is defined and sampleChannelsLast %}((batch * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + ic{% else %}((batch * params.inChannels + ic) * params.inH + u32(ih)) * params.inW + u32(iw){% endif %}];
32
+ }
33
+ {% endmacro %}
34
+ {{ spatial_constants() }}
35
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
36
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
37
+ let pos = gid.x;
38
+ if (pos >= params.outCount) {
39
+ return;
40
+ }
41
+ let k = gid.y;
42
+ let batch = gid.z;
43
+ {{ spatial_sample() }}
44
+ cols[(batch * params.kRows + k) * params.outCount + pos] = value;
45
+ }
build/webgpu/manifest.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.QLinearConv",
3
+ "id": "_com_microsoft_qlinearconv_webgpu_dd39e27",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "deKtgF/RQc9YtJ6rKQbZOEn+82qQqbE7gvVAbfsCrHI=",
11
+ "conv-int-accumulate-spatial.wgsl.jinja": "ovvyaZB9d/pmohPYt0/x2NLB2wSWMJ0/eoBHcm4i/5E=",
12
+ "conv-int-im2col-spatial.wgsl.jinja": "23v1AQ7sYZnMKE7b32VbAbjutCCZnsMgXRkXYm4W6/k=",
13
+ "manifest.json": "TbaAnjGDWtUPENa228Ulfk4p2D8TYPRcs0oyPBIHGoI=",
14
+ "qlinear-conv-im2col-nhwc.wgsl.jinja": "CfKXjliMWapwWUjjAk3hY51R2rsx1Wcd4LkvdYUWe6I=",
15
+ "qlinear-conv-nchw-x4.wgsl.jinja": "CrFYcoL6MLszFVzk877uUZu6eMi7h6Wii63GJXWC+x0=",
16
+ "qlinear-conv-nhwc-oc4.wgsl.jinja": "rTj6WQWmCd0BMzMZJVk/ZssxIDh2HrUEOnuK/owJb3Q=",
17
+ "qlinear-conv-requantize.wgsl.jinja": "ElB8XipqRr6CjNjGakSOhsdsM/TezfpRQ2zyh+vENPQ=",
18
+ "quant-dp4a-matmul.wgsl.jinja": "jrfKZZQ/P2yDNDElA9IBQJIxU2p7yJzDjj+dt5D4u1E=",
19
+ "quant-exact-matrix.wgsl.jinja": "EFugcaUsDIAeUHx17xX66qV/fqfila1p8nlXQCG97Kw=",
20
+ "quant-exact-portable.wgsl.jinja": "Ym20X8RcLRzXP9DcQSwJBp4z8RRCBc+bwhrofGC2SBc=",
21
+ "quant-exact-prepare.wgsl.jinja": "hAWISxWqTkqc1z9PRAlaNnKqajQKc6sw6IVWfyYmHns=",
22
+ "quant-weight-k-major.wgsl.jinja": "B2sKfFD51YrInBxbg8bUKVtU8j/QHUgyoJ7QONaV7iE=",
23
+ "test.json": "Yi0vCMNVybIz6uyywnEE6TeUR5B22AzjXtgta2kOmK4="
24
+ }
25
+ },
26
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
27
+ "webgpu": {
28
+ "manifestSpec": "2.1",
29
+ "variants": {
30
+ "spatial_nd2_scalar": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
31
+ "spatial_nd2_scalar_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
32
+ "spatial_nd2_per_channel": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
33
+ "spatial_nd2_per_channel_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
34
+ "spatial_nd3_scalar": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
35
+ "spatial_nd3_scalar_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
36
+ "spatial_nd3_per_channel": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
37
+ "spatial_nd3_per_channel_bias": ["conv-int-accumulate-spatial.wgsl.jinja", "qlinear-conv-requantize.wgsl.jinja"],
38
+ "nchw2d_columns_x4": ["qlinear-conv-nchw-x4.wgsl.jinja"],
39
+ "nhwc2d_channels_x4": ["qlinear-conv-nhwc-oc4.wgsl.jinja"],
40
+ "nhwc2d_depthwise_x4": ["qlinear-conv-nhwc-oc4.wgsl.jinja"],
41
+ "nhwc2d_channels_x4_bias": ["qlinear-conv-nhwc-oc4.wgsl.jinja"],
42
+ "nhwc2d_depthwise_x4_bias": ["qlinear-conv-nhwc-oc4.wgsl.jinja"],
43
+ "dp4a_pointwise_nchw": ["quant-dp4a-matmul.wgsl.jinja"],
44
+ "matrix_pointwise_nchw": ["quant-exact-matrix.wgsl.jinja"],
45
+ "dp4a_im2col_nchw": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja"],
46
+ "matrix_im2col_nchw": ["conv-int-im2col-spatial.wgsl.jinja", "quant-exact-matrix.wgsl.jinja"],
47
+ "dp4a_im2col_nchw_bias": ["conv-int-im2col-spatial.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja"],
48
+ "matrix_im2col_nchw_bias": ["conv-int-im2col-spatial.wgsl.jinja", "quant-exact-matrix.wgsl.jinja"],
49
+ "dp4a_pointwise_nhwc": ["quant-dp4a-matmul.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
50
+ "matrix_pointwise_nhwc": ["quant-exact-matrix.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
51
+ "dp4a_pointwise_nhwc_bias": ["quant-dp4a-matmul.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
52
+ "matrix_pointwise_nhwc_bias": ["quant-exact-matrix.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
53
+ "dp4a_im2col_nhwc": ["qlinear-conv-im2col-nhwc.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
54
+ "matrix_im2col_nhwc": ["qlinear-conv-im2col-nhwc.wgsl.jinja", "quant-exact-matrix.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
55
+ "dp4a_im2col_nhwc_bias": ["qlinear-conv-im2col-nhwc.wgsl.jinja", "quant-dp4a-matmul.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
56
+ "matrix_im2col_nhwc_bias": ["qlinear-conv-im2col-nhwc.wgsl.jinja", "quant-exact-matrix.wgsl.jinja", "quant-weight-k-major.wgsl.jinja"],
57
+ "precast_f16_pointwise_nchw_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
58
+ "portable_f16_pointwise_nchw_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
59
+ "precast_f16_pointwise_nchw_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
60
+ "portable_f16_pointwise_nchw_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
61
+ "precast_f16_im2col_nchw_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
62
+ "portable_f16_im2col_nchw_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
63
+ "precast_f16_im2col_nchw_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
64
+ "portable_f16_im2col_nchw_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
65
+ "precast_f16_im2col_nchw_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
66
+ "portable_f16_im2col_nchw_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
67
+ "precast_f16_im2col_nchw_bias_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
68
+ "portable_f16_im2col_nchw_bias_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
69
+ "precast_f16_pointwise_nhwc_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
70
+ "portable_f16_pointwise_nhwc_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
71
+ "precast_f16_pointwise_nhwc_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
72
+ "portable_f16_pointwise_nhwc_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
73
+ "precast_f16_pointwise_nhwc_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
74
+ "portable_f16_pointwise_nhwc_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
75
+ "precast_f16_pointwise_nhwc_bias_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
76
+ "portable_f16_pointwise_nhwc_bias_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
77
+ "precast_f16_im2col_nhwc_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
78
+ "portable_f16_im2col_nhwc_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
79
+ "precast_f16_im2col_nhwc_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
80
+ "portable_f16_im2col_nhwc_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
81
+ "precast_f16_im2col_nhwc_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
82
+ "portable_f16_im2col_nhwc_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
83
+ "precast_f16_im2col_nhwc_bias_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
84
+ "portable_f16_im2col_nhwc_bias_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
85
+ "precast_f32_pointwise_nchw_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
86
+ "portable_f32_pointwise_nchw_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
87
+ "precast_f32_pointwise_nchw_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
88
+ "portable_f32_pointwise_nchw_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
89
+ "precast_f32_im2col_nchw_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
90
+ "portable_f32_im2col_nchw_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
91
+ "precast_f32_im2col_nchw_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
92
+ "portable_f32_im2col_nchw_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
93
+ "precast_f32_im2col_nchw_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
94
+ "portable_f32_im2col_nchw_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
95
+ "precast_f32_im2col_nchw_bias_row": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
96
+ "portable_f32_im2col_nchw_bias_row": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
97
+ "precast_f32_pointwise_nhwc_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
98
+ "portable_f32_pointwise_nhwc_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
99
+ "precast_f32_pointwise_nhwc_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
100
+ "portable_f32_pointwise_nhwc_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
101
+ "precast_f32_pointwise_nhwc_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
102
+ "portable_f32_pointwise_nhwc_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
103
+ "precast_f32_pointwise_nhwc_bias_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
104
+ "portable_f32_pointwise_nhwc_bias_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
105
+ "precast_f32_im2col_nhwc_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
106
+ "portable_f32_im2col_nhwc_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
107
+ "precast_f32_im2col_nhwc_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
108
+ "portable_f32_im2col_nhwc_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
109
+ "precast_f32_im2col_nhwc_bias_tensor": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
110
+ "portable_f32_im2col_nhwc_bias_tensor": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
111
+ "precast_f32_im2col_nhwc_bias_column": ["quant-exact-matrix.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"],
112
+ "portable_f32_im2col_nhwc_bias_column": ["quant-exact-portable.wgsl.jinja", "quant-exact-prepare.wgsl.jinja"]
113
+ }
114
+ }
115
+ }
build/webgpu/qlinear-conv-im2col-nhwc.wgsl.jinja ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Widened-int8 im2col for a channels-last quantized convolution. The materialized
2
+ // A operand is `[batch, outSpatial, KPadded]` with K enumerating `(kh, kw, ic)`,
3
+ // so the input channel is the fastest axis of both the source activation and the
4
+ // reduction and every gather is a contiguous run. Spatial taps outside the input
5
+ // and the K tail hold the input zero point, so they contribute exactly zero after
6
+ // centering.
7
+ //
8
+ // KPadded is a multiple of four by construction, so the store is one vec4 word per
9
+ // K quadruple, and each invocation writes `WORDS_PER_LANE` of them strided by the
10
+ // lane count, which keeps neighbouring invocations on neighbouring words while
11
+ // giving each lane several stores in flight. When the input channel count is itself
12
+ // a multiple of four a quadruple never crosses a kernel tap, and the gather is the
13
+ // matching aligned vec4 load.
14
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
15
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
16
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
17
+ // per-axis workgroup fold width.
18
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
19
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
20
+ {{ env.wgsl.resourceDeclarations }}
21
+
22
+ const WORDS_PER_LANE: u32 = {{ im2colWords }}u;
23
+ const KERNEL_W: u32 = {{ kernelWSpec }}u;
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>) {
33
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, "lane", guardInline=true) }}
34
+ let zp = x_zero_point[0];
35
+ for (var i = 0u; i < WORDS_PER_LANE; i = i + 1u) {
36
+ let word = lane + i * params.count;
37
+ if (word >= params.totalWords) { break; }
38
+ let w = word % params.wordsPerRow;
39
+ let t = word / params.wordsPerRow;
40
+ let pos = t % params.outCount;
41
+ let batch = t / params.outCount;
42
+ let oh = pos / params.outW;
43
+ let ow = pos - oh * params.outW;
44
+ let k0 = w * 4u;
45
+ var value = vec4<{{ xScalar }}>(zp, zp, zp, zp);
46
+ {% if channelsAligned %}
47
+ // A quadruple of K entries stays inside one kernel tap, so one bounds test and
48
+ // one aligned vec4 load cover all four input channels.
49
+ if (k0 < params.kRows) {
50
+ let ic = k0 % params.inChannels;
51
+ let khw = k0 / params.inChannels;
52
+ let kw = khw % KERNEL_W;
53
+ let kh = khw / KERNEL_W;
54
+ let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
55
+ let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
56
+ if (ih >= 0 && ih < i32(params.inH) && iw >= 0 && iw < i32(params.inW)) {
57
+ value = x[(((batch * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + ic) / 4u];
58
+ }
59
+ }
60
+ {% else %}
61
+ // An unaligned channel count lets a quadruple straddle two kernel taps and the
62
+ // K tail, so each lane of the word resolves its own tap.
63
+ {% for j in range(4) %}
64
+ {
65
+ let k = k0 + {{ j }}u;
66
+ if (k < params.kRows) {
67
+ let ic = k % params.inChannels;
68
+ let khw = k / params.inChannels;
69
+ let kw = khw % KERNEL_W;
70
+ let kh = khw / KERNEL_W;
71
+ let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
72
+ let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
73
+ if (ih >= 0 && ih < i32(params.inH) && iw >= 0 && iw < i32(params.inW)) {
74
+ value[{{ j }}] = x[((batch * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + ic];
75
+ }
76
+ }
77
+ }
78
+ {% endfor %}
79
+ {% endif %}
80
+ cols[word] = value;
81
+ }
82
+ }
build/webgpu/qlinear-conv-nchw-x4.wgsl.jinja ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_scalar_zero(fn_name, buffer_name, is_unsigned, present=true) %}
2
+ fn {{ fn_name }}() -> i32 {
3
+ {% if is_unsigned %}
4
+ return i32({{ buffer_name }}[0]);
5
+ {% else %}
6
+ return {{ buffer_name }}[0];
7
+ {% endif %}
8
+ }{% endmacro %}
9
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
10
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
11
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
12
+ // per-axis workgroup fold width.
13
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};{% endmacro %}
14
+ {{ env.wgsl.resourceDeclarations }}
15
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
16
+ // and accumulator product using integer significands; no f64 feature required.
17
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
18
+ let low = a.x + b.x;
19
+ let middle = a.y + b.y;
20
+ let with_carry = middle + u32(low < a.x);
21
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
22
+ }
23
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
24
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
25
+ }
26
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
27
+ var q = value;
28
+ var sticky = false;
29
+ var guard = false;
30
+ for (var bit = 0u; bit < shift; bit++) {
31
+ sticky = sticky || guard;
32
+ guard = (q.x & 1u) != 0u;
33
+ q = quant_u96_shr(q);
34
+ }
35
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
36
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
37
+ }
38
+ return q;
39
+ }
40
+ fn quant_float_significand(bits: u32) -> u32 {
41
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
42
+ }
43
+ fn quant_float_exponent(bits: u32) -> i32 {
44
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
45
+ }
46
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
47
+ if (acc == 0) { return 0; }
48
+ let a_bits = bitcast<u32>(a_s);
49
+ let b_bits = bitcast<u32>(b_s);
50
+ let y_bits = bitcast<u32>(y_s);
51
+ let a = quant_float_significand(a_bits);
52
+ let b = quant_float_significand(b_bits);
53
+ let denominator = quant_float_significand(y_bits);
54
+ if (a == 0u || b == 0u) { return 0; }
55
+ // The exact product of two 24-bit significands occupies at most 48 bits.
56
+ let p0 = (a & 65535u) * (b & 65535u);
57
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
58
+ let product_low = p0 + (cross << 16u);
59
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
60
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
61
+ var remainder = 0u;
62
+ var significant_bits = 0u;
63
+ var leading_exponent = 0;
64
+ var significand = vec2<u32>(0u);
65
+ // Long division generates the 53 bits of the binary64 multiplier. The
66
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
67
+ // zero bits precede the first significant bit.
68
+ loop {
69
+ var incoming = 0u;
70
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
71
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
72
+ remainder = (remainder << 1u) | incoming;
73
+ let digit = u32(remainder >= denominator);
74
+ if (digit != 0u) { remainder -= denominator; }
75
+ if (significant_bits != 0u || digit != 0u) {
76
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
77
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
78
+ significant_bits++;
79
+ }
80
+ input_bit--;
81
+ if (significant_bits == 53u) { break; }
82
+ }
83
+ let twice_remainder = remainder << 1u;
84
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
85
+ let low = significand.x + 1u;
86
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
87
+ }
88
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
89
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
90
+ // significand by the exact accumulator into three little-endian u32 limbs.
91
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
92
+ var operand = vec3<u32>(significand, 0u);
93
+ var product = vec3<u32>(0u);
94
+ loop {
95
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
96
+ magnitude >>= 1u;
97
+ if (magnitude == 0u) { break; }
98
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
99
+ }
100
+ var product_top = 31 - i32(countLeadingZeros(product.x));
101
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
102
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
103
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
104
+ if (product_top + scale_exponent < -2) { return 0; }
105
+ let product_shift = u32(max(0, product_top - 52));
106
+ let rounded_product = quant_u96_round_shift(product, product_shift);
107
+ let result_exponent = scale_exponent + i32(product_shift);
108
+ var rounded = rounded_product;
109
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
110
+ else { rounded.x <<= u32(result_exponent); }
111
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
112
+ }
113
+
114
+ // A power-of-two division and an exactly represented <=48-bit product need
115
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
116
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
117
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
118
+ let significand = quant_float_significand(bitcast<u32>(scale));
119
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
120
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
121
+ return integer_bits + scale_bits <= 48u;
122
+ }
123
+ fn quant_normal_expansion_range(v: f32) -> bool {
124
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
125
+ // Reserve two binary32 significands plus four carry/error bits above the
126
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
127
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
128
+ }
129
+
130
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
131
+ let estimate = numerator_hi / denominator;
132
+ // Anything outside this range saturates after adding an 8-bit zero point;
133
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
134
+ if (estimate <= -1024.0) { return -1024; }
135
+ if (estimate >= 1024.0) { return 1024; }
136
+
137
+ let lo = floor(estimate);
138
+ let hi = lo + 1.0;
139
+ let midpoint = lo + 0.5;
140
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
141
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
142
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
143
+ // The high-word subtraction is exact for these similarly sized products
144
+ // (Sterbenz); adding the residual difference therefore preserves its sign
145
+ // even when the rounded high words straddle adjacent f32 values.
146
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
147
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
148
+ // Six FMA/product tails and their additions fit within this conservative
149
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
150
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
151
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
152
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
153
+ }
154
+ if (midpoint_delta < 0.0) {
155
+ return i32(lo);
156
+ }
157
+ if (midpoint_delta > 0.0) {
158
+ return i32(hi);
159
+ }
160
+ let half_lo = floor(lo * 0.5);
161
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
162
+ return i32(select(hi, lo, lo_is_even));
163
+ }
164
+
165
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
166
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
167
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
168
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
169
+ var rounded = 0u;
170
+ if (exponent >= 0) {
171
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
172
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
173
+ } else {
174
+ let shift = u32(-exponent);
175
+ if (shift >= 32u) { return 0; }
176
+ let whole = magnitude >> shift;
177
+ let remainder = magnitude & ((1u << shift) - 1u);
178
+ let half = 1u << (shift - 1u);
179
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
180
+ }
181
+ return select(i32(rounded), -i32(rounded), acc < 0);
182
+ }
183
+
184
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
185
+ if (acc == 0) { return 0; }
186
+ let a_bits = bitcast<u32>(a_s);
187
+ let b_bits = bitcast<u32>(b_s);
188
+ let y_bits = bitcast<u32>(y_s);
189
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
190
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
191
+ return quant_round_power_two(acc, exponent);
192
+ }
193
+ let acc_f = f32(acc);
194
+ // Every i32 high part aligned to 256 is exactly representable in f32.
195
+ // Recover the conversion residual without an out-of-range i32(acc_f)
196
+ // conversion when INT_MAX rounds up to 2^31.
197
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
198
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
199
+ // rounded intermediate cannot move a near-half value onto the tie.
200
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
201
+ let direct_hi = fma(acc_f, a_s, 0.0);
202
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
203
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
204
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
205
+ }
206
+
207
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
208
+ // which side of a rounding midpoint a near-half value occupies.
209
+ let ab_hi = fma(a_s, b_s, 0.0);
210
+ let ab_lo = fma(a_s, b_s, -ab_hi);
211
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
212
+ let product_hi = fma(acc_f, ab_hi, 0.0);
213
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
214
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
215
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
216
+ // Compare the expansion directly. Renormalizing it adds a cancelling
217
+ // subtraction/addition chain that can erase the residual during lowering.
218
+ let denominator_bits = bitcast<u32>(y_s);
219
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
220
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
221
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
222
+ }
223
+
224
+ // A conservative binary32 error filter accepts only a result safely inside one
225
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
226
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
227
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
228
+ if (acc == 0) { return 0; }
229
+ let a_bits = bitcast<u32>(a_s);
230
+ let b_bits = bitcast<u32>(b_s);
231
+ let y_bits = bitcast<u32>(y_s);
232
+ let ea = (a_bits >> 23u) & 255u;
233
+ let eb = (b_bits >> 23u) & 255u;
234
+ let ey = (y_bits >> 23u) & 255u;
235
+ // Certify the range before arithmetic: overflow need not produce infinity.
236
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
237
+ // scale-only bounds cover every accumulator and can be shared across lanes.
238
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
239
+ // below overflow for products, division and the expansion midpoint product.
240
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
241
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
242
+ ea + eb < 180u || ea + eb > 347u ||
243
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
244
+ return quant_round_binary64(acc, a_s, b_s, y_s);
245
+ }
246
+ let scale = fma(a_s, b_s, 0.0);
247
+ let product = fma(f32(acc), scale, 0.0);
248
+ let estimate = product / y_s;
249
+ if (estimate <= -1024.0) { return -1024; }
250
+ if (estimate >= 1024.0) { return 1024; }
251
+ let nearest = round(estimate);
252
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
253
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
254
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
255
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
256
+ {% if device.adapterInfo.vendor != "apple" %}
257
+ // A rounded half-integer estimate can hide which side the exact f32-scale
258
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
259
+ // the compact fast path; exact power-of-two ratios already have a cheap
260
+ // and correct path in round_requantized_precise.
261
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
262
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
263
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
264
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
265
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
266
+ }
267
+ {% endif %}
268
+ return round_requantized_precise(acc, a_s, b_s, y_s);
269
+ }
270
+
271
+ {{ quant_read_scalar_zero("x_zero", "x_zero_point", xUnsigned) }}
272
+
273
+ fn w_zero({% if wZeroPerChannel %}oc: u32{% endif %}) -> i32 {
274
+ {% if wZeroPerChannel %}
275
+ let i = oc;
276
+ {% else %}
277
+ let i = 0u;
278
+ {% endif %}
279
+ {% if wUnsigned %}
280
+ return i32(w_zero_point[i]);
281
+ {% else %}
282
+ return w_zero_point[i];
283
+ {% endif %}
284
+ }
285
+
286
+ fn centered_x(i: u32, zp: i32) -> i32 {
287
+ {% if xUnsigned %}
288
+ return i32(x[i]) - zp;
289
+ {% else %}
290
+ return x[i] - zp;
291
+ {% endif %}
292
+ }
293
+
294
+ fn centered_w(i: u32, zp: i32) -> i32 {
295
+ {% if wUnsigned %}
296
+ return i32(w[i]) - zp;
297
+ {% else %}
298
+ return w[i] - zp;
299
+ {% endif %}
300
+ }
301
+
302
+ {{ quant_read_scalar_zero("y_zero", "y_zero_point", yUnsigned) }}
303
+
304
+ fn requant(acc: i32{% if wScalePerChannel %}, oc: u32{% endif %}) -> i32 {
305
+ {% if wScalePerChannel %}
306
+ let ws = w_scale[oc];
307
+ {% else %}
308
+ let ws = w_scale[0];
309
+ {% endif %}
310
+ return clamp(round_requantized(acc, x_scale[0], ws, y_scale[0]) + y_zero(), {{ qMin }}, {{ qMax }});
311
+ }
312
+
313
+ fn write_y(i: u32, value: i32) {
314
+ {% if yUnsigned %}
315
+ y[i] = u32(value);
316
+ {% else %}
317
+ y[i] = value;
318
+ {% endif %}
319
+ }
320
+
321
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
322
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
323
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, "flat", "") }}
324
+ let groups_per_row = (params.outW + 3u) / 4u;
325
+ let oc_blocks = (params.outChannels + {{ ocTile - 1 }}u) / {{ ocTile }}u;
326
+ let group_count = params.batch * oc_blocks * params.outH * groups_per_row;
327
+ if (flat >= group_count) { return; }
328
+
329
+ let ow0 = (flat % groups_per_row) * 4u;
330
+ let row = flat / groups_per_row;
331
+ let oh = row % params.outH;
332
+ let oc0 = ((row / params.outH) % oc_blocks) * {{ ocTile }}u;
333
+ let n = row / (params.outH * oc_blocks);
334
+ let group = oc0 / params.outChannelsPerGroup;
335
+ let in_start = group * params.inChannelsPerGroup;
336
+ let xzp = x_zero();
337
+ {% for ot in range(ocTile) %}
338
+ var acc{{ ot }} = vec4<i32>(0);
339
+ {% endfor %}
340
+
341
+ for (var icg = 0u; icg < params.inChannelsPerGroup; icg = icg + 1u) {
342
+ let ic = in_start + icg;
343
+ for (var kh = 0u; kh < params.kernelH; kh = kh + 1u) {
344
+ let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
345
+ for (var kw = 0u; kw < params.kernelW; kw = kw + 1u) {
346
+ var x0 = 0i;
347
+ var x1 = 0i;
348
+ var x2 = 0i;
349
+ var x3 = 0i;
350
+ if (ih >= 0 && ih < i32(params.inH)) {
351
+ let iw0 = i32(ow0 * params.strideW + kw * params.dilationW) - params.padW;
352
+ let iw1 = i32((ow0 + 1u) * params.strideW + kw * params.dilationW) - params.padW;
353
+ let iw2 = i32((ow0 + 2u) * params.strideW + kw * params.dilationW) - params.padW;
354
+ let iw3 = i32((ow0 + 3u) * params.strideW + kw * params.dilationW) - params.padW;
355
+ let base = (n * params.inChannels + ic) * params.inH * params.inW + u32(ih) * params.inW;
356
+ if (ow0 < params.outW && iw0 >= 0 && iw0 < i32(params.inW)) { x0 = centered_x(base + u32(iw0), xzp); }
357
+ if (ow0 + 1u < params.outW && iw1 >= 0 && iw1 < i32(params.inW)) { x1 = centered_x(base + u32(iw1), xzp); }
358
+ if (ow0 + 2u < params.outW && iw2 >= 0 && iw2 < i32(params.inW)) { x2 = centered_x(base + u32(iw2), xzp); }
359
+ if (ow0 + 3u < params.outW && iw3 >= 0 && iw3 < i32(params.inW)) { x3 = centered_x(base + u32(iw3), xzp); }
360
+ }
361
+ let xv = vec4<i32>(x0, x1, x2, x3);
362
+ {% for ot in range(ocTile) %}
363
+ let oc{{ ot }} = oc0 + {{ ot }}u;
364
+ if (oc{{ ot }} < params.outChannels) {
365
+ let wi{{ ot }} = (((oc{{ ot }} * params.weightInChannels + icg) * params.kernelH + kh) * params.kernelW + kw);
366
+ let wc{{ ot }} = centered_w(wi{{ ot }}, w_zero({% if wZeroPerChannel %}oc{{ ot }}{% endif %}));
367
+ acc{{ ot }} = acc{{ ot }} + xv * vec4<i32>(wc{{ ot }});
368
+ }
369
+ {% endfor %}
370
+ }
371
+ }
372
+ }
373
+
374
+ {% for ot in range(ocTile) %}
375
+ let out_oc{{ ot }} = oc0 + {{ ot }}u;
376
+ if (out_oc{{ ot }} < params.outChannels) {
377
+ let out_base{{ ot }} = ((n * params.outChannels + out_oc{{ ot }}) * params.outH + oh) * params.outW + ow0;
378
+ write_y(out_base{{ ot }}, requant(acc{{ ot }}.x{% if wScalePerChannel %}, out_oc{{ ot }}{% endif %}));
379
+ if (ow0 + 1u < params.outW) { write_y(out_base{{ ot }} + 1u, requant(acc{{ ot }}.y{% if wScalePerChannel %}, out_oc{{ ot }}{% endif %})); }
380
+ if (ow0 + 2u < params.outW) { write_y(out_base{{ ot }} + 2u, requant(acc{{ ot }}.z{% if wScalePerChannel %}, out_oc{{ ot }}{% endif %})); }
381
+ if (ow0 + 3u < params.outW) { write_y(out_base{{ ot }} + 3u, requant(acc{{ ot }}.w{% if wScalePerChannel %}, out_oc{{ ot }}{% endif %})); }
382
+ }
383
+ {% endfor %}
384
+ }
build/webgpu/qlinear-conv-nhwc-oc4.wgsl.jinja ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_scalar_zero(fn_name, buffer_name, is_unsigned, present=true) %}
2
+ fn {{ fn_name }}() -> i32 {
3
+ {% if is_unsigned %}
4
+ return i32({{ buffer_name }}[0]);
5
+ {% else %}
6
+ return {{ buffer_name }}[0];
7
+ {% endif %}
8
+ }{% endmacro %}
9
+ // Channels-last quantized 2-D convolution without integer dot-product support.
10
+ // One invocation owns four adjacent output channels (a vec4 accumulator whose
11
+ // store is one contiguous run in `(N, H, W, C)`) across `OW_TILE` adjacent output
12
+ // columns, so each weight quadruple is reused by every column in the tile. The
13
+ // grouped arm broadcasts one activation across the quadruple; the depthwise arm
14
+ // (one input and one output channel per group) reads four adjacent activation
15
+ // channels instead, which is the same contiguous run as the store.
16
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
17
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
21
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
22
+ {{ env.wgsl.resourceDeclarations }}
23
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
24
+ // and accumulator product using integer significands; no f64 feature required.
25
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
26
+ let low = a.x + b.x;
27
+ let middle = a.y + b.y;
28
+ let with_carry = middle + u32(low < a.x);
29
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
30
+ }
31
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
32
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
33
+ }
34
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
35
+ var q = value;
36
+ var sticky = false;
37
+ var guard = false;
38
+ for (var bit = 0u; bit < shift; bit++) {
39
+ sticky = sticky || guard;
40
+ guard = (q.x & 1u) != 0u;
41
+ q = quant_u96_shr(q);
42
+ }
43
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
44
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
45
+ }
46
+ return q;
47
+ }
48
+ fn quant_float_significand(bits: u32) -> u32 {
49
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
50
+ }
51
+ fn quant_float_exponent(bits: u32) -> i32 {
52
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
53
+ }
54
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
55
+ if (acc == 0) { return 0; }
56
+ let a_bits = bitcast<u32>(a_s);
57
+ let b_bits = bitcast<u32>(b_s);
58
+ let y_bits = bitcast<u32>(y_s);
59
+ let a = quant_float_significand(a_bits);
60
+ let b = quant_float_significand(b_bits);
61
+ let denominator = quant_float_significand(y_bits);
62
+ if (a == 0u || b == 0u) { return 0; }
63
+ // The exact product of two 24-bit significands occupies at most 48 bits.
64
+ let p0 = (a & 65535u) * (b & 65535u);
65
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
66
+ let product_low = p0 + (cross << 16u);
67
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
68
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
69
+ var remainder = 0u;
70
+ var significant_bits = 0u;
71
+ var leading_exponent = 0;
72
+ var significand = vec2<u32>(0u);
73
+ // Long division generates the 53 bits of the binary64 multiplier. The
74
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
75
+ // zero bits precede the first significant bit.
76
+ loop {
77
+ var incoming = 0u;
78
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
79
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
80
+ remainder = (remainder << 1u) | incoming;
81
+ let digit = u32(remainder >= denominator);
82
+ if (digit != 0u) { remainder -= denominator; }
83
+ if (significant_bits != 0u || digit != 0u) {
84
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
85
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
86
+ significant_bits++;
87
+ }
88
+ input_bit--;
89
+ if (significant_bits == 53u) { break; }
90
+ }
91
+ let twice_remainder = remainder << 1u;
92
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
93
+ let low = significand.x + 1u;
94
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
95
+ }
96
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
97
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
98
+ // significand by the exact accumulator into three little-endian u32 limbs.
99
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
100
+ var operand = vec3<u32>(significand, 0u);
101
+ var product = vec3<u32>(0u);
102
+ loop {
103
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
104
+ magnitude >>= 1u;
105
+ if (magnitude == 0u) { break; }
106
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
107
+ }
108
+ var product_top = 31 - i32(countLeadingZeros(product.x));
109
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
110
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
111
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
112
+ if (product_top + scale_exponent < -2) { return 0; }
113
+ let product_shift = u32(max(0, product_top - 52));
114
+ let rounded_product = quant_u96_round_shift(product, product_shift);
115
+ let result_exponent = scale_exponent + i32(product_shift);
116
+ var rounded = rounded_product;
117
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
118
+ else { rounded.x <<= u32(result_exponent); }
119
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
120
+ }
121
+
122
+ // A power-of-two division and an exactly represented <=48-bit product need
123
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
124
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
125
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
126
+ let significand = quant_float_significand(bitcast<u32>(scale));
127
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
128
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
129
+ return integer_bits + scale_bits <= 48u;
130
+ }
131
+ fn quant_normal_expansion_range(v: f32) -> bool {
132
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
133
+ // Reserve two binary32 significands plus four carry/error bits above the
134
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
135
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
136
+ }
137
+
138
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
139
+ let estimate = numerator_hi / denominator;
140
+ // Anything outside this range saturates after adding an 8-bit zero point;
141
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
142
+ if (estimate <= -1024.0) { return -1024; }
143
+ if (estimate >= 1024.0) { return 1024; }
144
+
145
+ let lo = floor(estimate);
146
+ let hi = lo + 1.0;
147
+ let midpoint = lo + 0.5;
148
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
149
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
150
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
151
+ // The high-word subtraction is exact for these similarly sized products
152
+ // (Sterbenz); adding the residual difference therefore preserves its sign
153
+ // even when the rounded high words straddle adjacent f32 values.
154
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
155
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
156
+ // Six FMA/product tails and their additions fit within this conservative
157
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
158
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
159
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
160
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
161
+ }
162
+ if (midpoint_delta < 0.0) {
163
+ return i32(lo);
164
+ }
165
+ if (midpoint_delta > 0.0) {
166
+ return i32(hi);
167
+ }
168
+ let half_lo = floor(lo * 0.5);
169
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
170
+ return i32(select(hi, lo, lo_is_even));
171
+ }
172
+
173
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
174
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
175
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
176
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
177
+ var rounded = 0u;
178
+ if (exponent >= 0) {
179
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
180
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
181
+ } else {
182
+ let shift = u32(-exponent);
183
+ if (shift >= 32u) { return 0; }
184
+ let whole = magnitude >> shift;
185
+ let remainder = magnitude & ((1u << shift) - 1u);
186
+ let half = 1u << (shift - 1u);
187
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
188
+ }
189
+ return select(i32(rounded), -i32(rounded), acc < 0);
190
+ }
191
+
192
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
193
+ if (acc == 0) { return 0; }
194
+ let a_bits = bitcast<u32>(a_s);
195
+ let b_bits = bitcast<u32>(b_s);
196
+ let y_bits = bitcast<u32>(y_s);
197
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
198
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
199
+ return quant_round_power_two(acc, exponent);
200
+ }
201
+ let acc_f = f32(acc);
202
+ // Every i32 high part aligned to 256 is exactly representable in f32.
203
+ // Recover the conversion residual without an out-of-range i32(acc_f)
204
+ // conversion when INT_MAX rounds up to 2^31.
205
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
206
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
207
+ // rounded intermediate cannot move a near-half value onto the tie.
208
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
209
+ let direct_hi = fma(acc_f, a_s, 0.0);
210
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
211
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
212
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
213
+ }
214
+
215
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
216
+ // which side of a rounding midpoint a near-half value occupies.
217
+ let ab_hi = fma(a_s, b_s, 0.0);
218
+ let ab_lo = fma(a_s, b_s, -ab_hi);
219
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
220
+ let product_hi = fma(acc_f, ab_hi, 0.0);
221
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
222
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
223
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
224
+ // Compare the expansion directly. Renormalizing it adds a cancelling
225
+ // subtraction/addition chain that can erase the residual during lowering.
226
+ let denominator_bits = bitcast<u32>(y_s);
227
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
228
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
229
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
230
+ }
231
+
232
+ // A conservative binary32 error filter accepts only a result safely inside one
233
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
234
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
235
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
236
+ if (acc == 0) { return 0; }
237
+ let a_bits = bitcast<u32>(a_s);
238
+ let b_bits = bitcast<u32>(b_s);
239
+ let y_bits = bitcast<u32>(y_s);
240
+ let ea = (a_bits >> 23u) & 255u;
241
+ let eb = (b_bits >> 23u) & 255u;
242
+ let ey = (y_bits >> 23u) & 255u;
243
+ // Certify the range before arithmetic: overflow need not produce infinity.
244
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
245
+ // scale-only bounds cover every accumulator and can be shared across lanes.
246
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
247
+ // below overflow for products, division and the expansion midpoint product.
248
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
249
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
250
+ ea + eb < 180u || ea + eb > 347u ||
251
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
252
+ return quant_round_binary64(acc, a_s, b_s, y_s);
253
+ }
254
+ let scale = fma(a_s, b_s, 0.0);
255
+ let product = fma(f32(acc), scale, 0.0);
256
+ let estimate = product / y_s;
257
+ if (estimate <= -1024.0) { return -1024; }
258
+ if (estimate >= 1024.0) { return 1024; }
259
+ let nearest = round(estimate);
260
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
261
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
262
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
263
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
264
+ {% if device.adapterInfo.vendor != "apple" %}
265
+ // A rounded half-integer estimate can hide which side the exact f32-scale
266
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
267
+ // the compact fast path; exact power-of-two ratios already have a cheap
268
+ // and correct path in round_requantized_precise.
269
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
270
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
271
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
272
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
273
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
274
+ }
275
+ {% endif %}
276
+ return round_requantized_precise(acc, a_s, b_s, y_s);
277
+ }
278
+
279
+ const OW_TILE: u32 = {{ owTile }}u;
280
+
281
+ fn centered_x(i: u32, zp: i32) -> i32 {
282
+ {% if xUnsigned %}
283
+ return i32(x[i]) - zp;
284
+ {% else %}
285
+ return x[i] - zp;
286
+ {% endif %}
287
+ }
288
+
289
+ fn read_w(i: u32) -> i32 {
290
+ {% if wUnsigned %}
291
+ return i32(w[i]);
292
+ {% else %}
293
+ return w[i];
294
+ {% endif %}
295
+ }
296
+
297
+ {{ quant_read_scalar_zero("x_zero", "x_zero_point", xUnsigned) }}
298
+
299
+ fn w_zero({% if wZeroPerChannel %}oc: u32{% endif %}) -> i32 {
300
+ {% if wZeroPerChannel %}
301
+ let i = oc;
302
+ {% else %}
303
+ let i = 0u;
304
+ {% endif %}
305
+ {% if wUnsigned %}
306
+ return i32(w_zero_point[i]);
307
+ {% else %}
308
+ return w_zero_point[i];
309
+ {% endif %}
310
+ }
311
+
312
+ {{ quant_read_scalar_zero("y_zero", "y_zero_point", yUnsigned) }}
313
+
314
+ fn requant(acc: i32{% if wScalePerChannel %}, oc: u32{% endif %}) -> i32 {
315
+ {% if wScalePerChannel %}
316
+ let ws = w_scale[oc];
317
+ {% else %}
318
+ let ws = w_scale[0];
319
+ {% endif %}
320
+ return clamp(round_requantized(acc, x_scale[0], ws, y_scale[0]) + y_zero(), {{ qMin }}, {{ qMax }});
321
+ }
322
+
323
+ fn write_y(i: u32, value: i32) {
324
+ {% if yUnsigned %}
325
+ y[i] = u32(value);
326
+ {% else %}
327
+ y[i] = value;
328
+ {% endif %}
329
+ }
330
+
331
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
332
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
333
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, "flat", guardInline=true) }}
334
+ let ocb = flat % params.ocBlocks;
335
+ var rest = flat / params.ocBlocks;
336
+ let owg = rest % params.owGroups;
337
+ rest = rest / params.owGroups;
338
+ let oh = rest % params.outH;
339
+ let batch = rest / params.outH;
340
+ let oc0 = ocb * 4u;
341
+ let ow0 = owg * OW_TILE;
342
+ // Four adjacent output channels share one group whenever the group holds a
343
+ // multiple of four; the depthwise arm instead maps channel `oc0 + j` to input
344
+ // channel `oc0 + j`, which this same expression produces at unit group widths.
345
+ let in_start = (oc0 / params.outChannelsPerGroup) * params.inChannelsPerGroup;
346
+ let w_stride = params.weightInChannels * params.kernelH * params.kernelW;
347
+ let xzp = x_zero();
348
+ let wzp = vec4<i32>(w_zero({% if wZeroPerChannel %}oc0{% endif %}), w_zero({% if wZeroPerChannel %}oc0 + 1u{% endif %}), w_zero({% if wZeroPerChannel %}oc0 + 2u{% endif %}), w_zero({% if wZeroPerChannel %}oc0 + 3u{% endif %}));
349
+ {% for ot in range(owTile) %}
350
+ var acc{{ ot }} = vec4<i32>(0);
351
+ {% endfor %}
352
+
353
+ for (var kh = 0u; kh < params.kernelH; kh = kh + 1u) {
354
+ let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
355
+ if (ih < 0 || ih >= i32(params.inH)) { continue; }
356
+ let row_base = (batch * params.inH + u32(ih)) * params.inW;
357
+ for (var kw = 0u; kw < params.kernelW; kw = kw + 1u) {
358
+ for (var icg = 0u; icg < params.inChannelsPerGroup; icg = icg + 1u) {
359
+ let wb = ((oc0 * params.weightInChannels + icg) * params.kernelH + kh) * params.kernelW + kw;
360
+ let wv = vec4<i32>(read_w(wb), read_w(wb + w_stride), read_w(wb + 2u * w_stride), read_w(wb + 3u * w_stride)) - wzp;
361
+ {% for ot in range(owTile) %}
362
+ {
363
+ let ow = ow0 + {{ ot }}u;
364
+ let iw = i32(ow * params.strideW + kw * params.dilationW) - params.padW;
365
+ if (ow < params.outW && iw >= 0 && iw < i32(params.inW)) {
366
+ let xb = (row_base + u32(iw)) * params.inChannels + in_start + icg;
367
+ {% if depthwise %}
368
+ let xv = vec4<i32>(centered_x(xb, xzp), centered_x(xb + 1u, xzp), centered_x(xb + 2u, xzp), centered_x(xb + 3u, xzp));
369
+ {% else %}
370
+ let xv = vec4<i32>(centered_x(xb, xzp));
371
+ {% endif %}
372
+ acc{{ ot }} = acc{{ ot }} + xv * wv;
373
+ }
374
+ }
375
+ {% endfor %}
376
+ }
377
+ }
378
+ }
379
+
380
+ {% if hasBias | default(false) %} let bias4 = vec4<i32>(bias[oc0], bias[oc0 + 1u], bias[oc0 + 2u], bias[oc0 + 3u]);
381
+ {% for ot in range(owTile) %} acc{{ ot }} = acc{{ ot }} + bias4;
382
+ {% endfor %}{% endif %}{% for ot in range(owTile) %}
383
+ {
384
+ let ow = ow0 + {{ ot }}u;
385
+ if (ow < params.outW) {
386
+ let ob = ((batch * params.outH + oh) * params.outW + ow) * params.outChannels + oc0;
387
+ write_y(ob, requant(acc{{ ot }}.x{% if wScalePerChannel %}, oc0{% endif %}));
388
+ write_y(ob + 1u, requant(acc{{ ot }}.y{% if wScalePerChannel %}, oc0 + 1u{% endif %}));
389
+ write_y(ob + 2u, requant(acc{{ ot }}.z{% if wScalePerChannel %}, oc0 + 2u{% endif %}));
390
+ write_y(ob + 3u, requant(acc{{ ot }}.w{% if wScalePerChannel %}, oc0 + 3u{% endif %}));
391
+ }
392
+ }
393
+ {% endfor %}
394
+ }
build/webgpu/qlinear-conv-requantize.wgsl.jinja ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_scalar_zero(fn_name, buffer_name, is_unsigned, present=true) %}
2
+ fn {{ fn_name }}() -> i32 {
3
+ {% if is_unsigned %}
4
+ return i32({{ buffer_name }}[0]);
5
+ {% else %}
6
+ return {{ buffer_name }}[0];
7
+ {% endif %}
8
+ }{% endmacro %}
9
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
10
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
11
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
12
+ // per-axis workgroup fold width.
13
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
14
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
15
+ {{ env.wgsl.resourceDeclarations }}
16
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
17
+ // and accumulator product using integer significands; no f64 feature required.
18
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
19
+ let low = a.x + b.x;
20
+ let middle = a.y + b.y;
21
+ let with_carry = middle + u32(low < a.x);
22
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
23
+ }
24
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
25
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
26
+ }
27
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
28
+ var q = value;
29
+ var sticky = false;
30
+ var guard = false;
31
+ for (var bit = 0u; bit < shift; bit++) {
32
+ sticky = sticky || guard;
33
+ guard = (q.x & 1u) != 0u;
34
+ q = quant_u96_shr(q);
35
+ }
36
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
37
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
38
+ }
39
+ return q;
40
+ }
41
+ fn quant_float_significand(bits: u32) -> u32 {
42
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
43
+ }
44
+ fn quant_float_exponent(bits: u32) -> i32 {
45
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
46
+ }
47
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
48
+ if (acc == 0) { return 0; }
49
+ let a_bits = bitcast<u32>(a_s);
50
+ let b_bits = bitcast<u32>(b_s);
51
+ let y_bits = bitcast<u32>(y_s);
52
+ let a = quant_float_significand(a_bits);
53
+ let b = quant_float_significand(b_bits);
54
+ let denominator = quant_float_significand(y_bits);
55
+ if (a == 0u || b == 0u) { return 0; }
56
+ // The exact product of two 24-bit significands occupies at most 48 bits.
57
+ let p0 = (a & 65535u) * (b & 65535u);
58
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
59
+ let product_low = p0 + (cross << 16u);
60
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
61
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
62
+ var remainder = 0u;
63
+ var significant_bits = 0u;
64
+ var leading_exponent = 0;
65
+ var significand = vec2<u32>(0u);
66
+ // Long division generates the 53 bits of the binary64 multiplier. The
67
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
68
+ // zero bits precede the first significant bit.
69
+ loop {
70
+ var incoming = 0u;
71
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
72
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
73
+ remainder = (remainder << 1u) | incoming;
74
+ let digit = u32(remainder >= denominator);
75
+ if (digit != 0u) { remainder -= denominator; }
76
+ if (significant_bits != 0u || digit != 0u) {
77
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
78
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
79
+ significant_bits++;
80
+ }
81
+ input_bit--;
82
+ if (significant_bits == 53u) { break; }
83
+ }
84
+ let twice_remainder = remainder << 1u;
85
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
86
+ let low = significand.x + 1u;
87
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
88
+ }
89
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
90
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
91
+ // significand by the exact accumulator into three little-endian u32 limbs.
92
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
93
+ var operand = vec3<u32>(significand, 0u);
94
+ var product = vec3<u32>(0u);
95
+ loop {
96
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
97
+ magnitude >>= 1u;
98
+ if (magnitude == 0u) { break; }
99
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
100
+ }
101
+ var product_top = 31 - i32(countLeadingZeros(product.x));
102
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
103
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
104
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
105
+ if (product_top + scale_exponent < -2) { return 0; }
106
+ let product_shift = u32(max(0, product_top - 52));
107
+ let rounded_product = quant_u96_round_shift(product, product_shift);
108
+ let result_exponent = scale_exponent + i32(product_shift);
109
+ var rounded = rounded_product;
110
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
111
+ else { rounded.x <<= u32(result_exponent); }
112
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
113
+ }
114
+
115
+ // A power-of-two division and an exactly represented <=48-bit product need
116
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
117
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
118
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
119
+ let significand = quant_float_significand(bitcast<u32>(scale));
120
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
121
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
122
+ return integer_bits + scale_bits <= 48u;
123
+ }
124
+ fn quant_normal_expansion_range(v: f32) -> bool {
125
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
126
+ // Reserve two binary32 significands plus four carry/error bits above the
127
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
128
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
129
+ }
130
+
131
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
132
+ let estimate = numerator_hi / denominator;
133
+ // Anything outside this range saturates after adding an 8-bit zero point;
134
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
135
+ if (estimate <= -1024.0) { return -1024; }
136
+ if (estimate >= 1024.0) { return 1024; }
137
+
138
+ let lo = floor(estimate);
139
+ let hi = lo + 1.0;
140
+ let midpoint = lo + 0.5;
141
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
142
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
143
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
144
+ // The high-word subtraction is exact for these similarly sized products
145
+ // (Sterbenz); adding the residual difference therefore preserves its sign
146
+ // even when the rounded high words straddle adjacent f32 values.
147
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
148
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
149
+ // Six FMA/product tails and their additions fit within this conservative
150
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
151
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
152
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
153
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
154
+ }
155
+ if (midpoint_delta < 0.0) {
156
+ return i32(lo);
157
+ }
158
+ if (midpoint_delta > 0.0) {
159
+ return i32(hi);
160
+ }
161
+ let half_lo = floor(lo * 0.5);
162
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
163
+ return i32(select(hi, lo, lo_is_even));
164
+ }
165
+
166
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
167
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
168
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
169
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
170
+ var rounded = 0u;
171
+ if (exponent >= 0) {
172
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
173
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
174
+ } else {
175
+ let shift = u32(-exponent);
176
+ if (shift >= 32u) { return 0; }
177
+ let whole = magnitude >> shift;
178
+ let remainder = magnitude & ((1u << shift) - 1u);
179
+ let half = 1u << (shift - 1u);
180
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
181
+ }
182
+ return select(i32(rounded), -i32(rounded), acc < 0);
183
+ }
184
+
185
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
186
+ if (acc == 0) { return 0; }
187
+ let a_bits = bitcast<u32>(a_s);
188
+ let b_bits = bitcast<u32>(b_s);
189
+ let y_bits = bitcast<u32>(y_s);
190
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
191
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
192
+ return quant_round_power_two(acc, exponent);
193
+ }
194
+ let acc_f = f32(acc);
195
+ // Every i32 high part aligned to 256 is exactly representable in f32.
196
+ // Recover the conversion residual without an out-of-range i32(acc_f)
197
+ // conversion when INT_MAX rounds up to 2^31.
198
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
199
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
200
+ // rounded intermediate cannot move a near-half value onto the tie.
201
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
202
+ let direct_hi = fma(acc_f, a_s, 0.0);
203
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
204
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
205
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
206
+ }
207
+
208
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
209
+ // which side of a rounding midpoint a near-half value occupies.
210
+ let ab_hi = fma(a_s, b_s, 0.0);
211
+ let ab_lo = fma(a_s, b_s, -ab_hi);
212
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
213
+ let product_hi = fma(acc_f, ab_hi, 0.0);
214
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
215
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
216
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
217
+ // Compare the expansion directly. Renormalizing it adds a cancelling
218
+ // subtraction/addition chain that can erase the residual during lowering.
219
+ let denominator_bits = bitcast<u32>(y_s);
220
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
221
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
222
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
223
+ }
224
+
225
+ // A conservative binary32 error filter accepts only a result safely inside one
226
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
227
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
228
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
229
+ if (acc == 0) { return 0; }
230
+ let a_bits = bitcast<u32>(a_s);
231
+ let b_bits = bitcast<u32>(b_s);
232
+ let y_bits = bitcast<u32>(y_s);
233
+ let ea = (a_bits >> 23u) & 255u;
234
+ let eb = (b_bits >> 23u) & 255u;
235
+ let ey = (y_bits >> 23u) & 255u;
236
+ // Certify the range before arithmetic: overflow need not produce infinity.
237
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
238
+ // scale-only bounds cover every accumulator and can be shared across lanes.
239
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
240
+ // below overflow for products, division and the expansion midpoint product.
241
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
242
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
243
+ ea + eb < 180u || ea + eb > 347u ||
244
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
245
+ return quant_round_binary64(acc, a_s, b_s, y_s);
246
+ }
247
+ let scale = fma(a_s, b_s, 0.0);
248
+ let product = fma(f32(acc), scale, 0.0);
249
+ let estimate = product / y_s;
250
+ if (estimate <= -1024.0) { return -1024; }
251
+ if (estimate >= 1024.0) { return 1024; }
252
+ let nearest = round(estimate);
253
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
254
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
255
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
256
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
257
+ {% if device.adapterInfo.vendor != "apple" %}
258
+ // A rounded half-integer estimate can hide which side the exact f32-scale
259
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
260
+ // the compact fast path; exact power-of-two ratios already have a cheap
261
+ // and correct path in round_requantized_precise.
262
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
263
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
264
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
265
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
266
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
267
+ }
268
+ {% endif %}
269
+ return round_requantized_precise(acc, a_s, b_s, y_s);
270
+ }
271
+
272
+ {{ quant_read_scalar_zero("read_y_zero", "y_zero_point", yUnsigned) }}
273
+
274
+ fn write_y(index: u32, value: i32) {
275
+ {% if yUnsigned %}
276
+ y[index] = u32(value);
277
+ {% else %}
278
+ y[index] = value;
279
+ {% endif %}
280
+ }
281
+
282
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
283
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
284
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, guardInline=true) }}
285
+ {% if hasBias or wScalePerChannel %}
286
+ let oc = (i / params.outImageSize) % params.outChannels;
287
+ {% endif %}
288
+ {% if hasBias %}
289
+ // Optional int32 bias is per output channel, already in (x_scale * w_scale)
290
+ // units, so it is added to the int32 accumulator before requantization.
291
+ let raw = accum[i] + B[oc];
292
+ {% else %}
293
+ let raw = accum[i];
294
+ {% endif %}
295
+ {% if wScalePerChannel %}
296
+ let w_scale_value = w_scale[oc];
297
+ {% else %}
298
+ let w_scale_value = w_scale[0];
299
+ {% endif %}
300
+ let q = clamp(round_requantized(raw, x_scale[0], w_scale_value, y_scale[0]) + read_y_zero(), {{ qMin }}, {{ qMax }});
301
+ write_y(i, q);
302
+ }
build/webgpu/quant-dp4a-matmul.wgsl.jinja ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_scalar_zero(fn_name, buffer_name, is_unsigned, present=true) %}
2
+ fn {{ fn_name }}() -> i32 {
3
+ {% if is_unsigned %}
4
+ return i32({{ buffer_name }}[0]);
5
+ {% else %}
6
+ return {{ buffer_name }}[0];
7
+ {% endif %}
8
+ }{% endmacro %}
9
+ {% macro quant_read_column_zero(fn_name, buffer_name, is_unsigned, present=true, per_column=false, param_name="col", return_type="i32") %}
10
+ fn {{ fn_name }}({% if per_column %}{{ param_name }}: u32{% endif %}) -> {{ return_type }} {
11
+ {% if per_column %}
12
+ {% if is_unsigned %}
13
+ return i32({{ buffer_name }}[{{ param_name }}]);
14
+ {% else %}
15
+ return {{ buffer_name }}[{{ param_name }}];
16
+ {% endif %}
17
+ {% else %}
18
+ {% if is_unsigned %}
19
+ return i32({{ buffer_name }}[0]);
20
+ {% else %}
21
+ return {{ buffer_name }}[0];
22
+ {% endif %}
23
+ {% endif %}
24
+ }{% endmacro %}
25
+ {% if not env.device.wgslLanguageFeatures.has("packed_4x8_integer_dot_product") %}
26
+ // Runtime source-compatible fallbacks for the packed 4x8 integer built-ins used
27
+ // by quantized kernels. Native implementations remain unshadowed when the
28
+ // language feature is present. The user functions retain the built-ins'
29
+ // @must_use contract, but cannot be @const; every call is runtime.
30
+ // Inputs are function parameters, so replacing a built-in call with one of
31
+ // these same-name functions preserves WGSL's single evaluation of each
32
+ // argument. Byte extraction is unsigned. Signed lanes use the exact
33
+ // i32(byte ^ 0x80u) - 128i sign extension, avoiding signed-left-shift overflow
34
+ // while preserving every 8-bit two's-complement value.
35
+ @must_use fn dot4I8Packed(e1: u32, e2: u32) -> i32 {
36
+ let shifts = vec4<u32>(0u, 8u, 16u, 24u);
37
+ let a_bytes = (vec4<u32>(e1) >> shifts) & vec4<u32>(0xffu);
38
+ let b_bytes = (vec4<u32>(e2) >> shifts) & vec4<u32>(0xffu);
39
+ let a = vec4<i32>(a_bytes ^ vec4<u32>(0x80u)) - vec4<i32>(128i);
40
+ let b = vec4<i32>(b_bytes ^ vec4<u32>(0x80u)) - vec4<i32>(128i);
41
+ return dot(a, b);
42
+ }
43
+ {% endif %}
44
+ {{ env.wgsl.resourceDeclarations }}
45
+ {% if requant != "none" %}
46
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
47
+ // and accumulator product using integer significands; no f64 feature required.
48
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
49
+ let low = a.x + b.x;
50
+ let middle = a.y + b.y;
51
+ let with_carry = middle + u32(low < a.x);
52
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
53
+ }
54
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
55
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
56
+ }
57
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
58
+ var q = value;
59
+ var sticky = false;
60
+ var guard = false;
61
+ for (var bit = 0u; bit < shift; bit++) {
62
+ sticky = sticky || guard;
63
+ guard = (q.x & 1u) != 0u;
64
+ q = quant_u96_shr(q);
65
+ }
66
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
67
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
68
+ }
69
+ return q;
70
+ }
71
+ fn quant_float_significand(bits: u32) -> u32 {
72
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
73
+ }
74
+ fn quant_float_exponent(bits: u32) -> i32 {
75
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
76
+ }
77
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
78
+ if (acc == 0) { return 0; }
79
+ let a_bits = bitcast<u32>(a_s);
80
+ let b_bits = bitcast<u32>(b_s);
81
+ let y_bits = bitcast<u32>(y_s);
82
+ let a = quant_float_significand(a_bits);
83
+ let b = quant_float_significand(b_bits);
84
+ let denominator = quant_float_significand(y_bits);
85
+ if (a == 0u || b == 0u) { return 0; }
86
+ // The exact product of two 24-bit significands occupies at most 48 bits.
87
+ let p0 = (a & 65535u) * (b & 65535u);
88
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
89
+ let product_low = p0 + (cross << 16u);
90
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
91
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
92
+ var remainder = 0u;
93
+ var significant_bits = 0u;
94
+ var leading_exponent = 0;
95
+ var significand = vec2<u32>(0u);
96
+ // Long division generates the 53 bits of the binary64 multiplier. The
97
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
98
+ // zero bits precede the first significant bit.
99
+ loop {
100
+ var incoming = 0u;
101
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
102
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
103
+ remainder = (remainder << 1u) | incoming;
104
+ let digit = u32(remainder >= denominator);
105
+ if (digit != 0u) { remainder -= denominator; }
106
+ if (significant_bits != 0u || digit != 0u) {
107
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
108
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
109
+ significant_bits++;
110
+ }
111
+ input_bit--;
112
+ if (significant_bits == 53u) { break; }
113
+ }
114
+ let twice_remainder = remainder << 1u;
115
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
116
+ let low = significand.x + 1u;
117
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
118
+ }
119
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
120
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
121
+ // significand by the exact accumulator into three little-endian u32 limbs.
122
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
123
+ var operand = vec3<u32>(significand, 0u);
124
+ var product = vec3<u32>(0u);
125
+ loop {
126
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
127
+ magnitude >>= 1u;
128
+ if (magnitude == 0u) { break; }
129
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
130
+ }
131
+ var product_top = 31 - i32(countLeadingZeros(product.x));
132
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
133
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
134
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
135
+ if (product_top + scale_exponent < -2) { return 0; }
136
+ let product_shift = u32(max(0, product_top - 52));
137
+ let rounded_product = quant_u96_round_shift(product, product_shift);
138
+ let result_exponent = scale_exponent + i32(product_shift);
139
+ var rounded = rounded_product;
140
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
141
+ else { rounded.x <<= u32(result_exponent); }
142
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
143
+ }
144
+
145
+ // A power-of-two division and an exactly represented <=48-bit product need
146
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
147
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
148
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
149
+ let significand = quant_float_significand(bitcast<u32>(scale));
150
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
151
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
152
+ return integer_bits + scale_bits <= 48u;
153
+ }
154
+ fn quant_normal_expansion_range(v: f32) -> bool {
155
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
156
+ // Reserve two binary32 significands plus four carry/error bits above the
157
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
158
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
159
+ }
160
+
161
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
162
+ let estimate = numerator_hi / denominator;
163
+ // Anything outside this range saturates after adding an 8-bit zero point;
164
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
165
+ if (estimate <= -1024.0) { return -1024; }
166
+ if (estimate >= 1024.0) { return 1024; }
167
+
168
+ let lo = floor(estimate);
169
+ let hi = lo + 1.0;
170
+ let midpoint = lo + 0.5;
171
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
172
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
173
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
174
+ // The high-word subtraction is exact for these similarly sized products
175
+ // (Sterbenz); adding the residual difference therefore preserves its sign
176
+ // even when the rounded high words straddle adjacent f32 values.
177
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
178
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
179
+ // Six FMA/product tails and their additions fit within this conservative
180
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
181
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
182
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
183
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
184
+ }
185
+ if (midpoint_delta < 0.0) {
186
+ return i32(lo);
187
+ }
188
+ if (midpoint_delta > 0.0) {
189
+ return i32(hi);
190
+ }
191
+ let half_lo = floor(lo * 0.5);
192
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
193
+ return i32(select(hi, lo, lo_is_even));
194
+ }
195
+
196
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
197
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
198
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
199
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
200
+ var rounded = 0u;
201
+ if (exponent >= 0) {
202
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
203
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
204
+ } else {
205
+ let shift = u32(-exponent);
206
+ if (shift >= 32u) { return 0; }
207
+ let whole = magnitude >> shift;
208
+ let remainder = magnitude & ((1u << shift) - 1u);
209
+ let half = 1u << (shift - 1u);
210
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
211
+ }
212
+ return select(i32(rounded), -i32(rounded), acc < 0);
213
+ }
214
+
215
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
216
+ if (acc == 0) { return 0; }
217
+ let a_bits = bitcast<u32>(a_s);
218
+ let b_bits = bitcast<u32>(b_s);
219
+ let y_bits = bitcast<u32>(y_s);
220
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
221
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
222
+ return quant_round_power_two(acc, exponent);
223
+ }
224
+ let acc_f = f32(acc);
225
+ // Every i32 high part aligned to 256 is exactly representable in f32.
226
+ // Recover the conversion residual without an out-of-range i32(acc_f)
227
+ // conversion when INT_MAX rounds up to 2^31.
228
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
229
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
230
+ // rounded intermediate cannot move a near-half value onto the tie.
231
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
232
+ let direct_hi = fma(acc_f, a_s, 0.0);
233
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
234
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
235
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
236
+ }
237
+
238
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
239
+ // which side of a rounding midpoint a near-half value occupies.
240
+ let ab_hi = fma(a_s, b_s, 0.0);
241
+ let ab_lo = fma(a_s, b_s, -ab_hi);
242
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
243
+ let product_hi = fma(acc_f, ab_hi, 0.0);
244
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
245
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
246
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
247
+ // Compare the expansion directly. Renormalizing it adds a cancelling
248
+ // subtraction/addition chain that can erase the residual during lowering.
249
+ let denominator_bits = bitcast<u32>(y_s);
250
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
251
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
252
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
253
+ }
254
+
255
+ // A conservative binary32 error filter accepts only a result safely inside one
256
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
257
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
258
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
259
+ if (acc == 0) { return 0; }
260
+ let a_bits = bitcast<u32>(a_s);
261
+ let b_bits = bitcast<u32>(b_s);
262
+ let y_bits = bitcast<u32>(y_s);
263
+ let ea = (a_bits >> 23u) & 255u;
264
+ let eb = (b_bits >> 23u) & 255u;
265
+ let ey = (y_bits >> 23u) & 255u;
266
+ // Certify the range before arithmetic: overflow need not produce infinity.
267
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
268
+ // scale-only bounds cover every accumulator and can be shared across lanes.
269
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
270
+ // below overflow for products, division and the expansion midpoint product.
271
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
272
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
273
+ ea + eb < 180u || ea + eb > 347u ||
274
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
275
+ return quant_round_binary64(acc, a_s, b_s, y_s);
276
+ }
277
+ let scale = fma(a_s, b_s, 0.0);
278
+ let product = fma(f32(acc), scale, 0.0);
279
+ let estimate = product / y_s;
280
+ if (estimate <= -1024.0) { return -1024; }
281
+ if (estimate >= 1024.0) { return 1024; }
282
+ let nearest = round(estimate);
283
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
284
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
285
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
286
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
287
+ {% if device.adapterInfo.vendor != "apple" %}
288
+ // A rounded half-integer estimate can hide which side the exact f32-scale
289
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
290
+ // the compact fast path; exact power-of-two ratios already have a cheap
291
+ // and correct path in round_requantized_precise.
292
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
293
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
294
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
295
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
296
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
297
+ }
298
+ {% endif %}
299
+ return round_requantized_precise(acc, a_s, b_s, y_s);
300
+ }
301
+ {% endif %}
302
+
303
+ const TILE_M: u32 = {{ tileM }}u;
304
+ const TILE_N: u32 = {{ tileN }}u;
305
+ const KT_WORDS: u32 = {{ ktWords }}u;
306
+ const A_TILE_WORDS: u32 = TILE_M * KT_WORDS;
307
+ {% set GEMV_UNROLL = 4 %}
308
+ {% if tileM == 1 %}
309
+ const GEMV_K_UNROLL: u32 = {{ GEMV_UNROLL }}u;
310
+ {% endif %}
311
+
312
+ {% set needsBSum = true %}
313
+ var<workgroup> a_tile: array<u32, A_TILE_WORDS>;
314
+ var<workgroup> a_row_sum: array<atomic<i32>, TILE_M>;
315
+
316
+ // Pack 4 widened A values into one u32 (byte 0 = lowest k).
317
+ fn pack_a(v: vec4<{{ aScalar }}>) -> u32 {
318
+ {% if aUnsigned %}
319
+ // u8 -> biased i8: (u - 128) two's-complement low byte == u ^ 0x80.
320
+ return ((v.x ^ 0x80u) & 0xFFu) | (((v.y ^ 0x80u) & 0xFFu) << 8u) | (((v.z ^ 0x80u) & 0xFFu) << 16u) | (((v.w ^ 0x80u) & 0xFFu) << 24u);
321
+ {% else %}
322
+ return (u32(v.x) & 0xFFu) | ((u32(v.y) & 0xFFu) << 8u) | ((u32(v.z) & 0xFFu) << 16u) | ((u32(v.w) & 0xFFu) << 24u);
323
+ {% endif %}
324
+ }
325
+
326
+ // Sum of the 4 bias-shifted A values of one packed word.
327
+ fn sum4_a(v: vec4<{{ aScalar }}>) -> i32 {
328
+ {% if aUnsigned %}
329
+ return i32(v.x + v.y + v.z + v.w) - 512;
330
+ {% else %}
331
+ return v.x + v.y + v.z + v.w;
332
+ {% endif %}
333
+ }
334
+
335
+ fn pack_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> u32 {
336
+ {% if bUnsigned %}
337
+ return ((b0 ^ 0x80u) & 0xFFu) | (((b1 ^ 0x80u) & 0xFFu) << 8u) | (((b2 ^ 0x80u) & 0xFFu) << 16u) | (((b3 ^ 0x80u) & 0xFFu) << 24u);
338
+ {% else %}
339
+ return (u32(b0) & 0xFFu) | ((u32(b1) & 0xFFu) << 8u) | ((u32(b2) & 0xFFu) << 16u) | ((u32(b3) & 0xFFu) << 24u);
340
+ {% endif %}
341
+ }
342
+
343
+ // Centralize the signed packed-int8 dot expression so unrolled callers cannot
344
+ // collide in lowering-generated temporaries.
345
+ fn dot4_packed(a_word: u32, b_word: u32) -> i32 {
346
+ return dot4I8Packed(a_word, b_word);
347
+ }
348
+
349
+ fn sum4_b(b0: {{ bScalar }}, b1: {{ bScalar }}, b2: {{ bScalar }}, b3: {{ bScalar }}) -> i32 {
350
+ {% if bUnsigned %}
351
+ return i32(b0 + b1 + b2 + b3) - 512;
352
+ {% else %}
353
+ return b0 + b1 + b2 + b3;
354
+ {% endif %}
355
+ }
356
+
357
+ {{ quant_read_column_zero("read_a_zero", "a_zero_point", aUnsigned, per_column=aZeroPerRow, param_name="row") }}
358
+
359
+ {{ quant_read_column_zero("read_b_zero", "b_zero_point", bUnsigned, per_column=bZeroPerColumn) }}
360
+
361
+ {% if requant != "none" %}
362
+ {{ quant_read_scalar_zero("read_y_zero", "y_zero_point", yUnsigned) }}
363
+
364
+ {% endif %}
365
+ @compute @workgroup_size(TILE_N, 1, 1)
366
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
367
+ let batch = wg.z;
368
+ let a_base4 = batch * params.aBatchStride4;
369
+ let b_base = batch * params.bBatchStride;
370
+ let y_base = batch * params.yBatchStride;
371
+ let m_block = wg.y * TILE_M;
372
+ let n_block = wg.x * TILE_N;
373
+ let tid = lid.x;
374
+ let col = n_block + tid;
375
+ let n_valid = col < params.N;
376
+ {% if aPacked == false %}
377
+ // Tail variants keep the hot loop on complete words. Packing the one partial
378
+ // word afterward prevents its bounds checks from infecting every full-word load.
379
+ let k_words_total = params.K / 4u;
380
+ let tail_lanes = params.K - k_words_total * 4u;
381
+ {% else %}
382
+ let k_words_total = params.K / 4u;
383
+ {% endif %}
384
+
385
+ if (tid < TILE_M) {
386
+ atomicStore(&a_row_sum[tid], 0);
387
+ }
388
+ workgroupBarrier();
389
+
390
+ {% if tileM == 1 %}
391
+ var r0: i32 = 0;
392
+ {% for u in range(1, GEMV_UNROLL) %}
393
+ var r0_{{ u }}: i32 = 0;
394
+ {% endfor %}
395
+ {% else %}
396
+ {% for m in range(tileM) %}
397
+ var r{{ m }}: i32 = 0;
398
+ {% endfor %}
399
+ {% endif %}
400
+ var sum_b: i32 = 0;
401
+ {% if tileM == 1 %}
402
+ {% for u in range(1, GEMV_UNROLL) %}
403
+ var sum_b_{{ u }}: i32 = 0;
404
+ {% endfor %}
405
+ {% endif %}
406
+
407
+ for (var kw0 = 0u; kw0 < k_words_total; kw0 = kw0 + KT_WORDS) {
408
+ let words = min(KT_WORDS, k_words_total - kw0);
409
+ // Cooperative vec4 load + pack of the A tile (TILE_M rows x `words` u32).
410
+ for (var i = tid; i < TILE_M * words; i = i + TILE_N) {
411
+ let mi = i / words;
412
+ let ki = i % words;
413
+ let src_m = m_block + mi;
414
+ var packed = 0u;
415
+ if (src_m < params.M) {
416
+ {% if aPacked == false %}
417
+ let k4 = (kw0 + ki) * 4u;
418
+ let a_row = a_base4 + src_m * params.K;
419
+ let v = vec4<{{ aScalar }}>(
420
+ a[a_row + k4],
421
+ a[a_row + k4 + 1u],
422
+ a[a_row + k4 + 2u],
423
+ a[a_row + k4 + 3u],
424
+ );
425
+ {% else %}
426
+ let v = a[a_base4 + src_m * k_words_total + kw0 + ki];
427
+ {% endif %}
428
+ packed = pack_a(v);
429
+ atomicAdd(&a_row_sum[mi], sum4_a(v));
430
+ }
431
+ a_tile[mi * KT_WORDS + ki] = packed;
432
+ }
433
+ workgroupBarrier();
434
+
435
+ if (n_valid) {
436
+ {% if tileM == 1 %}
437
+ // M=1 has only one output accumulator, so a scalar K loop forms a long
438
+ // dependent DP4A chain. Interleave several words while preserving the
439
+ // coalesced mapping of neighboring threads to neighboring B columns.
440
+ let unrolled_words = words - words % GEMV_K_UNROLL;
441
+ for (var kk = 0u; kk < unrolled_words; kk += GEMV_K_UNROLL) {
442
+ {% for u in range(GEMV_UNROLL) %}
443
+ let k4_{{ u }} = (kw0 + kk + {{ u }}u) * 4u;
444
+ let b_row_{{ u }} = b_base + k4_{{ u }} * params.N + col;
445
+ let b0_{{ u }} = b[b_row_{{ u }}];
446
+ let b1_{{ u }} = b[b_row_{{ u }} + params.N];
447
+ let b2_{{ u }} = b[b_row_{{ u }} + 2u * params.N];
448
+ let b3_{{ u }} = b[b_row_{{ u }} + 3u * params.N];
449
+ sum_b{{ "" if u == 0 else "_" ~ u }} += sum4_b(b0_{{ u }}, b1_{{ u }}, b2_{{ u }}, b3_{{ u }});
450
+ let bp_{{ u }} = pack_b(b0_{{ u }}, b1_{{ u }}, b2_{{ u }}, b3_{{ u }});
451
+ r0{{ "" if u == 0 else "_" ~ u }} += dot4_packed(a_tile[kk + {{ u }}u], bp_{{ u }});
452
+ {% endfor %}
453
+ }
454
+ for (var kk = unrolled_words; kk < words; kk++) {
455
+ let k4 = (kw0 + kk) * 4u;
456
+ let b_row = b_base + k4 * params.N + col;
457
+ let b0 = b[b_row];
458
+ let b1 = b[b_row + params.N];
459
+ let b2 = b[b_row + 2u * params.N];
460
+ let b3 = b[b_row + 3u * params.N];
461
+ sum_b += sum4_b(b0, b1, b2, b3);
462
+ let bp = pack_b(b0, b1, b2, b3);
463
+ r0 += dot4_packed(a_tile[kk], bp);
464
+ }
465
+ {% else %}
466
+ for (var kk = 0u; kk < words; kk = kk + 1u) {
467
+ let k4 = (kw0 + kk) * 4u;
468
+ let b_row = b_base + k4 * params.N + col;
469
+ let b0 = b[b_row];
470
+ let b1 = b[b_row + params.N];
471
+ let b2 = b[b_row + 2u * params.N];
472
+ let b3 = b[b_row + 3u * params.N];
473
+ sum_b = sum_b + sum4_b(b0, b1, b2, b3);
474
+ let bp = pack_b(b0, b1, b2, b3);
475
+ {% for m in range(tileM) %}
476
+ r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS + kk], bp);
477
+ {% endfor %}
478
+ }
479
+ {% endif %}
480
+ }
481
+ workgroupBarrier();
482
+ }
483
+ {% if aPacked == false %}
484
+
485
+ // Exactly one uniform guarded word handles K % 4. Missing lanes use the raw
486
+ // bias-domain zero (128 for u8, 0 for i8), so their shifted values, sums, and
487
+ // dot-product contributions are all zero while params.K remains unpadded for
488
+ // the zero-point correction below.
489
+ if (tail_lanes != 0u) {
490
+ if (tid < TILE_M) {
491
+ let src_m = m_block + tid;
492
+ var packed = 0u;
493
+ if (src_m < params.M) {
494
+ let k4 = k_words_total * 4u;
495
+ let a_row = a_base4 + src_m * params.K;
496
+ let a0 = a[a_row + k4];
497
+ var a1: {{ aScalar }} = {{ "128u" if aUnsigned else "0" }};
498
+ var a2: {{ aScalar }} = {{ "128u" if aUnsigned else "0" }};
499
+ if (tail_lanes > 1u) { a1 = a[a_row + k4 + 1u]; }
500
+ if (tail_lanes > 2u) { a2 = a[a_row + k4 + 2u]; }
501
+ let a3: {{ aScalar }} = {{ "128u" if aUnsigned else "0" }};
502
+ let v = vec4<{{ aScalar }}>(a0, a1, a2, a3);
503
+ packed = pack_a(v);
504
+ atomicAdd(&a_row_sum[tid], sum4_a(v));
505
+ }
506
+ a_tile[tid * KT_WORDS] = packed;
507
+ }
508
+ workgroupBarrier();
509
+
510
+ if (n_valid) {
511
+ let k4 = k_words_total * 4u;
512
+ let b_row = b_base + k4 * params.N + col;
513
+ let b0 = b[b_row];
514
+ var b1: {{ bScalar }} = {{ "128u" if bUnsigned else "0" }};
515
+ var b2: {{ bScalar }} = {{ "128u" if bUnsigned else "0" }};
516
+ if (tail_lanes > 1u) { b1 = b[b_row + params.N]; }
517
+ if (tail_lanes > 2u) { b2 = b[b_row + 2u * params.N]; }
518
+ let b3: {{ bScalar }} = {{ "128u" if bUnsigned else "0" }};
519
+ sum_b = sum_b + sum4_b(b0, b1, b2, b3);
520
+ let bp = pack_b(b0, b1, b2, b3);
521
+ {% for m in range(tileM) %}
522
+ r{{ m }} = r{{ m }} + dot4_packed(a_tile[{{ m }}u * KT_WORDS], bp);
523
+ {% endfor %}
524
+ }
525
+ workgroupBarrier();
526
+ }
527
+
528
+ {% endif %}
529
+ if (!n_valid) {
530
+ return;
531
+ }
532
+
533
+ // Fold bias + zero-point corrections (exact in modular i32 arithmetic).
534
+ let cb: i32 = {{ "128" if bUnsigned else "0" }} - read_b_zero({% if bZeroPerColumn %}col{% endif %});
535
+ {% if not aZeroPerRow %}
536
+ let ca: i32 = {{ "128" if aUnsigned else "0" }} - read_a_zero();
537
+ let kcc: i32 = i32(params.K) * ca * cb;
538
+ {% endif %}
539
+ {% if needsBSum and tileM == 1 %}
540
+ let sum_b_total = sum_b{% for u in range(1, GEMV_UNROLL) %} + sum_b_{{ u }}{% endfor %};
541
+ {% endif %}
542
+
543
+ {% for m in range(tileM) %}
544
+ {
545
+ let mr = m_block + {{ m }}u;
546
+ if (mr < params.M) {
547
+ {% if aZeroPerRow %}
548
+ let ca: i32 = {{ "128" if aUnsigned else "0" }} - read_a_zero(mr);
549
+ let kcc: i32 = i32(params.K) * ca * cb;
550
+ {% endif %}
551
+ var acc = r{{ m }}{% if tileM == 1 %}{% for u in range(1, GEMV_UNROLL) %} + r0_{{ u }}{% endfor %}{% endif %};
552
+ acc = acc + cb * atomicLoad(&a_row_sum[{{ m }}u]);
553
+ acc = acc + ca * {% if tileM == 1 %}sum_b_total{% else %}sum_b{% endif %};
554
+ acc = acc + kcc;
555
+ {% if hasBias %}
556
+ acc = acc + bias[{{ "col" if biasPerColumn is defined and biasPerColumn else "mr" }}];
557
+ {% endif %}
558
+ {% if requant == "none" %}
559
+ y[y_base + mr * params.N + col] = acc;
560
+ {% else %}
561
+ {% set scale_index = "col" if requant == "per_column" else ("mr" if requant == "per_row" else "0u") %}
562
+ let rounded = round_requantized(acc, f32(a_scale[0]), f32(b_scale[{{ scale_index }}]), f32(y_scale[0]));
563
+ let q = clamp(rounded + read_y_zero(), {{ qMin }}, {{ qMax }});
564
+ {% if yUnsigned %}
565
+ y[y_base + mr * params.N + col] = u32(q);
566
+ {% else %}
567
+ y[y_base + mr * params.N + col] = q;
568
+ {% endif %}
569
+ {% endif %}
570
+ }
571
+ }
572
+ {% endfor %}
573
+ }
build/webgpu/quant-exact-matrix.wgsl.jinja ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set precastOperands = precastOperands | default(false) %}
2
+ {% set precastScalar = precastScalar | default("f32") %}
3
+ enable subgroups;
4
+ {% if pinSubgroupSize32 %}
5
+ enable subgroup_size_control;
6
+ {% endif %}
7
+ enable chromium_experimental_subgroup_matrix;
8
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
9
+ {{ env.wgsl.resourceDeclarations }}
10
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
11
+ // and accumulator product using integer significands; no f64 feature required.
12
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
13
+ let low = a.x + b.x;
14
+ let middle = a.y + b.y;
15
+ let with_carry = middle + u32(low < a.x);
16
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
17
+ }
18
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
19
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
20
+ }
21
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
22
+ var q = value;
23
+ var sticky = false;
24
+ var guard = false;
25
+ for (var bit = 0u; bit < shift; bit++) {
26
+ sticky = sticky || guard;
27
+ guard = (q.x & 1u) != 0u;
28
+ q = quant_u96_shr(q);
29
+ }
30
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
31
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
32
+ }
33
+ return q;
34
+ }
35
+ fn quant_float_significand(bits: u32) -> u32 {
36
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
37
+ }
38
+ fn quant_float_exponent(bits: u32) -> i32 {
39
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
40
+ }
41
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
42
+ if (acc == 0) { return 0; }
43
+ let a_bits = bitcast<u32>(a_s);
44
+ let b_bits = bitcast<u32>(b_s);
45
+ let y_bits = bitcast<u32>(y_s);
46
+ let a = quant_float_significand(a_bits);
47
+ let b = quant_float_significand(b_bits);
48
+ let denominator = quant_float_significand(y_bits);
49
+ if (a == 0u || b == 0u) { return 0; }
50
+ // The exact product of two 24-bit significands occupies at most 48 bits.
51
+ let p0 = (a & 65535u) * (b & 65535u);
52
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
53
+ let product_low = p0 + (cross << 16u);
54
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
55
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
56
+ var remainder = 0u;
57
+ var significant_bits = 0u;
58
+ var leading_exponent = 0;
59
+ var significand = vec2<u32>(0u);
60
+ // Long division generates the 53 bits of the binary64 multiplier. The
61
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
62
+ // zero bits precede the first significant bit.
63
+ loop {
64
+ var incoming = 0u;
65
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
66
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
67
+ remainder = (remainder << 1u) | incoming;
68
+ let digit = u32(remainder >= denominator);
69
+ if (digit != 0u) { remainder -= denominator; }
70
+ if (significant_bits != 0u || digit != 0u) {
71
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
72
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
73
+ significant_bits++;
74
+ }
75
+ input_bit--;
76
+ if (significant_bits == 53u) { break; }
77
+ }
78
+ let twice_remainder = remainder << 1u;
79
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
80
+ let low = significand.x + 1u;
81
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
82
+ }
83
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
84
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
85
+ // significand by the exact accumulator into three little-endian u32 limbs.
86
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
87
+ var operand = vec3<u32>(significand, 0u);
88
+ var product = vec3<u32>(0u);
89
+ loop {
90
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
91
+ magnitude >>= 1u;
92
+ if (magnitude == 0u) { break; }
93
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
94
+ }
95
+ var product_top = 31 - i32(countLeadingZeros(product.x));
96
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
97
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
98
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
99
+ if (product_top + scale_exponent < -2) { return 0; }
100
+ let product_shift = u32(max(0, product_top - 52));
101
+ let rounded_product = quant_u96_round_shift(product, product_shift);
102
+ let result_exponent = scale_exponent + i32(product_shift);
103
+ var rounded = rounded_product;
104
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
105
+ else { rounded.x <<= u32(result_exponent); }
106
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
107
+ }
108
+
109
+ // A power-of-two division and an exactly represented <=48-bit product need
110
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
111
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
112
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
113
+ let significand = quant_float_significand(bitcast<u32>(scale));
114
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
115
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
116
+ return integer_bits + scale_bits <= 48u;
117
+ }
118
+ fn quant_normal_expansion_range(v: f32) -> bool {
119
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
120
+ // Reserve two binary32 significands plus four carry/error bits above the
121
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
122
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
123
+ }
124
+
125
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
126
+ let estimate = numerator_hi / denominator;
127
+ // Anything outside this range saturates after adding an 8-bit zero point;
128
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
129
+ if (estimate <= -1024.0) { return -1024; }
130
+ if (estimate >= 1024.0) { return 1024; }
131
+
132
+ let lo = floor(estimate);
133
+ let hi = lo + 1.0;
134
+ let midpoint = lo + 0.5;
135
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
136
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
137
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
138
+ // The high-word subtraction is exact for these similarly sized products
139
+ // (Sterbenz); adding the residual difference therefore preserves its sign
140
+ // even when the rounded high words straddle adjacent f32 values.
141
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
142
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
143
+ // Six FMA/product tails and their additions fit within this conservative
144
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
145
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
146
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
147
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
148
+ }
149
+ if (midpoint_delta < 0.0) {
150
+ return i32(lo);
151
+ }
152
+ if (midpoint_delta > 0.0) {
153
+ return i32(hi);
154
+ }
155
+ let half_lo = floor(lo * 0.5);
156
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
157
+ return i32(select(hi, lo, lo_is_even));
158
+ }
159
+
160
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
161
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
162
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
163
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
164
+ var rounded = 0u;
165
+ if (exponent >= 0) {
166
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
167
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
168
+ } else {
169
+ let shift = u32(-exponent);
170
+ if (shift >= 32u) { return 0; }
171
+ let whole = magnitude >> shift;
172
+ let remainder = magnitude & ((1u << shift) - 1u);
173
+ let half = 1u << (shift - 1u);
174
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
175
+ }
176
+ return select(i32(rounded), -i32(rounded), acc < 0);
177
+ }
178
+
179
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
180
+ if (acc == 0) { return 0; }
181
+ let a_bits = bitcast<u32>(a_s);
182
+ let b_bits = bitcast<u32>(b_s);
183
+ let y_bits = bitcast<u32>(y_s);
184
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
185
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
186
+ return quant_round_power_two(acc, exponent);
187
+ }
188
+ let acc_f = f32(acc);
189
+ // Every i32 high part aligned to 256 is exactly representable in f32.
190
+ // Recover the conversion residual without an out-of-range i32(acc_f)
191
+ // conversion when INT_MAX rounds up to 2^31.
192
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
193
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
194
+ // rounded intermediate cannot move a near-half value onto the tie.
195
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
196
+ let direct_hi = fma(acc_f, a_s, 0.0);
197
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
198
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
199
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
200
+ }
201
+
202
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
203
+ // which side of a rounding midpoint a near-half value occupies.
204
+ let ab_hi = fma(a_s, b_s, 0.0);
205
+ let ab_lo = fma(a_s, b_s, -ab_hi);
206
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
207
+ let product_hi = fma(acc_f, ab_hi, 0.0);
208
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
209
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
210
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
211
+ // Compare the expansion directly. Renormalizing it adds a cancelling
212
+ // subtraction/addition chain that can erase the residual during lowering.
213
+ let denominator_bits = bitcast<u32>(y_s);
214
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
215
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
216
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
217
+ }
218
+
219
+ // A conservative binary32 error filter accepts only a result safely inside one
220
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
221
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
222
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
223
+ if (acc == 0) { return 0; }
224
+ let a_bits = bitcast<u32>(a_s);
225
+ let b_bits = bitcast<u32>(b_s);
226
+ let y_bits = bitcast<u32>(y_s);
227
+ let ea = (a_bits >> 23u) & 255u;
228
+ let eb = (b_bits >> 23u) & 255u;
229
+ let ey = (y_bits >> 23u) & 255u;
230
+ // Certify the range before arithmetic: overflow need not produce infinity.
231
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
232
+ // scale-only bounds cover every accumulator and can be shared across lanes.
233
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
234
+ // below overflow for products, division and the expansion midpoint product.
235
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
236
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
237
+ ea + eb < 180u || ea + eb > 347u ||
238
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
239
+ return quant_round_binary64(acc, a_s, b_s, y_s);
240
+ }
241
+ let scale = fma(a_s, b_s, 0.0);
242
+ let product = fma(f32(acc), scale, 0.0);
243
+ let estimate = product / y_s;
244
+ if (estimate <= -1024.0) { return -1024; }
245
+ if (estimate >= 1024.0) { return 1024; }
246
+ let nearest = round(estimate);
247
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
248
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
249
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
250
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
251
+ {% if device.adapterInfo.vendor != "apple" %}
252
+ // A rounded half-integer estimate can hide which side the exact f32-scale
253
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
254
+ // the compact fast path; exact power-of-two ratios already have a cheap
255
+ // and correct path in round_requantized_precise.
256
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
257
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
258
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
259
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
260
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
261
+ }
262
+ {% endif %}
263
+ return round_requantized_precise(acc, a_s, b_s, y_s);
264
+ }
265
+
266
+ // Adjusted byte integers fit exactly in f32. Each bounded f32 dot product
267
+ // has absolute magnitude at most 2^24; int32 adds then preserve wraparound.
268
+ const TILE_M: u32 = {{ sgmatTileM }}u;
269
+ const TILE_N: u32 = {{ sgmatTileN }}u;
270
+ const TILE_K: u32 = {{ sgmatTileK }}u;
271
+
272
+ {% if precastOperands %}
273
+ const PADDED_K: u32 = {{ preparedK }}u;
274
+ const PADDED_N: u32 = {{ preparedN }}u;
275
+ var<workgroup> matrix_out: array<f32, {{ sgmatSubgroups * 8 * 64 }}>;
276
+ {% else %}
277
+ var<workgroup> tile_A: array<f32, {{ sgmatTileM * sgmatTileK }}>;
278
+ var<workgroup> tile_B: array<f32, {{ sgmatTileK * sgmatTileN }}>;
279
+ {% if not sgmatReuseStaging %}
280
+ var<workgroup> scratch: array<array<array<f32, 64>, 4>, {{ sgmatSubgroups }}>;
281
+ {% endif %}
282
+ {% endif %}
283
+
284
+ @compute @workgroup_size({{ sgmatWorkgroup }}){{ " @subgroup_size(32)" if pinSubgroupSize32 else "" }}
285
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
286
+ {% if not precastOperands or not matrixSubgroupId %}
287
+ @builtin(local_invocation_index) tid: u32,
288
+ {% endif %}
289
+ @builtin(subgroup_invocation_id) lane: u32,
290
+ @builtin(subgroup_id) subgroup_index: u32) {
291
+ let subgroup = subgroup_index;
292
+ let base_A = (subgroup % {{ sgmatRowGroups }}u) * 16u;
293
+ let base_B = (subgroup / {{ sgmatRowGroups }}u) * 32u;
294
+ let row = lane / 4u;
295
+ let col = (lane % 4u) * 2u;
296
+ // Bound adjusted byte magnitudes from scalar zero points. A column-wise
297
+ // B zero point keeps the full byte-difference bound for the whole tile.
298
+ let max_a = {% if matrixZeroPerRow %}255u{% else %}u32(max(abs({{ 0 if aUnsigned else (0 - 128) }} - i32(a_zero_point[0])), abs({{ 255 if aUnsigned else 127 }} - i32(a_zero_point[0])))){% endif %};
299
+ let max_b = {% if hasBZero and not bZeroPerColumn %}u32(max(abs({{ 0 if bUnsigned else (0 - 128) }} - i32(b_zero_point[0])), abs({{ 255 if bUnsigned else 127 }} - i32(b_zero_point[0])))){% else %}{{ 255 if hasBZero or bUnsigned else 128 }}u{% endif %};
300
+ let partial_k = (16777216u / (max_a * max_b) / TILE_K) * TILE_K;
301
+ var totals: array<i32, 16>;
302
+ for (var partial = 0u; partial < params.K; partial += partial_k) {
303
+ {% for r in range(2) %}{% for c in range(4) %}
304
+ var matC{{ r }}{{ c }}: subgroup_matrix_result<f32, 8, 8>;
305
+ {% endfor %}{% endfor %}
306
+ let partial_end = min(params.K, partial + partial_k);
307
+ for (var kb = partial; kb < partial_end; kb += TILE_K) {
308
+ {% if not precastOperands %}
309
+ for (var index = tid; index < TILE_M * TILE_K; index += {{ sgmatWorkgroup }}u) {
310
+ let m = wid.y * TILE_M + index / TILE_K;
311
+ let k = kb + index % TILE_K;
312
+ var value = f32(0.0);
313
+ if (m < params.M && k < params.K) {
314
+ value = f32(i32(a[m * params.K + k]) - i32(a_zero_point[{{ "m" if matrixZeroPerRow else "0" }}]));
315
+ }
316
+ tile_A[index] = value;
317
+ }
318
+ for (var index = tid; index < TILE_K * TILE_N; index += {{ sgmatWorkgroup }}u) {
319
+ let k = kb + index / TILE_N;
320
+ let n = wid.x * TILE_N + index % TILE_N;
321
+ var value = f32(0.0);
322
+ if (k < params.K && n < params.N) {
323
+ value = f32(i32(b[{% if matrixBatchB %}wid.z * params.bBatchStride + {% endif %}k * params.N + n]) - i32(b_zero_point[{{ "n" if bZeroPerColumn else "0" }}]));
324
+ }
325
+ tile_B[index] = value;
326
+ }
327
+ workgroupBarrier();
328
+ {% endif %}
329
+ for (var step = 0u; step < TILE_K; step += 8u) {
330
+ {% for r in range(2) %}
331
+ let matA{{ r }} = subgroupMatrixLoad<subgroup_matrix_left<{{ precastScalar if precastOperands else "f32" }}, 8, 8>, row_major>(&{{ "a" if precastOperands else "tile_A" }}, {% if precastOperands %}(wid.y * TILE_M + base_A + {{ r * 8 }}u) * PADDED_K + kb + step{% else %}(base_A + {{ r * 8 }}u) * TILE_K + step{% endif %}, {{ "PADDED_K" if precastOperands else "TILE_K" }});
332
+ {% endfor %}
333
+ {% for c in range(4) %}
334
+ let matB{{ c }} = subgroupMatrixLoad<subgroup_matrix_right<{{ precastScalar if precastOperands else "f32" }}, 8, 8>, row_major>(&{{ "b" if precastOperands else "tile_B" }}, {% if precastOperands %}{% if matrixBatchB %}wid.z * PADDED_K * PADDED_N + {% endif %}(kb + step) * PADDED_N + wid.x * TILE_N + base_B + {{ c * 8 }}u{% else %}step * TILE_N + base_B + {{ c * 8 }}u{% endif %}, {{ "PADDED_N" if precastOperands else "TILE_N" }});
335
+ {% endfor %}
336
+ {% for r in range(2) %}{% for c in range(4) %}
337
+ matC{{ r }}{{ c }} = subgroupMatrixMultiplyAccumulate(matA{{ r }}, matB{{ c }}, matC{{ r }}{{ c }});
338
+ {% endfor %}{% endfor %}
339
+ }
340
+ {% if not precastOperands %}
341
+ workgroupBarrier();
342
+ {% endif %}
343
+ }
344
+ {% if precastOperands %}
345
+ // The prepared operands need no staging barriers. Publish all eight
346
+ // matrices together before converting the bounded partial to int32.
347
+ {% for r in range(2) %}{% for c in range(4) %}
348
+ subgroupMatrixStore<row_major>(&matrix_out, subgroup * 512u + {{ r * 256 + c * 64 }}u, matC{{ r }}{{ c }}, 8u);
349
+ {% endfor %}{% endfor %}
350
+ workgroupBarrier();
351
+ {% for r in range(2) %}{% for c in range(4) %}{% for h in range(2) %}
352
+ totals[{{ r * 8 + c * 2 + h }}] += i32(matrix_out[subgroup * 512u + {{ r * 256 + c * 64 }}u + row * 8u + col + {{ h }}u]);
353
+ {% endfor %}{% endfor %}{% endfor %}
354
+ workgroupBarrier();
355
+ {% else %}
356
+ {% if sgmatReuseStaging %}
357
+ // All staged A reads have completed. Reuse its storage for one result band
358
+ // at a time: each subgroup needs 4 * 64 floats, exactly its A-tile share.
359
+ {% endif %}
360
+ {% for r in range(2) %}
361
+ {% for c in range(4) %}
362
+ subgroupMatrixStore<row_major>({% if sgmatReuseStaging %}&tile_A{% else %}&scratch[subgroup][{{ c }}]{% endif %}, {% if sgmatReuseStaging %}subgroup * 256u + {{ c * 64 }}u{% else %}0u{% endif %}, matC{{ r }}{{ c }}, 8u);
363
+ {% endfor %}
364
+ workgroupBarrier();
365
+ {% for c in range(4) %}{% for h in range(2) %}
366
+ totals[{{ r * 8 + c * 2 + h }}] += i32({% if sgmatReuseStaging %}tile_A[subgroup * 256u + {{ c * 64 }}u + row * 8u + col + {{ h }}u]{% else %}scratch[subgroup][{{ c }}][row * 8u + col + {{ h }}u]{% endif %});
367
+ {% endfor %}{% endfor %}
368
+ workgroupBarrier();
369
+ {% endfor %}
370
+ {% endif %}
371
+ }
372
+ {% for r in range(2) %}{% for c in range(4) %}{% for h in range(2) %}
373
+ {
374
+ let m = wid.y * TILE_M + base_A + {{ r * 8 }}u + row;
375
+ let n = wid.x * TILE_N + base_B + {{ c * 8 + h }}u + col;
376
+ if (m < params.M && n < params.N) {
377
+ let acc = totals[{{ r * 8 + c * 2 + h }}]{% if matrixHasBias %} + bias[{{ "m" if matrixBiasPerRow else "n" }}]{% endif %};
378
+ let rounded = round_requantized(acc, f32(a_scale[0]), f32(b_scale[{{ "m" if matrixScalePerRow else ("n" if bScalePerColumn else "0") }}]), f32(y_scale[0]));
379
+ let q = clamp(rounded + i32(y_zero_point[0]), {{ qMin }}, {{ qMax }});
380
+ y[{% if matrixBatchB %}wid.z * params.yBatchStride + {% endif %}m * params.N + n] = {{ "u32(q)" if yUnsigned else "q" }};
381
+ }
382
+ }
383
+ {% endfor %}{% endfor %}{% endfor %}
384
+ }
build/webgpu/quant-exact-portable.wgsl.jinja ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set integerStoreRow = matrixScalePerRow or (matrixHasBias and matrixBiasPerRow) %}
2
+ {% set integerStoreColumn = (requantize and bScalePerColumn) or (matrixHasBias and not matrixBiasPerRow) %}
3
+ // Rare-boundary fallback: reproduce the reference's binary64 scale division
4
+ // and accumulator product using integer significands; no f64 feature required.
5
+ fn quant_u96_add(a: vec3<u32>, b: vec3<u32>) -> vec3<u32> {
6
+ let low = a.x + b.x;
7
+ let middle = a.y + b.y;
8
+ let with_carry = middle + u32(low < a.x);
9
+ return vec3<u32>(low, with_carry, a.z + b.z + u32(middle < a.y) + u32(with_carry < middle));
10
+ }
11
+ fn quant_u96_shr(a: vec3<u32>) -> vec3<u32> {
12
+ return vec3<u32>((a.x >> 1u) | (a.y << 31u), (a.y >> 1u) | (a.z << 31u), a.z >> 1u);
13
+ }
14
+ fn quant_u96_round_shift(value: vec3<u32>, shift: u32) -> vec3<u32> {
15
+ var q = value;
16
+ var sticky = false;
17
+ var guard = false;
18
+ for (var bit = 0u; bit < shift; bit++) {
19
+ sticky = sticky || guard;
20
+ guard = (q.x & 1u) != 0u;
21
+ q = quant_u96_shr(q);
22
+ }
23
+ if (guard && (sticky || (q.x & 1u) != 0u)) {
24
+ q = quant_u96_add(q, vec3<u32>(1u, 0u, 0u));
25
+ }
26
+ return q;
27
+ }
28
+ fn quant_float_significand(bits: u32) -> u32 {
29
+ return (bits & 0x7fffffu) | select(0x800000u, 0u, (bits & 0x7f800000u) == 0u);
30
+ }
31
+ fn quant_float_exponent(bits: u32) -> i32 {
32
+ return max(1, i32((bits >> 23u) & 255u)) - 150;
33
+ }
34
+ fn quant_round_binary64(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
35
+ if (acc == 0) { return 0; }
36
+ let a_bits = bitcast<u32>(a_s);
37
+ let b_bits = bitcast<u32>(b_s);
38
+ let y_bits = bitcast<u32>(y_s);
39
+ let a = quant_float_significand(a_bits);
40
+ let b = quant_float_significand(b_bits);
41
+ let denominator = quant_float_significand(y_bits);
42
+ if (a == 0u || b == 0u) { return 0; }
43
+ // The exact product of two 24-bit significands occupies at most 48 bits.
44
+ let p0 = (a & 65535u) * (b & 65535u);
45
+ let cross = (a >> 16u) * (b & 65535u) + (b >> 16u) * (a & 65535u);
46
+ let product_low = p0 + (cross << 16u);
47
+ let product_high = (a >> 16u) * (b >> 16u) + (cross >> 16u) + u32(product_low < p0);
48
+ var input_bit = select(31 - i32(countLeadingZeros(product_low)), 63 - i32(countLeadingZeros(product_high)), product_high != 0u);
49
+ var remainder = 0u;
50
+ var significant_bits = 0u;
51
+ var leading_exponent = 0;
52
+ var significand = vec2<u32>(0u);
53
+ // Long division generates the 53 bits of the binary64 multiplier. The
54
+ // remainder fits in 25 bits, including its left shift. At most 24 leading
55
+ // zero bits precede the first significant bit.
56
+ loop {
57
+ var incoming = 0u;
58
+ if (input_bit >= 32) { incoming = (product_high >> u32(input_bit - 32)) & 1u; }
59
+ else if (input_bit >= 0) { incoming = (product_low >> u32(input_bit)) & 1u; }
60
+ remainder = (remainder << 1u) | incoming;
61
+ let digit = u32(remainder >= denominator);
62
+ if (digit != 0u) { remainder -= denominator; }
63
+ if (significant_bits != 0u || digit != 0u) {
64
+ if (significant_bits == 0u) { leading_exponent = input_bit; }
65
+ significand = vec2<u32>((significand.x << 1u) | digit, (significand.y << 1u) | (significand.x >> 31u));
66
+ significant_bits++;
67
+ }
68
+ input_bit--;
69
+ if (significant_bits == 53u) { break; }
70
+ }
71
+ let twice_remainder = remainder << 1u;
72
+ if (twice_remainder > denominator || (twice_remainder == denominator && (significand.x & 1u) != 0u)) {
73
+ let low = significand.x + 1u;
74
+ significand = vec2<u32>(low, significand.y + u32(low == 0u));
75
+ }
76
+ let scale_exponent = quant_float_exponent(a_bits) + quant_float_exponent(b_bits) - quant_float_exponent(y_bits) + leading_exponent - 52;
77
+ // abs(INT_MIN) is representable as u32. Multiply the rounded multiplier's
78
+ // significand by the exact accumulator into three little-endian u32 limbs.
79
+ var magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
80
+ var operand = vec3<u32>(significand, 0u);
81
+ var product = vec3<u32>(0u);
82
+ loop {
83
+ if ((magnitude & 1u) != 0u) { product = quant_u96_add(product, operand); }
84
+ magnitude >>= 1u;
85
+ if (magnitude == 0u) { break; }
86
+ operand = vec3<u32>(operand.x << 1u, (operand.y << 1u) | (operand.x >> 31u), (operand.z << 1u) | (operand.y >> 31u));
87
+ }
88
+ var product_top = 31 - i32(countLeadingZeros(product.x));
89
+ if (product.y != 0u) { product_top = 63 - i32(countLeadingZeros(product.y)); }
90
+ if (product.z != 0u) { product_top = 95 - i32(countLeadingZeros(product.z)); }
91
+ if (product_top + scale_exponent >= 10) { return select(1024, -1024, acc < 0); }
92
+ if (product_top + scale_exponent < -2) { return 0; }
93
+ let product_shift = u32(max(0, product_top - 52));
94
+ let rounded_product = quant_u96_round_shift(product, product_shift);
95
+ let result_exponent = scale_exponent + i32(product_shift);
96
+ var rounded = rounded_product;
97
+ if (result_exponent < 0) { rounded = quant_u96_round_shift(rounded_product, u32(-result_exponent)); }
98
+ else { rounded.x <<= u32(result_exponent); }
99
+ return select(i32(rounded.x), -i32(rounded.x), acc < 0);
100
+ }
101
+
102
+ // A power-of-two division and an exactly represented <=48-bit product need
103
+ // no binary64 boundary resolver. This includes ordinary dyadic quantization.
104
+ fn quant_exact_dyadic_product(acc: i32, scale: f32) -> bool {
105
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
106
+ let significand = quant_float_significand(bitcast<u32>(scale));
107
+ let integer_bits = 32u - countLeadingZeros(magnitude) - countTrailingZeros(magnitude);
108
+ let scale_bits = 32u - countLeadingZeros(significand) - countTrailingZeros(significand);
109
+ return integer_bits + scale_bits <= 48u;
110
+ }
111
+ fn quant_normal_expansion_range(v: f32) -> bool {
112
+ let exponent = (bitcast<u32>(v) >> 23u) & 255u;
113
+ // Reserve two binary32 significands plus four carry/error bits above the
114
+ // minimum normal exponent, so residual arithmetic avoids FTZ underflow.
115
+ return exponent >= (1u + 2u * 24u + 4u) && exponent < 255u;
116
+ }
117
+
118
+ fn round_expansion_ratio(numerator_hi: f32, numerator_lo: f32, denominator: f32, dyadic_scale: f32, dyadic_candidate: bool, acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
119
+ let estimate = numerator_hi / denominator;
120
+ // Anything outside this range saturates after adding an 8-bit zero point;
121
+ // returning early also keeps floor/i32 conversion away from Inf and overflow.
122
+ if (estimate <= -1024.0) { return -1024; }
123
+ if (estimate >= 1024.0) { return 1024; }
124
+
125
+ let lo = floor(estimate);
126
+ let hi = lo + 1.0;
127
+ let midpoint = lo + 0.5;
128
+ let midpoint_hi = fma(midpoint, denominator, 0.0);
129
+ if ((bitcast<u32>(midpoint_hi) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
130
+ let midpoint_lo = fma(midpoint, denominator, -midpoint_hi);
131
+ // The high-word subtraction is exact for these similarly sized products
132
+ // (Sterbenz); adding the residual difference therefore preserves its sign
133
+ // even when the rounded high words straddle adjacent f32 values.
134
+ let midpoint_delta = fma(1.0, numerator_hi - midpoint_hi, numerator_lo - midpoint_lo);
135
+ // Escalate ambiguous expansion signs instead of changing the tie tolerance.
136
+ // Six FMA/product tails and their additions fit within this conservative
137
+ // 16*u^2 error envelope (u=2^-24), including binary64 reference rounding.
138
+ if ((bitcast<u32>(midpoint_delta) & 0x7f800000u) == 0x7f800000u) { return quant_round_binary64(acc, a_s, b_s, y_s); }
139
+ if (abs(midpoint_delta) <= max(abs(numerator_hi), abs(midpoint_hi)) * 0x1p-44) {
140
+ if (!dyadic_candidate || !quant_exact_dyadic_product(acc, dyadic_scale)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
141
+ }
142
+ if (midpoint_delta < 0.0) {
143
+ return i32(lo);
144
+ }
145
+ if (midpoint_delta > 0.0) {
146
+ return i32(hi);
147
+ }
148
+ let half_lo = floor(lo * 0.5);
149
+ let lo_is_even = lo - half_lo * 2.0 == 0.0;
150
+ return i32(select(hi, lo, lo_is_even));
151
+ }
152
+
153
+ // Multiplication by a power of two keeps every i32 bit in binary64. Round the
154
+ // exact dyadic value directly, including INT_MIN, half-even ties and saturation.
155
+ fn quant_round_power_two(acc: i32, exponent: i32) -> i32 {
156
+ let magnitude = select(u32(acc), 0u - u32(acc), acc < 0);
157
+ var rounded = 0u;
158
+ if (exponent >= 0) {
159
+ if (exponent >= 10) { return select(1024, -1024, acc < 0); }
160
+ rounded = min(magnitude, 1024u >> u32(exponent)) << u32(exponent);
161
+ } else {
162
+ let shift = u32(-exponent);
163
+ if (shift >= 32u) { return 0; }
164
+ let whole = magnitude >> shift;
165
+ let remainder = magnitude & ((1u << shift) - 1u);
166
+ let half = 1u << (shift - 1u);
167
+ rounded = min(1024u, whole + u32(remainder > half || (remainder == half && (whole & 1u) != 0u)));
168
+ }
169
+ return select(i32(rounded), -i32(rounded), acc < 0);
170
+ }
171
+
172
+ fn round_requantized_precise(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
173
+ if (acc == 0) { return 0; }
174
+ let a_bits = bitcast<u32>(a_s);
175
+ let b_bits = bitcast<u32>(b_s);
176
+ let y_bits = bitcast<u32>(y_s);
177
+ if (((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u && min(a_bits, min(b_bits, y_bits)) != 0u && max(a_bits, max(b_bits, y_bits)) < 0x7f800000u) {
178
+ let exponent = i32(a_bits >> 23u) + i32(b_bits >> 23u) - i32(y_bits >> 23u) - 127;
179
+ return quant_round_power_two(acc, exponent);
180
+ }
181
+ let acc_f = f32(acc);
182
+ // Every i32 high part aligned to 256 is exactly representable in f32.
183
+ // Recover the conversion residual without an out-of-range i32(acc_f)
184
+ // conversion when INT_MAX rounds up to 2^31.
185
+ let acc_tail = fma(-1.0, acc_f - f32(acc & -256), f32(acc & 255));
186
+ // Equal b/y scales cancel exactly. Bypass their product and division so a
187
+ // rounded intermediate cannot move a near-half value onto the tie.
188
+ if (bitcast<u32>(b_s) == bitcast<u32>(y_s)) {
189
+ let direct_hi = fma(acc_f, a_s, 0.0);
190
+ let direct_lo = fma(acc_tail, a_s, fma(acc_f, a_s, -direct_hi));
191
+ if (!quant_normal_expansion_range(direct_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
192
+ return round_expansion_ratio(direct_hi, direct_lo, 1.0, a_s, true, acc, a_s, b_s, y_s);
193
+ }
194
+
195
+ // Keep the scale product as a two-float expansion. Its FMA residual preserves
196
+ // which side of a rounding midpoint a near-half value occupies.
197
+ let ab_hi = fma(a_s, b_s, 0.0);
198
+ let ab_lo = fma(a_s, b_s, -ab_hi);
199
+ if (!quant_normal_expansion_range(ab_hi) || !quant_normal_expansion_range(y_s) || y_s > 0x1p126) { return quant_round_binary64(acc, a_s, b_s, y_s); }
200
+ let product_hi = fma(acc_f, ab_hi, 0.0);
201
+ if (!quant_normal_expansion_range(product_hi)) { return quant_round_binary64(acc, a_s, b_s, y_s); }
202
+ let rounded_acc_tail = fma(acc_f, ab_hi, -product_hi) + fma(acc_f, ab_lo, 0.0);
203
+ let product_tail = fma(acc_tail, ab_hi, rounded_acc_tail) + fma(acc_tail, ab_lo, 0.0);
204
+ // Compare the expansion directly. Renormalizing it adds a cancelling
205
+ // subtraction/addition chain that can erase the residual during lowering.
206
+ let denominator_bits = bitcast<u32>(y_s);
207
+ let denominator_power_two = (denominator_bits & 0x7fffffu) == 0u;
208
+ let dyadic_candidate = denominator_power_two && ab_lo == 0.0;
209
+ return round_expansion_ratio(product_hi, product_tail, y_s, ab_hi, dyadic_candidate, acc, a_s, b_s, y_s);
210
+ }
211
+
212
+ // A conservative binary32 error filter accepts only a result safely inside one
213
+ // integer rounding bin. Ambiguous boundaries keep the full expansion/binary64
214
+ // path; this is an exact-result shortcut, not an approximate quantization mode.
215
+ fn round_requantized(acc: i32, a_s: f32, b_s: f32, y_s: f32) -> i32 {
216
+ if (acc == 0) { return 0; }
217
+ let a_bits = bitcast<u32>(a_s);
218
+ let b_bits = bitcast<u32>(b_s);
219
+ let y_bits = bitcast<u32>(y_s);
220
+ let ea = (a_bits >> 23u) & 255u;
221
+ let eb = (b_bits >> 23u) & 255u;
222
+ let ey = (y_bits >> 23u) & 255u;
223
+ // Certify the range before arithmetic: overflow need not produce infinity.
224
+ // Any nonzero i32 converts to a float with biased exponent <=158. These
225
+ // scale-only bounds cover every accumulator and can be shared across lanes.
226
+ // Reserve two significands plus carry bits above FTZ, and a factor of two
227
+ // below overflow for products, division and the expansion midpoint product.
228
+ if (((a_bits | b_bits | y_bits) & 0x80000000u) != 0u ||
229
+ min(ea, eb) == 0u || max(ea, eb) >= 255u ||
230
+ ea + eb < 180u || ea + eb > 347u ||
231
+ ey < 53u || ey > 242u || ea + eb > ey + 220u) {
232
+ return quant_round_binary64(acc, a_s, b_s, y_s);
233
+ }
234
+ let scale = fma(a_s, b_s, 0.0);
235
+ let product = fma(f32(acc), scale, 0.0);
236
+ let estimate = product / y_s;
237
+ if (estimate <= -1024.0) { return -1024; }
238
+ if (estimate >= 1024.0) { return 1024; }
239
+ let nearest = round(estimate);
240
+ // 32 binary32 unit roundoffs reserve room for the i32 conversion, two
241
+ // products, quotient, and distance arithmetic, plus binary64 rounding.
242
+ let margin = max(1.0, abs(estimate)) * 0x1p-19;
243
+ if (abs(estimate - nearest) < 0.5 - margin) { return i32(nearest); }
244
+ {% if device.adapterInfo.vendor != "apple" %}
245
+ // A rounded half-integer estimate can hide which side the exact f32-scale
246
+ // ratio lies on. Resolve that one-ulp neighborhood conservatively outside
247
+ // the compact fast path; exact power-of-two ratios already have a cheap
248
+ // and correct path in round_requantized_precise.
249
+ if (abs(abs(estimate - nearest) - 0.5) <= max(1.0, abs(estimate)) * 0x1p-24) {
250
+ let dyadic_scales = ((a_bits | b_bits | y_bits) & 0x807fffffu) == 0u &&
251
+ min(a_bits, min(b_bits, y_bits)) != 0u &&
252
+ max(a_bits, max(b_bits, y_bits)) < 0x7f800000u;
253
+ if (!dyadic_scales) { return quant_round_binary64(acc, a_s, b_s, y_s); }
254
+ }
255
+ {% endif %}
256
+ return round_requantized_precise(acc, a_s, b_s, y_s);
257
+ }
258
+
259
+ // Both integer-output and requantizing consumers share the vector GEMM body.
260
+ // Its callback stores scalar outputs so padded operands also cover column tails.
261
+ fn store_integer_result(value: i32, output_index: u32{% if integerStoreColumn %}, column: u32{% endif %}{% if integerStoreRow %}, row: u32{% endif %}) {
262
+ {% if matrixHasBias %}
263
+ let acc = value + bias[{{ "row" if matrixBiasPerRow else "column" }}];
264
+ {% endif %}
265
+ let rounded = round_requantized({{ "acc" if matrixHasBias else "value" }}, f32(a_scale[0]), f32(b_scale[{{ "row" if matrixScalePerRow else ("column" if bScalePerColumn else "0") }}]), f32(y_scale[0]));
266
+ let q = clamp(rounded + i32(y_zero_point[0]), {{ 0 if yUnsigned else (0 - 128) }}, {{ 255 if yUnsigned else 127 }});
267
+ y[output_index] = {{ "u32(q)" if yUnsigned else "q" }};
268
+ }
269
+
270
+ {% set exactIntegerPartialsValue = true %}
271
+ {% set preparedNValue = preparedN | default(0) %}
272
+ {% set integerPartialKValue = integerPartialK | default(0) %}
273
+ {% set requantizeValue = true %}
274
+ {% set bScalePerColumnValue = bScalePerColumn is defined and bScalePerColumn %}
275
+ {% set exactBatchBValue = exactIntegerPartialsValue and (exactBatchB | default(false)) %}
276
+ {% set integerStoreColumnValue = (requantizeValue and bScalePerColumnValue) or (integerStoreColumn | default(false)) %}
277
+ {% set integerStoreRowValue = integerStoreRow | default(false) %}
278
+ {{ env.wgsl.resourceDeclarations }}
279
+ {% set kTileValue = kTile %}
280
+ {% set blockRowsValue = blockRows if blockRows is defined else 128 %}
281
+ {% set blockColsValue = blockCols if blockCols is defined else 64 %}
282
+ {% set accumulatorScalarValue = "f32" %}
283
+ {% set workgroupColsValue = workgroupCols if workgroupCols is defined else 16 %}
284
+ {% set workgroupRowsValue = workgroupRows if workgroupRows is defined else 16 %}
285
+ {% set laneCount = workgroupColsValue * workgroupRowsValue %}
286
+ {% set threadRows = (blockRowsValue / workgroupRowsValue)|int %}
287
+ {% set threadColVectors = (blockColsValue / (workgroupColsValue * 4))|int %}
288
+ {% set threadCols = threadColVectors * 4 %}
289
+ {% set aVectorCount = (blockRowsValue * kTileValue / (4 * laneCount))|int %}
290
+ {% set bVectorCount = (kTileValue * blockColsValue / (4 * laneCount))|int %}
291
+ {% set batchStridesValue = false %}
292
+
293
+ // High-intensity matrix multiplication for the large, untransposed layout:
294
+ // Y[M,N] = alpha * A[M,K] @ B[K,N].
295
+ // B may alternatively be stored physically as [N,K]; that mode retains vec4
296
+ // K loads and transposes only while staging the tile in workgroup memory.
297
+ //
298
+ // Both inputs and the output are four-wide along their contiguous dimension.
299
+ // The output-tile geometry and BK are selected from the shape and device's
300
+ // workgroup-storage limit. f16 operands stay packed in global and workgroup
301
+ // memory, then widen at the FMA so accumulation remains f32.
302
+ const M: u32 = {{ M }}u;
303
+ const K: u32 = {{ K }}u;
304
+ const N: u32 = {{ N }}u;
305
+ const K_V4: u32 = K / 4u;
306
+ const B_N_V4: u32 = {{ preparedNValue }}u / 4u;
307
+ const BK: u32 = {{ kTileValue }}u;
308
+ const BM: u32 = {{ blockRowsValue }}u;
309
+ const BN: u32 = {{ blockColsValue }}u;
310
+ const TM: u32 = {{ threadRows }}u;
311
+ const TN: u32 = {{ threadCols }}u;
312
+
313
+ // A is loaded and consumed four K values at a time. Keeping that packing in
314
+ // workgroup memory reduces A-side shared-memory instructions and inner-loop
315
+ // control without changing the four scalar FMA operations or their order.
316
+ var<workgroup> tileA: array<array<vec4<{{ scalar }}>, BK / 4u>, BM>;
317
+ var<workgroup> tileB: array<vec4<{{ scalar }}>, BK * (BN / 4u)>;
318
+
319
+ @compute @workgroup_size({{ workgroupColsValue }}, {{ workgroupRowsValue }}, 1)
320
+ fn main(
321
+ @builtin(workgroup_id) wg: vec3<u32>,
322
+ @builtin(local_invocation_id) lid: vec3<u32>
323
+ ) {
324
+ let li = lid.y * {{ workgroupColsValue }}u + lid.x;
325
+ let mBase = wg.y * BM;
326
+ let nBase = wg.x * BN;
327
+ {% if exactBatchBValue %}
328
+ let bBatch = wg.z * K * B_N_V4;
329
+ let yBatch = wg.z * M * N;
330
+ {% endif %}
331
+ {% set aOff = "" %}
332
+ {% set bOff = "bBatch + " if batchStridesValue or exactBatchBValue else "" %}
333
+
334
+ {% for i in range(threadRows) %}
335
+ {% for j in range(threadColVectors) %}
336
+ var acc{{ i }}_{{ j }} = vec4<{{ accumulatorScalarValue }}>(0.0);
337
+ var total{{ i }}_{{ j }} = vec4<i32>(0);
338
+ {% endfor %}
339
+ {% endfor %}
340
+
341
+ for (var kBase: u32 = 0u; kBase < K; kBase = kBase + BK) {
342
+ {% for av in range(aVectorCount) %}
343
+ let aVector{{ av }} = li + {{ av * laneCount }}u;
344
+ let aRow{{ av }} = aVector{{ av }} / (BK / 4u);
345
+ let aColV4_{{ av }} = aVector{{ av }} % (BK / 4u);
346
+ let am{{ av }} = mBase + aRow{{ av }};
347
+ var av4_{{ av }} = vec4<{{ scalar }}>({{ scalar }}(0));
348
+ if (am{{ av }} < M) {
349
+ av4_{{ av }} = a[{{ aOff }}am{{ av }} * K_V4 + kBase / 4u + aColV4_{{ av }}];
350
+ }
351
+ tileA[aRow{{ av }}][aColV4_{{ av }}] = av4_{{ av }};
352
+ {% endfor %}
353
+ {% for bv in range(bVectorCount) %}
354
+ let bVector{{ bv }} = li + {{ bv * laneCount }}u;
355
+ let bRow{{ bv }} = bVector{{ bv }} / (BN / 4u);
356
+ let bColV4_{{ bv }} = bVector{{ bv }} % (BN / 4u);
357
+ let bn{{ bv }} = nBase + bColV4_{{ bv }} * 4u;
358
+ var bv4_{{ bv }} = vec4<{{ scalar }}>({{ scalar }}(0));
359
+ if (bn{{ bv }} < N) {
360
+ bv4_{{ bv }} = b[{{ bOff }}(kBase + bRow{{ bv }}) * {{ "B_N_V4" if exactIntegerPartialsValue else "N_V4" }} + bn{{ bv }} / 4u];
361
+ }
362
+ tileB[bRow{{ bv }} * (BN / 4u) + bColV4_{{ bv }}] = bv4_{{ bv }};
363
+ {% endfor %}
364
+ workgroupBarrier();
365
+
366
+ let aRow = lid.y * TM;
367
+ let bCol4 = lid.x * {{ threadColVectors }}u;
368
+ for (var kv: u32 = 0u; kv < BK / 4u; kv = kv + 1u) {
369
+ let kk = kv * 4u;
370
+ {% for j in range(threadColVectors) %}
371
+ let bv0_{{ j }} = vec4<{{ accumulatorScalarValue }}>(tileB[(kk + 0u) * (BN / 4u) + bCol4 + {{ j }}u]);
372
+ let bv1_{{ j }} = vec4<{{ accumulatorScalarValue }}>(tileB[(kk + 1u) * (BN / 4u) + bCol4 + {{ j }}u]);
373
+ let bv2_{{ j }} = vec4<{{ accumulatorScalarValue }}>(tileB[(kk + 2u) * (BN / 4u) + bCol4 + {{ j }}u]);
374
+ let bv3_{{ j }} = vec4<{{ accumulatorScalarValue }}>(tileB[(kk + 3u) * (BN / 4u) + bCol4 + {{ j }}u]);
375
+ {% endfor %}
376
+ {% for i in range(threadRows) %}
377
+ let av{{ i }} = tileA[aRow + {{ i }}u][kv];
378
+ {% for j in range(threadColVectors) %}
379
+ acc{{ i }}_{{ j }} = acc{{ i }}_{{ j }} + vec4<{{ accumulatorScalarValue }}>({{ accumulatorScalarValue }}(av{{ i }}.x)) * bv0_{{ j }};
380
+ acc{{ i }}_{{ j }} = acc{{ i }}_{{ j }} + vec4<{{ accumulatorScalarValue }}>({{ accumulatorScalarValue }}(av{{ i }}.y)) * bv1_{{ j }};
381
+ acc{{ i }}_{{ j }} = acc{{ i }}_{{ j }} + vec4<{{ accumulatorScalarValue }}>({{ accumulatorScalarValue }}(av{{ i }}.z)) * bv2_{{ j }};
382
+ acc{{ i }}_{{ j }} = acc{{ i }}_{{ j }} + vec4<{{ accumulatorScalarValue }}>({{ accumulatorScalarValue }}(av{{ i }}.w)) * bv3_{{ j }};
383
+ {% endfor %}
384
+ {% endfor %}
385
+ }
386
+ workgroupBarrier();
387
+ // Adjusted byte integers and each bounded partial are exact in f32.
388
+ // Convert before exceeding that bound; int32 additions retain wraparound.
389
+ if ((kBase + BK) % {{ integerPartialKValue }}u == 0u || kBase + BK == K) {
390
+ {% for i in range(threadRows) %}{% for j in range(threadColVectors) %}
391
+ total{{ i }}_{{ j }} += vec4<i32>(acc{{ i }}_{{ j }});
392
+ acc{{ i }}_{{ j }} = vec4<f32>(0.0);
393
+ {% endfor %}{% endfor %}
394
+ }
395
+ }
396
+
397
+ let m0 = mBase + lid.y * TM;
398
+ {% for j in range(threadColVectors) %}
399
+ let n{{ j }} = nBase + lid.x * TN + {{ j * 4 }}u;
400
+ {% endfor %}
401
+ {% for i in range(threadRows) %}
402
+ {% for j in range(threadColVectors) %}
403
+ if (m0 + {{ i }}u < M && n{{ j }} < N) {
404
+ {% for h in range(4) %}
405
+ if (n{{ j }} + {{ h }}u < N) {
406
+ store_integer_result(total{{ i }}_{{ j }}[{{ h }}], {% if exactBatchBValue %}yBatch + {% endif %}(m0 + {{ i }}u) * N + n{{ j }} + {{ h }}u{% if integerStoreColumnValue %}, n{{ j }} + {{ h }}u{% endif %}{% if integerStoreRowValue %}, m0 + {{ i }}u{% endif %});
407
+ }
408
+ {% endfor %}
409
+ }
410
+ {% endfor %}
411
+ {% endfor %}
412
+ }
build/webgpu/quant-exact-prepare.wgsl.jinja ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% if prepareSpatialA or prepareSpatialB %}
3
+ {% macro spatial_constants() %}
4
+ const KERNEL_H: u32 = {{ kernelHSpec }}u;
5
+ const KERNEL_W: u32 = {{ kernelWSpec }}u;
6
+ const KERNEL_HW: u32 = KERNEL_H * KERNEL_W;
7
+ const KSIZE: u32 = KERNEL_HW;
8
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
9
+ const STRIDE_W: u32 = {{ strideWSpec }}u;
10
+ const DILATION_H: u32 = {{ dilationHSpec }}u;
11
+ const DILATION_W: u32 = {{ dilationWSpec }}u;
12
+ const PAD_TOP: i32 = {{ padTopSpec }};
13
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
14
+ {% endmacro %}
15
+ {% macro spatial_sample(value="value", input="x", zero="x_zero_point", sampleK="k", sampleScalar="auto") %}
16
+ let ic = {{ sampleK }} / KSIZE;
17
+ let kq = {{ sampleK }} - ic * KSIZE;
18
+ let kh = kq / KERNEL_W;
19
+ let kw = kq - kh * KERNEL_W;
20
+ let oh = pos / params.outW;
21
+ let ow = pos - oh * params.outW;
22
+ let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
23
+ let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
24
+ var {{ value }}: {{ bScalar if sampleScalar == "auto" else sampleScalar }} = {{ zero }}[0];
25
+ if (ih >= 0 && ih < i32(params.inH) && iw >= 0 && iw < i32(params.inW)) {
26
+ {{ value }} = {{ input }}[{% if sampleChannelsLast is defined and sampleChannelsLast %}((batch * params.inH + u32(ih)) * params.inW + u32(iw)) * params.inChannels + ic{% else %}((batch * params.inChannels + ic) * params.inH + u32(ih)) * params.inW + u32(iw){% endif %}];
27
+ }
28
+ {% endmacro %}
29
+ {{ spatial_constants() }}
30
+ {% endif %}
31
+ @compute @workgroup_size({{ prepareWorkgroup }})
32
+ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_index) tid: u32) {
33
+ let offset = ((wid.y * params.groupsX + wid.x) * {{ prepareWorkgroup }}u + tid) * 4u;
34
+ if (offset >= params.count) { return; }
35
+ if (offset < params.aCount) {
36
+ let m = offset / params.paddedK;
37
+ let k = offset % params.paddedK;
38
+ {% for h in range(4) %}
39
+ {
40
+ var value = 0.0;
41
+ if (m < params.M && k + {{ h }}u < params.K) {
42
+ {% if prepareSpatialA %}
43
+ let batch = m / params.outCount;
44
+ let pos = m % params.outCount;
45
+ let sourceK = k + {{ h }}u;
46
+ {{ spatial_sample("sample", "a", "a_zero_point", "sourceK", aScalar) }}
47
+ value = f32(i32(sample) - i32(a_zero_point[0]));
48
+ {% else %}
49
+ value = f32(i32(a[m * params.K + k + {{ h }}u]) - i32(a_zero_point[{{ "m" if prepareZeroPerRow else "0" }}]));
50
+ {% endif %}
51
+ }
52
+ aFloat[offset + {{ h }}u] = {% if precastScalar == "f16" %}f16(value){% else %}value{% endif %};
53
+ }
54
+ {% endfor %}
55
+ } else {
56
+ let bOffset = offset - params.aCount;
57
+ {% if prepareBatchB %}
58
+ let batch = bOffset / (params.paddedK * params.paddedN);
59
+ let localOffset = bOffset % (params.paddedK * params.paddedN);
60
+ {% endif %}
61
+ let k = {{ "localOffset" if prepareBatchB else "bOffset" }} / params.paddedN;
62
+ let n = {{ "localOffset" if prepareBatchB else "bOffset" }} % params.paddedN;
63
+ {% for h in range(4) %}
64
+ {
65
+ var value = 0.0;
66
+ if (k < params.K && n + {{ h }}u < params.N) {
67
+ {% if prepareSpatialB %}
68
+ let pos = n + {{ h }}u;
69
+ {{ spatial_sample("sample", "b", "b_zero_point") }}
70
+ value = f32(i32(sample) - i32(b_zero_point[0]));
71
+ {% else %}
72
+ value = f32(i32(b[{% if prepareTransposeB %}(n + {{ h }}u) * params.K + k{% else %}{% if prepareBatchB %}batch * params.K * params.N + {% endif %}k * params.N + n + {{ h }}u{% endif %}]) - i32(b_zero_point[{% if bZeroPerColumn %}n + {{ h }}u{% else %}0{% endif %}]));
73
+ {% endif %}
74
+ }
75
+ bFloat[bOffset + {{ h }}u] = {% if precastScalar == "f16" %}f16(value){% else %}value{% endif %};
76
+ }
77
+ {% endfor %}
78
+ }
79
+ }
build/webgpu/quant-weight-k-major.wgsl.jinja ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro quant_read_column_zero(fn_name, buffer_name, is_unsigned, present=true, per_column=false, param_name="col", return_type="i32") %}
2
+ fn {{ fn_name }}({% if per_column %}{{ param_name }}: u32{% endif %}) -> {{ return_type }} {
3
+ {% if per_column %}
4
+ return {{ buffer_name }}[{{ param_name }}];
5
+ {% else %}
6
+ return {{ buffer_name }}[0];
7
+ {% endif %}
8
+ }{% endmacro %}
9
+ // Reorder a quantized `[M, C/group, kH, kW]` convolution weight into the K-major
10
+ // `[KPadded, M]` operand the shared DP4A GEMM core reads as B. K enumerates
11
+ // `(kh, kw, ic)` so the reduction's fastest axis is the input channel, matching
12
+ // the fastest axis of a channels-last activation. Rows past the source K hold the
13
+ // weight zero point, so padded lanes contribute exactly zero to
14
+ // `(a - a_zero) * (b - b_zero)` while the GEMM keeps its aligned vec4 A path.
15
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
16
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
17
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
18
+ // per-axis workgroup fold width.
19
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
20
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
21
+ {{ env.wgsl.resourceDeclarations }}
22
+
23
+ {{ quant_read_column_zero("read_w_zero", "w_zero_point", false, per_column=wZeroPerChannel, param_name="oc", return_type=wScalar) }}
24
+
25
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
26
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
27
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE, "flat", guardInline=true) }}
28
+ // Neighbouring invocations cover neighbouring output channels, so both the
29
+ // K-major store and the requantization that later reads column `oc` stay
30
+ // contiguous.
31
+ let oc = flat % params.outChannels;
32
+ let k = flat / params.outChannels;
33
+ var value = read_w_zero({% if wZeroPerChannel %}oc{% endif %});
34
+ if (k < params.kRows) {
35
+ let ic = k % params.weightInChannels;
36
+ let khw = k / params.weightInChannels;
37
+ let kw = khw % params.kernelW;
38
+ let kh = khw / params.kernelW;
39
+ value = w[((oc * params.weightInChannels + ic) * params.kernelH + kh) * params.kernelW + kw];
40
+ }
41
+ w_k_major[flat] = value;
42
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff