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

819 lines
32 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
from collections.abc import Sequence
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING
import torch
from tokenspeed_kernel import (
mha_decode_with_kvcache,
mha_extend_with_kvcache,
mha_plan,
mha_prefill,
)
from tokenspeed_kernel.ops.kvcache.triton import (
fused_fp8_set_kv_buffer,
gather_page_table_with_padding,
)
from tokenspeed.runtime.configs.model_config import AttentionArch
from tokenspeed.runtime.execution.breakable_cuda_graph import scrub_padding_tail
from tokenspeed.runtime.execution.forward_batch_info import ForwardMode
from tokenspeed.runtime.layers.attention.backends.base import AttentionBackend
from tokenspeed.runtime.layers.attention.backends.flat_groups import (
FlatCacheGroupsMixin,
)
from tokenspeed.runtime.layers.attention.configs.mha import MHAConfig
from tokenspeed.runtime.layers.attention.registry import register_backend
from tokenspeed.runtime.layers.attention.utils import build_page_table
from tokenspeed.runtime.utils.common import ceil_div
if TYPE_CHECKING:
from tokenspeed.runtime.layers.paged_attention import PagedAttention
_KERNEL_SOLUTION_BY_BACKEND = {
"mha": None,
"fa3": "fa3",
"fa4": "fa4",
"triton": "triton",
"flashinfer": "flashinfer",
}
def _scrub_extend_padding(metadata, q, k, v) -> None:
"""Zero the q/k/v rows beyond the real (unpadded) token count under a prefill graph.
Reads the count from the pinned CPU cu-seqlens mirror (sync-free) and delegates the
zeroing to the shared prefill-graph padding helper. No-op on normal unpadded forwards.
"""
scrub_padding_tail(metadata.cu_extend_seq_lens_cpu[-1], q, k, v)
@dataclass(kw_only=True)
class MHAExtendMetadata:
# Device-side metadata:
# - seq_lens: total length after this step
# - extend_seq_lens: length of new tokens
# cu_extend_seq_lens: the cumsum version of extend_seq_lens
# cu_seqlens_kv: the cumsum version of seq_lens
# - extend_prefix_lens: length of the cached prefix tokens
# seq_lens[i] = extend_prefix_lens[i] + extend_seq_lens[i]
# page_table is None on the flat path (per-group page_tables route reads).
page_table: torch.Tensor | None
seq_lens: torch.Tensor
extend_seq_lens: torch.Tensor
cu_extend_seq_lens: torch.Tensor
cu_seqlens_kv: torch.Tensor
extend_prefix_lens: torch.Tensor
extend_seq_lens_cpu: list[int]
cu_extend_seq_lens_cpu: list[int]
max_extend_seq_len: int
max_extend_prefix_len: int = 0
# Flat per-group page tables (group_id -> [num_reqs, max_pages]); None on
# the single-table path. TODO(radix-removal): drop the single page_table.
page_tables: dict[str, torch.Tensor] | None = None
# Flat per-group KV write locations (group_id -> [num_tokens] int32),
# built with page_tables — same groups, same lifecycle.
out_cache_locs: dict[str, torch.Tensor] | None = None
@dataclass(kw_only=True)
class MHADecodeMetadata:
# page_table is None on the flat path (per-group page_tables route reads).
page_table: torch.Tensor | None
seq_lens: torch.Tensor
# Flat per-group tables/write-locs; see MHAExtendMetadata.
page_tables: dict[str, torch.Tensor] | None = None
out_cache_locs: dict[str, torch.Tensor] | None = None
class MHAAttnBackend(FlatCacheGroupsMixin, AttentionBackend):
"""Standard MHA backend that routes through tokenspeed_kernel attention APIs."""
# Unconditional: safety comes from the publication rule
# (paged_cache_spec.publish_paged_cache_groups) plus the replay
# stale-table guard. TODO(radix-removal): drop the flag.
uses_flat_cache_groups: bool = True
def support_kv_cache_prewrite(
self, forward_mode: ForwardMode | None = None
) -> bool:
return forward_mode is not None and forward_mode.is_decode()
def __init__(self, config: MHAConfig):
super().__init__(config)
# Map the selected backend to the corresponding kernel solution string.
backend_name = config.backend_name or "mha"
self.kernel_solution = _KERNEL_SOLUTION_BY_BACKEND[backend_name]
# Static information needed for metadata construction and kernel dispatch
self.max_context_len = config.context_len
self.page_size = config.page_size
self.max_num_pages = ceil_div(self.max_context_len, self.page_size)
num_q_heads = config.num_attention_heads
num_kv_heads = config.num_kv_heads
self.tp_q_head_num = max(num_q_heads // config.attn_tp_size, 1)
self.tp_kv_head_num = max(num_kv_heads // config.attn_tp_size, 1)
self.head_dim = config.head_dim
self.qkv_dtype = config.dtype
self.kv_cache_dtype = config.kv_cache_dtype
self.is_fp8 = self.kv_cache_dtype in (
torch.float8_e4m3fn,
torch.float8_e5m2,
)
self.plan = partial(
mha_plan,
dtype=self.kv_cache_dtype if self.is_fp8 else self.qkv_dtype,
head_dim=self.head_dim,
return_lse=False,
solution=self.kernel_solution,
)
# DFLASH draft: expand decode metadata to spec_num_tokens rows/request
# (whole block in one decode forward), with uniform non-causal seq_lens.
self.draft_block_decode = bool(getattr(config, "draft_block_decode", False))
# Forward metadata is initialized in the runner per forward call
self.forward_decode_metadata: MHADecodeMetadata | None = None
self.forward_extend_metadata: MHAExtendMetadata | None = None
# ------------------------------------------------------------------
# Metadata initialization
# ------------------------------------------------------------------
def init_forward_metadata(
self,
bs: int,
num_extends: int,
req_pool_indices: torch.Tensor,
seq_lens: torch.Tensor,
req_to_page: torch.Tensor,
forward_mode: ForwardMode,
# Only consumed on the extend/mixed path; decode callers (e.g. the
# DFLASH draft and the cuda-graph wrapper's draft decode init) omit
# them, so they must be optional.
extend_seq_lens: torch.Tensor | None = None,
extend_seq_lens_cpu: torch.Tensor | None = None,
extend_prefix_lens: torch.Tensor | None = None,
extend_prefix_lens_cpu: torch.Tensor | None = None,
flat_block_tables: dict[str, torch.Tensor] | None = None,
**kwargs,
):
assert not forward_mode.is_mixed(), "mha backend does not support mixed batch"
seq_lens = seq_lens[:bs]
flat_page_tables = self._shed_state_groups(flat_block_tables)
flat_out_cache_locs = None
if flat_page_tables:
# Verify keeps [bs]-row tables; only DFLASH expands rows. TODO(flat+dflash).
assert not (
self.draft_block_decode and self.spec_num_tokens > 1
), "flat cache groups are unsupported with DFLASH block decode"
# The flat path routes every read/write through the per-group
# tables; the radix single table would be dead work.
page_table = None
if forward_mode.is_extend_or_mixed():
assert extend_prefix_lens_cpu is not None
assert extend_seq_lens_cpu is not None
flat_out_cache_locs = self._compute_flat_extend_out_cache_locs(
flat_page_tables,
extend_prefix_lens_cpu[:bs],
extend_seq_lens_cpu[:bs],
self.page_size,
)
else:
verify_tokens = (
self.spec_num_tokens
if self.spec_num_tokens > 1 and not self.is_draft
else 1
)
flat_out_cache_locs = self._compute_flat_decode_out_cache_locs(
flat_page_tables,
seq_lens,
self.page_size,
verify_tokens,
)
self._maybe_check_flat_write_locs(
flat_page_tables, flat_out_cache_locs, self.page_size
)
else:
page_table = build_page_table(
req_pool_indices[:bs],
req_to_page,
self.page_size,
self.max_context_len,
)
if forward_mode.is_extend_or_mixed():
assert extend_seq_lens is not None
assert extend_seq_lens_cpu is not None
assert extend_prefix_lens is not None
assert extend_prefix_lens_cpu is not None
# Create cumulative sum of the sequence lengths for Q and KV.
extend_seq_lens = extend_seq_lens[:bs]
extend_seq_lens_cpu = [int(x) for x in extend_seq_lens_cpu[:bs].tolist()]
cu_extend_seq_lens = torch.nn.functional.pad(
torch.cumsum(extend_seq_lens, dim=0, dtype=torch.int32),
(1, 0),
)
cu_extend_seq_lens_cpu = [0]
for length in extend_seq_lens_cpu:
cu_extend_seq_lens_cpu.append(cu_extend_seq_lens_cpu[-1] + length)
cu_seqlens_kv = torch.nn.functional.pad(
torch.cumsum(seq_lens, dim=0, dtype=torch.int32),
(1, 0),
)
extend_prefix_lens = extend_prefix_lens[:bs]
max_extend_seq_len = max(extend_seq_lens_cpu)
max_extend_prefix_len = int(extend_prefix_lens_cpu[:bs].max().item())
self.forward_extend_metadata = MHAExtendMetadata(
page_table=page_table,
seq_lens=seq_lens,
extend_seq_lens=extend_seq_lens,
cu_extend_seq_lens=cu_extend_seq_lens,
cu_seqlens_kv=cu_seqlens_kv,
extend_prefix_lens=extend_prefix_lens,
extend_seq_lens_cpu=extend_seq_lens_cpu,
cu_extend_seq_lens_cpu=cu_extend_seq_lens_cpu,
max_extend_seq_len=max_extend_seq_len,
max_extend_prefix_len=max_extend_prefix_len,
page_tables=flat_page_tables,
out_cache_locs=flat_out_cache_locs,
)
# Drafter step 1+ decodes under an EXTEND/MIXED target; seq_lens
# aliases the drafter's live buffer (pre-written by the wrapper).
if self.is_draft:
self.forward_decode_metadata = MHADecodeMetadata(
page_table=page_table,
seq_lens=seq_lens,
page_tables=flat_page_tables,
out_cache_locs=flat_out_cache_locs,
)
else:
if self.draft_block_decode and self.spec_num_tokens > 1:
# DFLASH drafts a whole block in one decode forward; the decode
# kernel keys masking off max_seqlen_q, so expand each request
# into spec_num_tokens rows with the SAME full seq_len. That
# makes max_seqlen_q == 1 per row, so every block query attends
# over the entire block (non-causal block-diffusion drafting).
# Target verify keeps the unexpanded multi-query decode path.
expanded_page_table, expanded_seq_lens = (
self._make_spec_metadata_buffers(
bs,
page_table.device,
)
)
self._fill_spec_metadata_uniform(
expanded_page_table,
expanded_seq_lens,
page_table,
seq_lens,
)
self.forward_decode_metadata = MHADecodeMetadata(
page_table=expanded_page_table,
seq_lens=expanded_seq_lens,
page_tables=flat_page_tables,
out_cache_locs=flat_out_cache_locs,
)
else:
self.forward_decode_metadata = MHADecodeMetadata(
page_table=page_table,
seq_lens=seq_lens,
page_tables=flat_page_tables,
out_cache_locs=flat_out_cache_locs,
)
def init_cuda_graph_state(
self,
max_bs: int,
seq_lens_buf: torch.Tensor,
paged_cache_group_specs: Sequence = (),
**kwargs,
):
# State-family groups (GDN/mamba pages) belong to the mamba backend;
# learn their ids from the pool's specs so every flat table/loc path
# here (eager, capture, replay) sheds them.
self._learn_flat_state_groups(paged_cache_group_specs)
assert (
seq_lens_buf.dtype == torch.int32
and seq_lens_buf.dim() == 1
and seq_lens_buf.shape[0] >= max_bs
), (
f"seq_lens_buf must be int32 with shape[0] >= {max_bs}, "
f"got {seq_lens_buf.dtype} {tuple(seq_lens_buf.shape)}"
)
self.cuda_graph_decode_metadata = {}
# Flat per-group persistent buffers, lazily allocated at first
# capture. TODO(radix-removal): parallels cuda_graph_page_table.
# Initialized before the DFLASH early return: replay reads the dict
# unconditionally for the stale-table guard.
self._init_flat_graph_buffers(max_bs)
if self.draft_block_decode and self.spec_num_tokens > 1:
# DFLASH draft block: expand to spec_num_tokens decode rows per
# request (one row per block position), so max_seqlen_q == 1 per row
# and every block query attends over the whole block (non-causal).
self.cuda_graph_page_table, self.cuda_graph_seq_lens = (
self._make_spec_metadata_buffers(max_bs, self.device)
)
self.cuda_graph_page_table.zero_()
# seq_lens are filled from the live draft length inside the captured
# graph; seed a valid baseline so any pre-broadcast read stays in range.
self.cuda_graph_seq_lens.fill_(self.spec_num_tokens)
return
self.cuda_graph_page_table = torch.zeros(
(max_bs, self.max_num_pages), dtype=torch.int32, device=self.device
)
if self.spec_num_tokens > 1 and not self.is_draft:
self.cuda_graph_seq_lens = torch.empty(
(max_bs,), dtype=torch.int32, device=self.device
)
else:
# Alias controller's seq_lens_buf — backend never mutates it.
self.cuda_graph_seq_lens = seq_lens_buf
def init_forward_metadata_capture_cuda_graph(
self,
bs: int,
req_pool_indices: torch.Tensor,
seq_lens: torch.Tensor,
forward_mode: ForwardMode,
flat_cache_group_ids: tuple[str, ...] = (),
**kwargs,
):
assert not forward_mode.is_extend_or_mixed()
# Real tables only arrive at replay: capture lazily allocates
# persistent per-group buffers and records metadata views into them,
# so replay can copy_ fresh data to the graph-recorded addresses.
if flat_cache_group_ids:
# Verify keeps [bs]-row tables + [bs*N] loc views. TODO(flat+dflash).
assert not (
self.draft_block_decode and self.spec_num_tokens > 1
), "flat_cache_group_ids is unsupported with DFLASH block decode"
page_tables, out_cache_locs = self._flat_capture_group_views(
bs,
flat_cache_group_ids,
tokens_per_req=(
self.spec_num_tokens
if self.spec_num_tokens > 1 and not self.is_draft
else 1
),
)
if self.draft_block_decode and self.spec_num_tokens > 1:
# DFLASH draft block: spec_num_tokens decode rows per request.
expanded_bs = bs * self.spec_num_tokens
metadata = MHADecodeMetadata(
page_table=self.cuda_graph_page_table[:expanded_bs, :],
seq_lens=self.cuda_graph_seq_lens[:expanded_bs],
page_tables=page_tables,
out_cache_locs=out_cache_locs,
)
# Uniform non-causal seq_lens are written by the drafter inside the
# captured graph (see fill_block_decode_seq_lens); seed a safe
# baseline for the capture run before that op records.
metadata.seq_lens.fill_(self.spec_num_tokens)
else:
metadata = MHADecodeMetadata(
# Flat captures route reads through the per-group tables and
# replay never fills the radix single table, so mirror the
# eager flat path: page_table=None instead of a slice of the
# never-filled zero buffer.
page_table=(
None
if page_tables is not None
else self.cuda_graph_page_table[:bs, :]
),
seq_lens=self.cuda_graph_seq_lens[:bs],
page_tables=page_tables,
out_cache_locs=out_cache_locs,
)
if self.spec_num_tokens > 1 and not self.is_draft:
metadata.seq_lens.copy_(seq_lens[:bs].clamp_min(self.spec_num_tokens))
self.cuda_graph_decode_metadata[bs] = metadata
self.forward_decode_metadata = metadata
def init_forward_metadata_replay_cuda_graph(
self,
bs: int,
req_pool_indices: torch.Tensor,
seq_lens: torch.Tensor,
req_to_page: torch.Tensor,
forward_mode: ForwardMode,
flat_block_tables: dict[str, torch.Tensor] | None = None,
**kwargs,
):
assert not forward_mode.is_extend_or_mixed()
# Fail loudly instead of replaying over stale/zero page tables.
self._flat_replay_stale_guard(bs, flat_block_tables)
# Flat captures read only the per-group buffers; the radix single
# table (cuda_graph_page_table) would be dead work there.
if not self.cuda_graph_flat_page_tables:
gather_page_table_with_padding(
req_to_page=req_to_page,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
out=self.cuda_graph_page_table,
bs=bs,
max_num_pages=self.max_num_pages,
page_size=self.page_size,
dummy_slot=0,
)
if self.spec_num_tokens > 1 and not self.is_draft:
self.cuda_graph_seq_lens[:bs].copy_(seq_lens[:bs])
elif self.draft_block_decode:
# DFLASH draft: replicate each request's page table to its
# spec_num_tokens block rows. The block-end seq_lens are filled by
# the drafter inside the captured graph, so they are not touched
# here (they re-derive from the live draft length on every replay).
base_page_table = req_to_page[req_pool_indices[:bs], : self.max_num_pages]
self.cuda_graph_page_table[: bs * self.spec_num_tokens, :].view(
bs, self.spec_num_tokens, self.max_num_pages
).copy_(base_page_table[:, None, :])
# cuda_graph_seq_lens is filled by input prep / the spec copy above.
if flat_block_tables:
self._flat_replay_fill(
bs,
flat_block_tables,
self.cuda_graph_seq_lens,
tokens_per_req=(
self.spec_num_tokens
if self.spec_num_tokens > 1 and not self.is_draft
else 1
),
)
if bs in self.cuda_graph_decode_metadata:
self.forward_decode_metadata = self.cuda_graph_decode_metadata[bs]
def fill_block_decode_seq_lens(self, bs: int, block_seq_lens: torch.Tensor) -> None:
"""DFLASH: broadcast each request's block-end length to its
spec_num_tokens cuda-graph decode rows (uniform, non-causal).
Called by the drafter inside the captured graph so that on every replay
the expanded seq_lens re-derive from the live draft length (which is
recomputed in-graph from the target's accept lengths).
Args:
bs: Number of draft requests.
block_seq_lens: ``[bs]`` per-request block-end lengths
(prefix + spec_num_tokens).
"""
spec = self.spec_num_tokens
self.cuda_graph_seq_lens[: bs * spec].view(bs, spec).copy_(
block_seq_lens[:bs]
.clamp(self.spec_num_tokens, self.max_context_len)
.unsqueeze(1)
)
# ------------------------------------------------------------------
# Forward
# ------------------------------------------------------------------
def forward_decode(
self,
q: torch.Tensor,
k: torch.Tensor | None,
v: torch.Tensor | None,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
bs: int,
save_kv_cache: bool = False,
**kwargs,
) -> torch.Tensor:
assert layer.qk_head_dim == layer.v_head_dim
assert (k is None) == (v is None)
has_kv = k is not None
q = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim)
if has_kv:
k = k.view(-1, layer.tp_k_head_num, layer.qk_head_dim)
v = v.view(-1, layer.tp_v_head_num, layer.v_head_dim)
sinks = kwargs.get("sinks")
out_cache_loc = self._select_out_cache_loc(
layer,
self.forward_decode_metadata,
out_cache_loc,
prefer_caller=self.is_draft,
)
return self._forward_decode(
q,
k,
v,
layer,
out_cache_loc,
token_to_kv_pool,
self.forward_decode_metadata,
save_kv_cache=save_kv_cache,
sinks=sinks,
)
def forward_extend(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
bs: int,
save_kv_cache: bool = False,
**kwargs,
) -> torch.Tensor:
assert layer.qk_head_dim == layer.v_head_dim
assert (k is None) == (v is None)
assert k is not None
q = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim)
k = k.view(-1, layer.tp_k_head_num, layer.qk_head_dim)
v = v.view(-1, layer.tp_v_head_num, layer.v_head_dim)
metadata = self.forward_extend_metadata
sinks = kwargs.get("sinks")
out_cache_loc = self._select_out_cache_loc(layer, metadata, out_cache_loc)
plan = self.plan(
window_left=layer.sliding_window_size,
logit_cap=layer.logit_cap,
sinks=sinks,
)
extend_mode = plan.get("extend_mode", "prewrite")
if metadata.max_extend_prefix_len == 0 and extend_mode == "postwrite":
return self._forward_prefill(
q,
k,
v,
layer,
out_cache_loc,
token_to_kv_pool,
metadata,
save_kv_cache,
sinks,
)
else:
return self._forward_extend(
q,
k,
v,
layer,
out_cache_loc,
token_to_kv_pool,
metadata,
save_kv_cache,
sinks,
)
def _forward_prefill(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
metadata: MHAExtendMetadata,
save_kv_cache: bool,
sinks: torch.Tensor | None,
) -> torch.Tensor:
_scrub_extend_padding(metadata, q, k, v)
# TODO: use a custom kernel to do downcast
if self.is_fp8:
q = q.to(self.kv_cache_dtype)
k = k.to(self.kv_cache_dtype)
v = v.to(self.kv_cache_dtype)
output = mha_prefill(
q=q,
k=k,
v=v,
cu_seqlens=metadata.cu_extend_seq_lens,
cu_seqlens_cpu=metadata.cu_extend_seq_lens_cpu,
max_seqlen=metadata.max_extend_seq_len,
window_left=layer.sliding_window_size,
logit_cap=layer.logit_cap,
sinks=sinks,
solution=self.kernel_solution,
)
output = output.reshape(-1, layer.tp_q_head_num * layer.v_head_dim)
if save_kv_cache:
self._save_kv_cache(layer, out_cache_loc, token_to_kv_pool, k, v)
return output
def _forward_extend(
self,
q: torch.Tensor,
k: torch.Tensor | None,
v: torch.Tensor | None,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
metadata: MHAExtendMetadata,
save_kv_cache: bool,
sinks: torch.Tensor | None,
) -> torch.Tensor:
_scrub_extend_padding(metadata, q, k, v)
if save_kv_cache:
self._save_kv_cache(layer, out_cache_loc, token_to_kv_pool, k, v)
if self.is_fp8:
q = q.to(self.kv_cache_dtype)
k_cache, v_cache = self._get_kv_cache(layer, token_to_kv_pool)
output = mha_extend_with_kvcache(
q=q,
cu_seqlens_q=metadata.cu_extend_seq_lens,
cu_seqlens_kv=metadata.cu_seqlens_kv,
k_cache=k_cache,
v_cache=v_cache,
page_table=self._select_page_table(layer, metadata),
cache_seqlens=metadata.seq_lens,
max_seqlen_q=metadata.max_extend_seq_len,
max_seqlen_k=self.max_context_len,
# DFLASH marks its draft attention non-causal so the draft block's
# query positions attend bidirectionally. Every other layer leaves
# the attribute unset, so this stays causal by default.
is_causal=not bool(getattr(layer, "non_causal", False)),
window_left=layer.sliding_window_size,
logit_cap=layer.logit_cap,
sinks=sinks,
solution=self.kernel_solution,
)
return output.reshape(-1, layer.tp_q_head_num * layer.v_head_dim)
def _forward_decode(
self,
q: torch.Tensor,
k: torch.Tensor | None,
v: torch.Tensor | None,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
metadata: MHADecodeMetadata,
save_kv_cache: bool,
sinks: torch.Tensor | None,
) -> torch.Tensor:
if save_kv_cache:
self._save_kv_cache(layer, out_cache_loc, token_to_kv_pool, k, v)
if self.is_fp8:
q = q.to(self.kv_cache_dtype)
k_cache, v_cache = self._get_kv_cache(layer, token_to_kv_pool)
max_seqlen_q = q.shape[0] // metadata.seq_lens.shape[0]
output = mha_decode_with_kvcache(
q=q,
k_cache=k_cache,
v_cache=v_cache,
page_table=self._select_page_table(layer, metadata),
cache_seqlens=metadata.seq_lens,
window_left=layer.sliding_window_size,
logit_cap=layer.logit_cap,
sinks=sinks,
max_seqlen_k=self.max_context_len,
max_seqlen_q=max_seqlen_q,
solution=self.kernel_solution,
)
return output.reshape(-1, layer.tp_q_head_num * layer.v_head_dim)
# ------------------------------------------------------------------
# Helper methods
# ------------------------------------------------------------------
def _save_kv_cache(
self,
layer: PagedAttention,
out_cache_loc: torch.Tensor,
token_to_kv_pool,
k: torch.Tensor | None,
v: torch.Tensor | None,
) -> None:
if k is None:
return
if (
self.kv_cache_dtype == torch.float8_e4m3fn
and k.dtype != torch.float8_e4m3fn
):
k_cache, v_cache = token_to_kv_pool.get_kv_buffer(layer.layer_id)
fused_fp8_set_kv_buffer(
k=k,
v=v,
k_cache=k_cache,
v_cache=v_cache,
cache_loc=out_cache_loc,
k_scale=layer.k_scale,
v_scale=layer.v_scale,
page_size=self.page_size,
)
else:
token_to_kv_pool.set_kv_buffer(
layer,
out_cache_loc,
k,
v,
layer.k_scale,
layer.v_scale,
)
def _get_kv_cache(self, layer: PagedAttention, token_to_kv_pool):
k_cache = token_to_kv_pool.get_key_buffer(layer.layer_id).view(
-1,
self.page_size,
layer.tp_k_head_num,
layer.qk_head_dim,
)
v_cache = token_to_kv_pool.get_value_buffer(layer.layer_id).view(
-1,
self.page_size,
layer.tp_v_head_num,
layer.v_head_dim,
)
return k_cache, v_cache
def _make_spec_metadata_buffers(
self,
bs: int,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
expanded_bs = bs * self.spec_num_tokens
cuda_graph_page_table = torch.empty(
(expanded_bs, self.max_num_pages),
dtype=torch.int32,
device=device,
)
cuda_graph_seq_lens = torch.empty(
(expanded_bs,),
dtype=torch.int32,
device=device,
)
return (cuda_graph_page_table, cuda_graph_seq_lens)
def _fill_spec_metadata_uniform(
self,
expanded_page_table: torch.Tensor,
expanded_seq_lens: torch.Tensor,
page_table: torch.Tensor,
seq_lens: torch.Tensor,
):
"""Expand spec metadata with a uniform (non-causal) seq_len per row.
Replicates the full seq_len to all spec_num_tokens rows of a request so
each row decodes with max_seqlen_q == 1 over the whole block. Used by the
DFLASH drafter so every block query attends over the entire block
(non-causal block-diffusion drafting), as opposed to the target's
unexpanded causal multi-query verify path.
"""
bs = seq_lens.shape[0]
spec_num_tokens = self.spec_num_tokens
expanded_page_table = expanded_page_table.view(
bs, spec_num_tokens, self.max_num_pages
)
expanded_page_table.copy_(page_table[:, None, :])
# Clamp to max_context_len so the draft decode never asks the attention
# kernel for more than max_num_pages worth of page-table columns. The
# block-end length is prefix + spec_num_tokens, which can exceed
# max_context_len for a request near the context limit; without the
# clamp the kernel reads page_table[:, >= max_num_pages] out of bounds
# (CUDA illegal memory access). Mirrors fill_block_decode_seq_lens on the
# cuda-graph path (this eager path is taken by mixed prefill+decode
# batches even when cuda graphs are enabled).
expanded_seq_lens.view(bs, spec_num_tokens).copy_(
seq_lens.clamp(spec_num_tokens, self.max_context_len)[:, None]
)
for _backend_name in _KERNEL_SOLUTION_BY_BACKEND:
register_backend(_backend_name, {AttentionArch.MHA}, MHAAttnBackend)