Skip to content

Commit 4f5ac5b

Browse files
committed
Add SM80 (Ampere/A100) dense MLA decode kernel
Implements a CUDA kernel for the dense MLA decode path on SM80 GPUs. Upstream currently supports SM90/SM100 only; this enables A100 deployment without forcing migration to Hopper hardware. Kernel design: - BLOCK_M=16, 4 warpgroups x 1 warp x V-quarter split (HEAD_DIM_V/4 cols/wg) - mma.m16n8k16 (BF16/FP16 with FP32 accumulator) - cp.async with double-buffered sK + cross-block prefetch - XOR swizzle (Swizzle<3,3,3>-style) for zero-bank-conflict SMEM access - cp.async.cg (L1 bypass) for K loading - SMEM 162 KB / CTA: 18 KB sQ + 2 x 72 KB sK, fits within 164 KB cap Functional coverage: - BF16 + FP16 - Multi-batch, multi-KV-head, causal mask - Split-K via the existing combine kernel (no changes to combine path) - Drop-in API compatibility: dense_decode_fwd signature unchanged Performance (A100-SXM4-80GB, 2039 GB/s peak HBM): - Peak: 490 GB/s on b=64 sk=4096 (24 percent of HBM peak) - Long-seq: 276 GB/s on b=1 sk=65536 - 9-117x speedup vs PyTorch eager BMM reference across the sweep Build: FLASH_MLA_DISABLE_SM100=1 FLASH_MLA_DISABLE_SM90=1 pip install -v . SM80-only is the supported configuration; SM80+SM90 combined builds need __launch_bounds__ portability fixes in upstream sm90 sources (deferred). Tests: - benchmark/bench_sm80_decode.py --check (correctness vs torch eager) - benchmark/profile_decode_step.py (DeepSeek-V3-shape step profile) Modifications outside csrc/sm80/: - csrc/api/{api.cpp,common.h,dense_decode.h}: SM80 arch dispatch - csrc/smxx/decode/combine/combine.cu: __CUDA_ARCH__>=900 guard for the PDL device intrinsic so the combine kernel compiles for sm_80 - csrc/smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.cu: drop the sm90-only third arg from __launch_bounds__ - setup.py: SM80 build flag and source list, plus include paths from pip-installed nvidia-* wheels (system CUDA may lack cusparse headers)
1 parent 9241ae3 commit 4f5ac5b

15 files changed

Lines changed: 1148 additions & 52 deletions

File tree

benchmark/bench_sm80_decode.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Benchmark for the SM80 dense MLA decode kernel.
2+
3+
Compares against a PyTorch eager (BMM-based) reference. The eager path is
4+
slow for long sequences -- iteration counts shrink accordingly. Reports
5+
latency, KV bandwidth, and speedup across a config sweep."""
6+
7+
import argparse
8+
import os
9+
import sys
10+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11+
12+
import torch
13+
import flash_mla.cuda as cuda
14+
15+
16+
def torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v, is_causal=False):
17+
"""PyTorch BMM-based MLA decode. Returns the same shape as the kernel output."""
18+
b, sq, hq, dk = q.shape
19+
_, pbs, hk, _ = kcache.shape
20+
nq_per_hk = hq // hk
21+
out = torch.zeros(b, sq, hq, head_size_v, dtype=q.dtype, device=q.device)
22+
for bi in range(b):
23+
sk = int(seqlens_k[bi].item())
24+
bt = block_table[bi]
25+
nb = (sk + pbs - 1) // pbs
26+
ks = [kcache[bt[bl].item()] for bl in range(nb)]
27+
kc = torch.cat(ks, dim=0)[:sk] # (sk, hk, dk)
28+
k_full = kc.transpose(0, 1).contiguous() # (hk, sk, dk)
29+
v_full = k_full[:, :, :head_size_v] # (hk, sk, dv)
30+
q_b = q[bi] # (sq, hq, dk)
31+
q_rs = (q_b.view(sq, hk, nq_per_hk, dk)
32+
.permute(1, 0, 2, 3)
33+
.reshape(hk, sq * nq_per_hk, dk))
34+
scores = torch.bmm(q_rs.float(), k_full.float().transpose(1, 2)) * softmax_scale
35+
if is_causal:
36+
for sq_idx in range(sq):
37+
rb = max(0, sk - (sq - sq_idx - 1))
38+
if rb < sk:
39+
for nq_idx in range(nq_per_hk):
40+
scores[:, sq_idx * nq_per_hk + nq_idx, rb:] = float('-inf')
41+
probs = torch.softmax(scores, dim=-1)
42+
o = torch.bmm(probs, v_full.float()) # (hk, q_per_hk, dv)
43+
o_rs = (o.view(hk, sq, nq_per_hk, head_size_v)
44+
.permute(1, 2, 0, 3)
45+
.reshape(sq, hq, head_size_v))
46+
out[bi] = o_rs.to(q.dtype)
47+
return out
48+
49+
50+
def bench(fn, iters, warmup):
51+
for _ in range(warmup):
52+
fn()
53+
torch.cuda.synchronize()
54+
s = torch.cuda.Event(enable_timing=True)
55+
e = torch.cuda.Event(enable_timing=True)
56+
s.record()
57+
for _ in range(iters):
58+
fn()
59+
e.record()
60+
torch.cuda.synchronize()
61+
return s.elapsed_time(e) / iters
62+
63+
64+
def make_inputs(batch, sq, hq, hk, sk, dtype, device):
65+
head_size_k = 576
66+
page_block_size = 64
67+
q = torch.randn(batch, sq, hq, head_size_k, dtype=dtype, device=device) * 0.1
68+
nb = (sk + page_block_size - 1) // page_block_size
69+
kcache = torch.randn(nb * batch, page_block_size, hk, head_size_k, dtype=dtype, device=device) * 0.1
70+
seqlens_k = torch.full((batch,), sk, dtype=torch.int32, device=device)
71+
block_table = torch.arange(nb * batch, dtype=torch.int32, device=device).view(batch, nb)
72+
return q, kcache, seqlens_k, block_table
73+
74+
75+
def main():
76+
parser = argparse.ArgumentParser()
77+
parser.add_argument('--dtype', default='bfloat16', choices=['bfloat16', 'float16'])
78+
parser.add_argument('--no-torch-baseline', action='store_true',
79+
help='skip PyTorch eager reference (much faster sweep)')
80+
parser.add_argument('--check', action='store_true',
81+
help='also run a one-shot correctness check vs eager')
82+
args = parser.parse_args()
83+
84+
torch.manual_seed(0)
85+
device = 'cuda'
86+
dtype = getattr(torch, args.dtype)
87+
head_size_k = 576
88+
head_size_v = 512
89+
softmax_scale = 1.0 / (head_size_k ** 0.5)
90+
91+
configs = [
92+
# (batch, sq, hq, hk, sk)
93+
(1, 1, 16, 1, 256),
94+
(1, 1, 16, 1, 1024),
95+
(1, 1, 16, 1, 4096),
96+
(1, 1, 16, 1, 16384),
97+
(1, 1, 16, 1, 65536),
98+
(4, 1, 16, 1, 1024),
99+
(4, 1, 16, 1, 4096),
100+
(16, 1, 16, 1, 1024),
101+
(16, 1, 16, 1, 4096),
102+
(64, 1, 16, 1, 1024),
103+
(64, 1, 16, 1, 4096),
104+
(1, 1, 64, 1, 4096),
105+
]
106+
107+
print(f'{"config":<32} {"ours(ms)":>9} {"torch(ms)":>10} {"speedup":>8} {"ours BW(GB/s)":>14}')
108+
print('-' * 75)
109+
for batch, sq, hq, hk, sk in configs:
110+
q, kcache, seqlens_k, block_table = make_inputs(batch, sq, hq, hk, sk, dtype, device)
111+
112+
ours_fn = lambda: cuda.dense_decode_fwd(
113+
q, kcache, head_size_v, seqlens_k, block_table, softmax_scale, False, None, None
114+
)
115+
ours_ms = bench(ours_fn, iters=200, warmup=20)
116+
kv_bytes = batch * hk * sk * head_size_k * 2
117+
bw = kv_bytes / (ours_ms * 1e-3) / 1e9
118+
119+
if args.no_torch_baseline:
120+
torch_str = 'skip'
121+
speedup_str = '-'
122+
else:
123+
iters = 5 if sk * batch >= 8192 else (20 if sk * batch >= 1024 else 50)
124+
warmup = 2
125+
torch_fn = lambda: torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v)
126+
torch_ms = bench(torch_fn, iters=iters, warmup=warmup)
127+
torch_str = f'{torch_ms:.3f}'
128+
speedup_str = f'{torch_ms / ours_ms:.1f}x'
129+
130+
if args.check:
131+
out, _, _, _ = ours_fn()
132+
ref = torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v)
133+
diff = (out.float() - ref.float()).abs().max().item()
134+
tag = 'OK' if diff < 0.02 else f'FAIL diff={diff:.4f}'
135+
speedup_str = f'{speedup_str} ({tag})'
136+
137+
cfg = f'b={batch} sq={sq} hq={hq} hk={hk} sk={sk}'
138+
print(f'{cfg:<32} {ours_ms:>9.3f} {torch_str:>10} {speedup_str:>8} {bw:>14.1f}')
139+
140+
141+
if __name__ == '__main__':
142+
main()

benchmark/profile_decode_step.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Profile dense_decode_fwd's share of an MLA decode step.
2+
3+
Uses a DeepSeek-V3-shaped 1-layer attention block:
4+
x (b, H) -> Q proj -> q (b, hq, dk)
5+
q + KV cache -> dense_decode_fwd -> o (b, hq, dv)
6+
o -> O proj -> y (b, H)
7+
8+
This is an under-estimate of full decode step time (no FFN / MoE / layernorm),
9+
which means dense_decode's measured share here is an UPPER bound on its share
10+
of a full step. If decode is < 30% even here, BLOCK_M=8 redesign (which gives
11+
+3-5pp on decode itself) won't move the full-step needle meaningfully."""
12+
13+
import argparse
14+
import os
15+
import sys
16+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17+
18+
import torch
19+
import flash_mla.cuda as cuda
20+
21+
22+
def time_fn(fn, iters=100, warmup=20):
23+
for _ in range(warmup):
24+
fn()
25+
torch.cuda.synchronize()
26+
s = torch.cuda.Event(enable_timing=True)
27+
e = torch.cuda.Event(enable_timing=True)
28+
s.record()
29+
for _ in range(iters):
30+
fn()
31+
e.record()
32+
torch.cuda.synchronize()
33+
return s.elapsed_time(e) / iters
34+
35+
36+
def profile(batch, seqlen, hidden, num_q_heads, num_kv_heads, head_dim_k, head_dim_v, dtype):
37+
device = 'cuda'
38+
pbs = 64
39+
softmax_scale = 1.0 / (head_dim_k ** 0.5)
40+
41+
# Linear projections (Q absorbed: hidden -> num_q_heads*head_dim_k).
42+
x = torch.randn(batch, hidden, dtype=dtype, device=device) * 0.1
43+
W_q = torch.randn(hidden, num_q_heads * head_dim_k, dtype=dtype, device=device) * 0.01
44+
W_o = torch.randn(num_q_heads * head_dim_v, hidden, dtype=dtype, device=device) * 0.01
45+
46+
# KV cache (paged, num_kv_heads heads).
47+
nb = (seqlen + pbs - 1) // pbs
48+
total_blocks = nb * batch
49+
kcache = torch.randn(total_blocks, pbs, num_kv_heads, head_dim_k, dtype=dtype, device=device) * 0.1
50+
seqlens_k = torch.full((batch,), seqlen, dtype=torch.int32, device=device)
51+
block_table = torch.arange(total_blocks, dtype=torch.int32, device=device).view(batch, nb)
52+
53+
def attn_block():
54+
q = (x @ W_q).view(batch, 1, num_q_heads, head_dim_k)
55+
out, _, _, _ = cuda.dense_decode_fwd(
56+
q, kcache, head_dim_v, seqlens_k, block_table,
57+
softmax_scale, False, None, None,
58+
)
59+
out_flat = out.contiguous().view(batch, num_q_heads * head_dim_v)
60+
return out_flat @ W_o
61+
62+
def attn_no_oproj():
63+
q = (x @ W_q).view(batch, 1, num_q_heads, head_dim_k)
64+
out, _, _, _ = cuda.dense_decode_fwd(
65+
q, kcache, head_dim_v, seqlens_k, block_table,
66+
softmax_scale, False, None, None,
67+
)
68+
return out
69+
70+
def decode_only():
71+
q = torch.randn(batch, 1, num_q_heads, head_dim_k, dtype=dtype, device=device) * 0.1
72+
out, _, _, _ = cuda.dense_decode_fwd(
73+
q, kcache, head_dim_v, seqlens_k, block_table,
74+
softmax_scale, False, None, None,
75+
)
76+
return out
77+
78+
full = time_fn(attn_block)
79+
no_op = time_fn(attn_no_oproj)
80+
only = time_fn(decode_only)
81+
qproj = no_op - only
82+
oproj = full - no_op
83+
share = only / full * 100.0
84+
return full, qproj, only, oproj, share
85+
86+
87+
def main():
88+
ap = argparse.ArgumentParser()
89+
ap.add_argument('--dtype', default='bfloat16', choices=['bfloat16', 'float16'])
90+
args = ap.parse_args()
91+
dtype = getattr(torch, args.dtype)
92+
93+
# DeepSeek-V3 architectural constants (MLA absorbed mode).
94+
HIDDEN = 7168
95+
NUM_Q_HEADS = 128
96+
NUM_KV_HEADS = 1
97+
HEAD_DIM_K = 576
98+
HEAD_DIM_V = 512
99+
100+
print(f'DeepSeek-V3-shaped 1-layer attention block, dtype={args.dtype}')
101+
print(f' hidden={HIDDEN} hq={NUM_Q_HEADS} hk={NUM_KV_HEADS} dk={HEAD_DIM_K} dv={HEAD_DIM_V}')
102+
print()
103+
print(f'{"config":<22} {"attn(ms)":>10} {"qproj":>8} {"decode":>8} {"oproj":>8} {"decode%":>9}')
104+
print('-' * 70)
105+
106+
configs = [
107+
# (batch, seqlen)
108+
(1, 1024), (1, 4096), (1, 16384), (1, 65536),
109+
(4, 1024), (4, 4096), (4, 16384),
110+
(16, 1024), (16, 4096), (16, 16384),
111+
(64, 1024), (64, 4096), (64, 16384),
112+
(128, 4096),
113+
]
114+
for b, sk in configs:
115+
try:
116+
full, qproj, only, oproj, share = profile(b, sk, HIDDEN, NUM_Q_HEADS, NUM_KV_HEADS, HEAD_DIM_K, HEAD_DIM_V, dtype)
117+
tag = f'b={b} sk={sk}'
118+
print(f'{tag:<22} {full:>10.3f} {qproj:>8.3f} {only:>8.3f} {oproj:>8.3f} {share:>8.1f}%')
119+
except torch.cuda.OutOfMemoryError:
120+
print(f'b={b} sk={sk}: OOM (skipped)')
121+
122+
print()
123+
print('Note: this 1-layer attention block excludes FFN/MoE/layernorm/residual,')
124+
print('which together typically dominate full step time. The "decode%" above')
125+
print('is therefore an UPPER bound on dense_decode share of a real decode step.')
126+
127+
128+
if __name__ == '__main__':
129+
main()

csrc/api/api.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
#include <pybind11/pybind11.h>
22

3+
#include "dense_decode.h"
4+
5+
#if !defined(FLASH_MLA_DISABLE_SM90)
36
#include "sparse_fwd.h"
47
#include "sparse_decode.h"
5-
#include "dense_decode.h"
8+
#endif
9+
10+
#if !defined(FLASH_MLA_DISABLE_SM100)
611
#include "dense_fwd.h"
12+
#endif
713

814
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
915
m.doc() = "FlashMLA";
10-
m.def("sparse_decode_fwd", &sparse_attn_decode_interface);
1116
m.def("dense_decode_fwd", &dense_attn_decode_interface);
17+
#if !defined(FLASH_MLA_DISABLE_SM90)
18+
m.def("sparse_decode_fwd", &sparse_attn_decode_interface);
1219
m.def("sparse_prefill_fwd", &sparse_attn_prefill_interface);
20+
#endif
21+
#if !defined(FLASH_MLA_DISABLE_SM100)
1322
m.def("dense_prefill_fwd", &FMHACutlassSM100FwdRun);
1423
m.def("dense_prefill_bwd", &FMHACutlassSM100BwdRun);
24+
#endif
1525
}

csrc/api/common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ struct Arch {
3131
num_sms = device_prop->multiProcessorCount;
3232
}
3333

34+
bool is_sm80() const {
35+
return major == 8 && minor == 0;
36+
}
37+
3438
bool is_sm90a() const {
3539
return major == 9 && minor == 0;
3640
}

csrc/api/dense_decode.h

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
#include "common.h"
77
#include "params.h"
88

9+
#ifndef FLASH_MLA_DISABLE_SM90
910
#include "sm90/decode/dense/splitkv_mla.h"
11+
#endif
12+
#ifndef FLASH_MLA_DISABLE_SM80
13+
#include "sm80/decode/dense/splitkv_mla.h"
14+
#endif
1015
#include "smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.h"
1116
#include "smxx/decode/combine/combine.h"
1217

@@ -24,8 +29,8 @@ dense_attn_decode_interface(
2429
) {
2530
// Check arch
2631
Arch arch = Arch();
27-
if (!arch.is_sm90a()) {
28-
TORCH_CHECK(false, "Dense decode MLA is only supported on SM90a architecture");
32+
if (!arch.is_sm90a() && !arch.is_sm80()) {
33+
TORCH_CHECK(false, "Dense decode MLA is only supported on SM80 or SM90a architectures");
2934
}
3035

3136
// Check data types
@@ -172,18 +177,45 @@ dense_attn_decode_interface(
172177

173178
params.stream = at::cuda::getCurrentCUDAStream().stream();
174179

180+
#define DISPATCH_DENSE_DECODE_KERNEL(SCALAR_T) \
181+
do { \
182+
if (arch.is_sm90a()) { \
183+
CALL_SM90_DENSE_DECODE(SCALAR_T); \
184+
} else if (arch.is_sm80()) { \
185+
CALL_SM80_DENSE_DECODE(SCALAR_T); \
186+
} else { \
187+
TORCH_CHECK(false, "Unsupported arch for dense MLA decode"); \
188+
} \
189+
} while (0)
190+
191+
#ifndef FLASH_MLA_DISABLE_SM90
192+
#define CALL_SM90_DENSE_DECODE(SCALAR_T) sm90::run_flash_splitkv_mla_kernel<SCALAR_T>(params)
193+
#else
194+
#define CALL_SM90_DENSE_DECODE(SCALAR_T) TORCH_CHECK(false, "FlashMLA was built with FLASH_MLA_DISABLE_SM90; cannot run on SM90 GPU")
195+
#endif
196+
197+
#ifndef FLASH_MLA_DISABLE_SM80
198+
#define CALL_SM80_DENSE_DECODE(SCALAR_T) sm80::run_flash_splitkv_mla_kernel<SCALAR_T>(params)
199+
#else
200+
#define CALL_SM80_DENSE_DECODE(SCALAR_T) TORCH_CHECK(false, "FlashMLA was built with FLASH_MLA_DISABLE_SM80; cannot run on SM80 GPU")
201+
#endif
202+
175203
if (q_dtype == torch::kBFloat16) {
176-
sm90::run_flash_splitkv_mla_kernel<cutlass::bfloat16_t>(params);
204+
DISPATCH_DENSE_DECODE_KERNEL(cutlass::bfloat16_t);
177205
} else if (q_dtype == torch::kHalf) {
178206
#ifdef FLASH_MLA_DISABLE_FP16
179207
TORCH_CHECK(false, "FlashMLA is compiled with -DFLASH_MLA_DISABLE_FP16. Please remove this flag from your environment and re-compile FlashMLA.");
180208
#else
181-
sm90::run_flash_splitkv_mla_kernel<cutlass::half_t>(params);
209+
DISPATCH_DENSE_DECODE_KERNEL(cutlass::half_t);
182210
#endif
183211
} else {
184-
TORCH_CHECK(false, "Unsupported dtype for dense MLA on SM90");
212+
TORCH_CHECK(false, "Unsupported dtype for dense MLA decode");
185213
}
186214

215+
#undef DISPATCH_DENSE_DECODE_KERNEL
216+
#undef CALL_SM90_DENSE_DECODE
217+
#undef CALL_SM80_DENSE_DECODE
218+
187219
CombineParams combine_params = {
188220
batch_size, seqlen_q_ori,
189221
num_heads_q, head_size_v,

csrc/sm80/decode/dense/config.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
3+
namespace sm80::Config {
4+
5+
static constexpr int BLOCK_SIZE_M = 64;
6+
static constexpr int PAGE_BLOCK_SIZE = 64;
7+
8+
static constexpr int HEAD_DIM_K = 576;
9+
static constexpr int HEAD_DIM_V = 512;
10+
11+
static constexpr int NUM_THREADS = 128;
12+
13+
}

0 commit comments

Comments
 (0)