Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

2574 lines
105 KiB
Python

import functools
from functools import lru_cache
from typing import Any, Optional, Tuple
import tilelang
import tilelang.language as T
import torch
from sglang.srt.layers.quantization.fp8_kernel import is_fp8_fnuz
from sglang.srt.utils import is_gfx95_supported, is_hip
tilelang.set_log_level("WARNING")
# Workaround a tilelang bug: BaseKernelAdapter._legalize_result_idx mutates the
# `out_idx` list in place when normalising negative indices to positive ones.
# That breaks any @tilelang.jit factory that compiles two prim_funcs with
# different param counts (e.g. our unified single/dual partial kernel) — the
# second compile sees indices already-converted for the first's len(params)
# and silently builds the wrong adapter, leading to IndexError at call time.
# Patch once on import to copy the list before mutation.
from tilelang.jit.adapter.base import ( # noqa: E402
BaseKernelAdapter as _BaseKernelAdapter,
)
if not getattr(_BaseKernelAdapter, "_legalize_result_idx_patched", False):
_orig_legalize = _BaseKernelAdapter._legalize_result_idx
def _legalize_result_idx_safe(self, result_idx):
if isinstance(result_idx, list):
result_idx = list(result_idx)
return _orig_legalize(self, result_idx)
_BaseKernelAdapter._legalize_result_idx = _legalize_result_idx_safe
_BaseKernelAdapter._legalize_result_idx_patched = True
pass_configs = {
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
}
# TL_DISABLE_FAST_MATH has deprecated in v0.1.7.post1 tilelang
if hasattr(tilelang.PassConfigKey, "TL_DISABLE_FAST_MATH"):
pass_configs[tilelang.PassConfigKey.TL_DISABLE_FAST_MATH] = True
elif hasattr(tilelang.PassConfigKey, "TL_ENABLE_FAST_MATH"):
pass_configs[tilelang.PassConfigKey.TL_ENABLE_FAST_MATH] = False
_is_hip = is_hip()
_is_gfx95_supported = is_gfx95_supported()
_is_fp8_fnuz = is_fp8_fnuz()
BF16 = "bfloat16"
FP8 = "float8_e4m3fnuz" if _is_fp8_fnuz else "float8_e4m3fn"
FP8_DTYPE = torch.float8_e4m3fnuz if _is_fp8_fnuz else torch.float8_e4m3fn
FP32 = "float32"
INT32 = "int32"
UINT8 = "uint8"
def fast_log2_ceil(x):
bits_x = T.reinterpret("uint32", x)
exp_x = (bits_x >> 23) & 0xFF
man_bits = bits_x & ((1 << 23) - 1)
return T.Cast("int32", exp_x - 127 + T.if_then_else(man_bits != 0, 1, 0))
def fast_pow2(x):
bits_x = (x + 127) << 23
return T.reinterpret("float32", bits_x)
def fast_round_scale(amax, fp8_max_inv):
return fast_pow2(fast_log2_ceil(amax * fp8_max_inv))
@lru_cache(maxsize=8)
def _pick_inner_iter(seq: int, ni: int, cu: int, block_per_cu: int) -> int:
"""
Pick the largest valid inner_iter (power-of-two divisor of ni) that keeps
enough work per CU (seq * ni / inner_iter / cu >= block_per_cu), so we avoid
under-utilization while minimizing the number of partial groups.
"""
max_it = int(seq * ni / (cu * block_per_cu))
it = ni
while it >= 2:
if it <= max_it and ni % it == 0:
return it
it //= 2
return 1
@tilelang.jit(pass_configs=pass_configs)
def act_quant_kernel(
N, in_dtype=BF16, out_dtype=FP8, scale_dtype=FP32, round_scale=False
):
M = T.symbolic("M")
fp8_min = -224.0 if _is_fp8_fnuz else -448.0
fp8_max = 224.0 if _is_fp8_fnuz else 448.0
fp8_max_inv = 1 / fp8_max
num_stages = 0 if round_scale else 2
blk_m = 32
group_size = 128
@T.prim_func
def act_quant_kernel_(
X: T.Tensor[(M, N), in_dtype],
Y: T.Tensor[(M, N), out_dtype],
S: T.Tensor[(M, T.ceildiv(N, group_size)), scale_dtype],
):
with T.Kernel(T.ceildiv(M, blk_m), T.ceildiv(N, group_size), threads=128) as (
pid_m,
pid_n,
):
x_shared = T.alloc_shared((blk_m, group_size), in_dtype)
x_local = T.alloc_fragment((blk_m, group_size), in_dtype)
amax_local = T.alloc_fragment((blk_m,), scale_dtype)
s_local = T.alloc_fragment((blk_m,), scale_dtype)
y_local = T.alloc_fragment((blk_m, group_size), out_dtype)
y_shared = T.alloc_shared((blk_m, group_size), out_dtype)
for _ in T.Pipelined(1, num_stages=num_stages):
T.copy(X[pid_m * blk_m, pid_n * group_size], x_shared)
T.copy(x_shared, x_local)
T.reduce_absmax(x_local, amax_local, dim=1)
for i in T.Parallel(blk_m):
amax_local[i] = T.max(amax_local[i], 1e-4)
if round_scale:
s_local[i] = fast_round_scale(amax_local[i], fp8_max_inv)
else:
s_local[i] = amax_local[i] * fp8_max_inv
for i, j in T.Parallel(blk_m, group_size):
y_local[i, j] = T.clamp(
x_local[i, j] / s_local[i], fp8_min, fp8_max
)
for i in T.Parallel(blk_m):
S[pid_m * blk_m + i, pid_n] = s_local[i]
T.copy(y_local, y_shared)
T.copy(y_shared, Y[pid_m * blk_m, pid_n * group_size])
return act_quant_kernel_
def act_quant(
x: torch.Tensor, block_size: int = 128, scale_fmt: Optional[str] = None
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Quantizes the input tensor `x` using block-wise quantization.
Args:
x (torch.Tensor): The input tensor to be quantized. Must be contiguous and its last dimension size must be divisible by `block_size`.
block_size (int, optional): The size of the blocks to be used for quantization. Default is 128.
scale_fmt (Optional[str], optional): The format of the scale. Default is None.
Returns:
Tuple[torch.Tensor, torch.Tensor]: A tuple containing:
- The quantized tensor with dtype `torch.float8_e4m3fn`.
- A tensor of scaling factors with dtype `torch.float32`.
"""
assert x.is_contiguous(), "Input tensor must be contiguous"
assert (
x.size(-1) % block_size == 0
), f"Last dimension size must be divisible by block_size (block_size={block_size})"
N = x.size(-1)
if _is_fp8_fnuz:
y = torch.empty_like(x, dtype=torch.float8_e4m3fnuz)
else:
y = torch.empty_like(x, dtype=torch.float8_e4m3fn)
s = x.new_empty(*x.size()[:-1], N // block_size, dtype=torch.float32)
kernel = act_quant_kernel(N, round_scale=scale_fmt is not None)
kernel(x.view(-1, N), y.view(-1, N), s.view(-1, N // block_size))
return y, s
@tilelang.jit(out_idx=[4], pass_configs=pass_configs)
def fp8_index_kernel(h: int, d: int, clear_accum=True):
b = T.symbolic("b")
m = T.symbolic("m")
n = T.symbolic("n")
blk_n1 = 512
blk_n2 = 128
@T.prim_func
def fp8_index_kernel_(
q: T.Tensor[(b, m, h, d), FP8],
q_s: T.Tensor[(b, m, h), FP32],
k: T.Tensor[(b, n, d), FP8],
k_s: T.Tensor[(b, n), FP32],
o: T.Tensor[(b, m, n), FP32],
) -> None:
with T.Kernel(b, m, T.ceildiv(n, blk_n1)) as (i_b, i_m, i1_n):
q_smem = T.alloc_shared((h, d), FP8)
T.copy(q[i_b, i_m, 0, 0], q_smem)
q_s_frag = T.alloc_fragment(h, FP32)
T.copy(q_s[i_b, i_m, 0], q_s_frag)
for i2_n in T.Pipelined(blk_n1 // blk_n2, num_stages=2):
k_smem = T.alloc_shared((blk_n2, d), FP8)
T.copy(k[i_b, i1_n * blk_n1 + i2_n * blk_n2, 0], k_smem)
k_s_frag = T.alloc_fragment(blk_n2, FP32)
T.copy(k_s[i_b, i1_n * blk_n1 + i2_n * blk_n2], k_s_frag)
logits = T.alloc_fragment((blk_n2, h), FP32)
if not clear_accum:
T.fill(logits, 0)
T.gemm(
k_smem,
q_smem,
logits,
transpose_A=False,
transpose_B=True,
clear_accum=clear_accum,
)
for i_h, i3_n in T.Parallel(h, blk_n2):
logits[i3_n, i_h] = T.max(logits[i3_n, i_h], 0) * q_s_frag[i_h]
logits_sum = T.alloc_fragment(blk_n2, FP32)
T.reduce_sum(logits, logits_sum, dim=1)
for i3_n in T.Parallel(blk_n2):
logits_sum[i3_n] *= k_s_frag[i3_n]
T.copy(logits_sum, o[i_b, i_m, i1_n * blk_n1 + i2_n * blk_n2])
return fp8_index_kernel_
def fp8_index(
q: torch.Tensor,
q_s: torch.Tensor,
k: torch.Tensor,
k_s: torch.Tensor,
) -> torch.Tensor:
"""
Perform index score using FP8 precision.
Args:
q (torch.Tensor): The Q tensor, must be contiguous.
q_s (torch.Tensor): The scaling factor for Q (float), must be contiguous.
k (torch.Tensor): The K tensor, must be contiguous.
k_s (torch.Tensor): The scaling factor for K (e8m0 here), must be contiguous.
fp8 q @ fp8 k -> fp32 logits
relu(fp32 logits) * q_s (weights) -> fp32 logits
fp32 logits -> fp32 logits_sum
fp32 logits_sum * k_s (e8m0) -> fp32 index_score
"""
if _is_hip:
return fp8_index_kernel(q.shape[2], q.shape[3], False)(q, q_s, k, k_s)
else:
return fp8_index_kernel(q.shape[2], q.shape[3])(q, q_s, k, k_s)
@tilelang.jit(
out_idx=[-1],
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
},
)
def sparse_attention_fwd_kernel_v1(
num_heads,
dim,
tail_dim,
topk,
*,
kv_group=1,
sm_scale=None,
is_causal=True,
block_I=64,
num_stages=2,
threads=256,
):
assert dim == tilelang.math.next_power_of_2(
dim
), f"haven't check padding correctness yet, dim={dim}"
assert tail_dim == tilelang.math.next_power_of_2(
tail_dim
), f"haven't check padding correctness yet, dim={tail_dim}"
assert is_causal == True, "non-casual is not supported"
assert (
topk % block_I == 0
), "otherwise will load some index=0 thus causing wrong kv to be loaded"
if sm_scale is None:
sm_scale = (1.0 / (dim + tail_dim)) ** 0.5 * 1.44269504 # log2(e)
else:
sm_scale = sm_scale * 1.44269504 # log2(e)
batch = T.symbolic("batch")
seq_len = T.symbolic("seq_len")
seq_len_kv = T.symbolic("seq_len_kv")
head_kv = num_heads // kv_group
q_shape = [batch, seq_len, num_heads, dim + tail_dim]
kv_shape = [batch, seq_len_kv, kv_group, dim + tail_dim]
o_shape = [batch, seq_len, num_heads, dim]
indices_shape = [batch, seq_len, kv_group, topk]
indices_dtype = "int32"
dtype = "bfloat16"
accum_dtype = "float"
H = head_kv
padded_H = max(tilelang.math.next_power_of_2(head_kv), 16)
if padded_H != H:
assert kv_group == 1
BI = block_I
NI = tilelang.cdiv(topk, block_I)
D = dim
D_tail = tail_dim
if head_kv > 64:
assert head_kv % 64 == 0, "head_kv should be a multiple of 64"
REPLICATE_H = head_kv // 64
else:
REPLICATE_H = 1
H_per_block = padded_H if REPLICATE_H == 1 else 64
@T.prim_func
def main(
Q: T.Tensor(q_shape, dtype), # type: ignore
KV: T.Tensor(kv_shape, dtype), # type: ignore
Indices: T.Tensor(indices_shape, indices_dtype), # type: ignore
Output: T.Tensor(o_shape, dtype), # type: ignore
):
with T.Kernel(seq_len * REPLICATE_H, batch, kv_group, threads=threads) as (
bx,
by,
bz,
):
Q_shared = T.alloc_shared([H_per_block, D], dtype)
Q_tail_shared = T.alloc_shared([H_per_block, D_tail], dtype)
KV_shared = T.alloc_shared([BI, D], dtype)
K_tail_shared = T.alloc_shared([BI, D_tail], dtype)
O_shared = T.alloc_shared([H_per_block, D], dtype)
mask = T.alloc_fragment([BI], "bool")
acc_o = T.alloc_fragment([H_per_block, D], accum_dtype)
acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype)
S_shared = T.alloc_shared([H_per_block, BI], dtype)
sumexp = T.alloc_fragment([H_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([H_per_block], accum_dtype)
alpha = T.alloc_fragment([H_per_block], accum_dtype)
m_i = T.alloc_fragment([H_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([H_per_block], accum_dtype)
T.fill(acc_o, 0)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30)) # avoid -inf - inf to cause nan
b_i, g_i = by, bz
s_i = bx if REPLICATE_H == 1 else (bx // REPLICATE_H)
q_i = s_i
max_kv_i = q_i
H0 = g_i * padded_H + (0 if REPLICATE_H == 1 else (bx % REPLICATE_H) * 64)
H1 = H0 + H_per_block
T.copy(Q[b_i, s_i, H0:H1, :D], Q_shared)
T.copy(Q[b_i, s_i, H0:H1, D:], Q_tail_shared)
for i_i in T.Pipelined(NI, num_stages=num_stages):
for bi_i in T.Parallel(BI):
mask[bi_i] = Indices[b_i, s_i, g_i, i_i * BI + bi_i] >= 0
for bi_i, d_i in T.Parallel(BI, D):
KV_shared[bi_i, d_i] = KV[
b_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i], g_i, d_i
]
for bi_i, d_i in T.Parallel(BI, D_tail):
K_tail_shared[bi_i, d_i] = KV[
b_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i], g_i, D + d_i
]
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(
Q_shared,
KV_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullCol,
)
T.gemm(
Q_tail_shared,
K_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullCol,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1) # is this a accumulate operator?
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] = acc_o[h_i, d_i] * alpha[h_i]
T.copy(acc_s, S_shared)
T.gemm(S_shared, KV_shared, acc_o, policy=T.GemmWarpPolicy.FullCol)
# Rescale
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] /= sumexp[h_i]
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale
T.copy(acc_o, O_shared)
T.copy(acc_o, Output[b_i, s_i, H0:H1, :])
return main
@tilelang.jit(
out_idx=[-1],
compile_flags=[
"-O3",
"-Wno-deprecated-declarations",
"-U__CUDA_NO_HALF_OPERATORS__",
"-U__CUDA_NO_HALF_CONVERSIONS__",
"-U__CUDA_NO_HALF2_OPERATORS__",
"-U__CUDA_NO_BFLOAT16_CONVERSIONS__",
"--expt-relaxed-constexpr",
"--expt-extended-lambda",
"--ptxas-options=-v,--register-usage-level=10",
"-DNDEBUG",
],
) # type: ignore
def sparse_attention_fwd_kernel_v2(
num_heads: int,
dim: int,
tail_dim: int,
topk: int,
*,
kv_group: int = 1,
sm_scale: Optional[float] = None,
block_I: int = 64,
):
assert dim == tilelang.math.next_power_of_2(
dim
), f"haven't check padding correctness yet, dim={dim}"
assert tail_dim == tilelang.math.next_power_of_2(
tail_dim
), f"haven't check padding correctness yet, dim={tail_dim}"
assert (
topk % block_I == 0
), "otherwise will load some index=0 thus causing wrong kv to be loaded"
if sm_scale is None:
sm_scale = (1.0 / (dim + tail_dim)) ** 0.5 * 1.44269504 # log2(e)
else:
sm_scale = sm_scale * 1.44269504 # log2(e)
threads = 384
batch = T.symbolic("batch")
qo_len = T.symbolic("seq_len")
num_pages = T.symbolic("num_pages")
q_shape = [batch, qo_len, num_heads, dim + tail_dim]
kv_shape = [batch, num_pages, kv_group, dim + tail_dim]
o_shape = [batch, qo_len, num_heads, dim]
indices_shape = [batch, qo_len, kv_group, topk]
indices_dtype = "int32"
dtype = "bfloat16"
accum_dtype = "float"
H = num_heads
padded_H = max(tilelang.math.next_power_of_2(num_heads), 16)
if padded_H != H:
assert kv_group == 1
BI = block_I
NI = tilelang.cdiv(topk, block_I)
assert NI % 2 == 0, "NI should be a multiple of 2"
D = dim
D_tail = tail_dim
if num_heads > 64:
assert num_heads % 64 == 0, "head_kv should be a multiple of 64"
REPLICATE_H = num_heads // 64
else:
REPLICATE_H = 1
H_per_block = padded_H if REPLICATE_H == 1 else 64
@T.prim_func
def main(
Q: T.Tensor(q_shape, dtype), # type: ignore
KV: T.Tensor(kv_shape, dtype), # type: ignore
Indices: T.Tensor(indices_shape, indices_dtype), # type: ignore
Output: T.Tensor(o_shape, dtype), # type: ignore
):
"""
Q: [b, qo_len, H, D + D_tail] (bfloat16)
KV: [b, num_pages, kv_group, D + D_tail] (bfloat16)
Indices: [b, qo_len, kv_group, topk] (int32)
"""
with T.Kernel(qo_len * REPLICATE_H, batch, 1, threads=threads) as (bx, by, bz): # type: ignore
Q_shared_l = T.alloc_shared([H_per_block, D // 2], dtype)
Q_shared_r = T.alloc_shared([H_per_block, D // 2], dtype)
Q_tail_shared = T.alloc_shared([H_per_block, D_tail], dtype)
KV_shared_0_l = T.alloc_shared([BI, D // 2], dtype)
KV_shared_0_r = T.alloc_shared([BI, D // 2], dtype)
KV_shared_1_l = T.alloc_shared([BI, D // 2], dtype)
KV_shared_1_r = T.alloc_shared([BI, D // 2], dtype)
K_tail_shared_0 = T.alloc_shared([BI, D_tail], dtype)
K_tail_shared_1 = T.alloc_shared([BI, D_tail], dtype)
O_shared_l = Q_shared_l
O_shared_r = Q_shared_r
is_kv_valid_0 = T.alloc_shared([BI], "bool", scope="shared")
is_kv_valid_1 = T.alloc_shared([BI], "bool", scope="shared")
acc_o_l = T.alloc_fragment([H_per_block, D // 2], accum_dtype)
acc_o_r = T.alloc_fragment([H_per_block, D // 2], accum_dtype)
acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype)
S_shared = T.alloc_shared([H_per_block, BI], dtype)
sumexp = T.alloc_fragment([H_per_block], accum_dtype)
sum_exp_shared = T.alloc_shared([H_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([H_per_block], accum_dtype)
alpha_shared = T.alloc_shared([H_per_block], accum_dtype, scope="shared")
alpha_local = T.alloc_fragment([H_per_block], accum_dtype)
m_i = T.alloc_fragment([H_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([H_per_block], accum_dtype)
indices_local = T.alloc_local([1], indices_dtype)
indices_tmp = T.alloc_local([1], indices_dtype)
bar_q = T.alloc_barrier(arrive_count=384)
bar_k_0_ready = T.alloc_barrier(arrive_count=128)
bar_k_1_ready = T.alloc_barrier(arrive_count=128)
bar_k_0_free = T.alloc_barrier(arrive_count=256)
bar_k_1_free = T.alloc_barrier(arrive_count=256)
bar_sScale_and_sS_ready = T.alloc_barrier(arrive_count=256)
bar_sScale_and_sS_free = T.alloc_barrier(arrive_count=256)
bar_0_128 = T.alloc_barrier(arrive_count=128)
bar_1_128 = T.alloc_barrier(arrive_count=128)
bar_2_128 = T.alloc_barrier(arrive_count=128)
bar_final = T.alloc_barrier(arrive_count=128)
b_i, g_i = by, bz
s_i = bx if REPLICATE_H == 1 else bx // REPLICATE_H
H0 = g_i * padded_H + (0 if REPLICATE_H == 1 else (bx % REPLICATE_H) * 64)
H1 = H0 + H_per_block
tx = T.get_thread_binding()
T.copy(Q[b_i, s_i, H0:H1, 0 : D // 2], Q_shared_l)
T.copy(Q[b_i, s_i, H0:H1, D // 2 : D], Q_shared_r)
T.copy(Q[b_i, s_i, H0:H1, D:], Q_tail_shared)
T.barrier_arrive(bar_q)
if tx < 128:
T.set_max_nreg(240, 1)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30)) # avoid -inf - inf to cause nan
T.fill(acc_o_l, 0)
T.barrier_wait(bar_q, 0)
for i_i in T.serial(T.ceildiv(NI, 2)):
# Buffer 0
# with sync_at(bar_0_128, 0):
T.barrier_wait(bar_k_0_ready[0], (i_i & 1))
T.barrier_arrive(bar_0_128)
T.barrier_wait(bar_0_128, 0)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
is_kv_valid_0[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(Q_shared_l, KV_shared_0_l, acc_s, transpose_B=True)
T.gemm(Q_shared_r, KV_shared_0_r, acc_s, transpose_B=True)
T.gemm(
Q_tail_shared,
K_tail_shared_0,
acc_s,
transpose_B=True,
)
if i_i != 0:
T.barrier_arrive(bar_sScale_and_sS_free)
T.barrier_wait(bar_sScale_and_sS_free, ((i_i * 2) & 1) ^ 1)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
alpha_local[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(
acc_s, sumexp_i, dim=1
) # is this a accumulate operator?
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha_local[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_l[h_i, d_i] *= alpha_local[h_i]
T.copy(alpha_local, alpha_shared)
T.copy(acc_s, S_shared)
T.gemm(S_shared, KV_shared_0_l, acc_o_l)
T.barrier_arrive(bar_sScale_and_sS_ready)
T.barrier_arrive(bar_k_0_free[0])
# Buffer 1
T.barrier_wait(bar_k_1_ready[0], (i_i & 1))
T.barrier_arrive(bar_0_128)
T.barrier_wait(bar_0_128, 1)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
is_kv_valid_1[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(Q_shared_l, KV_shared_1_l, acc_s, transpose_B=True)
T.gemm(Q_shared_r, KV_shared_1_r, acc_s, transpose_B=True)
T.gemm(
Q_tail_shared,
K_tail_shared_1,
acc_s,
transpose_B=True,
)
T.barrier_arrive(bar_sScale_and_sS_free)
T.barrier_wait(bar_sScale_and_sS_free, ((i_i * 2 + 1) & 1) ^ 1)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
alpha_local[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(
acc_s, sumexp_i, dim=1
) # is this a accumulate operator?
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha_local[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_l[h_i, d_i] *= alpha_local[h_i]
T.copy(alpha_local, alpha_shared)
T.copy(acc_s, S_shared)
T.gemm(S_shared, KV_shared_1_l, acc_o_l)
T.barrier_arrive(bar_sScale_and_sS_ready)
T.barrier_arrive(bar_k_1_free[0])
# Rescale
for h_i in T.Parallel(H_per_block):
sum_exp_shared[h_i] = sumexp[h_i]
T.barrier_arrive(bar_final)
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_l[h_i, d_i] /= sumexp[h_i]
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale
T.copy(acc_o_l, O_shared_l)
T.copy(O_shared_l, Output[b_i, s_i, H0:H1, 0 : D // 2])
elif tx >= 128 and tx < 256:
# T.set_max_nreg(168, 1)
T.fill(acc_o_r, 0)
for i_i in T.serial(T.ceildiv(NI, 2)):
# Buffer 0
T.barrier_arrive(bar_sScale_and_sS_ready)
T.barrier_wait(bar_sScale_and_sS_ready, ((i_i * 2) & 1))
T.barrier_arrive(bar_1_128)
T.barrier_wait(bar_1_128, 0)
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_r[h_i, d_i] *= alpha_shared[h_i]
T.gemm(S_shared, KV_shared_0_r, acc_o_r)
T.barrier_arrive(bar_k_0_free[0])
T.barrier_arrive(bar_sScale_and_sS_free)
# Buffer 1
T.barrier_arrive(bar_sScale_and_sS_ready)
T.barrier_wait(bar_sScale_and_sS_ready, ((i_i * 2 + 1) & 1))
T.barrier_arrive(bar_1_128)
T.barrier_wait(bar_1_128, 1)
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_r[h_i, d_i] *= alpha_shared[h_i]
T.gemm(S_shared, KV_shared_1_r, acc_o_r)
T.barrier_arrive(bar_k_1_free[0])
if i_i != T.ceildiv(NI, 2) - 1:
T.barrier_arrive(bar_sScale_and_sS_free)
# Rescale
T.barrier_wait(bar_final, 0)
for h_i, d_i in T.Parallel(H_per_block, D // 2):
acc_o_r[h_i, d_i] /= sum_exp_shared[h_i]
T.copy(acc_o_r, O_shared_r)
T.copy(O_shared_r, Output[b_i, s_i, H0:H1, D // 2 : D])
elif tx >= 256:
# producer
T.set_max_nreg(80, 0)
indices_local[0] = 0
for i_i in T.serial(T.ceildiv(NI, 2)):
# Buffer 0
T.barrier_wait(bar_k_0_free[0], ((i_i & 1) ^ 1))
T.barrier_arrive(bar_2_128)
T.barrier_wait(bar_2_128, 0)
for r in T.serial(4):
indices_tmp[0] = Indices[
b_i, s_i, g_i, (i_i * 2) * BI + r * 16 + (tx - 256) // 8
]
is_kv_valid_0[r * 16 + (tx - 256) // 8] = indices_tmp[0] >= 0
if is_kv_valid_0[r * 16 + (tx - 256) // 8]:
indices_local[0] = indices_tmp[0]
with T.attr("default", "async_scope", 1): # type: ignore
for u in T.serial(4):
for v in T.vectorized(8):
KV_shared_0_l[
r * 16 + (tx - 256) // 8,
64 * u + (tx - 256) % 8 * 8 + v,
] = KV[
b_i,
indices_local[0],
g_i,
64 * u + (tx - 256) % 8 * 8 + v,
]
KV_shared_0_r[
r * 16 + (tx - 256) // 8,
64 * u + (tx - 256) % 8 * 8 + v,
] = KV[
b_i,
indices_local[0],
g_i,
D // 2 + 64 * u + (tx - 256) % 8 * 8 + v,
]
with T.attr("default", "async_scope", 1): # type: ignore
for v in T.vectorized(8):
K_tail_shared_0[
r * 16 + (tx - 256) // 8, (tx - 256) % 8 * 8 + v
] = KV[
b_i,
indices_local[0],
g_i,
D + (tx - 256) % 8 * 8 + v,
]
T.cp_async_barrier_noinc(bar_k_0_ready[0])
# Buffer 1
T.barrier_wait(bar_k_1_free[0], ((i_i & 1) ^ 1))
T.barrier_arrive(bar_2_128)
T.barrier_wait(bar_2_128, 1)
for r in T.serial(4):
indices_tmp[0] = Indices[
b_i, s_i, g_i, (i_i * 2 + 1) * BI + r * 16 + (tx - 256) // 8
]
is_kv_valid_1[r * 16 + (tx - 256) // 8] = indices_tmp[0] >= 0
if is_kv_valid_1[r * 16 + (tx - 256) // 8]:
indices_local[0] = indices_tmp[0]
with T.attr("default", "async_scope", 1): # type: ignore
for u in T.serial(4):
for v in T.vectorized(8):
KV_shared_1_l[
r * 16 + (tx - 256) // 8,
64 * u + (tx - 256) % 8 * 8 + v,
] = KV[
b_i,
indices_local[0],
g_i,
64 * u + (tx - 256) % 8 * 8 + v,
]
KV_shared_1_r[
r * 16 + (tx - 256) // 8,
64 * u + (tx - 256) % 8 * 8 + v,
] = KV[
b_i,
indices_local[0],
g_i,
D // 2 + 64 * u + (tx - 256) % 8 * 8 + v,
]
with T.attr("default", "async_scope", 1): # type: ignore
for v in T.vectorized(8):
K_tail_shared_1[
r * 16 + (tx - 256) // 8, (tx - 256) % 8 * 8 + v
] = KV[
b_i,
indices_local[0],
g_i,
D + (tx - 256) % 8 * 8 + v,
]
T.cp_async_barrier_noinc(bar_k_1_ready[0])
return main
@tilelang.jit(
out_idx=[-2, -1],
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
},
)
def sparse_mla_fwd_decode_partial(
heads,
dim,
tail_dim,
topk,
*,
kv_group=1,
sm_scale=None,
is_causal=True,
block_I=64,
inner_iter=1,
num_stages=1,
threads=256,
):
"""
grid: (seq_len * REPLICATE_H, top_k / block_I / inner_iter)
Each GPU block processes `inner_iter` consecutive KV tiles and writes one (partial_o, partial_lse) entry.
"""
assert is_causal == True, "non-causal is not supported"
assert kv_group == 1
assert topk % block_I == 0
assert topk % (block_I * inner_iter) == 0, (
f"topk ({topk}) must be divisible by block_I * inner_iter = "
f"{block_I} * {inner_iter}"
)
# log2(e) = 1.44269504
if sm_scale is None:
sm_scale = (1.0 / (dim + tail_dim)) ** 0.5 * 1.44269504
else:
sm_scale = sm_scale * 1.44269504
batch = 1
seq_len = T.dynamic("seq_len")
seq_len_kv = T.dynamic("seq_len_kv")
head_kv = heads // kv_group
padded_H = max(tilelang.math.next_power_of_2(head_kv), 16)
REPLICATE_H = (head_kv // 64) if head_kv > 64 else 1
H_per_block = padded_H if REPLICATE_H == 1 else 64
N_GROUPS = topk // (block_I * inner_iter)
BI = block_I
D = dim
D_tail = tail_dim
q_shape = [batch, seq_len, heads, dim + tail_dim]
kv_shape = [batch, seq_len_kv, kv_group, dim + tail_dim]
indices_shape = [batch, seq_len, kv_group, topk]
partial_o_shape = [batch, seq_len, N_GROUPS, heads, dim]
partial_lse_shape = [batch, seq_len, N_GROUPS, heads]
indices_dtype = T.int32
dtype = T.bfloat16
accum_dtype = T.float32
_q_in_shared = inner_iter == 1
@T.prim_func
def main(
Q: T.Tensor(q_shape, dtype),
KV: T.Tensor(kv_shape, dtype),
Indices: T.Tensor(indices_shape, indices_dtype),
Partial_O: T.Tensor(partial_o_shape, dtype),
Partial_Lse: T.Tensor(partial_lse_shape, accum_dtype),
):
with T.Kernel(seq_len * REPLICATE_H, N_GROUPS, threads=threads) as (bx, by):
if _q_in_shared:
Q_buf = T.alloc_shared([H_per_block, D], dtype)
Q_tail_buf = T.alloc_shared([H_per_block, D_tail], dtype)
else:
Q_buf = T.alloc_fragment([H_per_block, D], dtype)
Q_tail_buf = T.alloc_fragment([H_per_block, D_tail], dtype)
KV_shared = T.alloc_shared([BI, D], dtype)
K_tail_shared = T.alloc_shared([BI, D_tail], dtype)
S_shared = T.alloc_shared([H_per_block, BI], dtype)
mask = T.alloc_fragment([BI], T.bool)
acc_o = T.alloc_fragment([H_per_block, D], accum_dtype)
acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype)
sumexp = T.alloc_fragment([H_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([H_per_block], accum_dtype)
alpha = T.alloc_fragment([H_per_block], accum_dtype)
m_i = T.alloc_fragment([H_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([H_per_block], accum_dtype)
T.fill(acc_o, 0)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30))
b_i, g_i = 0, 0
s_i = bx if REPLICATE_H == 1 else (bx // REPLICATE_H)
group_i = by
H0 = 0 if REPLICATE_H == 1 else (bx % REPLICATE_H) * 64
H1 = H0 + H_per_block
T.copy(Q[b_i, s_i, H0:H1, :D], Q_buf)
T.copy(Q[b_i, s_i, H0:H1, D:], Q_tail_buf)
for k_i in T.Pipelined(inner_iter, num_stages=num_stages):
topk_block_i = group_i * inner_iter + k_i
for bi_i in T.Parallel(BI):
mask[bi_i] = Indices[b_i, s_i, g_i, topk_block_i * BI + bi_i] >= 0
for bi_i, d_i in T.Parallel(BI, D):
idx = Indices[b_i, s_i, g_i, topk_block_i * BI + bi_i]
KV_shared[bi_i, d_i] = KV[
b_i, T.if_then_else(idx >= 0, idx, 0), g_i, d_i
]
for bi_i, d_i in T.Parallel(BI, D_tail):
idx = Indices[b_i, s_i, g_i, topk_block_i * BI + bi_i]
K_tail_shared[bi_i, d_i] = KV[
b_i, T.if_then_else(idx >= 0, idx, 0), g_i, D + d_i
]
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(
Q_buf,
KV_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullCol,
)
T.gemm(
Q_tail_buf,
K_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullCol,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1)
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] *= alpha[h_i]
T.copy(acc_s, S_shared)
T.gemm(S_shared, KV_shared, acc_o, policy=T.GemmWarpPolicy.FullCol)
# sumexp==0 (all masked), divide by 1 to get 0 and avoid nan
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] = acc_o[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
# sumexp==0 (all masked), use large negative so combine ignores this split
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = T.if_then_else(
sumexp[h_i] == 0.0,
-(2**30),
T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale,
)
T.copy(acc_o, Partial_O[b_i, s_i, group_i, H0:H1, :])
T.copy(sumexp, Partial_Lse[b_i, s_i, group_i, H0:H1])
return main
@tilelang.jit(
out_idx=[-1],
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
},
)
def sparse_mla_fwd_decode_combine(
heads,
dim,
topk,
head_per_block,
*,
block_I=64,
threads=256,
):
"""
grid: (seq_len * REPLICATE_H). batch=1, kv_group=1.
Each block does one tile of heads (e.g. 4 or 8 for decode).
"""
assert heads % head_per_block == 0, f"head_per_block must divide heads"
batch = 1
seq_len = T.dynamic("seq_len")
NI = topk // block_I
H_per_block = head_per_block
REPLICATE_H = heads // H_per_block
partial_o_shape = [batch, seq_len, NI, heads, dim]
partial_lse_shape = [batch, seq_len, NI, heads]
o_shape = [batch, seq_len, heads, dim]
dtype = T.bfloat16
accum_dtype = T.float32
@T.prim_func
def main(
Partial_O: T.Tensor(partial_o_shape, dtype),
Partial_Lse: T.Tensor(partial_lse_shape, accum_dtype),
Output: T.Tensor(o_shape, dtype),
):
with T.Kernel(seq_len * REPLICATE_H, threads=threads) as (bx,):
shared_lse = T.alloc_shared([NI, H_per_block], accum_dtype)
lse_max = T.alloc_fragment([H_per_block], accum_dtype)
lse_sum = T.alloc_fragment([H_per_block], accum_dtype)
scale = T.alloc_fragment([H_per_block, NI], accum_dtype)
acc_o = T.alloc_fragment([H_per_block, dim], accum_dtype)
b_i = 0
s_i = bx if REPLICATE_H == 1 else (bx // REPLICATE_H)
H0 = 0 if REPLICATE_H == 1 else (bx % REPLICATE_H) * H_per_block
H1 = H0 + H_per_block
for k in T.serial(NI):
T.copy(Partial_Lse[b_i, s_i, k, H0:H1], shared_lse[k, :])
T.fill(lse_max, -(2**30))
for k in T.serial(NI):
for h_i in T.Parallel(H_per_block):
lse_max[h_i] = T.max(lse_max[h_i], shared_lse[k, h_i])
T.fill(lse_sum, 0)
for k in T.serial(NI):
for h_i in T.Parallel(H_per_block):
lse_sum[h_i] = lse_sum[h_i] + T.exp2(
shared_lse[k, h_i] - lse_max[h_i]
)
for k in T.serial(NI):
for h_i in T.Parallel(H_per_block):
scale[h_i, k] = T.exp2(
shared_lse[k, h_i] - lse_max[h_i] - T.log2(lse_sum[h_i])
)
T.fill(acc_o, 0)
for k in T.serial(NI):
for h_i, d_i in T.Parallel(H_per_block, dim):
acc_o[h_i, d_i] = acc_o[h_i, d_i] + scale[h_i, k] * Partial_O[
b_i, s_i, k, H0 + h_i, d_i
].astype(accum_dtype)
T.copy(acc_o, Output[b_i, s_i, H0:H1, :])
return main
@tilelang.jit(out_idx=[-2, -1], pass_configs=pass_configs)
def sparse_mla_fwd_decode_partial_fp8(
num_heads: int,
d_v: int,
d_tail: int,
topk: int,
*,
sm_scale=None,
block_I=64,
inner_iter=1,
threads=256,
):
assert d_v == 512, f"only support d_v=512"
assert (
topk % block_I == 0
), "otherwise will load some index=0 thus causing wrong kv to be loaded"
# Softmax scores are in [0, 1]. We scale by fp8_max_val before FP8 cast
# to better utilize FP8 dynamic range, then apply the inverse scale after GEMM.
# This is numerically safe because softmax output is bounded by 1.
fp8_dtype = "float8_e4m3fnuz" if _is_fp8_fnuz else "float8_e4m3fn"
fp8_max_val = 240.0 if _is_fp8_fnuz else 448.0
s_inv_scale_const = fp8_max_val
s_scale_const = 1.0 / fp8_max_val
BI = block_I
group_size = 128
dim_quant_fp8 = d_v + d_tail
rope_offset_fp8 = d_v
n_groups = topk // (BI * inner_iter)
if sm_scale is None:
sm_scale = (1.0 / (d_v + d_tail)) ** 0.5 * 1.44269504
else:
sm_scale = sm_scale * 1.44269504
h_per_block = 16
# Match bf16 partial behavior: keep fixed 16-head tiles and use
# sliced T.copy on H0:H1 for tail handling.
assert (
num_heads <= h_per_block or num_heads % h_per_block == 0
), "num_heads must be <=16 or divisible by 16"
head_blocks_per_seq = (num_heads + h_per_block - 1) // h_per_block
batch = 1
kv_group = 1
seq_len = T.symbolic("seq_len")
num_pages = T.symbolic("num_pages")
q_fp8_shape = [batch, seq_len, num_heads, d_v + d_tail]
kv_fp8_shape = [batch, num_pages, kv_group, dim_quant_fp8]
idx_shape = [batch, seq_len, kv_group, topk]
partial_o_shape = [batch, seq_len, n_groups, num_heads, d_v]
partial_lse_shape = [batch, seq_len, n_groups, num_heads]
accum_dtype = T.float32
dtype_bf16 = T.bfloat16
@T.prim_func
def main(
q_fp8: T.Tensor(q_fp8_shape, fp8_dtype),
kv_fp8: T.Tensor(kv_fp8_shape, fp8_dtype),
indices: T.Tensor(idx_shape, T.int32),
partial_o: T.Tensor(partial_o_shape, dtype_bf16),
partial_lse: T.Tensor(partial_lse_shape, accum_dtype),
):
with T.Kernel(seq_len * head_blocks_per_seq, n_groups, threads=threads) as (
bx,
by,
):
b_i, g_i = 0, 0
s_i = bx // head_blocks_per_seq
group_i = by
H0 = (bx % head_blocks_per_seq) * h_per_block
H1 = H0 + h_per_block
# We intentionally split the K=512 GEMM into 4x128 tiles.
# Although this adds extra intermediate memory traffic,
# it shortens the MFMA accumulation dependency chain and improves performance.
q_tile0 = T.alloc_shared([h_per_block, group_size], fp8_dtype)
q_tile1 = T.alloc_shared([h_per_block, group_size], fp8_dtype)
q_tile2 = T.alloc_shared([h_per_block, group_size], fp8_dtype)
q_tile3 = T.alloc_shared([h_per_block, group_size], fp8_dtype)
kv_tile0 = T.alloc_shared([BI, group_size], fp8_dtype)
kv_tile1 = T.alloc_shared([BI, group_size], fp8_dtype)
kv_tile2 = T.alloc_shared([BI, group_size], fp8_dtype)
kv_tile3 = T.alloc_shared([BI, group_size], fp8_dtype)
q_tail_buf = T.alloc_shared([h_per_block, d_tail], fp8_dtype)
k_tail_shared = T.alloc_shared([BI, d_tail], fp8_dtype)
s_fp8_shared = T.alloc_shared([h_per_block, BI], fp8_dtype)
page_idx_shared = T.alloc_shared([BI], T.int32)
mask = T.alloc_fragment([BI], T.bool)
acc_s = T.alloc_fragment([h_per_block, BI], accum_dtype)
acc_tile = T.alloc_fragment([h_per_block, BI], accum_dtype)
sv_tile = T.alloc_fragment([h_per_block, group_size], accum_dtype)
sumexp = T.alloc_fragment([h_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([h_per_block], accum_dtype)
alpha = T.alloc_fragment([h_per_block], accum_dtype)
m_i = T.alloc_fragment([h_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([h_per_block], accum_dtype)
inv_denom = T.alloc_fragment([h_per_block], accum_dtype)
acc_o_tile0 = T.alloc_fragment([h_per_block, group_size], accum_dtype)
acc_o_tile1 = T.alloc_fragment([h_per_block, group_size], accum_dtype)
acc_o_tile2 = T.alloc_fragment([h_per_block, group_size], accum_dtype)
acc_o_tile3 = T.alloc_fragment([h_per_block, group_size], accum_dtype)
T.fill(acc_o_tile0, 0)
T.fill(acc_o_tile1, 0)
T.fill(acc_o_tile2, 0)
T.fill(acc_o_tile3, 0)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30))
T.copy(q_fp8[b_i, s_i, H0:H1, d_v:], q_tail_buf)
T.copy(q_fp8[b_i, s_i, H0:H1, 0 * group_size : 1 * group_size], q_tile0)
T.copy(q_fp8[b_i, s_i, H0:H1, 1 * group_size : 2 * group_size], q_tile1)
T.copy(q_fp8[b_i, s_i, H0:H1, 2 * group_size : 3 * group_size], q_tile2)
T.copy(q_fp8[b_i, s_i, H0:H1, 3 * group_size : 4 * group_size], q_tile3)
for k_i in T.serial(inner_iter):
topk_block_i = group_i * inner_iter + k_i
for bi_i in T.Parallel(BI):
idx = indices[b_i, s_i, g_i, topk_block_i * BI + bi_i]
valid = idx >= 0
page_idx_shared[bi_i] = T.if_then_else(valid, idx, 0)
mask[bi_i] = valid
for bi_i, j in T.Parallel(BI, group_size):
page = page_idx_shared[bi_i]
kv_tile0[bi_i, j] = kv_fp8[b_i, page, g_i, 0 * group_size + j]
kv_tile1[bi_i, j] = kv_fp8[b_i, page, g_i, 1 * group_size + j]
kv_tile2[bi_i, j] = kv_fp8[b_i, page, g_i, 2 * group_size + j]
kv_tile3[bi_i, j] = kv_fp8[b_i, page, g_i, 3 * group_size + j]
for bi_i, j in T.Parallel(BI, d_tail):
page = page_idx_shared[bi_i]
k_tail_shared[bi_i, j] = kv_fp8[b_i, page, g_i, rope_offset_fp8 + j]
for h_i, bi_i in T.Parallel(h_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(q_tile0, kv_tile0, acc_s, transpose_B=True, clear_accum=False)
T.gemm(q_tile1, kv_tile1, acc_tile, transpose_B=True, clear_accum=True)
for h_i, bi_i in T.Parallel(h_per_block, BI):
acc_s[h_i, bi_i] += acc_tile[h_i, bi_i]
T.gemm(q_tile2, kv_tile2, acc_tile, transpose_B=True, clear_accum=True)
for h_i, bi_i in T.Parallel(h_per_block, BI):
acc_s[h_i, bi_i] += acc_tile[h_i, bi_i]
T.gemm(q_tile3, kv_tile3, acc_tile, transpose_B=True, clear_accum=True)
for h_i, bi_i in T.Parallel(h_per_block, BI):
acc_s[h_i, bi_i] += acc_tile[h_i, bi_i]
T.gemm(
q_tail_buf,
k_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullCol,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(h_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(h_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1)
for h_i in T.Parallel(h_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile0[h_i, j] = acc_o_tile0[h_i, j] * alpha[h_i]
acc_o_tile1[h_i, j] = acc_o_tile1[h_i, j] * alpha[h_i]
acc_o_tile2[h_i, j] = acc_o_tile2[h_i, j] * alpha[h_i]
acc_o_tile3[h_i, j] = acc_o_tile3[h_i, j] * alpha[h_i]
for h_i, bi_i in T.Parallel(h_per_block, BI):
s_fp8_shared[h_i, bi_i] = T.clamp(
acc_s[h_i, bi_i] * s_inv_scale_const,
-fp8_max_val,
fp8_max_val,
)
T.gemm(s_fp8_shared, kv_tile0, sv_tile, clear_accum=True)
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile0[h_i, j] = (
acc_o_tile0[h_i, j] + sv_tile[h_i, j] * s_scale_const
)
T.gemm(s_fp8_shared, kv_tile1, sv_tile, clear_accum=True)
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile1[h_i, j] = (
acc_o_tile1[h_i, j] + sv_tile[h_i, j] * s_scale_const
)
T.gemm(s_fp8_shared, kv_tile2, sv_tile, clear_accum=True)
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile2[h_i, j] = (
acc_o_tile2[h_i, j] + sv_tile[h_i, j] * s_scale_const
)
T.gemm(s_fp8_shared, kv_tile3, sv_tile, clear_accum=True)
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile3[h_i, j] = (
acc_o_tile3[h_i, j] + sv_tile[h_i, j] * s_scale_const
)
for h_i in T.Parallel(h_per_block):
denom = T.if_then_else(sumexp[h_i] == 0.0, 1.0, sumexp[h_i])
inv_denom[h_i] = 1.0 / denom
for h_i, j in T.Parallel(h_per_block, group_size):
acc_o_tile0[h_i, j] = acc_o_tile0[h_i, j] * inv_denom[h_i]
acc_o_tile1[h_i, j] = acc_o_tile1[h_i, j] * inv_denom[h_i]
acc_o_tile2[h_i, j] = acc_o_tile2[h_i, j] * inv_denom[h_i]
acc_o_tile3[h_i, j] = acc_o_tile3[h_i, j] * inv_denom[h_i]
for h_i in T.Parallel(h_per_block):
sumexp[h_i] = T.if_then_else(
sumexp[h_i] == 0.0,
-(2**30),
T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale,
)
T.copy(
acc_o_tile0,
partial_o[b_i, s_i, group_i, H0:H1, 0 * group_size : 1 * group_size],
)
T.copy(
acc_o_tile1,
partial_o[b_i, s_i, group_i, H0:H1, 1 * group_size : 2 * group_size],
)
T.copy(
acc_o_tile2,
partial_o[b_i, s_i, group_i, H0:H1, 2 * group_size : 3 * group_size],
)
T.copy(
acc_o_tile3,
partial_o[b_i, s_i, group_i, H0:H1, 3 * group_size : 4 * group_size],
)
T.copy(sumexp, partial_lse[b_i, s_i, group_i, H0:H1])
return main
def tilelang_sparse_fwd(
q: torch.Tensor,
kv: torch.Tensor,
indices: torch.Tensor,
sm_scale: float,
d_v: int = 512,
) -> torch.Tensor:
assert q.dim() == 3 and kv.dim() == 3 and indices.dim() == 3
num_heads = q.shape[1]
dim = q.shape[2]
tail_dim = dim - d_v
topk = indices.shape[-1]
assert topk == 2048
if _is_hip:
is_fp8_kv = kv.dtype in (torch.float8_e4m3fn, torch.float8_e4m3fnuz)
if is_fp8_kv:
if q.dtype != kv.dtype:
q = q.to(kv.dtype)
if _is_gfx95_supported:
block_I, threads, block_per_cu, cu = 64, 256, 2, 256
else:
block_I, threads, block_per_cu, cu = 64, 256, 1, 304
ni = topk // block_I
inner_iter = _pick_inner_iter(q.shape[0], ni, cu, block_per_cu)
kernel_partial = sparse_mla_fwd_decode_partial_fp8(
num_heads,
d_v,
tail_dim,
topk,
sm_scale=sm_scale,
block_I=block_I,
inner_iter=inner_iter,
threads=threads,
)
else:
if _is_gfx95_supported:
block_I, threads, block_per_cu, cu = 64, 256, 2, 256
else:
block_I, threads, block_per_cu, cu = 32, 128, 1, 304
ni = topk // block_I
inner_iter = _pick_inner_iter(q.shape[0], ni, cu, block_per_cu)
kernel_partial = sparse_mla_fwd_decode_partial(
num_heads,
d_v,
tail_dim,
topk,
sm_scale=sm_scale,
block_I=block_I,
inner_iter=inner_iter,
threads=threads,
)
partial_o_batched, partial_lse_batched = kernel_partial(
q.unsqueeze(0), kv.unsqueeze(0), indices.unsqueeze(0)
)
n_groups = ni // inner_iter
kernel_combine = sparse_mla_fwd_decode_combine(
num_heads,
d_v,
n_groups * block_I,
head_per_block=4,
block_I=block_I,
threads=threads,
)
out = kernel_combine(partial_o_batched, partial_lse_batched)
else:
kernel = sparse_attention_fwd_kernel_v2(
num_heads, d_v, tail_dim, topk, sm_scale=sm_scale
)
out = kernel(q.unsqueeze(0), kv.unsqueeze(0), indices.unsqueeze(0)) # type: ignore
return out
@functools.cache
def fp8_paged_mqa_logits_kernel(
head_dim: int = 128,
num_heads: int = 64,
block_size: int = 64,
clear_accum: bool = True,
split_kv: int = 1,
) -> Any:
N = T.symbolic("batch_size")
L = T.symbolic("max_table_length")
S = T.symbolic("max_seq_len")
C = T.symbolic("num_blocks")
B = block_size
D = head_dim
H = num_heads
SK = int(split_kv)
BLOCK_BYTES = B * (D + 4)
SCALE_OFFSET = B * D
assert D % 4 == 0
assert H % 4 == 0
assert D == 128
assert SK >= 1
@tilelang.jit(
pass_configs={
**pass_configs,
tilelang.PassConfigKey.TL_DISABLE_SAFE_MEMORY_ACCESS: True,
}
)
def fp8_paged_mqa_logits(
q: T.Tensor[(N, H, D), FP8],
kvcache_u8: T.Tensor[(C, BLOCK_BYTES), UINT8],
weight: T.Tensor[(N, H), FP32],
seq_lens: T.Tensor[(N,), INT32],
page_table: T.Tensor[(N, L), INT32],
o: T.Tensor[(N, S), FP32],
) -> None:
_ = N, L, S, C, D, H, B
with T.Kernel(N * SK) as bxs:
bx = bxs % N
pid_split = bxs // N
seq_len = seq_lens[bx]
np_total = T.ceildiv(seq_len, B)
stride = T.ceildiv(np_total, SK)
i_start = pid_split * stride
n_iters = T.max(0, T.min(stride, np_total - i_start))
q_smem = T.alloc_shared((H, D), FP8)
q_s_frag = T.alloc_fragment((H,), FP32)
T.copy(q[bx, 0, 0], q_smem)
T.copy(weight[bx, 0], q_s_frag)
for j in T.Pipelined(n_iters, num_stages=2):
i = i_start + j
page = page_table[bx, i]
k_smem_u8 = T.alloc_shared((B * D,), UINT8)
T.copy(kvcache_u8[page, 0:SCALE_OFFSET], k_smem_u8)
k_smem = T.view(k_smem_u8, (B, D), FP8)
k_s_smem_u8 = T.alloc_shared((B * 4,), UINT8)
T.copy(kvcache_u8[page, SCALE_OFFSET:BLOCK_BYTES], k_s_smem_u8)
k_s_smem = T.view(k_s_smem_u8, (B,), FP32)
k_s_frag = T.alloc_fragment((B,), FP32)
T.copy(k_s_smem, k_s_frag)
logits = T.alloc_fragment((B, H), FP32)
if not clear_accum:
T.fill(logits, 0.0)
T.gemm(
k_smem,
q_smem,
logits,
transpose_A=False,
transpose_B=True,
clear_accum=clear_accum,
)
# post processing
for h, j2 in T.Parallel(H, B):
logits[j2, h] = T.max(logits[j2, h], 0.0) * q_s_frag[h]
logits_sum = T.alloc_fragment((B,), FP32)
T.reduce_sum(logits, logits_sum, dim=1)
for j2 in T.Parallel(B):
logits_sum[j2] *= k_s_frag[j2]
T.copy(logits_sum, o[bx, i * B])
return fp8_paged_mqa_logits
def tilelang_fp8_paged_mqa_logits(
q_fp8: torch.Tensor,
kvcache_fp8: torch.Tensor,
weight: torch.Tensor,
seq_lens: torch.Tensor,
page_table: torch.Tensor,
deep_gemm_metadata: Any,
max_seq_len: int,
clean_logits: bool = True,
) -> torch.Tensor:
_ = deep_gemm_metadata
batch_size, _, num_heads, head_dim = q_fp8.shape
block_size = kvcache_fp8.shape[1]
assert head_dim == 128, "TODO"
assert block_size == 64, "TODO"
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
assert weight.shape == (batch_size, num_heads)
assert seq_lens.shape == (batch_size,)
assert page_table.shape[0] == batch_size
assert clean_logits == False
logits = page_table.new_empty((batch_size, max_seq_len), dtype=torch.float32)
NUM_CU = 256
split_kv = split_kv = max(1, min(max_seq_len // block_size, NUM_CU // batch_size))
kernel = fp8_paged_mqa_logits_kernel(
head_dim=head_dim,
num_heads=num_heads,
block_size=block_size,
clear_accum=clean_logits,
split_kv=split_kv,
)
q_fp8 = q_fp8.view(batch_size, num_heads, head_dim)
kvcache_u8 = kvcache_fp8.view(-1, block_size * (head_dim + 4))
kernel(q_fp8, kvcache_u8, weight, seq_lens, page_table, logits)
return logits
def _build_fp8_combined_view(k_cache: torch.Tensor) -> Tuple[torch.Tensor, int, int]:
"""
Reinterpret a MODEL1_FP8Sparse KV cache as a contiguous uint32 view.
Input: k_cache (num_blocks, block_size, 1, d_qk) fp8/uint8
— per-block storage also holds scales + padding past d_qk.
Output: (num_blocks, block_pad_u32) uint32 covering the full block
stride. Same storage ashe input, no copy.
"""
k_u8 = k_cache.view(torch.uint8) if k_cache.dtype != torch.uint8 else k_cache
num_blocks = k_u8.shape[0]
block_size = k_u8.shape[1]
block_pad_u32 = k_u8.stride(0) // 4
storage = k_u8.untyped_storage()
flat_u32 = torch.empty(0, dtype=torch.uint32, device=k_u8.device).set_(
storage, 0, (storage.nbytes() // 4,), (1,)
)
k_combined = torch.as_strided(
flat_u32,
size=(num_blocks, block_pad_u32),
stride=(block_pad_u32, 1),
storage_offset=k_u8.storage_offset() // 4,
)
return k_combined, num_blocks, block_size
_TOPK_LEN_SENTINEL_CACHE: dict = {}
_INT32_MAX = 2**30
def _topk_length_sentinel(device: torch.device, batch: int) -> torch.Tensor:
"""Cached `(batch,) int32 INT_MAX` tensor used when `topk_length` is None."""
cur = _TOPK_LEN_SENTINEL_CACHE.get(device)
if cur is None or cur.numel() < batch:
cur = torch.full(
(max(batch, 256),), _INT32_MAX, dtype=torch.int32, device=device
)
_TOPK_LEN_SENTINEL_CACHE[device] = cur
return cur[:batch]
@tilelang.jit(
out_idx=[-2, -1],
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_ENABLE_FAST_MATH: True,
},
)
def dpsk_v4_fp8_partial_kernel(
num_heads: int,
topk_1: int,
block_size_kv_1: int,
topk_2: int = 0,
block_size_kv_2: int = 0,
*,
dim: int = 448,
tail_dim: int = 64,
sm_scale: float = 0.0,
block_I: int = 64,
inner_iter_1: int = 1,
inner_iter_2: int = 0,
num_stages: int = 0,
threads: int = 512,
) -> Any:
"""
Read FP8 K cache directly, dequantise to BF16 in-kernel, do flash-attn
online softmax with split-K. Supports a second cache (`topk_2>0`) and
`attn_sink` is folded later by the combine kernel.
"""
log2e: float = 1.44269504
if sm_scale <= 0.0:
sm_scale = (1.0 / (dim + tail_dim)) ** 0.5 * log2e
else:
sm_scale = sm_scale * log2e
assert dim == 448 and tail_dim == 64
assert topk_1 % block_I == 0
assert (
topk_1 // block_I
) % inner_iter_1 == 0, (
f"NI_1={topk_1 // block_I} must be divisible by inner_iter_1={inner_iter_1}"
)
assert block_size_kv_1 > 0 and (block_size_kv_1 & (block_size_kv_1 - 1)) == 0
is_dual = topk_2 > 0
if is_dual:
assert inner_iter_2 > 0, "dual-cache call requires inner_iter_2 > 0"
assert topk_2 % block_I == 0
assert (
topk_2 // block_I
) % inner_iter_2 == 0, (
f"NI_2={topk_2 // block_I} must be divisible by inner_iter_2={inner_iter_2}"
)
assert block_size_kv_2 > 0 and (block_size_kv_2 & (block_size_kv_2 - 1)) == 0
PACKED_W = dim + 2 * tail_dim
NOPE_TILE = 64
NUM_TILES = dim // NOPE_TILE
SCALE_W = 8
PACKED_W4 = PACKED_W // 4
SCALE_W4 = SCALE_W // 4
kv_group = 1
batch = T.symbolic("batch")
seq_len = T.symbolic("seq_len")
num_blocks_kv_1 = T.symbolic("num_blocks_kv_1")
block_pad_u32_1 = T.symbolic("block_pad_u32_1")
if is_dual:
num_blocks_kv_2 = T.symbolic("num_blocks_kv_2")
block_pad_u32_2 = T.symbolic("block_pad_u32_2")
head_kv = num_heads // kv_group
D = dim
D_tail = tail_dim
BI = block_I
padded_H = max(tilelang.math.next_power_of_2(head_kv), 16)
if head_kv > 64:
assert head_kv % 64 == 0
REPLICATE_H = (head_kv + 63) // 64 if head_kv > 64 else 1
H_per_block = 64 if REPLICATE_H > 1 else padded_H
NI_1 = topk_1 // BI
n_groups_1 = NI_1 // inner_iter_1
NI_2 = (topk_2 // BI) if is_dual else 0
n_groups_2 = (NI_2 // inner_iter_2) if is_dual else 0
n_groups = n_groups_1 + n_groups_2
BS_KV_1 = block_size_kv_1
NOPE_ROPE_U32_PER_BLOCK_1 = BS_KV_1 * PACKED_W4
if is_dual:
BS_KV_2 = block_size_kv_2
NOPE_ROPE_U32_PER_BLOCK_2 = BS_KV_2 * PACKED_W4
q_shape = [batch, seq_len, num_heads, D + D_tail]
k1_shape = [num_blocks_kv_1, block_pad_u32_1]
indices1_shape = [batch, seq_len, topk_1]
topk_length_shape = [batch]
partial_o_shape = [batch, seq_len, n_groups, num_heads, D + D_tail]
partial_lse_shape = [batch, seq_len, n_groups, num_heads]
if is_dual:
k2_shape = [num_blocks_kv_2, block_pad_u32_2]
indices2_shape = [batch, seq_len, topk_2]
accum_dtype = "float"
indices_dtype = INT32
if is_dual:
@T.prim_func
def main(
Q: T.Tensor(q_shape, BF16), # type: ignore
K_combined_1: T.Tensor(k1_shape, "uint32"), # type: ignore
Indices_1: T.Tensor(indices1_shape, indices_dtype), # type: ignore
Topk_length_1: T.Tensor(topk_length_shape, indices_dtype), # type: ignore
K_combined_2: T.Tensor(k2_shape, "uint32"), # type: ignore
Indices_2: T.Tensor(indices2_shape, indices_dtype), # type: ignore
Topk_length_2: T.Tensor(topk_length_shape, indices_dtype), # type: ignore
Partial_O: T.Tensor(partial_o_shape, BF16), # type: ignore
Partial_LSE: T.Tensor(partial_lse_shape, accum_dtype), # type: ignore
) -> None:
"""
grid: (seq_len * REPLICATE_H * n_groups, batch, 1)
Each block processes `inner_iter_1` (or `inner_iter_2`) consecutive
KV tiles of one phase and writes one (partial_o, partial_lse) entry.
"""
with T.Kernel(
seq_len * REPLICATE_H * n_groups, batch, kv_group, threads=threads
) as (bx, by, bz):
Q_shared = T.alloc_fragment([H_per_block, D], BF16)
Q_tail_shared = T.alloc_fragment([H_per_block, D_tail], BF16)
K_packed_shared = T.alloc_shared([BI, PACKED_W4], "uint32")
K_scale_shared = T.alloc_shared([BI, SCALE_W4], "uint32")
KV_shared = T.alloc_shared([BI, D], BF16)
K_tail_shared = T.alloc_shared([BI, D_tail], BF16)
S_shared = T.alloc_shared([H_per_block, BI], BF16)
page_idx_shared = T.alloc_shared([BI], INT32)
mask = T.alloc_fragment([BI], "bool")
scale_byte_local = T.alloc_fragment([BI, NUM_TILES], "uint32")
acc_o = T.alloc_fragment([H_per_block, D], accum_dtype)
acc_o_tail = T.alloc_fragment([H_per_block, D_tail], accum_dtype)
acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype)
sumexp = T.alloc_fragment([H_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([H_per_block], accum_dtype)
alpha = T.alloc_fragment([H_per_block], accum_dtype)
m_i = T.alloc_fragment([H_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([H_per_block], accum_dtype)
T.fill(acc_o, 0)
T.fill(acc_o_tail, 0)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30))
b_i, g_i = by, bz
# bx encodes (s_i, h_replicate, group_i).
spans_per_seq = REPLICATE_H * n_groups
s_i = bx // spans_per_seq
rest = bx % spans_per_seq
group_i = rest // REPLICATE_H
h_rep = rest % REPLICATE_H
H0 = g_i * padded_H + (0 if REPLICATE_H == 1 else h_rep * 64)
H1 = H0 + H_per_block
tk_len_1 = Topk_length_1[b_i]
tk_len_2 = Topk_length_2[b_i]
actual_n_groups_1 = T.ceildiv(tk_len_1, BI * inner_iter_1)
actual_n_groups_2 = T.ceildiv(tk_len_2, BI * inner_iter_2)
if (group_i < n_groups_1) & (group_i < actual_n_groups_1):
# Phase 1 active: SWA cache work + Partial_O write.
T.copy(Q[b_i, s_i, H0:H1, :D], Q_shared)
T.copy(Q[b_i, s_i, H0:H1, D : D + D_tail], Q_tail_shared)
for k_i in T.Pipelined(inner_iter_1, num_stages=num_stages):
iter_i = group_i * inner_iter_1 + k_i
for bi_i in T.Parallel(BI):
pos = iter_i * BI + bi_i
idx = Indices_1[b_i, s_i, pos]
valid = (idx >= 0) & (pos < tk_len_1)
page_idx_shared[bi_i] = T.if_then_else(valid, idx, 0)
mask[bi_i] = valid
for bi_i, w_i in T.Parallel(BI, PACKED_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_1
t_in_block = page % BS_KV_1
K_packed_shared[bi_i, w_i] = K_combined_1[
block_id, t_in_block * PACKED_W4 + w_i
]
for bi_i, w_i in T.Parallel(BI, SCALE_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_1
t_in_block = page % BS_KV_1
K_scale_shared[bi_i, w_i] = K_combined_1[
block_id,
NOPE_ROPE_U32_PER_BLOCK_1 + t_in_block * SCALE_W4 + w_i,
]
for bi_i, ti in T.Parallel(BI, NUM_TILES):
word_idx = ti // 4
byte_in_word = ti % 4
word = K_scale_shared[bi_i, word_idx]
scale_byte_local[bi_i, ti] = (
word >> T.Cast("uint32", byte_in_word * 8)
) & T.uint32(0xFF)
for bi_i, d_i in T.Parallel(BI, D):
word_idx = d_i // 4
byte_in_word = d_i % 4
word = K_packed_shared[bi_i, word_idx]
b_u32 = (
word >> T.Cast("uint32", byte_in_word * 8)
) & T.uint32(0xFF)
sign_bf = (b_u32 & T.uint32(0x80)) * T.uint32(0x100)
exp_e4 = (b_u32 & T.uint32(0x78)) >> T.uint32(3)
mant_bf = (b_u32 & T.uint32(0x7)) * T.uint32(0x10)
scale_byte = scale_byte_local[bi_i, d_i // NOPE_TILE]
exp_combined = exp_e4 + scale_byte - T.uint32(7)
bf16_bits = (
sign_bf | (exp_combined << T.uint32(7)) | mant_bf
)
KV_shared[bi_i, d_i] = T.reinterpret(
BF16, T.Cast("uint16", bf16_bits)
)
for bi_i, j in T.Parallel(BI, D_tail):
abs_off = D + 2 * j
word_idx = abs_off // 4
word_off = abs_off % 4
word = K_packed_shared[bi_i, word_idx]
half_u32 = T.if_then_else(
word_off == 0,
word & T.uint32(0xFFFF),
(word >> T.uint32(16)) & T.uint32(0xFFFF),
)
K_tail_shared[bi_i, j] = T.reinterpret(
BF16, T.Cast("uint16", half_u32)
)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(
Q_shared,
KV_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.gemm(
Q_tail_shared,
K_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.max(m_i[h_i], m_i_prev[h_i])
for h_i in T.Parallel(H_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1)
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] *= alpha[h_i]
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] *= alpha[h_i]
T.copy(acc_s, S_shared)
T.gemm(
S_shared,
KV_shared,
acc_o,
policy=T.GemmWarpPolicy.FullRow,
)
T.gemm(
S_shared,
K_tail_shared,
acc_o_tail,
policy=T.GemmWarpPolicy.FullRow,
)
# ---- finalize phase 1 (active) ----
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] = acc_o[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] = acc_o_tail[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.if_then_else(
sumexp[h_i] == 0.0,
-(2.0**30),
T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale,
)
T.copy(acc_o, Partial_O[b_i, s_i, group_i, H0:H1, :D])
T.copy(
acc_o_tail,
Partial_O[b_i, s_i, group_i, H0:H1, D : D + D_tail],
)
T.copy(m_i, Partial_LSE[b_i, s_i, group_i, H0:H1])
elif group_i < n_groups_1:
# Phase 1 skipped: m_i is still the -2^30
T.copy(m_i, Partial_LSE[b_i, s_i, group_i, H0:H1])
elif (group_i - n_groups_1) < actual_n_groups_2:
# Phase 2 active: c128 cache work + Partial_O write.
T.copy(Q[b_i, s_i, H0:H1, :D], Q_shared)
T.copy(Q[b_i, s_i, H0:H1, D : D + D_tail], Q_tail_shared)
for k_i in T.Pipelined(inner_iter_2, num_stages=num_stages):
iter_i = (group_i - n_groups_1) * inner_iter_2 + k_i
for bi_i in T.Parallel(BI):
pos = iter_i * BI + bi_i
idx = Indices_2[b_i, s_i, pos]
valid = (idx >= 0) & (pos < tk_len_2)
page_idx_shared[bi_i] = T.if_then_else(valid, idx, 0)
mask[bi_i] = valid
for bi_i, w_i in T.Parallel(BI, PACKED_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_2
t_in_block = page % BS_KV_2
K_packed_shared[bi_i, w_i] = K_combined_2[
block_id, t_in_block * PACKED_W4 + w_i
]
for bi_i, w_i in T.Parallel(BI, SCALE_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_2
t_in_block = page % BS_KV_2
K_scale_shared[bi_i, w_i] = K_combined_2[
block_id,
NOPE_ROPE_U32_PER_BLOCK_2 + t_in_block * SCALE_W4 + w_i,
]
for bi_i, ti in T.Parallel(BI, NUM_TILES):
word_idx = ti // 4
byte_in_word = ti % 4
word = K_scale_shared[bi_i, word_idx]
scale_byte_local[bi_i, ti] = (
word >> T.Cast("uint32", byte_in_word * 8)
) & T.uint32(0xFF)
for bi_i, d_i in T.Parallel(BI, D):
word_idx = d_i // 4
byte_in_word = d_i % 4
word = K_packed_shared[bi_i, word_idx]
b_u32 = (
word >> T.Cast("uint32", byte_in_word * 8)
) & T.uint32(0xFF)
sign_bf = (b_u32 & T.uint32(0x80)) * T.uint32(0x100)
exp_e4 = (b_u32 & T.uint32(0x78)) >> T.uint32(3)
mant_bf = (b_u32 & T.uint32(0x7)) * T.uint32(0x10)
scale_byte = scale_byte_local[bi_i, d_i // NOPE_TILE]
exp_combined = exp_e4 + scale_byte - T.uint32(7)
bf16_bits = (
sign_bf | (exp_combined << T.uint32(7)) | mant_bf
)
KV_shared[bi_i, d_i] = T.reinterpret(
BF16, T.Cast("uint16", bf16_bits)
)
for bi_i, j in T.Parallel(BI, D_tail):
abs_off = D + 2 * j
word_idx = abs_off // 4
word_off = abs_off % 4
word = K_packed_shared[bi_i, word_idx]
half_u32 = T.if_then_else(
word_off == 0,
word & T.uint32(0xFFFF),
(word >> T.uint32(16)) & T.uint32(0xFFFF),
)
K_tail_shared[bi_i, j] = T.reinterpret(
BF16, T.Cast("uint16", half_u32)
)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(
Q_shared,
KV_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.gemm(
Q_tail_shared,
K_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.max(m_i[h_i], m_i_prev[h_i])
for h_i in T.Parallel(H_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1)
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] *= alpha[h_i]
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] *= alpha[h_i]
T.copy(acc_s, S_shared)
T.gemm(
S_shared,
KV_shared,
acc_o,
policy=T.GemmWarpPolicy.FullRow,
)
T.gemm(
S_shared,
K_tail_shared,
acc_o_tail,
policy=T.GemmWarpPolicy.FullRow,
)
# ---- finalize phase 2 (active) ----
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] = acc_o[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] = acc_o_tail[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.if_then_else(
sumexp[h_i] == 0.0,
-(2.0**30),
T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale,
)
T.copy(acc_o, Partial_O[b_i, s_i, group_i, H0:H1, :D])
T.copy(
acc_o_tail,
Partial_O[b_i, s_i, group_i, H0:H1, D : D + D_tail],
)
T.copy(m_i, Partial_LSE[b_i, s_i, group_i, H0:H1])
else:
# Phase 2 skipped: m_i is still the -2^30
T.copy(m_i, Partial_LSE[b_i, s_i, group_i, H0:H1])
return main
@T.prim_func
def main(
Q: T.Tensor(q_shape, BF16), # type: ignore
K_combined_1: T.Tensor(k1_shape, "uint32"), # type: ignore
Indices_1: T.Tensor(indices1_shape, indices_dtype), # type: ignore
Topk_length_1: T.Tensor(topk_length_shape, indices_dtype), # type: ignore
Partial_O: T.Tensor(partial_o_shape, BF16), # type: ignore
Partial_LSE: T.Tensor(partial_lse_shape, accum_dtype), # type: ignore
) -> None:
"""
grid: (seq_len * REPLICATE_H * n_groups, batch, 1)
Each block processes `inner_iter_1` consecutive KV tiles and writes
one (partial_o, partial_lse) entry.
"""
with T.Kernel(
seq_len * REPLICATE_H * n_groups, batch, kv_group, threads=threads
) as (bx, by, bz):
Q_shared = T.alloc_fragment([H_per_block, D], BF16)
Q_tail_shared = T.alloc_fragment([H_per_block, D_tail], BF16)
K_packed_shared = T.alloc_shared([BI, PACKED_W4], "uint32")
K_scale_shared = T.alloc_shared([BI, SCALE_W4], "uint32")
KV_shared = T.alloc_shared([BI, D], BF16)
K_tail_shared = T.alloc_shared([BI, D_tail], BF16)
S_shared = T.alloc_shared([H_per_block, BI], BF16)
page_idx_shared = T.alloc_shared([BI], INT32)
mask = T.alloc_fragment([BI], "bool")
scale_byte_local = T.alloc_fragment([BI, NUM_TILES], "uint32")
acc_o = T.alloc_fragment([H_per_block, D], accum_dtype)
acc_o_tail = T.alloc_fragment([H_per_block, D_tail], accum_dtype)
acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype)
sumexp = T.alloc_fragment([H_per_block], accum_dtype)
sumexp_i = T.alloc_fragment([H_per_block], accum_dtype)
alpha = T.alloc_fragment([H_per_block], accum_dtype)
m_i = T.alloc_fragment([H_per_block], accum_dtype)
m_i_prev = T.alloc_fragment([H_per_block], accum_dtype)
T.fill(acc_o, 0)
T.fill(acc_o_tail, 0)
T.fill(sumexp, 0)
T.fill(m_i, -(2**30))
b_i, g_i = by, bz
spans_per_seq = REPLICATE_H * n_groups
s_i = bx // spans_per_seq
rest = bx % spans_per_seq
group_i = rest // REPLICATE_H
h_rep = rest % REPLICATE_H
H0 = g_i * padded_H + (0 if REPLICATE_H == 1 else h_rep * 64)
H1 = H0 + H_per_block
T.copy(Q[b_i, s_i, H0:H1, :D], Q_shared)
T.copy(Q[b_i, s_i, H0:H1, D : D + D_tail], Q_tail_shared)
tk_len_1 = Topk_length_1[b_i]
for k_i in T.Pipelined(inner_iter_1, num_stages=num_stages):
iter_i = group_i * inner_iter_1 + k_i
for bi_i in T.Parallel(BI):
pos = iter_i * BI + bi_i
idx = Indices_1[b_i, s_i, pos]
valid = (idx >= 0) & (pos < tk_len_1)
page_idx_shared[bi_i] = T.if_then_else(valid, idx, 0)
mask[bi_i] = valid
for bi_i, w_i in T.Parallel(BI, PACKED_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_1
t_in_block = page % BS_KV_1
K_packed_shared[bi_i, w_i] = K_combined_1[
block_id, t_in_block * PACKED_W4 + w_i
]
for bi_i, w_i in T.Parallel(BI, SCALE_W4):
page = page_idx_shared[bi_i]
block_id = page // BS_KV_1
t_in_block = page % BS_KV_1
K_scale_shared[bi_i, w_i] = K_combined_1[
block_id,
NOPE_ROPE_U32_PER_BLOCK_1 + t_in_block * SCALE_W4 + w_i,
]
for bi_i, ti in T.Parallel(BI, NUM_TILES):
word_idx = ti // 4
byte_in_word = ti % 4
word = K_scale_shared[bi_i, word_idx]
scale_byte_local[bi_i, ti] = (
word >> T.Cast("uint32", byte_in_word * 8)
) & T.uint32(0xFF)
for bi_i, d_i in T.Parallel(BI, D):
word_idx = d_i // 4
byte_in_word = d_i % 4
word = K_packed_shared[bi_i, word_idx]
b_u32 = (word >> T.Cast("uint32", byte_in_word * 8)) & T.uint32(
0xFF
)
sign_bf = (b_u32 & T.uint32(0x80)) * T.uint32(0x100)
exp_e4 = (b_u32 & T.uint32(0x78)) >> T.uint32(3)
mant_bf = (b_u32 & T.uint32(0x7)) * T.uint32(0x10)
scale_byte = scale_byte_local[bi_i, d_i // NOPE_TILE]
exp_combined = exp_e4 + scale_byte - T.uint32(7)
bf16_bits = sign_bf | (exp_combined << T.uint32(7)) | mant_bf
KV_shared[bi_i, d_i] = T.reinterpret(
BF16, T.Cast("uint16", bf16_bits)
)
for bi_i, j in T.Parallel(BI, D_tail):
abs_off = D + 2 * j
word_idx = abs_off // 4
word_off = abs_off % 4
word = K_packed_shared[bi_i, word_idx]
half_u32 = T.if_then_else(
word_off == 0,
word & T.uint32(0xFFFF),
(word >> T.uint32(16)) & T.uint32(0xFFFF),
)
K_tail_shared[bi_i, j] = T.reinterpret(
BF16, T.Cast("uint16", half_u32)
)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.if_then_else(
mask[bi_i], 0, -T.infinity(acc_s.dtype)
)
T.gemm(
Q_shared,
KV_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.gemm(
Q_tail_shared,
K_tail_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow,
)
T.copy(m_i, m_i_prev)
T.reduce_max(acc_s, m_i, dim=1, clear=False)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.max(m_i[h_i], m_i_prev[h_i])
for h_i in T.Parallel(H_per_block):
alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale)
for h_i, bi_i in T.Parallel(H_per_block, BI):
acc_s[h_i, bi_i] = T.exp2(
acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale
)
T.reduce_sum(acc_s, sumexp_i, dim=1)
for h_i in T.Parallel(H_per_block):
sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i]
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] *= alpha[h_i]
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] *= alpha[h_i]
T.copy(acc_s, S_shared)
T.gemm(S_shared, KV_shared, acc_o, policy=T.GemmWarpPolicy.FullRow)
T.gemm(
S_shared, K_tail_shared, acc_o_tail, policy=T.GemmWarpPolicy.FullRow
)
for h_i, d_i in T.Parallel(H_per_block, D):
acc_o[h_i, d_i] = acc_o[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i, d_i in T.Parallel(H_per_block, D_tail):
acc_o_tail[h_i, d_i] = acc_o_tail[h_i, d_i] / T.if_then_else(
sumexp[h_i] == 0.0, 1.0, sumexp[h_i]
)
for h_i in T.Parallel(H_per_block):
m_i[h_i] = T.if_then_else(
sumexp[h_i] == 0.0,
-(2.0**30),
T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale,
)
T.copy(acc_o, Partial_O[b_i, s_i, group_i, H0:H1, :D])
T.copy(acc_o_tail, Partial_O[b_i, s_i, group_i, H0:H1, D : D + D_tail])
T.copy(m_i, Partial_LSE[b_i, s_i, group_i, H0:H1])
return main
@tilelang.jit(
out_idx=[-2, -1],
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_ENABLE_FAST_MATH: True,
},
)
def dpsk_v4_combine_kernel(
num_heads: int,
n_groups_1: int,
n_groups_2: int = 0,
*,
block_I: int = 64,
inner_iter_1: int = 1,
inner_iter_2: int = 1,
dim: int = 448,
tail_dim: int = 64,
head_per_block: int = 16,
threads: int = 256,
use_attn_sink: bool = False,
) -> Any:
"""
Combine `n_groups` flash-attention partials into the final output.
Inputs:
Partial_O : (batch, seq_len, n_groups, num_heads, dim+tail_dim) bf16
Partial_LSE : (batch, seq_len, n_groups, num_heads) fp32, log2 form
Topk_length_1: (batch,) int32, actual phase-1 length
Topk_length_2: (batch,) int32, actual phase-2 length (dual only)
Attn_sink : (num_heads,) fp32
Outputs:
Output : (batch, seq_len, num_heads, dim+tail_dim) bf16
LSE : (batch, seq_len, num_heads) fp32, natural log
Each grid block handles `head_per_block` heads of one (batch, seq) row.
"""
log2e: float = 1.44269504
ln2: float = 0.69314718
assert num_heads % head_per_block == 0
is_dual = n_groups_2 > 0
n_groups = n_groups_1 + n_groups_2
H_per_block = head_per_block
HEAD_BLOCKS = num_heads // H_per_block
DT = dim + tail_dim
batch = T.symbolic("batch")
seq_len = T.symbolic("seq_len")
accum_dtype = "float"
if is_dual:
@T.prim_func
def main(
Partial_O: T.Tensor(
[batch, seq_len, n_groups, num_heads, DT], BF16
), # type: ignore
Partial_LSE: T.Tensor(
[batch, seq_len, n_groups, num_heads], accum_dtype
), # type: ignore
Topk_length_1: T.Tensor([batch], INT32), # type: ignore
Topk_length_2: T.Tensor([batch], INT32), # type: ignore
Attn_sink: T.Tensor([num_heads], FP32), # type: ignore
Output: T.Tensor([batch, seq_len, num_heads, DT], BF16), # type: ignore
LSE: T.Tensor([batch, seq_len, num_heads], accum_dtype), # type: ignore
) -> None:
with T.Kernel(seq_len * HEAD_BLOCKS, batch, threads=threads) as (
bx,
by,
):
shared_lse = T.alloc_shared([n_groups, H_per_block], accum_dtype)
lse_max = T.alloc_fragment([H_per_block], accum_dtype)
lse_sum = T.alloc_fragment([H_per_block], accum_dtype)
scale = T.alloc_fragment([H_per_block, n_groups], accum_dtype)
acc_o = T.alloc_fragment([H_per_block, DT], accum_dtype)
attn_sink_frag = T.alloc_fragment([H_per_block], accum_dtype)
o_scale_frag = T.alloc_fragment([H_per_block], accum_dtype)
final_lse = T.alloc_fragment([H_per_block], accum_dtype)
b_i = by
s_i = bx // HEAD_BLOCKS
head_block = bx % HEAD_BLOCKS
H0 = head_block * H_per_block
H1 = H0 + H_per_block
# Clamp to the captured-shape upper bounds so callers passing
# the INT32_MAX sentinel (= "all valid") still iterate exactly
# n_groups groups, not 33M.
actual_n_groups_1 = T.min(
T.ceildiv(Topk_length_1[b_i], block_I * inner_iter_1),
n_groups_1,
)
actual_n_groups_2 = T.min(
T.ceildiv(Topk_length_2[b_i], block_I * inner_iter_2),
n_groups - n_groups_1,
)
actual_n_groups = actual_n_groups_1 + actual_n_groups_2
# Pass 1: load only active groups' LSE into compact slots.
for k_c in T.serial(actual_n_groups):
k = T.if_then_else(
k_c < actual_n_groups_1,
k_c,
n_groups_1 + (k_c - actual_n_groups_1),
)
T.copy(Partial_LSE[b_i, s_i, k, H0:H1], shared_lse[k_c, :])
T.fill(lse_max, -(2**30))
for k_c in T.serial(actual_n_groups):
for h_i in T.Parallel(H_per_block):
lse_max[h_i] = T.max(lse_max[h_i], shared_lse[k_c, h_i])
T.fill(lse_sum, 0)
for k_c in T.serial(actual_n_groups):
for h_i in T.Parallel(H_per_block):
lse_sum[h_i] = lse_sum[h_i] + T.exp2(
shared_lse[k_c, h_i] - lse_max[h_i]
)
for k_c in T.serial(actual_n_groups):
for h_i in T.Parallel(H_per_block):
scale[h_i, k_c] = T.exp2(
shared_lse[k_c, h_i] - lse_max[h_i] - T.log2(lse_sum[h_i])
)
T.fill(acc_o, 0)
for k_c in T.serial(actual_n_groups):
k = T.if_then_else(
k_c < actual_n_groups_1,
k_c,
n_groups_1 + (k_c - actual_n_groups_1),
)
for h_i, d_i in T.Parallel(H_per_block, DT):
acc_o[h_i, d_i] = acc_o[h_i, d_i] + scale[h_i, k_c] * Partial_O[
b_i, s_i, k, H0 + h_i, d_i
].astype(accum_dtype)
for h_i in T.Parallel(H_per_block):
empty = lse_max[h_i] <= -(2**29)
final_lse[h_i] = T.if_then_else(
empty,
T.infinity(accum_dtype),
(lse_max[h_i] + T.log2(lse_sum[h_i])) * ln2,
)
if use_attn_sink:
for h_i in T.Parallel(H_per_block):
attn_sink_frag[h_i] = Attn_sink[H0 + h_i]
for h_i in T.Parallel(H_per_block):
empty = lse_max[h_i] <= -(2**29)
o_scale_frag[h_i] = T.if_then_else(
empty,
0.0,
1.0
/ (
1.0
+ T.exp2((attn_sink_frag[h_i] - final_lse[h_i]) * log2e)
),
)
for h_i, d_i in T.Parallel(H_per_block, DT):
acc_o[h_i, d_i] = acc_o[h_i, d_i] * o_scale_frag[h_i]
T.copy(acc_o, Output[b_i, s_i, H0:H1, :])
T.copy(final_lse, LSE[b_i, s_i, H0:H1])
return main
@T.prim_func
def main(
Partial_O: T.Tensor(
[batch, seq_len, n_groups, num_heads, DT], BF16
), # type: ignore
Partial_LSE: T.Tensor(
[batch, seq_len, n_groups, num_heads], accum_dtype
), # type: ignore
Attn_sink: T.Tensor([num_heads], FP32), # type: ignore
Output: T.Tensor([batch, seq_len, num_heads, DT], BF16), # type: ignore
LSE: T.Tensor([batch, seq_len, num_heads], accum_dtype), # type: ignore
) -> None:
with T.Kernel(seq_len * HEAD_BLOCKS, batch, threads=threads) as (bx, by):
shared_lse = T.alloc_shared([n_groups, H_per_block], accum_dtype)
lse_max = T.alloc_fragment([H_per_block], accum_dtype)
lse_sum = T.alloc_fragment([H_per_block], accum_dtype)
scale = T.alloc_fragment([H_per_block, n_groups], accum_dtype)
acc_o = T.alloc_fragment([H_per_block, DT], accum_dtype)
attn_sink_frag = T.alloc_fragment([H_per_block], accum_dtype)
o_scale_frag = T.alloc_fragment([H_per_block], accum_dtype)
final_lse = T.alloc_fragment([H_per_block], accum_dtype)
b_i = by
s_i = bx // HEAD_BLOCKS
head_block = bx % HEAD_BLOCKS
H0 = head_block * H_per_block
H1 = H0 + H_per_block
for k in T.serial(n_groups):
T.copy(Partial_LSE[b_i, s_i, k, H0:H1], shared_lse[k, :])
T.fill(lse_max, -(2**30))
for k in T.serial(n_groups):
for h_i in T.Parallel(H_per_block):
lse_max[h_i] = T.max(lse_max[h_i], shared_lse[k, h_i])
T.fill(lse_sum, 0)
for k in T.serial(n_groups):
for h_i in T.Parallel(H_per_block):
lse_sum[h_i] = lse_sum[h_i] + T.exp2(
shared_lse[k, h_i] - lse_max[h_i]
)
for k in T.serial(n_groups):
for h_i in T.Parallel(H_per_block):
scale[h_i, k] = T.exp2(
shared_lse[k, h_i] - lse_max[h_i] - T.log2(lse_sum[h_i])
)
T.fill(acc_o, 0)
for k in T.serial(n_groups):
for h_i, d_i in T.Parallel(H_per_block, DT):
acc_o[h_i, d_i] = acc_o[h_i, d_i] + scale[h_i, k] * Partial_O[
b_i, s_i, k, H0 + h_i, d_i
].astype(accum_dtype)
for h_i in T.Parallel(H_per_block):
empty = lse_max[h_i] <= -(2**29)
final_lse[h_i] = T.if_then_else(
empty,
T.infinity(accum_dtype),
(lse_max[h_i] + T.log2(lse_sum[h_i])) * ln2,
)
if use_attn_sink:
for h_i in T.Parallel(H_per_block):
attn_sink_frag[h_i] = Attn_sink[H0 + h_i]
for h_i in T.Parallel(H_per_block):
empty = lse_max[h_i] <= -(2**29)
o_scale_frag[h_i] = T.if_then_else(
empty,
0.0,
1.0
/ (
1.0 + T.exp2((attn_sink_frag[h_i] - final_lse[h_i]) * log2e)
),
)
for h_i, d_i in T.Parallel(H_per_block, DT):
acc_o[h_i, d_i] = acc_o[h_i, d_i] * o_scale_frag[h_i]
T.copy(acc_o, Output[b_i, s_i, H0:H1, :])
T.copy(final_lse, LSE[b_i, s_i, H0:H1])
return main
"""
2-stage attention kernel (partial + combine) over an FP8 KV cache,
with optional second cache (`extra_k_cache`).
"""
def dpsk_v4_fp8_attention_fwd(
q: torch.Tensor,
k_cache: torch.Tensor,
block_table: Optional[torch.Tensor],
cache_seqlens: Optional[torch.Tensor],
head_dim_v: int,
tile_scheduler_metadata: Any,
num_splits: None = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
is_fp8_kvcache: bool = False,
indices: Optional[torch.Tensor] = None,
attn_sink: Optional[torch.Tensor] = None,
extra_k_cache: Optional[torch.Tensor] = None,
extra_indices_in_kvcache: Optional[torch.Tensor] = None,
topk_length: Optional[torch.Tensor] = None,
extra_topk_length: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Follows the original `flash_mla.flash_mla_with_kvcache` signature.
"""
if _is_gfx95_supported:
block_I, threads, num_stages, block_per_cu, cu = 64, 512, 0, 2, 256
else:
block_I, threads, num_stages, block_per_cu, cu = 32, 128, 1, 1, 304
batch, seq_len, num_heads, _ = q.shape
# Partial grid is (seq_len * REPLICATE_H * n_groups, batch, kv_group); the
# heuristic in _pick_inner_iter assumes `total_blocks = seq * ni / inner_iter`,
# so `seq` must include REPLICATE_H or n_groups doubles for medium batches.
replicate_h = max((num_heads + 63) // 64, 1)
seq = batch * seq_len * replicate_h
k1, _, bs_kv_1 = _build_fp8_combined_view(k_cache)
topk_1 = indices.shape[-1]
ni_1 = topk_1 // block_I
tk_len_1 = (
topk_length
if topk_length is not None
else _topk_length_sentinel(q.device, batch)
)
if attn_sink is None:
attn_sink = torch.full(
(num_heads,), float("-inf"), dtype=torch.float32, device=q.device
)
has_extra = extra_k_cache is not None
if not has_extra:
inner_iter_1 = _pick_inner_iter(seq, ni_1, cu, block_per_cu)
inner_iter_2 = 1
n_groups_1 = ni_1 // inner_iter_1
n_groups_2 = 0
partial = dpsk_v4_fp8_partial_kernel(
num_heads,
topk_1,
bs_kv_1,
sm_scale=softmax_scale,
block_I=block_I,
inner_iter_1=inner_iter_1,
num_stages=num_stages,
threads=threads,
)
partial_o, partial_lse = partial(q, k1, indices, tk_len_1)
else:
k2, _, bs_kv_2 = _build_fp8_combined_view(extra_k_cache)
topk_2 = extra_indices_in_kvcache.shape[-1]
ni_2 = topk_2 // block_I
# Each phase picks its own optimal split-K independently — kernel
# body uses two T.Pipelined loops with separate compile-time iter
# counts, no shared-divisor constraint.
inner_iter_1 = _pick_inner_iter(seq, ni_1, cu, block_per_cu)
inner_iter_2 = _pick_inner_iter(seq, ni_2, cu, block_per_cu)
n_groups_1 = ni_1 // inner_iter_1
n_groups_2 = ni_2 // inner_iter_2
tk_len_2 = (
extra_topk_length
if extra_topk_length is not None
else _topk_length_sentinel(q.device, batch)
)
partial = dpsk_v4_fp8_partial_kernel(
num_heads,
topk_1,
bs_kv_1,
topk_2,
bs_kv_2,
sm_scale=softmax_scale,
block_I=block_I,
inner_iter_1=inner_iter_1,
inner_iter_2=inner_iter_2,
num_stages=num_stages,
threads=threads,
)
partial_o, partial_lse = partial(
q,
k1,
indices,
tk_len_1,
k2,
extra_indices_in_kvcache,
tk_len_2,
)
combine = dpsk_v4_combine_kernel(
num_heads,
n_groups_1,
n_groups_2,
block_I=block_I,
inner_iter_1=inner_iter_1,
inner_iter_2=inner_iter_2,
head_per_block=4,
threads=256,
use_attn_sink=True,
)
if has_extra:
return combine(partial_o, partial_lse, tk_len_1, tk_len_2, attn_sink)
return combine(partial_o, partial_lse, attn_sink)