94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
1240 lines
49 KiB
Python
1240 lines
49 KiB
Python
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
import functools
|
|
import os
|
|
from collections.abc import Sequence
|
|
from contextlib import nullcontext
|
|
from typing import Type
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
from torch.nn.attention import SDPBackend, sdpa_kernel
|
|
|
|
from sglang.jit_kernel.diffusion.triton.varlen_pack_pad import (
|
|
build_inv_indices,
|
|
fused_pack_qkv,
|
|
fused_scatter_to_padded,
|
|
)
|
|
from sglang.jit_kernel.flash_attention import flash_attn_varlen_func
|
|
from sglang.multimodal_gen.runtime.breakable_cuda_graph.replay_token import (
|
|
get_current_replay_token,
|
|
)
|
|
from sglang.multimodal_gen.runtime.distributed.communication_op import (
|
|
sequence_model_parallel_all_gather,
|
|
sequence_model_parallel_all_to_all_4D,
|
|
)
|
|
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
|
|
get_ring_parallel_world_size,
|
|
get_sequence_parallel_world_size,
|
|
get_sp_group,
|
|
get_sp_parallel_rank,
|
|
get_sp_world_size,
|
|
get_ulysses_parallel_world_size,
|
|
)
|
|
from sglang.multimodal_gen.runtime.layers.attention.backends import (
|
|
flash_attn as _fa_backend,
|
|
)
|
|
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
|
|
AttentionImpl,
|
|
wrap_attention_impl_forward,
|
|
)
|
|
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
|
|
from sglang.multimodal_gen.runtime.layers.attention.turbo_layer import (
|
|
async_a2a_communicate,
|
|
)
|
|
from sglang.multimodal_gen.runtime.layers.usp import (
|
|
_usp_input_all_to_all,
|
|
_usp_input_all_to_all_varlen,
|
|
_usp_output_all_to_all,
|
|
_usp_output_all_to_all_varlen,
|
|
ring_attn,
|
|
)
|
|
from sglang.multimodal_gen.runtime.managers.forward_context import (
|
|
ForwardContext,
|
|
get_forward_context,
|
|
)
|
|
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
|
from sglang.multimodal_gen.utils import get_compute_dtype
|
|
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph import (
|
|
eager_on_graph,
|
|
is_in_breakable_cuda_graph,
|
|
)
|
|
|
|
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS = [
|
|
SDPBackend.CUDNN_ATTENTION,
|
|
SDPBackend.FLASH_ATTENTION,
|
|
SDPBackend.EFFICIENT_ATTENTION,
|
|
SDPBackend.MATH,
|
|
]
|
|
|
|
# Set ``SGLANG_VARLEN_FA=0`` to disable the varlen FA fast path in
|
|
# USPAttention masked branch and fall back to SDPA.
|
|
_VARLEN_FA_ENABLED = os.environ.get("SGLANG_VARLEN_FA", "1") != "0"
|
|
|
|
|
|
def build_varlen_mask_meta(
|
|
key_mask: torch.Tensor,
|
|
) -> dict:
|
|
"""Build varlen FA metadata from a ``[B, S]`` key mask.
|
|
|
|
Returns ``cu_seqlens``, ``indices``, ``inv_indices``, ``max_seqlen``.
|
|
Passing the result via ``joint_attention_kwargs`` opts the caller into
|
|
``USPAttention``'s varlen FA fast path, which zero-fills masked query
|
|
rows on output; only use when those rows are dropped or ignored
|
|
downstream.
|
|
"""
|
|
assert key_mask.dim() == 2, "key_mask must be [B, S]"
|
|
bs, seq = key_mask.shape
|
|
bool_mask = key_mask.to(dtype=torch.bool)
|
|
valid_lens = bool_mask.sum(dim=1, dtype=torch.int32)
|
|
indices = bool_mask.reshape(-1).nonzero(as_tuple=False).flatten()
|
|
cu_seqlens = torch.zeros(bs + 1, dtype=torch.int32, device=key_mask.device)
|
|
cu_seqlens[1:] = torch.cumsum(valid_lens, dim=0)
|
|
inv_indices = build_inv_indices(indices, bs * seq)
|
|
|
|
return {
|
|
"cu_seqlens": cu_seqlens,
|
|
"indices": indices,
|
|
"inv_indices": inv_indices,
|
|
"max_seqlen": seq, # upper bound; FA varlen uses cu_seqlens for actual ranges
|
|
}
|
|
|
|
|
|
def build_varlen_mask_meta_from_lengths(
|
|
lengths: Sequence[int],
|
|
max_seqlen: int,
|
|
device: torch.device,
|
|
) -> dict:
|
|
"""Build varlen FA metadata for prefix-valid masks without CUDA nonzero.
|
|
|
|
This is equivalent to ``build_varlen_mask_meta`` for masks where row ``i`` is
|
|
true on ``[:lengths[i]]`` and false afterwards. Keeping the lengths on the
|
|
host lets callers avoid a GPU ``nonzero``/dynamic-shape path while still
|
|
producing the same packed indices.
|
|
"""
|
|
|
|
return build_varlen_mask_meta_from_ranges(
|
|
[[(0, int(length))] for length in lengths],
|
|
max_seqlen=max_seqlen,
|
|
device=device,
|
|
)
|
|
|
|
|
|
def build_varlen_mask_meta_from_ranges(
|
|
valid_ranges: Sequence[Sequence[tuple[int, int]]],
|
|
max_seqlen: int,
|
|
device: torch.device,
|
|
) -> dict:
|
|
"""Build varlen FA metadata from host-side valid token ranges.
|
|
|
|
``valid_ranges[i]`` contains half-open intervals in row-local coordinates.
|
|
The intervals are packed in the provided order, matching the flattened
|
|
``nonzero`` order for ordinary left-to-right masks.
|
|
"""
|
|
|
|
range_values = [
|
|
[(int(start), int(end)) for start, end in row_ranges]
|
|
for row_ranges in valid_ranges
|
|
]
|
|
if any(
|
|
start < 0 or end < start or end > max_seqlen
|
|
for row_ranges in range_values
|
|
for start, end in row_ranges
|
|
):
|
|
raise ValueError(
|
|
f"All ranges must be within [0, {max_seqlen}], got {range_values}"
|
|
)
|
|
|
|
bs = len(range_values)
|
|
length_values = [
|
|
sum(end - start for start, end in row_ranges) for row_ranges in range_values
|
|
]
|
|
valid_lens = torch.as_tensor(length_values, dtype=torch.int32, device=device)
|
|
cu_seqlens = torch.zeros(bs + 1, dtype=torch.int32, device=device)
|
|
cu_seqlens[1:] = torch.cumsum(valid_lens, dim=0)
|
|
|
|
index_parts = [
|
|
torch.arange(
|
|
row * max_seqlen + start,
|
|
row * max_seqlen + end,
|
|
dtype=torch.long,
|
|
device=device,
|
|
)
|
|
for row, row_ranges in enumerate(range_values)
|
|
for start, end in row_ranges
|
|
if end > start
|
|
]
|
|
if index_parts:
|
|
indices = torch.cat(index_parts, dim=0)
|
|
else:
|
|
indices = torch.empty((0,), dtype=torch.long, device=device)
|
|
inv_indices = build_inv_indices(indices, bs * max_seqlen)
|
|
|
|
return {
|
|
"cu_seqlens": cu_seqlens,
|
|
"indices": indices,
|
|
"inv_indices": inv_indices,
|
|
"max_seqlen": max_seqlen,
|
|
}
|
|
|
|
|
|
class DynamicVarlenMaskMeta:
|
|
"""Replay-local builder for varlen attention metadata.
|
|
|
|
BCG attention break points capture Python kwargs once. Passing a plain
|
|
``attn_mask_meta`` dict would replay stale cu_seqlens/indices when the same
|
|
graph bucket is reused for a different prompt length. This helper keeps only
|
|
replay-local metadata and rebuilds it from the current ``attn_mask`` tensor
|
|
on the first attention block of each graph replay.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._cache_key = None
|
|
self._meta = None
|
|
|
|
def resolve(self, attn_mask: torch.Tensor | None) -> dict | None:
|
|
if attn_mask is None:
|
|
self._cache_key = None
|
|
self._meta = None
|
|
return None
|
|
|
|
replay_token = get_current_replay_token()
|
|
if replay_token is None:
|
|
cache_key = ("capture", id(attn_mask), tuple(attn_mask.shape))
|
|
else:
|
|
cache_key = ("replay", replay_token, tuple(attn_mask.shape))
|
|
|
|
if cache_key != self._cache_key:
|
|
self._meta = build_varlen_mask_meta(attn_mask)
|
|
self._cache_key = cache_key
|
|
return self._meta
|
|
|
|
|
|
class UlyssesAttention(nn.Module):
|
|
"""Ulysses-style SequenceParallelism attention layer."""
|
|
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
num_kv_heads: int | None = None,
|
|
softmax_scale: float | None = None,
|
|
causal: bool = False,
|
|
supported_attention_backends: set[AttentionBackendEnum] | None = None,
|
|
prefix: str = "",
|
|
**extra_impl_args,
|
|
) -> None:
|
|
super().__init__()
|
|
if softmax_scale is None:
|
|
self.softmax_scale = head_size**-0.5
|
|
else:
|
|
self.softmax_scale = softmax_scale
|
|
|
|
if num_kv_heads is None:
|
|
num_kv_heads = num_heads
|
|
|
|
dtype = get_compute_dtype()
|
|
attn_backend = get_attn_backend(
|
|
head_size, dtype, supported_attention_backends=supported_attention_backends
|
|
)
|
|
impl_cls = attn_backend.get_impl_cls()
|
|
|
|
self.attn_impl = impl_cls(
|
|
num_heads=num_heads,
|
|
head_size=head_size,
|
|
causal=causal,
|
|
softmax_scale=self.softmax_scale,
|
|
num_kv_heads=num_kv_heads,
|
|
prefix=f"{prefix}.impl",
|
|
**extra_impl_args,
|
|
)
|
|
wrap_attention_impl_forward(self.attn_impl)
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.num_kv_heads = num_kv_heads
|
|
self.backend = attn_backend.get_enum()
|
|
self.dtype = dtype
|
|
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
replicated_q: torch.Tensor | None = None,
|
|
replicated_k: torch.Tensor | None = None,
|
|
replicated_v: torch.Tensor | None = None,
|
|
seq_lens: list[int] | None = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
|
"""Forward pass for distributed attention.
|
|
|
|
Args:
|
|
q (torch.Tensor): Query tensor [batch_size, seq_len, num_heads, head_dim]
|
|
k (torch.Tensor): Key tensor [batch_size, seq_len, num_heads, head_dim]
|
|
v (torch.Tensor): Value tensor [batch_size, seq_len, num_heads, head_dim]
|
|
replicated_q (Optional[torch.Tensor]): Replicated query tensor, typically for text tokens
|
|
replicated_k (Optional[torch.Tensor]): Replicated key tensor
|
|
replicated_v (Optional[torch.Tensor]): Replicated value tensor
|
|
|
|
Returns:
|
|
Tuple[torch.Tensor, Optional[torch.Tensor]]: A tuple containing:
|
|
- o (torch.Tensor): Output tensor after attention for the main sequence
|
|
- replicated_o (Optional[torch.Tensor]): Output tensor for replicated tokens, if provided
|
|
"""
|
|
# Check input shapes
|
|
assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"
|
|
batch_size, seq_len, num_heads, head_dim = q.shape
|
|
local_rank = get_sp_parallel_rank()
|
|
world_size = get_sp_world_size()
|
|
|
|
forward_context: ForwardContext = get_forward_context()
|
|
ctx_attn_metadata = forward_context.attn_metadata
|
|
|
|
if seq_lens is not None:
|
|
assert (
|
|
replicated_q is None and replicated_k is None and replicated_v is None
|
|
), "Varlen Ulysses attention does not support replicated QKV"
|
|
|
|
# Stack QKV
|
|
qkv = torch.cat([q, k, v], dim=0) # [3, seq_len, num_heads, head_dim]
|
|
|
|
# Redistribute heads across sequence dimension
|
|
if seq_lens is None:
|
|
qkv = sequence_model_parallel_all_to_all_4D(
|
|
qkv, scatter_dim=2, gather_dim=1
|
|
)
|
|
else:
|
|
qkv = _usp_input_all_to_all_varlen(qkv, seq_lens, head_dim=2)
|
|
# Apply backend-specific preprocess_qkv
|
|
qkv = self.attn_impl.preprocess_qkv(qkv, ctx_attn_metadata)
|
|
|
|
# Concatenate with replicated QKV if provided
|
|
if replicated_q is not None:
|
|
assert replicated_k is not None and replicated_v is not None
|
|
replicated_qkv = torch.cat(
|
|
[replicated_q, replicated_k, replicated_v], dim=0
|
|
) # [3, seq_len, num_heads, head_dim]
|
|
heads_per_rank = num_heads // world_size
|
|
replicated_qkv = replicated_qkv[
|
|
:, :, local_rank * heads_per_rank : (local_rank + 1) * heads_per_rank
|
|
]
|
|
qkv = torch.cat([qkv, replicated_qkv], dim=1)
|
|
|
|
q, k, v = qkv.chunk(3, dim=0)
|
|
|
|
output = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
|
|
# Redistribute back if using sequence parallelism
|
|
replicated_output = None
|
|
if replicated_q is not None:
|
|
replicated_output = output[:, seq_len * world_size :]
|
|
output = output[:, : seq_len * world_size]
|
|
# TODO: make this asynchronous
|
|
replicated_output = sequence_model_parallel_all_gather(
|
|
replicated_output.contiguous(), dim=2
|
|
)
|
|
# Apply backend-specific postprocess_output
|
|
output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)
|
|
|
|
if seq_lens is None:
|
|
output = sequence_model_parallel_all_to_all_4D(
|
|
output, scatter_dim=1, gather_dim=2
|
|
)
|
|
else:
|
|
output = _usp_output_all_to_all_varlen(output, seq_lens, head_dim=2)
|
|
return output, replicated_output
|
|
|
|
|
|
class UlyssesAttention_VSA(UlyssesAttention):
|
|
"""Distributed attention layer with VSA support."""
|
|
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
replicated_q: torch.Tensor | None = None,
|
|
replicated_k: torch.Tensor | None = None,
|
|
replicated_v: torch.Tensor | None = None,
|
|
gate_compress: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
"""Forward pass for distributed attention.
|
|
|
|
Args:
|
|
q (torch.Tensor): Query tensor [batch_size, seq_len, num_heads, head_dim]
|
|
k (torch.Tensor): Key tensor [batch_size, seq_len, num_heads, head_dim]
|
|
v (torch.Tensor): Value tensor [batch_size, seq_len, num_heads, head_dim]
|
|
gate_compress (torch.Tensor): Gate compress tensor [batch_size, seq_len, num_heads, head_dim]
|
|
replicated_q (Optional[torch.Tensor]): Replicated query tensor, typically for text tokens
|
|
replicated_k (Optional[torch.Tensor]): Replicated key tensor
|
|
replicated_v (Optional[torch.Tensor]): Replicated value tensor
|
|
|
|
Returns:
|
|
Tuple[torch.Tensor, Optional[torch.Tensor]]: A tuple containing:
|
|
- o (torch.Tensor): Output tensor after attention for the main sequence
|
|
- replicated_o (Optional[torch.Tensor]): Output tensor for replicated tokens, if provided
|
|
"""
|
|
# Check text tokens are not supported for VSA now
|
|
assert (
|
|
replicated_q is None and replicated_k is None and replicated_v is None
|
|
), "Replicated QKV is not supported for VSA now"
|
|
# Check input shapes
|
|
assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"
|
|
|
|
forward_context: ForwardContext = get_forward_context()
|
|
ctx_attn_metadata = forward_context.attn_metadata
|
|
|
|
# Stack QKV
|
|
qkvg = torch.cat(
|
|
[q, k, v, gate_compress], dim=0
|
|
) # [3, seq_len, num_heads, head_dim]
|
|
|
|
# Redistribute heads across sequence dimension
|
|
qkvg = sequence_model_parallel_all_to_all_4D(qkvg, scatter_dim=2, gather_dim=1)
|
|
|
|
qkvg = self.attn_impl.preprocess_qkv(qkvg, ctx_attn_metadata)
|
|
|
|
q, k, v, gate_compress = qkvg.chunk(4, dim=0)
|
|
output = self.attn_impl.forward(
|
|
q, k, v, gate_compress=gate_compress, attn_metadata=ctx_attn_metadata
|
|
) # type: ignore[call-arg]
|
|
|
|
# Apply backend-specific postprocess_output
|
|
output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)
|
|
|
|
output = sequence_model_parallel_all_to_all_4D(
|
|
output, scatter_dim=1, gather_dim=2
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
class LocalAttention(nn.Module):
|
|
"""Attention layer."""
|
|
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
num_kv_heads: int | None = None,
|
|
softmax_scale: float | None = None,
|
|
causal: bool = False,
|
|
supported_attention_backends: set[AttentionBackendEnum] | None = None,
|
|
compute_dtype: torch.dtype | None = None,
|
|
**extra_impl_args,
|
|
) -> None:
|
|
super().__init__()
|
|
if softmax_scale is None:
|
|
self.softmax_scale = head_size**-0.5
|
|
else:
|
|
self.softmax_scale = softmax_scale
|
|
if num_kv_heads is None:
|
|
num_kv_heads = num_heads
|
|
|
|
dtype = compute_dtype or get_compute_dtype()
|
|
attn_backend = get_attn_backend(
|
|
head_size, dtype, supported_attention_backends=supported_attention_backends
|
|
)
|
|
impl_cls = attn_backend.get_impl_cls()
|
|
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
|
|
self.attn_impl = impl_cls(
|
|
num_heads=num_heads,
|
|
head_size=head_size,
|
|
softmax_scale=self.softmax_scale,
|
|
num_kv_heads=num_kv_heads,
|
|
causal=causal,
|
|
**extra_impl_args,
|
|
)
|
|
wrap_attention_impl_forward(self.attn_impl)
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.num_kv_heads = num_kv_heads
|
|
self.backend = attn_backend.get_enum()
|
|
self.dtype = dtype
|
|
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
attn_mask: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
"""
|
|
Apply local attention between query, key and value tensors.
|
|
|
|
Args:
|
|
q (torch.Tensor): Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
|
k (torch.Tensor): Key tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
|
v (torch.Tensor): Value tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
|
|
|
Returns:
|
|
torch.Tensor: Output tensor after local attention
|
|
"""
|
|
# Check input shapes
|
|
assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"
|
|
|
|
forward_context: ForwardContext = get_forward_context()
|
|
ctx_attn_metadata = forward_context.attn_metadata
|
|
|
|
if attn_mask is not None:
|
|
q_ = q.transpose(1, 2)
|
|
k_ = k.transpose(1, 2)
|
|
v_ = v.transpose(1, 2)
|
|
|
|
if torch.is_floating_point(attn_mask):
|
|
mask = attn_mask.to(dtype=q_.dtype, device=q_.device)
|
|
if mask.dim() == 2:
|
|
mask = mask[:, None, None, :]
|
|
elif mask.dim() == 3:
|
|
mask = mask[:, None, :, :]
|
|
else:
|
|
mask = attn_mask.to(dtype=q_.dtype, device=q_.device)
|
|
if mask.dim() == 2:
|
|
mask = mask[:, None, None, :]
|
|
elif mask.dim() == 3:
|
|
mask = mask[:, None, :, :]
|
|
mask = (mask - 1.0) * torch.finfo(q_.dtype).max
|
|
|
|
if q_.shape[1] != k_.shape[1]:
|
|
repeat_factor = q_.shape[1] // k_.shape[1]
|
|
k_ = k_.repeat_interleave(repeat_factor, dim=1)
|
|
v_ = v_.repeat_interleave(repeat_factor, dim=1)
|
|
|
|
sdpa_context = (
|
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
|
else nullcontext()
|
|
)
|
|
attn_kwargs = {
|
|
"attn_mask": mask,
|
|
"dropout_p": 0.0,
|
|
"is_causal": False,
|
|
"scale": self.softmax_scale,
|
|
}
|
|
with sdpa_context:
|
|
return torch.nn.functional.scaled_dot_product_attention(
|
|
q_,
|
|
k_,
|
|
v_,
|
|
**attn_kwargs,
|
|
).transpose(1, 2)
|
|
|
|
output = self.attn_impl.forward(q, k, v, attn_metadata=ctx_attn_metadata)
|
|
return output
|
|
|
|
|
|
class USPAttention(nn.Module):
|
|
"""
|
|
Ulysses Sequence Parallelism with Ring Attention.
|
|
|
|
This class implements the USP algorithm, which is a combination of
|
|
Ulysses-style all-to-all communication for sequence-head dimension sharding
|
|
and Ring Attention for fine-grained sequence parallelism within subgroups.
|
|
"""
|
|
|
|
_usp_a2a_stream = None
|
|
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
num_kv_heads: int | None = None,
|
|
softmax_scale: float | None = None,
|
|
causal: bool = False,
|
|
supported_attention_backends: set[AttentionBackendEnum] | None = None,
|
|
prefix: str = "",
|
|
dropout_rate: float = 0.0,
|
|
skip_sequence_parallel: bool = False,
|
|
enable_packed_qkv_input_a2a: bool = False,
|
|
**extra_impl_args,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
skip_sequence_parallel:
|
|
when KV is replicated across all SP ranks (e.g. cross-attention to
|
|
text/image encoder outputs), the full USP pipeline is redundant:
|
|
each rank's local Q shard can attend directly to the locally-held
|
|
full KV without any collective communication.
|
|
"""
|
|
super().__init__()
|
|
if softmax_scale is None:
|
|
self.softmax_scale = head_size**-0.5
|
|
else:
|
|
self.softmax_scale = softmax_scale
|
|
|
|
if num_kv_heads is None:
|
|
num_kv_heads = num_heads
|
|
|
|
dtype = get_compute_dtype()
|
|
attn_backend = get_attn_backend(
|
|
head_size, dtype, supported_attention_backends=supported_attention_backends
|
|
)
|
|
if get_ring_parallel_world_size() > 1:
|
|
backend_enum = attn_backend.get_enum()
|
|
if backend_enum not in (
|
|
AttentionBackendEnum.FA,
|
|
AttentionBackendEnum.SAGE_ATTN,
|
|
):
|
|
raise RuntimeError(
|
|
f"Ring Attention is only supported for FlashAttention or SageAttention backends, "
|
|
f"but got {backend_enum.name}. "
|
|
f"Please ensure your platform supports these backends."
|
|
)
|
|
impl_cls: Type[AttentionImpl] = attn_backend.get_impl_cls()
|
|
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
|
|
self.attn_impl = impl_cls(
|
|
num_heads=num_heads,
|
|
head_size=head_size,
|
|
causal=causal,
|
|
softmax_scale=self.softmax_scale,
|
|
num_kv_heads=num_kv_heads,
|
|
prefix=f"{prefix}.impl",
|
|
**extra_impl_args,
|
|
)
|
|
wrap_attention_impl_forward(self.attn_impl)
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.num_kv_heads = num_kv_heads
|
|
self.backend = attn_backend.get_enum()
|
|
self.dtype = dtype
|
|
self.causal = causal
|
|
self.dropout_p = dropout_rate
|
|
|
|
self.skip_sequence_parallel = skip_sequence_parallel
|
|
self.enable_packed_qkv_input_a2a = bool(enable_packed_qkv_input_a2a)
|
|
|
|
def _get_usp_a2a_stream(self):
|
|
if USPAttention._usp_a2a_stream is None:
|
|
USPAttention._usp_a2a_stream = torch.get_device_module().Stream()
|
|
return USPAttention._usp_a2a_stream
|
|
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
attn_mask: torch.Tensor | None = None,
|
|
num_replicated_prefix: int = 0,
|
|
num_replicated_suffix: int = 0,
|
|
num_replicated_kv_prefix: int = 0,
|
|
skip_sequence_parallel_override: bool = False,
|
|
attn_mask_meta: dict | None = None,
|
|
) -> torch.Tensor:
|
|
"""
|
|
Forward pass for USPAttention.
|
|
|
|
q, k, v: [B, S_local, H, D]
|
|
num_replicated_prefix: number of leading tokens in q/k/v that are
|
|
replicated (identical) across all SP ranks, e.g. text tokens
|
|
in FLUX joint attention. These tokens are excluded from the
|
|
Ulysses all-to-all so they appear exactly once in the gathered
|
|
sequence, preserving correct attention weights.
|
|
num_replicated_suffix: number of trailing tokens in q/k/v that are
|
|
replicated across all SP ranks, e.g. caption tokens appended
|
|
after image tokens in Z-Image joint attention.
|
|
num_replicated_kv_prefix: number of leading tokens in k/v only
|
|
(not q) that are replicated across all SP ranks. Used for
|
|
cross-attention where the keys/values include a fully-replicated
|
|
conditioning prefix (e.g. cached text K/V) followed by a
|
|
sequence-sharded suffix (image tokens). Q has no replicated
|
|
portion and is fully sequence-sharded.
|
|
attn_mask_meta: optional metadata for the varlen FA fast path.
|
|
Callers may pass ``build_varlen_mask_meta(attn_mask)`` or a
|
|
known contiguous padding gap. Masked query rows are zero-filled
|
|
on output (differs from SDPA semantics).
|
|
|
|
Note: Replicated tensors are not supported in this implementation.
|
|
When skip_sequence_parallel=True (set at construction time), all SP
|
|
communication is bypassed — use this for cross-attention where KV
|
|
content is replicated across ranks (distinct from replicated_k/v args).
|
|
"""
|
|
forward_context: ForwardContext = get_forward_context()
|
|
ctx_attn_metadata = forward_context.attn_metadata
|
|
effective_skip_sp = (
|
|
self.skip_sequence_parallel or skip_sequence_parallel_override
|
|
)
|
|
if isinstance(attn_mask_meta, DynamicVarlenMaskMeta):
|
|
attn_mask_meta = attn_mask_meta.resolve(attn_mask)
|
|
|
|
# Tail-pad meta alone (sp_shard.tail_attn_meta; mask derivable from the
|
|
# pad span) also opts into the masked SP branch. gap_* = legacy alias.
|
|
meta_pad_start = meta_pad_end = None
|
|
if attn_mask_meta is not None:
|
|
meta_pad_start = attn_mask_meta.get(
|
|
"pad_start", attn_mask_meta.get("gap_start")
|
|
)
|
|
meta_pad_end = attn_mask_meta.get("pad_end", attn_mask_meta.get("gap_end"))
|
|
meta_only_pad = (
|
|
attn_mask is None
|
|
and meta_pad_start is not None
|
|
and not effective_skip_sp
|
|
and get_sequence_parallel_world_size() > 1
|
|
)
|
|
if attn_mask is not None or meta_only_pad:
|
|
|
|
def _prepare_sdpa_mask(
|
|
mask: torch.Tensor, *, dtype: torch.dtype, device: torch.device
|
|
) -> torch.Tensor:
|
|
mask = mask.to(device=device)
|
|
if torch.is_floating_point(mask):
|
|
mask = mask.to(dtype=dtype)
|
|
if mask.dim() == 2:
|
|
mask = mask[:, None, None, :]
|
|
elif mask.dim() == 3:
|
|
mask = mask[:, None, :, :]
|
|
return mask
|
|
|
|
mask = mask.to(dtype=dtype)
|
|
if mask.dim() == 2:
|
|
mask = mask[:, None, None, :]
|
|
elif mask.dim() == 3:
|
|
mask = mask[:, None, :, :]
|
|
return (mask - 1.0) * torch.finfo(dtype).max
|
|
|
|
sp_world_size = get_sequence_parallel_world_size()
|
|
if effective_skip_sp or sp_world_size == 1:
|
|
# Varlen FA fast path: SDPA with a non-None mask falls back
|
|
# to cutlassF. Meta-gated to opt in callers that drop masked
|
|
# query rows downstream (zero-filled on output, differs from
|
|
# SDPA semantics). Without meta, fall through to SDPA.
|
|
if (
|
|
_VARLEN_FA_ENABLED
|
|
and attn_mask_meta is not None
|
|
and self.backend == AttentionBackendEnum.FA
|
|
and attn_mask.dim() == 2
|
|
and attn_mask.dtype
|
|
in (torch.bool, torch.uint8, torch.int32, torch.int64)
|
|
and q.device.type == "cuda"
|
|
and attn_mask.device == q.device
|
|
and q.dtype in (torch.float16, torch.bfloat16)
|
|
and q.shape[:2] == attn_mask.shape == k.shape[:2] == v.shape[:2]
|
|
):
|
|
bs, seq = q.shape[0], q.shape[1]
|
|
indices = attn_mask_meta["indices"]
|
|
cu_seqlens = attn_mask_meta["cu_seqlens"]
|
|
max_seqlen = attn_mask_meta["max_seqlen"]
|
|
inv_indices = attn_mask_meta["inv_indices"]
|
|
# Guard against a caller passing meta from a different
|
|
# mask shape (silent corruption otherwise).
|
|
assert (
|
|
inv_indices.shape[0] == bs * seq
|
|
), "attn_mask_meta shape does not match attn_mask"
|
|
# All-False mask: FA varlen rejects zero-length input.
|
|
# Fall through to SDPA which handles it via broadcast.
|
|
# (Joint attention with an image side is always non-empty
|
|
# in practice, so this only guards malformed inputs.)
|
|
if indices.shape[0] > 0:
|
|
q_unpad, k_unpad, v_unpad = fused_pack_qkv(q, k, v, indices)
|
|
out_unpad = flash_attn_varlen_func(
|
|
q=q_unpad,
|
|
k=k_unpad,
|
|
v=v_unpad,
|
|
cu_seqlens_q=cu_seqlens,
|
|
cu_seqlens_k=cu_seqlens,
|
|
max_seqlen_q=max_seqlen,
|
|
max_seqlen_k=max_seqlen,
|
|
softmax_scale=self.softmax_scale,
|
|
causal=False,
|
|
ver=_fa_backend.fa_ver,
|
|
)
|
|
return fused_scatter_to_padded(out_unpad, inv_indices, bs, seq)
|
|
|
|
q_ = q.transpose(1, 2)
|
|
k_ = k.transpose(1, 2)
|
|
v_ = v.transpose(1, 2)
|
|
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
|
|
sdpa_context = (
|
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
|
else nullcontext()
|
|
)
|
|
with sdpa_context:
|
|
return torch.nn.functional.scaled_dot_product_attention(
|
|
q_,
|
|
k_,
|
|
v_,
|
|
attn_mask=mask,
|
|
dropout_p=0.0,
|
|
is_causal=False,
|
|
scale=self.softmax_scale,
|
|
).transpose(1, 2)
|
|
|
|
if get_ring_parallel_world_size() > 1:
|
|
raise NotImplementedError(
|
|
"USPAttention masked path does not support ring parallelism yet."
|
|
)
|
|
if attn_mask is not None and attn_mask.dim() != 2:
|
|
raise NotImplementedError(
|
|
"USPAttention masked SP path currently expects a [B, S_local] key mask."
|
|
)
|
|
|
|
sp_size = get_ulysses_parallel_world_size()
|
|
if sp_size > 1:
|
|
q = _usp_input_all_to_all(q, head_dim=2)
|
|
k = _usp_input_all_to_all(k, head_dim=2)
|
|
v = _usp_input_all_to_all(v, head_dim=2)
|
|
|
|
if (
|
|
_VARLEN_FA_ENABLED
|
|
and self.backend == AttentionBackendEnum.FA
|
|
and meta_pad_start is not None
|
|
and meta_pad_end is not None
|
|
and meta_pad_end > meta_pad_start
|
|
and q.device.type == "cuda"
|
|
and q.dtype in (torch.float16, torch.bfloat16)
|
|
):
|
|
bs, seq = q.shape[0], q.shape[1]
|
|
assert 0 <= meta_pad_start < meta_pad_end <= seq
|
|
cu_tail = attn_mask_meta.get("cu_seqlens_tail")
|
|
if cu_tail is not None and meta_pad_end == seq:
|
|
# Zero-copy tail path: run varlen FA straight over the
|
|
# padded layout, each row split into [valid | pad] segments
|
|
# (contiguous reshapes only, no repacking).
|
|
assert (
|
|
cu_tail.numel() == 2 * bs + 1
|
|
), "cu_seqlens_tail does not match the batch size"
|
|
out = flash_attn_varlen_func(
|
|
q=q.reshape(bs * seq, *q.shape[2:]),
|
|
k=k.reshape(bs * seq, *k.shape[2:]),
|
|
v=v.reshape(bs * seq, *v.shape[2:]),
|
|
cu_seqlens_q=cu_tail,
|
|
cu_seqlens_k=cu_tail,
|
|
max_seqlen_q=attn_mask_meta["max_seqlen_tail"],
|
|
max_seqlen_k=attn_mask_meta["max_seqlen_tail"],
|
|
softmax_scale=self.softmax_scale,
|
|
causal=False,
|
|
ver=_fa_backend.fa_ver,
|
|
).reshape(bs, seq, *q.shape[2:])
|
|
# Match the packed paths: masked query rows read as zeros.
|
|
out[:, meta_pad_start:].zero_()
|
|
if sp_size > 1:
|
|
out = _usp_output_all_to_all(out, head_dim=2)
|
|
return out
|
|
valid_seq = seq - (meta_pad_end - meta_pad_start)
|
|
q_dense = torch.cat([q[:, :meta_pad_start], q[:, meta_pad_end:]], dim=1)
|
|
k_dense = torch.cat([k[:, :meta_pad_start], k[:, meta_pad_end:]], dim=1)
|
|
v_dense = torch.cat([v[:, :meta_pad_start], v[:, meta_pad_end:]], dim=1)
|
|
cu_seqlens = torch.arange(
|
|
0,
|
|
(bs + 1) * valid_seq,
|
|
valid_seq,
|
|
dtype=torch.int32,
|
|
device=q.device,
|
|
)
|
|
out_dense = flash_attn_varlen_func(
|
|
q=q_dense.reshape(bs * valid_seq, *q.shape[2:]),
|
|
k=k_dense.reshape(bs * valid_seq, *k.shape[2:]),
|
|
v=v_dense.reshape(bs * valid_seq, *v.shape[2:]),
|
|
cu_seqlens_q=cu_seqlens,
|
|
cu_seqlens_k=cu_seqlens,
|
|
max_seqlen_q=valid_seq,
|
|
max_seqlen_k=valid_seq,
|
|
softmax_scale=self.softmax_scale,
|
|
causal=False,
|
|
ver=_fa_backend.fa_ver,
|
|
).reshape(bs, valid_seq, *q.shape[2:])
|
|
gap_out = out_dense.new_zeros(
|
|
bs,
|
|
meta_pad_end - meta_pad_start,
|
|
out_dense.shape[2],
|
|
out_dense.shape[3],
|
|
)
|
|
out = torch.cat(
|
|
[
|
|
out_dense[:, :meta_pad_start],
|
|
gap_out,
|
|
out_dense[:, meta_pad_start:],
|
|
],
|
|
dim=1,
|
|
)
|
|
if sp_size > 1:
|
|
out = _usp_output_all_to_all(out, head_dim=2)
|
|
return out
|
|
|
|
# If NCCL timeout/deadlock occurs here, check whether
|
|
# attn_mask is inconsistent across SP ranks (None on some, Tensor on
|
|
# others), which causes all_gather participant mismatch. Upstream
|
|
# mask builders must ensure all ranks produce the same mask type.
|
|
if attn_mask is None:
|
|
# Meta-only tail-pad caller on a non-FA fallback: the gathered
|
|
# mask is fully determined by the pad span, no collective needed.
|
|
gathered_mask = torch.ones(
|
|
q.shape[0], q.shape[1], dtype=torch.bool, device=q.device
|
|
)
|
|
gathered_mask[:, meta_pad_start:meta_pad_end] = False
|
|
else:
|
|
gathered_mask = sequence_model_parallel_all_gather(
|
|
attn_mask.contiguous(), dim=1
|
|
)
|
|
if (
|
|
_VARLEN_FA_ENABLED
|
|
and self.backend == AttentionBackendEnum.FA
|
|
and gathered_mask.dtype
|
|
in (torch.bool, torch.uint8, torch.int32, torch.int64)
|
|
and q.device.type == "cuda"
|
|
and gathered_mask.device == q.device
|
|
and q.dtype in (torch.float16, torch.bfloat16)
|
|
and q.shape[:2] == gathered_mask.shape == k.shape[:2] == v.shape[:2]
|
|
):
|
|
bs, seq = q.shape[0], q.shape[1]
|
|
gathered_mask_meta = build_varlen_mask_meta(gathered_mask)
|
|
indices = gathered_mask_meta["indices"]
|
|
inv_indices = gathered_mask_meta["inv_indices"]
|
|
assert (
|
|
inv_indices.shape[0] == bs * seq
|
|
), "gathered attn_mask shape does not match q/k/v"
|
|
if indices.shape[0] > 0:
|
|
q_unpad, k_unpad, v_unpad = fused_pack_qkv(q, k, v, indices)
|
|
out_unpad = flash_attn_varlen_func(
|
|
q=q_unpad,
|
|
k=k_unpad,
|
|
v=v_unpad,
|
|
cu_seqlens_q=gathered_mask_meta["cu_seqlens"],
|
|
cu_seqlens_k=gathered_mask_meta["cu_seqlens"],
|
|
max_seqlen_q=gathered_mask_meta["max_seqlen"],
|
|
max_seqlen_k=gathered_mask_meta["max_seqlen"],
|
|
softmax_scale=self.softmax_scale,
|
|
causal=False,
|
|
ver=_fa_backend.fa_ver,
|
|
)
|
|
out = fused_scatter_to_padded(out_unpad, inv_indices, bs, seq)
|
|
if sp_size > 1:
|
|
out = _usp_output_all_to_all(out, head_dim=2)
|
|
return out
|
|
|
|
q_ = q.transpose(1, 2)
|
|
k_ = k.transpose(1, 2)
|
|
v_ = v.transpose(1, 2)
|
|
mask = _prepare_sdpa_mask(gathered_mask, dtype=q_.dtype, device=q_.device)
|
|
sdpa_context = (
|
|
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
|
|
if self.allow_cudnn_sdp and q_.device.type == "cuda"
|
|
else nullcontext()
|
|
)
|
|
with sdpa_context:
|
|
out = torch.nn.functional.scaled_dot_product_attention(
|
|
q_,
|
|
k_,
|
|
v_,
|
|
attn_mask=mask,
|
|
dropout_p=0.0,
|
|
is_causal=False,
|
|
scale=self.softmax_scale,
|
|
).transpose(1, 2)
|
|
if sp_size > 1:
|
|
out = _usp_output_all_to_all(out, head_dim=2)
|
|
return out
|
|
|
|
if effective_skip_sp or get_sequence_parallel_world_size() == 1:
|
|
# No sequence parallelism, just run local attention.
|
|
out = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
return out
|
|
|
|
sp_size = get_ulysses_parallel_world_size()
|
|
if (
|
|
(num_replicated_prefix > 0 and num_replicated_suffix > 0)
|
|
or (num_replicated_prefix > 0 and num_replicated_kv_prefix > 0)
|
|
or (num_replicated_suffix > 0 and num_replicated_kv_prefix > 0)
|
|
):
|
|
raise ValueError(
|
|
"USPAttention supports at most one replicated-token mode per call."
|
|
)
|
|
if sp_size > 1 and num_replicated_prefix > 0:
|
|
return self._forward_with_replicated_prefix(
|
|
q, k, v, ctx_attn_metadata, num_replicated_prefix
|
|
)
|
|
if sp_size > 1 and num_replicated_suffix > 0:
|
|
return self._forward_with_replicated_suffix(
|
|
q, k, v, ctx_attn_metadata, num_replicated_suffix
|
|
)
|
|
if sp_size > 1 and num_replicated_kv_prefix > 0:
|
|
return self._forward_with_replicated_kv_prefix(
|
|
q, k, v, ctx_attn_metadata, num_replicated_kv_prefix
|
|
)
|
|
|
|
# Ulysses-style All-to-All for sequence/head sharding
|
|
if sp_size > 1:
|
|
# -> [B, S, H_local, D]
|
|
if self.enable_packed_qkv_input_a2a and q.device.type == "cuda":
|
|
q, k, v = async_a2a_communicate(
|
|
[q, k, v],
|
|
sp_size,
|
|
get_sp_group().ulysses_group,
|
|
self._get_usp_a2a_stream(),
|
|
local_seq_2_local_head=True,
|
|
)
|
|
q = q.contiguous()
|
|
k = k.contiguous()
|
|
v = v.contiguous()
|
|
else:
|
|
q = _usp_input_all_to_all(q, head_dim=2)
|
|
k = _usp_input_all_to_all(k, head_dim=2)
|
|
v = _usp_input_all_to_all(v, head_dim=2)
|
|
|
|
# Ring Attention within subgroups or local attention
|
|
if get_ring_parallel_world_size() > 1:
|
|
out = ring_attn(
|
|
q,
|
|
k,
|
|
v,
|
|
attn_impl=self.attn_impl,
|
|
is_causal=self.causal,
|
|
dropout_p=self.dropout_p,
|
|
)
|
|
else:
|
|
# -> [B, S, H_local, D]
|
|
out = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
|
|
# Ulysses-style All-to-All to restore original sharding
|
|
if sp_size > 1:
|
|
# -> [B, S_local, H, D]
|
|
out = _usp_output_all_to_all(out, head_dim=2)
|
|
|
|
return out
|
|
|
|
def _forward_with_replicated_prefix(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
ctx_attn_metadata,
|
|
num_rep: int,
|
|
) -> torch.Tensor:
|
|
"""Ulysses attention where the first *num_rep* tokens are replicated
|
|
across SP ranks (e.g. text tokens) and should NOT be duplicated by the
|
|
all-to-all.
|
|
|
|
Strategy:
|
|
1. Split q/k/v into replicated prefix and SP-sharded suffix.
|
|
2. All-to-all only the sharded suffix (gathers sequence, shards heads).
|
|
3. Locally slice the replicated prefix to the same head shard.
|
|
4. Concatenate [prefix_h_local, gathered_suffix] and run attention.
|
|
5. Split output, all-to-all back the suffix, all-gather prefix heads.
|
|
"""
|
|
sp_size = get_ulysses_parallel_world_size()
|
|
sp_rank = get_sp_parallel_rank()
|
|
|
|
q_rep, q_shard = q[:, :num_rep], q[:, num_rep:]
|
|
k_rep, k_shard = k[:, :num_rep], k[:, num_rep:]
|
|
v_rep, v_shard = v[:, :num_rep], v[:, num_rep:]
|
|
|
|
q_shard = _usp_input_all_to_all(q_shard, head_dim=2)
|
|
k_shard = _usp_input_all_to_all(k_shard, head_dim=2)
|
|
v_shard = _usp_input_all_to_all(v_shard, head_dim=2)
|
|
|
|
# Q and KV can have different head counts (GQA), so slice each replicated
|
|
# prefix by its own per-rank head shard to match the all-to-all'd suffix.
|
|
# For MHA (kv heads == q heads) this is identical to the q shard.
|
|
h_local = q_shard.shape[2]
|
|
kv_h_local = k_shard.shape[2]
|
|
h_start = sp_rank * h_local
|
|
kv_h_start = sp_rank * kv_h_local
|
|
q_rep = q_rep[:, :, h_start : h_start + h_local, :].contiguous()
|
|
k_rep = k_rep[:, :, kv_h_start : kv_h_start + kv_h_local, :].contiguous()
|
|
v_rep = v_rep[:, :, kv_h_start : kv_h_start + kv_h_local, :].contiguous()
|
|
|
|
q = torch.cat([q_rep, q_shard], dim=1)
|
|
k = torch.cat([k_rep, k_shard], dim=1)
|
|
v = torch.cat([v_rep, v_shard], dim=1)
|
|
|
|
out = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
|
|
out_rep = out[:, :num_rep]
|
|
out_shard = out[:, num_rep:]
|
|
|
|
out_shard = _usp_output_all_to_all(out_shard, head_dim=2)
|
|
|
|
gathered = [torch.empty_like(out_rep) for _ in range(sp_size)]
|
|
torch.distributed.all_gather(
|
|
gathered,
|
|
out_rep.contiguous(),
|
|
group=get_sp_group().ulysses_group,
|
|
)
|
|
out_rep = torch.cat(gathered, dim=2)
|
|
|
|
return torch.cat([out_rep, out_shard], dim=1)
|
|
|
|
def forward_with_replicated_kv_prefix(
|
|
self,
|
|
q: torch.Tensor,
|
|
k_prefix: torch.Tensor,
|
|
v_prefix: torch.Tensor,
|
|
k_suffix: torch.Tensor,
|
|
v_suffix: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
"""attention with replicated K/V prefix supplied separately"""
|
|
forward_context: ForwardContext = get_forward_context()
|
|
ctx_attn_metadata = forward_context.attn_metadata
|
|
|
|
if self.skip_sequence_parallel or get_sequence_parallel_world_size() == 1:
|
|
k = torch.cat([k_prefix, k_suffix], dim=1)
|
|
v = torch.cat([v_prefix, v_suffix], dim=1)
|
|
return self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
|
|
if get_ulysses_parallel_world_size() == 1:
|
|
k = torch.cat([k_prefix, k_suffix], dim=1)
|
|
v = torch.cat([v_prefix, v_suffix], dim=1)
|
|
return self(q, k, v)
|
|
|
|
return self._forward_with_replicated_kv_prefix_split(
|
|
q, k_prefix, v_prefix, k_suffix, v_suffix, ctx_attn_metadata
|
|
)
|
|
|
|
def _forward_with_replicated_kv_prefix(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
ctx_attn_metadata,
|
|
num_rep: int,
|
|
) -> torch.Tensor:
|
|
"""Ulysses cross-attention where only K/V have a replicated prefix.
|
|
|
|
Q is sequence-sharded across SP ranks with no replicated portion. K/V
|
|
carry a fully-replicated prefix (``[:num_rep]``, same on every rank,
|
|
e.g. cached text K/V) followed by a sequence-sharded suffix (e.g.
|
|
image tokens) that aligns with Q's sharding.
|
|
|
|
Strategy:
|
|
1. All-to-all Q and the sharded K/V suffix (seq → head shard).
|
|
2. Locally slice the replicated K/V prefix to the same head shard.
|
|
3. Concatenate prefix + suffix on the sequence dim and attend.
|
|
4. All-to-all the output back (head shard → seq shard).
|
|
"""
|
|
k_rep, k_shard = k[:, :num_rep], k[:, num_rep:]
|
|
v_rep, v_shard = v[:, :num_rep], v[:, num_rep:]
|
|
|
|
return self._forward_with_replicated_kv_prefix_split(
|
|
q, k_rep, v_rep, k_shard, v_shard, ctx_attn_metadata
|
|
)
|
|
|
|
def _forward_with_replicated_kv_prefix_split(
|
|
self,
|
|
q: torch.Tensor,
|
|
k_rep: torch.Tensor,
|
|
v_rep: torch.Tensor,
|
|
k_shard: torch.Tensor,
|
|
v_shard: torch.Tensor,
|
|
ctx_attn_metadata,
|
|
) -> torch.Tensor:
|
|
"""split form avoids materializing full K/V before Ulysses all-to-all"""
|
|
sp_rank = get_sp_parallel_rank()
|
|
|
|
if q.device.type == "cuda":
|
|
q, k_shard, v_shard = async_a2a_communicate(
|
|
[q, k_shard, v_shard],
|
|
get_ulysses_parallel_world_size(),
|
|
get_sp_group().ulysses_group,
|
|
self._get_usp_a2a_stream(),
|
|
local_seq_2_local_head=True,
|
|
)
|
|
q = q.contiguous()
|
|
k_shard = k_shard.contiguous()
|
|
v_shard = v_shard.contiguous()
|
|
else:
|
|
q = _usp_input_all_to_all(q, head_dim=2)
|
|
k_shard = _usp_input_all_to_all(k_shard, head_dim=2)
|
|
v_shard = _usp_input_all_to_all(v_shard, head_dim=2)
|
|
|
|
h_kv_local = k_shard.shape[2]
|
|
h_start = sp_rank * h_kv_local
|
|
h_end = h_start + h_kv_local
|
|
k_rep = k_rep[:, :, h_start:h_end, :].contiguous()
|
|
v_rep = v_rep[:, :, h_start:h_end, :].contiguous()
|
|
|
|
k = torch.cat([k_rep, k_shard], dim=1)
|
|
v = torch.cat([v_rep, v_shard], dim=1)
|
|
|
|
out = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
return _usp_output_all_to_all(out, head_dim=2)
|
|
|
|
def _forward_with_replicated_suffix(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
ctx_attn_metadata,
|
|
num_rep: int,
|
|
) -> torch.Tensor:
|
|
"""Ulysses attention where the last num_rep tokens are replicated
|
|
across SP ranks and should not be duplicated by the all-to-all."""
|
|
if num_rep <= 0:
|
|
raise ValueError("num_rep must be positive for replicated suffix.")
|
|
|
|
q_shard, q_rep = q[:, :-num_rep], q[:, -num_rep:]
|
|
k_shard, k_rep = k[:, :-num_rep], k[:, -num_rep:]
|
|
v_shard, v_rep = v[:, :-num_rep], v[:, -num_rep:]
|
|
|
|
# dense self-attention is permutation equivariant for non-causal use.
|
|
# 1. rotate the replicated suffix to the front
|
|
# 2. reuse the validated replicated-prefix path, then
|
|
# 3. rotate the output back
|
|
out = self._forward_with_replicated_prefix(
|
|
torch.cat([q_rep, q_shard], dim=1),
|
|
torch.cat([k_rep, k_shard], dim=1),
|
|
torch.cat([v_rep, v_shard], dim=1),
|
|
ctx_attn_metadata,
|
|
num_rep,
|
|
)
|
|
out_rep, out_shard = out[:, :num_rep], out[:, num_rep:]
|
|
return torch.cat([out_shard, out_rep], dim=1)
|
|
|
|
|
|
class _BCGBoxedTupleOutput:
|
|
"""Box a tuple-returning break-point output as tensor attributes.
|
|
|
|
``_copy_output`` copies tensors and objects-with-tensor-attributes in
|
|
place across replays but ignores tuples, so tuple-returning attention
|
|
forwards (``UlyssesAttention``) are boxed for the break point and
|
|
unboxed after.
|
|
"""
|
|
|
|
def __init__(self, values: tuple) -> None:
|
|
self.num_values = len(values)
|
|
for i, value in enumerate(values):
|
|
setattr(self, f"value_{i}", value)
|
|
|
|
def astuple(self) -> tuple:
|
|
return tuple(getattr(self, f"value_{i}") for i in range(self.num_values))
|
|
|
|
|
|
def _make_breakable_attention_forward(forward_method):
|
|
"""Wrap a DiT attention module's ``forward`` so it becomes a breakable
|
|
CUDA graph (BCG) break point.
|
|
|
|
During BCG capture the whole attention forward runs eagerly between
|
|
captured graph segments -- the sequence-parallel all-to-all collectives,
|
|
varlen packing, and dynamic/sparse attention kernels that live here
|
|
cannot (or should not) be captured into a static CUDA graph. When BCG is
|
|
disabled this is a transparent pass-through to the original method.
|
|
"""
|
|
|
|
def _forward_boxing_tuples(*args, **kwargs):
|
|
out = forward_method(*args, **kwargs)
|
|
return _BCGBoxedTupleOutput(out) if isinstance(out, tuple) else out
|
|
|
|
bcg_forward = eager_on_graph(True)(_forward_boxing_tuples)
|
|
|
|
@functools.wraps(forward_method)
|
|
def forward(self, *args, **kwargs):
|
|
if is_in_breakable_cuda_graph():
|
|
out = bcg_forward(self, *args, **kwargs)
|
|
return out.astuple() if isinstance(out, _BCGBoxedTupleOutput) else out
|
|
return forward_method(self, *args, **kwargs)
|
|
|
|
return forward
|
|
|
|
|
|
# Install the break points on every DiT attention entry point. All diffusion
|
|
# models route attention through one of these modules (e.g. FLUX -> USPAttention),
|
|
# so wrapping here gives universal, model-agnostic BCG break points without
|
|
# touching individual model files.
|
|
for _attn_cls in (
|
|
UlyssesAttention,
|
|
UlyssesAttention_VSA,
|
|
LocalAttention,
|
|
USPAttention,
|
|
):
|
|
_attn_cls.forward = _make_breakable_attention_forward(_attn_cls.forward)
|
|
del _attn_cls
|