Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

567 lines
18 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
from tokenspeed_kernel.platform import CapabilityRequirement
from tokenspeed_kernel.registry import Priority, register_kernel
from tokenspeed_kernel.signature import dense_tensor_format, format_signature
@triton.jit
def _dsa_packed_kv_kernel(
q,
kv_fp8,
kv_scale,
kv_rope,
topk_indices,
topk_lens,
out,
num_heads: tl.constexpr,
head_dim: tl.constexpr,
kv_lora_rank: tl.constexpr,
qk_rope_head_dim: tl.constexpr,
row_bytes: tl.constexpr,
topk: tl.constexpr,
softmax_scale: tl.constexpr,
BLOCK_TOPK: tl.constexpr,
BLOCK_K: tl.constexpr,
BLOCK_V: tl.constexpr,
):
token = tl.program_id(0)
head = tl.program_id(1)
v_block = tl.program_id(2)
topk_offsets = tl.arange(0, BLOCK_TOPK)
k_offsets = tl.arange(0, BLOCK_K)
rope_offsets = tl.arange(0, 64)
v_offsets = v_block * BLOCK_V + tl.arange(0, BLOCK_V)
q_base = (token * num_heads + head) * head_dim
q_nope_base = q_base
q_rope_base = q_base + kv_lora_rank
q_rope = tl.load(
q + q_rope_base + rope_offsets,
mask=rope_offsets < qk_rope_head_dim,
other=0.0,
).to(tl.float32)
valid_len = tl.load(topk_lens + token).to(tl.int32)
max_score = tl.full((), -float("inf"), tl.float32)
for start in range(0, topk, BLOCK_TOPK):
cols = start + topk_offsets
valid = cols < valid_len
slots = tl.load(
topk_indices + token * topk + cols,
mask=valid,
other=0,
).to(tl.int64)
valid = valid & (slots >= 0)
score = tl.zeros((BLOCK_TOPK,), tl.float32)
for k_start in range(0, kv_lora_rank, BLOCK_K):
ks = k_start + k_offsets
q_vals = tl.load(q + q_nope_base + ks).to(tl.float32)
k_vals = tl.load(
kv_fp8 + slots[:, None] * row_bytes + ks[None, :],
mask=valid[:, None],
other=0.0,
).to(tl.float32)
k_scale = tl.load(
kv_scale
+ (slots * row_bytes + kv_lora_rank + (k_start // 128) * 4) // 4,
mask=valid,
other=0.0,
).to(tl.float32)
score += tl.sum(k_vals * k_scale[:, None] * q_vals[None, :], axis=1)
k_rope = tl.load(
kv_rope
+ (slots[:, None] * row_bytes + kv_lora_rank + (kv_lora_rank // 128) * 4)
// 2
+ rope_offsets[None, :],
mask=valid[:, None] & (rope_offsets[None, :] < qk_rope_head_dim),
other=0.0,
).to(tl.float32)
score += tl.sum(k_rope * q_rope[None, :], axis=1)
score *= softmax_scale
score = tl.where(valid, score, -float("inf"))
max_score = tl.maximum(max_score, tl.max(score, axis=0))
denom = tl.full((), 0.0, tl.float32)
acc = tl.zeros((BLOCK_V,), tl.float32)
v_mask = v_offsets < kv_lora_rank
for start in range(0, topk, BLOCK_TOPK):
cols = start + topk_offsets
valid = cols < valid_len
slots = tl.load(
topk_indices + token * topk + cols,
mask=valid,
other=0,
).to(tl.int64)
valid = valid & (slots >= 0)
score = tl.zeros((BLOCK_TOPK,), tl.float32)
for k_start in range(0, kv_lora_rank, BLOCK_K):
ks = k_start + k_offsets
q_vals = tl.load(q + q_nope_base + ks).to(tl.float32)
k_vals = tl.load(
kv_fp8 + slots[:, None] * row_bytes + ks[None, :],
mask=valid[:, None],
other=0.0,
).to(tl.float32)
k_scale = tl.load(
kv_scale
+ (slots * row_bytes + kv_lora_rank + (k_start // 128) * 4) // 4,
mask=valid,
other=0.0,
).to(tl.float32)
score += tl.sum(k_vals * k_scale[:, None] * q_vals[None, :], axis=1)
k_rope = tl.load(
kv_rope
+ (slots[:, None] * row_bytes + kv_lora_rank + (kv_lora_rank // 128) * 4)
// 2
+ rope_offsets[None, :],
mask=valid[:, None] & (rope_offsets[None, :] < qk_rope_head_dim),
other=0.0,
).to(tl.float32)
score += tl.sum(k_rope * q_rope[None, :], axis=1)
score *= softmax_scale
score = tl.where(valid, score, -float("inf"))
probs = tl.exp(score - max_score)
probs = tl.where(valid, probs, 0.0)
denom += tl.sum(probs, axis=0)
v_vals = tl.load(
kv_fp8 + slots[:, None] * row_bytes + v_offsets[None, :],
mask=valid[:, None] & v_mask[None, :],
other=0.0,
).to(tl.float32)
v_scale = tl.load(
kv_scale
+ (
slots[:, None] * row_bytes
+ kv_lora_rank
+ (v_offsets[None, :] // 128) * 4
)
// 4,
mask=valid[:, None] & v_mask[None, :],
other=0.0,
).to(tl.float32)
acc += tl.sum(probs[:, None] * v_vals * v_scale, axis=0)
result = acc / denom
result = tl.where(denom > 0.0, result, 0.0)
out_base = (token * num_heads + head) * kv_lora_rank
tl.store(out + out_base + v_offsets, result, mask=v_mask)
@triton.jit
def _dsa_dense_kv_kernel(
q,
kv,
topk_indices,
topk_lens,
out,
num_heads: tl.constexpr,
head_dim: tl.constexpr,
kv_lora_rank: tl.constexpr,
qk_rope_head_dim: tl.constexpr,
kv_dim: tl.constexpr,
topk: tl.constexpr,
softmax_scale: tl.constexpr,
BLOCK_TOPK: tl.constexpr,
BLOCK_K: tl.constexpr,
BLOCK_V: tl.constexpr,
):
token = tl.program_id(0)
head = tl.program_id(1)
v_block = tl.program_id(2)
topk_offsets = tl.arange(0, BLOCK_TOPK)
k_offsets = tl.arange(0, BLOCK_K)
rope_offsets = tl.arange(0, 64)
v_offsets = v_block * BLOCK_V + tl.arange(0, BLOCK_V)
q_base = (token * num_heads + head) * head_dim
q_nope_base = q_base
q_rope_base = q_base + kv_lora_rank
q_rope = tl.load(
q + q_rope_base + rope_offsets,
mask=rope_offsets < qk_rope_head_dim,
other=0.0,
).to(tl.float32)
valid_len = tl.load(topk_lens + token).to(tl.int32)
max_score = tl.full((), -float("inf"), tl.float32)
for start in range(0, topk, BLOCK_TOPK):
cols = start + topk_offsets
valid = cols < valid_len
slots = tl.load(
topk_indices + token * topk + cols,
mask=valid,
other=0,
).to(tl.int64)
valid = valid & (slots >= 0)
score = tl.zeros((BLOCK_TOPK,), tl.float32)
for k_start in range(0, kv_lora_rank, BLOCK_K):
ks = k_start + k_offsets
q_vals = tl.load(q + q_nope_base + ks).to(tl.float32)
k_vals = tl.load(
kv + slots[:, None] * kv_dim + ks[None, :],
mask=valid[:, None],
other=0.0,
).to(tl.float32)
score += tl.sum(k_vals * q_vals[None, :], axis=1)
k_rope = tl.load(
kv + slots[:, None] * kv_dim + kv_lora_rank + rope_offsets[None, :],
mask=valid[:, None] & (rope_offsets[None, :] < qk_rope_head_dim),
other=0.0,
).to(tl.float32)
score += tl.sum(k_rope * q_rope[None, :], axis=1)
score *= softmax_scale
score = tl.where(valid, score, -float("inf"))
max_score = tl.maximum(max_score, tl.max(score, axis=0))
denom = tl.full((), 0.0, tl.float32)
acc = tl.zeros((BLOCK_V,), tl.float32)
v_mask = v_offsets < kv_lora_rank
for start in range(0, topk, BLOCK_TOPK):
cols = start + topk_offsets
valid = cols < valid_len
slots = tl.load(
topk_indices + token * topk + cols,
mask=valid,
other=0,
).to(tl.int64)
valid = valid & (slots >= 0)
score = tl.zeros((BLOCK_TOPK,), tl.float32)
for k_start in range(0, kv_lora_rank, BLOCK_K):
ks = k_start + k_offsets
q_vals = tl.load(q + q_nope_base + ks).to(tl.float32)
k_vals = tl.load(
kv + slots[:, None] * kv_dim + ks[None, :],
mask=valid[:, None],
other=0.0,
).to(tl.float32)
score += tl.sum(k_vals * q_vals[None, :], axis=1)
k_rope = tl.load(
kv + slots[:, None] * kv_dim + kv_lora_rank + rope_offsets[None, :],
mask=valid[:, None] & (rope_offsets[None, :] < qk_rope_head_dim),
other=0.0,
).to(tl.float32)
score += tl.sum(k_rope * q_rope[None, :], axis=1)
score *= softmax_scale
score = tl.where(valid, score, -float("inf"))
probs = tl.exp(score - max_score)
probs = tl.where(valid, probs, 0.0)
denom += tl.sum(probs, axis=0)
v_vals = tl.load(
kv + slots[:, None] * kv_dim + v_offsets[None, :],
mask=valid[:, None] & v_mask[None, :],
other=0.0,
).to(tl.float32)
acc += tl.sum(probs[:, None] * v_vals, axis=0)
result = acc / denom
result = tl.where(denom > 0.0, result, 0.0)
out_base = (token * num_heads + head) * kv_lora_rank
tl.store(out + out_base + v_offsets, result, mask=v_mask)
def _run_packed_kv(
q: torch.Tensor,
packed_kv: torch.Tensor,
topk_indices: torch.Tensor,
topk_lens: torch.Tensor,
*,
softmax_scale: float,
kv_lora_rank: int,
qk_rope_head_dim: int,
) -> torch.Tensor:
row_bytes = int(packed_kv.shape[1])
out = torch.empty(
(q.shape[0], q.shape[1], kv_lora_rank),
dtype=torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype,
device=q.device,
)
grid = (q.shape[0], q.shape[1], triton.cdiv(kv_lora_rank, 64))
_dsa_packed_kv_kernel[grid](
q,
packed_kv.view(torch.float8_e4m3fn),
packed_kv.view(torch.float32),
packed_kv.view(torch.bfloat16),
topk_indices,
topk_lens,
out,
q.shape[1],
q.shape[2],
kv_lora_rank,
qk_rope_head_dim,
row_bytes,
topk_indices.shape[1],
float(softmax_scale),
BLOCK_TOPK=32,
BLOCK_K=64,
BLOCK_V=64,
num_warps=4,
num_stages=1,
)
return out
def _run_dense_kv(
q: torch.Tensor,
kv_cache: torch.Tensor,
topk_indices: torch.Tensor,
topk_lens: torch.Tensor,
*,
softmax_scale: float,
kv_lora_rank: int,
qk_rope_head_dim: int,
) -> torch.Tensor:
kv_dim = int(kv_lora_rank) + int(qk_rope_head_dim)
out = torch.empty(
(q.shape[0], q.shape[1], kv_lora_rank),
dtype=torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype,
device=q.device,
)
grid = (q.shape[0], q.shape[1], triton.cdiv(kv_lora_rank, 64))
_dsa_dense_kv_kernel[grid](
q,
kv_cache,
topk_indices,
topk_lens,
out,
q.shape[1],
q.shape[2],
kv_lora_rank,
qk_rope_head_dim,
kv_dim,
topk_indices.shape[1],
float(softmax_scale),
BLOCK_TOPK=32,
BLOCK_K=64,
BLOCK_V=64,
num_warps=4,
num_stages=1,
)
return out
def _flatten_packed_kv_cache(packed_kv_cache: torch.Tensor) -> torch.Tensor:
if packed_kv_cache.dim() == 2:
return packed_kv_cache
return packed_kv_cache.reshape(-1, packed_kv_cache.shape[-1])
def _flatten_dense_kv_cache(kv_cache: torch.Tensor) -> torch.Tensor:
if kv_cache.dim() == 2:
return kv_cache
if kv_cache.dim() == 3:
return kv_cache.squeeze(1)
if kv_cache.shape[1] == 1:
kv_cache = kv_cache.permute(0, 2, 1, 3)
return kv_cache.reshape(-1, kv_cache.shape[-1])
def _flatten_query(q: torch.Tensor) -> torch.Tensor:
if q.dim() == 3:
return q
return q.reshape(-1, q.shape[-2], q.shape[-1])
def _run_dsa(
*,
q: torch.Tensor,
kv_cache: torch.Tensor | None,
packed_kv_cache: torch.Tensor | None,
topk_slots: torch.Tensor,
topk_lens: torch.Tensor,
kv_lora_rank: int,
qk_rope_head_dim: int,
softmax_scale: float,
k_scale: float,
out: torch.Tensor | None,
) -> torch.Tensor:
q = _flatten_query(q).contiguous()
topk_slots = topk_slots.contiguous()
topk_lens = topk_lens.contiguous()
softmax_scale = float(softmax_scale) * float(k_scale)
if packed_kv_cache is not None:
result = _run_packed_kv(
q,
_flatten_packed_kv_cache(packed_kv_cache).contiguous(),
topk_slots,
topk_lens,
softmax_scale=softmax_scale,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
)
else:
result = _run_dense_kv(
q,
_flatten_dense_kv_cache(kv_cache).contiguous(),
topk_slots,
topk_lens,
softmax_scale=softmax_scale,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
)
if out is None:
return result
out_view = out.reshape_as(result)
out_view.copy_(result)
return out
@register_kernel(
"attention",
"dsa_decode",
name="triton_dsa_decode",
solution="triton",
capability=CapabilityRequirement(vendors=frozenset({"nvidia", "amd"})),
signatures=frozenset(
{
format_signature(q=dense_tensor_format(torch.bfloat16)),
format_signature(q=dense_tensor_format(torch.float8_e4m3fn)),
}
),
traits={
"page_size": frozenset({64}),
"q_len_per_req": frozenset({1, 2, 3, 4, 5, 6}),
"qk_nope_head_dim": frozenset({128, 192}),
"kv_lora_rank": frozenset({128, 512}),
"qk_rope_head_dim": frozenset({64}),
"topk": frozenset({512, 1024, 2048}),
"kv_cache_available": frozenset({False, True}),
"sparse_kv_cache_available": frozenset({False, True}),
"topk_layout": frozenset({"global_slots"}),
"support_logit_cap": frozenset({False}),
"return_lse": frozenset({False}),
},
priority=Priority.PORTABLE,
tags={"portability"},
)
def triton_dsa_decode(
q: torch.Tensor,
kv_cache: torch.Tensor | None,
sparse_kv_cache: torch.Tensor | None,
topk_slots: torch.Tensor,
topk_lens: torch.Tensor | None,
max_seqlen_k: int,
qk_nope_head_dim: int,
kv_lora_rank: int,
qk_rope_head_dim: int,
softmax_scale: float,
page_size: int,
q_len_per_req: int = 1,
logit_cap: float = 0.0,
k_scale: float = 1.0,
return_lse: bool = False,
out: torch.Tensor | None = None,
) -> torch.Tensor:
return _run_dsa(
q=q,
kv_cache=kv_cache,
packed_kv_cache=sparse_kv_cache,
topk_slots=topk_slots,
topk_lens=topk_lens,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
softmax_scale=softmax_scale,
k_scale=k_scale,
out=out,
)
@register_kernel(
"attention",
"dsa_prefill",
name="triton_dsa_prefill",
solution="triton",
capability=CapabilityRequirement(vendors=frozenset({"nvidia", "amd"})),
signatures=frozenset(
{
format_signature(q=dense_tensor_format(torch.bfloat16)),
format_signature(q=dense_tensor_format(torch.float8_e4m3fn)),
}
),
traits={
"page_size": frozenset({64}),
"q_len_per_req": frozenset({1}),
"qk_nope_head_dim": frozenset({128, 192}),
"kv_lora_rank": frozenset({128, 512}),
"qk_rope_head_dim": frozenset({64}),
"topk": frozenset({512, 1024, 2048}),
"kv_cache_available": frozenset({False, True}),
"sparse_kv_cache_available": frozenset({False, True}),
"topk_layout": frozenset({"global_slots"}),
"support_logit_cap": frozenset({False}),
"return_lse": frozenset({False}),
},
priority=Priority.PORTABLE,
tags={"portability"},
)
def triton_dsa_prefill(
q: torch.Tensor,
kv_cache: torch.Tensor | None,
sparse_kv_cache: torch.Tensor | None,
topk_slots: torch.Tensor,
topk_lens: torch.Tensor | None,
max_seqlen_k: int,
qk_nope_head_dim: int,
kv_lora_rank: int,
qk_rope_head_dim: int,
softmax_scale: float,
page_size: int,
q_len_per_req: int = 1,
logit_cap: float = 0.0,
k_scale: float = 1.0,
return_lse: bool = False,
out: torch.Tensor | None = None,
) -> torch.Tensor:
return _run_dsa(
q=q,
kv_cache=kv_cache,
packed_kv_cache=sparse_kv_cache,
topk_slots=topk_slots,
topk_lens=topk_lens,
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
softmax_scale=softmax_scale,
k_scale=k_scale,
out=out,
)