Add optimized_w3: 3-bit LUT GEMM on a new mainloop - #36
Open
phi-jkim wants to merge 1 commit into
Open
Conversation
A drop-in 3-bit LUT decode GEMM for FLUTE (M<=64) that keeps FLUTE's learned non-uniform quant map and offline-pack model, but runs on a Marlin-derived mainloop (deep cp.async pipeline, striped Stream-K, fat register tiles, in-register prmt-LUT dequant) instead of FLUTE's CuTe mainloop. On the memory-bound decode band this raises DRAM utilisation ~36% -> ~68% and gives 1.13-1.50x over FLUTE's best 3-bit template per shape (do_bench, A100), at strictly tighter accuracy. Self-contained under tests/optimized_w3/: CUDA kernel + PyTorch binding, offline packing (nibble + mma-fragment perm), learned-qmap prmt-LUT, data-driven per-shape dispatch, an autotuner, a correctness+bench test, and docs (README, results, KERNEL_ANATOMY). The mainloop derives from Marlin (Frantar et al., IST-DASLab, Apache-2.0; attributed in csrc/); the W3 prmt-LUT dequant, the nibble+fragment pack, and the FLUTE-qmap integration are the additions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
optimized_w3 — a faster 3-bit LUT decode GEMM for FLUTE
A drop-in 3-bit LUT decode GEMM for FLUTE that keeps FLUTE's learned
non-uniform quant map and offline-pack model, but replaces the CuTe mainloop with
a memory-bound-optimal one. Across the entire M = 1–16 decode band it is
1.1–1.4× faster than FLUTE's best-tuned 3-bit kernel while being ~2×
tighter in accuracy — measured with FLUTE's own
do_benchon an A100. It liftsDRAM utilisation from ~36% → ~68%, i.e. onto the same bandwidth roofline the
W4 kernels hit.
Self-contained under
tests/optimized_w3/: CUDA kernel + PyTorch binding, offlinepacking, learned-qmap prmt-LUT, data-driven per-shape dispatch, an autotuner, a
correctness+bench test, and docs (README, results, KERNEL_ANATOMY).
Why this exists
Decode-phase GEMM at batch M ≤ 16 is memory-bound: it streams far more weight
bytes than it does math, so the roofline is DRAM bandwidth and any dequant work
not hidden under memory traffic is pure overhead on the critical path. FLUTE's
3-bit kernel dequantises via a two-plane bit-slice unpack plus a shared-memory
paired-LDS through the learned map, on a thin-tiled loop that hides latency by
running two CTAs/SM. That unpack + smem round-trip sits on the critical path
and the thin tile feeds each weight to only a few
mma, so the kernel stallsissue-bound at ~36% DRAM — the dequant is added to memory time, not hidden
under it.
optimized_w3 keeps the two things FLUTE gets right — the learned non-uniform
qmap (its accuracy edge) and the offline-pack model — and rebuilds the
mainloop so dequant is hidden under memory (
time ≈ max(mem, compute)),putting the kernel on the DRAM roofline at ~68%.
Core contributions (what's novel here)
The memory-bound mainloop structure is adapted from Marlin (Frantar et al.,
IST-DASLab — see Citation), which showed a low-M mixed-precision GEMM can be
held at near-peak DRAM bandwidth by running one fat CTA per SM with a deep
prefetch pipeline and a large register accumulator, instead of relying on
multi-CTA occupancy. Marlin's fast path, however, dequantises uniform W4
levels with a
lop3magic-bias arithmetic trick that does not apply to alearned, non-uniform map. The novel contributions here carry that learned-LUT
case onto the memory-bound mainloop:
In-register
prmt-LUT dequant for a learned map. The 8-entry learned qmapis held in registers as two fp16 byte-planes; dequant is a
prmt(bytepermute) that uses the fetched code word itself as the selector — 8 values
= 1 shift + 8
prmt, zero shared-memory traffic.prmtis the tablelookup, so FLUTE's non-uniform accuracy is preserved while FLUTE's smem
paired-LDS is eliminated entirely. This is the piece Marlin's arithmetic path
can't express.
Nibble + mma-fragment offline pack. Each 3-bit code is packed into its own
nibble, so the word loaded from DRAM is already the
prmtselector — nounpack ALU on the critical path (vs FLUTE's cross-plane shift/mask/recombine).
The packer also lays weights in
mma.syncfragment order (_base_perm), so alane's single coalesced 16-byte load lands directly in its
mmaB-fragmentregisters — no shared-memory round-trip for the weights at all.
FLUTE-qmap integration on the fat-tile mainloop. Wiring the learned map,
group scales, and offline pack through Marlin's fat register tile + 4-stage
cp.asyncpipeline so the dequant overlaps the weight loads. The fatregister accumulator is the lever that moves the roofline: each weight
fetched from DRAM now feeds many
mma, raising arithmetic intensity untilDRAM bandwidth, not instruction issue, bounds the kernel — this is the single
change that takes DRAM utilisation from ~36% to ~68%.
Adopted from Marlin (attributed, Apache-2.0): one CTA/SM at 256 threads, the
4-stage
cp.asyncpipeline with fully-unrolled static addressing, stripedStream-K with in-L2 serial reduction, and L2 evict-first streaming of the
single-use weights.
It's a co-design, not one knob
The same pipeline-depth knob has opposite sign on the two kernels
(28672×8192, M=16): this kernel at
Stages=2is slower than FLUTE (165 µs vs122 µs), and FLUTE at
Stages=4is slower than FLUTE at 2. Only the fat-tile ×deep-pipe product crosses (→ 88 µs). There is no incremental config-space path
from FLUTE — the crossing is the whole mainloop rewrite, which is why it ships as
a separate kernel.
Results — full M = 1–16 decode band (A100-SXM4-80GB)
triton.testing.do_bench(L2 flush, rep=100 — FLUTE's own tuning method), vsFLUTE's best 3-bit template per shape (autotuned, not just tid20). Both
kernels use identical output allocation (apples-to-apples). The speedup is
flat across M = 1–16 because both kernels are weight-bandwidth-bound and the
M×Kactivation is negligible — so the decode band is won uniformly, not justat one point.
Per-M detail (speedup at M ∈ {1, 2, 4, 8, 16}):
weight-bandwidth share), so it gets the biggest lift — ~88 µs at ~68% DRAM, the
W4 roofline.
dequant here rounds to fp16 before the accumulate → relative error is roughly
half FLUTE's own 3-bit output on every shape. Every autotune/test candidate
is exactness-gated (
err ≤ 2e-3and must not regress vs FLUTE) before it isallowed to be timed.
Reproduce:
python -m tests.optimized_w3.test_correctness(A100). Numbers aboveare a fresh
do_benchsweep; run-to-run variance is ±~0.03× on the ratio.Citation
The mainloop structure — one CTA/SM, deep
cp.asyncpipeline, fat registertiles, striped Stream-K, static unrolling — derives from Marlin:
The Apache-2.0 license header is preserved in
csrc/optimized_w3_kernel.cu. Thecontributions in this package are the W3-specific pieces Marlin's uniform-W4
path does not cover: the in-register
prmt-LUT dequant for a learned,non-uniform map, the nibble +
mma-fragment offline pack, and the FLUTE-qmapintegration on the fat-tile mainloop.