Files
vllm-omni/vllm_omni/diffusion/models/magi2/attention.py
T
2026-09-05 15:32:30 +08:00

294 lines
11 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# Copyright (c) 2026 SandAI. All Rights Reserved.
"""Native MAGI-2 packed attention with sinks and Ulysses exchange.
The sink and context-parallel math is adapted from SandAI's Apache-2.0
MAGI-2 preview implementation. This version uses vLLM's bundled
FlashAttention extension and vLLM-Omni's existing Ulysses process group; the
PyTorch path is an exact, portable oracle for small tests.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from functools import cache
import torch
import torch.nn as nn
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.utils.fa import (
resolve_vllm_flash_attn_version,
vllm_flash_attn_varlen_with_lse,
)
from .parallel import (
Magi2ParallelGroup,
get_magi2_ulysses_group,
scatter_heads_gather_seqlen,
scatter_seqlen_gather_heads,
)
logger = logging.getLogger(__name__)
@cache
def _resolve_flash_attn_version() -> int:
"""Apply MAGI-2's operator override to the shared FA2/FA3/FA4 resolver."""
requested = os.environ.get("MAGI2_FLASH_ATTN_VERSION")
version = resolve_vllm_flash_attn_version(requested)
logger.info("MAGI-2 selected FlashAttention %d", version)
return version
@dataclass(frozen=True)
class VarlenHandler:
"""Packed-sequence metadata consumed by MAGI-2 attention."""
cu_seqlens_q: torch.Tensor | None
cu_seqlens_k: torch.Tensor | None
max_seqlen_q: int | None = None
max_seqlen_k: int | None = None
def resolved(self, q_tokens: int, k_tokens: int) -> tuple[torch.Tensor, torch.Tensor, int, int]:
if self.cu_seqlens_q is None:
cu_q = torch.tensor([0, q_tokens], device="cpu", dtype=torch.int32)
else:
cu_q = self.cu_seqlens_q
if self.cu_seqlens_k is None:
cu_k = torch.tensor([0, k_tokens], device="cpu", dtype=torch.int32)
else:
cu_k = self.cu_seqlens_k
max_q = self.max_seqlen_q
max_k = self.max_seqlen_k
if max_q is None:
max_q = int(torch.diff(cu_q).max().item()) if cu_q.numel() > 1 else 0
if max_k is None:
max_k = int(torch.diff(cu_k).max().item()) if cu_k.numel() > 1 else 0
return cu_q, cu_k, max_q, max_k
def rotate_half(x: torch.Tensor, interleaved: bool = False) -> torch.Tensor:
if not interleaved:
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
x1, x2 = x[..., ::2], x[..., 1::2]
return torch.stack((-x2, x1), dim=-1).flatten(-2)
def apply_rotary_emb(
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
*,
interleaved: bool = False,
) -> torch.Tensor:
"""Apply the released element-wise RoPE layout to ``[...,H,D]``."""
rotary_dim = cos.shape[-1] * 2
if rotary_dim > x.shape[-1]:
raise ValueError(f"RoPE dimension {rotary_dim} exceeds head dimension {x.shape[-1]}")
if interleaved:
cos = cos.unsqueeze(-2).repeat_interleave(2, dim=-1)
sin = sin.unsqueeze(-2).repeat_interleave(2, dim=-1)
else:
cos = torch.cat((cos, cos), dim=-1).unsqueeze(-2)
sin = torch.cat((sin, sin), dim=-1).unsqueeze(-2)
rotated = x[..., :rotary_dim] * cos + rotate_half(x[..., :rotary_dim], interleaved) * sin
return torch.cat((rotated, x[..., rotary_dim:]), dim=-1)
def correct_out_lse_with_sink(
out: torch.Tensor,
lse: torch.Tensor,
sink: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Add zero-valued attention sinks to an already-computed softmax.
FlashAttention returns ``out[T,H,D]`` and conventionally ``lse[H,T]``.
A MAGI sink contains additional logits ``[num_sink,H]`` whose values are
zero vectors, so only the denominator and LSE change.
"""
if sink is None or sink.numel() == 0:
return out, lse
if out.ndim != 3 or lse.ndim != 2 or sink.ndim != 2:
raise ValueError(
"expected out[T,H,D], lse[H,T], sink[num_sink,H], got "
f"{tuple(out.shape)}, {tuple(lse.shape)}, {tuple(sink.shape)}"
)
old_lse = lse.float().transpose(0, 1)
sink_lse = torch.logsumexp(sink.float(), dim=0).unsqueeze(0)
if old_lse.shape[-1] != sink_lse.shape[-1]:
raise ValueError("attention sink and FlashAttention head counts differ")
new_lse = torch.logaddexp(old_lse, sink_lse)
delta = old_lse - new_lse
delta = torch.where(torch.isfinite(delta), delta, torch.full_like(delta, -torch.inf))
corrected = out * torch.exp(delta).unsqueeze(-1).to(out.dtype)
return corrected, new_lse.transpose(0, 1).contiguous()
def _repeat_kv_heads(tensor: torch.Tensor, query_heads: int) -> torch.Tensor:
kv_heads = tensor.shape[1]
if kv_heads == query_heads:
return tensor
if query_heads % kv_heads:
raise ValueError(f"query heads {query_heads} must be divisible by KV heads {kv_heads}")
return tensor.repeat_interleave(query_heads // kv_heads, dim=1)
def torch_varlen_attention_with_sink(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens_q: torch.Tensor,
cu_seqlens_k: torch.Tensor,
softcap: float = -1.0,
sink: torch.Tensor | None = None,
) -> torch.Tensor:
"""Reference packed attention, including GQA and sink logits."""
if q.ndim != 3 or k.ndim != 3 or v.ndim != 3:
raise ValueError("packed attention expects q/k/v shaped [tokens,heads,dim]")
if cu_seqlens_q.numel() != cu_seqlens_k.numel():
raise ValueError("query and key cumulative-length arrays must contain the same batch count")
output = torch.empty_like(q)
scale = q.shape[-1] ** -0.5
for batch_idx in range(cu_seqlens_q.numel() - 1):
q_start, q_end = (int(v) for v in cu_seqlens_q[batch_idx : batch_idx + 2].tolist())
k_start, k_end = (int(v) for v in cu_seqlens_k[batch_idx : batch_idx + 2].tolist())
q_part = q[q_start:q_end].float()
k_part = _repeat_kv_heads(k[k_start:k_end], q.shape[1]).float()
v_part = _repeat_kv_heads(v[k_start:k_end], q.shape[1]).float()
scores = torch.einsum("qhd,khd->hqk", q_part, k_part) * scale
if softcap > 0:
scores = softcap * torch.tanh(scores / softcap)
if sink is not None and sink.numel() > 0:
sink_scores = sink.float().transpose(0, 1).unsqueeze(1).expand(-1, q_part.shape[0], -1)
probabilities = torch.softmax(torch.cat((scores, sink_scores), dim=-1), dim=-1)[..., : k_part.shape[0]]
else:
probabilities = torch.softmax(scores, dim=-1)
output[q_start:q_end] = torch.einsum("hqk,khd->qhd", probabilities, v_part).to(output.dtype)
return output
def packed_attention_with_sink(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
varlen: VarlenHandler,
*,
softcap: float = -1.0,
sink: torch.Tensor | None = None,
) -> torch.Tensor:
"""Run packed attention on one rank after Ulysses head exchange."""
cu_q, cu_k, max_q, max_k = varlen.resolved(q.shape[0], k.shape[0])
cu_q = cu_q.to(device=q.device, dtype=torch.int32).contiguous()
cu_k = cu_k.to(device=q.device, dtype=torch.int32).contiguous()
if q.is_cuda:
out, lse = vllm_flash_attn_varlen_with_lse(
q,
k,
v,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
max_seqlen_q=max_q,
max_seqlen_k=max_k,
softcap=softcap,
deterministic=os.environ.get("MAGI2_DETERMINISTIC", "0") == "1",
fa_version=_resolve_flash_attn_version(),
)
return correct_out_lse_with_sink(out, lse, sink)[0]
return torch_varlen_attention_with_sink(
q,
k,
v,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
softcap=softcap,
sink=sink,
)
def ulysses_packed_attention_with_sink(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
varlen: VarlenHandler,
split_sizes: list[int] | torch.Tensor,
*,
softcap: float = -1.0,
sink: torch.Tensor | None = None,
group: Magi2ParallelGroup | None = None,
) -> torch.Tensor:
"""MAGI-2 attention with overlapping Ulysses CP/head exchange."""
group = group or get_magi2_ulysses_group()
if isinstance(split_sizes, torch.Tensor):
split_sizes = [int(v) for v in split_sizes.detach().cpu().tolist()]
if group.world_size > 1:
q, k, v = scatter_heads_gather_seqlen((q, k, v), split_sizes, group)
if sink is not None:
if sink.shape[-1] % group.world_size:
raise ValueError("attention sink heads must divide across Ulysses ranks")
sink = torch.chunk(sink, group.world_size, dim=-1)[group.rank].contiguous()
output = packed_attention_with_sink(q, k, v, varlen, softcap=softcap, sink=sink)
if group.world_size > 1:
output = scatter_seqlen_gather_heads(output.contiguous(), split_sizes, group)
assert isinstance(output, torch.Tensor)
return output
class Magi2PackedAttentionKernel(nn.Module):
"""Model kernel plugged into the shared diffusion Attention contract."""
def __init__(self, softcap: float) -> None:
super().__init__()
self.softcap = softcap
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
if attn_metadata is None:
raise ValueError("MAGI-2 packed attention requires attention metadata")
varlen = attn_metadata.extra.get("magi2_varlen")
split_sizes = attn_metadata.extra.get("magi2_split_sizes")
sink = attn_metadata.extra.get("magi2_sink")
if not isinstance(varlen, VarlenHandler):
raise TypeError("magi2_varlen must be a VarlenHandler")
if not isinstance(split_sizes, (list, torch.Tensor)):
raise TypeError("magi2_split_sizes must be a list or tensor")
if sink is not None and not isinstance(sink, torch.Tensor):
raise TypeError("magi2_sink must be a tensor or None")
return ulysses_packed_attention_with_sink(
query,
key,
value,
varlen,
split_sizes,
softcap=self.softcap,
sink=sink,
)
__all__ = [
"VarlenHandler",
"Magi2PackedAttentionKernel",
"apply_rotary_emb",
"correct_out_lse_with_sink",
"packed_attention_with_sink",
"torch_varlen_attention_with_sink",
"ulysses_packed_attention_with_sink",
]