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

561 lines
19 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.
"""Cache-less multi-headed attention used by multimodal encoders.
Encode-only attention layer used by vision and audio towers, plus its backend
dispatch table. Backends are free functions (not
``AttentionBackend`` subclasses) because the vision encoder is single-shot
with no KV cache, no decode/extend split, and no graph capture protocol --
the ``AttentionBackend`` ABC's prefill/decode/extend lifecycle does not
apply. This file is therefore kept out of ``backends/`` (the home of
``AttentionBackend`` subclasses) and lives at the ``layers/attention/`` top
level alongside ``registry.py`` / ``utils.py``.
"""
from __future__ import annotations
import functools
import logging
from collections.abc import Callable
from typing import Any
import torch
import torch.nn as nn
from einops import rearrange
from tokenspeed_kernel.platform import current_platform
from tokenspeed.runtime.distributed import utils as dist_utils
from tokenspeed.runtime.distributed.mapping import Mapping
from tokenspeed.runtime.layers.linear import (
QKVParallelLinear,
RowParallelLinear,
)
from tokenspeed.runtime.layers.quantization import QuantizationConfig
from tokenspeed.runtime.layers.rotary_embedding import apply_rotary_pos_emb_native
from tokenspeed.runtime.utils import add_prefix, round_up
logger = logging.getLogger(__name__)
_platform = current_platform()
_is_nvidia = _platform.is_nvidia
_is_amd = _platform.is_amd
if _is_nvidia:
from tokenspeed_kernel.ops.attention.flash_attn import flash_attn_varlen_func
from tokenspeed_kernel.ops.attention.flashinfer import (
cudnn_batch_prefill_with_kv_cache,
)
from tokenspeed_kernel.ops.attention.triton.context import context_attention_fwd
from tokenspeed_kernel.ops.attention.triton.qkv_rotary import (
packed_qkv_complex_rotary,
packed_qkv_neox_rotary,
)
# CUDA-graph bucketing for the cuDNN vision prefill backend: batch and max
# seqlen are quantized so a small set of captured graphs covers the request
# distribution. The consts are consumed by VLM tower models, not by
# ``MultimodalEncoderAttention`` itself.
VIT_CUDNN_WORKSPACE_BYTES = 128 * 1024 * 1024
VIT_CUDNN_BATCH_BUCKETS: tuple[int, ...] = (8, 16, 32, 64)
VIT_CUDNN_SEQLEN_BUCKETS: tuple[int, ...] = (4096, 8192, 16384, 32768, 65536, 131072)
def round_up_to_bucket(value: int, buckets: tuple[int, ...]) -> int:
"""Smallest bucket >= value; values past the last bucket round up to a
multiple of it. Used by vision tower code to pad batch size and max-seqlen
into a finite set of captured cuDNN graph shapes.
"""
if value <= 0:
return buckets[0]
for bucket in buckets:
if bucket >= value:
return bucket
return round_up(value, buckets[-1])
# === Backend dispatch ===
# The dispatcher always passes the full kwarg set (cu_seqlens / bsz / seq_len /
# softmax_scale / max_seqlen / sequence_lengths / workspace_buffer); each
# backend declares the subset it uses and absorbs the rest via ``**_``.
def _varlen_metadata(
cu_seqlens: torch.Tensor | None,
bsz: int,
seq_len: int,
*,
device: torch.device,
max_seqlen: int | None,
) -> tuple[torch.Tensor, torch.Tensor, int]:
"""Resolve cu_seqlens / seq_lens / max_seqlen shared by the varlen backends.
``max_seqlen`` is honored when the caller supplies it (the capture-safe
path); only the eager fallback derives it via ``.item()``, which forces a
GPU->CPU sync that is illegal inside a captured CUDA graph. Deriving it
once here keeps every varlen backend capture-safe instead of each kernel
wrapper re-deriving (and re-syncing) it.
"""
if cu_seqlens is None:
cu_seqlens = torch.arange(
0, (bsz + 1) * seq_len, step=seq_len, dtype=torch.int32, device=device
)
else:
cu_seqlens = cu_seqlens.to(dtype=torch.int32, device=device)
seq_lens = cu_seqlens[1:] - cu_seqlens[:-1]
if max_seqlen is None:
max_seqlen = int(seq_lens.max().item())
return cu_seqlens, seq_lens, int(max_seqlen)
def vision_attn_triton(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens: torch.Tensor | None,
bsz: int,
seq_len: int,
softmax_scale: float | None = None,
max_seqlen: int | None = None,
**_: Any,
) -> torch.Tensor:
"""Triton context attention without a causal mask."""
cu_seqlens, seq_lens, max_seqlen = _varlen_metadata(
cu_seqlens, bsz, seq_len, device=q.device, max_seqlen=max_seqlen
)
output = torch.empty_like(q)
context_attention_fwd(
q,
k,
v,
output,
cu_seqlens,
seq_lens,
max_seqlen,
is_causal=False,
sm_scale=softmax_scale,
)
return output
def vision_attn_fa3(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens: torch.Tensor | None,
bsz: int,
seq_len: int,
softmax_scale: float | None = None,
max_seqlen: int | None = None,
**_: Any,
) -> torch.Tensor:
cu_seqlens, _, max_seqlen = _varlen_metadata(
cu_seqlens, bsz, seq_len, device=q.device, max_seqlen=max_seqlen
)
return flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens,
cu_seqlens_k=cu_seqlens,
max_seqlen_q=max_seqlen,
max_seqlen_k=max_seqlen,
softmax_scale=softmax_scale,
)
def vision_attn_fa4(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens: torch.Tensor | None,
bsz: int,
seq_len: int,
softmax_scale: float | None = None,
max_seqlen: int | None = None,
**_: Any,
) -> torch.Tensor:
cu_seqlens, _, max_seqlen = _varlen_metadata(
cu_seqlens, bsz, seq_len, device=q.device, max_seqlen=max_seqlen
)
result = flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens,
cu_seqlens_k=cu_seqlens,
max_seqlen_q=max_seqlen,
max_seqlen_k=max_seqlen,
softmax_scale=softmax_scale,
)
# FA4 CUTE returns (output, lse) in newer builds and bare output in older
# ones; downstream callers only consume the tensor.
return result[0] if isinstance(result, tuple) else result
def vision_attn_flashinfer_cudnn(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens: torch.Tensor | None,
softmax_scale: float | None = None,
max_seqlen: Any = None,
sequence_lengths: torch.Tensor | None = None,
workspace_buffer: torch.Tensor | None = None,
**_: Any,
) -> torch.Tensor:
"""cuDNN prefill backend. The caller (vision tower with cuDNN graph capture)
prepares ``cu_seqlens`` as three concatenated element-offset indptrs
``[qk | v | o]`` of length ``batch+1`` each, plus ``sequence_lengths`` per
real (un-padded) sequence and ``max_seqlen`` as the bucketed budget.
"""
if (
sequence_lengths is None
or max_seqlen is None
or not isinstance(cu_seqlens, torch.Tensor)
):
raise ValueError(
"flashinfer_cudnn needs sequence_lengths, max_seqlen, and packed indptrs"
)
# cuDNN wants a python int for the seq budget.
max_seqlen = int(
max_seqlen.item() if isinstance(max_seqlen, torch.Tensor) else max_seqlen
)
# Flatten (b, s, h, d) -> (b*s, h, d) when the caller hands us 4-D.
in_4d = q.dim() == 4
if in_4d:
b4 = q.shape[0]
q, k, v = (rearrange(t, "b s ... -> (b s) ...") for t in (q, k, v))
seq_lens = sequence_lengths.view(-1).to(device=q.device, dtype=torch.int32)
batch = seq_lens.numel()
packed = cu_seqlens.view(-1).to(device=q.device, dtype=torch.int32)
expected_packed = 3 * (batch + 1)
if packed.numel() != expected_packed:
raise ValueError(
f"Expected packed indptr length {expected_packed}, got {packed.numel()}."
)
chunk = batch + 1
qk_off = packed[:chunk].view(chunk, 1, 1, 1)
v_off = packed[chunk : 2 * chunk].view(chunk, 1, 1, 1)
o_off = packed[2 * chunk :].view(chunk, 1, 1, 1)
seq_lens_4d = seq_lens.view(batch, 1, 1, 1)
head_size = q.shape[-1]
scale = softmax_scale if softmax_scale is not None else head_size**-0.5
output, _ = cudnn_batch_prefill_with_kv_cache(
q,
k,
v,
scale,
workspace_buffer,
max_token_per_sequence=max_seqlen,
max_sequence_kv=max_seqlen,
actual_seq_lens_q=seq_lens_4d,
actual_seq_lens_kv=seq_lens_4d,
causal=False,
return_lse=True,
batch_offsets_q=qk_off,
batch_offsets_k=qk_off,
batch_offsets_v=v_off,
batch_offsets_o=o_off,
is_cuda_graph_compatible=True,
)
if in_4d:
output = rearrange(output, "(b s) h d -> b s h d", b=b4)
return output
_BACKENDS: dict[str, Callable[..., torch.Tensor]] = {
"triton_attn": vision_attn_triton,
"fa3": vision_attn_fa3,
"fa4": vision_attn_fa4,
"flashinfer_cudnn": vision_attn_flashinfer_cudnn,
}
def _default_multimodal_encoder_attn_backend() -> str:
"""Platform default backend name."""
if _is_nvidia:
if _platform.arch_version.major == 9: # Hopper SM90
return "fa3"
if _platform.arch_version.major == 10: # Blackwell SM100
return "fa4"
return "triton_attn"
if _is_amd:
return "triton_attn"
raise RuntimeError(
f"No default multimodal encoder attention backend for platform {_platform}; "
"set --mm-attention-backend explicitly."
)
@functools.lru_cache(maxsize=None)
def _resolve_backend(name: str | None) -> Callable[..., torch.Tensor]:
"""Resolve a backend name to its dispatch function.
``None`` falls back to the platform default; an unknown or platform-
incompatible name raises ValueError listing the registered backends.
Cached so a process logs the chosen backend exactly once per name.
"""
explicit = name is not None
if name is None:
name = _default_multimodal_encoder_attn_backend()
fn = _BACKENDS.get(name)
if fn is None:
raise ValueError(
f"Unknown multimodal encoder attention backend {name!r} "
f"(check --mm-attention-backend); available: {sorted(_BACKENDS)}"
)
if name in ("fa3", "fa4", "flashinfer_cudnn") and not _is_nvidia:
raise ValueError(
f"multimodal encoder attention backend {name!r} is only available "
"on NVIDIA CUDA"
)
if name == "fa3" and _platform.is_blackwell:
raise ValueError("The 'fa3' backend is not supported on Blackwell GPUs")
logger.info(
f"multimodal encoder attention backend: {name} "
f"({'override' if explicit else 'auto'})"
)
return fn
class MultimodalEncoderAttention(nn.Module):
r"""Multi-headed attention without a KV cache for multimodal encoders."""
def __init__(
self,
embed_dim: int,
num_heads: int,
mapping: Mapping,
head_size: int | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
proj_bias: bool = True,
qkv_bias: bool = True,
customized_position_embedding_applier: Callable[
[torch.Tensor, torch.Tensor, Any, Any], tuple[torch.Tensor, torch.Tensor]
] = None,
position_embedding_mode: str | None = None,
workspace_buffer: torch.Tensor | None = None,
mm_attention_backend: str | None = None,
):
super().__init__()
self.vision = mapping.vision
self.tp_size = self.vision.tp_size
self.tp_rank = self.vision.tp_rank
self.tp_group = self.vision.tp_group
self.head_size = head_size if head_size is not None else embed_dim // num_heads
self.num_attention_heads_per_partition = dist_utils.divide(
num_heads, self.tp_size
)
self.num_attention_kv_heads_per_partition = dist_utils.divide(
num_heads, self.tp_size
)
self.q_size = self.num_attention_heads_per_partition * self.head_size
self.kv_size = self.num_attention_kv_heads_per_partition * self.head_size
self.customized_position_embedding_applier = (
customized_position_embedding_applier
)
if position_embedding_mode not in (None, "complex_rope"):
raise ValueError(
f"Unknown vision position embedding mode: {position_embedding_mode}"
)
self.position_embedding_mode = position_embedding_mode
self._backend_fn = _resolve_backend(mm_attention_backend)
self._use_packed_qkv_complex_rotary = (
self._backend_fn is vision_attn_fa4
and self.position_embedding_mode == "complex_rope"
)
self._use_packed_qkv_rotary = (
self._backend_fn is vision_attn_fa4
and self.customized_position_embedding_applier is None
)
self._copy_v_after_packed_qkv_rotary = False
self._workspace_buffer = workspace_buffer
self.qkv_proj = QKVParallelLinear(
hidden_size=embed_dim,
head_size=self.head_size,
total_num_heads=num_heads,
total_num_kv_heads=num_heads,
bias=qkv_bias,
quant_config=quant_config,
tp_rank=self.tp_rank,
tp_size=self.tp_size,
tp_group=self.tp_group,
prefix=add_prefix("qkv_proj", prefix),
)
self.proj = RowParallelLinear(
input_size=num_heads * self.head_size,
output_size=embed_dim,
bias=proj_bias,
quant_config=quant_config,
tp_rank=self.tp_rank,
tp_size=self.tp_size,
tp_group=self.tp_group,
prefix=add_prefix("proj", prefix),
reduce_results=True,
)
def forward(
self,
x: torch.Tensor,
cu_seqlens: torch.Tensor | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
rotary_pos_emb_cos: torch.Tensor | None = None,
rotary_pos_emb_sin: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor:
r"""
Args:
x: [b, s, embed_dim]
cu_seqlens: [b]
Returns:
[b, s, head * head_size]
"""
if x.dim() == 2:
x = x.unsqueeze(0)
x_shape = x.shape
bsz, s, _ = x_shape
head = self.num_attention_heads_per_partition
kv_head = self.num_attention_kv_heads_per_partition
max_seqlen = kwargs["max_seqlen"] if "max_seqlen" in kwargs else None
sequence_lengths = (
kwargs["sequence_lengths"] if "sequence_lengths" in kwargs else None
)
qkv, _ = self.qkv_proj(x)
use_packed_qkv_rotary = (
self._use_packed_qkv_rotary
and position_embeddings is None
and rotary_pos_emb_cos is not None
and rotary_pos_emb_sin is not None
)
use_packed_qkv_complex_rotary = (
self._use_packed_qkv_complex_rotary and position_embeddings is not None
)
cos = rotary_pos_emb_cos if use_packed_qkv_rotary else None
sin = rotary_pos_emb_sin if use_packed_qkv_rotary else None
if use_packed_qkv_rotary:
if cos.size(-1) * 2 == self.head_size:
cos = torch.cat([cos, cos], dim=-1)
sin = torch.cat([sin, sin], dim=-1)
q, k, v = packed_qkv_neox_rotary(
qkv,
self.q_size,
self.kv_size,
head,
self.head_size,
cos,
sin,
copy_v=self._copy_v_after_packed_qkv_rotary,
)
elif use_packed_qkv_complex_rotary:
q, k, v = packed_qkv_complex_rotary(
qkv,
self.q_size,
self.kv_size,
head,
self.head_size,
position_embeddings,
copy_v=self._copy_v_after_packed_qkv_rotary,
)
else:
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q = q.reshape(bsz * s, head, -1)
k = k.reshape(bsz * s, kv_head, -1)
v = v.reshape(bsz * s, kv_head, -1)
cos = None
sin = None
if position_embeddings is not None:
if self.customized_position_embedding_applier is not None:
q, k = self.customized_position_embedding_applier(
q, k, position_embeddings, x_shape
)
else:
cos, sin = position_embeddings
elif rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:
cos = rotary_pos_emb_cos
sin = rotary_pos_emb_sin
if (
not use_packed_qkv_rotary
and not use_packed_qkv_complex_rotary
and cos is not None
and sin is not None
):
original_shape = q.shape
# [total_tokens, head, head_size]
q = q.view(-1, head, self.head_size)
k = k.view(-1, head, self.head_size)
if cos.size(-1) * 2 == self.head_size:
cos = torch.cat([cos, cos], dim=-1)
sin = torch.cat([sin, sin], dim=-1)
q, k = apply_rotary_pos_emb_native(q, k, cos, sin)
q = q.view(original_shape)
k = k.view(original_shape)
q, k, v = [
rearrange(t, "b s ... -> (b s) ...") if t.dim() == 4 else t
for t in (q, k, v)
]
output = self._backend_fn(
q,
k,
v,
cu_seqlens=cu_seqlens,
bsz=bsz,
seq_len=s,
max_seqlen=max_seqlen,
sequence_lengths=sequence_lengths,
workspace_buffer=self._workspace_buffer,
)
output = rearrange(output, "(b s) ... h d -> b s ... (h d)", b=bsz)
output, _ = self.proj(output)
return output
# Compatibility alias for existing vision tower implementations.
VisionAttention = MultimodalEncoderAttention