Skip to content

feat(core): tiled matmul for quantW weights with f16 activations and the epilogue - #69

Merged
chmjkb merged 2 commits into
mainfrom
feat/quant-tiled-matmul
Oct 27, 2026
Merged

chmjkb merged 2 commits into
mainfrom
feat/quant-tiled-matmul

Conversation

@xxpopielxx

Copy link
Copy Markdown
Member

Description

A whole quantW weight (int4 or int8) now runs on a 32×32 tiled kernel from 8 rows, and always for an f16 input or a bias/activation/addend epilogue. Expert views of a fused weight stay on the plain kernel, so privacy-filter routes as before.

  • matmul op: f16 and the epilogue on quantW, shape checks at graph build
  • half() keeps quantW weights and their f32 scales instead of throwing
  • quantCommon: std.bitcast in place of the deprecated bitcastU32toI32, same WGSL
  • bench: whole-weight quantW cases

pnpm --filter runntime bench:ab kernels -k matmul against main, quantW rows (every other matmul case same, max abs(A−B) 0):

case A (µs) B (µs) Δ verdict max abs(A−B)
quantW int4 whole, q|k projection [256,1024]×[1024,2304] 2791 680 -75.6% faster 4.8e-6
quantW int8 whole, per channel [256,2048]×[2048,1024] 2433 633 -74.0% faster 2.1e-4
quantW int4 whole, decode [1,1024]×[1024,2304] 102 102 -0.2% same 0
quantW int8, privacy-filter expert [32,640]×[640,640] 91.1 90.9 -0.2% same 0
quantW int4, privacy-filter expert [32,640]×[640,640] 90.4 90.1 -0.3% same 0
quantW int8, decode [1,640]×[640,640] 60.3 60.5 +0.3% same 0

Introduces a breaking change?

  • Yes
  • No

Type of change

  • Bug fix (change which fixes an issue)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Performance (kernel tuning, graph building, anything related to performance)
  • Other (chores, tests, code style improvements etc.)

Testing instructions

  1. pnpm --filter runntime exec playwright install chromium (once per machine)
  2. pnpm --filter runntime test
  3. pnpm --filter runntime bench:ab kernels -k matmul

Screenshots

Related issues

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

The first user is Echo TTS (int4 by default, f16 activations, residual adds in the epilogue), which comes in a later PR together with nn.QuantLinear. Expert views on the tiled kernel (and f16 for quantized privacy-filter) can follow separately with their own A/B.

…the epilogue

A whole quantW weight (int4 or int8) now runs on a 32×32 tiled kernel from
8 rows, and always for an f16 input or a bias/activation/addend epilogue.
Expert views of a fused weight stay on the plain kernel, so privacy-filter
routes as before.

- matmul op: f16 and the epilogue on quantW, shape checks at graph build
- half() keeps quantW weights and their f32 scales instead of throwing
- quantCommon: std.bitcast in place of the deprecated bitcastU32toI32,
  same WGSL
- bench: whole-weight quantW cases, 4x faster than main at [256,1024]x[1024,2304]
  and [256,2048]x[2048,1024]; every other matmul case unchanged
@xxpopielxx xxpopielxx changed the title feat(core): tiled matmul for quantW weights with f16 activations and … feat(core): tiled matmul for quantW weights with f16 activations and the epilogue Oct 26, 2026

@chmjkb chmjkb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows past a workgroup of units (m = 70) now runs on the tiled kernel, so nothing tests the plain kernel with many rows anymore, though privacy-filter's experts still use it that way. Could you add a many-rows case that stays on the plain kernel (an expert slice), and a comment per case saying which kernel it takes? The "One route" comment at the top needs updating too.

Comment on lines +88 to +109
const loadRow =
bits === 4
? (wb: number, r: number) => {
'use gpu';
const w0 = std.bitcast(d.u32, d.i32)(layout.$.w[wb]!);
const w1 = std.bitcast(d.u32, d.i32)(layout.$.w[wb + 1]!);
return d.vec4f(
d.f32(std.extractBits(w0, r * 4, 4)),
d.f32(std.extractBits(w0, 16 + r * 4, 4)),
d.f32(std.extractBits(w1, r * 4, 4)),
d.f32(std.extractBits(w1, 16 + r * 4, 4)),
);
}
: (wb: number, r: number) => {
'use gpu';
return d.vec4f(
d.f32(std.extractBits(std.bitcast(d.u32, d.i32)(layout.$.w[wb]!), r * 8, 8)),
d.f32(std.extractBits(std.bitcast(d.u32, d.i32)(layout.$.w[wb + 1]!), r * 8, 8)),
d.f32(std.extractBits(std.bitcast(d.u32, d.i32)(layout.$.w[wb + 2]!), r * 8, 8)),
d.f32(std.extractBits(std.bitcast(d.u32, d.i32)(layout.$.w[wb + 3]!), r * 8, 8)),
);
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadRow is a second way of unpacking the weights, next to deqVec4 in quantCommon.ts. Could we keep just one? One thread could unpack a whole 4×4 block with four deqVec4 calls, swap rows and columns, and write the 4 rows into tileB. Right now 4 threads each read the same words to get their own row, so this might be faster too. Probably worth a quick bench:ab to check. Not blocking.

If you'd rather keep loadRow, could you update the comment in quantCommon.ts? It still says only two kernels read this layout.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried one thread per 4×4 block with four deqVec4 calls. Same output, but slower in bench:ab: 679 → 761 µs (+12%) on int4 [256,1024]×[1024,2304], 631 → 687 µs (+9%) on int8 [256,2048]×[2048,1024]. Only 16 of 64 threads load B that way. So I kept one row per thread and moved it into quantCommon.ts as deqRow, next to deqVec4. The comment there now lists all three kernels.

Comment thread packages/runntime/test/core/gpu/dispatch/matmulQuantW.test.ts Outdated
@chmjkb chmjkb added core Engine: graph ops, dispatch, buffer pool, weight loading enhancement New feature or request kernels GPU kernels and kernel routes labels Oct 27, 2026
…e quantW tests

- Unpack the tiled kernel's weight rows with deqRow in quantCommon.ts, next
  to deqVec4, and list every kernel that reads the layout.
- Keep a many-rows case on the plain kernel through an expert slice, add
  cases on each side of the tiled switch and name each case's kernel.
- Drop the dispatch routing test in favour of those GPU cases.
@xxpopielxx

Copy link
Copy Markdown
Member Author

rows past a workgroup of units (m = 70) now runs on the tiled kernel, so nothing tests the plain kernel with many rows anymore, though privacy-filter's experts still use it that way. Could you add a many-rows case that stays on the plain kernel (an expert slice), and a comment per case saying which kernel it takes? The "One route" comment at the top needs updating too.

Done. One thing though: privacy-filter runs its experts through moeUp/moeDown in forwardGather.

@xxpopielxx
xxpopielxx requested a review from chmjkb October 27, 2026 12:51
@chmjkb
chmjkb merged commit 6ab0501 into main Oct 27, 2026
2 checks passed
@chmjkb
chmjkb deleted the feat/quant-tiled-matmul branch October 27, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Engine: graph ops, dispatch, buffer pool, weight loading enhancement New feature or request kernels GPU kernels and kernel routes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants