348 lines
15 KiB
Python
348 lines
15 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import os
|
|
from functools import wraps
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from fastvideo.attention.selector import backend_name_to_enum, get_attn_backend
|
|
from fastvideo.distributed.communication_op import (sequence_model_parallel_all_gather,
|
|
sequence_model_parallel_all_to_all_4D)
|
|
from fastvideo.distributed.parallel_state import (get_sp_parallel_rank, get_sp_world_size)
|
|
from fastvideo.forward_context import ForwardContext, get_forward_context
|
|
from fastvideo.platforms import AttentionBackendEnum
|
|
from fastvideo.utils import get_compute_dtype
|
|
from fastvideo.layers.rotary_embedding import _apply_rotary_emb
|
|
|
|
|
|
def _attention_compile_disabled() -> bool:
|
|
"""Whether to keep attention ``forward`` out of the torch.compile graph.
|
|
|
|
Defaults to ``True`` (the historical behavior: attention runs eager via
|
|
``torch.compiler.disable``). Set ``FASTVIDEO_DISABLE_ATTENTION_COMPILE=0``
|
|
to let attention instances constructed under that environment be traced.
|
|
"""
|
|
val = os.environ.get("FASTVIDEO_DISABLE_ATTENTION_COMPILE")
|
|
if val is None:
|
|
return True
|
|
return val.strip().lower() not in ("0", "false", "no", "off", "")
|
|
|
|
|
|
def _attention_compile_explicitly_disabled() -> bool:
|
|
"""Whether the environment explicitly requests the eager boundary.
|
|
|
|
Regional compile can override the historical default for one loaded
|
|
transformer, but it must still honor an explicit debugging escape hatch.
|
|
"""
|
|
return "FASTVIDEO_DISABLE_ATTENTION_COMPILE" in os.environ and _attention_compile_disabled()
|
|
|
|
|
|
def _maybe_compiler_disable(fn):
|
|
"""Defer the eager/traceable choice to each attention instance.
|
|
|
|
A class-definition-time choice makes a process-wide default the only
|
|
option. The deferred wrapper keeps ordinary instances on the historical
|
|
eager boundary while allowing the regional loader to opt in only the
|
|
attention modules owned by the transformer it is compiling.
|
|
"""
|
|
disabled_fn = torch.compiler.disable(fn)
|
|
|
|
@wraps(fn)
|
|
def _dispatch(self, *args, **kwargs):
|
|
if self._compile_forward_enabled:
|
|
return fn(self, *args, **kwargs)
|
|
return disabled_fn(self, *args, **kwargs)
|
|
|
|
return _dispatch
|
|
|
|
|
|
class DistributedAttention(nn.Module):
|
|
"""Distributed 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: tuple[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)
|
|
# Register attn_impl as submodule if it has learnable parameters (e.g., SLA's proj_l)
|
|
# This ensures its parameters are included in state_dict() for saving/loading
|
|
if isinstance(self.attn_impl, nn.Module):
|
|
self.add_module('attn_impl', self.attn_impl)
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.num_kv_heads = num_kv_heads
|
|
self.backend = backend_name_to_enum(attn_backend.get_name())
|
|
self.dtype = dtype
|
|
# Preserve the historical compiler-disabled default. The regional
|
|
# inference loader may enable this one instance after validating the
|
|
# transformer's resolved backend; no process-global default changes.
|
|
self._compile_forward_enabled = not _attention_compile_disabled()
|
|
|
|
def _set_compile_forward_enabled(self, enabled: bool) -> None:
|
|
self._compile_forward_enabled = enabled
|
|
|
|
@_maybe_compiler_disable
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
original_seq_len: int | None = None,
|
|
replicated_q: torch.Tensor | None = None,
|
|
replicated_k: torch.Tensor | None = None,
|
|
replicated_v: torch.Tensor | None = None,
|
|
freqs_cis: tuple[torch.Tensor, torch.Tensor] | 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]
|
|
original_seq_len (int): Original (unpadded) full sequence length
|
|
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, _, num_heads, _ = 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
|
|
|
|
# Stack QKV
|
|
qkv = torch.cat([q, k, v], dim=0) # [3*batch, seq_len, num_heads, head_dim]
|
|
|
|
# Redistribute heads across sequence dimension
|
|
qkv = sequence_model_parallel_all_to_all_4D(qkv, scatter_dim=2, gather_dim=1)
|
|
|
|
# After all-to-all, each rank has the full sequence but only a subset of heads.
|
|
# Trim away SP padding for attention compute, then pad back before returning.
|
|
original_seq_len = original_seq_len or qkv.shape[1]
|
|
pad_seq_len = qkv.shape[1] - original_seq_len
|
|
qkv = qkv[:, :original_seq_len, :, :]
|
|
|
|
if freqs_cis is not None:
|
|
cos, sin = freqs_cis
|
|
qkv[:batch_size * 2] = _apply_rotary_emb(qkv[:batch_size * 2], cos, sin, is_neox_style=False)
|
|
# 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:
|
|
split_idx = original_seq_len
|
|
replicated_output = output[:, split_idx:]
|
|
output = output[:, :split_idx]
|
|
# 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)
|
|
|
|
output = torch.nn.functional.pad(output, (0, 0, 0, 0, 0, pad_seq_len))
|
|
|
|
output = sequence_model_parallel_all_to_all_4D(output, scatter_dim=1, gather_dim=2)
|
|
|
|
return output, replicated_output
|
|
|
|
|
|
class DistributedAttention_VSA(DistributedAttention):
|
|
"""Distributed attention layer with VSA support.
|
|
"""
|
|
|
|
@_maybe_compiler_disable
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
original_seq_len: int,
|
|
replicated_q: torch.Tensor | None = None,
|
|
replicated_k: torch.Tensor | None = None,
|
|
replicated_v: torch.Tensor | None = None,
|
|
gate_compress: torch.Tensor | None = None,
|
|
freqs_cis: tuple[torch.Tensor, torch.Tensor] | 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]
|
|
original_seq_len (int): Original (unpadded) full sequence length
|
|
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
|
|
|
|
batch_size, seq_len, num_heads, head_dim = q.shape
|
|
# Stack QKV (a caller with a structurally-zero gate passes None and
|
|
# skips the gate's share of the all-to-all/tile traffic)
|
|
stack = [q, k, v] if gate_compress is None else [q, k, v, gate_compress]
|
|
qkvg = torch.cat(stack, dim=0) # [3or4*batch, seq_len, num_heads, head_dim]
|
|
|
|
# Redistribute heads across sequence dimension
|
|
# Before: [3or4*batch, shard_seq_len, num_heads, head_dim]
|
|
# After: [4*batch, full_seq_len, shard_num_heads, head_dim]
|
|
qkvg = sequence_model_parallel_all_to_all_4D(qkvg, scatter_dim=2, gather_dim=1)
|
|
|
|
# After all-to-all, each rank has the full sequence but only a subset of heads
|
|
pad_seq_len = qkvg.shape[1] - original_seq_len
|
|
qkvg = qkvg[:, :original_seq_len, :, :]
|
|
|
|
if freqs_cis is not None:
|
|
cos, sin = freqs_cis
|
|
qkvg[:batch_size * 2] = _apply_rotary_emb(qkvg[:batch_size * 2], cos, sin, is_neox_style=False)
|
|
|
|
qkvg = self.attn_impl.preprocess_qkv(qkvg, ctx_attn_metadata)
|
|
|
|
if gate_compress is None:
|
|
q, k, v = qkvg.chunk(3, dim=0)
|
|
else:
|
|
q, k, v, gate_compress = qkvg.chunk(4, dim=0)
|
|
output = self.attn_impl.forward(q, k, v, gate_compress, ctx_attn_metadata) # type: ignore[call-arg]
|
|
|
|
# Redistribute back if using sequence parallelism
|
|
replicated_output = None
|
|
|
|
# Apply backend-specific postprocess_output
|
|
output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)
|
|
|
|
output = torch.nn.functional.pad(output, (0, 0, 0, 0, 0, pad_seq_len))
|
|
|
|
output = sequence_model_parallel_all_to_all_4D(output, scatter_dim=1, gather_dim=2)
|
|
return output, replicated_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: tuple[AttentionBackendEnum, ...]
|
|
| None = None,
|
|
default_backend: AttentionBackendEnum | 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 = get_compute_dtype()
|
|
attn_backend = get_attn_backend(head_size,
|
|
dtype,
|
|
supported_attention_backends=supported_attention_backends,
|
|
default_backend=default_backend)
|
|
impl_cls = attn_backend.get_impl_cls()
|
|
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)
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.num_kv_heads = num_kv_heads
|
|
self.backend = backend_name_to_enum(attn_backend.get_name())
|
|
self.dtype = dtype
|
|
|
|
def forward(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
freqs_cis: tuple[torch.Tensor, 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 freqs_cis is not None:
|
|
cos, sin = freqs_cis
|
|
q = _apply_rotary_emb(q, cos, sin, is_neox_style=False)
|
|
k = _apply_rotary_emb(k, cos, sin, is_neox_style=False)
|
|
|
|
output = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
|
|
return output
|