[ExecuTorch][WebGPU] Add 256-thread "steel" q4gsw prefill GEMM#20730
[ExecuTorch][WebGPU] Add 256-thread "steel" q4gsw prefill GEMM#20730JCNTH wants to merge 6 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/20730
Note: Links to docs will display an error until the docs builds have been completed. ❌ 4 New Failures, 4 PendingAs of commit 4ad3bd1 with merge base fa49f8f ( NEW FAILURES - The following jobs have failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
|
@claude throoughly review, check for overengineering, correctness, and areaas for modularization |
|
Claude finished @JCNTH's task in 2m 43s —— View job Review: steel q4gsw prefill GEMM
Solid, well-scoped change. Numerics and dequant mirror the shmem sibling exactly, the fail-closed device guard is the right instinct, and the dynamic-resize wiring is complete. A few things worth addressing before merge — nothing blocking correctness for the target shapes, but a couple of real inconsistencies and clear dedup wins. Correctness1. Build-time falls back gracefully, resize hard-throws — asymmetric. 2. 3. The steel branch throw is unreachable on the build path (only reachable via resize). Since 4. Kernel numerics — verified correct. The A/B staging ( Overengineering5. Steel now shadows the shmem GEMM for essentially all M>1 shapes. Previously shmem was gated on Beyond that, I don't see gratuitous complexity — the kernel is a straightforward 64×64/256-thread tile and the host guards are minimal. Modularization6. Three copies of the "max workgroups per dimension" query. The block WGPULimits limits = {};
const uint32_t max_wgs =
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
limits.maxComputeWorkgroupsPerDimension > 0
? limits.maxComputeWorkgroupsPerDimension : 65535u;appears in uint32_t max_workgroups_per_dim(WGPUDevice device) {
WGPULimits limits = {};
return (wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
limits.maxComputeWorkgroupsPerDimension > 0)
? limits.maxComputeWorkgroupsPerDimension : 65535u;
}7. The steel and shmem branches of uint32_t tiled_wg_count(WGPUDevice device, uint32_t m, uint32_t n,
uint32_t tile_m, uint32_t tile_n,
const char* op_name, const char* kind) {
const int64_t total = utils::div_up<int64_t>(m, tile_m) *
utils::div_up<int64_t>(n, tile_n);
if (total > static_cast<int64_t>(max_workgroups_per_dim(device))) {
throw std::runtime_error(std::string("WebGPU ") + op_name + ": " + kind +
" tile count exceeds the 1D dispatch limit");
}
return static_cast<uint32_t>(total);
}Then both branches are one call each, and combined with #6 the routing function drops ~25 lines. 8. Consider deriving Minor / nits
Overall: correct for the target shapes, good guard discipline, and the perf win is well-motivated. The main asks are the max-limit dedup (#6/#7) and a comment clarifying the build-vs-resize fallback asymmetry (#1). |
Stack from ghstack (oldest at bottom):
Add a shared-memory 64x64-tile prefill GEMM for
et_vk.linear_q4gswthat beats the existing shmem/tiled GEMM by 1.75-3.17x and moves in-browser Llama-3.2-1B prefill from behind to ahead of llama.cpp WebGPU (+11-33%, M4 Pro / Chrome Canary).Problem: the q4gsw prefill path (M>1) routes to either the register-tiled or the 32x32 shmem GEMM. On Apple / M4 Pro both leave the 4-bit linear ~2.65x slower than llama.cpp's simdgroup-matrix GEMM, so prefill is the one axis where the WebGPU delegate trails.
Solution:
use_shmem_gemm(large K/N) else the register-tiled GEMM.use_steelbranch above shmem — a 64x64 output tile computed by 256 threads (16x16) with a 4x4 register sub-tile per thread, staging a BK=16 K-slice of activations (f32) and dequantized weights into shared memory once and reusing it across the tile. shmem/tiled remain the fallback when steel is ineligible.Implementation:
q4gsw_linear_gemm_steel.wgsl(same 6 bindings +Paramsas the sibling q4gsw kernels) and its generated_wgsl.h.steel_supported(requiresmaxComputeInvocationsPerWorkgroup >= 256; SwiftShader caps at 128 so it falls back) andsteel_workgroup_count(one workgroup per 64x64 tile; returns 0 to fall back whenK % 16 != 0— the kernel stages a full BK K-tile with no K-mask — or when the tile count exceeds the 1D dispatch limit).use_steelthrough the sharedcompute_q4gsw_workgroup_counthelper (a steel branch beside gemv/shmem) AND theadd_tensor_resize_hook, so a dynamic-shape prefill recomputes the steel tile count for the live M; add steel tofixed_wg(fixed@workgroup_size(16, 16), nowg_sizeoverride).is_gemvsplit and no 256-thread shmem-staged tile. This tiling is WebGPU-specific (buffer-only storage, compile-time@workgroup_size, 1D-dispatch fold); only the signed-nibble dequant mirrors the q4gsw reference.Constraints: engine-independent — depends only on shape + device limits and participates in the dynamic-resize hook exactly like gemv/shmem. Falls back to shmem/tiled on non-256-invocation devices, odd K, or over-limit dispatch, so behavior is unchanged where steel is ineligible. Layout and numerics are unchanged (same bindings, same dequant, f32 accumulator). The f16-multiply variant is a separate follow-up.
Co-authored-with: Claude Code.
@exported-using-ghexport
Differential Revision: D110660965
Differential Revision: D110660965