Skip to content

Commit ac021a8

Browse files
committed
[ExecuTorch][WebGPU] Packed-word-dequant f16 steel q4gsw prefill GEMM
Pull Request resolved: #20798 **+24-28% microbench GFLOP/s (+11-16% end-to-end prefill tok/s) on the f16 `steel` q4gsw prefill GEMM, Apple M4 Pro / Chrome Canary — bit-exact.** **Problem:** the f16 `steel` prefill GEMM (the `half` variant) dequantizes weights per-nibble — each of the 256 threads extracts ONE 4-bit weight from a full `u32` word, so every packed word is re-loaded ~8x and the per-column scale is re-read ~16x across the `BK=16` tile. That redundant global traffic dominates the kernel on Apple. **Solution:** a packed-word-dequant staging variant, bit-exact to the `half` kernel — identical `As` staging, compute loop, epilogue, and `64x64` tile / 256-thread / `BK=16` geometry; only the weight-dequant staging differs. Before (`half`): each thread extracts 1 nibble from a re-loaded word; scale re-read per nibble. After (`pwdq`): threads `[0,BN)` each stage one full `BK`-column — load the column's two `u32` words ONCE, unpack all 16 nibbles, read the per-column scale ONCE. **Implementation:** - New `PWDQ` fork in the shared `q4gsw_linear_gemm_steel.wgsl` template (a `shader_variants` entry in `q4gsw_linear_gemm_steel.yaml` generates `q4gsw_linear_gemm_steel_half_pwdq_wgsl.h`) — no standalone shader file. Selected at runtime when the device negotiated shader-f16 (`ctx->shader_f16_supported`) AND `group_size % BK == 0` (the hoisted scale must be constant across the `BK` tile), else the per-nibble `half` kernel. - No new build option, dispatch, or bindings — same tile/count as `steel`; only the generated shader variant differs. **Constraints:** requires `K % BK == 0` (the steel route already guarantees it, making `K_packed = K/2` a multiple of 8 so every column is `u32`-aligned) and `group_size % BK == 0` (all real q4 group sizes: 32/64/128). f16-multiply / f32-accumulate, so numerically identical to the `half` kernel. Co-authored-with: Claude Code. ghstack-source-id: 401564878 @exported-using-ghexport Differential Revision: [D111163510](https://our.internmc.facebook.com/intern/diff/D111163510/)
1 parent e2329b8 commit ac021a8

6 files changed

Lines changed: 247 additions & 39 deletions

File tree

backends/webgpu/runtime/ops/quantized_linear/QuantizedLinear.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
1313
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_coop4_bicol_wgsl.h>
1414
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_shmem_wgsl.h>
15+
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel_half_pwdq_wgsl.h>
1516
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel_half_wgsl.h>
1617
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel_wgsl.h>
1718
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_wgsl.h>
@@ -270,7 +271,13 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
270271
if (use_steel) {
271272
const WebGPUContext* ctx = get_default_webgpu_context();
272273
if (ctx != nullptr && ctx->shader_f16_supported) {
273-
shader_src = kQ4gswLinearGemmSteelHalfWGSL;
274+
// Packed-word dequant: bit-exact to the steel `half` kernel but loads
275+
// each u32 weight word once + hoists the per-column scale (half re-reads
276+
// them ~8x/~16x). Needs group_size % BK == 0 so the hoisted scale is
277+
// constant across the BK tile; else the per-nibble `half` kernel.
278+
shader_src = (gs % kQ4gswSteelBK == 0u)
279+
? kQ4gswLinearGemmSteelHalfPwdqWGSL
280+
: kQ4gswLinearGemmSteelHalfWGSL;
274281
}
275282
}
276283
const uint32_t workgroup_count = compute_q4gsw_workgroup_count(

backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel.wgsl

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ struct Params {
2121
// "steel" prefill GEMM (M>1): 64x64 tile, 256 threads; K%16==0 host-guarded.
2222
// The "steel" name + register-tiled dequant-to-shared GEMM structure are
2323
// inspired by MLX's steel GEMM kernels (github.com/ml-explore/mlx,
24-
// mlx/backend/metal/kernels/steel).
24+
// mlx/backend/metal/kernels/steel). One template, four variants:
25+
// DTYPE=float f32 storage/multiply, per-nibble weight staging.
26+
// DTYPE=half f16 storage/multiply, per-nibble weight staging.
27+
// PWDQ (half only) packed-word dequant: load each u32 weight word ONCE,
28+
// unpack all 16 nibbles of a column + hoist the per-column scale to one read
29+
// (the per-nibble path re-reads each word ~8x). Requires K%BK==0 (steel
30+
// route guarantees it) and group_size%BK==0 (hoisted scale across the tile).
31+
// ACC=half (PWDQ only) f16 accumulate with fma(), cast to f32 in the epilogue
32+
// -- LOSSY, perplexity-gated, opt-in via a runtime spec. ACC=float is f32
33+
// accumulate -- BIT-EXACT to the per-nibble half kernel.
2534
const BM: u32 = 64u; const BN: u32 = 64u; const BK: u32 = 16u;
2635
var<workgroup> As: array<${buffer_scalar_type(DTYPE)}, 1024>; // BM*BK
2736
var<workgroup> Bs: array<${buffer_scalar_type(DTYPE)}, 1024>; // BK*BN
@@ -34,16 +43,17 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
3443
let row0 = by * BM;
3544
let col0 = bx * BN;
3645
let tid = lid.y * 16u + lid.x;
37-
var acc: array<array<f32, 4>, 4>;
46+
var acc: array<array<${buffer_scalar_type(ACC)}, 4>, 4>;
3847
for (var m: u32 = 0u; m < 4u; m = m + 1u) {
39-
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = 0.0; }
48+
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = ${"0.0h" if ACC == "half" else "0.0"}; }
4049
}
4150
// A staging coords: 256 threads load 64x16 = 1024 f32 -> 4 rows each (4 contiguous K).
4251
let ar = tid / 4u; // 0..63 (row in tile)
4352
let ac = (tid % 4u) * 4u; // 0,4,8,12 (K offset, 4 contiguous)
44-
// B staging coords: 256 threads load 16x64 = 1024 dequant weights -> 4 cols each.
45-
let br = tid / 16u; // 0..15 (K within BK)
46-
let bc = (tid % 16u) * 4u; // 0,4,..60 (N offset, 4 contiguous)
53+
$if not PWDQ:
54+
// B staging coords: 256 threads load 16x64 = 1024 dequant weights -> 4 cols each.
55+
let br = tid / 16u; // 0..15 (K within BK)
56+
let bc = (tid % 16u) * 4u; // 0,4,..60 (N offset, 4 contiguous)
4757

4858
var k0: u32 = 0u;
4959
loop {
@@ -57,36 +67,63 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
5767
As[ar * BK + ac + 2u] = ${buffer_scalar_type(DTYPE)}(t_input[base + 2u]);
5868
As[ar * BK + ac + 3u] = ${buffer_scalar_type(DTYPE)}(t_input[base + 3u]);
5969
} else {
60-
As[ar * BK + ac + 0u] = 0.0; As[ar * BK + ac + 1u] = 0.0;
61-
As[ar * BK + ac + 2u] = 0.0; As[ar * BK + ac + 3u] = 0.0;
70+
As[ar * BK + ac + 0u] = ${"0.0h" if PWDQ else "0.0"}; As[ar * BK + ac + 1u] = ${"0.0h" if PWDQ else "0.0"};
71+
As[ar * BK + ac + 2u] = ${"0.0h" if PWDQ else "0.0"}; As[ar * BK + ac + 3u] = ${"0.0h" if PWDQ else "0.0"};
6272
}
63-
// stage DEQUANTIZED weights into Bs[k][n]: 4 contiguous N per thread.
64-
let kk = k0 + br; // K index for this shmem row
65-
let scale_row = (kk / params.group_size) * params.padded_N;
66-
for (var j: u32 = 0u; j < 4u; j = j + 1u) {
67-
let n = col0 + bc + j;
68-
var dqv: ${buffer_scalar_type(DTYPE)} = 0.0;
69-
if (n < params.N) {
70-
let byte_idx = n * params.K_packed + (kk >> 1u);
71-
let word = t_weight[byte_idx >> 2u];
72-
let b = (word >> ((byte_idx & 3u) * 8u)) & 0xFFu;
73-
var nib: u32;
74-
if ((kk & 1u) == 0u) { nib = b & 0x0Fu; } else { nib = (b >> 4u) & 0x0Fu; }
75-
$if DTYPE == "half":
76-
dqv = f16(i32(nib) - 8) * f16(t_scales[scale_row + n]);
77-
$else:
78-
dqv = f32(i32(nib) - 8) * t_scales[scale_row + n];
73+
$if PWDQ:
74+
// Packed-word dequant: threads [0,BN) each stage one full BK-column of Bs.
75+
if (tid < BN) {
76+
let c = tid; // Bs column within this tile
77+
let n = col0 + c; // global output column
78+
if (n < params.N) {
79+
// Scale is constant across the BK tile (group_size % BK == 0 for all real
80+
// group sizes; K%BK==0 on the steel route), so hoist it to one read.
81+
let scale_row = (k0 / params.group_size) * params.padded_N;
82+
let scale = f16(t_scales[scale_row + n]);
83+
// Column n's 16-nibble K-slice for this tile = two consecutive words.
84+
// K_packed multiple of 8 => base_word stays inside column n's own region.
85+
let base_word = n * (params.K_packed >> 2u) + (k0 >> 3u);
86+
let w0 = t_weight[base_word];
87+
let w1 = t_weight[base_word + 1u];
88+
for (var br: u32 = 0u; br < BK; br = br + 1u) {
89+
let word = select(w1, w0, br < 8u); // word0 holds K-slice [0,8)
90+
let nib = (word >> ((br & 7u) * 4u)) & 0x0Fu;
91+
Bs[br * BN + c] = f16(i32(nib) - 8) * scale;
92+
}
93+
} else {
94+
for (var br: u32 = 0u; br < BK; br = br + 1u) { Bs[br * BN + c] = 0.0h; }
95+
}
96+
}
97+
$else:
98+
// stage DEQUANTIZED weights into Bs[k][n]: 4 contiguous N per thread.
99+
let kk = k0 + br; // K index for this shmem row
100+
let scale_row = (kk / params.group_size) * params.padded_N;
101+
for (var j: u32 = 0u; j < 4u; j = j + 1u) {
102+
let n = col0 + bc + j;
103+
var dqv: ${buffer_scalar_type(DTYPE)} = 0.0;
104+
if (n < params.N) {
105+
let byte_idx = n * params.K_packed + (kk >> 1u);
106+
let word = t_weight[byte_idx >> 2u];
107+
let b = (word >> ((byte_idx & 3u) * 8u)) & 0xFFu;
108+
var nib: u32;
109+
if ((kk & 1u) == 0u) { nib = b & 0x0Fu; } else { nib = (b >> 4u) & 0x0Fu; }
110+
$if DTYPE == "half":
111+
dqv = f16(i32(nib) - 8) * f16(t_scales[scale_row + n]);
112+
$else:
113+
dqv = f32(i32(nib) - 8) * t_scales[scale_row + n];
114+
}
115+
Bs[br * BN + bc + j] = dqv;
79116
}
80-
Bs[br * BN + bc + j] = dqv;
81-
}
82117
workgroupBarrier();
83118
for (var k: u32 = 0u; k < BK; k = k + 1u) {
84119
var a: array<${buffer_scalar_type(DTYPE)}, 4>;
85120
var bvec: array<${buffer_scalar_type(DTYPE)}, 4>;
86121
for (var m: u32 = 0u; m < 4u; m = m + 1u) { a[m] = As[(lid.y * 4u + m) * BK + k]; }
87122
for (var n: u32 = 0u; n < 4u; n = n + 1u) { bvec[n] = Bs[k * BN + lid.x * 4u + n]; }
88123
for (var m: u32 = 0u; m < 4u; m = m + 1u) {
89-
$if DTYPE == "half":
124+
$if ACC == "half":
125+
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = fma(a[m], bvec[n], acc[m][n]); }
126+
$elif DTYPE == "half":
90127
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = acc[m][n] + f32(a[m] * bvec[n]); }
91128
$else:
92129
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = acc[m][n] + a[m] * bvec[n]; }
@@ -100,7 +137,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
100137
let r = row0 + lid.y * 4u + m;
101138
let c = col0 + lid.x * 4u + n;
102139
if (r < params.M && c < params.N) {
103-
var v = acc[m][n];
140+
var v = ${"f32(acc[m][n])" if ACC == "half" else "acc[m][n]"};
104141
if (params.has_bias != 0u) { v = v + t_bias[c]; }
105142
t_out[r * params.N + c] = v;
106143
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
q4gsw_linear_gemm_steel:
22
parameter_names_with_default_values:
33
DTYPE: float
4-
generate_variant_forall:
5-
DTYPE:
6-
- VALUE: float
7-
SUFFIX: ""
8-
- VALUE: half
9-
SUFFIX: half
4+
PWDQ: false
5+
ACC: float
106
shader_variants:
117
- NAME: q4gsw_linear_gemm_steel
8+
DTYPE: float
9+
PWDQ: false
10+
ACC: float
11+
- NAME: q4gsw_linear_gemm_steel_half
12+
DTYPE: half
13+
PWDQ: false
14+
ACC: float
15+
- NAME: q4gsw_linear_gemm_steel_half_pwdq
16+
DTYPE: half
17+
PWDQ: true
18+
ACC: float
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#pragma once
10+
11+
#include <cstdint>
12+
13+
namespace executorch::backends::webgpu {
14+
15+
// @generated from q4gsw_linear_gemm_steel.wgsl - DO NOT EDIT.
16+
// wgsl-sha256: 1f916bcd30dbbbcc7eca37e795ecc26e3c72e645ccd2c361fa0ac4e66f1a174a
17+
inline constexpr const char* kQ4gswLinearGemmSteelHalfPwdqWGSL = R"(
18+
enable f16;
19+
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
20+
@group(0) @binding(1) var<storage, read> t_input: array<f32>;
21+
@group(0) @binding(2) var<storage, read> t_weight: array<u32>;
22+
@group(0) @binding(3) var<storage, read> t_scales: array<f32>;
23+
@group(0) @binding(4) var<storage, read> t_bias: array<f32>;
24+
25+
struct Params {
26+
M: u32,
27+
N: u32,
28+
K: u32,
29+
K_packed: u32,
30+
group_size: u32,
31+
padded_N: u32,
32+
has_bias: u32,
33+
_pad: u32,
34+
}
35+
@group(0) @binding(5) var<uniform> params: Params;
36+
37+
// "steel" prefill GEMM (M>1): 64x64 tile, 256 threads; K%16==0 host-guarded.
38+
// The "steel" name + register-tiled dequant-to-shared GEMM structure are
39+
// inspired by MLX's steel GEMM kernels (github.com/ml-explore/mlx,
40+
// mlx/backend/metal/kernels/steel). One template, four variants:
41+
// DTYPE=float f32 storage/multiply, per-nibble weight staging.
42+
// DTYPE=half f16 storage/multiply, per-nibble weight staging.
43+
// PWDQ (half only) packed-word dequant: load each u32 weight word ONCE,
44+
// unpack all 16 nibbles of a column + hoist the per-column scale to one read
45+
// (the per-nibble path re-reads each word ~8x). Requires K%BK==0 (steel
46+
// route guarantees it) and group_size%BK==0 (hoisted scale across the tile).
47+
// ACC=half (PWDQ only) f16 accumulate with fma(), cast to f32 in the epilogue
48+
// -- LOSSY, perplexity-gated, opt-in via a runtime spec. ACC=float is f32
49+
// accumulate -- BIT-EXACT to the per-nibble half kernel.
50+
const BM: u32 = 64u; const BN: u32 = 64u; const BK: u32 = 16u;
51+
var<workgroup> As: array<f16, 1024>; // BM*BK
52+
var<workgroup> Bs: array<f16, 1024>; // BK*BN
53+
@compute @workgroup_size(16, 16)
54+
fn main(@builtin(workgroup_id) wid: vec3<u32>,
55+
@builtin(local_invocation_id) lid: vec3<u32>) {
56+
let nbN = (params.N + BN - 1u) / BN;
57+
let bx = wid.x % nbN; // decode 2D tile id from 1D dispatch
58+
let by = wid.x / nbN;
59+
let row0 = by * BM;
60+
let col0 = bx * BN;
61+
let tid = lid.y * 16u + lid.x;
62+
var acc: array<array<f32, 4>, 4>;
63+
for (var m: u32 = 0u; m < 4u; m = m + 1u) {
64+
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = 0.0; }
65+
}
66+
// A staging coords: 256 threads load 64x16 = 1024 f32 -> 4 rows each (4 contiguous K).
67+
let ar = tid / 4u; // 0..63 (row in tile)
68+
let ac = (tid % 4u) * 4u; // 0,4,8,12 (K offset, 4 contiguous)
69+
70+
var k0: u32 = 0u;
71+
loop {
72+
if (k0 >= params.K) { break; }
73+
// stage activations (edge-masked on M; K is a multiple of BK for our shapes)
74+
let arow = row0 + ar;
75+
if (arow < params.M) {
76+
let base = arow * params.K + k0 + ac;
77+
As[ar * BK + ac + 0u] = f16(t_input[base]);
78+
As[ar * BK + ac + 1u] = f16(t_input[base + 1u]);
79+
As[ar * BK + ac + 2u] = f16(t_input[base + 2u]);
80+
As[ar * BK + ac + 3u] = f16(t_input[base + 3u]);
81+
} else {
82+
As[ar * BK + ac + 0u] = 0.0h; As[ar * BK + ac + 1u] = 0.0h;
83+
As[ar * BK + ac + 2u] = 0.0h; As[ar * BK + ac + 3u] = 0.0h;
84+
}
85+
// Packed-word dequant: threads [0,BN) each stage one full BK-column of Bs.
86+
if (tid < BN) {
87+
let c = tid; // Bs column within this tile
88+
let n = col0 + c; // global output column
89+
if (n < params.N) {
90+
// Scale is constant across the BK tile (group_size % BK == 0 for all real
91+
// group sizes; K%BK==0 on the steel route), so hoist it to one read.
92+
let scale_row = (k0 / params.group_size) * params.padded_N;
93+
let scale = f16(t_scales[scale_row + n]);
94+
// Column n's 16-nibble K-slice for this tile = two consecutive words.
95+
// K_packed multiple of 8 => base_word stays inside column n's own region.
96+
let base_word = n * (params.K_packed >> 2u) + (k0 >> 3u);
97+
let w0 = t_weight[base_word];
98+
let w1 = t_weight[base_word + 1u];
99+
for (var br: u32 = 0u; br < BK; br = br + 1u) {
100+
let word = select(w1, w0, br < 8u); // word0 holds K-slice [0,8)
101+
let nib = (word >> ((br & 7u) * 4u)) & 0x0Fu;
102+
Bs[br * BN + c] = f16(i32(nib) - 8) * scale;
103+
}
104+
} else {
105+
for (var br: u32 = 0u; br < BK; br = br + 1u) { Bs[br * BN + c] = 0.0h; }
106+
}
107+
}
108+
workgroupBarrier();
109+
for (var k: u32 = 0u; k < BK; k = k + 1u) {
110+
var a: array<f16, 4>;
111+
var bvec: array<f16, 4>;
112+
for (var m: u32 = 0u; m < 4u; m = m + 1u) { a[m] = As[(lid.y * 4u + m) * BK + k]; }
113+
for (var n: u32 = 0u; n < 4u; n = n + 1u) { bvec[n] = Bs[k * BN + lid.x * 4u + n]; }
114+
for (var m: u32 = 0u; m < 4u; m = m + 1u) {
115+
for (var n: u32 = 0u; n < 4u; n = n + 1u) { acc[m][n] = acc[m][n] + f32(a[m] * bvec[n]); }
116+
}
117+
}
118+
workgroupBarrier();
119+
k0 = k0 + BK;
120+
}
121+
for (var m: u32 = 0u; m < 4u; m = m + 1u) {
122+
for (var n: u32 = 0u; n < 4u; n = n + 1u) {
123+
let r = row0 + lid.y * 4u + m;
124+
let c = col0 + lid.x * 4u + n;
125+
if (r < params.M && c < params.N) {
126+
var v = acc[m][n];
127+
if (params.has_bias != 0u) { v = v + t_bias[c]; }
128+
t_out[r * params.N + c] = v;
129+
}
130+
}
131+
}
132+
}
133+
)";
134+
135+
inline constexpr uint32_t kQ4gswLinearGemmSteelHalfPwdqWorkgroupSizeX = 16;
136+
inline constexpr uint32_t kQ4gswLinearGemmSteelHalfPwdqWorkgroupSizeY = 16;
137+
inline constexpr uint32_t kQ4gswLinearGemmSteelHalfPwdqWorkgroupSizeZ = 1;
138+
139+
} // namespace executorch::backends::webgpu

backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel_half_wgsl.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from q4gsw_linear_gemm_steel.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: e3c21e7db7c18f6e085de71e283988f0bd3b2543807ddc17774a1c607e69c766
16+
// wgsl-sha256: 00cdd2f2fb98a5c7343d16fdf7e59f1b840e180cec3f82bf9b569513c0a45396
1717
inline constexpr const char* kQ4gswLinearGemmSteelHalfWGSL = R"(
1818
enable f16;
1919
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
@@ -37,7 +37,16 @@ struct Params {
3737
// "steel" prefill GEMM (M>1): 64x64 tile, 256 threads; K%16==0 host-guarded.
3838
// The "steel" name + register-tiled dequant-to-shared GEMM structure are
3939
// inspired by MLX's steel GEMM kernels (github.com/ml-explore/mlx,
40-
// mlx/backend/metal/kernels/steel).
40+
// mlx/backend/metal/kernels/steel). One template, four variants:
41+
// DTYPE=float f32 storage/multiply, per-nibble weight staging.
42+
// DTYPE=half f16 storage/multiply, per-nibble weight staging.
43+
// PWDQ (half only) packed-word dequant: load each u32 weight word ONCE,
44+
// unpack all 16 nibbles of a column + hoist the per-column scale to one read
45+
// (the per-nibble path re-reads each word ~8x). Requires K%BK==0 (steel
46+
// route guarantees it) and group_size%BK==0 (hoisted scale across the tile).
47+
// ACC=half (PWDQ only) f16 accumulate with fma(), cast to f32 in the epilogue
48+
// -- LOSSY, perplexity-gated, opt-in via a runtime spec. ACC=float is f32
49+
// accumulate -- BIT-EXACT to the per-nibble half kernel.
4150
const BM: u32 = 64u; const BN: u32 = 64u; const BK: u32 = 16u;
4251
var<workgroup> As: array<f16, 1024>; // BM*BK
4352
var<workgroup> Bs: array<f16, 1024>; // BK*BN

backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_gemm_steel_wgsl.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from q4gsw_linear_gemm_steel.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: 43536b16026d3b62d77087b86289885606c04834d9783c3c871512d6789ee6f6
16+
// wgsl-sha256: dd771b9ab096410f3ad0d9259bef7816e41330a434325ea28baa5abbfb2841d2
1717
inline constexpr const char* kQ4gswLinearGemmSteelWGSL = R"(
1818
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
1919
@group(0) @binding(1) var<storage, read> t_input: array<f32>;
@@ -36,7 +36,16 @@ struct Params {
3636
// "steel" prefill GEMM (M>1): 64x64 tile, 256 threads; K%16==0 host-guarded.
3737
// The "steel" name + register-tiled dequant-to-shared GEMM structure are
3838
// inspired by MLX's steel GEMM kernels (github.com/ml-explore/mlx,
39-
// mlx/backend/metal/kernels/steel).
39+
// mlx/backend/metal/kernels/steel). One template, four variants:
40+
// DTYPE=float f32 storage/multiply, per-nibble weight staging.
41+
// DTYPE=half f16 storage/multiply, per-nibble weight staging.
42+
// PWDQ (half only) packed-word dequant: load each u32 weight word ONCE,
43+
// unpack all 16 nibbles of a column + hoist the per-column scale to one read
44+
// (the per-nibble path re-reads each word ~8x). Requires K%BK==0 (steel
45+
// route guarantees it) and group_size%BK==0 (hoisted scale across the tile).
46+
// ACC=half (PWDQ only) f16 accumulate with fma(), cast to f32 in the epilogue
47+
// -- LOSSY, perplexity-gated, opt-in via a runtime spec. ACC=float is f32
48+
// accumulate -- BIT-EXACT to the per-nibble half kernel.
4049
const BM: u32 = 64u; const BN: u32 = 64u; const BK: u32 = 16u;
4150
var<workgroup> As: array<f32, 1024>; // BM*BK
4251
var<workgroup> Bs: array<f32, 1024>; // BK*BN

0 commit comments

Comments
 (0)