feat(attention): add architecture-owned SM12x FA4 kernels (#32991)

This commit is contained in:
Gabriel Wu
2026-08-12 23:58:42 -07:00
committed by GitHub
parent ebc144ce3f
commit 889c2f31aa
16 changed files with 10449 additions and 198 deletions
@@ -0,0 +1 @@
"""SGLang-owned FlashAttention-4 kernels and launch policy for SM120."""
@@ -0,0 +1,58 @@
# Copyright (c) 2026, SGLang Team.
"""Lightweight dispatch bridge for SGLang-owned SM120 FA4 kernels.
The vendored FA4 interface dispatches through this module so the SM120
implementation and its launch state remain outside ``flash_attn/cute``.
"""
from functools import lru_cache
@lru_cache(maxsize=None)
def get_forward_host(arch: int):
"""Return the optional architecture-owned forward host."""
if arch // 10 == 12:
from sglang.kernels.ops.attention.fa4_sm120.runtime import (
sm120_forward_host,
)
return sm120_forward_host
return None
def resolve_runtime_policy(
*,
device_capability: tuple[int, int],
deterministic: bool,
) -> tuple[int, int, bool]:
"""Resolve generic and architecture-owned SplitKV launch policy."""
arch = device_capability[0] * 10 + device_capability[1]
uses_arch_decode_policy = get_forward_host(arch) is not None
no_splitkv = device_capability < (9, 0) or uses_arch_decode_policy
num_splits = 1 if deterministic or no_splitkv else 0
decode_num_splits = (
0 if uses_arch_decode_policy and not deterministic else num_splits
)
return num_splits, decode_num_splits, uses_arch_decode_policy
@lru_cache(maxsize=None)
def get_forward_arch(device) -> int | None:
"""Return the device arch when it has an architecture-owned forward host."""
import torch
major, minor = torch.cuda.get_device_capability(device)
arch = major * 10 + minor
return arch if get_forward_host(arch) is not None else None
def try_cached_paged_decode(*, arch: int, **kwargs):
"""Try an architecture-owned paged-decode launch plan."""
host = get_forward_host(arch)
return None if host is None else host.try_paged_decode(arch=arch, **kwargs)
def try_cached_varlen(*, arch: int, **kwargs):
"""Try an architecture-owned varlen launch plan."""
host = get_forward_host(arch)
return None if host is None else host.try_varlen(arch=arch, **kwargs)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,711 @@
# Copyright (c) 2026, SGLang Team.
"""End-to-end transposed SM120 paged-decode specialization.
This path keeps the packed query axis on the N=8 dimension of warp MMA:
scores.T = K @ Q.T # (64, 8)
output.T = V.T @ P.T # (256, 8)
The dataflow is isolated from the general SM120 kernel because its page-TMA
transport, column-wise online softmax, and transposed epilogue form one compile
specialization.
"""
import math
from typing import Optional
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, const_expr
from cutlass.cute.nvgpu import warp
from cutlass.pipeline import PipelineAsync, PipelineState
from quack import layout_utils
from sglang.kernels.ops.attention.fa4_sm120.flash_fwd import (
FlashAttentionForwardSm120,
)
from sglang.kernels.ops.attention.flash_attn.cute import utils
from sglang.kernels.ops.attention.flash_attn.cute.block_info import BlockInfo
from sglang.kernels.ops.attention.flash_attn.cute.named_barrier import NamedBarrierFwd
from sglang.kernels.ops.attention.flash_attn.cute.pack_gqa import PackGQA
from sglang.kernels.ops.attention.flash_attn.cute.seqlen_info import SeqlenInfoQK
from sglang.kernels.ops.attention.flash_attn.cute.utils import AuxData
class FlashAttentionForwardSm120DecodeTranspose(FlashAttentionForwardSm120):
"""M64N8 QK and transposed PV for qualified packed single-token decode."""
# Paged TMA is part of this kernel's dataflow, not a runtime tuning knob.
# Keeping it on the distinct class identity prevents a gather-compiled
# specialization from being reused for the TMA tensor layout.
paged_tma = True
query_mma_n = 8
query_in_regs = True
def _uses_n_distributed_qk(self) -> bool:
# Reuse the base kernel's four-consumer-warp dispatch. All four warps
# participate in both transposed MMA phases.
return True
def _uses_split_pv_warps(self) -> bool:
# Experimental mixed-HDV channel slices retain the same shared P
# handoff even though they are outside the base kernel's qualified
# (HDQ, HDV) configuration table.
return True
def _setup_attributes(self):
super()._setup_attributes()
# The tiled MMA is deliberately warp-local. The base kernel normally
# derives its cooperative-group size from TiledMma.size, which would
# expose only one consumer warp here. Four physical consumer warps
# instead operate on disjoint K rows.
self.num_qk_threads = self.num_threads
self.num_mma_threads = self.num_threads
self.num_Q_load_threads = self.num_threads
self.num_epilogue_threads = self.num_threads
def _get_tiled_mma(self):
mma_op = warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16))
tiled_mma_qk = cute.make_tiled_mma(
mma_op,
(1, 1, 1),
permutation_mnk=(16, self.query_mma_n, 16),
)
# Each warp covers 64 value rows; the four disjoint SMEM views cover
# HDV=256 without a CTA-level M permutation.
tiled_mma_pv = cute.make_tiled_mma(
mma_op,
(1, 1, 1),
permutation_mnk=(64, self.query_mma_n, 16),
)
return tiled_mma_qk, tiled_mma_pv
@cute.jit
def _gemm_n8(
self,
tiled_mma: cute.TiledMma,
acc: cute.Tensor,
tCrA: cute.Tensor,
tCrB: cute.Tensor,
tCsA: cute.Tensor,
tCsB: cute.Tensor,
smem_thr_copy_A: cute.TiledCopy,
smem_thr_copy_B: cute.TiledCopy,
B_in_regs: cutlass.Constexpr[bool] = False,
):
"""Issue an N=8 warp-MMA mainloop through the underlying MMA atom.
CuTe DSL's tiled-MMA verifier rejects m16n8k16 when logical N is
exactly eight because it compares the raw A value mode against the C
value mode. The hardware atom itself has the correct native fragment
contract, so keep tiling for partitioning/copies and issue GEMM through
that atom.
"""
mma_atom = cute.make_mma_atom(tiled_mma.op)
tCrA_copy_view = smem_thr_copy_A.retile(tCrA)
tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
cute.copy(
smem_thr_copy_A,
tCsA[None, None, 0],
tCrA_copy_view[None, None, 0],
)
if const_expr(not B_in_regs):
cute.copy(
smem_thr_copy_B,
tCsB[None, None, 0],
tCrB_copy_view[None, None, 0],
)
for k in cutlass.range_constexpr(cute.size(tCsA.shape[2])):
if k < cute.size(tCsA.shape[2]) - 1:
cute.copy(
smem_thr_copy_A,
tCsA[None, None, k + 1],
tCrA_copy_view[None, None, k + 1],
)
if const_expr(not B_in_regs):
cute.copy(
smem_thr_copy_B,
tCsB[None, None, k + 1],
tCrB_copy_view[None, None, k + 1],
)
cute.gemm(
mma_atom,
acc,
tCrA[None, None, k],
tCrB[None, None, k],
acc,
)
@cute.jit
def _rescale_transposed_o(
self,
acc_O: cute.Tensor,
tiled_mma_pv: cute.TiledMma,
tidx: Int32,
sRowScale: cute.Tensor,
scale_row: cutlass.Constexpr[int],
):
lane_idx = tidx % cute.arch.WARP_SIZE
thr_mma_pv = tiled_mma_pv.get_slice(lane_idx)
acc_O_qd = layout_utils.reshape_acc_to_mn(acc_O, transpose=True)
cO = cute.make_identity_tensor((64, self.query_mma_n))
tOcO_qd = layout_utils.reshape_acc_to_mn(
thr_mma_pv.partition_C(cO), transpose=True
)
for r in cutlass.range(cute.size(acc_O_qd, mode=[0]), unroll_full=True):
query_row = tOcO_qd[r, 0][1]
acc_O_qd[r, None].store(
acc_O_qd[r, None].load() * sRowScale[scale_row, query_row]
)
@cute.jit
def _compute_one_n_block_transposed(
self,
n_block: Int32,
consumer_state: PipelineState,
acc_O: cute.Tensor,
sQ: cute.Tensor,
sK: cute.Tensor,
sV: cute.Tensor,
sP: cute.Tensor,
sRowScale: cute.Tensor,
pipeline_k: PipelineAsync,
pipeline_v: PipelineAsync,
tiled_mma_qk: cute.TiledMma,
tiled_mma_pv: cute.TiledMma,
smem_thr_copy_K: cute.TiledCopy,
smem_thr_copy_Q: cute.TiledCopy,
smem_thr_copy_V: cute.TiledCopy,
smem_thr_copy_P: cute.TiledCopy,
tSrK: cute.Tensor,
tSrQ: cute.Tensor,
tOrV: cute.Tensor,
tOrP: cute.Tensor,
tKsK: cute.Tensor,
tQsQ: cute.Tensor,
tVsV: cute.Tensor,
tPsP: cute.Tensor,
tidx: Int32,
softmax_scale_log2: Float32,
seqlen: SeqlenInfoQK,
window_size_left: Optional[Int32],
is_first_n_block: cutlass.Constexpr[bool] = False,
):
num_qk_warps = const_expr(4)
local_sum_base = const_expr(num_qk_warps)
global_max_row = const_expr(2 * num_qk_warps)
global_sum_row = const_expr(global_max_row + 1)
old_o_scale_row = const_expr(global_max_row + 2)
warp_scale_base = const_expr(global_max_row + 3)
p_stage = consumer_state.index
warp_idx = tidx // cute.arch.WARP_SIZE
lane_idx = tidx % cute.arch.WARP_SIZE
key_row_base = warp_idx * const_expr(16)
k_wait_token = pipeline_k.consumer_try_wait(consumer_state)
pipeline_k.consumer_wait(consumer_state, k_wait_token)
thr_mma_qk = tiled_mma_qk.get_slice(lane_idx)
acc_shape_S = thr_mma_qk.partition_shape_C((16, self.query_mma_n))
acc_S = cute.make_rmem_tensor(acc_shape_S, Float32)
acc_S.fill(0.0)
self._gemm_n8(
tiled_mma_qk,
acc_S,
tSrK,
tSrQ,
tKsK[None, None, None, p_stage],
tQsQ,
smem_thr_copy_K,
smem_thr_copy_Q,
B_in_regs=self.query_in_regs,
)
pipeline_k.consumer_release(consumer_state)
acc_S_qk = layout_utils.reshape_acc_to_mn(acc_S, transpose=True)
cS = cute.make_identity_tensor((16, self.query_mma_n))
tScS_qk = layout_utils.reshape_acc_to_mn(
thr_mma_qk.partition_C(cS), transpose=True
)
num_query_rows = cute.size(acc_S_qk, mode=[0])
row_max_local = cute.make_rmem_tensor(num_query_rows, Float32)
row_sum_local = cute.make_rmem_tensor(num_query_rows, Float32)
for r in cutlass.range(num_query_rows, unroll_full=True):
# The final paged tile can be only partially populated. The page
# gather leaves invalid SMEM rows untouched. Keep the test outside
# the unrolled fragment loop so complete 64-token tiles pay no
# per-element coordinate/comparison cost.
tile_start = n_block * self.tile_n
local_window_start = (
cutlass.max(
seqlen.seqlen_k - 1 - window_size_left,
0,
)
if const_expr(self.is_local and window_size_left is not None)
else Int32(0)
)
if tile_start + self.tile_n > seqlen.seqlen_k or (
const_expr(self.is_local and window_size_left is not None)
and tile_start < local_window_start
):
for c in cutlass.range(cute.size(acc_S_qk, mode=[1]), unroll_full=True):
key_row = tile_start + key_row_base + tScS_qk[r, c][0]
if key_row >= seqlen.seqlen_k or (
const_expr(self.is_local and window_size_left is not None)
and key_row < local_window_start
):
acc_S_qk[r, c] = -Float32.inf
acc_S_row = acc_S_qk[r, None].load()
row_max = utils.fmax_reduce(acc_S_row)
# In an m16n8 accumulator, lanes with the same low two lane
# bits own the same pair of N/query columns. Reducing across M
# (keys) therefore uses the strided lane group
# {lane, lane^4, lane^8, lane^16}, not a contiguous width-4
# group as in the ordinary row-wise QK layout.
for offset in cutlass.range_constexpr(2, 5):
row_max = utils.fmax(
row_max,
cute.arch.shuffle_sync_bfly(row_max, offset=1 << offset),
)
row_max_safe = 0.0 if row_max == -Float32.inf else row_max
acc_S_row_exp = cute.math.exp2(
(acc_S_row - row_max_safe) * softmax_scale_log2,
fastmath=True,
)
row_sum = utils.fadd_reduce(acc_S_row_exp)
for offset in cutlass.range_constexpr(2, 5):
row_sum += cute.arch.shuffle_sync_bfly(row_sum, offset=1 << offset)
row_max_local[r] = row_max
row_sum_local[r] = row_sum
acc_S_qk[r, None].store(acc_S_row_exp)
keys_per_warp = const_expr(self.tile_n // num_qk_warps)
if tScS_qk[0, 0][0] % keys_per_warp == 0:
for r in cutlass.range(num_query_rows, unroll_full=True):
query_row = tScS_qk[r, 0][1]
sRowScale[warp_idx, query_row] = row_max_local[r]
sRowScale[local_sum_base + warp_idx, query_row] = row_sum_local[r]
cute.arch.fence_view_async_shared()
cute.arch.barrier(
barrier_id=int(NamedBarrierFwd.PFull),
number_of_threads=self.num_mma_threads,
)
if tidx < self.query_mma_n:
query_row = tidx
row_max = sRowScale[0, query_row]
for warp_idx_it in cutlass.range_constexpr(1, num_qk_warps):
row_max = utils.fmax(row_max, sRowScale[warp_idx_it, query_row])
row_max_prev = (
row_max
if const_expr(is_first_n_block)
else sRowScale[global_max_row, query_row]
)
row_max_new = (
row_max
if const_expr(is_first_n_block)
else utils.fmax(row_max_prev, row_max)
)
row_max_new_safe = 0.0 if row_max_new == -Float32.inf else row_max_new
old_o_scale = (
1.0
if const_expr(is_first_n_block)
else cute.math.exp2(
(row_max_prev - row_max_new_safe) * softmax_scale_log2,
fastmath=True,
)
)
row_sum_new = (
0.0
if const_expr(is_first_n_block)
else sRowScale[global_sum_row, query_row] * old_o_scale
)
for warp_idx_it in cutlass.range_constexpr(num_qk_warps):
warp_scale = cute.math.exp2(
(sRowScale[warp_idx_it, query_row] - row_max_new_safe)
* softmax_scale_log2,
fastmath=True,
)
sRowScale[warp_scale_base + warp_idx_it, query_row] = warp_scale
row_sum_new += (
sRowScale[local_sum_base + warp_idx_it, query_row] * warp_scale
)
sRowScale[global_max_row, query_row] = row_max_new
sRowScale[global_sum_row, query_row] = row_sum_new
if const_expr(not is_first_n_block):
sRowScale[old_o_scale_row, query_row] = old_o_scale
cute.arch.fence_view_async_shared()
cute.arch.barrier(
barrier_id=int(NamedBarrierFwd.PEmpty),
number_of_threads=self.num_mma_threads,
)
for r in cutlass.range(num_query_rows, unroll_full=True):
query_row = tScS_qk[r, 0][1]
warp_scale = sRowScale[warp_scale_base + warp_idx, query_row]
acc_S_qk[r, None].store(acc_S_qk[r, None].load() * warp_scale)
for r in cutlass.range(num_query_rows, unroll_full=True):
query_row = tScS_qk[r, 0][1]
for c in cutlass.range(cute.size(acc_S_qk, mode=[1]), unroll_full=True):
key_row = key_row_base + tScS_qk[r, c][0]
sP[query_row, key_row, p_stage] = self.dtype(acc_S_qk[r, c])
cute.arch.fence_view_async_shared()
cute.arch.barrier(
barrier_id=int(NamedBarrierFwd.PFull),
number_of_threads=self.num_mma_threads,
)
# acc_O is zero-initialized immediately before the first KV tile, so
# multiplying it by that tile's compile-time unit scale is pure
# overhead. Later tiles still rescale accumulated output before PV.
if const_expr(not is_first_n_block):
self._rescale_transposed_o(
acc_O,
tiled_mma_pv,
tidx,
sRowScale,
old_o_scale_row,
)
v_wait_token = pipeline_v.consumer_try_wait(consumer_state)
pipeline_v.consumer_wait(consumer_state, v_wait_token)
self._gemm_n8(
tiled_mma_pv,
acc_O,
tOrV,
tOrP,
tVsV[None, None, None, p_stage],
tPsP[None, None, None, p_stage],
smem_thr_copy_V,
smem_thr_copy_P,
)
pipeline_v.consumer_release(consumer_state)
cute.arch.barrier(
barrier_id=int(NamedBarrierFwd.PEmpty),
number_of_threads=self.num_mma_threads,
)
consumer_state.advance()
return consumer_state
@cute.jit
def _store_transposed_output(
self,
acc_O: cute.Tensor,
mO: cute.Tensor,
mLSE: Optional[cute.Tensor],
sLSE: cute.Tensor,
tiled_mma_pv: cute.TiledMma,
tidx: Int32,
seqlen: SeqlenInfoQK,
m_block: Int32,
head_idx: Int32,
batch_idx: Int32,
split_idx: Int32,
):
row_limit = seqlen.seqlen_q * self.qhead_per_kvhead
warp_idx = tidx // cute.arch.WARP_SIZE
lane_idx = tidx % cute.arch.WARP_SIZE
thr_mma_pv = tiled_mma_pv.get_slice(lane_idx)
acc_O_qd = layout_utils.reshape_acc_to_mn(acc_O, transpose=True)
cO = cute.make_identity_tensor((64, self.query_mma_n))
tOcO_qd = layout_utils.reshape_acc_to_mn(
thr_mma_pv.partition_C(cO), transpose=True
)
mO_cur = (
seqlen.offset_batch_Q(mO, batch_idx, dim=3)[None, None, head_idx, split_idx]
if const_expr(self.is_split_kv)
else seqlen.offset_batch_Q(mO, batch_idx, dim=3)[None, None, head_idx]
)
for r in cutlass.range(cute.size(acc_O_qd, mode=[0]), unroll_full=True):
query_row_local = tOcO_qd[r, 0][1]
query_row = m_block * self.tile_m + query_row_local
if query_row < row_limit:
for c in cutlass.range(cute.size(acc_O_qd, mode=[1]), unroll_full=True):
value_col = warp_idx * const_expr(64) + tOcO_qd[r, c][0]
mO_cur[query_row, value_col] = self.dtype(acc_O_qd[r, c])
if const_expr(mLSE is not None):
if tidx < self.query_mma_n:
query_row = m_block * self.tile_m + tidx
if query_row < row_limit:
mLSE_cur = (
seqlen.offset_batch_Q(mLSE, batch_idx, dim=2)[
None, head_idx, split_idx
]
if const_expr(self.is_split_kv)
else seqlen.offset_batch_Q(mLSE, batch_idx, dim=2)[
None, head_idx
]
)
mLSE_cur[query_row] = sLSE[tidx]
@cute.jit
def mma(
self,
mQ: cute.Tensor,
mO: cute.Tensor,
mLSE: Optional[cute.Tensor],
sQ: cute.Tensor,
sK: cute.Tensor,
sV: cute.Tensor,
sO: cute.Tensor,
sP: Optional[cute.Tensor],
sRowScale: Optional[cute.Tensor],
sLSE: Optional[cute.Tensor],
learnable_sink: Optional[cute.Tensor],
pipeline_k: PipelineAsync,
pipeline_v: PipelineAsync,
gmem_tiled_copy_Q: cute.TiledCopy,
gmem_tiled_copy_O: cute.TiledCopy,
tiled_mma_qk: cute.TiledMma,
tiled_mma_pv: cute.TiledMma,
tidx: Int32,
softmax_scale_log2: Float32,
softmax_scale: Optional[Float32],
consumer_state: PipelineState,
block_info: BlockInfo,
seqlen: SeqlenInfoQK,
n_block_min: Int32,
n_block_max: Int32,
m_block: Int32,
head_idx: Int32,
batch_idx: Int32,
split_idx: Int32,
is_qk_owner: cutlass.Constexpr[bool],
aux_data: AuxData = AuxData(),
fastdiv_mods=None,
):
assert self.paged_kv
assert self.pack_gqa
assert self.is_causal or self.is_local
assert self.score_mod is None
assert self.mask_mod is None
assert self.tile_m == 16 and self.tile_n == 64
assert self.tile_hdim == 256
assert self.tile_hdimv in (64, 256)
assert self.qhead_per_kvhead <= self.query_mma_n
assert is_qk_owner
mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
warp_idx = tidx // cute.arch.WARP_SIZE
lane_idx = tidx % cute.arch.WARP_SIZE
thr_mma_qk = tiled_mma_qk.get_slice(lane_idx)
thr_mma_pv = tiled_mma_pv.get_slice(lane_idx)
acc_shape_O = thr_mma_pv.partition_shape_C((64, self.query_mma_n))
acc_O = cute.make_rmem_tensor(acc_shape_O, Float32)
acc_O.fill(0.0)
smem_copy_atom_k_major = cute.make_copy_atom(
warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4),
self.dtype,
)
smem_copy_atom_v_major = cute.make_copy_atom(
warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4),
self.dtype,
)
smem_thr_copy_K = utils.make_tiled_copy_A(
smem_copy_atom_k_major, tiled_mma_qk
).get_slice(lane_idx)
smem_thr_copy_Q = utils.make_tiled_copy_B(
smem_copy_atom_k_major, tiled_mma_qk
).get_slice(lane_idx)
smem_thr_copy_V = utils.make_tiled_copy_A(
smem_copy_atom_v_major, tiled_mma_pv
).get_slice(lane_idx)
smem_thr_copy_P = utils.make_tiled_copy_B(
smem_copy_atom_k_major, tiled_mma_pv
).get_slice(lane_idx)
sK_warp = cute.local_tile(
sK,
(16, self.tile_hdim, self._num_k_stages()),
(warp_idx, 0, 0),
)
sQ_query = cute.local_tile(sQ, (self.query_mma_n, self.tile_hdim), (0, 0))
sP_query = cute.local_tile(
sP,
(self.query_mma_n, self.tile_n, self._num_p_stages()),
(0, 0, 0),
)
tSrK = thr_mma_qk.make_fragment_A(
thr_mma_qk.partition_A(sK_warp[None, None, 0])
)
tSrQ = thr_mma_qk.make_fragment_B(thr_mma_qk.partition_B(sQ_query))
sV_warp = cute.local_tile(
sV,
(64, self.tile_n, self._num_v_stages()),
(warp_idx, 0, 0),
)
tOrV = thr_mma_pv.make_fragment_A(
thr_mma_pv.partition_A(sV_warp[None, None, 0])
)
tOrP = thr_mma_pv.make_fragment_B(
thr_mma_pv.partition_B(sP_query[None, None, 0])
)
tVsV = smem_thr_copy_V.partition_S(sV_warp)
tKsK = smem_thr_copy_K.partition_S(sK_warp)
tQsQ = smem_thr_copy_Q.partition_S(sQ_query)
tPsP = smem_thr_copy_P.partition_S(sP_query)
PackGQA(
self.tile_m,
self.tile_hdim,
self.check_hdim_oob,
self.qhead_per_kvhead,
).load_Q(
mQ_cur,
sQ,
gmem_tiled_copy_Q,
tidx,
m_block,
seqlen.seqlen_q,
)
cute.arch.cp_async_commit_group()
cute.arch.cp_async_wait_group(0)
cute.arch.barrier(
barrier_id=1,
number_of_threads=self.num_Q_load_threads,
)
if const_expr(self.query_in_regs):
# Q.T is invariant across all KV tiles and its N=8 fragment is
# small. Load it while the first K TMA is in flight, then reuse it
# instead of reloading Q from SMEM per tile.
tSrQ_copy_view = smem_thr_copy_Q.retile(tSrQ)
for k in cutlass.range_constexpr(cute.size(tQsQ.shape[2])):
cute.copy(
smem_thr_copy_Q,
tQsQ[None, None, k],
tSrQ_copy_view[None, None, k],
)
n_block = cutlass.max(n_block_max - 1, n_block_min)
consumer_state = self._compute_one_n_block_transposed(
n_block,
consumer_state,
acc_O,
sQ,
sK,
sV,
sP,
sRowScale,
pipeline_k,
pipeline_v,
tiled_mma_qk,
tiled_mma_pv,
smem_thr_copy_K,
smem_thr_copy_Q,
smem_thr_copy_V,
smem_thr_copy_P,
tSrK,
tSrQ,
tOrV,
tOrP,
tKsK,
tQsQ,
tVsV,
tPsP,
tidx,
softmax_scale_log2,
seqlen,
block_info.window_size_left,
is_first_n_block=True,
)
for n_tile in cutlass.range(n_block - n_block_min, unroll=1):
consumer_state = self._compute_one_n_block_transposed(
n_block - n_tile - 1,
consumer_state,
acc_O,
sQ,
sK,
sV,
sP,
sRowScale,
pipeline_k,
pipeline_v,
tiled_mma_qk,
tiled_mma_pv,
smem_thr_copy_K,
smem_thr_copy_Q,
smem_thr_copy_V,
smem_thr_copy_P,
tSrK,
tSrQ,
tOrV,
tOrP,
tKsK,
tQsQ,
tVsV,
tPsP,
tidx,
softmax_scale_log2,
seqlen,
block_info.window_size_left,
)
global_max_row = const_expr(8)
global_sum_row = const_expr(9)
final_scale_row = const_expr(0)
row_limit = seqlen.seqlen_q * self.qhead_per_kvhead
if tidx < self.query_mma_n:
query_row = tidx
row_max = sRowScale[global_max_row, query_row]
row_sum = sRowScale[global_sum_row, query_row]
if query_row < row_limit:
if const_expr(learnable_sink is not None):
if const_expr(not self.is_split_kv) or split_idx == 0:
q_head_idx = query_row + head_idx * self.qhead_per_kvhead
sink_val = Float32(learnable_sink[q_head_idx])
log2_e = math.log2(math.e)
if row_max == -Float32.inf:
row_max = sink_val * (log2_e / softmax_scale_log2)
row_sum = 1.0
else:
row_sum += cute.math.exp2(
sink_val * log2_e - row_max * softmax_scale_log2,
fastmath=True,
)
row_sum_is_zero_or_nan = row_sum == 0.0 or row_sum != row_sum
sRowScale[final_scale_row, query_row] = cute.arch.rcp_approx(
row_sum if not row_sum_is_zero_or_nan else 1.0
)
sLSE[query_row] = (
(
row_max * softmax_scale_log2
+ cute.math.log2(row_sum, fastmath=True)
)
* math.log(2.0)
if not row_sum_is_zero_or_nan
else -Float32.inf
)
else:
sRowScale[final_scale_row, query_row] = 1.0
sLSE[query_row] = -Float32.inf
cute.arch.fence_view_async_shared()
cute.arch.barrier(
barrier_id=int(NamedBarrierFwd.PFull),
number_of_threads=self.num_mma_threads,
)
self._rescale_transposed_o(
acc_O,
tiled_mma_pv,
tidx,
sRowScale,
final_scale_row,
)
self._store_transposed_output(
acc_O,
mO,
mLSE,
sLSE,
tiled_mma_pv,
tidx,
seqlen,
m_block,
head_idx,
batch_idx,
split_idx,
)
@@ -0,0 +1,216 @@
import math
from dataclasses import dataclass
from typing import Type
import cutlass
import cutlass.cute as cute
from cutlass import Int32, const_expr
from cutlass.cute import FastDivmodDivisor
from cutlass.cute.nvgpu import cpasync
from quack.cute_dsl_utils import ParamsBase
from sglang.kernels.ops.attention.flash_attn.cute import utils
@dataclass
class Sm120PagedKVManager(ParamsBase):
"""SM120 paged-KV loader for the stage-sliced FA4 pipeline."""
mPageTable: cute.Tensor
mK_paged: cute.Tensor
mV_paged: cute.Tensor
thread_idx: Int32
page_size_divmod: FastDivmodDivisor
seqlen_k: Int32
leftpad_k: Int32
n_block_size: cutlass.Constexpr[Int32]
num_threads: cutlass.Constexpr[Int32]
head_dim_padded: cutlass.Constexpr[Int32]
head_dim_v_padded: cutlass.Constexpr[Int32]
gmem_threads_per_row: cutlass.Constexpr[Int32]
page_entry_per_thread: cutlass.Constexpr[Int32]
async_copy_elems: cutlass.Constexpr[Int32]
gmem_tiled_copy_KV: cute.TiledCopy
gmem_thr_copy_KV: cute.TiledCopy
tPrPage: cute.Tensor
tPrPageOffset: cute.Tensor
@staticmethod
def create(
mPageTable: cute.Tensor,
mK_paged: cute.Tensor,
mV_paged: cute.Tensor,
page_size_divmod: FastDivmodDivisor,
bidb: Int32,
bidh: Int32,
thread_idx: Int32,
seqlen_k: Int32,
leftpad_k: Int32,
n_block_size: cutlass.Constexpr[Int32],
head_dim_padded: cutlass.Constexpr[Int32],
head_dim_v_padded: cutlass.Constexpr[Int32],
num_threads: cutlass.Constexpr[Int32],
dtype: Type[cutlass.Numeric],
):
universal_copy_bits = 128
async_copy_elems = universal_copy_bits // dtype.width
dtype_bytes = dtype.width // 8
gmem_k_block_size = math.gcd(
head_dim_padded,
head_dim_v_padded,
128 // dtype_bytes,
)
assert gmem_k_block_size % async_copy_elems == 0
gmem_threads_per_row = gmem_k_block_size // async_copy_elems
assert cute.arch.WARP_SIZE % gmem_threads_per_row == 0
atom_async_copy = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
dtype,
num_bits_per_copy=universal_copy_bits,
)
thr_layout = cute.make_ordered_layout(
(num_threads // gmem_threads_per_row, gmem_threads_per_row),
order=(1, 0),
)
val_layout = cute.make_layout((1, async_copy_elems))
gmem_tiled_copy_KV = cute.make_tiled_copy_tv(
atom_async_copy, thr_layout, val_layout
)
gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
# SM120 decode tiles can have fewer rows than DMA threads. Keep one
# register entry per thread so those shapes do not create zero-sized
# register tensors.
page_entry_per_thread = max(1, (n_block_size + num_threads - 1) // num_threads)
tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
return Sm120PagedKVManager(
mPageTable[bidb, None],
mK_paged[None, None, bidh, None],
mV_paged[None, None, bidh, None],
thread_idx,
page_size_divmod,
seqlen_k,
leftpad_k,
n_block_size,
num_threads,
head_dim_padded,
head_dim_v_padded,
gmem_threads_per_row,
page_entry_per_thread,
async_copy_elems,
gmem_tiled_copy_KV,
gmem_thr_copy_KV,
tPrPage,
tPrPageOffset,
)
@cute.jit
def _load_page_table_entry(self, i: Int32, n_block: Int32):
row = (
i * self.num_threads
+ (self.thread_idx % self.gmem_threads_per_row)
* (self.num_threads // self.gmem_threads_per_row)
+ (self.thread_idx // self.gmem_threads_per_row)
)
row_idx = n_block * self.n_block_size + row
page_idx, page_offset = divmod(row_idx + self.leftpad_k, self.page_size_divmod)
is_valid = (
(i + 1) * self.num_threads <= self.n_block_size or row < self.n_block_size
) and row_idx < self.seqlen_k
page = self.mPageTable[page_idx] if is_valid else 0
self.tPrPage[i] = page
self.tPrPageOffset[i] = page_offset
@cute.jit
def load_page_table(self, n_block: Int32):
# The entry count is a specialization constant for SM120. Expanding
# this small loop removes a measurable dynamic-loop cost in decode.
for i in cutlass.range_constexpr(self.page_entry_per_thread):
self._load_page_table_entry(i, n_block)
@cute.jit
def compute_X_ptr(self, K_or_V: str):
tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
mX = self.mK_paged if const_expr(K_or_V == "K") else self.mV_paged
for i in cutlass.range_constexpr(self.page_entry_per_thread):
page = self.tPrPage[i]
page_offset = self.tPrPageOffset[i]
# SGLang stores both paged K and paged V as
# (page_size, head_dim, num_pages).
tPrXPtr[i] = utils.elem_pointer(mX, (page_offset, 0, page)).toint()
return tPrXPtr
@cute.jit
def _copy_row_async(
self,
tXsX: cute.Tensor,
tXcX: cute.Tensor,
mX_paged_cur_copy: cute.Tensor,
m: Int32,
should_load: cute.Tensor,
):
for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
ki = tXcX[0, 0, k][1] // self.async_copy_elems
mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
tXsX_k = tXsX[None, m, k]
mX_paged_cur_copy_ki = cute.make_tensor(
mX_paged_cur_copy_ki.iterator, tXsX_k.layout
)
cute.copy(
self.gmem_tiled_copy_KV,
mX_paged_cur_copy_ki,
tXsX_k,
pred=should_load,
)
@cute.jit
def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
assert K_or_V in ("K", "V")
tPrXPtr = self.compute_X_ptr(K_or_V)
# The SM120 pipeline passes one stage at a time. V has already been
# transposed by the caller's shared-memory view.
sX_pi = cute.group_modes(sX, 0, 1)
head_dim = (
self.head_dim_v_padded
if const_expr(K_or_V == "V")
else self.head_dim_padded
)
cX = cute.make_identity_tensor((self.n_block_size, head_dim))
tXsX = self.gmem_thr_copy_KV.partition_D(sX_pi)
tXcX = self.gmem_thr_copy_KV.partition_S(cX)
tXc0X = self.gmem_thr_copy_KV.get_slice(0).partition_S(cX)
seqlenk_row_limit = (
self.seqlen_k - n_block * self.n_block_size - tXcX[0][0]
if n_block >= 0
else 0
)
for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
row_valid = tXc0X[0, m, 0][0] < seqlenk_row_limit
should_load = cute.make_fragment_like(tXsX[(0, None), m, 0], cute.Boolean)
should_load.fill(row_valid)
x_ptr_i64 = utils.shuffle_sync(
tPrXPtr[m // self.gmem_threads_per_row],
m % self.gmem_threads_per_row,
width=self.gmem_threads_per_row,
)
x_gmem_ptr = cute.make_ptr(
self.mK_paged.element_type,
x_ptr_i64,
cute.AddressSpace.gmem,
assumed_align=16,
)
mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
mX_paged_cur_copy = cute.tiled_divide(
mX_paged_cur, (self.async_copy_elems,)
)
self._copy_row_async(tXsX, tXcX, mX_paged_cur_copy, m, should_load)
@@ -0,0 +1,90 @@
# Copyright (c) 2026, SGLang Team.
"""Pure workload qualification shared by the SM120 FA4 host and kernel."""
from typing import Optional
LOW_HD_DECODE_SHAPES = frozenset({(64, 64), (128, 128)})
LOW_HD_DECODE_TILE_N = 64
LOW_HD_DECODE_MIN_VISIBLE_K = 256
LOW_HD_DECODE_SHORT_VISIBLE_K = 512
LOW_HD_DECODE_MAX_SPLITS = 8
LOW_HD_DECODE_SHORT_MIN_TILES_PER_CTA = 2
LOW_HD_DECODE_LONG_MIN_TILES_PER_CTA = 8
def visible_decode_seqlen_k(
max_seqlen_k: int,
*,
is_local: bool,
window_size_left: Optional[int],
window_size_right: Optional[int],
) -> int:
"""Return the exact KV span visible to a single query position."""
if not is_local:
return max(0, max_seqlen_k)
left = max_seqlen_k if window_size_left is None else max(0, window_size_left)
right = max_seqlen_k if window_size_right is None else max(0, window_size_right)
return max(0, min(max_seqlen_k, left + 1 + right))
def low_hd_paged_decode_tile_m(
*,
head_dim: int,
head_dim_v: int,
paged_kv: bool,
seqlen_q: Optional[int],
visible_seqlen_k: Optional[int],
qhead_per_kvhead: Optional[int],
num_sms: Optional[int] = None,
total_mblocks: Optional[int] = None,
) -> Optional[int]:
"""Return the qualified low-HD decode M tile, or ``None`` for fallback."""
if (
not paged_kv
or seqlen_q != 1
or visible_seqlen_k is None
or visible_seqlen_k <= LOW_HD_DECODE_MIN_VISIBLE_K
or (head_dim, head_dim_v) not in LOW_HD_DECODE_SHAPES
):
return None
qhead_ratio = 1 if qhead_per_kvhead is None else qhead_per_kvhead
if (
(head_dim, head_dim_v) == (64, 64)
and qhead_ratio >= 8
and visible_seqlen_k <= LOW_HD_DECODE_SHORT_VISIBLE_K
):
if num_sms is not None and total_mblocks is not None:
num_n_blocks = (
visible_seqlen_k + LOW_HD_DECODE_TILE_N - 1
) // LOW_HD_DECODE_TILE_N
max_short_splits = min(
LOW_HD_DECODE_MAX_SPLITS,
num_n_blocks // LOW_HD_DECODE_SHORT_MIN_TILES_PER_CTA,
)
# Use the one-warp M16 CTA only when bounded SplitKV can fill an
# SM wave without reducing each partition below two KV tiles.
if total_mblocks * max_short_splits >= num_sms:
return 16
return None
if qhead_ratio >= 8 and visible_seqlen_k <= LOW_HD_DECODE_SHORT_VISIBLE_K:
return 32
return 16
def is_low_hd_paged_decode_tile(
*,
head_dim: int,
head_dim_v: int,
paged_kv: bool,
seqlen_q: int,
tile_m: int,
tile_n: int,
) -> bool:
"""Return whether a selected tile belongs to the qualified low-HD path."""
return (
paged_kv
and seqlen_q == 1
and (head_dim, head_dim_v) in LOW_HD_DECODE_SHAPES
and tile_m in (16, 32)
and tile_n == LOW_HD_DECODE_TILE_N
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
# Copyright (c) 2026, SGLang Team.
"""Schedulers owned by the SGLang SM120 FA4 implementation."""
from dataclasses import dataclass
from typing import Tuple
import cutlass
import cutlass.cute as cute
from cutlass import Int32
from quack.cute_dsl_utils import ParamsBase
from sglang.kernels.ops.attention.flash_attn.cute.tile_scheduler import (
SchedulingMode,
TileSchedulerArguments,
WorkTileInfo,
)
class Sm120UniformBatchScheduler:
"""Map uniform varlen batches without the generic prefix-sum walk.
SM120 paged decode uses one equally sized query segment per request. Its
compile-time dispatch proves that invariant before selecting this scheduler,
so each CTA can recover ``(block, head, batch)`` arithmetically.
"""
@dataclass
class Params(ParamsBase):
num_head: Int32
num_batch: Int32
total_q: Int32
num_splits: Int32
tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
is_split_kv: cutlass.Constexpr[bool] = False
@staticmethod
@cute.jit
def create(
args: TileSchedulerArguments, *, loc=None, ip=None
) -> "Sm120UniformBatchScheduler.Params":
assert args.cluster_shape_mn == (
1,
1,
), "SM120 uniform-batch scheduling requires a 1x1 cluster"
return Sm120UniformBatchScheduler.Params(
num_head=args.num_head,
num_batch=args.num_batch,
total_q=args.total_q,
num_splits=args.num_splits,
tile_shape_mn=args.tile_shape_mn,
is_split_kv=args.is_split_kv,
)
def __init__(
self,
params: Params,
tile_idx: Int32,
split_idx: Int32,
*,
loc=None,
ip=None,
):
self.params = params
self._tile_idx = tile_idx
self._split_idx = split_idx
self._is_first_block = True
self._loc = loc
self._ip = ip
@staticmethod
def to_underlying_arguments(
args: TileSchedulerArguments,
*,
scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
loc=None,
ip=None,
) -> Params:
assert (
scheduling_mode == SchedulingMode.STATIC
), f"SM120 uniform-batch scheduler only supports STATIC, got {scheduling_mode!r}"
return Sm120UniformBatchScheduler.Params.create(args, loc=loc, ip=ip)
@staticmethod
@cute.jit
def create(
params: Params, clc=None, *, loc=None, ip=None
) -> "Sm120UniformBatchScheduler":
tile_idx, split_idx, _ = cute.arch.block_idx()
return Sm120UniformBatchScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
@staticmethod
@cute.jit
def get_grid_shape(
params: Params, *, loc=None, ip=None
) -> Tuple[Int32, Int32, Int32]:
rows_per_batch = params.total_q // params.num_batch
num_m_blocks = cute.ceil_div(rows_per_batch, params.tile_shape_mn[0])
return (
num_m_blocks * params.num_head * params.num_batch,
params.num_splits,
Int32(1),
)
@cute.jit
def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
params = self.params
rows_per_batch = params.total_q // params.num_batch
num_m_blocks = cute.ceil_div(rows_per_batch, params.tile_shape_mn[0])
mh_blocks_per_batch = num_m_blocks * params.num_head
batch_idx = self._tile_idx // mh_blocks_per_batch
mh_block = self._tile_idx - batch_idx * mh_blocks_per_batch
block = mh_block // params.num_head
head_idx = mh_block - block * params.num_head
split_idx = (
self._split_idx if cutlass.const_expr(params.is_split_kv) else Int32(0)
)
return WorkTileInfo(
(Int32(block), Int32(head_idx), Int32(batch_idx), split_idx),
self._is_first_block,
)
def initial_work_tile_info(self, *, loc=None, ip=None) -> WorkTileInfo:
return self.get_current_work(loc=loc, ip=ip)
def prefetch_next_work(self, *, loc=None, ip=None):
pass
def advance_to_next_work(self, *, loc=None, ip=None) -> WorkTileInfo:
self._is_first_block = False
return self.get_current_work(loc=loc, ip=ip)
def producer_tail(self, *, loc=None, ip=None):
pass
def __extract_mlir_values__(self):
values, self._values_pos = [], []
for obj in (self.params, self._tile_idx, self._split_idx):
obj_values = cutlass.extract_mlir_values(obj)
values += obj_values
self._values_pos.append(len(obj_values))
return values
def __new_from_mlir_values__(self, values):
objects = []
for obj, n_items in zip(
(self.params, self._tile_idx, self._split_idx), self._values_pos
):
objects.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
values = values[n_items:]
return Sm120UniformBatchScheduler(*objects, loc=self._loc)
@@ -48,6 +48,7 @@ def flash_attn_with_kvcache(
rel_bias_prep_cache=None,
ver=3,
out=None,
max_seqlen_k: Optional[int] = None,
):
"""
If k and v are not None, k_cache and v_cache will be updated *inplace* with the new values from
@@ -194,6 +195,7 @@ def flash_attn_with_kvcache(
page_table=page_table,
cu_seqlens_q=cu_seqlens_q,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
rotary_seqlens=rotary_seqlens,
q_descale=q_descale,
k_descale=k_descale,
@@ -213,6 +215,7 @@ def flash_attn_with_kvcache(
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=return_softmax_lse,
out=out,
)
else:
raise RuntimeError(f"Unknown flash attention version {ver}")
@@ -319,6 +322,7 @@ def flash_attn_varlen_func(
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=return_softmax_lse,
out=out,
)
else:
raise RuntimeError(f"Unknown flash attention version {ver}")
@@ -0,0 +1,339 @@
# Copyright (c) 2026, SGLang Team.
"""SGLang-facing FlashAttention-4 APIs specialized for SM12x."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Callable, Optional, Tuple, Union
import torch
from sglang.kernel_api_logging import debug_kernel_api
from sglang.kernels.ops.attention.flash_attention_v4 import (
_flash_attn_import_error,
_flash_attn_varlen_func,
_maybe_contiguous,
_pad_mla_q_heads,
_unpad_mla_result,
)
if os.environ.get("SGLANG_INKLING_FA4_USE_PIP") == "1":
# The pip escape hatch deliberately bypasses SGLang-owned SM12x kernels.
get_forward_arch = None
resolve_runtime_policy = None
try_cached_paged_decode = None
else:
from sglang.kernels.ops.attention.fa4_sm120.dispatch import (
get_forward_arch,
resolve_runtime_policy,
try_cached_paged_decode,
)
@dataclass(frozen=True)
class FlashAttentionV4SM120RuntimePolicy:
num_splits: int
decode_num_splits: int
decode_uses_static_max_seqlen_k: bool
def get_flash_attention_v4_sm120_runtime_policy(
*,
device_capability: tuple[int, int],
deterministic: bool,
) -> FlashAttentionV4SM120RuntimePolicy:
"""Resolve the SM12x FA4 launch policy exposed to SGLang."""
if resolve_runtime_policy is None:
num_splits = 1 if deterministic or device_capability < (9, 0) else 0
return FlashAttentionV4SM120RuntimePolicy(
num_splits=num_splits,
decode_num_splits=num_splits,
decode_uses_static_max_seqlen_k=False,
)
num_splits, decode_num_splits, decode_uses_static_max_seqlen_k = (
resolve_runtime_policy(
device_capability=device_capability,
deterministic=deterministic,
)
)
return FlashAttentionV4SM120RuntimePolicy(
num_splits=num_splits,
decode_num_splits=decode_num_splits,
decode_uses_static_max_seqlen_k=decode_uses_static_max_seqlen_k,
)
def _validate_out_contract(out: Optional[torch.Tensor]) -> None:
if out is None:
return
if out.requires_grad:
raise ValueError("out must not require gradients")
if out.stride(-1) != 1:
raise ValueError("out must have stride 1 in the last dimension")
@debug_kernel_api
def flash_attn_varlen_func(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
qv: Optional[torch.Tensor] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
softcap: Optional[float] = None,
window_size: Tuple[Optional[int], Optional[int]] = (-1, -1),
learnable_sink: Optional[torch.Tensor] = None,
sinks: Optional[torch.Tensor] = None,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
score_mod: Optional[Callable] = None,
aux_tensors: Optional[list] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
sfq: Optional[torch.Tensor] = None,
sfk: Optional[torch.Tensor] = None,
sfv: Optional[torch.Tensor] = None,
rel_bias: Optional[torch.Tensor] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_softmax_lse: bool = False,
out: Optional[torch.Tensor] = None,
**_: object,
):
if _flash_attn_varlen_func is None: # pragma: no cover
raise ImportError(
"FlashAttention-4 CUTE is not available. Install flash-attn-4 with "
"its CUDA/CUTE dependencies, or run from a source tree where the "
"vendored FA4 package is importable."
) from _flash_attn_import_error
_validate_out_contract(out)
q, k, v, qv = [_maybe_contiguous(t) for t in (q, k, v, qv)]
if qv is None and q.shape[-1] == 256 and k.shape[-1] == 256 and v.shape[-1] == 256:
# The vendored hd256 kernel assumes dense Q/K/V strides.
q, k, v = [t.contiguous() for t in (q, k, v)]
q, qv, mla_head_padding = _pad_mla_q_heads(q, qv, v, pack_gqa)
if qv is not None and num_splits < 1:
# FA4 MLA does not implement split-KV; auto mode must use one split.
num_splits = 1
cu_seqlens_q, cu_seqlens_k = [
_maybe_contiguous(t) for t in (cu_seqlens_q, cu_seqlens_k)
]
seqused_q, seqused_k = [_maybe_contiguous(t) for t in (seqused_q, seqused_k)]
page_table = _maybe_contiguous(page_table)
if learnable_sink is None and sinks is not None:
learnable_sink = sinks
if window_size == (-1, -1):
window_size = (None, None)
sf_kwargs = {}
if sfq is not None:
sf_kwargs["sfq"] = sfq
if sfk is not None:
sf_kwargs["sfk"] = sfk
if sfv is not None:
sf_kwargs["sfv"] = sfv
descale_kwargs = {}
if q_descale is not None:
descale_kwargs["q_descale"] = q_descale
if k_descale is not None:
descale_kwargs["k_descale"] = k_descale
if v_descale is not None:
descale_kwargs["v_descale"] = v_descale
rel_bias_kwargs = {}
if rel_bias is not None:
rel_bias_kwargs["rel_bias"] = rel_bias
if rel_bias_prep_cache is not None:
rel_bias_kwargs["rel_bias_prep_cache"] = rel_bias_prep_cache
result = _flash_attn_varlen_func(
q=q,
k=k,
v=v,
qv=qv,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
softcap=softcap,
window_size=window_size,
learnable_sink=learnable_sink,
num_splits=num_splits,
pack_gqa=pack_gqa,
score_mod=score_mod,
aux_tensors=aux_tensors,
return_lse=return_softmax_lse,
out=out,
**sf_kwargs,
**descale_kwargs,
**rel_bias_kwargs,
)
result = _unpad_mla_result(result, mla_head_padding)
if return_softmax_lse:
return result
if isinstance(result, tuple):
return result[0]
return result
@debug_kernel_api
def flash_attn_with_kvcache(
q: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
k: Optional[torch.Tensor] = None,
v: Optional[torch.Tensor] = None,
qv: Optional[torch.Tensor] = None,
rotary_cos: Optional[torch.Tensor] = None,
rotary_sin: Optional[torch.Tensor] = None,
cache_seqlens: Optional[Union[int, torch.Tensor]] = None,
cache_batch_idx: Optional[torch.Tensor] = None,
cache_leftpad: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k_new: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
rotary_seqlens: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
window_size: Tuple[int, int] = (-1, -1),
attention_chunk: Optional[int] = None,
softcap: float = 0.0,
rotary_interleaved: bool = True,
scheduler_metadata=None,
num_splits: int = 0,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
sinks: Optional[torch.Tensor] = None,
score_mod: Optional[Callable] = None,
aux_tensors: Optional[list] = None,
sfq: Optional[torch.Tensor] = None,
sfk: Optional[torch.Tensor] = None,
sfv: Optional[torch.Tensor] = None,
rel_bias: Optional[torch.Tensor] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_softmax_lse: bool = False,
out: Optional[torch.Tensor] = None,
max_seqlen_k: Optional[int] = None,
**_: object,
):
_validate_out_contract(out)
if k is not None or v is not None:
raise NotImplementedError("FA4 does not support updating KV cache in-place.")
if rotary_cos is not None or rotary_sin is not None or rotary_seqlens is not None:
raise NotImplementedError("FA4 path does not support rotary embedding.")
if cache_batch_idx is not None or cache_leftpad is not None:
raise NotImplementedError(
"FA4 path does not support non-consecutive batch indices or left padding."
)
if isinstance(cache_seqlens, int):
cache_seqlens = torch.full(
(k_cache.shape[0],), cache_seqlens, dtype=torch.int32, device=k_cache.device
)
forward_arch = get_forward_arch(q.device) if get_forward_arch is not None else None
if (
forward_arch is not None
and not return_softmax_lse
and softcap in (None, 0.0)
and all(
value is None
for value in (
qv,
score_mod,
aux_tensors,
q_descale,
k_descale,
v_descale,
sfq,
sfk,
sfv,
rel_bias,
rel_bias_prep_cache,
)
)
):
q, k_cache, v_cache = [_maybe_contiguous(t) for t in (q, k_cache, v_cache)]
cu_seqlens_q, cache_seqlens, page_table = [
_maybe_contiguous(t) for t in (cu_seqlens_q, cache_seqlens, page_table)
]
fast_result = try_cached_paged_decode(
arch=forward_arch,
q=q,
k=k_cache,
v=v_cache,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=None,
seqused_q=None,
seqused_k=cache_seqlens,
page_table=page_table,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=softmax_scale,
causal=causal,
window_size_left=window_size[0],
window_size_right=window_size[1],
learnable_sink=sinks,
requested_num_splits=num_splits,
pack_gqa=pack_gqa,
out=out,
)
if fast_result is not None:
return fast_result[0]
result = flash_attn_varlen_func(
q=q,
k=k_cache,
v=v_cache,
qv=qv,
cu_seqlens_q=cu_seqlens_q,
seqused_k=cache_seqlens,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
softcap=softcap if softcap != 0.0 else None,
window_size=window_size,
num_splits=num_splits,
pack_gqa=pack_gqa,
learnable_sink=sinks,
score_mod=score_mod,
aux_tensors=aux_tensors,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
sfq=sfq,
sfk=sfk,
sfv=sfv,
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=return_softmax_lse if forward_arch is not None else True,
out=out,
)
if return_softmax_lse:
return result
if isinstance(result, tuple):
return result[0]
return result
@@ -1,63 +0,0 @@
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
# SM120 (Blackwell GeForce / DGX Spark) forward pass.
#
# SM120 uses the same SM80-era MMA instructions (mma.sync.aligned.m16n8k16) but has
# a smaller shared memory capacity (99 KB vs 163 KB on SM80). This module subclasses
# FlashAttentionForwardSm80 and overrides the SMEM capacity check accordingly.
import cutlass
import cutlass.utils as utils_basic
from sglang.kernels.ops.attention.flash_attn.cute.flash_fwd import (
FlashAttentionForwardSm80,
)
class FlashAttentionForwardSm120(FlashAttentionForwardSm80):
# Keep arch = 80 to use CpAsync code paths (no TMA for output).
# The compilation target is determined by the GPU at compile time, not this field.
arch = 80
@staticmethod
def can_implement(
dtype,
head_dim,
head_dim_v,
tile_m,
tile_n,
num_stages,
num_threads,
is_causal,
Q_in_regs=False,
) -> bool:
"""Check if the kernel can be implemented on SM120.
Same logic as SM80 but uses SM120's shared memory capacity (99 KB).
"""
if dtype not in [cutlass.Float16, cutlass.BFloat16]:
return False
if head_dim % 8 != 0:
return False
if head_dim_v % 8 != 0:
return False
if tile_n % 16 != 0:
return False
if num_threads % 32 != 0:
return False
# Shared memory usage: Q tile + (K tile + V tile)
smem_usage_Q = tile_m * head_dim * 2
smem_usage_K = tile_n * head_dim * num_stages * 2
smem_usage_V = tile_n * head_dim_v * num_stages * 2
smem_usage_QV = (
(smem_usage_Q + smem_usage_V)
if not Q_in_regs
else max(smem_usage_Q, smem_usage_V)
)
smem_usage = smem_usage_QV + smem_usage_K
# SM120 has 99 KB shared memory (vs 163 KB on SM80)
smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_120")
if smem_usage > smem_capacity:
return False
if (tile_m * 2) % num_threads != 0:
return False
return True
@@ -58,8 +58,10 @@ from sglang.kernels.ops.attention.flash_attn.cute.flash_fwd_sm100 import (
DescaleTensors,
FlashAttentionForwardSm100,
)
from sglang.kernels.ops.attention.flash_attn.cute.flash_fwd_sm120 import (
FlashAttentionForwardSm120,
from sglang.kernels.ops.attention.fa4_sm120.dispatch import (
get_forward_host,
try_cached_paged_decode,
try_cached_varlen,
)
from sglang.kernels.ops.attention.flash_attn.cute.shearing_bias import ShearingBias
@@ -85,9 +87,8 @@ def _parse_arch_str(arch_str):
def _get_device_arch():
"""Cached device arch check.
Override with FLASH_ATTENTION_ARCH (e.g. 'sm_80' or '80') to select which
kernel path to use (SM80/SM90/SM100/SM120) independently of the compilation
target (CUTE_DSL_ARCH).
Override with FLASH_ATTENTION_ARCH (e.g. 'sm_80' or '80') to select the
kernel path independently of the compilation target (CUTE_DSL_ARCH).
For CPU-only compilation (no GPU), set both:
FLASH_ATTENTION_ARCH=sm_80 (kernel selection)
@@ -100,6 +101,12 @@ def _get_device_arch():
return major * 10 + int(minor)
@lru_cache(maxsize=None)
def _get_device_num_sms(device: torch.device) -> int:
"""Return the stable SM count without querying CUDA on every launch."""
return torch.cuda.get_device_properties(device).multi_processor_count
def _validate_head_dims(
head_dim: int, head_dim_v: int, compute_capability: int, alignment: int
) -> None:
@@ -342,6 +349,60 @@ def _flash_attn_fwd(
aux_tensors: Some score_mods will want to read from global aux_tensors. This is how we thread them through to the inner kernel.
aux_scalars: Runtime scalar captures used by score_mod or mask_mod.
"""
fake_mode = is_fake_mode()
arch = _get_device_arch() if _arch is None else _arch
arch_forward_host = get_forward_host(arch)
requested_num_splits = num_splits
if (
not fake_mode
and arch_forward_host is not None
and not return_lse
and lse is None
and softcap in (None, 0.0)
and all(
value is None
for value in (
qv,
gather_kv_indices,
score_mod,
mask_mod,
block_sparse_tensors,
aux_tensors,
aux_scalars,
q_descale,
k_descale,
v_descale,
rel_bias,
sfq,
sfk,
sfv,
)
)
):
fast_result = try_cached_paged_decode(
arch=arch,
q=q,
k=k,
v=v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
page_table=page_table,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=softmax_scale,
causal=causal,
window_size_left=window_size_left,
window_size_right=window_size_right,
learnable_sink=learnable_sink,
requested_num_splits=requested_num_splits,
pack_gqa=pack_gqa,
out=out,
)
if fast_result is not None:
return fast_result
aux_scalars = tuple(aux_scalars) if aux_scalars else None
q, k, v, qv = [maybe_contiguous(t) for t in (q, k, v, qv)]
assert q is not None or qv is not None
@@ -478,7 +539,7 @@ def _flash_attn_fwd(
assert learnable_sink.shape == (num_head,)
assert learnable_sink.dtype == torch.bfloat16, "learnable_sink must be bfloat16"
if not is_fake_mode():
if not fake_mode:
assert all(
t is None or t.is_cuda
for t in (
@@ -497,17 +558,13 @@ def _flash_attn_fwd(
learnable_sink,
)
), "inputs must be on CUDA device"
arch = _get_device_arch() if _arch is None else _arch
assert arch // 10 in [
8,
9,
10,
11,
12,
], "Unsupported compute capability. Supported: 8.x, 9.x, 10.x, 11.x, 12.x"
assert arch // 10 in [8, 9, 10, 11] or arch_forward_host is not None, (
"Unsupported compute capability. Supported: 8.x, 9.x, 10.x, 11.x, "
"and architectures registered through the forward-host bridge"
)
assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
alignment = 16 // v.element_size()
if arch // 10 not in [8, 12]:
if arch // 10 != 8 and arch_forward_host is None:
_validate_head_dims(head_dim, head_dim_v, arch // 10, alignment)
if softmax_scale is None:
softmax_scale = (
@@ -565,6 +622,10 @@ def _flash_attn_fwd(
device=device,
)
else:
if out.requires_grad:
raise ValueError("out must not require gradients")
if out.stride(-1) != 1:
raise ValueError("out must have stride 1 in the last dimension")
_validate_tensor(
out,
"out",
@@ -619,21 +680,41 @@ def _flash_attn_fwd(
current_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
# SM80/SM120: uses SM80 MMA, 128 threads (4 warps)
if arch // 10 in [8, 12]:
if arch // 10 == 8:
num_threads = 128
num_SMs = 132 if fake_mode else _get_device_num_sms(device)
fwd_cfg = FwdConfig(128, 128, True, True) # default
if tile_mn is None:
if arch // 10 == 12:
# SM120 tile sizes tuned for 99 KB SMEM capacity:
# D<=64: 128x128 → 48 KB (good occupancy)
# D>64: 128x64 → 64 KB (128x128 would use 96 KB, hurting occupancy)
if head_dim <= 64:
fwd_cfg = FwdConfig(128, 128, True, True)
else:
fwd_cfg = FwdConfig(128, 64, True, True)
elif arch // 10 == 8:
arch_forward_config = None
if arch_forward_host is not None:
arch_forward_config = arch_forward_host.select_config(
head_dim=head_dim,
head_dim_v=head_dim_v,
tile_mn=tile_mn,
has_bias=rel_bias is not None,
total_q_rows=total_q * num_head,
num_sms=None if fake_mode else num_SMs,
num_batch=batch_size,
seqlen_q=(max_seqlen_q if max_seqlen_q is not None else seqlen_q),
seqlen_k=(max_seqlen_k if max_seqlen_k is not None else seqlen_k),
num_head_kv=num_head_kv,
qhead_per_kvhead=qhead_per_kvhead,
is_causal=causal,
is_local=local,
window_size_left=window_size_left,
window_size_right=window_size_right,
pack_gqa=pack_gqa,
paged_kv=page_table is not None,
)
fwd_cfg = FwdConfig(
arch_forward_config.tile_m,
arch_forward_config.tile_n,
True,
True,
)
num_threads = arch_forward_config.num_threads
elif tile_mn is None:
if arch // 10 == 8:
fwd_cfg = FwdConfig(128, 64, True, True) # SM80, should tune
elif arch // 10 == 9:
sparse_q = get_sparse_q_block_size(block_sparse_tensors, seqlen_q)
@@ -683,13 +764,58 @@ def _flash_attn_fwd(
) // m_block_size_effective
total_mblocks = batch_size * num_head_kv * num_m_blocks
num_n_blocks = (seqlen_k_loaded + tile_n - 1) // tile_n
num_SMs = (
132
if is_fake_mode()
else torch.cuda.get_device_properties(device).multi_processor_count
)
if num_splits < 1:
num_splits = num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, 128)
arch_forward_plan = None
if arch_forward_host is not None:
arch_forward_plan = arch_forward_host.resolve_plan(
requested_num_splits=num_splits,
generic_num_n_blocks=num_n_blocks,
head_dim=head_dim,
head_dim_v=head_dim_v,
batch_size=batch_size,
num_head_kv=num_head_kv,
paged_kv=page_table is not None,
page_size=page_size,
k=k,
v=v,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
pack_gqa=pack_gqa,
element_size=q.element_size(),
packed_q_rows=seqlen_q_packgqa,
tile_m=tile_m,
tile_n=tile_n,
num_m_blocks=num_m_blocks,
total_mblocks=total_mblocks,
num_sms=num_SMs,
total_q=total_q,
has_cu_seqlens_q=cu_seqlens_q is not None,
has_seqused_q=seqused_q is not None,
has_seqused_k=seqused_k is not None,
is_causal=causal,
is_local=local,
window_size_left=window_size_left,
window_size_right=window_size_right,
has_score_or_mask_mod=(
softcap is not None
or score_mod is not None
or mask_mod is not None
or rel_bias is not None
),
is_stream_capturing=(
not fake_mode and torch.cuda.is_current_stream_capturing()
),
device=device,
fake_mode=fake_mode,
generic_heuristic=num_splits_heuristic,
)
num_splits = arch_forward_plan.num_splits
elif num_splits < 1:
num_splits = num_splits_heuristic(
total_mblocks,
num_SMs,
num_n_blocks,
128,
)
# SplitKV uses float32 partial output, which doubles the O buffer size
# in shared memory, causing OOM for diff-headdim (192, 128)
@@ -859,8 +985,8 @@ def _flash_attn_fwd(
disable_sparse_kv_bitmask = None
p = row_max = None
# rel_bias -> sheared bias (Inkling relative attention). Produces `bias`, the column-aligned
# bias the SM100 kernel adds to pre-softmax scores via its dedicated TMA pipeline.
# Inkling relative attention. Shear the relative rows into the column-aligned
# tiles consumed by the architecture-specific attention mainloop.
rel_extent = 0
rel_extent_padded = 0
bias = None
@@ -868,16 +994,17 @@ def _flash_attn_fwd(
cu_total_m_blocks_bias = None
blocks_to_batch_idx = None
if rel_bias is not None:
assert arch // 10 in [
9,
10,
11,
], "rel_bias (sheared bias) is only supported on SM9x/10x"
assert arch // 10 in [9, 10, 11] or arch_forward_host is not None, (
"rel_bias requires SM9x/10x/11x or an architecture-owned "
"forward implementation"
)
qhead_per_kvhead_packgqa = qhead_per_kvhead if pack_gqa else 1
rel_extent = rel_bias.shape[-1]
rel_extent_padded = rel_extent + 256
assert rel_extent % 128 == 0
assert tile_m == 128 and tile_n == 128
assert tile_n == 128
if arch_forward_host is None:
assert tile_m == 128
assert (
causal
or window_size_left is None
@@ -975,7 +1102,7 @@ def _flash_attn_fwd(
current_stream,
options="--enable-tvm-ffi",
)
if not is_fake_mode():
if not fake_mode:
_flash_attn_fwd.compile_cache_prepare_shear_bias[
compile_key_prepare
](
@@ -1003,6 +1130,7 @@ def _flash_attn_fwd(
qhead_per_kvhead,
rows_per_cta,
group_tile_bias,
tile_m,
max_m_blocks_leq_one,
cu_total_m_blocks_bias is not None,
blocks_to_batch_idx is not None,
@@ -1039,6 +1167,7 @@ def _flash_attn_fwd(
qhead_per_kvhead=qhead_per_kvhead,
rows_per_cta=rows_per_cta,
tile_m=group_tile_bias,
attention_tile_m=tile_m,
max_m_blocks_leq_one=max_m_blocks_leq_one,
use_pdl=use_pdl,
),
@@ -1057,7 +1186,7 @@ def _flash_attn_fwd(
current_stream,
options="--enable-tvm-ffi",
)
if not is_fake_mode():
if not fake_mode:
_flash_attn_fwd.compile_cache_shear_bias[shear_compile_key](
rel_bias,
bias,
@@ -1111,6 +1240,8 @@ def _flash_attn_fwd(
is_split_kv,
pack_gqa,
arch,
arch_forward_config.compile_key if arch_forward_config is not None else None,
arch_forward_plan.compile_key if arch_forward_plan is not None else None,
page_size not in [None, tile_n], # paged KV non-TMA
use_2cta_instrs,
q_subtile_factor,
@@ -1364,31 +1495,37 @@ def _flash_attn_fwd(
)
),
)
elif arch // 10 == 12:
# SM120 (Blackwell GeForce / DGX Spark): uses SM80 MMA with SM120 SMEM capacity
assert not use_block_sparsity, "Block sparsity not supported on SM 12.0"
assert page_table is None, "Paged KV not supported on SM 12.0 in this PR"
assert not is_split_kv, "SplitKV not supported on SM 12.0 in this PR"
fa_fwd = FlashAttentionForwardSm120(
dtype,
head_dim,
head_dim_v,
qhead_per_kvhead,
elif arch_forward_host is not None:
assert not use_block_sparsity, (
"Block sparsity is not supported by the architecture-owned "
"forward implementation"
)
assert arch_forward_config is not None
assert arch_forward_plan is not None
fa_fwd = arch_forward_host.make_kernel(
dtype=dtype,
head_dim=head_dim,
head_dim_v=head_dim_v,
qhead_per_kvhead=qhead_per_kvhead,
is_causal=causal,
is_local=local,
pack_gqa=pack_gqa,
tile_m=tile_m,
tile_n=tile_n,
num_stages=1,
num_threads=num_threads,
Q_in_regs=False,
config=arch_forward_config,
paged_kv=page_table is not None,
score_mod=score_mod,
mask_mod=mask_mod,
has_aux_tensors=aux_tensors is not None,
is_split_kv=is_split_kv,
has_bias=bias is not None,
bias_block_size=tile_bias,
rel_extent_padded=rel_extent_padded,
plan=arch_forward_plan,
)
else:
raise ValueError(
f"Unsupported compute capability: {arch}. Supported: 8.x, 9.x, 10.x, 11.x, 12.x"
f"Unsupported compute capability: {arch}. Supported: 8.x, 9.x, "
"10.x, 11.x, and architectures registered through the "
"forward-host bridge"
)
# TODO: check @can_implement
if qv is not None:
@@ -1440,7 +1577,7 @@ def _flash_attn_fwd(
AuxData(cute_aux_tensors, aux_scalars),
]
)
if arch // 10 in [9, 10, 11]:
if arch // 10 in [9, 10, 11] or arch_forward_host is not None:
compile_args.append(bias_tensor) # mBias
if arch // 10 in [10, 11]:
if not use_dedicated_hd256_kernel:
@@ -1453,12 +1590,17 @@ def _flash_attn_fwd(
v_sf_vec_size,
]
)
if arch_forward_host is not None:
assert arch_forward_plan is not None
compile_args.extend(
arch_forward_host.compile_arguments(arch_forward_plan)
)
compile_args.append(current_stream)
_flash_attn_fwd.compile_cache[compile_key] = cute.compile(
*compile_args, options="--enable-tvm-ffi"
)
if not is_fake_mode():
if not fake_mode:
q_call, k_call, v_call, qv_call = [
t.detach() if t is not None else None for t in (q, k, v, qv)
]
@@ -1543,7 +1685,7 @@ def _flash_attn_fwd(
AuxData(aux_tensors, aux_scalars),
]
)
if arch // 10 in [9, 10, 11]:
if arch // 10 in [9, 10, 11] or arch_forward_host is not None:
call_args.append(bias) # mBias
if arch // 10 in [10, 11]:
if not use_dedicated_hd256_kernel:
@@ -1556,7 +1698,52 @@ def _flash_attn_fwd(
sfv_call, # mSFV (None unless v_blockscaled)
]
)
_flash_attn_fwd.compile_cache[compile_key](*call_args)
if arch_forward_host is not None:
assert arch_forward_plan is not None
call_args.extend(arch_forward_host.runtime_arguments(arch_forward_plan))
compiled_fwd = _flash_attn_fwd.compile_cache[compile_key]
if (
arch_forward_host is not None
and cu_seqlens_q is not None
and cu_seqlens_k is not None
and seqused_q is None
and seqused_k is None
and not is_split_kv
and not requires_grad
and (learnable_sink is None or not learnable_sink.requires_grad)
and lse is None
and softcap is None
and score_mod is None
and mask_mod is None
and block_sparse_tensors is None
and aux_tensors is None
and aux_scalars is None
and q_descale is None
and k_descale is None
and v_descale is None
and rel_bias is None
and sfq is None
and sfk is None
and sfv is None
):
arch_forward_host.register_varlen(
arch=arch,
compiled_fn=compiled_fwd,
compile_key=compile_key,
q=q,
k=k,
v=v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
causal=causal,
window_size_left=window_size_left,
window_size_right=window_size_right,
learnable_sink=learnable_sink,
pack_gqa=pack_gqa,
)
compiled_fwd(*call_args)
if is_split_kv:
_flash_attn_fwd_combine(
out_partial,
@@ -1566,6 +1753,72 @@ def _flash_attn_fwd(
cu_seqlens_q,
seqused_q,
)
if (
not fake_mode
and not torch.cuda.is_current_stream_capturing()
and arch_forward_host is not None
and q is not None
and k is not None
and qv is None
and cu_seqlens_q is not None
and cu_seqlens_k is None
and seqused_q is None
and seqused_k is not None
and page_table is not None
and not requires_grad
and (learnable_sink is None or not learnable_sink.requires_grad)
and lse is None
and softcap is None
and score_mod is None
and mask_mod is None
and block_sparse_tensors is None
and aux_tensors is None
and aux_scalars is None
and q_descale is None
and k_descale is None
and v_descale is None
and rel_bias is None
and sfq is None
and sfk is None
and sfv is None
):
compiled_combine = None
if is_split_kv:
lse_partial_transposed = lse_partial.transpose(-1, -2)
combine_key = _fwd_combine_compile_key(
out_partial,
out,
None,
cu_seqlens_q,
None,
None,
)
compiled_combine = _flash_attn_fwd_combine.compile_cache[combine_key]
arch_forward_host.register_paged_decode(
arch=arch,
compiled_fn=compiled_fwd,
compile_key=compile_key,
compiled_combine=compiled_combine,
actual_num_splits=num_splits,
tile_m=tile_m,
tile_n=tile_n,
q=q,
k=k,
v=v,
cu_seqlens_q=cu_seqlens_q,
seqused_k=seqused_k,
page_table=page_table,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
causal=causal,
window_size_left=window_size_left,
window_size_right=window_size_right,
learnable_sink=learnable_sink,
pack_gqa=pack_gqa,
requested_num_splits=requested_num_splits,
out_partial=out_partial if is_split_kv else None,
lse_partial=lse_partial if is_split_kv else None,
)
return out, lse
@@ -1689,6 +1942,7 @@ class FlashAttnVarlenFunc(torch.autograd.Function):
v_sf_vec_size: Optional[int] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_lse: bool = False,
out: Optional[torch.Tensor] = None,
):
aux_scalars = tuple(aux_scalars) if aux_scalars else None
shared_kv = k is v
@@ -1736,32 +1990,34 @@ class FlashAttnVarlenFunc(torch.autograd.Function):
qk_sf_vec_size=qk_sf_vec_size,
v_sf_vec_size=v_sf_vec_size,
rel_bias_prep_cache=rel_bias_prep_cache,
out=out,
)
ctx.save_for_backward(
q,
k,
v,
out,
lse,
cu_seqlens_q,
cu_seqlens_k,
seqused_q,
seqused_k,
*(aux_tensors or ()),
)
ctx.softmax_scale = softmax_scale
ctx.causal = causal
ctx.window_size = window_size
ctx.softcap = softcap
ctx.deterministic = deterministic
ctx.max_seqlen_q = max_seqlen_q
ctx.max_seqlen_k = max_seqlen_k
ctx.return_lse = return_lse
ctx.score_mod = score_mod
ctx.score_mod_bwd = score_mod_bwd
ctx.mask_mod = mask_mod
ctx.aux_scalars = aux_scalars
ctx.set_materialize_grads(False)
if ctx is not None:
ctx.save_for_backward(
q,
k,
v,
out,
lse,
cu_seqlens_q,
cu_seqlens_k,
seqused_q,
seqused_k,
*(aux_tensors or ()),
)
ctx.softmax_scale = softmax_scale
ctx.causal = causal
ctx.window_size = window_size
ctx.softcap = softcap
ctx.deterministic = deterministic
ctx.max_seqlen_q = max_seqlen_q
ctx.max_seqlen_k = max_seqlen_k
ctx.return_lse = return_lse
ctx.score_mod = score_mod
ctx.score_mod_bwd = score_mod_bwd
ctx.mask_mod = mask_mod
ctx.aux_scalars = aux_scalars
ctx.set_materialize_grads(False)
return out, lse
@@ -1852,6 +2108,7 @@ def flash_attn_varlen_func(
v_sf_vec_size: Optional[int] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_lse: bool = False,
out: Optional[torch.Tensor] = None,
):
"""
Tensor arguments:
@@ -1891,7 +2148,63 @@ def flash_attn_varlen_func(
qk_sf_vec_size = 32
if v_sf_vec_size is None and sfv is not None and sfv.dtype == torch.float8_e8m0fnu:
v_sf_vec_size = 32
return FlashAttnVarlenFunc.apply(
if out is not None and out.requires_grad:
raise ValueError("out must not require gradients")
if out is not None and out.stride(-1) != 1:
raise ValueError("out must have stride 1 in the last dimension")
runtime_arch = _get_device_arch()
forward_host = None if is_fake_mode() else get_forward_host(runtime_arch)
if (
forward_host is not None
and q is not None
and k is not None
and cu_seqlens_q is not None
and cu_seqlens_k is not None
and num_splits == 1
and softcap in (None, 0.0)
and not return_lse
and all(
value is None
for value in (
qv,
seqused_q,
seqused_k,
gather_kv_indices,
page_table,
score_mod,
mask_mod,
block_sparse_tensors,
aux_tensors,
aux_scalars,
q_descale,
k_descale,
v_descale,
rel_bias,
sfq,
sfk,
sfv,
)
)
):
fast_result = try_cached_varlen(
arch=runtime_arch,
q=q,
k=k,
v=v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=softmax_scale,
causal=causal,
window_size=window_size,
learnable_sink=learnable_sink,
pack_gqa=pack_gqa,
out=out,
)
if fast_result is not None:
return fast_result
autograd_args = (
q,
k,
v,
@@ -1930,7 +2243,34 @@ def flash_attn_varlen_func(
v_sf_vec_size,
rel_bias_prep_cache,
return_lse,
out,
)
needs_autograd = False
if forward_host is not None or out is not None:
differentiable_tensors = (
q,
k,
v,
qv,
learnable_sink,
q_descale,
k_descale,
v_descale,
rel_bias,
sfq,
sfk,
sfv,
*(aux_tensors or ()),
)
needs_autograd = torch.is_grad_enabled() and any(
tensor is not None and tensor.requires_grad
for tensor in differentiable_tensors
)
if needs_autograd and out is not None:
raise ValueError("out is only supported for forward-only inference")
if not needs_autograd and forward_host is not None:
return FlashAttnVarlenFunc.forward(None, *autograd_args)
return FlashAttnVarlenFunc.apply(*autograd_args)
def _compile_fwd_combine(
@@ -2039,6 +2379,35 @@ def _compile_fwd_combine(
)
def _fwd_combine_compile_key(
out_partial: torch.Tensor,
out: torch.Tensor,
lse: Optional[torch.Tensor],
cu_seqlens: Optional[torch.Tensor],
seqused: Optional[torch.Tensor],
varlen_batch_idx: Optional[torch.Tensor],
) -> tuple:
head_dim = out_partial.shape[-1]
num_splits = out_partial.shape[0]
k_block_size = 64 if head_dim <= 64 else 128
tile_m = 8 if k_block_size % 128 == 0 else (16 if k_block_size % 64 == 0 else 32)
log_max_splits = max(math.ceil(math.log2(num_splits)), 4)
if tile_m == 8:
log_max_splits = max(log_max_splits, 5)
return (
torch2cute_dtype_map[out.dtype],
torch2cute_dtype_map[out_partial.dtype],
head_dim,
tile_m,
k_block_size,
log_max_splits,
cu_seqlens is not None,
seqused is not None,
lse is not None,
varlen_batch_idx is not None,
)
def _flash_attn_fwd_combine(
out_partial: torch.Tensor,
lse_partial: torch.Tensor,
@@ -2092,38 +2461,18 @@ def _flash_attn_fwd_combine(
if not is_fake_mode():
assert t.is_cuda, f"{name} must be on CUDA device"
assert t.is_contiguous(), f"{name} must be contiguous"
head_dim = out_partial.shape[-1]
num_splits = out_partial.shape[0]
assert num_splits <= 256
# If hdim is 96 or 192, it's faster to round them to 128 or 256 respectively
# so that kBlockM is smaller and we have more parallelism.
k_block_size = 64 if head_dim <= 64 else 128
# We want kBlockM to be as small as possible to maximize parallelism.
# E.g., if hdim is 64, we want kBlockM to be 16 so that we can use 256 threads, each reading 4 elements (floats).
tile_m = 8 if k_block_size % 128 == 0 else (16 if k_block_size % 64 == 0 else 32)
log_max_splits = max(math.ceil(math.log2(num_splits)), 4)
if tile_m == 8:
# If kBlockM == 8 then the minimum number of splits is 32.
# TODO: we can deal w this by using 128 threads instead
log_max_splits = max(log_max_splits, 5)
# Create combine kernel configuration
dtype = torch2cute_dtype_map[out.dtype]
dtype_partial = torch2cute_dtype_map[out_partial.dtype]
# Device architecture is invariant for the lifetime of this server/JIT
# cache, so PDL does not belong in the compile key.
use_pdl = is_arch_support_pdl()
compile_key = (
dtype,
dtype_partial,
head_dim,
tile_m,
k_block_size,
log_max_splits,
cu_seqlens is not None,
seqused is not None,
lse is not None,
varlen_batch_idx is not None,
compile_key = _fwd_combine_compile_key(
out_partial,
out,
lse,
cu_seqlens,
seqused,
varlen_batch_idx,
)
if compile_key not in _flash_attn_fwd_combine.compile_cache:
_flash_attn_fwd_combine.compile_cache[compile_key] = _compile_fwd_combine(
@@ -168,6 +168,9 @@ class AttentionMask:
1 # only pass in if we're doing PackGQA
)
swap_AB: cutlass.Constexpr[bool] = False
# R2P assumes the canonical row ownership of the QK accumulator. Kernels
# with a different accumulator mapping must use the direct predicate path.
enable_r2p_optimization: cutlass.Constexpr[bool] = True
@property
def seqlen_q(self) -> Int32:
@@ -330,7 +333,7 @@ class AttentionMask:
)
if const_expr(mask_causal):
r2p = const_expr(
not self.swap_AB
not self.swap_AB and self.enable_r2p_optimization
) # R2P trick, see apply_mask_sm100
for r in cutlass.range(
cute.size(tScS_mn.shape[0]), unroll_full=True
@@ -375,7 +378,9 @@ class AttentionMask:
if const_expr(self.window_size_left is not None)
else None
)
r2p_local = const_expr(not self.swap_AB)
r2p_local = const_expr(
not self.swap_AB and self.enable_r2p_optimization
)
for r in cutlass.range(
cute.size(tScS_mn.shape[0]), unroll_full=True
):
@@ -35,6 +35,7 @@ class ShearingBias:
qhead_per_kvhead: cutlass.Constexpr[int] = 1,
rows_per_cta: int = 4,
tile_m: int = 128,
attention_tile_m: int = 128,
max_m_blocks_leq_one: bool = False,
use_pdl: bool = False,
clamp_subtiles: bool = True,
@@ -67,6 +68,12 @@ class ShearingBias:
# only used with block packed scheduling
self.tile_m = tile_m
# The output columns are aligned to the rightmost N tile visible to the
# attention CTA. Keep this distinct from ``tile_m`` above: the shear
# scheduler may group rows in larger blocks than the attention kernel
# consumes per CTA.
assert attention_tile_m % self.rows_per_cta == 0
self.attention_tile_m = attention_tile_m
# Shrink the subtile grid dim to the rows a block can actually hold
# (decode blocks hold qhead_per_kvhead*seqlen_q rows, not tile_m).
self.clamp_subtiles = clamp_subtiles
@@ -335,7 +342,7 @@ class ShearingBias:
)
block_info = BlockInfo(
128,
self.attention_tile_m,
128,
self.is_causal,
self.is_local,
@@ -386,7 +393,7 @@ class ShearingBias:
# Convention: inclusive min, exclusive max
m_idx = m_block * self.rows_per_cta + warp_idx
attn_m_block = m_idx // 128
attn_m_block = m_idx // self.attention_tile_m
_, attn_n_block_max = block_info.get_n_block_min_max(
seqlen_info,
@@ -253,6 +253,7 @@ class FlashAttentionBackend(AttentionBackend):
# Select version
self.fa_impl_ver = fa_impl_ver
device_capability = get_device_capability()
if self.fa_impl_ver == 3:
from sgl_kernel.flash_attn import (
flash_attn_varlen_func,
@@ -261,11 +262,25 @@ class FlashAttentionBackend(AttentionBackend):
)
self._get_scheduler_metadata = get_scheduler_metadata
self._get_fa_runtime_policy = None
elif self.fa_impl_ver == 4:
from sglang.kernels.ops.attention.flash_attention_v4 import (
flash_attn_varlen_func,
flash_attn_with_kvcache,
)
if device_capability[0] == 12:
from sglang.kernels.ops.attention.flash_attention_v4_sm120 import (
flash_attn_varlen_func,
flash_attn_with_kvcache,
get_flash_attention_v4_sm120_runtime_policy,
)
self._get_fa_runtime_policy = (
get_flash_attention_v4_sm120_runtime_policy
)
else:
from sglang.kernels.ops.attention.flash_attention_v4 import (
flash_attn_varlen_func,
flash_attn_with_kvcache,
)
self._get_fa_runtime_policy = None
self._get_scheduler_metadata = None
if model_runner.server_args.enable_deterministic_inference:
@@ -295,15 +310,22 @@ class FlashAttentionBackend(AttentionBackend):
)
self.has_softcap = _softcapping is not None and _softcapping > 0.0
# If num_splits == 0, we use a heuristic to automatically determine the number of splits.
# We set nums splits to 1 if deterministic inference is enabled.
# See https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/ for more details.
fa4_no_splitkv = self.fa_impl_ver == 4 and get_device_capability() < (9, 0)
self.num_splits = (
1
if model_runner.server_args.enable_deterministic_inference or fa4_no_splitkv
else 0
)
# num_splits == 0 delegates SplitKV sizing to the selected FA runtime.
deterministic = model_runner.server_args.enable_deterministic_inference
if self._get_fa_runtime_policy is None:
self.num_splits = 1 if deterministic else 0
self.decode_num_splits = self.num_splits
self._decode_uses_static_max_seqlen_k = False
else:
runtime_policy = self._get_fa_runtime_policy(
device_capability=device_capability,
deterministic=deterministic,
)
self.num_splits = runtime_policy.num_splits
self.decode_num_splits = runtime_policy.decode_num_splits
self._decode_uses_static_max_seqlen_k = (
runtime_policy.decode_uses_static_max_seqlen_k
)
# Set (never getattr'd) so forward_extend can identity-check "is this the
# full-CG prefill metadata?" to disable the pointer-keyed shear-bias
# block-schedule cache (see forward_extend rel_bias handling).
@@ -506,6 +528,12 @@ class FlashAttentionBackend(AttentionBackend):
# Local attention and scheduler metadata require capture-time slice sizing.
# Both depend on data already filled by replay above.
metadata = self.decode_cuda_graph_metadata[bs]
if self._decode_uses_static_max_seqlen_k:
# FA4 bakes its N-tile grid and SplitKV specialization into
# the graph. Capture against the full replay bound, not the
# padded seq-len fill value (1), or a later long-context
# replay would leave K/V tiles uncovered.
metadata.max_seq_len_k = self.max_context_len
self._maybe_update_local_attn_metadata_for_capture(metadata, bs)
if self._sched_meta_buf is not None:
sched = self._compute_scheduler_metadata(
@@ -1860,6 +1888,8 @@ class FlashAttentionBackend(AttentionBackend):
if layer.is_cross_attention:
# Always use non-chunked logic for cross-attention
if self._decode_uses_static_max_seqlen_k:
kwargs["max_seqlen_k"] = metadata.encoder_max_seq_len_k
o = flash_attn_with_kvcache(
q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
k_cache=key_cache,
@@ -1879,6 +1909,8 @@ class FlashAttentionBackend(AttentionBackend):
)
elif use_local_attn:
# Use chunked (local) attention batching for self-attention
if self._decode_uses_static_max_seqlen_k:
kwargs["max_seqlen_k"] = local_attn_metadata.local_max_seq_len
o = flash_attn_with_kvcache(
q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
k_cache=key_cache,
@@ -1932,6 +1964,8 @@ class FlashAttentionBackend(AttentionBackend):
and not pa_swa_active
):
sched_meta = metadata.scheduler_metadata
if self._decode_uses_static_max_seqlen_k:
kwargs["max_seqlen_k"] = metadata.max_seq_len_k
result = flash_attn_with_kvcache(
q=q_reshaped,
k_cache=key_cache,
@@ -1945,7 +1979,13 @@ class FlashAttentionBackend(AttentionBackend):
window_size=window_size,
softcap=layer.logit_cap,
return_softmax_lse=use_cascade_attn,
num_splits=self.num_splits,
num_splits=(
self.decode_num_splits
if not is_swa_layer
and not use_cascade_attn
and not pa_swa_active
else self.num_splits
),
out=_fa_out,
ver=self.fa_impl_ver,
scheduler_metadata=sched_meta,
@@ -1953,6 +1993,10 @@ class FlashAttentionBackend(AttentionBackend):
)
if use_cascade_attn:
o, softmax_lse, *rest = result
if self._decode_uses_static_max_seqlen_k:
kwargs["max_seqlen_k"] = (
self.forward_metadata_spec_decode_expand.max_seq_len_k
)
o_expand, softmax_lse_expand, *rest_expand = (
flash_attn_with_kvcache(
q=q_reshaped,