chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:13 +08:00
commit 1037506f2e
6050 changed files with 1731598 additions and 0 deletions
View File
@@ -0,0 +1,351 @@
import math
import torch
import triton
import triton.language as tl
def is_hip():
return triton.runtime.driver.active.get_current_target().backend == "hip"
def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, num_m_blocks, size_one_kv_head,
is_causal_or_local, max_splits):
"""
Determines the optimal number of splits for maximizing GPU occupancy while balancing memory efficiency.
Parameters:
- total_mblocks (int): Total number of m_blocks.
- num_SMs (int): Number of Streaming Multiprocessors (SMs) in the GPU.
- num_n_blocks (int): Number of n_blocks.
- num_m_blocks (int): Number of m_blocks.
- size_one_kv_head (int): Size of one KV head in bytes.
- is_causal_or_local (bool): Indicates whether the operation is causal or local.
- max_splits (int): Maximum number of allowed splits.
Returns:
- int: The optimal number of splits.
"""
# If we have enough m_blocks to almost fill the SMs, prefer 1 split unless memory constraints apply.
if total_mblocks >= 0.8 * num_SMs:
size_l2 = 50 * 1024 * 1024 # L2 cache size assumption (50MB)
# Only split if each KV head is too large for L2 and there are enough m_blocks
if size_one_kv_head > size_l2 and num_m_blocks >= num_SMs * 2 and not is_causal_or_local:
return min((size_one_kv_head + size_l2 - 1) // size_l2, max_splits)
else:
return 1
# If num_n_blocks is too small, we don't split
if num_n_blocks <= 4:
return 1
# Limit max_splits to a reasonable range
max_splits = min(max_splits, num_SMs, num_n_blocks)
max_efficiency = 0.0
efficiency = []
# Compute efficiency for different splits
for num_splits in range(1, max_splits + 1):
n_waves = (total_mblocks * num_splits) / num_SMs
eff = n_waves / math.ceil(n_waves)
# Track max efficiency
if eff > max_efficiency:
max_efficiency = eff
efficiency.append(eff)
# Find the smallest number of splits that achieves at least 85% of max efficiency
for num_splits in range(1, max_splits + 1):
if efficiency[num_splits - 1] >= 0.95 * max_efficiency:
return num_splits
return 1
@triton.autotune(
configs=[
triton.Config({}, num_warps=num_warps)
for num_warps in [1, 2, 4, 8, 16]
],
key=['gqa_group_size', 'BLOCK_H', 'BLOCK_N', 'BLOCK_D', 'BLOCK_V'],
)
@triton.jit
def _fwd_kernel_with_kv_cache(
Q, K, V, Out, L,
sm_scale,
cache_seqlens,
stride_qz, stride_qt, stride_qh, stride_qd,
stride_kz, stride_kt, stride_kh, stride_kd,
stride_vz, stride_vt, stride_vh, stride_vd,
stride_oz, stride_ot, stride_oh, stride_os, stride_od,
stride_lz, stride_lt, stride_lh, stride_ls,
num_splits: tl.constexpr,
seqlen_q: tl.constexpr,
num_m_blocks: tl.constexpr,
gqa_group_size: tl.constexpr,
BLOCK_H: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
BLOCK_D: tl.constexpr,
BLOCK_V: tl.constexpr,
):
off_sm = tl.program_id(0).to(tl.int64)
off_split, off_m = off_sm // num_m_blocks, off_sm % num_m_blocks
off_h_for_kv = tl.program_id(1).to(tl.int64)
off_z = tl.program_id(2).to(tl.int64)
off_h_q = off_h_for_kv * gqa_group_size
offs_h = tl.arange(0, BLOCK_H)
offs_n = tl.arange(0, BLOCK_N)
offs_d = tl.arange(0, BLOCK_D)
offs_v = tl.arange(0, BLOCK_V)
mask_h = offs_h < gqa_group_size
seqlen_k = tl.load(cache_seqlens + off_z)
Q += off_z * stride_qz + off_h_q * stride_qh
K += off_z * stride_kz + off_h_for_kv * stride_kh
V += off_z * stride_vz + off_h_for_kv * stride_vh
L += off_z * stride_lz + off_h_q * stride_lh + off_split * stride_ls
Out += off_z * stride_oz + off_h_q * stride_oh + off_split * stride_os
num_kv_blocks = tl.cdiv(seqlen_k, BLOCK_N)
blocks_per_split = num_kv_blocks // num_splits
remaining_blocks = num_kv_blocks % num_splits
loop_range = blocks_per_split + (1 if off_split < remaining_blocks else 0)
start = blocks_per_split * off_split + min(off_split, remaining_blocks)
offs_m = tl.arange(0, BLOCK_M) + off_m * BLOCK_M
mask_q = offs_m < seqlen_q
mask_qh = mask_q[:, None] & mask_h[None, :]
q_idx = offs_m[:, None] + tl.zeros([BLOCK_H], dtype=tl.int32) + seqlen_k - seqlen_q
q_idx = tl.reshape(q_idx, [BLOCK_M * BLOCK_H])
q = tl.load(Q + offs_m[:, None, None] * stride_qt + offs_h[None, :, None] * stride_qh + offs_d[None, None, :] * stride_qd, mask=mask_qh[:, :, None]) ## padding to min 16
q = tl.reshape(q, (BLOCK_M * BLOCK_H, BLOCK_D))
m_i = tl.full([BLOCK_M * BLOCK_H], float("-inf"), dtype=tl.float32)
l_i = tl.full([BLOCK_M * BLOCK_H], 1.0, dtype=tl.float32)
acc = tl.zeros([BLOCK_M * BLOCK_H, BLOCK_V], dtype=tl.float32)
k_ptrs = K + offs_n[None, :] * stride_kt + offs_d[:, None] * stride_kd
v_ptrs = V + offs_n[:, None] * stride_vt + offs_v[None, :] * stride_vd
for block_idx in range(start, start + loop_range):
start_n = block_idx * BLOCK_N
k = tl.load(k_ptrs + start_n * stride_kt, mask=offs_n[None, :] + start_n < seqlen_k, cache_modifier=".ca")
qk = tl.dot(q, k)
causal_mask = q_idx[:, None] >= start_n + offs_n[None, :]
qk = tl.where(causal_mask, qk, -1.0e6)
qk *= sm_scale
m_ij = tl.maximum(m_i, tl.max(qk, 1))
qk -= m_ij[:, None]
p = tl.exp(qk)
l_ij = tl.sum(p, 1)
alpha = tl.exp(m_i - m_ij)
l_i = l_i * alpha + l_ij
acc = acc * alpha[:, None]
v = tl.load(v_ptrs + start_n * stride_vt, mask=offs_n[:, None] + start_n < seqlen_k, cache_modifier=".ca")
p = p.to(v.type.element_ty)
acc += tl.dot(p, v)
m_i = m_ij
l_recip = 1 / l_i[:, None]
acc = acc * l_recip
m_i += tl.math.log(l_i)
l_ptrs = L + offs_m[:, None] * stride_lt + offs_h * stride_lh
m_i = tl.reshape(m_i, (BLOCK_M, BLOCK_H))
tl.store(l_ptrs, m_i, mask=mask_qh)
O_ptrs = Out + offs_m[:, None, None] * stride_ot + offs_h[None, :, None] * stride_oh + offs_v[None, None, :] * stride_od
acc = tl.reshape(acc, (BLOCK_M, BLOCK_H, BLOCK_V))
tl.store(O_ptrs, acc, mask=mask_qh[:, :, None])
@triton.autotune(
configs=[
triton.Config({}, num_warps=num_warps)
for num_warps in [1, 2, 4, 8, 16]
],
key=['BLOCK_V'],
)
@triton.jit
def combine(
out_partial, out, L,
stride_op_z, stride_op_t, stride_op_h, stride_op_s, stride_op_d,
stride_o_z, stride_o_t, stride_o_h, stride_o_d,
stride_l_z, stride_l_t, stride_l_h, stride_l_s,
num_splits: tl.constexpr,
num_splits_pow2: tl.constexpr,
BLOCK_V: tl.constexpr,
):
off_h = tl.program_id(0).to(tl.int64)
off_t = tl.program_id(1).to(tl.int64)
off_z = tl.program_id(2).to(tl.int64)
split = tl.arange(0, num_splits_pow2)
split_mask = split < num_splits
L += off_z * stride_l_z + off_t * stride_l_t + off_h * stride_l_h
out_partial += off_z * stride_op_z + off_t * stride_op_t + off_h * stride_op_h
out += off_z * stride_o_z + off_t * stride_o_t + off_h * stride_o_h
lse_local = tl.load(L + split * stride_l_s, mask=split_mask, other=float("-inf"))
lse_max_local = tl.max(lse_local, axis=0)
lse_logsum_local = tl.sum(tl.exp(lse_local - lse_max_local), axis=0)
lse_logsum_local = tl.log(lse_logsum_local) + lse_max_local
po_local = tl.load(out_partial + split[:, None] * stride_op_s + tl.arange(0, BLOCK_V) * stride_op_d, mask=split_mask[:, None])
scale_local = tl.exp(lse_local - lse_logsum_local)
accum_local = tl.sum(po_local * scale_local[:, None], axis=0)
tl.store(out + tl.arange(0, BLOCK_V) * stride_o_d, accum_local)
def flash_attention_with_kv_cache(
q, k, v,
cache_seqlens,
sm_scale=None,
):
# split q to blocks
batch, seqlen_q, n_heads, key_dim = q.shape
_, _, n_kv_heads, head_dim = v.shape
gqa_group_size = n_heads // n_kv_heads
block_h = triton.next_power_of_2(gqa_group_size)
block_m = max(256 // block_h, 1)
block_n = 32
# assert seqlen_q <= 32, "it seems the performance is not good when seqlen_q > 32"
assert k.size(0) == v.size(0)
assert q.size(3) == k.size(3)
assert k.size(1) == v.size(1)
assert key_dim in {64, 128, 256}
assert head_dim in {64, 128, 256}
props = torch.cuda.get_device_properties(torch.device("cuda:0"))
num_sm = props.multi_processor_count
num_m_blocks = triton.cdiv(seqlen_q, block_m)
num_n_blocks = triton.cdiv(cache_seqlens.max(), block_n)
size_one_kv_head = cache_seqlens.max() * block_n * (key_dim + head_dim) * 2
total_mblocks = batch * n_kv_heads
num_splits = num_splits_heuristic(
total_mblocks, num_sm, num_n_blocks, num_m_blocks,
size_one_kv_head, is_causal_or_local=True, max_splits=16
)
out_partial = torch.empty((batch, seqlen_q, n_heads, num_splits, head_dim), device=q.device, dtype=torch.float32)
out = torch.empty((batch, seqlen_q, n_heads, head_dim), device=q.device, dtype=q.dtype)
L = torch.empty((batch, seqlen_q, n_heads, num_splits), device=q.device, dtype=torch.float32)
if is_hip():
extra_kern_args = {"waves_per_eu": 1}
else:
extra_kern_args = {}
with torch.cuda.device(q.device.index):
grid = lambda META: (num_splits * num_m_blocks, n_kv_heads, batch)
_fwd_kernel_with_kv_cache[grid](
q, k, v, out_partial, L,
sm_scale if sm_scale is not None else key_dim ** -0.5,
cache_seqlens.contiguous(),
*q.stride(),
*k.stride(),
*v.stride(),
*out_partial.stride(),
*L.stride(),
num_splits=num_splits,
seqlen_q=seqlen_q,
num_m_blocks=num_m_blocks,
gqa_group_size=gqa_group_size,
BLOCK_H = block_h,
BLOCK_M = block_m,
BLOCK_N = block_n,
BLOCK_D = key_dim,
BLOCK_V = head_dim,
**extra_kern_args
)
grid = lambda META: (n_heads, seqlen_q, batch)
combine[grid](
out_partial, out, L,
*out_partial.stride(),
*out.stride(),
*L.stride(),
num_splits=num_splits,
num_splits_pow2=triton.next_power_of_2(num_splits),
BLOCK_V=head_dim,
**extra_kern_args
)
return out
def ref_program_fa(query, key, value, cache_seqlens):
# latency reference
# from flash_attn_interface import flash_attn_with_kvcache, flash_attn_func # fa3
from flash_attn import flash_attn_with_kvcache, flash_attn_func #fa2
output = flash_attn_with_kvcache(query, key, value, cache_seqlens=cache_seqlens, causal=True)
return output
def debug(name,expect, actual, atol=1e-3, rtol=1e-3):
all_close = torch.allclose(expect, actual, atol=atol, rtol=rtol)
print(name + " all_close={}".format(all_close))
if not all_close:
# print(expect[3, 28])
# print(actual[3, 28])
diff = (expect - actual).abs()
print("all_close={}, max={}, min={}, mean={}".format(all_close, diff.max().item(), diff.min().item(), diff.mean().item()))
max_indices = torch.nonzero(diff == diff.max().item())
first_index = tuple(max_indices[0].tolist())
print(f"Index: {first_index}, expect: {expect[first_index]}, actual: {actual[first_index]}")
if __name__ == "__main__":
import argparse
import time
parser = argparse.ArgumentParser()
parser.add_argument('--batch', type=int, default=8, help='batch size')
parser.add_argument('--seqlen_q', type=int, default=128, help='sequence length')
parser.add_argument('--heads', type=int, default=28, help='heads')
parser.add_argument('--heads_kv', type=int, default=4, help='heads_kv')
parser.add_argument('--max_cache_seqlen', type=int, default=65536, help='kvcache sequence length')
parser.add_argument('--dim', type=int, default=128, help='dim')
parser.add_argument('--dim_v', type=int, default=128, help='dim_v')
parser.add_argument('--load_from_file', type=str, default=None, help='load from file')
args = parser.parse_args()
batch, seqlen_q, heads, heads_kv, max_cache_seqlen, dim, dim_v = args.batch, args.seqlen_q, args.heads, args.heads_kv, args.max_cache_seqlen, args.dim, args.dim_v
dtype = torch.bfloat16
Q = torch.randn((batch, seqlen_q, heads, dim), dtype=dtype, device='cuda')
K = torch.randn((batch, max_cache_seqlen, heads_kv, dim), dtype=dtype, device='cuda')
V = torch.randn((batch, max_cache_seqlen, heads_kv, dim_v), dtype=dtype, device='cuda')
cache_seqlens = torch.randint(max_cache_seqlen - 32, max_cache_seqlen, (batch,), device='cuda', dtype=torch.int32)
print("cache_seqlens: ", cache_seqlens)
# parity reference
ref = ref_program_fa(Q, K, V, cache_seqlens)
# ref = ref_program_triton(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
# out = kernel(Q, K, V, block_indices, cache_seqlens, actual_num_blocks, glse, Output_partial)
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
out = flash_attention_with_kv_cache(Q, K, V, cache_seqlens)
debug("output", ref, out, atol=1e-3, rtol=1e-3)
## latency reference
for i in range(10):
ref = ref_program_fa(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
ref = ref_program_fa(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
print("dense time: ", (time.time() - start) / 100*1000)
for i in range(10):
out = flash_attention_with_kv_cache(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
out = flash_attention_with_kv_cache(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
print("sparse time: ", (time.time() - start) / 100*1000)
+315
View File
@@ -0,0 +1,315 @@
import math
import torch
import triton
import triton.language as tl
def is_hip():
return triton.runtime.driver.active.get_current_target().backend == "hip"
def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, num_m_blocks, size_one_kv_head,
is_causal_or_local, max_splits):
"""
Determines the optimal number of splits for maximizing GPU occupancy while balancing memory efficiency.
Parameters:
- total_mblocks (int): Total number of m_blocks.
- num_SMs (int): Number of Streaming Multiprocessors (SMs) in the GPU.
- num_n_blocks (int): Number of n_blocks.
- num_m_blocks (int): Number of m_blocks.
- size_one_kv_head (int): Size of one KV head in bytes.
- is_causal_or_local (bool): Indicates whether the operation is causal or local.
- max_splits (int): Maximum number of allowed splits.
Returns:
- int: The optimal number of splits.
"""
# If we have enough m_blocks to almost fill the SMs, prefer 1 split unless memory constraints apply.
if total_mblocks >= 0.8 * num_SMs:
size_l2 = 50 * 1024 * 1024 # L2 cache size assumption (50MB)
# Only split if each KV head is too large for L2 and there are enough m_blocks
if size_one_kv_head > size_l2 and num_m_blocks >= num_SMs * 2 and not is_causal_or_local:
return min((size_one_kv_head + size_l2 - 1) // size_l2, max_splits)
else:
return 1
# If num_n_blocks is too small, we don't split
if num_n_blocks <= 4:
return 1
# Limit max_splits to a reasonable range
max_splits = min(max_splits, num_SMs, num_n_blocks)
max_efficiency = 0.0
efficiency = []
# Compute efficiency for different splits
for num_splits in range(1, max_splits + 1):
n_waves = (total_mblocks * num_splits) / num_SMs
eff = n_waves / math.ceil(n_waves)
# Track max efficiency
if eff > max_efficiency:
max_efficiency = eff
efficiency.append(eff)
# Find the smallest number of splits that achieves at least 85% of max efficiency
for num_splits in range(1, max_splits + 1):
if efficiency[num_splits - 1] >= 0.95 * max_efficiency:
return num_splits
return 1
@triton.autotune(
configs=[
triton.Config({}, num_warps=num_warps)
for num_warps in [1, 2, 4, 8, 16]
],
key=['gqa_group_size', 'BLOCK_H', 'BLOCK_N', 'BLOCK_D', 'BLOCK_V'],
)
@triton.jit
def _fwd_kernel_decoding(
Q, K, V, Out, L,
sm_scale,
cache_seqlens,
block_indices_ptr,
stride_qz, stride_qh, stride_qd,
stride_kz, stride_kt, stride_kh, stride_kd,
stride_vz, stride_vt, stride_vh, stride_vd,
stride_oz, stride_oh, stride_os, stride_od,
stride_lz, stride_lh, stride_ls,
stride_bz, stride_bn, stride_bd,
max_selected_blocks: tl.constexpr,
num_splits: tl.constexpr,
gqa_group_size: tl.constexpr,
BLOCK_H: tl.constexpr,
BLOCK_N: tl.constexpr,
BLOCK_D: tl.constexpr,
BLOCK_V: tl.constexpr,
):
off_z = tl.program_id(0).to(tl.int64)
off_h_for_kv = tl.program_id(1).to(tl.int64)
off_split = tl.program_id(2).to(tl.int64)
off_h_q = off_h_for_kv * gqa_group_size
offs_m = tl.arange(0, BLOCK_H) ## head
offs_n = tl.arange(0, BLOCK_N)
offs_d = tl.arange(0, BLOCK_D)
offs_v = tl.arange(0, BLOCK_V)
seqlen_k = tl.load(cache_seqlens + off_z)
Q += off_z * stride_qz + off_h_q * stride_qh
K += off_z * stride_kz + off_h_for_kv * stride_kh
V += off_z * stride_vz + off_h_for_kv * stride_vh
L += off_z * stride_lz + off_h_q * stride_lh + off_split * stride_ls
Out += off_z * stride_oz + off_h_q * stride_oh + off_split * stride_os
block_indices_ptr += off_z * stride_bz + off_h_for_kv * stride_bn
q = tl.load(Q + offs_m[:, None] * stride_qh + offs_d[None, :] * stride_qd,
mask=(offs_m[:, None] < gqa_group_size)) ## padding to min 16
blocks_per_split = max_selected_blocks // num_splits
remaining_blocks = max_selected_blocks % num_splits
loop_range = blocks_per_split + (1 if off_split < remaining_blocks else 0)
start = blocks_per_split * off_split + min(off_split, remaining_blocks)
m_i = tl.full([BLOCK_H], float("-inf"), dtype=tl.float32)
l_i = tl.full([BLOCK_H], 1.0, dtype=tl.float32)
acc = tl.zeros([BLOCK_H, BLOCK_V], dtype=tl.float32)
k_ptrs = K + offs_n[None, :] * stride_kt + offs_d[:, None] * stride_kd
v_ptrs = V + offs_n[:, None] * stride_vt + offs_v[None, :] * stride_vd
for block_ptr_idx in range(start, start + loop_range):
block_idx = tl.load(block_indices_ptr + block_ptr_idx * stride_bd)
if block_idx >= 0:
start_n = block_idx * BLOCK_N
k = tl.load(k_ptrs + start_n * stride_kt, mask=offs_n[None, :] + start_n < seqlen_k)
qk = tl.dot(q, k)
qk = tl.where(offs_n[None, :] + start_n < seqlen_k, qk, -1e6)
qk *= sm_scale
m_ij = tl.maximum(m_i, tl.max(qk, 1))
qk -= m_ij[:, None]
p = tl.exp(qk)
l_ij = tl.sum(p, 1)
alpha = tl.exp(m_i - m_ij)
l_i = l_i * alpha + l_ij
acc = acc * alpha[:, None]
v = tl.load(v_ptrs + start_n * stride_vt, mask=offs_n[:, None] + start_n < seqlen_k)
p = p.to(v.type.element_ty)
acc += tl.dot(p, v)
m_i = m_ij
l_recip = 1 / l_i[:, None]
acc = acc * l_recip
m_i += tl.math.log(l_i)
l_ptrs = L + offs_m * stride_lh
tl.store(l_ptrs, m_i, mask=(offs_m < gqa_group_size))
O_ptrs = Out + offs_m[:, None] * stride_oh + offs_v[None, :] * stride_od
tl.store(O_ptrs, acc, mask=(offs_m[:, None] < gqa_group_size))
@triton.autotune(
configs=[
triton.Config({}, num_warps=num_warps)
for num_warps in [1, 2, 4, 8, 16]
],
key=['BLOCK_V'],
)
@triton.jit
def combine(
out_partial, out, L,
stride_op_z, stride_op_h, stride_op_s, stride_op_d,
stride_o_z, stride_o_h, stride_o_d,
stride_l_z, stride_l_h, stride_l_s,
num_splits: tl.constexpr,
num_splits_pow2: tl.constexpr,
BLOCK_V: tl.constexpr,
):
off_z = tl.program_id(0).to(tl.int64)
off_h = tl.program_id(1).to(tl.int64)
split = tl.arange(0, num_splits_pow2)
split_mask = split < num_splits
lse_local = tl.load(L + off_z * stride_l_z + off_h * stride_l_h + split * stride_l_s, mask=split_mask, other=float("-inf"))
lse_max_local = tl.max(lse_local, axis=0)
lse_logsum_local = tl.sum(tl.exp(lse_local - lse_max_local), axis=0)
lse_logsum_local = tl.log(lse_logsum_local) + lse_max_local
po_local = tl.load(out_partial + off_z * stride_op_z + off_h * stride_op_h + split[:, None] * stride_op_s + tl.arange(0, BLOCK_V) * stride_op_d, mask=split_mask[:, None])
scale_local = tl.exp(lse_local - lse_logsum_local)
accum_local = tl.sum(po_local * scale_local[:, None], axis=0)
tl.store(out + off_z * stride_o_z + off_h * stride_o_h + tl.arange(0, BLOCK_V) * stride_o_d, accum_local)
def flash_block_sparse_decoding(
q, k, v,
cache_seqlens,
block_indices,
sm_scale=None,
block_size=64,
num_splits=None
):
# split q to blocks
batch, n_heads, key_dim = q.shape
_, _, n_kv_heads, head_dim = v.shape
gqa_group_size = n_heads // n_kv_heads
max_selected_blocks = block_indices.shape[-1]
block_h = max(triton.next_power_of_2(gqa_group_size), 16)
assert k.size(0) == v.size(0)
assert q.size(2) == k.size(3)
assert k.size(1) == v.size(1)
assert key_dim in {64, 128, 256}
assert head_dim in {64, 128, 256}
assert triton.next_power_of_2(block_size) == block_size, "block size must be power of 2"
props = torch.cuda.get_device_properties(torch.device("cuda:0"))
num_sm = props.multi_processor_count
num_m_blocks = 1
num_n_blocks = max_selected_blocks
size_one_kv_head = max_selected_blocks * block_size * (key_dim + head_dim) * 2
total_mblocks = batch * n_kv_heads * num_m_blocks
if num_splits is None:
num_splits = num_splits_heuristic(
total_mblocks, num_sm, num_n_blocks, num_m_blocks,
size_one_kv_head, is_causal_or_local=True, max_splits=8)
out_partial = torch.empty((batch, n_heads, num_splits, head_dim), device=q.device, dtype=torch.float32)
out = torch.empty((batch, n_heads, head_dim), device=q.device, dtype=q.dtype)
L = torch.empty((batch, n_heads, num_splits), device=q.device, dtype=torch.float32)
if is_hip():
extra_kern_args = {"waves_per_eu": 1}
else:
extra_kern_args = {}
with torch.cuda.device(q.device.index):
grid = lambda META: (batch, n_kv_heads, num_splits)
_fwd_kernel_decoding[grid](
q, k, v, out_partial, L,
sm_scale if sm_scale is not None else key_dim ** -0.5,
cache_seqlens.contiguous(),
block_indices.contiguous(),
*q.stride(),
*k.stride(),
*v.stride(),
*out_partial.stride(),
*L.stride(),
*block_indices.stride(),
max_selected_blocks=max_selected_blocks,
num_splits=num_splits,
gqa_group_size=gqa_group_size,
BLOCK_H = block_h,
BLOCK_N = block_size,
BLOCK_D = key_dim,
BLOCK_V = head_dim,
**extra_kern_args
)
grid = lambda META: (batch, n_heads)
combine[grid](
out_partial, out, L,
*out_partial.stride(),
*out.stride(),
*L.stride(),
num_splits=num_splits,
num_splits_pow2=triton.next_power_of_2(num_splits),
BLOCK_V = head_dim,
**extra_kern_args
)
return out
def main():
from torch.nn import functional as F
import time
torch.cuda.manual_seed(0)
bsz, n_head, key_dim = 4, 2, 128
n_kv_seq = 8192
head_dim = 128
gqa_size = 6
block_size = 16
dtype = torch.float16
xq = torch.randn((bsz, n_head * gqa_size, key_dim), device='cuda', dtype=dtype)
xk = torch.randn((bsz, n_kv_seq, n_head, key_dim), device='cuda', dtype=dtype)
xv = torch.randn((bsz, n_kv_seq, n_head, head_dim), device='cuda', dtype=dtype)
cache_seqlens = torch.randint(100, n_kv_seq, (bsz,), device='cuda', dtype=torch.int32)
sparse_mask = torch.rand((bsz, n_head, (n_kv_seq + block_size - 1) // block_size), device='cuda') > 0.9
max_selected_blocks = sparse_mask.sum(dim=-1).max()
print("max_selected_blocks", max_selected_blocks)
sparse_indices = torch.full((bsz, n_head, max_selected_blocks), -1, device='cuda', dtype=torch.int32)
for i in range(bsz):
for j in range(n_head):
valid_blocks = torch.where(sparse_mask[i, j])[0]
sparse_indices[i, j, :len(valid_blocks)] = valid_blocks
torch.cuda.synchronize()
start_time = time.time()
for _ in range(100):
triton_output = flash_block_sparse_decoding(xq, xk, xv, cache_seqlens, sparse_indices, block_size=block_size)
torch.cuda.synchronize()
end_time = time.time()
print(f"Triton Time taken: {end_time - start_time} seconds")
naive_mask = torch.zeros((bsz, n_head, 1, n_kv_seq), device=xq.device, dtype=torch.bool)
for i in range(bsz):
block_mask = sparse_mask[i].repeat_interleave(block_size, dim=-1)
block_mask = torch.masked_fill(block_mask, torch.arange(n_kv_seq, device=xq.device) >= cache_seqlens[i], False)
naive_mask[i] = block_mask.unsqueeze(1)
torch.cuda.synchronize()
start_time = time.time()
for _ in range(100):
output = F.scaled_dot_product_attention(xq.unsqueeze(2), xk.transpose(1, 2), xv.transpose(1, 2), attn_mask=naive_mask.repeat_interleave(gqa_size, dim=1), enable_gqa=True)
output = output.view(bsz, n_head * gqa_size, head_dim)
torch.cuda.synchronize()
end_time = time.time()
print(f"Torch SDPA Time taken: {end_time - start_time} seconds")
print(output.shape, triton_output.shape)
print((output - triton_output).abs().max(), (output - triton_output).abs().mean())
if __name__ == "__main__":
main()
+354
View File
@@ -0,0 +1,354 @@
# Copyright (c) 2023, Tri Dao.
from typing import Optional, Union
import torch
import triton
import triton.language as tl
from typing import Optional, Union
from einops import rearrange, repeat
def rotate_half(x, interleaved=False):
if not interleaved:
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
else:
x1, x2 = x[..., ::2], x[..., 1::2]
return rearrange(torch.stack((-x2, x1), dim=-1), "... d two -> ... (d two)", two=2)
def apply_rotary_emb_torch(x, cos, sin, interleaved=False, inplace=False):
"""
x: (batch_size, seqlen, nheads, headdim)
cos, sin: (seqlen, rotary_dim / 2) or (batch_size, seqlen, rotary_dim / 2)
"""
ro_dim = cos.shape[-1] * 2
assert ro_dim <= x.shape[-1]
cos = repeat(cos, "... d -> ... 1 (2 d)" if not interleaved else "... d -> ... 1 (d 2)")
sin = repeat(sin, "... d -> ... 1 (2 d)" if not interleaved else "... d -> ... 1 (d 2)")
return torch.cat(
[x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim], interleaved) * sin, x[..., ro_dim:]],
dim=-1,
)
@triton.jit
def rotary_kernel(
OUT, # Pointers to matrices
X,
COS,
SIN,
CU_SEQLENS,
SEQLEN_OFFSETS, # this could be int or a pointer
# Matrix dimensions
seqlen,
rotary_dim,
seqlen_ro,
# strides
stride_out_batch,
stride_out_seqlen,
stride_out_nheads,
stride_out_headdim,
stride_x_batch,
stride_x_seqlen,
stride_x_nheads,
stride_x_headdim,
# Meta-parameters
BLOCK_K: tl.constexpr,
IS_SEQLEN_OFFSETS_TENSOR: tl.constexpr,
IS_VARLEN: tl.constexpr,
INTERLEAVED: tl.constexpr,
CONJUGATE: tl.constexpr,
BLOCK_M: tl.constexpr,
):
pid_m = tl.program_id(axis=0)
pid_batch = tl.program_id(axis=1)
pid_head = tl.program_id(axis=2)
rotary_dim_half = rotary_dim // 2
if not IS_VARLEN:
X = X + pid_batch * stride_x_batch + pid_head * stride_x_nheads
OUT = OUT + pid_batch * stride_out_batch + pid_head * stride_out_nheads
else:
start_idx = tl.load(CU_SEQLENS + pid_batch)
seqlen = tl.load(CU_SEQLENS + pid_batch + 1) - start_idx
X = X + start_idx * stride_x_seqlen + pid_head * stride_x_nheads
OUT = OUT + start_idx * stride_out_seqlen + pid_head * stride_out_nheads
if pid_m * BLOCK_M >= seqlen:
return
rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
if not IS_SEQLEN_OFFSETS_TENSOR:
rm_cs = rm + SEQLEN_OFFSETS
else:
rm_cs = rm + tl.load(SEQLEN_OFFSETS + pid_batch)
rk = tl.arange(0, BLOCK_K)
rk_half = tl.arange(0, BLOCK_K // 2)
if not INTERLEAVED:
# Load the 1st and 2nd halves of X, do calculation, then store to 1st and 2nd halves of OUT
X = X + (rm[:, None] * stride_x_seqlen + rk_half[None, :] * stride_x_headdim)
COS = COS + (rm_cs[:, None] * rotary_dim_half + rk_half[None, :])
SIN = SIN + (rm_cs[:, None] * rotary_dim_half + rk_half[None, :])
cos = tl.load(
COS, mask=(rm_cs[:, None] < seqlen_ro) & (rk_half[None, :] < rotary_dim_half), other=1.0
).to(tl.float32)
sin = tl.load(
SIN, mask=(rm_cs[:, None] < seqlen_ro) & (rk_half[None, :] < rotary_dim_half), other=0.0
).to(tl.float32)
x0 = tl.load(
X, mask=(rm[:, None] < seqlen) & (rk_half[None, :] < rotary_dim_half), other=0.0
).to(tl.float32)
x1 = tl.load(
X + rotary_dim_half * stride_x_headdim,
mask=(rm[:, None] < seqlen) & (rk_half[None, :] < rotary_dim_half),
other=0.0,
).to(tl.float32)
if CONJUGATE:
sin = -sin
o0 = x0 * cos - x1 * sin
o1 = x0 * sin + x1 * cos
# write back result
OUT = OUT + (rm[:, None] * stride_out_seqlen + rk_half[None, :] * stride_out_headdim)
tl.store(OUT, o0, mask=(rm[:, None] < seqlen) & (rk_half[None, :] < rotary_dim_half))
tl.store(
OUT + rotary_dim_half * stride_out_headdim,
o1,
mask=(rm[:, None] < seqlen) & (rk_half[None, :] < rotary_dim_half),
)
else:
# We don't want to load X[0, 2, 4, ...] and X[1, 3, 5, ...] separately since both are slow.
# Instead, we load x0 = X[0, 1, 2, 3, ...] and x1 = X[1, 0, 3, 2, ...].
# Loading x0 will be fast but x1 will be slow.
# Then we load cos = COS[0, 0, 1, 1, ...] and sin = SIN[0, 0, 1, 1, ...].
# Then we do the calculation and use tl.where to pick put the right outputs for the even
# and for the odd indices.
rk_swap = rk + ((rk + 1) % 2) * 2 - 1 # 1, 0, 3, 2, 5, 4, ...
rk_repeat = tl.arange(0, BLOCK_K) // 2
X0 = X + (rm[:, None] * stride_x_seqlen + rk[None, :] * stride_x_headdim)
X1 = X + (rm[:, None] * stride_x_seqlen + rk_swap[None, :] * stride_x_headdim)
COS = COS + (rm_cs[:, None] * rotary_dim_half + rk_repeat[None, :])
SIN = SIN + (rm_cs[:, None] * rotary_dim_half + rk_repeat[None, :])
cos = tl.load(
COS,
mask=(rm_cs[:, None] < seqlen_ro) & (rk_repeat[None, :] < rotary_dim_half),
other=1.0,
).to(tl.float32)
sin = tl.load(
SIN,
mask=(rm_cs[:, None] < seqlen_ro) & (rk_repeat[None, :] < rotary_dim_half),
other=0.0,
).to(tl.float32)
x0 = tl.load(X0, mask=(rm[:, None] < seqlen) & (rk[None, :] < rotary_dim), other=0.0).to(
tl.float32
)
x1 = tl.load(
X1, mask=(rm[:, None] < seqlen) & (rk_swap[None, :] < rotary_dim), other=0.0
).to(tl.float32)
if CONJUGATE:
sin = -sin
x0_cos = x0 * cos
x1_sin = x1 * sin
out = tl.where(rk[None, :] % 2 == 0, x0_cos - x1_sin, x0_cos + x1_sin)
OUT = OUT + (rm[:, None] * stride_out_seqlen + rk[None, :] * stride_out_headdim)
tl.store(OUT, out, mask=(rm[:, None] < seqlen) & (rk[None, :] < rotary_dim))
def apply_rotary(
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
seqlen_offsets: Union[int, torch.Tensor] = 0,
cu_seqlens: Optional[torch.Tensor] = None,
max_seqlen: Optional[int] = None,
interleaved=False,
inplace=False,
conjugate=False,
) -> torch.Tensor:
"""
Arguments:
x: (batch, seqlen, nheads, headdim) if cu_seqlens is None
else (total_seqlen, nheads, headdim).
cos: (seqlen_ro, rotary_dim / 2)
sin: (seqlen_ro, rotary_dim / 2)
seqlen_offsets: integer or integer tensor of size (batch,)
cu_seqlens: (batch + 1,) or None
max_seqlen: int
Returns:
y: (batch, seqlen, nheads, headdim)
"""
is_varlen = cu_seqlens is not None
if not is_varlen:
batch, seqlen, nheads, headdim = x.shape
else:
assert max_seqlen is not None, "If cu_seqlens is passed in, then max_seqlen must be passed"
total_seqlen, nheads, headdim = x.shape
batch_p_1 = cu_seqlens.shape[0]
batch = batch_p_1 - 1
seqlen = max_seqlen
seqlen_ro, rotary_dim = cos.shape
assert sin.shape == cos.shape
rotary_dim *= 2
assert rotary_dim <= headdim, f"rotary_dim must be <= headdim, but got {rotary_dim} and {headdim}"
assert headdim <= 256, "Only support headdim <= 256"
assert seqlen_ro >= seqlen, f"seqlen_ro must be >= seqlen, but got {seqlen_ro} and {seqlen}"
assert (
cos.dtype == sin.dtype
), f"cos and sin must have the same dtype, got {cos.dtype} and {sin.dtype}"
assert (
x.dtype == cos.dtype
), f"Input and cos/sin must have the same dtype, got {x.dtype} and {cos.dtype}"
cos, sin = cos.contiguous(), sin.contiguous()
if isinstance(seqlen_offsets, torch.Tensor):
assert seqlen_offsets.shape == (batch,)
assert seqlen_offsets.dtype in [torch.int32, torch.int64]
seqlen_offsets = seqlen_offsets.contiguous()
else:
assert seqlen_offsets + seqlen <= seqlen_ro
output = torch.empty_like(x) if not inplace else x
if rotary_dim < headdim and not inplace:
output[..., rotary_dim:].copy_(x[..., rotary_dim:])
BLOCK_K = (
32
if rotary_dim <= 32
else (64 if rotary_dim <= 64 else (128 if rotary_dim <= 128 else 256))
)
grid = lambda META: (triton.cdiv(seqlen, META["BLOCK_M"]), batch, nheads) # noqa
BLOCK_M = 4 if interleaved else (8 if rotary_dim <= 128 else 4)
# Need this, otherwise Triton tries to launch from cuda:0 and we get
# ValueError: Pointer argument (at 0) cannot be accessed from Triton (cpu tensor?)
with torch.cuda.device(x.device.index):
rotary_kernel[grid](
output, # data ptrs
x,
cos,
sin,
cu_seqlens,
seqlen_offsets,
seqlen, # shapes
rotary_dim,
seqlen_ro,
output.stride(0) if not is_varlen else 0, # batch_strides if not varlen else 0
output.stride(-3), # seqlen_stride or total_seqlen_stride
output.stride(-2), # nheads_stride
output.stride(-1), # headdim_stride
x.stride(0) if not is_varlen else 0, # batch_strides if not varlen else 0
x.stride(-3), # seqlen stride or total_seqlen_stride
x.stride(-2), # nheads stride
x.stride(-1), # headdim stride
BLOCK_K,
isinstance(seqlen_offsets, torch.Tensor),
is_varlen,
interleaved,
conjugate,
BLOCK_M,
)
return output
class ApplyRotaryEmb(torch.autograd.Function):
@staticmethod
def forward(
ctx,
x,
cos,
sin,
interleaved=False,
inplace=False,
seqlen_offsets: Union[int, torch.Tensor] = 0,
cu_seqlens: Optional[torch.Tensor] = None,
max_seqlen: Optional[int] = None,
):
out = apply_rotary(
x,
cos,
sin,
seqlen_offsets=seqlen_offsets,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
interleaved=interleaved,
inplace=inplace,
)
if isinstance(seqlen_offsets, int):
ctx.save_for_backward(cos, sin, cu_seqlens) # Can't save int with save_for_backward
ctx.seqlen_offsets = seqlen_offsets
else:
ctx.save_for_backward(cos, sin, cu_seqlens, seqlen_offsets)
ctx.seqlen_offsets = None
ctx.interleaved = interleaved
ctx.inplace = inplace
ctx.max_seqlen = max_seqlen
return out if not inplace else x
@staticmethod
def backward(ctx, do):
seqlen_offsets = ctx.seqlen_offsets
if seqlen_offsets is None:
cos, sin, cu_seqlens, seqlen_offsets = ctx.saved_tensors
else:
cos, sin, cu_seqlens = ctx.saved_tensors
# TD [2023-09-02]: For some reason Triton (2.0.0.post1) errors with
# "[CUDA]: invalid device context", and cloning makes it work. Idk why. Triton 2.1.0 works.
if not ctx.interleaved and not ctx.inplace:
do = do.clone()
dx = apply_rotary(
do,
cos,
sin,
seqlen_offsets=seqlen_offsets,
cu_seqlens=cu_seqlens,
max_seqlen=ctx.max_seqlen,
interleaved=ctx.interleaved,
inplace=ctx.inplace,
conjugate=True,
)
return dx, None, None, None, None, None, None, None
def apply_rotary_emb_triton(
x,
cos,
sin,
interleaved=False,
inplace=False,
seqlen_offsets: Union[int, torch.Tensor] = 0,
cu_seqlens: Optional[torch.Tensor] = None,
max_seqlen: Optional[int] = None,
):
"""
Arguments:
x: (batch_size, seqlen, nheads, headdim) if cu_seqlens is None
else (total_seqlen, nheads, headdim)
cos, sin: (seqlen_rotary, rotary_dim / 2)
interleaved: if True, rotate pairs of even and odd dimensions (GPT-J style) instead
of 1st half and 2nd half (GPT-NeoX style).
inplace: if True, apply rotary embedding in-place.
seqlen_offsets: (batch_size,) or int. Each sequence in x is shifted by this amount.
Most commonly used in inference when we have KV cache.
cu_seqlens: (batch + 1,) or None
max_seqlen: int
Return:
out: (batch_size, seqlen, nheads, headdim) if cu_seqlens is None
else (total_seqlen, nheads, headdim)
rotary_dim must be <= headdim
Apply rotary embedding to the first rotary_dim of x.
"""
return ApplyRotaryEmb.apply(
x, cos, sin, interleaved, inplace, seqlen_offsets, cu_seqlens, max_seqlen
)
if torch.cuda.is_available() and torch.version.hip:
# do something specific for HIP
apply_rotary_emb = apply_rotary_emb_torch
elif torch.cuda.is_available() and torch.version.cuda:
# do something specific for CUDA
apply_rotary_emb = apply_rotary_emb_triton
else:
# do something for CPU
apply_rotary_emb = apply_rotary_emb_torch
@@ -0,0 +1,379 @@
# Copyright (c) Tile-AI Corporation.
# Licensed under the MIT License.
import torch
import torch.nn.functional as F
import tilelang
from tilelang.autotuner import *
import tilelang.language as T
import argparse
import time
import math
def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, num_m_blocks, size_one_kv_head,
is_causal_or_local, max_splits):
"""
Determines the optimal number of splits for maximizing GPU occupancy while balancing memory efficiency.
Parameters:
- total_mblocks (int): Total number of m_blocks.
- num_SMs (int): Number of Streaming Multiprocessors (SMs) in the GPU.
- num_n_blocks (int): Number of n_blocks.
- num_m_blocks (int): Number of m_blocks.
- size_one_kv_head (int): Size of one KV head in bytes.
- is_causal_or_local (bool): Indicates whether the operation is causal or local.
- max_splits (int): Maximum number of allowed splits.
Returns:
- int: The optimal number of splits.
"""
# If we have enough m_blocks to almost fill the SMs, prefer 1 split unless memory constraints apply.
if total_mblocks >= 0.8 * num_SMs:
size_l2 = 50 * 1024 * 1024 # L2 cache size assumption (50MB)
# Only split if each KV head is too large for L2 and there are enough m_blocks
if size_one_kv_head > size_l2 and num_m_blocks >= num_SMs * 2 and not is_causal_or_local:
return min((size_one_kv_head + size_l2 - 1) // size_l2, max_splits)
else:
return 1
# If num_n_blocks is too small, we don't split
if num_n_blocks <= 4:
return 1
# Limit max_splits to a reasonable range
max_splits = min(max_splits, num_SMs, num_n_blocks)
max_efficiency = 0.0
efficiency = []
# Compute efficiency for different splits
for num_splits in range(1, max_splits + 1):
n_waves = (total_mblocks * num_splits) / num_SMs
eff = n_waves / math.ceil(n_waves)
# Track max efficiency
if eff > max_efficiency:
max_efficiency = eff
efficiency.append(eff)
# Find the smallest number of splits that achieves at least 85% of max efficiency
for num_splits in range(1, max_splits + 1):
if efficiency[num_splits - 1] >= 0.95 * max_efficiency:
return num_splits
return 1
def flashattn(heads, heads_kv, dim, dim_v):
scale = (1.0 / dim)**0.5 * 1.44269504 # log2(e)
dtype = "bfloat16"
accum_dtype = "float"
kv_group_num = heads // heads_kv
def kernel_func(batch, block_N, block_H, block_M, num_split, num_stages, threads, seqlen_q, max_cache_seqlen):
shape_q = [batch, seqlen_q, heads, dim]
shape_k = [batch, max_cache_seqlen, heads_kv, dim]
shape_v = [batch, max_cache_seqlen, heads_kv, dim_v]
shape_o = [batch, seqlen_q, heads, dim_v]
part_shape = [batch, seqlen_q, heads, num_split, dim_v]
num_block_M = (seqlen_q + block_M - 1) // block_M
@T.macro
def flash_attn_split(
Q: T.Tensor(shape_q, dtype),
K: T.Tensor(shape_k, dtype),
V: T.Tensor(shape_v, dtype),
cache_seqlens: T.Tensor([batch], "int32"),
glse: T.Tensor([batch, seqlen_q, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
):
with T.Kernel(
batch * num_block_M, heads_kv, num_split, threads=threads) as (bx, by, bz):
Q_shared = T.alloc_shared([block_M * block_H, dim], dtype)
K_shared = T.alloc_shared([block_N, dim], dtype)
V_shared = T.alloc_shared([block_N, dim_v], dtype)
acc_s = T.alloc_fragment([block_M * block_H, block_N], accum_dtype)
acc_s_cast = T.alloc_fragment([block_M * block_H, block_N], dtype)
acc_o = T.alloc_fragment([block_M * block_H, dim_v], accum_dtype)
scores_max = T.alloc_fragment([block_M * block_H], accum_dtype)
scores_max_prev = T.alloc_fragment([block_M * block_H], accum_dtype)
scores_scale = T.alloc_fragment([block_M * block_H], accum_dtype)
scores_sum = T.alloc_fragment([block_M * block_H], accum_dtype)
logsum = T.alloc_fragment([block_M * block_H], accum_dtype)
bid = T.floordiv(bx, num_block_M)
mid = T.floormod(bx, num_block_M) * block_M
hid = by
sid = bz
for i, d in T.Parallel(block_M * block_H, dim):
i_m = T.floordiv(i, block_H)
i_h = T.floormod(i, block_H)
if i_h < kv_group_num:
Q_shared[i, d] = Q[bid, mid + i_m, hid * kv_group_num + i_h, d]
# T.copy(Q[bid, mid:(mid + block_M), hid * kv_group_num : (hid + 1) * kv_group_num, :], Q_shared)
T.fill(acc_o, 0)
T.fill(logsum, 0)
T.fill(scores_max, -T.infinity(accum_dtype))
# num_blocks = actual_num_blocks[bid]
num_blocks = (cache_seqlens[bid] + block_N - 1) // block_N
blocks_per_split = T.floordiv(num_blocks, num_split)
remaining_blocks = T.floormod(num_blocks, num_split)
loop_range = (blocks_per_split + T.if_then_else(sid < remaining_blocks, 1, 0))
start = blocks_per_split * sid + T.min(sid, remaining_blocks)
for k in T.Pipelined(loop_range, num_stages=num_stages):
i_s = start + k
T.copy(
K[bid, i_s * block_N: (i_s + 1) * block_N, hid, :], K_shared)
T.clear(acc_s)
T.gemm(
Q_shared,
K_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow)
for i, j in T.Parallel(block_M * block_H, block_N):
i_m = T.floordiv(i, block_H)
i_h = T.floormod(i, block_H)
acc_s[i_m * block_H + i_h, j] = T.if_then_else(i_s * block_N + j > cache_seqlens[bid] - seqlen_q + (mid + i_m), -T.infinity(accum_dtype), acc_s[i_m * block_H + i_h, j])
T.copy(scores_max, scores_max_prev)
T.fill(scores_max, -T.infinity(accum_dtype))
T.reduce_max(acc_s, scores_max, dim=1, clear=False)
for i in T.Parallel(block_M * block_H):
scores_max[i] = T.if_then_else(scores_max[i] > scores_max_prev[i], scores_max[i], scores_max_prev[i])
scores_scale[i] = T.exp2(scores_max_prev[i] * scale - scores_max[i] * scale)
for i, j in T.Parallel(block_M * block_H, block_N):
acc_s[i, j] = T.exp2(acc_s[i, j] * scale - scores_max[i] * scale)
T.reduce_sum(acc_s, scores_sum, dim=1)
for i in T.Parallel(block_M * block_H):
logsum[i] = logsum[i] * scores_scale[i] + scores_sum[i]
T.copy(acc_s, acc_s_cast)
for i, j in T.Parallel(block_M * block_H, dim_v):
acc_o[i, j] *= scores_scale[i]
T.copy(
V[bid, i_s * block_N: (i_s + 1) * block_N, hid, :], V_shared)
T.gemm(acc_s_cast, V_shared, acc_o, policy=T.GemmWarpPolicy.FullRow)
for i, j in T.Parallel(block_M * block_H, dim_v):
acc_o[i, j] /= logsum[i]
for i in T.Parallel(block_M * block_H):
logsum[i] = T.log2(logsum[i]) + scores_max[i] * scale
for i in T.Parallel(block_M * block_H):
i_m = T.floordiv(i, block_H)
i_h = T.floormod(i, block_H)
if i_h < kv_group_num:
glse[bid, mid + i_m, hid * kv_group_num + i_h, sid] = logsum[i]
for i, v in T.Parallel(block_M * block_H, dim_v):
i_m = T.floordiv(i, block_H)
i_h = T.floormod(i, block_H)
if i_h < kv_group_num:
Output_partial[bid, mid + i_m, hid * kv_group_num + i_h, sid, v] = acc_o[i, v]
@T.macro
def combine(
glse: T.Tensor([batch, seqlen_q, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
Output: T.Tensor(shape_o, dtype),
):
with T.Kernel(heads, seqlen_q, batch, threads=128) as (bx, by, bz):
po_local = T.alloc_fragment([dim_v], accum_dtype)
o_accum_local = T.alloc_fragment([dim_v], accum_dtype)
lse_local_split = T.alloc_local([1], accum_dtype)
lse_logsum_local = T.alloc_local([1], accum_dtype)
lse_max_local = T.alloc_local([1], accum_dtype)
scale_local = T.alloc_local([1], accum_dtype)
max_split = T.alloc_local([1], "int32")
T.annotate_layout({
lse_logsum_local: T.Fragment(lse_logsum_local.shape, forward_thread_fn=lambda i: i),
})
T.clear(lse_logsum_local)
T.clear(o_accum_local)
lse_max_local[0] = -T.infinity(accum_dtype)
for k in T.serial(num_split):
lse_local_split[0] = glse[bz, by, bx, k]
if (lse_local_split[0] != 0):
max_split[0] = k
lse_max_local[0] = T.max(lse_max_local[0], glse[bz, by, bx, k])
for k in T.Pipelined(num_split, num_stages=1):
if k <= max_split[0]:
lse_local_split[0] = glse[bz, by, bx, k]
lse_logsum_local[0] += T.exp2(lse_local_split[0] - lse_max_local[0])
lse_logsum_local[0] = T.log2(lse_logsum_local[0]) + lse_max_local[0]
for k in T.serial(num_split):
if k <= max_split[0]:
for i in T.Parallel(dim_v):
po_local[i] = Output_partial[bz, by, bx, k, i]
lse_local_split[0] = glse[bz, by, bx, k]
scale_local[0] = T.exp2(lse_local_split[0] - lse_logsum_local[0])
for i in T.Parallel(dim_v):
o_accum_local[i] += po_local[i] * scale_local[0]
for i in T.Parallel(dim_v):
Output[bz, by, bx, i] = o_accum_local[i]
@T.prim_func
def main(
Q: T.Tensor(shape_q, dtype),
K: T.Tensor(shape_k, dtype),
V: T.Tensor(shape_v, dtype),
cache_seqlens: T.Tensor([batch], "int32"),
glse: T.Tensor([batch, seqlen_q, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
Output: T.Tensor(shape_o, dtype),
):
flash_attn_split(Q, K, V, cache_seqlens, glse, Output_partial)
combine(glse, Output_partial, Output)
return main
return kernel_func
class AttentionWithKVCache(torch.nn.Module):
def __init__(self, heads, heads_kv, dim, dim_v, seqlen_q):
super(AttentionWithKVCache, self).__init__()
self.heads = heads
self.heads_kv = heads_kv
self.dim = dim
self.dim_v = dim_v
self.block_N = 32
self.block_H = tilelang.next_power_of_2(heads // heads_kv)
self.block_M = seqlen_q
program = flashattn(heads, heads_kv, dim, dim_v)(
batch=T.symbolic("batch"),
block_N=self.block_N,
block_H=self.block_H,
block_M=self.block_M,
num_split=T.symbolic("num_split"),
num_stages=2,
threads=128,
seqlen_q=seqlen_q,
max_cache_seqlen=T.symbolic("max_cache_seqlen"),
)
self.kernel = tilelang.compile(
program,
out_idx=-1,
target='cuda',
execution_backend="cython"
)
props = torch.cuda.get_device_properties(torch.device("cuda:0"))
self.num_sm = props.multi_processor_count
def forward(self, query, key, value, cache_seqlens):
batch = query.shape[0]
seqlen_q = query.shape[1]
heads = self.heads
heads_kv = self.heads_kv
dim = self.dim
dim_v = self.dim_v
# Compute static scheduling parameters
num_m_blocks = (seqlen_q + self.block_M - 1) // self.block_M
num_n_blocks = (cache_seqlens.max().item() + self.block_N - 1) // self.block_N
size_one_kv_head = num_n_blocks * self.block_N * (dim + dim_v) * 2
total_mblocks = batch * heads_kv * num_m_blocks
# num_sm = 132
num_sm = self.num_sm
num_split = num_splits_heuristic(
total_mblocks, num_sm, num_n_blocks, num_m_blocks,
size_one_kv_head, is_causal_or_local=True, max_splits=16
)
glse = torch.empty((batch, seqlen_q, heads, num_split), dtype=torch.float32, device='cuda')
output_partial = torch.empty((batch, seqlen_q, heads, num_split, dim_v), dtype=torch.float32, device='cuda')
output = self.kernel(
query, key, value, cache_seqlens,
glse, output_partial
)
return output
def ref_program_fa(query, key, value, cache_seqlens):
# latency reference
# from flash_attn_interface import flash_attn_with_kvcache, flash_attn_func # fa3
from flash_attn import flash_attn_with_kvcache, flash_attn_func #fa2
output = flash_attn_with_kvcache(query, key, value, cache_seqlens=cache_seqlens)
return output
def debug(name,expect, actual, atol=1e-3, rtol=1e-3):
all_close = torch.allclose(expect, actual, atol=atol, rtol=rtol)
print(name + " all_close={}".format(all_close))
if not all_close:
# print(expect[3, 28])
# print(actual[3, 28])
diff = (expect - actual).abs()
print("all_close={}, max={}, min={}, mean={}".format(all_close, diff.max().item(), diff.min().item(), diff.mean().item()))
max_indices = torch.nonzero(diff == diff.max().item())
first_index = tuple(max_indices[0].tolist())
print(f"Index: {first_index}, expect: {expect[first_index]}, actual: {actual[first_index]}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--batch', type=int, default=4, help='batch size')
parser.add_argument('--seqlen_q', type=int, default=32, help='sequence length')
parser.add_argument('--heads', type=int, default=28, help='heads')
parser.add_argument('--heads_kv', type=int, default=4, help='heads_kv')
parser.add_argument('--max_cache_seqlen', type=int, default=65536, help='kvcache sequence length')
parser.add_argument('--dim', type=int, default=128, help='dim')
parser.add_argument('--dim_v', type=int, default=128, help='dim_v')
parser.add_argument('--block_size', type=int, default=32, help='block_size')
parser.add_argument('--load_from_file', type=str, default=None, help='load from file')
args = parser.parse_args()
batch, seqlen_q, heads, heads_kv, max_cache_seqlen, dim, dim_v = args.batch, args.seqlen_q, args.heads, args.heads_kv, args.max_cache_seqlen, args.dim, args.dim_v
block_size = args.block_size
dtype = torch.bfloat16
Q = torch.randn((batch, seqlen_q, heads, dim), dtype=dtype, device='cuda')
K = torch.randn((batch, max_cache_seqlen, heads_kv, dim), dtype=dtype, device='cuda')
V = torch.randn((batch, max_cache_seqlen, heads_kv, dim_v), dtype=dtype, device='cuda')
cache_seqlens = torch.randint(max_cache_seqlen - 32, max_cache_seqlen, (batch,), device='cuda', dtype=torch.int32)
print("cache_seqlens: ", cache_seqlens)
# parity reference
ref = ref_program_fa(Q, K, V, cache_seqlens)
# ref = ref_program_triton(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
# out = kernel(Q, K, V, block_indices, cache_seqlens, actual_num_blocks, glse, Output_partial)
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
sparse_kernel = AttentionWithKVCache(heads, heads_kv, dim, dim_v, seqlen_q)
out = sparse_kernel(Q, K, V, cache_seqlens)
debug("output", ref, out, atol=1e-3, rtol=1e-3)
## latency reference
for i in range(10):
ref = ref_program_fa(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
ref = ref_program_fa(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
print("dense time: ", (time.time() - start) / 100*1000)
for i in range(10):
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
out = sparse_kernel(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
out = sparse_kernel(Q, K, V, cache_seqlens)
torch.cuda.synchronize()
print("sparse time: ", (time.time() - start) / 100*1000)
+538
View File
@@ -0,0 +1,538 @@
# Copyright (c) Tile-AI Corporation.
# Licensed under the MIT License.
import torch
import torch.nn.functional as F
import tilelang
from tilelang.autotuner import *
import tilelang.language as T
from einops import rearrange, einsum
import argparse
import time
import math
def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, num_m_blocks, size_one_kv_head,
is_causal_or_local, max_splits):
"""
Determines the optimal number of splits for maximizing GPU occupancy while balancing memory efficiency.
Parameters:
- total_mblocks (int): Total number of m_blocks.
- num_SMs (int): Number of Streaming Multiprocessors (SMs) in the GPU.
- num_n_blocks (int): Number of n_blocks.
- num_m_blocks (int): Number of m_blocks.
- size_one_kv_head (int): Size of one KV head in bytes.
- is_causal_or_local (bool): Indicates whether the operation is causal or local.
- max_splits (int): Maximum number of allowed splits.
Returns:
- int: The optimal number of splits.
"""
# If we have enough m_blocks to almost fill the SMs, prefer 1 split unless memory constraints apply.
if total_mblocks >= 0.8 * num_SMs:
size_l2 = 50 * 1024 * 1024 # L2 cache size assumption (50MB)
# Only split if each KV head is too large for L2 and there are enough m_blocks
if size_one_kv_head > size_l2 and num_m_blocks >= num_SMs * 2 and not is_causal_or_local:
return min((size_one_kv_head + size_l2 - 1) // size_l2, max_splits)
else:
return 1
# If num_n_blocks is too small, we don't split
if num_n_blocks <= 4:
return 1
# Limit max_splits to a reasonable range
max_splits = min(max_splits, num_SMs, num_n_blocks)
max_efficiency = 0.0
efficiency = []
# Compute efficiency for different splits
for num_splits in range(1, max_splits + 1):
n_waves = (total_mblocks * num_splits) / num_SMs
eff = n_waves / math.ceil(n_waves)
# Track max efficiency
if eff > max_efficiency:
max_efficiency = eff
efficiency.append(eff)
# Find the smallest number of splits that achieves at least 85% of max efficiency
for num_splits in range(1, max_splits + 1):
if efficiency[num_splits - 1] >= 0.95 * max_efficiency:
return num_splits
return 1
def flashattn(heads, heads_kv, dim, dim_v):
scale = (1.0 / dim)**0.5 * 1.44269504 # log2(e)
dtype = "float16"
accum_dtype = "float"
kv_group_num = heads // heads_kv
def kernel_func(batch, block_N, block_H, num_split, num_stages, threads, max_cache_seqlen, max_selected_blocks):
shape_q = [batch, heads, dim]
shape_k = [batch, max_cache_seqlen, heads_kv, dim]
shape_v = [batch, max_cache_seqlen, heads_kv, dim_v]
shape_indices = [batch, heads_kv, max_selected_blocks]
shape_o = [batch, heads, dim_v]
part_shape = [batch, heads, num_split, dim_v]
valid_block_H = min(block_H, kv_group_num)
@T.macro
def flash_attn_split(
Q: T.Tensor(shape_q, dtype),
K: T.Tensor(shape_k, dtype),
V: T.Tensor(shape_v, dtype),
block_indices: T.Tensor(shape_indices, "int32"),
cache_seqlens: T.Tensor([batch], "int32"),
# actual_num_blocks: T.Tensor([batch], "int32"),
glse: T.Tensor([batch, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
):
with T.Kernel(
batch, heads // valid_block_H, num_split, threads=threads) as (bx, by, bz):
Q_shared = T.alloc_shared([block_H, dim], dtype)
K_shared = T.alloc_shared([block_N, dim], dtype)
V_shared = T.alloc_shared([block_N, dim_v], dtype)
# O_shared = T.alloc_shared([valid_block_H, dim_v], dtype)
acc_s = T.alloc_fragment([block_H, block_N], accum_dtype)
acc_s_cast = T.alloc_fragment([block_H, block_N], dtype)
acc_o = T.alloc_fragment([block_H, dim_v], accum_dtype)
scores_max = T.alloc_fragment([block_H], accum_dtype)
scores_max_prev = T.alloc_fragment([block_H], accum_dtype)
scores_scale = T.alloc_fragment([block_H], accum_dtype)
scores_sum = T.alloc_fragment([block_H], accum_dtype)
logsum = T.alloc_fragment([block_H], accum_dtype)
has_valid_block=T.alloc_var("bool")
# num_blocks = T.alloc_local([1], "int32")
bid = bx
hid = by
sid = bz
cur_kv_head = hid // (kv_group_num // valid_block_H)
T.copy(Q[bid, hid * valid_block_H:hid * valid_block_H + block_H, :], Q_shared)
T.fill(acc_o, 0)
T.fill(logsum, 0)
T.fill(scores_max, -T.infinity(accum_dtype))
# num_blocks = actual_num_blocks[bid]
num_blocks = max_selected_blocks
blocks_per_split = T.floordiv(num_blocks, num_split)
remaining_blocks = T.floormod(num_blocks, num_split)
loop_range = (blocks_per_split + T.if_then_else(sid < remaining_blocks, 1, 0))
start = blocks_per_split * sid + T.min(sid, remaining_blocks)
has_valid_block=False
# if (start < num_blocks):
for k in T.Pipelined(loop_range, num_stages=num_stages):
i_s = block_indices[bid, cur_kv_head, start + k]
if i_s >= 0:
has_valid_block = True
T.copy(
K[bid, i_s * block_N: (i_s + 1) * block_N,
cur_kv_head, :], K_shared)
T.clear(acc_s)
T.gemm(
Q_shared,
K_shared,
acc_s,
transpose_B=True,
policy=T.GemmWarpPolicy.FullRow)
# if k == 0: # assume block_indices is sorted in reverse order, otherwise, remove this if condition
for i, j in T.Parallel(block_H, block_N):
acc_s[i, j] = T.if_then_else(i_s * block_N + j >= cache_seqlens[bid], -T.infinity(accum_dtype), acc_s[i, j])
T.copy(scores_max, scores_max_prev)
T.fill(scores_max, -T.infinity(accum_dtype))
T.reduce_max(acc_s, scores_max, dim=1, clear=False)
for i in T.Parallel(block_H):
scores_max[i] = T.if_then_else(scores_max[i] > scores_max_prev[i], scores_max[i], scores_max_prev[i])
scores_scale[i] = T.exp2(scores_max_prev[i] * scale - scores_max[i] * scale)
for i, j in T.Parallel(block_H, block_N):
acc_s[i, j] = T.exp2(acc_s[i, j] * scale - scores_max[i] * scale)
T.reduce_sum(acc_s, scores_sum, dim=1)
for i in T.Parallel(block_H):
logsum[i] = logsum[i] * scores_scale[i] + scores_sum[i]
T.copy(acc_s, acc_s_cast)
for i, j in T.Parallel(block_H, dim_v):
acc_o[i, j] *= scores_scale[i]
T.copy(
V[bid, i_s * block_N: (i_s + 1) * block_N,
cur_kv_head, :], V_shared)
T.gemm(acc_s_cast, V_shared, acc_o, policy=T.GemmWarpPolicy.FullRow)
if has_valid_block:
for i, j in T.Parallel(block_H, dim_v):
acc_o[i, j] /= logsum[i]
for i in T.Parallel(block_H):
logsum[i] = T.log2(logsum[i]) + scores_max[i] * scale
for i in T.Parallel(block_H):
if i < valid_block_H:
glse[bid, hid * valid_block_H + i, sid] = logsum[i]
for i, j in T.Parallel(block_H, dim_v):
if i < valid_block_H:
Output_partial[bid, hid * valid_block_H + i, sid, j] = acc_o[i, j]
@T.macro
def combine(
glse: T.Tensor([batch, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
Output: T.Tensor(shape_o, dtype),
):
with T.Kernel(heads, batch, threads=128) as (by, bz):
po_local = T.alloc_fragment([dim_v], accum_dtype)
o_accum_local = T.alloc_fragment([dim_v], accum_dtype)
lse_local_split = T.alloc_local([1], accum_dtype)
lse_logsum_local = T.alloc_local([1], accum_dtype)
lse_max_local = T.alloc_local([1], accum_dtype)
scale_local = T.alloc_local([1], accum_dtype)
max_split = T.alloc_local([1], "int32")
T.annotate_layout({
lse_logsum_local: T.Fragment(lse_logsum_local.shape, forward_thread_fn=lambda i: i),
})
T.clear(lse_logsum_local)
T.clear(o_accum_local)
lse_max_local[0] = -T.infinity(accum_dtype)
for k in T.serial(num_split):
lse_local_split[0] = glse[bz, by, k]
if (lse_local_split[0] != 0):
max_split[0] = k
lse_max_local[0] = T.max(lse_max_local[0], glse[bz, by, k])
for k in T.Pipelined(num_split, num_stages=1):
if k <= max_split[0]:
lse_local_split[0] = glse[bz, by, k]
lse_logsum_local[0] += T.exp2(lse_local_split[0] - lse_max_local[0])
lse_logsum_local[0] = T.log2(lse_logsum_local[0]) + lse_max_local[0]
for k in T.serial(num_split):
if k <= max_split[0]:
for i in T.Parallel(dim_v):
po_local[i] = Output_partial[bz, by, k, i]
lse_local_split[0] = glse[bz, by, k]
scale_local[0] = T.exp2(lse_local_split[0] - lse_logsum_local[0])
for i in T.Parallel(dim_v):
o_accum_local[i] += po_local[i] * scale_local[0]
for i in T.Parallel(dim_v):
Output[bz, by, i] = o_accum_local[i]
@T.prim_func
def main(
Q: T.Tensor(shape_q, dtype),
K: T.Tensor(shape_k, dtype),
V: T.Tensor(shape_v, dtype),
block_indices: T.Tensor(shape_indices, "int32"),
cache_seqlens: T.Tensor([batch], "int32"),
# actual_num_blocks: T.Tensor([batch], "int32"),
glse: T.Tensor([batch, heads, num_split], accum_dtype),
Output_partial: T.Tensor(part_shape, accum_dtype),
Output: T.Tensor(shape_o, dtype),
):
# flash_attn_split(Q, K, V, block_indices, cache_seqlens, actual_num_blocks, glse, Output_partial)
flash_attn_split(Q, K, V, block_indices, cache_seqlens, glse, Output_partial)
combine(glse, Output_partial, Output)
return main
return kernel_func
class SparseFlashAttn(torch.nn.Module):
def __init__(self, heads, heads_kv, dim, dim_v, block_size):
super(SparseFlashAttn, self).__init__()
self.heads = heads
self.heads_kv = heads_kv
self.dim = dim
self.dim_v = dim_v
self.block_size = block_size
self.block_H = 64
program = flashattn(heads, heads_kv, dim, dim_v)(
batch=T.symbolic("batch"),
block_N=block_size,
block_H=self.block_H,
num_split=T.symbolic("num_split"),
num_stages=2,
threads=128,
max_cache_seqlen=T.symbolic("max_cache_seqlen"),
max_selected_blocks=T.symbolic("max_selected_blocks")
)
self.kernel = tilelang.compile(
program,
out_idx=-1,
target='cuda',
execution_backend="cython"
)
props = torch.cuda.get_device_properties(torch.device("cuda:0"))
self.num_sm = props.multi_processor_count
def forward(self, query, key, value, block_indices, cache_seqlens):
batch = query.shape[0]
heads = self.heads
heads_kv = self.heads_kv
dim = self.dim
dim_v = self.dim_v
block_size = self.block_size
max_selected_blocks = block_indices.shape[-1]
# Compute static scheduling parameters
num_m_blocks = 1 * (heads // heads_kv + self.block_H - 1) // self.block_H
num_n_blocks = max_selected_blocks
size_one_kv_head = max_selected_blocks * block_size * (dim + dim_v) * 2
total_mblocks = batch * heads_kv * num_m_blocks
# num_sm = 132
num_sm = self.num_sm
num_split = num_splits_heuristic(
total_mblocks, num_sm, num_n_blocks, num_m_blocks,
size_one_kv_head, is_causal_or_local=True, max_splits=16
)
# Function to compile
# def compute_actual_num_blocks(block_indices):
# actual_num_blocks = torch.sum(block_indices != -1, dim=-1).to(torch.int32)
# actual_num_blocks = actual_num_blocks[:, 0] # [batch]
# return actual_num_blocks
# compiled_fn = torch.compile(compute_actual_num_blocks)
# actual_num_blocks = compiled_fn(block_indices)
glse = torch.empty((batch, heads, num_split), dtype=torch.float32, device='cuda')
output_partial = torch.empty((batch, heads, num_split, dim_v), dtype=torch.float32, device='cuda')
# output = self.kernel(
# query, key, value, block_indices, cache_seqlens,
# actual_num_blocks, glse, output_partial
# )
output = self.kernel(
query, key, value, block_indices, cache_seqlens,
glse, output_partial
)
return output
def sparse_gqa_decode_varlen_indice(query, key, value, block_indices, cache_seqlens, max_cache_seqlen, block_size):
"""
Args:
query: [batch, heads, dim]
key: [batch, max_cache_seqlen, heads_kv, dim]
value: [batch, max_cache_seqlen, heads_kv, dim_v]
block_indices: [batch, heads_kv, max_selected_blocks], indices of selected blocks, -1 for padding
cache_seqlens: [batch], sequence lengths of the kvcache
max_cache_seqlen: maximum sequence length of kvcache
block_size: block size
Returns:
output: [batch, heads, dim_v]
"""
batch, heads, dim = query.shape
heads_kv = key.shape[2]
dim_v = value.shape[-1]
max_selected_blocks = block_indices.shape[-1]
block_H = 64
actual_num_blocks = torch.sum(block_indices != -1, dim=-1).to(torch.int32)
actual_num_blocks = actual_num_blocks[:,0] #[batch], number of valid blocks, assum all groups in the same batch have the same number of blocks
# get num_split
num_m_blocks = 1 * (heads // heads_kv + block_H - 1) // block_H
num_n_blocks = max_selected_blocks#(kv_seqlen + block_size - 1 ) // block_size
# num_n_blocks = torch.sum(actual_num_blocks, dim=-1).item() * heads_kv # total number of blocks
size_one_kv_head = max_selected_blocks * block_size * (dim + dim_v) * 2 #kv_seqlen * (dim + dim_v) * 2
total_mblocks = batch * heads_kv * num_m_blocks
num_sm = 132
num_split = num_splits_heuristic(total_mblocks, num_sm, num_n_blocks, num_m_blocks, size_one_kv_head, is_causal_or_local=True, max_splits=128)
program = flashattn(
batch, heads, heads_kv, dim, dim_v)(
block_N=block_size, block_H=block_H, num_split=T.symbolic("num_split"), num_stages=2, threads=128,
max_cache_seqlen=T.symbolic("max_cache_seqlen"), max_selected_blocks=T.symbolic("max_selected_blocks"))
glse = torch.empty((batch, heads, num_split), dtype=torch.float32, device='cuda')
Output_partial = torch.empty((batch, heads, num_split, dim_v), dtype=torch.float32, device='cuda')
kernel = tilelang.compile(program, out_idx=-1, target='cuda', execution_backend="cython", pass_configs={"tl.config_index_bitwidth": 64})
# print(kernel.get_kernel_source())
# output = kernel(query, key, value, block_indices, cache_seqlens, actual_num_blocks, glse, Output_partial)
output = kernel(query, key, value, block_indices, cache_seqlens, glse, Output_partial)
return output
def ref_program_torch(query, key, value, block_indices, cache_seqlens, max_cache_seqlen, num_blocks, block_size):
batch, heads, dim = query.shape
heads_kv = key.shape[2]
dim_v = value.shape[-1]
num_head_groups = query.shape[1] // key.shape[2]
scale = dim**0.5
key = rearrange(key, 'b n h d -> b h n d') # [batch_size, heads_kv, seqlen_kv, dim]
value = rearrange(value, 'b n h d -> b h n d') # [batch_size, heads_kv, seqlen_kv, dim]
query = rearrange(
query, 'b (h g) d -> b g h d',
g=num_head_groups) # [batch_size, num_head_groups, heads_kv, dim]
scores = einsum(
query, key,
'b g h d, b h s d -> b g h s') # [batch_size, num_head_groups, heads_kv, seqlen_kv]
sparse_mask = torch.zeros_like(scores)
# Assign mask values based on block_indices
for b in range(batch):
for h in range(heads_kv):
valid_indices = block_indices[b, h] # Extract indices for this batch and head
for idx in valid_indices:
if idx >= 0:
sparse_mask[b, :, h, idx * block_size: (idx + 1) * block_size] = 1
scores = scores.masked_fill(sparse_mask == 0, float('-inf'))
range_len = torch.arange(scores.shape[-1], device='cuda').unsqueeze(0)
cache_seqlens_expanded = cache_seqlens.unsqueeze(1)
pad_mask = range_len >= cache_seqlens_expanded
pad_mask = pad_mask[:, None, None, :]
scores = scores.masked_fill(pad_mask, float('-inf'))
attention = F.softmax(
scores / scale, dim=-1) # [batch_size, num_head_groups, heads_kv, seqlen_kv]
out = einsum(attention, value,
'b g h s, b h s d -> b g h d') # [batch_size, num_head_groups, heads_kv, dim]
out = rearrange(out, 'b g h d -> b (h g) d') # [batch_size, heads, dim]
return out
def ref_program_fa(query, key, value, block_indices, cache_seqlens, max_cache_seqlen, num_blocks, block_size):
# latency reference
# from flash_attn_interface import flash_attn_with_kvcache, flash_attn_func # fa3
from flash_attn import flash_attn_with_kvcache, flash_attn_func #fa2
query = query.unsqueeze(1)
output = flash_attn_with_kvcache(query, key, value, cache_seqlens=cache_seqlens)
output = output.squeeze(1)
return output
def debug(name,expect, actual, atol=1e-3, rtol=1e-3):
all_close = torch.allclose(expect, actual, atol=atol, rtol=rtol)
print(name + " all_close={}".format(all_close))
if not all_close:
# print(expect[3, 28])
# print(actual[3, 28])
diff = (expect - actual).abs()
print("all_close={}, max={}, min={}, mean={}".format(all_close, diff.max().item(), diff.min().item(), diff.mean().item()))
max_indices = torch.nonzero(diff == diff.max().item())
first_index = tuple(max_indices[0].tolist())
print(f"Index: {first_index}, expect: {expect[first_index]}, actual: {actual[first_index]}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--batch', type=int, default=4, help='batch size')
parser.add_argument('--heads', type=int, default=28, help='heads')
parser.add_argument('--heads_kv', type=int, default=4, help='heads_kv')
parser.add_argument('--max_cache_seqlen', type=int, default=1048576, help='kvcache sequence length')
parser.add_argument('--dim', type=int, default=128, help='dim')
parser.add_argument('--dim_v', type=int, default=128, help='dim_v')
parser.add_argument('--sparse_ratio', type=float, default=0.9, help='sparse ratio')
parser.add_argument('--block_size', type=int, default=32, help='block_size')
parser.add_argument('--load_from_file', type=str, default=None, help='load from file')
args = parser.parse_args()
block_H = 64
if args.load_from_file is None:
batch, heads, heads_kv, max_cache_seqlen, dim, dim_v = args.batch, args.heads, args.heads_kv, args.max_cache_seqlen, args.dim, args.dim_v
sparse_ratio = args.sparse_ratio
block_size = args.block_size
max_selected_blocks = int(math.ceil(max_cache_seqlen * (1-sparse_ratio)/ block_size))
print("max_selected_blocks: ", max_selected_blocks)
dtype = torch.float16
Q = torch.randn((batch, heads, dim), dtype=dtype, device='cuda')
K = torch.randn((batch, max_cache_seqlen, heads_kv, dim), dtype=dtype, device='cuda')
V = torch.randn((batch, max_cache_seqlen, heads_kv, dim_v), dtype=dtype, device='cuda')
cache_seqlens = torch.full((batch,), max_cache_seqlen, dtype=torch.int32, device='cuda')
# cache_seqlens = torch.randint(1, max_cache_seqlen, (batch,), dtype=torch.int32, device='cuda')
# cache_seqlens = torch.full((batch,), max_cache_seqlen, dtype=torch.int32, device='cuda')
# Ensure at least one element equals cache_seqlen
random_index = torch.randint(0, batch, (1,), device='cuda').item() # Select a random index
cache_seqlens[random_index] = max_cache_seqlen # Assign cache_seqlen to ensure at least one occurrence
else:
save_dict = torch.load(args.load_from_file)
Q = save_dict["xq"]
K = save_dict["key"]
V = save_dict["value"]
block_indices = save_dict["sparse_indices"]
cache_seqlens = save_dict["seqlens_k"]
batch, heads, dim = Q.shape
heads_kv = K.shape[2]
max_cache_seqlen = K.shape[1]
dim_v = V.shape[-1]
block_size = max_cache_seqlen // block_indices.shape[-1]
max_selected_blocks = block_indices.shape[-1]
print(f"Load debug data from file with batch={batch}, heads={heads}, heads_kv={heads_kv}, max_cache_seqlen={max_cache_seqlen}, dim={dim}, dim_v={dim_v}, block_size={block_size}")
print("cache_seqlens: ", cache_seqlens)
max_valid_num_blocks = torch.ceil(cache_seqlens / block_size).int()
print("max_valid_num_blocks: ", max_valid_num_blocks)
# Initialize block_indices with -1 (for padding blocks)
block_indices = torch.full((batch, heads_kv, max_selected_blocks), -1, dtype=torch.int32, device='cuda')
# Assign valid indices while ensuring no duplicates within each batch-group
for b in range(batch):
max_valid_block = max_valid_num_blocks[b].item() # Max valid blocks for this batch
if max_valid_block > 0: # Ensure there's at least one valid block
for h in range(heads_kv):
valid_indices = torch.randperm(max_valid_block, device='cuda', dtype=torch.int32)[:max_selected_blocks]
block_indices[b, h, :len(valid_indices)] = valid_indices
# Sort indices within each batch-group for consistency
block_indices, _ = block_indices.sort(dim=-1, descending=True)
# print("block_indices: ", block_indices)
actual_num_blocks = torch.sum(block_indices != -1, dim=-1).to(torch.int32)[:,0]
print("actual_num_blocks: ", actual_num_blocks)
# print(block_indices.shape, actual_num_blocks.shape)
max_num_blocks = torch.max(max_valid_num_blocks).item()
print("max_num_blocks: ", max_num_blocks)
# parity reference
ref = ref_program_torch(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
# ref = ref_program_triton(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
# out = kernel(Q, K, V, block_indices, cache_seqlens, actual_num_blocks, glse, Output_partial)
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
sparse_kernel = SparseFlashAttn(heads, heads_kv, dim, dim_v, block_size)
out = sparse_kernel(Q, K, V, block_indices, cache_seqlens)
debug("output", ref, out, atol=1e-3, rtol=1e-3)
## latency reference
for i in range(10):
ref = ref_program_fa(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
ref = ref_program_fa(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, max_num_blocks, block_size)
torch.cuda.synchronize()
print("dense time: ", (time.time() - start) / 100*1000)
for i in range(10):
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
out = sparse_kernel(Q, K, V, block_indices, cache_seqlens)
torch.cuda.synchronize()
start = time.time()
for i in range(100):
# out = sparse_gqa_decode_varlen_indice(Q, K, V, block_indices, cache_seqlens, max_cache_seqlen, block_size)
out = sparse_kernel(Q, K, V, block_indices, cache_seqlens)
torch.cuda.synchronize()
print("sparse time: ", (time.time() - start) / 100*1000)