Skip to content

Commit 020a10c

Browse files
committed
[executorch][cuda] Add a length-aware Triton SDPA over per-sequence KV columns
Pull Request resolved: #24072 Adds `triton::sdpa_varlen`, the attention kernel the CUDA batched-sequence KV cache needs. Each resident sequence owns one column of a `[rows, cols, H, D]` store, and a device segment table `seq_info Int32[S, 4] = (q_start, q_len, col, kv_end)` says which packed query rows belong to which column and how long that sequence is. Lengths are read on device, so one captured CUDA graph serves any mix of sequence lengths, as the MLX per-sequence SDPA does per group. Covers causal, non-causal (DFlash draft blocks), sliding window and ring storage, GQA, rows no segment covers (zeroed), and split-K for short steps. A symbolic query length takes the generic path without host-side length branches, so exporting it with a dynamic step width does not pin that width. It is a new op in its own file; `triton::sdpa` and its dispatch are untouched. ghstack-source-id: 469359730 @exported-using-ghexport Differential Revision: [D125987296](https://our.internmc.facebook.com/intern/diff/D125987296/)
1 parent 4eb094b commit 020a10c

6 files changed

Lines changed: 1175 additions & 0 deletions

File tree

‎backends/cuda/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ fbcode_target(
178178
"triton/kernels/quantized_gemm_family.py",
179179
"triton/kernels/quantized_gemm_utils.py",
180180
"triton/kernels/sdpa.py",
181+
"triton/kernels/sdpa_varlen.py",
181182
"triton/kernels/topk.py",
182183
"triton/kernels/tq4_sdpa.py",
183184
"triton/kernels/triton_mutation_cache.py",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Benchmark triton::sdpa_varlen against the cell layout's triton::sdpa.
8+
9+
Muse Glimmer 30B attention (H_q=32, H_kv=2, D=128). Each case is a step of
10+
the batching runtime: per sequence, the query rows it adds and the context it
11+
then holds. The cell baseline sweeps one shared pool holding every sequence,
12+
masked per row; sdpa_varlen reads each sequence's own column.
13+
14+
usage: python bench_sdpa_varlen.py [--check]
15+
"""
16+
17+
import argparse
18+
19+
import torch
20+
import triton
21+
22+
from executorch.backends.cuda.triton.kernels.sdpa import sdpa
23+
from executorch.backends.cuda.triton.kernels.sdpa_varlen import sdpa_varlen
24+
25+
HQ, HKV, D = 32, 2, 128
26+
WINDOW = 2048
27+
28+
# (name, [(query rows, context after the step)] per sequence)
29+
CASES = [
30+
("decode c1 ctx600", [(1, 600)]),
31+
("decode c4 ctx600", [(1, 600)] * 4),
32+
("decode c4 ctx4k", [(1, 4096)] * 4),
33+
("decode c4 mixed", [(1, 300), (1, 900), (1, 2500), (1, 4096)]),
34+
("verify c1 16x600", [(16, 600)]),
35+
("verify c4 16x600", [(16, 600)] * 4),
36+
("verify c4 16x4k", [(16, 4096)] * 4),
37+
("prefill 97", [(97, 97)]),
38+
("prefill 512 + 3 decode", [(512, 512), (1, 600), (1, 600), (1, 600)]),
39+
("prefill 4096", [(4096, 4096)]),
40+
]
41+
42+
43+
def _step(case, window, device="cuda"):
44+
t = sum(q for q, _ in case)
45+
cols = len(case) + 1
46+
rows = max(c for _, c in case)
47+
q = torch.randn(1, HQ, t, D, device=device, dtype=torch.bfloat16)
48+
k_seq = torch.randn(rows, cols, HKV, D, device=device, dtype=torch.bfloat16)
49+
v_seq = torch.randn_like(k_seq)
50+
info, start = [], 0
51+
for col, (q_len, ctx) in enumerate(case):
52+
info.append((start, q_len, col, ctx))
53+
start += q_len
54+
info = torch.tensor(info, dtype=torch.int32, device=device)
55+
56+
# Cell layout: every sequence's context packed into one pool, each row's
57+
# visibility in an explicit mask, the sweep bounded by the occupied extent.
58+
pool = sum(c for _, c in case)
59+
k_pool = torch.empty(1, pool, HKV, D, device=device, dtype=torch.bfloat16)
60+
v_pool = torch.empty_like(k_pool)
61+
mask = torch.zeros(1, 1, t, pool, device=device, dtype=torch.bool)
62+
base, row = 0, 0
63+
for col, (q_len, ctx) in enumerate(case):
64+
k_pool[0, base : base + ctx] = k_seq[:ctx, col]
65+
v_pool[0, base : base + ctx] = v_seq[:ctx, col]
66+
for r in range(q_len):
67+
p = ctx - q_len + r
68+
lo = max(0, p - window + 1) if window else 0
69+
mask[0, 0, row + r, base + lo : base + p + 1] = True
70+
base += ctx
71+
row += q_len
72+
read_len = torch.tensor([pool], device=device, dtype=torch.int64)
73+
return q, k_seq, v_seq, info, k_pool, v_pool, mask, read_len
74+
75+
76+
def _time(fn):
77+
fn()
78+
torch.cuda.synchronize()
79+
graph = torch.cuda.CUDAGraph()
80+
with torch.cuda.graph(graph):
81+
fn()
82+
return triton.testing.do_bench(graph.replay, warmup=25, rep=200)
83+
84+
85+
def main():
86+
parser = argparse.ArgumentParser()
87+
parser.add_argument("--check", action="store_true")
88+
args = parser.parse_args()
89+
torch.manual_seed(0)
90+
scale = D**-0.5
91+
print(
92+
f"{'case':28s} {'window':>6s} {'cell ms':>9s} {'varlen ms':>10s} {'speedup':>8s}"
93+
)
94+
for name, case in CASES:
95+
for window in (0, WINDOW):
96+
q, k, v, info, k_pool, v_pool, mask, read_len = _step(case, window)
97+
step_args = (q, k, v, info, scale, True, window, 0)
98+
cell_args = (
99+
q,
100+
k_pool.transpose(1, 2),
101+
v_pool.transpose(1, 2),
102+
mask,
103+
0.0,
104+
False,
105+
scale,
106+
True,
107+
read_len,
108+
)
109+
110+
def varlen(args=step_args):
111+
return sdpa_varlen(*args)
112+
113+
def cell(args=cell_args):
114+
return sdpa(*args)
115+
116+
if args.check:
117+
torch.testing.assert_close(
118+
varlen().float(), cell().float(), atol=2e-2, rtol=2e-2
119+
)
120+
t_cell, t_var = _time(cell), _time(varlen)
121+
print(
122+
f"{name:28s} {window:6d} {t_cell:9.4f} {t_var:10.4f} {t_cell / t_var:7.2f}x"
123+
)
124+
125+
126+
if __name__ == "__main__":
127+
main()

‎backends/cuda/tests/targets.bzl‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ def define_common_targets(is_fbcode = False):
134134
),
135135
)
136136

137+
python_unittest_remote_gpu(
138+
name = "test_sdpa_varlen",
139+
srcs = [
140+
"test_sdpa_varlen.py",
141+
],
142+
visibility = [
143+
"//executorch/...",
144+
],
145+
deps = [
146+
"//caffe2:torch",
147+
"//executorch/backends/cuda:triton_kernels",
148+
"//executorch/examples/models/muse-glimmer/model:dflash_model",
149+
],
150+
keep_gpu_sections = True,
151+
remote_execution = re_test_utils.remote_execution(
152+
platform = "gpu-remote-execution",
153+
subplatform = "A100-exclusive",
154+
),
155+
)
156+
137157
python_unittest_remote_gpu(
138158
name = "test_int4_large_quantized_gemm",
139159
srcs = [

0 commit comments

Comments
 (0)