[Model] Add native MAGI-2 Preview diffusion support (#5918)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com> Signed-off-by: hsliuustc0106 <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
13c617a140
commit
d4096e01fe
78 files changed
+9896
-459
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""SandAI MAGI-2 Preview diffusion support."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["Magi2Pipeline", "get_magi2_post_process_func"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name in __all__:
|
||||
from .pipeline_magi2 import Magi2Pipeline, get_magi2_post_process_func
|
||||
|
||||
return {
|
||||
"Magi2Pipeline": Magi2Pipeline,
|
||||
"get_magi2_post_process_func": get_magi2_post_process_func,
|
||||
}[name]
|
||||
raise AttributeError(name)
|
||||
@@ -0,0 +1,293 @@
|
||||
# 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",
|
||||
]
|
||||
@@ -0,0 +1,195 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Native Stable Audio Open decoder used by MAGI-2.
|
||||
|
||||
The MAGI checkpoint embeds Stable Audio's Oobleck VAE under the
|
||||
``pretransform.model`` prefix and uses the sequential module names from
|
||||
``stable-audio-tools``. This module converts those names to Diffusers'
|
||||
``AutoencoderOobleck`` layout and loads only the decoder tensors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from diffusers import AutoencoderOobleck
|
||||
from safetensors import safe_open
|
||||
|
||||
_CHECKPOINT_PREFIX = "pretransform.model.decoder."
|
||||
_BLOCK_COMPONENTS = {
|
||||
0: "snake1",
|
||||
1: "conv_t1",
|
||||
}
|
||||
_RESIDUAL_COMPONENTS = {
|
||||
0: "snake1",
|
||||
1: "conv1",
|
||||
2: "snake2",
|
||||
3: "conv2",
|
||||
}
|
||||
|
||||
|
||||
def convert_stable_audio_decoder_key(key: str) -> str | None:
|
||||
"""Map one Stable Audio decoder key to Diffusers' Oobleck decoder.
|
||||
|
||||
Unrelated checkpoint tensors return ``None``. A key inside the decoder
|
||||
namespace that does not match the released Stable Audio layout raises so a
|
||||
changed checkpoint cannot be loaded partially by accident.
|
||||
"""
|
||||
|
||||
if not key.startswith(_CHECKPOINT_PREFIX):
|
||||
return None
|
||||
suffix = key[len(_CHECKPOINT_PREFIX) :]
|
||||
parts = suffix.split(".")
|
||||
if len(parts) < 3 or parts[0] != "layers":
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}")
|
||||
|
||||
try:
|
||||
layer = int(parts[1])
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}") from exc
|
||||
tail = parts[2:]
|
||||
|
||||
if layer == 0 and len(tail) == 1:
|
||||
return f"conv1.{tail[0]}"
|
||||
if 1 <= layer <= 5:
|
||||
block = layer - 1
|
||||
if len(tail) == 3 and tail[0] == "layers":
|
||||
try:
|
||||
component = _BLOCK_COMPONENTS[int(tail[1])]
|
||||
except (KeyError, ValueError) as exc:
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}") from exc
|
||||
return f"block.{block}.{component}.{tail[2]}"
|
||||
if len(tail) == 5 and tail[0] == "layers" and tail[2] == "layers":
|
||||
try:
|
||||
residual = int(tail[1]) - 1
|
||||
component = _RESIDUAL_COMPONENTS[int(tail[3])]
|
||||
except (KeyError, ValueError) as exc:
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}") from exc
|
||||
if residual not in (1, 2, 3):
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}")
|
||||
return f"block.{block}.res_unit{residual}.{component}.{tail[4]}"
|
||||
if layer == 6 and len(tail) == 1:
|
||||
return f"snake1.{tail[0]}"
|
||||
if layer == 7 and len(tail) == 1:
|
||||
return f"conv2.{tail[0]}"
|
||||
raise ValueError(f"Unsupported Stable Audio decoder key: {key}")
|
||||
|
||||
|
||||
def convert_stable_audio_decoder_state_dict(
|
||||
state_dict: Mapping[str, torch.Tensor],
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Convert and filter a Stable Audio checkpoint state dictionary."""
|
||||
|
||||
converted: dict[str, torch.Tensor] = {}
|
||||
for key, value in state_dict.items():
|
||||
target_key = convert_stable_audio_decoder_key(key)
|
||||
if target_key is None:
|
||||
continue
|
||||
if target_key in converted:
|
||||
raise ValueError(f"Duplicate converted Stable Audio key: {target_key}")
|
||||
if target_key.endswith((".alpha", ".beta")):
|
||||
value = value.reshape(1, -1, 1)
|
||||
converted[target_key] = value
|
||||
return converted
|
||||
|
||||
|
||||
def _load_oobleck_config(model_path: Path) -> tuple[dict[str, Any], int]:
|
||||
config_path = model_path / "model_config.json"
|
||||
if not config_path.is_file():
|
||||
raise FileNotFoundError(f"Stable Audio config does not exist: {config_path}")
|
||||
with config_path.open(encoding="utf-8") as file:
|
||||
full_config = json.load(file)
|
||||
|
||||
try:
|
||||
pretransform = full_config["model"]["pretransform"]
|
||||
config = pretransform["config"]
|
||||
decoder = config["decoder"]
|
||||
decoder_config = decoder["config"]
|
||||
sample_rate = int(full_config["sample_rate"])
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"Invalid Stable Audio model config: {config_path}") from exc
|
||||
|
||||
if pretransform.get("type") != "autoencoder" or decoder.get("type") != "oobleck":
|
||||
raise ValueError("MAGI-2 requires an Oobleck Stable Audio pretransform")
|
||||
if decoder_config.get("use_nearest_upsample", False):
|
||||
raise ValueError("Diffusers Oobleck does not support nearest-neighbor decoder upsampling")
|
||||
if decoder_config.get("final_tanh", True):
|
||||
raise ValueError("MAGI-2 requires the Stable Audio decoder with final_tanh=false")
|
||||
|
||||
strides = list(decoder_config["strides"])
|
||||
downsampling_ratio = int(config["downsampling_ratio"])
|
||||
if int(torch.tensor(strides).prod().item()) != downsampling_ratio:
|
||||
raise ValueError("Stable Audio strides do not match downsampling_ratio")
|
||||
|
||||
kwargs = {
|
||||
"encoder_hidden_size": int(config["encoder"]["config"]["channels"]),
|
||||
"downsampling_ratios": strides,
|
||||
"channel_multiples": list(decoder_config["c_mults"]),
|
||||
"decoder_channels": int(decoder_config["channels"]),
|
||||
"decoder_input_channels": int(decoder_config["latent_dim"]),
|
||||
"audio_channels": int(decoder_config["out_channels"]),
|
||||
"sampling_rate": sample_rate,
|
||||
}
|
||||
return kwargs, downsampling_ratio
|
||||
|
||||
|
||||
class Magi2AudioDecoder(nn.Module):
|
||||
"""Decode MAGI-2 audio latents with the bundled Stable Audio VAE weights."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_path: str | Path,
|
||||
*,
|
||||
device: str | torch.device = "cpu",
|
||||
dtype: torch.dtype = torch.float32,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
model_path = Path(model_path)
|
||||
if not model_path.is_dir():
|
||||
raise ValueError(f"Stable Audio model path must be a local directory: {model_path}")
|
||||
|
||||
oobleck_kwargs, self.downsampling_ratio = _load_oobleck_config(model_path)
|
||||
self.sample_rate = int(oobleck_kwargs["sampling_rate"])
|
||||
# Construct the Diffusers implementation for architectural compatibility,
|
||||
# then retain only the decoder because MAGI-2 has no audio input encoder.
|
||||
autoencoder = AutoencoderOobleck(**oobleck_kwargs)
|
||||
self.decoder = autoencoder.decoder.to(device=device, dtype=dtype)
|
||||
del autoencoder
|
||||
|
||||
weights_path = model_path / "model.safetensors"
|
||||
if not weights_path.is_file():
|
||||
raise FileNotFoundError(f"Stable Audio weights do not exist: {weights_path}")
|
||||
selected: dict[str, torch.Tensor] = {}
|
||||
with safe_open(weights_path, framework="pt", device=str(device)) as checkpoint:
|
||||
for key in checkpoint.keys():
|
||||
if key.startswith(_CHECKPOINT_PREFIX):
|
||||
selected[key] = checkpoint.get_tensor(key)
|
||||
converted = convert_stable_audio_decoder_state_dict(selected)
|
||||
converted = {key: value.to(dtype=dtype) for key, value in converted.items()}
|
||||
self.decoder.load_state_dict(converted, strict=True)
|
||||
self.eval().requires_grad_(False)
|
||||
|
||||
@property
|
||||
def latent_fps(self) -> float:
|
||||
return self.sample_rate / self.downsampling_ratio
|
||||
|
||||
@torch.inference_mode()
|
||||
def decode(self, latents: torch.Tensor) -> torch.Tensor:
|
||||
parameter = next(self.decoder.parameters())
|
||||
return self.decoder(latents.to(device=parameter.device, dtype=parameter.dtype))
|
||||
|
||||
forward = decode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2AudioDecoder",
|
||||
"convert_stable_audio_decoder_key",
|
||||
"convert_stable_audio_decoder_state_dict",
|
||||
]
|
||||
@@ -0,0 +1,321 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Native MAGI-2 prompt conditioning.
|
||||
|
||||
Adapted from SandAI's MAGI-2 Preview inference implementation. The model
|
||||
loading remains in-tree and uses the Transformers Qwen3.5 implementation; no
|
||||
MAGI-2 reference checkout is imported at runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from tokenizers import Regex, pre_tokenizers
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
|
||||
def _strip_empty(obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
key: value
|
||||
for key, value in ((key, _strip_empty(value)) for key, value in obj.items())
|
||||
if value is not None and value != [] and value != {}
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [_strip_empty(value) for value in obj if value is not None]
|
||||
return obj
|
||||
|
||||
|
||||
def _one_line(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
return " ".join(value.split())
|
||||
|
||||
|
||||
def _as_dict(value: Any) -> dict[str, Any]:
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _as_list(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
return value if isinstance(value, list) else [value]
|
||||
|
||||
|
||||
def _pick(values: dict[str, Any], keys: Iterable[str]) -> list[tuple[str, Any]]:
|
||||
return [(key, values[key]) for key in keys if key in values and values[key] not in (None, [], {})]
|
||||
|
||||
|
||||
def json_to_compact_markdown(raw_json: str | dict[str, Any]) -> str:
|
||||
"""Convert MAGI's structured prompt JSON into its training-time text form."""
|
||||
|
||||
if isinstance(raw_json, str):
|
||||
obj = json.loads(raw_json.strip())
|
||||
else:
|
||||
obj = raw_json
|
||||
|
||||
obj = _strip_empty(obj)
|
||||
if not isinstance(obj, dict) or "global_layer" not in obj:
|
||||
return raw_json if isinstance(raw_json, str) else json.dumps(raw_json, ensure_ascii=False)
|
||||
|
||||
global_layer = _as_dict(obj.get("global_layer"))
|
||||
dynamic_layer = _as_dict(obj.get("dynamic_layer"))
|
||||
reference_layer = obj.get("reference_layer", [])
|
||||
|
||||
lines: list[str] = []
|
||||
context = _one_line(global_layer.get("context"))
|
||||
description = _one_line(global_layer.get("description"))
|
||||
if context or description:
|
||||
lines.append(f"context: {context}" if context else "context")
|
||||
if description:
|
||||
lines.append(description)
|
||||
|
||||
aesthetics = _as_dict(global_layer.get("aesthetics"))
|
||||
aesthetic_parts = []
|
||||
for label, key in (("style", "style"), ("mood", "mood_atmosphere"), ("color", "color_scheme")):
|
||||
value = _one_line(aesthetics.get(key))
|
||||
if value:
|
||||
aesthetic_parts.append(f"{label}={value}")
|
||||
if aesthetic_parts:
|
||||
lines.append("aesthetics: " + "; ".join(aesthetic_parts))
|
||||
|
||||
audio = _as_dict(global_layer.get("audio_baseline"))
|
||||
dialogue = _as_dict(audio.get("dialogue"))
|
||||
audio_parts = []
|
||||
language = _one_line(dialogue.get("language"))
|
||||
speakers = _as_list(dialogue.get("speaker_tags"))
|
||||
ambience = _one_line(audio.get("ambience"))
|
||||
if language:
|
||||
audio_parts.append(f"language={language}")
|
||||
if speakers:
|
||||
audio_parts.append("speakers=" + ",".join(map(_one_line, speakers)))
|
||||
if ambience:
|
||||
audio_parts.append(f"ambience={ambience}")
|
||||
if audio_parts:
|
||||
lines.append("audio: " + "; ".join(audio_parts))
|
||||
|
||||
subjects = global_layer.get("alive_subjects_static")
|
||||
if isinstance(subjects, list) and subjects:
|
||||
lines.append("subjects:")
|
||||
for subject in subjects:
|
||||
if not isinstance(subject, dict):
|
||||
continue
|
||||
sid = _one_line(subject.get("subject_id"))
|
||||
desc = _one_line(subject.get("description"))
|
||||
position = _one_line(subject.get("position"))
|
||||
orientation = _one_line(subject.get("orientation"))
|
||||
attrs = []
|
||||
for key, value in _pick(
|
||||
_as_dict(subject.get("visual_attributes")),
|
||||
["gender", "age_group", "ethnicity", "clothing", "appearance_details"],
|
||||
):
|
||||
value = _one_line(value)
|
||||
if value:
|
||||
attrs.append(f"{key}={value}")
|
||||
row = " - " + " | ".join(part for part in (sid, desc) if part)
|
||||
extra = "; ".join(part for part in (position, orientation) if part)
|
||||
if extra:
|
||||
row += f" ({extra})"
|
||||
if attrs:
|
||||
row += " :: " + "; ".join(attrs)
|
||||
lines.append(row)
|
||||
|
||||
objects = global_layer.get("objects_static")
|
||||
if isinstance(objects, list) and objects:
|
||||
lines.append("objects:")
|
||||
for obj_item in objects:
|
||||
if not isinstance(obj_item, dict):
|
||||
continue
|
||||
oid = _one_line(obj_item.get("object_id"))
|
||||
desc = _one_line(obj_item.get("description"))
|
||||
shape = _one_line(obj_item.get("shape_and_color"))
|
||||
position = _one_line(obj_item.get("position"))
|
||||
row = " - " + " | ".join(part for part in (oid, desc) if part)
|
||||
details = "; ".join(part for part in (shape, position) if part)
|
||||
if details:
|
||||
row += " :: " + details
|
||||
lines.append(row)
|
||||
|
||||
segments = dynamic_layer.get("timeline_segments")
|
||||
if isinstance(segments, list) and segments:
|
||||
lines.append("timeline:")
|
||||
for segment in segments:
|
||||
if not isinstance(segment, dict):
|
||||
continue
|
||||
basic = _as_dict(segment.get("segment_basic_info"))
|
||||
timestamp = _one_line(basic.get("timestamp_range"))
|
||||
desc = _one_line(basic.get("segment_description"))
|
||||
head = f" - {timestamp}" if timestamp else " -"
|
||||
if desc:
|
||||
head += f" {desc}"
|
||||
lines.append(head.rstrip())
|
||||
|
||||
segment_audio = _as_dict(segment.get("audio"))
|
||||
for dialogue_line in segment_audio.get("dialogue_lines", []) or []:
|
||||
if not isinstance(dialogue_line, dict):
|
||||
continue
|
||||
speaker = _one_line(dialogue_line.get("speaker"))
|
||||
text = _one_line(dialogue_line.get("text"))
|
||||
timestamp = _one_line(dialogue_line.get("timestamp"))
|
||||
if text:
|
||||
line = (f" - dialogue {speaker}: " if speaker else " - dialogue: ") + text
|
||||
if timestamp:
|
||||
line += f" ({timestamp})"
|
||||
lines.append(line)
|
||||
|
||||
for alive in segment.get("alive_subjects", []) or []:
|
||||
if not isinstance(alive, dict):
|
||||
continue
|
||||
sid = _one_line(alive.get("subject_id"))
|
||||
action = _as_dict(alive.get("action"))
|
||||
parts = [
|
||||
_one_line(action.get("primary_action")),
|
||||
_one_line(action.get("interaction")),
|
||||
_one_line(action.get("facial_expression")),
|
||||
]
|
||||
parts = [part for part in parts if part]
|
||||
if parts:
|
||||
lines.append(f" - action {sid}: " + "; ".join(parts))
|
||||
|
||||
for obj_item in segment.get("objects", []) or []:
|
||||
if not isinstance(obj_item, dict):
|
||||
continue
|
||||
oid = _one_line(obj_item.get("object_id"))
|
||||
state = _as_dict(obj_item.get("dynamic_state"))
|
||||
parts = [_one_line(state.get("state_change")), _one_line(state.get("motion_detail"))]
|
||||
parts = [part for part in parts if part]
|
||||
if parts:
|
||||
lines.append(f" - objects {oid}: " + "; ".join(parts))
|
||||
|
||||
if isinstance(reference_layer, list):
|
||||
lines.extend(str(line) for line in reference_layer if str(line).strip())
|
||||
return "\n".join(line for line in lines if line.strip())
|
||||
|
||||
|
||||
def normalize_prompt(prompt: str) -> str:
|
||||
"""Normalize structured prompts while leaving ordinary text byte-for-byte intact."""
|
||||
|
||||
try:
|
||||
parsed = json.loads(prompt)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return prompt
|
||||
if isinstance(parsed, dict | list):
|
||||
return json_to_compact_markdown(prompt)
|
||||
return prompt
|
||||
|
||||
|
||||
class Magi2Qwen35TextEncoder(nn.Module):
|
||||
"""Qwen3.5-27B feature extractor used by MAGI-2 Preview."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_path: str,
|
||||
*,
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
max_length: int = 7000,
|
||||
skip_layer: int = 2,
|
||||
local_files_only: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.max_length = max_length
|
||||
self.skip_layer = skip_layer
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_path,
|
||||
padding_side="right",
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
cjk_split = pre_tokenizers.Split(
|
||||
pattern=Regex(
|
||||
r"([\u1100-\u11ff\u2e80-\ua4cf\ua840-\uD7AF"
|
||||
r"\uF900-\uFAFF\uFE30-\uFE4F\uFF65-\uFFDC\U00020000-\U0002FFFF])"
|
||||
),
|
||||
behavior="isolated",
|
||||
)
|
||||
original = self.tokenizer.backend_tokenizer.pre_tokenizer
|
||||
self.tokenizer.backend_tokenizer.pre_tokenizer = pre_tokenizers.Sequence([cjk_split, original])
|
||||
|
||||
from transformers import Qwen3_5TextModel
|
||||
|
||||
self.text_model = Qwen3_5TextModel.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=dtype,
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
self.text_model.eval().requires_grad_(False)
|
||||
|
||||
def _tokenize(self, prompt: str, *, offsets: bool = False):
|
||||
return self.tokenizer(
|
||||
[normalize_prompt(prompt)],
|
||||
return_tensors="pt",
|
||||
padding="longest",
|
||||
return_offsets_mapping=offsets,
|
||||
max_length=self.max_length,
|
||||
truncation=True,
|
||||
)
|
||||
|
||||
def get_target_token_indices(self, prompt: str, target: str | None) -> list[int] | None:
|
||||
if not target:
|
||||
return None
|
||||
normalized = normalize_prompt(prompt)
|
||||
offsets = self._tokenize(prompt, offsets=True)["offset_mapping"][0]
|
||||
start = normalized.find(target)
|
||||
if start < 0:
|
||||
return None
|
||||
end = start + len(target)
|
||||
indices = []
|
||||
for index, (token_start, token_end) in enumerate(offsets.tolist()):
|
||||
if token_start == 0 and token_end == 0 and index != 0:
|
||||
continue
|
||||
if max(token_start, start) < min(token_end, end):
|
||||
indices.append(index)
|
||||
return indices
|
||||
|
||||
def pool_figure_tokens(
|
||||
self,
|
||||
prompt: str,
|
||||
targets: list[str],
|
||||
text_features: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Mean-pool the token features spelling each ``<Figure N>`` marker."""
|
||||
|
||||
embeddings = []
|
||||
for target in targets:
|
||||
indices = self.get_target_token_indices(prompt, target)
|
||||
if indices:
|
||||
embeddings.append(text_features[0, indices].mean(dim=0).clone())
|
||||
else:
|
||||
embeddings.append(text_features.new_zeros(text_features.shape[-1]))
|
||||
return torch.stack(embeddings)
|
||||
|
||||
@torch.inference_mode()
|
||||
def encode(self, prompt: str) -> torch.Tensor:
|
||||
inputs = self._tokenize(prompt)
|
||||
device = next(self.text_model.parameters()).device
|
||||
inputs = inputs.to(device)
|
||||
outputs = self.text_model(
|
||||
input_ids=inputs["input_ids"],
|
||||
attention_mask=inputs["attention_mask"],
|
||||
output_hidden_states=True,
|
||||
return_dict=True,
|
||||
)
|
||||
if self.skip_layer == 0:
|
||||
return outputs.last_hidden_state
|
||||
return outputs.hidden_states[-(self.skip_layer + 1)]
|
||||
|
||||
forward = encode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2Qwen35TextEncoder",
|
||||
"json_to_compact_markdown",
|
||||
"normalize_prompt",
|
||||
]
|
||||
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
||||
|
||||
"""Native configuration for the released MAGI-2 Preview checkpoint.
|
||||
|
||||
The defaults mirror ``sand-ai/MAGI-2-preview``. Keeping the architecture in
|
||||
vLLM-Omni makes model construction independent of SandAI's Python package and
|
||||
also gives tests a small-config entry point.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2MHCConfig:
|
||||
enabled: bool = True
|
||||
num_streams: int = 4
|
||||
alpha_init: float = 0.01
|
||||
sinkhorn_iterations: int = 20
|
||||
sinkhorn_epsilon: float = 1e-12
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2MoEConfig:
|
||||
num_heads: int = 12
|
||||
num_experts: int = 256
|
||||
top_k: int = 6
|
||||
expert_intermediate_size: int = 1280
|
||||
shared_expert_intermediate_size: int = 1280
|
||||
modality_shared_expert_intermediate_size: int = 1280
|
||||
score_function: str = "sigmoid"
|
||||
normalize_routing_weights: bool = True
|
||||
routing_scale: float = 4.9
|
||||
layers: tuple[int, ...] = tuple(range(2, 38))
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.num_heads <= 0 or self.num_experts <= 0:
|
||||
raise ValueError("MAGI-2 MoE head and expert counts must be positive")
|
||||
if not 0 < self.top_k <= self.num_experts:
|
||||
raise ValueError("MAGI-2 MoE top_k must be in [1, num_experts]")
|
||||
if self.score_function != "sigmoid":
|
||||
raise ValueError("The released MAGI-2 checkpoint uses sigmoid routing")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2PreviewConfig:
|
||||
num_layers: int = 40
|
||||
hidden_size: int = 3072
|
||||
head_dim: int = 128
|
||||
num_query_groups: int = 24
|
||||
video_in_channels: int = 48
|
||||
audio_in_channels: int = 64
|
||||
text_in_channels: int = 5120
|
||||
intermediate_factor: float = 4.0
|
||||
multimodal_layers: tuple[int, ...] = (0, 1, 38, 39)
|
||||
params_dtype: torch.dtype = torch.bfloat16
|
||||
attention_softcap: float = -1.0
|
||||
attention_sink_tokens: int = 1
|
||||
attention_gating: bool = True
|
||||
mhc: Magi2MHCConfig = field(default_factory=Magi2MHCConfig)
|
||||
moe: Magi2MoEConfig = field(default_factory=Magi2MoEConfig)
|
||||
|
||||
@property
|
||||
def num_attention_heads(self) -> int:
|
||||
if self.hidden_size % self.head_dim:
|
||||
raise ValueError("hidden_size must be divisible by head_dim")
|
||||
return self.hidden_size // self.head_dim
|
||||
|
||||
@property
|
||||
def num_heads_q(self) -> int:
|
||||
return self.num_attention_heads
|
||||
|
||||
@property
|
||||
def num_heads_kv(self) -> int:
|
||||
return self.num_query_groups
|
||||
|
||||
@property
|
||||
def virtual_width(self) -> int:
|
||||
return self.hidden_size * self.mhc.num_streams
|
||||
|
||||
def validate(self) -> None:
|
||||
if not self.mhc.enabled:
|
||||
raise ValueError("The released MAGI-2 Preview checkpoint requires mHC")
|
||||
if not self.attention_gating:
|
||||
raise ValueError("The released MAGI-2 Preview checkpoint requires attention gating")
|
||||
if self.attention_sink_tokens < 1:
|
||||
raise ValueError("The released MAGI-2 Preview checkpoint requires an attention sink")
|
||||
if self.hidden_size % self.moe.num_heads:
|
||||
raise ValueError("hidden_size must be divisible by the number of MoE heads")
|
||||
layer_ids = set(range(self.num_layers))
|
||||
if not set(self.multimodal_layers).issubset(layer_ids):
|
||||
raise ValueError("multimodal layer index is outside the transformer")
|
||||
if not set(self.moe.layers).issubset(layer_ids):
|
||||
raise ValueError("MoE layer index is outside the transformer")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2GenerationConfig:
|
||||
duration_seconds: float = 10.0
|
||||
fps: float = 12.5
|
||||
output_frames: int = 125
|
||||
preview_steps: int = 100
|
||||
shift: float = 7.0
|
||||
video_guidance_scale: float = 5.0
|
||||
audio_guidance_scale: float = 7.0
|
||||
audio_latent_fps: float = 25.0
|
||||
video_vae_stride: tuple[int, int, int] = (8, 16, 16)
|
||||
patch_size: tuple[int, int, int] = (1, 1, 1)
|
||||
video_latent_channels: int = 48
|
||||
audio_latent_channels: int = 64
|
||||
audio_sample_rate: int = 44_100
|
||||
|
||||
|
||||
MAGI2_PREVIEW_CONFIG = Magi2PreviewConfig()
|
||||
MAGI2_GENERATION_CONFIG = Magi2GenerationConfig()
|
||||
@@ -0,0 +1,437 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
||||
|
||||
"""Checkpoint-compatible native layers for MAGI-2 Preview.
|
||||
|
||||
The grouped-linear, normalization, Fourier, and mHC formulas are adapted from
|
||||
SandAI's Apache-2.0 preview implementation. They have been modified to remove
|
||||
MagiCompiler and external Triton runtime dependencies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .parallel import Magi2ParallelGroup, get_magi2_tp_group
|
||||
|
||||
|
||||
def swiglu7(
|
||||
x: torch.Tensor,
|
||||
alpha: float = 1.702,
|
||||
limit: float = 7.0,
|
||||
out_dtype: torch.dtype | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Released GPT-OSS-style clamped SwiGLU activation."""
|
||||
|
||||
out_dtype = x.dtype if out_dtype is None else out_dtype
|
||||
x = x.to(torch.float32)
|
||||
gate, linear = x[..., ::2], x[..., 1::2]
|
||||
gate = gate.clamp(max=limit)
|
||||
linear = linear.clamp(min=-limit, max=limit)
|
||||
return (gate * torch.sigmoid(alpha * gate) * (linear + 1.0)).to(out_dtype)
|
||||
|
||||
|
||||
class ModalityDispatcher:
|
||||
"""Precompute stable modality grouping and inverse permutation metadata."""
|
||||
|
||||
def __init__(self, modality_mapping: torch.Tensor, num_modalities: int) -> None:
|
||||
if modality_mapping.ndim != 1:
|
||||
raise ValueError("modality mapping must be a one-dimensional tensor")
|
||||
self.modality_mapping = modality_mapping
|
||||
self.num_modalities = num_modalities
|
||||
self.permute_mapping = torch.argsort(modality_mapping, stable=True)
|
||||
self.inv_permute_mapping = torch.argsort(self.permute_mapping)
|
||||
self.permuted_modality_mapping = modality_mapping.index_select(0, self.permute_mapping)
|
||||
self.group_size = torch.bincount(self.permuted_modality_mapping.long(), minlength=num_modalities).to(
|
||||
torch.int32
|
||||
)
|
||||
self.group_size_cpu = [int(value) for value in self.group_size.cpu().tolist()]
|
||||
self.cu_group_sizes = F.pad(torch.cumsum(self.group_size, dim=0), (1, 0))
|
||||
|
||||
def dispatch(self, tensor: torch.Tensor) -> list[torch.Tensor]:
|
||||
return list(torch.split(tensor, self.group_size_cpu, dim=0))
|
||||
|
||||
@staticmethod
|
||||
def undispatch(*groups: torch.Tensor) -> torch.Tensor:
|
||||
return torch.cat(groups, dim=0)
|
||||
|
||||
def permute(self, tensor: torch.Tensor) -> torch.Tensor:
|
||||
return tensor.index_select(0, self.permute_mapping)
|
||||
|
||||
def inverse_permute(self, tensor: torch.Tensor) -> torch.Tensor:
|
||||
return tensor.index_select(0, self.inv_permute_mapping)
|
||||
|
||||
|
||||
class Magi2GroupedLinear(nn.Module):
|
||||
"""Modality-grouped linear with the released flattened weight layout."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_features: int,
|
||||
out_features: int,
|
||||
*,
|
||||
num_experts: int = 1,
|
||||
bias: bool = False,
|
||||
dtype: torch.dtype | None = None,
|
||||
device: torch.device | str | None = None,
|
||||
parallel_mode: str | None = None,
|
||||
qkv_splits: tuple[int, int, int] | None = None,
|
||||
tp_group: Magi2ParallelGroup | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if parallel_mode not in (None, "column", "row"):
|
||||
raise ValueError(f"unknown MAGI-2 linear parallel mode {parallel_mode!r}")
|
||||
if qkv_splits is not None and parallel_mode != "column":
|
||||
raise ValueError("segmented QKV slicing requires column parallelism")
|
||||
if qkv_splits is not None and sum(qkv_splits) != out_features:
|
||||
raise ValueError("QKV splits must sum to out_features")
|
||||
self.in_features = in_features
|
||||
self.out_features = out_features
|
||||
self.num_experts = num_experts
|
||||
self.parallel_mode = parallel_mode
|
||||
self.qkv_splits = qkv_splits
|
||||
self.tp_group = tp_group or get_magi2_tp_group()
|
||||
self.local_in_features = in_features
|
||||
self.local_out_features = out_features
|
||||
if parallel_mode == "column" and self.tp_group.world_size > 1:
|
||||
split_dims = qkv_splits or (out_features,)
|
||||
if any(size % self.tp_group.world_size for size in split_dims):
|
||||
raise ValueError(
|
||||
f"column-parallel output dimensions {split_dims} must divide TP={self.tp_group.world_size}"
|
||||
)
|
||||
self.local_out_features = sum(size // self.tp_group.world_size for size in split_dims)
|
||||
elif parallel_mode == "row" and self.tp_group.world_size > 1:
|
||||
if in_features % self.tp_group.world_size:
|
||||
raise ValueError(f"row-parallel input {in_features} must divide TP={self.tp_group.world_size}")
|
||||
self.local_in_features = in_features // self.tp_group.world_size
|
||||
|
||||
self.weight = nn.Parameter(
|
||||
torch.empty(
|
||||
num_experts * self.local_out_features,
|
||||
self.local_in_features,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
)
|
||||
)
|
||||
if bias:
|
||||
bias_features = self.local_out_features if parallel_mode == "column" else out_features
|
||||
self.bias = nn.Parameter(torch.empty(num_experts * bias_features, dtype=dtype, device=device))
|
||||
else:
|
||||
self.register_parameter("bias", None)
|
||||
if self.tp_group.world_size > 1 and parallel_mode is not None:
|
||||
self.weight.checkpoint_weight_transform = self.shard_checkpoint_weight
|
||||
if self.bias is not None and parallel_mode == "column":
|
||||
self.bias.checkpoint_weight_transform = self.shard_checkpoint_bias
|
||||
|
||||
def _column_slices(self) -> tuple[tuple[int, int], ...]:
|
||||
splits = self.qkv_splits or (self.out_features,)
|
||||
offsets: list[tuple[int, int]] = []
|
||||
base = 0
|
||||
for size in splits:
|
||||
local = size // self.tp_group.world_size
|
||||
start = base + self.tp_group.rank * local
|
||||
offsets.append((start, start + local))
|
||||
base += size
|
||||
return tuple(offsets)
|
||||
|
||||
def shard_checkpoint_weight(self, checkpoint_tensor: torch.Tensor) -> torch.Tensor:
|
||||
"""Convert the released grouped weight into this TP rank's shard."""
|
||||
|
||||
if tuple(checkpoint_tensor.shape) == tuple(self.weight.shape):
|
||||
return checkpoint_tensor
|
||||
expected = (self.num_experts * self.out_features, self.in_features)
|
||||
if tuple(checkpoint_tensor.shape) != expected:
|
||||
raise ValueError(
|
||||
f"grouped linear checkpoint has shape {tuple(checkpoint_tensor.shape)}, expected {expected}"
|
||||
)
|
||||
grouped = checkpoint_tensor.view(self.num_experts, self.out_features, self.in_features)
|
||||
if self.parallel_mode == "column":
|
||||
shards = [grouped[:, start:end] for start, end in self._column_slices()]
|
||||
return torch.cat(shards, dim=1).reshape(self.num_experts * self.local_out_features, self.in_features)
|
||||
if self.parallel_mode == "row":
|
||||
start = self.tp_group.rank * self.local_in_features
|
||||
return grouped[:, :, start : start + self.local_in_features].reshape(
|
||||
self.num_experts * self.out_features,
|
||||
self.local_in_features,
|
||||
)
|
||||
return checkpoint_tensor
|
||||
|
||||
def shard_checkpoint_bias(self, checkpoint_tensor: torch.Tensor) -> torch.Tensor:
|
||||
if self.bias is None:
|
||||
raise ValueError("cannot load bias into a bias-free grouped linear")
|
||||
if tuple(checkpoint_tensor.shape) == tuple(self.bias.shape):
|
||||
return checkpoint_tensor
|
||||
expected = (self.num_experts * self.out_features,)
|
||||
if tuple(checkpoint_tensor.shape) != expected:
|
||||
raise ValueError(f"grouped linear bias has shape {tuple(checkpoint_tensor.shape)}, expected {expected}")
|
||||
grouped = checkpoint_tensor.view(self.num_experts, self.out_features)
|
||||
shards = [grouped[:, start:end] for start, end in self._column_slices()]
|
||||
return torch.cat(shards, dim=1).reshape(-1)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
modality_dispatcher: ModalityDispatcher | None = None,
|
||||
) -> torch.Tensor:
|
||||
weight = self.weight.view(self.num_experts, self.local_out_features, self.local_in_features)
|
||||
bias_features = self.local_out_features if self.parallel_mode == "column" else self.out_features
|
||||
bias = self.bias.view(self.num_experts, bias_features) if self.bias is not None else None
|
||||
linear_bias = None if self.parallel_mode == "row" else bias
|
||||
if self.num_experts == 1:
|
||||
output = F.linear(tensor, weight[0], None if linear_bias is None else linear_bias[0])
|
||||
else:
|
||||
if modality_dispatcher is None:
|
||||
raise ValueError("modality_dispatcher is required for grouped linear")
|
||||
if modality_dispatcher.num_modalities != self.num_experts:
|
||||
raise ValueError("grouped linear expert count does not match modality dispatcher")
|
||||
inputs = modality_dispatcher.dispatch(tensor)
|
||||
outputs = [
|
||||
F.linear(part, weight[index], None if linear_bias is None else linear_bias[index])
|
||||
for index, part in enumerate(inputs)
|
||||
]
|
||||
output = torch.cat(outputs, dim=0)
|
||||
|
||||
if self.parallel_mode == "row" and self.tp_group.world_size > 1:
|
||||
dist.all_reduce(output, group=self.tp_group.group)
|
||||
if self.parallel_mode == "row" and bias is not None:
|
||||
if self.num_experts == 1:
|
||||
output = output + bias[0]
|
||||
else:
|
||||
assert modality_dispatcher is not None
|
||||
outputs = [part + bias[index] for index, part in enumerate(modality_dispatcher.dispatch(output))]
|
||||
output = torch.cat(outputs, dim=0)
|
||||
return output
|
||||
|
||||
|
||||
def make_grouped_linear(
|
||||
in_features: int,
|
||||
out_features: int,
|
||||
*,
|
||||
num_experts: int = 1,
|
||||
bias: bool = False,
|
||||
dtype: torch.dtype | None = None,
|
||||
parallel_mode: str | None = None,
|
||||
qkv_splits: tuple[int, int, int] | None = None,
|
||||
) -> Magi2GroupedLinear:
|
||||
return Magi2GroupedLinear(
|
||||
in_features,
|
||||
out_features,
|
||||
num_experts=num_experts,
|
||||
bias=bias,
|
||||
dtype=dtype,
|
||||
parallel_mode=parallel_mode,
|
||||
qkv_splits=qkv_splits,
|
||||
)
|
||||
|
||||
|
||||
class MultiModalityRMSNorm(nn.Module):
|
||||
"""RMSNorm with independent modality and mHC-stream scales."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
*,
|
||||
eps: float = 1e-6,
|
||||
num_modality: int = 1,
|
||||
num_patterns: int = 1,
|
||||
out_dtype: torch.dtype | None = None,
|
||||
device: torch.device | str | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.eps = eps
|
||||
self.num_modality = num_modality
|
||||
self.num_patterns = num_patterns
|
||||
self.out_dtype = out_dtype
|
||||
self.weight = nn.Parameter(torch.zeros(num_patterns * dim * num_modality, dtype=torch.float32, device=device))
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
modality_dispatcher: ModalityDispatcher | None = None,
|
||||
) -> torch.Tensor:
|
||||
original_dtype = tensor.dtype
|
||||
normalized = tensor.float()
|
||||
normalized = normalized * torch.rsqrt(normalized.square().mean(dim=-1, keepdim=True) + self.eps)
|
||||
if self.num_modality == 1:
|
||||
weight = self.weight.view(self.num_patterns, self.dim) + 1.0
|
||||
result = normalized * weight
|
||||
else:
|
||||
if modality_dispatcher is None:
|
||||
raise ValueError("modality_dispatcher is required for multimodal RMSNorm")
|
||||
inputs = modality_dispatcher.dispatch(normalized)
|
||||
weights = self.weight.view(self.num_modality, self.num_patterns, self.dim)
|
||||
result = modality_dispatcher.undispatch(
|
||||
*(part * (weights[index] + 1.0) for index, part in enumerate(inputs))
|
||||
)
|
||||
return result.to(self.out_dtype or original_dtype)
|
||||
|
||||
|
||||
def _frequency_bands(
|
||||
num_bands: int,
|
||||
*,
|
||||
temperature: float,
|
||||
device: torch.device | str | None,
|
||||
) -> torch.Tensor:
|
||||
exponent = torch.arange(num_bands, dtype=torch.float32, device=device) / num_bands
|
||||
return 1.0 / temperature**exponent
|
||||
|
||||
|
||||
class ElementWiseFourierEmbed(nn.Module):
|
||||
"""Nine-coordinate Fourier embedding used as MAGI's element-wise RoPE."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
*,
|
||||
temperature: float = 10000.0,
|
||||
learnable: bool = False,
|
||||
device: torch.device | str | None = None,
|
||||
dtype: torch.dtype = torch.float32,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.temperature = temperature
|
||||
bands = _frequency_bands(dim // 8, temperature=temperature, device=device).to(dtype)
|
||||
if learnable:
|
||||
self.bands = nn.Parameter(bands)
|
||||
else:
|
||||
# ``bands`` is part of the released checkpoint. Keep it in the
|
||||
# parameter namespace so the generic DLO mmap loader can bind it
|
||||
# without first materializing a full CPU transformer. It remains
|
||||
# immutable during inference.
|
||||
self.bands = nn.Parameter(bands, requires_grad=False)
|
||||
|
||||
def forward(self, coords: torch.Tensor) -> torch.Tensor:
|
||||
if coords.ndim != 2 or coords.shape[-1] != 9:
|
||||
raise ValueError("MAGI coordinates must have shape [tokens,9]")
|
||||
xyz = coords[:, :3]
|
||||
sizes = coords[:, 3:6]
|
||||
references = coords[:, 6:9]
|
||||
scales = (references - 1) / (sizes - 1)
|
||||
scales = torch.where((references == 1) & (sizes == 1), torch.ones_like(scales), scales)
|
||||
if not torch.isfinite(scales).all():
|
||||
raise ValueError("invalid MAGI coordinate scale")
|
||||
centers = (sizes - 1) / 2
|
||||
centers = centers.clone()
|
||||
centers[:, 0] = 0
|
||||
projection = (xyz - centers).unsqueeze(-1) * scales.unsqueeze(-1) * self.bands
|
||||
return torch.cat((projection.sin(), projection.cos()), dim=1).flatten(1)
|
||||
|
||||
|
||||
MHCTensorTuple = tuple[torch.Tensor, torch.Tensor, torch.Tensor]
|
||||
|
||||
|
||||
def sinkhorn_knopp(matrix_logits: torch.Tensor, iterations: int, epsilon: float) -> torch.Tensor:
|
||||
matrix = torch.exp(matrix_logits - matrix_logits.amax(dim=(-2, -1), keepdim=True))
|
||||
for _ in range(iterations):
|
||||
matrix = matrix / (matrix.sum(dim=-2, keepdim=True) + epsilon)
|
||||
matrix = matrix / (matrix.sum(dim=-1, keepdim=True) + epsilon)
|
||||
return matrix
|
||||
|
||||
|
||||
class MHCHandler:
|
||||
"""Exact four-stream manifold-constrained hyper-connection math."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_streams: int,
|
||||
hidden_size: int,
|
||||
*,
|
||||
sinkhorn_iterations: int = 20,
|
||||
sinkhorn_epsilon: float = 1e-12,
|
||||
dtype: torch.dtype = torch.float32,
|
||||
) -> None:
|
||||
self.num_streams = num_streams
|
||||
self.hidden_size = hidden_size
|
||||
self.sinkhorn_iterations = sinkhorn_iterations
|
||||
self.sinkhorn_epsilon = sinkhorn_epsilon
|
||||
self.dtype = dtype
|
||||
self.matmul_scale = 1.0 / math.sqrt(float(num_streams * hidden_size))
|
||||
|
||||
def flatten(self, tensor: torch.Tensor) -> torch.Tensor:
|
||||
self._check_multi(tensor)
|
||||
return tensor.view(tensor.shape[0], -1)
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
flattened: torch.Tensor,
|
||||
norm: Callable[[torch.Tensor], torch.Tensor],
|
||||
phi_fused: torch.Tensor,
|
||||
) -> MHCTensorTuple:
|
||||
if flattened.ndim != 2 or flattened.shape[-1] != self.num_streams * self.hidden_size:
|
||||
raise ValueError("invalid flattened mHC shape")
|
||||
fused = norm(flattened).to(self.dtype) @ phi_fused
|
||||
pre, post, residual = torch.split(
|
||||
fused,
|
||||
(self.num_streams, self.num_streams, self.num_streams**2),
|
||||
dim=-1,
|
||||
)
|
||||
return pre, post, residual.view(-1, self.num_streams, self.num_streams)
|
||||
|
||||
def apply_pre(
|
||||
self,
|
||||
streams: torch.Tensor,
|
||||
alpha_bias_logits: MHCTensorTuple,
|
||||
*,
|
||||
out_dtype: torch.dtype | None = None,
|
||||
) -> torch.Tensor:
|
||||
self._check_multi(streams)
|
||||
alpha, bias, logits = alpha_bias_logits
|
||||
coefficients = torch.sigmoid(alpha * self.matmul_scale * logits + bias.unsqueeze(0))
|
||||
return torch.einsum("tn,tnc->tc", coefficients.to(out_dtype or streams.dtype), streams)
|
||||
|
||||
def compute_post_residual(
|
||||
self,
|
||||
post: MHCTensorTuple,
|
||||
residual: MHCTensorTuple,
|
||||
*,
|
||||
out_dtype: torch.dtype,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
alpha_post, bias_post, post_logits = post
|
||||
alpha_residual, bias_residual, residual_logits = residual
|
||||
post_coefficients = 2.0 * torch.sigmoid(alpha_post * self.matmul_scale * post_logits + bias_post.unsqueeze(0))
|
||||
residual_matrix = sinkhorn_knopp(
|
||||
alpha_residual * self.matmul_scale * residual_logits.float() + bias_residual.unsqueeze(0).float(),
|
||||
self.sinkhorn_iterations,
|
||||
self.sinkhorn_epsilon,
|
||||
)
|
||||
return post_coefficients.to(out_dtype), residual_matrix.to(out_dtype)
|
||||
|
||||
def hyper_connect(
|
||||
self,
|
||||
residual_streams: torch.Tensor,
|
||||
branch_output: torch.Tensor,
|
||||
post_coefficients: torch.Tensor,
|
||||
residual_matrix: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
self._check_multi(residual_streams)
|
||||
if branch_output.ndim != 2 or branch_output.shape[-1] != self.hidden_size:
|
||||
raise ValueError("invalid mHC branch-output shape")
|
||||
branch = torch.einsum("tn,tc->tnc", post_coefficients, branch_output)
|
||||
mixed = torch.einsum("tij,tjc->tic", residual_matrix, residual_streams)
|
||||
return mixed + branch
|
||||
|
||||
def _check_multi(self, tensor: torch.Tensor) -> None:
|
||||
expected = (self.num_streams, self.hidden_size)
|
||||
if tensor.ndim != 3 or tensor.shape[1:] != expected:
|
||||
raise ValueError(f"expected mHC tensor [tokens,{expected[0]},{expected[1]}], got {tuple(tensor.shape)}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ElementWiseFourierEmbed",
|
||||
"MHCHandler",
|
||||
"Magi2GroupedLinear",
|
||||
"ModalityDispatcher",
|
||||
"MultiModalityRMSNorm",
|
||||
"make_grouped_linear",
|
||||
"sinkhorn_knopp",
|
||||
"swiglu7",
|
||||
]
|
||||
@@ -0,0 +1,547 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright (c) 2025-2026 SandAI. All Rights Reserved.
|
||||
|
||||
"""Native multi-head MoE used by MAGI-2 Preview.
|
||||
|
||||
Adapted from SandAI's Apache-2.0 ``flash_mh_moe`` implementation and modified
|
||||
to use vLLM's existing expert-parallel group. MAGI's routing is unusual: each
|
||||
of twelve 256-wide hidden-state heads independently selects experts from its
|
||||
own 256-expert bank. It is therefore not representable by vLLM's conventional
|
||||
whole-token :class:`FusedMoE` primitive.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from vllm.triton_utils import tl, triton
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
from .parallel import Magi2ParallelGroup, ep_dispatch, ep_undispatch, get_magi2_ep_group
|
||||
|
||||
RoutingScore = Literal["softmax", "sigmoid"]
|
||||
|
||||
|
||||
def swiglu7_pair(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor:
|
||||
"""Released clamped SwiGLU7 expert activation, evaluated in fp32."""
|
||||
|
||||
dtype = gate.dtype
|
||||
gate = gate.float().clamp(max=7.0)
|
||||
up = up.float().clamp(min=-7.0, max=7.0)
|
||||
return (gate * torch.sigmoid(1.702 * gate) * (up + 1.0)).to(dtype)
|
||||
|
||||
|
||||
def compute_topk_probs_and_indices(
|
||||
router_logits: torch.Tensor,
|
||||
top_k: int,
|
||||
*,
|
||||
score_func: RoutingScore = "sigmoid",
|
||||
expert_bias: torch.Tensor | None = None,
|
||||
route_norm: bool = True,
|
||||
norm_eps: float = 1e-12,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Route independently for every ``[head, token]`` pair.
|
||||
|
||||
The auxiliary-free bias affects expert selection but deliberately does not
|
||||
affect the returned routing probability, matching the training recipe.
|
||||
"""
|
||||
|
||||
if router_logits.ndim != 3:
|
||||
raise ValueError("router_logits must be [heads,tokens,experts]")
|
||||
if not 0 < top_k <= router_logits.shape[-1]:
|
||||
raise ValueError("top_k must be in [1, num_experts]")
|
||||
if score_func == "sigmoid":
|
||||
router_scores = torch.sigmoid(router_logits)
|
||||
elif score_func == "softmax":
|
||||
router_scores = torch.softmax(router_logits, dim=-1)
|
||||
else:
|
||||
raise ValueError(f"unsupported routing score function {score_func!r}")
|
||||
selection_scores = router_scores
|
||||
if expert_bias is not None:
|
||||
selection_scores = selection_scores + expert_bias.view(router_logits.shape[0], 1, -1)
|
||||
# Keep the reference's default sorted=True behavior. Besides defining the
|
||||
# route order for ties, this also fixes the reduction order used by the
|
||||
# following L1 normalization.
|
||||
topk_indices = torch.topk(selection_scores, top_k, dim=-1).indices
|
||||
topk_probs = router_scores.gather(-1, topk_indices)
|
||||
if route_norm:
|
||||
topk_probs = F.normalize(topk_probs, p=1, dim=-1, eps=norm_eps)
|
||||
return topk_probs, topk_indices
|
||||
|
||||
|
||||
def global_sort_routes(
|
||||
topk_probs: torch.Tensor,
|
||||
topk_indices: torch.Tensor,
|
||||
num_experts: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Convert per-head routes into a stable flattened-expert CSR layout."""
|
||||
|
||||
if topk_probs.shape != topk_indices.shape or topk_indices.ndim != 3:
|
||||
raise ValueError("top-k probabilities and indices must have the same [H,S,K] shape")
|
||||
heads, sequence, top_k = topk_indices.shape
|
||||
device = topk_indices.device
|
||||
head_offset = torch.arange(heads, device=device).view(heads, 1, 1) * num_experts
|
||||
flattened_experts = (topk_indices + head_offset).reshape(-1)
|
||||
flat_probs = topk_probs.reshape(-1)
|
||||
flat_tokens = torch.arange(sequence, device=device).view(1, sequence, 1).expand(heads, sequence, top_k).reshape(-1)
|
||||
order = flattened_experts.argsort(stable=True)
|
||||
gather_ids = flat_tokens[order].to(torch.int32)
|
||||
sorted_probs = flat_probs[order].float()
|
||||
counts = torch.bincount(flattened_experts, minlength=heads * num_experts)
|
||||
offsets = torch.zeros(heads * num_experts + 1, device=device, dtype=torch.long)
|
||||
offsets[1:] = counts.cumsum(0)
|
||||
return gather_ids, sorted_probs, offsets
|
||||
|
||||
|
||||
def torch_mh_moe_forward(
|
||||
x: torch.Tensor,
|
||||
gather_ids: torch.Tensor,
|
||||
probs: torch.Tensor,
|
||||
expert_offsets: torch.Tensor,
|
||||
w_gate: torch.Tensor,
|
||||
w_up: torch.Tensor,
|
||||
w_down: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Small-shape correctness oracle for the fused expert kernel."""
|
||||
|
||||
if x.ndim != 3:
|
||||
raise ValueError("multi-head MoE input must be [tokens,heads,head_dim]")
|
||||
output = torch.zeros_like(x)
|
||||
experts_per_head = (expert_offsets.numel() - 1) // x.shape[1]
|
||||
for flat_expert in range(expert_offsets.numel() - 1):
|
||||
begin = int(expert_offsets[flat_expert].item())
|
||||
end = int(expert_offsets[flat_expert + 1].item())
|
||||
if begin == end:
|
||||
continue
|
||||
head = flat_expert // experts_per_head
|
||||
token_ids = gather_ids[begin:end].long()
|
||||
expert_input = x.index_select(0, token_ids)[:, head]
|
||||
gate = expert_input @ w_gate[flat_expert]
|
||||
up = expert_input @ w_up[flat_expert]
|
||||
hidden = swiglu7_pair(gate, up)
|
||||
expert_output = hidden @ w_down[flat_expert]
|
||||
expert_output = expert_output * probs[begin:end, None].to(expert_output.dtype)
|
||||
output[:, head].index_add_(0, token_ids, expert_output)
|
||||
return output
|
||||
|
||||
|
||||
_SWIGLU7_ALPHA = tl.constexpr(1.702)
|
||||
_SWIGLU7_LIMIT = tl.constexpr(7.0)
|
||||
_SWIGLU7_BIAS = tl.constexpr(1.0)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _swiglu7_kernel(gate, up, out_dtype: tl.constexpr):
|
||||
gate_clamped = tl.minimum(gate, _SWIGLU7_LIMIT)
|
||||
up_clamped = tl.maximum(tl.minimum(up, _SWIGLU7_LIMIT), -_SWIGLU7_LIMIT)
|
||||
sigmoid = tl.sigmoid(_SWIGLU7_ALPHA * gate_clamped)
|
||||
swish = gate_clamped * sigmoid
|
||||
return (swish * (up_clamped + _SWIGLU7_BIAS)).to(out_dtype)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _binary_search_expert(
|
||||
cumulative_tiles,
|
||||
tile_id,
|
||||
num_experts: tl.constexpr,
|
||||
log2_num_experts: tl.constexpr,
|
||||
):
|
||||
lo = 0
|
||||
hi = num_experts
|
||||
for _ in tl.static_range(0, log2_num_experts + 1):
|
||||
mid = (lo + hi + 1) // 2
|
||||
below = tl.load(cumulative_tiles + mid) <= tile_id
|
||||
lo = tl.where(below, mid, lo)
|
||||
hi = tl.where(below, hi, mid - 1)
|
||||
return lo
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mh_moe_kernel(
|
||||
x_ptr,
|
||||
wg_ptr,
|
||||
wu_ptr,
|
||||
wd_ptr,
|
||||
y_ptr,
|
||||
gather_ids_ptr,
|
||||
probs_ptr,
|
||||
expert_offsets_ptr,
|
||||
cumulative_tiles_ptr,
|
||||
stride_x_s,
|
||||
stride_x_h,
|
||||
stride_x_dh,
|
||||
stride_wg_e,
|
||||
stride_wg_dh,
|
||||
stride_wg_de,
|
||||
stride_wu_e,
|
||||
stride_wu_dh,
|
||||
stride_wu_de,
|
||||
stride_wd_e,
|
||||
stride_wd_de,
|
||||
stride_wd_dh,
|
||||
stride_y_s,
|
||||
stride_y_h,
|
||||
stride_y_dh,
|
||||
d_head: tl.constexpr,
|
||||
d_expert: tl.constexpr,
|
||||
num_heads: tl.constexpr,
|
||||
num_flat_experts: tl.constexpr,
|
||||
log2_num_experts: tl.constexpr,
|
||||
block_t: tl.constexpr,
|
||||
block_dh: tl.constexpr,
|
||||
block_de: tl.constexpr,
|
||||
acc_dtype: tl.constexpr = tl.float32,
|
||||
deterministic: tl.constexpr = False,
|
||||
):
|
||||
tile_id = tl.program_id(0)
|
||||
total_tiles = tl.load(cumulative_tiles_ptr + num_flat_experts)
|
||||
if tile_id >= total_tiles:
|
||||
return
|
||||
|
||||
expert = _binary_search_expert(cumulative_tiles_ptr, tile_id, num_flat_experts, log2_num_experts)
|
||||
expert_i64 = expert.to(tl.int64)
|
||||
head = expert // (num_flat_experts // num_heads)
|
||||
tile_in_expert = tile_id - tl.load(cumulative_tiles_ptr + expert)
|
||||
token_start = tl.load(expert_offsets_ptr + expert) + tile_in_expert * block_t
|
||||
expert_end = tl.load(expert_offsets_ptr + expert + 1)
|
||||
count = tl.minimum(token_start + block_t, expert_end) - token_start
|
||||
|
||||
dh_block_offsets = tl.arange(0, block_dh)
|
||||
de_block_offsets = tl.arange(0, block_de)
|
||||
token_offsets = tl.arange(0, block_t)
|
||||
dh_offsets = tl.arange(0, d_head)
|
||||
|
||||
token_positions = token_start + token_offsets
|
||||
token_mask = token_offsets < count
|
||||
# Token indices fit in int32, but multiplying a large packed-batch index by
|
||||
# the hidden-width stride does not. Promote before computing element offsets.
|
||||
gather_ids = tl.load(gather_ids_ptr + token_positions, mask=token_mask, other=0).to(tl.int64)
|
||||
probabilities = tl.load(probs_ptr + token_positions, mask=token_mask, other=0.0)
|
||||
x_base = gather_ids * stride_x_s + head * stride_x_h
|
||||
output_acc = tl.zeros([block_t, d_head], dtype=acc_dtype)
|
||||
|
||||
for de_start in tl.range(0, d_expert, block_de):
|
||||
de_offsets = de_start + de_block_offsets
|
||||
gate_acc = tl.zeros([block_t, block_de], dtype=acc_dtype)
|
||||
up_acc = tl.zeros([block_t, block_de], dtype=acc_dtype)
|
||||
for dh_start in tl.static_range(0, d_head, block_dh):
|
||||
local_dh = dh_start + dh_block_offsets
|
||||
x_block = tl.load(
|
||||
x_ptr + x_base[:, None] + local_dh[None, :] * stride_x_dh,
|
||||
mask=token_mask[:, None],
|
||||
other=0.0,
|
||||
)
|
||||
wg = tl.load(
|
||||
wg_ptr
|
||||
+ expert_i64 * stride_wg_e
|
||||
+ local_dh[:, None] * stride_wg_dh
|
||||
+ de_offsets[None, :] * stride_wg_de
|
||||
)
|
||||
wu = tl.load(
|
||||
wu_ptr
|
||||
+ expert_i64 * stride_wu_e
|
||||
+ local_dh[:, None] * stride_wu_dh
|
||||
+ de_offsets[None, :] * stride_wu_de
|
||||
)
|
||||
gate_acc += tl.dot(x_block, wg)
|
||||
up_acc += tl.dot(x_block, wu)
|
||||
hidden = _swiglu7_kernel(gate_acc, up_acc, wd_ptr.dtype.element_ty)
|
||||
down = tl.load(
|
||||
wd_ptr + expert_i64 * stride_wd_e + de_offsets[:, None] * stride_wd_de + dh_offsets[None, :] * stride_wd_dh
|
||||
)
|
||||
output_acc += tl.dot(hidden, down)
|
||||
output_acc = output_acc * probabilities[:, None]
|
||||
|
||||
if deterministic:
|
||||
output_ptrs = y_ptr + token_positions[:, None] * stride_y_s + dh_offsets[None, :] * stride_y_dh
|
||||
tl.store(output_ptrs, output_acc.to(y_ptr.dtype.element_ty), mask=token_mask[:, None])
|
||||
else:
|
||||
output_base = gather_ids * stride_y_s + head * stride_y_h
|
||||
output_ptrs = y_ptr + output_base[:, None] + dh_offsets[None, :] * stride_y_dh
|
||||
tl.atomic_add(output_ptrs, output_acc.to(y_ptr.dtype.element_ty), mask=token_mask[:, None])
|
||||
|
||||
|
||||
def _deterministic_scatter(
|
||||
sorted_output: torch.Tensor,
|
||||
reference: torch.Tensor,
|
||||
gather_ids: torch.Tensor,
|
||||
expert_offsets: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
num_flat_experts = expert_offsets.numel() - 1
|
||||
experts_per_head = num_flat_experts // reference.shape[1]
|
||||
expert_lengths = torch.diff(expert_offsets)
|
||||
head_values = torch.arange(num_flat_experts, device=gather_ids.device) // experts_per_head
|
||||
head_ids = torch.repeat_interleave(head_values, expert_lengths)
|
||||
scatter_ids = gather_ids.long() * reference.shape[1] + head_ids.long()
|
||||
output = torch.zeros_like(reference).view(-1, reference.shape[-1])
|
||||
output.scatter_add_(0, scatter_ids[:, None].expand_as(sorted_output), sorted_output.to(output.dtype))
|
||||
return output.view_as(reference)
|
||||
|
||||
|
||||
def _select_block_config() -> tuple[int, int, int, int, int]:
|
||||
"""Return the reference kernel config, capped for pre-Blackwell GPUs."""
|
||||
|
||||
capability = current_omni_platform.get_device_capability()
|
||||
if capability is not None and capability.major >= 10: # Blackwell
|
||||
return (128, 64, 32, 2, 8)
|
||||
# BLOCK_T=128 needs 122,880 bytes of shared memory and is not safe on the
|
||||
# qualified L20X path. This is the reference kernel's portable config.
|
||||
return (64, 64, 32, 2, 4)
|
||||
|
||||
|
||||
def triton_mh_moe_forward(
|
||||
x: torch.Tensor,
|
||||
gather_ids: torch.Tensor,
|
||||
probs: torch.Tensor,
|
||||
expert_offsets: torch.Tensor,
|
||||
w_gate: torch.Tensor,
|
||||
w_up: torch.Tensor,
|
||||
w_down: torch.Tensor,
|
||||
*,
|
||||
deterministic: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Fused gather/expert/scatter kernel for released MAGI dimensions."""
|
||||
|
||||
routed_tokens = gather_ids.numel()
|
||||
if routed_tokens == 0:
|
||||
return torch.zeros_like(x)
|
||||
d_head, d_expert = x.shape[-1], w_down.shape[1]
|
||||
block_t, block_dh, block_de, num_stages, num_warps = _select_block_config()
|
||||
if d_head % block_dh or d_expert % block_de:
|
||||
return torch_mh_moe_forward(x, gather_ids, probs, expert_offsets, w_gate, w_up, w_down)
|
||||
|
||||
if deterministic:
|
||||
output = torch.empty((routed_tokens, 1, d_head), device=x.device, dtype=x.dtype)
|
||||
else:
|
||||
output = torch.zeros_like(x)
|
||||
num_flat_experts = expert_offsets.numel() - 1
|
||||
expert_tiles = (torch.diff(expert_offsets) + block_t - 1) // block_t
|
||||
cumulative_tiles = torch.cat(
|
||||
(torch.zeros(1, dtype=torch.int32, device=x.device), expert_tiles.cumsum(0, dtype=torch.int32))
|
||||
)
|
||||
# Match the reference launch bound. Empty/excess programs return after
|
||||
# comparing against ``cumulative_tiles[-1]`` inside the kernel.
|
||||
grid = ((routed_tokens + block_t - 1) // block_t + num_flat_experts,)
|
||||
log2_experts = max(1, math.ceil(math.log2(max(num_flat_experts, 1) + 1)))
|
||||
_mh_moe_kernel[grid](
|
||||
x,
|
||||
w_gate,
|
||||
w_up,
|
||||
w_down,
|
||||
output,
|
||||
gather_ids,
|
||||
probs,
|
||||
expert_offsets,
|
||||
cumulative_tiles,
|
||||
x.stride(0),
|
||||
x.stride(1),
|
||||
x.stride(2),
|
||||
w_gate.stride(0),
|
||||
w_gate.stride(1),
|
||||
w_gate.stride(2),
|
||||
w_up.stride(0),
|
||||
w_up.stride(1),
|
||||
w_up.stride(2),
|
||||
w_down.stride(0),
|
||||
w_down.stride(1),
|
||||
w_down.stride(2),
|
||||
output.stride(0),
|
||||
output.stride(1),
|
||||
output.stride(2),
|
||||
d_head,
|
||||
d_expert,
|
||||
x.shape[1],
|
||||
num_flat_experts,
|
||||
log2_experts,
|
||||
block_t,
|
||||
block_dh,
|
||||
block_de,
|
||||
tl.float32,
|
||||
deterministic,
|
||||
num_stages=num_stages,
|
||||
num_warps=num_warps,
|
||||
)
|
||||
if deterministic:
|
||||
return _deterministic_scatter(output.view(routed_tokens, d_head), x, gather_ids, expert_offsets)
|
||||
return output
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2MultiHeadMoEConfig:
|
||||
hidden_size: int
|
||||
num_heads: int
|
||||
num_experts: int
|
||||
top_k: int
|
||||
expert_intermediate_size: int
|
||||
params_dtype: torch.dtype
|
||||
score_func: RoutingScore = "sigmoid"
|
||||
route_norm: bool = True
|
||||
route_scale: float = 1.0
|
||||
|
||||
|
||||
class Magi2MultiHeadMoE(nn.Module):
|
||||
"""Checkpoint-compatible MAGI-2 head-routed expert layer."""
|
||||
|
||||
_EP_SHARDED_PARAMETER_NAMES = frozenset(
|
||||
{"gate", "W_gate", "W_up", "W_down", "router.expert_bias", "router.expert_bias_ema"}
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Magi2MultiHeadMoEConfig,
|
||||
*,
|
||||
ep_group: Magi2ParallelGroup | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if config.hidden_size % config.num_heads:
|
||||
raise ValueError("hidden_size must be divisible by the number of MoE heads")
|
||||
self.config = config
|
||||
self.num_heads = config.num_heads
|
||||
self.num_experts = config.num_experts
|
||||
self.top_k = config.top_k
|
||||
self.d_head = config.hidden_size // config.num_heads
|
||||
self.d_expert = config.expert_intermediate_size
|
||||
self.ep_group = ep_group or get_magi2_ep_group()
|
||||
self.padded_num_heads = math.ceil(self.num_heads / self.ep_group.world_size) * self.ep_group.world_size
|
||||
self.local_num_heads = self.padded_num_heads // self.ep_group.world_size
|
||||
self.local_flatten_num_experts = self.local_num_heads * self.num_experts
|
||||
self.ep_pad_heads = self.padded_num_heads - self.num_heads
|
||||
self.local_head_start = self.ep_group.rank * self.local_num_heads
|
||||
self.has_real_moe_heads = self.local_head_start < self.num_heads
|
||||
|
||||
self.gate = nn.Parameter(torch.empty(self.local_flatten_num_experts, self.d_head, dtype=torch.float32))
|
||||
self.W_gate = nn.Parameter(
|
||||
torch.empty(self.local_flatten_num_experts, self.d_head, self.d_expert, dtype=config.params_dtype)
|
||||
)
|
||||
self.W_up = nn.Parameter(
|
||||
torch.empty(self.local_flatten_num_experts, self.d_head, self.d_expert, dtype=config.params_dtype)
|
||||
)
|
||||
self.W_down = nn.Parameter(
|
||||
torch.empty(self.local_flatten_num_experts, self.d_expert, self.d_head, dtype=config.params_dtype)
|
||||
)
|
||||
self.router = nn.Module()
|
||||
# Both tensors are released checkpoint entries. Non-trainable
|
||||
# Parameters let the DLO mmap path bind them on a meta-constructed
|
||||
# model; persistent buffers are intentionally not mmap-loaded by the
|
||||
# generic backend.
|
||||
self.router.expert_bias = nn.Parameter(
|
||||
torch.zeros(self.local_flatten_num_experts, dtype=torch.float32),
|
||||
requires_grad=False,
|
||||
)
|
||||
self.router.expert_bias_ema = nn.Parameter(
|
||||
torch.zeros(self.local_flatten_num_experts, dtype=torch.float32),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
for name in self._EP_SHARDED_PARAMETER_NAMES:
|
||||
target: nn.Module | Magi2MultiHeadMoE = self
|
||||
parts = name.split(".")
|
||||
for part in parts[:-1]:
|
||||
target = getattr(target, part)
|
||||
parameter = getattr(target, parts[-1])
|
||||
parameter.mmap_weight_transform = self.ep_slice
|
||||
|
||||
def ep_slice(self, checkpoint_tensor: torch.Tensor) -> torch.Tensor:
|
||||
"""Slice flattened ``(head,expert)`` checkpoint rows for this rank."""
|
||||
|
||||
if checkpoint_tensor.shape[0] == self.local_flatten_num_experts:
|
||||
return checkpoint_tensor
|
||||
start = self.local_head_start * self.num_experts
|
||||
end = min(start + self.local_flatten_num_experts, checkpoint_tensor.shape[0])
|
||||
if start >= checkpoint_tensor.shape[0]:
|
||||
return torch.zeros(
|
||||
(self.local_flatten_num_experts, *checkpoint_tensor.shape[1:]),
|
||||
dtype=checkpoint_tensor.dtype,
|
||||
device=checkpoint_tensor.device,
|
||||
)
|
||||
local = checkpoint_tensor[start:end]
|
||||
if local.shape[0] < self.local_flatten_num_experts:
|
||||
# Uneven EP/head partitions require materialized zero padding;
|
||||
# divisible production layouts keep the mmap-backed slice above.
|
||||
padding = torch.zeros(
|
||||
(self.local_flatten_num_experts - local.shape[0], *local.shape[1:]),
|
||||
dtype=local.dtype,
|
||||
device=local.device,
|
||||
)
|
||||
local = torch.cat((local, padding), dim=0)
|
||||
return local
|
||||
|
||||
def _route(self, x_heads: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
gate = self.gate.view(self.local_num_heads, self.num_experts, self.d_head).float()
|
||||
logits = torch.einsum("shd,hed->hse", x_heads.float(), gate)
|
||||
bias_source = (os.environ.get("MAGI2_ROUTER_BIAS_SOURCE") or "ema").strip().lower()
|
||||
bias_tensor = self.router.expert_bias if bias_source == "main" else self.router.expert_bias_ema
|
||||
bias = bias_tensor.view(self.local_num_heads, self.num_experts)
|
||||
probs, indices = compute_topk_probs_and_indices(
|
||||
logits,
|
||||
self.top_k,
|
||||
score_func=self.config.score_func,
|
||||
expert_bias=bias,
|
||||
route_norm=self.config.route_norm,
|
||||
)
|
||||
return probs * self.config.route_scale, indices
|
||||
|
||||
def _local_forward(self, x_heads: torch.Tensor) -> torch.Tensor:
|
||||
probabilities, indices = self._route(x_heads)
|
||||
gather_ids, sorted_probs, offsets = global_sort_routes(probabilities, indices, self.num_experts)
|
||||
if x_heads.is_cuda:
|
||||
return triton_mh_moe_forward(
|
||||
x_heads,
|
||||
gather_ids,
|
||||
sorted_probs,
|
||||
offsets,
|
||||
self.W_gate,
|
||||
self.W_up,
|
||||
self.W_down,
|
||||
deterministic=os.environ.get("MAGI2_DETERMINISTIC", "0") == "1",
|
||||
)
|
||||
return torch_mh_moe_forward(x_heads, gather_ids, sorted_probs, offsets, self.W_gate, self.W_up, self.W_down)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if self.ep_group.world_size > 1 and self.ep_group.replicated_sequence:
|
||||
# TP column-parallel ``split_linear`` already emits exactly this
|
||||
# rank's contiguous MoE-head slice. Compute it once and leave it
|
||||
# sharded for the row-parallel ``merge_linear``; no token dispatch
|
||||
# or head all-gather belongs on the true TP path.
|
||||
local_hidden_size = self.local_num_heads * self.d_head
|
||||
if x.shape[-1] != local_hidden_size:
|
||||
raise ValueError(f"TP-local MAGI MoE input has width {x.shape[-1]}, expected {local_hidden_size}")
|
||||
local = x.view(-1, self.local_num_heads, self.d_head)
|
||||
output = self._local_forward(local) if self.has_real_moe_heads else torch.zeros_like(local)
|
||||
return output.reshape(-1, local_hidden_size)
|
||||
|
||||
x_heads = x.view(-1, self.num_heads, self.d_head)
|
||||
if self.ep_pad_heads:
|
||||
padding = x_heads.new_zeros((x_heads.shape[0], self.ep_pad_heads, self.d_head))
|
||||
x_heads = torch.cat((x_heads, padding), dim=1)
|
||||
sequence_split_sizes: list[int] | None = None
|
||||
if self.ep_group.world_size > 1:
|
||||
local_size = torch.tensor([x_heads.shape[0]], dtype=torch.int64, device=x_heads.device)
|
||||
gathered_sizes = [torch.empty_like(local_size) for _ in range(self.ep_group.world_size)]
|
||||
torch.distributed.all_gather(gathered_sizes, local_size, group=self.ep_group.group)
|
||||
sequence_split_sizes = [int(size.item()) for size in gathered_sizes]
|
||||
x_heads = ep_dispatch(x_heads, self.ep_group, sequence_split_sizes)
|
||||
output = self._local_forward(x_heads) if self.has_real_moe_heads else torch.zeros_like(x_heads)
|
||||
if self.ep_group.world_size > 1:
|
||||
output = ep_undispatch(output, self.ep_group, sequence_split_sizes)
|
||||
if self.ep_pad_heads:
|
||||
output = output[:, : self.num_heads]
|
||||
return output.reshape(-1, self.num_heads * self.d_head)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2MultiHeadMoE",
|
||||
"Magi2MultiHeadMoEConfig",
|
||||
"compute_topk_probs_and_indices",
|
||||
"global_sort_routes",
|
||||
"torch_mh_moe_forward",
|
||||
"triton_mh_moe_forward",
|
||||
]
|
||||
@@ -0,0 +1,739 @@
|
||||
# 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 Preview diffusion transformer.
|
||||
|
||||
The architecture and mHC/MoE equations are adapted from SandAI's Apache-2.0
|
||||
MAGI-2 Preview implementation. This modified vLLM-Omni version removes the
|
||||
external ``inference`` package, MagiCompiler, and SandAI process manager while
|
||||
preserving the released checkpoint module names and tensor layouts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
from .attention import Magi2PackedAttentionKernel, VarlenHandler, apply_rotary_emb
|
||||
from .configuration_magi2 import Magi2PreviewConfig
|
||||
from .layers import (
|
||||
ElementWiseFourierEmbed,
|
||||
MHCHandler,
|
||||
ModalityDispatcher,
|
||||
MultiModalityRMSNorm,
|
||||
make_grouped_linear,
|
||||
swiglu7,
|
||||
)
|
||||
from .mh_moe import Magi2MultiHeadMoE, Magi2MultiHeadMoEConfig
|
||||
from .parallel import Magi2SequenceDispatcher
|
||||
|
||||
|
||||
class Modality(IntEnum):
|
||||
VIDEO = 0
|
||||
AUDIO = 1
|
||||
TEXT = 2
|
||||
TIME = 3
|
||||
|
||||
|
||||
class Magi2Attention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: Magi2PreviewConfig,
|
||||
*,
|
||||
num_modality: int,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.num_modality = num_modality
|
||||
self.head_dim = config.head_dim
|
||||
global_q_size = config.num_heads_q * self.head_dim
|
||||
global_kv_size = config.num_heads_kv * self.head_dim
|
||||
|
||||
self.pre_norm = MultiModalityRMSNorm(
|
||||
config.hidden_size,
|
||||
num_modality=num_modality,
|
||||
)
|
||||
self.linear_g = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
config.num_heads_q,
|
||||
num_experts=num_modality,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
)
|
||||
self.linear_qkv = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
global_q_size + 2 * global_kv_size,
|
||||
num_experts=num_modality,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
qkv_splits=(global_q_size, global_kv_size, global_kv_size),
|
||||
)
|
||||
self.linear_proj = make_grouped_linear(
|
||||
global_q_size,
|
||||
config.hidden_size,
|
||||
num_experts=num_modality,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="row",
|
||||
)
|
||||
self.tp_group = self.linear_qkv.tp_group
|
||||
self.num_heads_q = config.num_heads_q // self.tp_group.world_size
|
||||
self.num_heads_kv = config.num_heads_kv // self.tp_group.world_size
|
||||
self.q_size = self.num_heads_q * self.head_dim
|
||||
self.kv_size = self.num_heads_kv * self.head_dim
|
||||
self.sinks = nn.Parameter(torch.empty(config.attention_sink_tokens, self.num_heads_q, dtype=torch.float32))
|
||||
if self.tp_group.world_size > 1:
|
||||
self.sinks.checkpoint_weight_transform = self._shard_sinks
|
||||
self.q_norm = MultiModalityRMSNorm(
|
||||
self.head_dim,
|
||||
num_modality=num_modality,
|
||||
out_dtype=torch.float32,
|
||||
)
|
||||
self.k_norm = MultiModalityRMSNorm(
|
||||
self.head_dim,
|
||||
num_modality=num_modality,
|
||||
out_dtype=torch.float32,
|
||||
)
|
||||
# MAGI-2's packed CFG sequences, learned sink logits, softcap, and
|
||||
# uneven Ulysses splits require a model kernel. Route it through the
|
||||
# framework Attention layer so compile/dispatch ownership remains
|
||||
# shared while the model kernel owns its specialized communication.
|
||||
self.packed_attention = Attention(
|
||||
num_heads=self.num_heads_q,
|
||||
num_kv_heads=self.num_heads_kv,
|
||||
head_size=self.head_dim,
|
||||
causal=False,
|
||||
softmax_scale=self.head_dim**-0.5,
|
||||
qkv_layout="THD",
|
||||
skip_sequence_parallel=True,
|
||||
disable_kv_quant=True,
|
||||
custom_attention=Magi2PackedAttentionKernel(config.attention_softcap),
|
||||
)
|
||||
|
||||
def _shard_sinks(self, checkpoint_tensor: torch.Tensor) -> torch.Tensor:
|
||||
if tuple(checkpoint_tensor.shape) == tuple(self.sinks.shape):
|
||||
return checkpoint_tensor
|
||||
expected = (self.config.attention_sink_tokens, self.config.num_heads_q)
|
||||
if tuple(checkpoint_tensor.shape) != expected:
|
||||
raise ValueError(f"attention sinks have shape {tuple(checkpoint_tensor.shape)}, expected {expected}")
|
||||
start = self.tp_group.rank * self.num_heads_q
|
||||
return checkpoint_tensor[:, start : start + self.num_heads_q]
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
rope: torch.Tensor,
|
||||
varlen_handler: VarlenHandler,
|
||||
modality_dispatcher: ModalityDispatcher,
|
||||
cp_split_sizes: list[int] | torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
normalized = self.pre_norm(hidden_states, modality_dispatcher)
|
||||
gates = self.linear_g(normalized, modality_dispatcher)
|
||||
qkv = self.linear_qkv(normalized, modality_dispatcher)
|
||||
q, k, v = torch.split(qkv, (self.q_size, self.kv_size, self.kv_size), dim=-1)
|
||||
q = q.view(-1, self.num_heads_q, self.head_dim)
|
||||
k = k.view(-1, self.num_heads_kv, self.head_dim)
|
||||
v = v.view(-1, self.num_heads_kv, self.head_dim)
|
||||
gates = gates.view(-1, self.num_heads_q, 1)
|
||||
|
||||
q = self.q_norm(q, modality_dispatcher)
|
||||
k = self.k_norm(k, modality_dispatcher)
|
||||
q = modality_dispatcher.inverse_permute(q).unsqueeze(0)
|
||||
k = modality_dispatcher.inverse_permute(k).unsqueeze(0)
|
||||
v = modality_dispatcher.inverse_permute(v).unsqueeze(0)
|
||||
sin, cos = rope.tensor_split(2, dim=-1)
|
||||
# The released implementation normalizes Q/K in fp32, applies RoPE,
|
||||
# then converts Q/K/V to the checkpoint dtype at the attention
|
||||
# boundary. This conversion is also required by FlashAttention.
|
||||
q = apply_rotary_emb(q, cos, sin).squeeze(0).to(self.config.params_dtype)
|
||||
k = apply_rotary_emb(k, cos, sin).squeeze(0).to(self.config.params_dtype)
|
||||
v = v.squeeze(0).to(self.config.params_dtype)
|
||||
|
||||
output = self.packed_attention(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
AttentionMetadata(
|
||||
extra={
|
||||
"magi2_varlen": varlen_handler,
|
||||
"magi2_split_sizes": cp_split_sizes,
|
||||
"magi2_sink": self.sinks,
|
||||
}
|
||||
),
|
||||
)
|
||||
output = modality_dispatcher.permute(output)
|
||||
output = output * torch.sigmoid(gates)
|
||||
output = output.reshape(-1, self.q_size).to(self.config.params_dtype)
|
||||
return self.linear_proj(output, modality_dispatcher)
|
||||
|
||||
|
||||
class Magi2MLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: Magi2PreviewConfig,
|
||||
*,
|
||||
num_modality: int,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
intermediate_size = int(config.hidden_size * config.intermediate_factor * 2 / 3) // 128 * 128
|
||||
# Tiny unit-test configurations can be narrower than the release's
|
||||
# 128-channel rounding unit while retaining the same formula family.
|
||||
if intermediate_size == 0:
|
||||
intermediate_size = max(1, int(config.hidden_size * config.intermediate_factor * 2 / 3))
|
||||
self.intermediate_size = intermediate_size
|
||||
self.pre_norm = MultiModalityRMSNorm(config.hidden_size, num_modality=num_modality)
|
||||
self.up_gate_proj = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
2 * intermediate_size,
|
||||
num_experts=num_modality,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
)
|
||||
self.down_proj = make_grouped_linear(
|
||||
intermediate_size,
|
||||
config.hidden_size,
|
||||
num_experts=num_modality,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="row",
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor, dispatcher: ModalityDispatcher) -> torch.Tensor:
|
||||
hidden_states = self.pre_norm(hidden_states, dispatcher)
|
||||
hidden_states = self.up_gate_proj(hidden_states, dispatcher)
|
||||
hidden_states = swiglu7(hidden_states)
|
||||
return self.down_proj(hidden_states, dispatcher)
|
||||
|
||||
|
||||
class Magi2MultiHeadMoELayer(nn.Module):
|
||||
def __init__(self, config: Magi2PreviewConfig) -> None:
|
||||
super().__init__()
|
||||
moe = config.moe
|
||||
self.config = config
|
||||
self.pre_norm = MultiModalityRMSNorm(config.hidden_size, num_modality=3)
|
||||
self.split_linear = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
config.hidden_size,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
)
|
||||
self.moe_mlp = Magi2MultiHeadMoE(
|
||||
Magi2MultiHeadMoEConfig(
|
||||
hidden_size=config.hidden_size,
|
||||
num_heads=moe.num_heads,
|
||||
num_experts=moe.num_experts,
|
||||
top_k=moe.top_k,
|
||||
expert_intermediate_size=moe.expert_intermediate_size,
|
||||
params_dtype=config.params_dtype,
|
||||
score_func=moe.score_function, # type: ignore[arg-type]
|
||||
route_norm=moe.normalize_routing_weights,
|
||||
route_scale=moe.routing_scale,
|
||||
)
|
||||
)
|
||||
self.merge_linear = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
config.hidden_size,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="row",
|
||||
)
|
||||
self.shared_expert_fc1 = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
2 * moe.shared_expert_intermediate_size,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
)
|
||||
self.shared_expert_fc2 = make_grouped_linear(
|
||||
moe.shared_expert_intermediate_size,
|
||||
config.hidden_size,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="row",
|
||||
)
|
||||
self.modality_specific_shared_expert_fc1 = make_grouped_linear(
|
||||
config.hidden_size,
|
||||
2 * moe.modality_shared_expert_intermediate_size,
|
||||
num_experts=3,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="column",
|
||||
)
|
||||
self.modality_specific_shared_expert_fc2 = make_grouped_linear(
|
||||
moe.modality_shared_expert_intermediate_size,
|
||||
config.hidden_size,
|
||||
num_experts=3,
|
||||
bias=False,
|
||||
dtype=config.params_dtype,
|
||||
parallel_mode="row",
|
||||
)
|
||||
tp_size = self.split_linear.tp_group.world_size
|
||||
self.local_shared_expert_intermediate_size = moe.shared_expert_intermediate_size // tp_size
|
||||
self.local_modality_shared_expert_intermediate_size = moe.modality_shared_expert_intermediate_size // tp_size
|
||||
|
||||
def _shared_experts(
|
||||
self,
|
||||
normalized: torch.Tensor,
|
||||
dispatcher: ModalityDispatcher,
|
||||
) -> torch.Tensor:
|
||||
shared = self.shared_expert_fc1(normalized)
|
||||
modality = self.modality_specific_shared_expert_fc1(normalized, dispatcher)
|
||||
activated = swiglu7(torch.cat((shared, modality), dim=-1))
|
||||
shared, modality = activated.split(
|
||||
(
|
||||
self.local_shared_expert_intermediate_size,
|
||||
self.local_modality_shared_expert_intermediate_size,
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
return self.shared_expert_fc2(shared) + self.modality_specific_shared_expert_fc2(
|
||||
modality.contiguous(), dispatcher
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor, dispatcher: ModalityDispatcher) -> torch.Tensor:
|
||||
normalized = self.pre_norm(hidden_states, dispatcher)
|
||||
routed = self.split_linear(normalized)
|
||||
routed = self.moe_mlp(routed)
|
||||
routed = self.merge_linear(routed)
|
||||
return routed + self._shared_experts(normalized, dispatcher)
|
||||
|
||||
|
||||
class Magi2PreAdapter(nn.Module):
|
||||
def __init__(self, config: Magi2PreviewConfig) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.adapter_dim = config.virtual_width
|
||||
self.video_embedder = nn.Linear(
|
||||
config.video_in_channels,
|
||||
self.adapter_dim,
|
||||
bias=True,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
self.text_embedder = nn.Linear(
|
||||
config.text_in_channels,
|
||||
self.adapter_dim,
|
||||
bias=True,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
self.audio_embedder = nn.Linear(
|
||||
config.audio_in_channels,
|
||||
self.adapter_dim,
|
||||
bias=True,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
self.rope = ElementWiseFourierEmbed(config.head_dim, learnable=False)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
packed: torch.Tensor,
|
||||
coords_mapping: torch.Tensor,
|
||||
video_indices: torch.Tensor,
|
||||
audio_indices: torch.Tensor,
|
||||
text_indices: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
rope = self.rope(coords_mapping)
|
||||
output = torch.zeros(
|
||||
packed.shape[0],
|
||||
self.adapter_dim,
|
||||
dtype=torch.float32,
|
||||
device=packed.device,
|
||||
)
|
||||
if text_indices.numel():
|
||||
output.index_copy_(
|
||||
0,
|
||||
text_indices,
|
||||
self.text_embedder(packed.index_select(0, text_indices)[:, : self.config.text_in_channels].float()),
|
||||
)
|
||||
if audio_indices.numel():
|
||||
output.index_copy_(
|
||||
0,
|
||||
audio_indices,
|
||||
self.audio_embedder(packed.index_select(0, audio_indices)[:, : self.config.audio_in_channels].float()),
|
||||
)
|
||||
if video_indices.numel():
|
||||
output.index_copy_(
|
||||
0,
|
||||
video_indices,
|
||||
self.video_embedder(packed.index_select(0, video_indices)[:, : self.config.video_in_channels].float()),
|
||||
)
|
||||
return output, rope
|
||||
|
||||
|
||||
class Magi2PostAdapter(nn.Module):
|
||||
def __init__(self, config: Magi2PreviewConfig) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.adapter_dim = config.virtual_width
|
||||
self.final_norm_video = MultiModalityRMSNorm(self.adapter_dim)
|
||||
self.final_norm_audio = MultiModalityRMSNorm(self.adapter_dim)
|
||||
self.final_linear_video = nn.Linear(
|
||||
self.adapter_dim,
|
||||
config.video_in_channels,
|
||||
bias=False,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
self.final_linear_audio = nn.Linear(
|
||||
self.adapter_dim,
|
||||
config.audio_in_channels,
|
||||
bias=False,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
self.final_out_dim = max(config.video_in_channels, config.audio_in_channels)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
video_indices: torch.Tensor,
|
||||
audio_indices: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
output = torch.zeros(
|
||||
hidden_states.shape[0],
|
||||
self.final_out_dim,
|
||||
dtype=torch.float32,
|
||||
device=hidden_states.device,
|
||||
)
|
||||
if video_indices.numel():
|
||||
video = self.final_norm_video(hidden_states.index_select(0, video_indices).float())
|
||||
video = self.final_linear_video(video).float()
|
||||
output[:, : self.config.video_in_channels].index_copy_(0, video_indices, video)
|
||||
if audio_indices.numel():
|
||||
audio = self.final_norm_audio(hidden_states.index_select(0, audio_indices).float())
|
||||
audio = self.final_linear_audio(audio).float()
|
||||
output[:, : self.config.audio_in_channels].index_copy_(0, audio_indices, audio)
|
||||
return output
|
||||
|
||||
|
||||
class Magi2TransformerLayer(nn.Module):
|
||||
def __init__(self, config: Magi2PreviewConfig, layer_index: int) -> None:
|
||||
super().__init__()
|
||||
num_modality = 3 if layer_index in config.multimodal_layers else 1
|
||||
self.config = config
|
||||
self.attention = Magi2Attention(config, num_modality=num_modality)
|
||||
self.mlp: nn.Module
|
||||
if layer_index in config.moe.layers:
|
||||
self.mlp = Magi2MultiHeadMoELayer(config)
|
||||
else:
|
||||
self.mlp = Magi2MLP(config, num_modality=num_modality)
|
||||
self._init_mhc(num_modality)
|
||||
|
||||
def _init_mhc(self, num_modality: int) -> None:
|
||||
streams = self.config.mhc.num_streams
|
||||
hidden = self.config.hidden_size
|
||||
alpha = self.config.mhc.alpha_init
|
||||
for branch in ("attn", "mlp"):
|
||||
for coefficient in ("pre", "post", "res"):
|
||||
self.register_parameter(
|
||||
f"mhc_alpha_{coefficient}_{branch}",
|
||||
nn.Parameter(torch.full((1,), alpha, dtype=torch.float32)),
|
||||
)
|
||||
self.register_parameter(
|
||||
f"mhc_bias_pre_{branch}",
|
||||
nn.Parameter(torch.empty(streams, dtype=torch.float32)),
|
||||
)
|
||||
self.register_parameter(
|
||||
f"mhc_bias_post_{branch}",
|
||||
nn.Parameter(torch.empty(streams, dtype=torch.float32)),
|
||||
)
|
||||
self.register_parameter(
|
||||
f"mhc_bias_res_{branch}",
|
||||
nn.Parameter(torch.empty(streams, streams, dtype=torch.float32)),
|
||||
)
|
||||
self.register_parameter(
|
||||
f"mhc_phi_fused_{branch}",
|
||||
nn.Parameter(
|
||||
torch.empty(
|
||||
streams * hidden,
|
||||
streams + streams + streams * streams,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
),
|
||||
)
|
||||
self.mhc_norm = MultiModalityRMSNorm(
|
||||
streams * hidden,
|
||||
num_modality=num_modality,
|
||||
out_dtype=torch.float32,
|
||||
)
|
||||
self.mhc_handler = MHCHandler(
|
||||
streams,
|
||||
hidden,
|
||||
sinkhorn_iterations=self.config.mhc.sinkhorn_iterations,
|
||||
sinkhorn_epsilon=self.config.mhc.sinkhorn_epsilon,
|
||||
)
|
||||
|
||||
def _branch_logits(
|
||||
self,
|
||||
streams: torch.Tensor,
|
||||
branch: str,
|
||||
dispatcher: ModalityDispatcher,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
return self.mhc_handler.compute_logits(
|
||||
self.mhc_handler.flatten(streams),
|
||||
partial(self.mhc_norm, modality_dispatcher=dispatcher),
|
||||
getattr(self, f"mhc_phi_fused_{branch}"),
|
||||
)
|
||||
|
||||
def _branch_input(
|
||||
self,
|
||||
streams: torch.Tensor,
|
||||
branch: str,
|
||||
logits: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
||||
) -> torch.Tensor:
|
||||
pre_logits, _, _ = logits
|
||||
return self.mhc_handler.apply_pre(
|
||||
streams,
|
||||
(
|
||||
getattr(self, f"mhc_alpha_pre_{branch}"),
|
||||
getattr(self, f"mhc_bias_pre_{branch}"),
|
||||
pre_logits,
|
||||
),
|
||||
out_dtype=streams.dtype,
|
||||
)
|
||||
|
||||
def _connect(
|
||||
self,
|
||||
streams: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
branch: str,
|
||||
logits: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
||||
) -> torch.Tensor:
|
||||
_, post_logits, residual_logits = logits
|
||||
post, residual = self.mhc_handler.compute_post_residual(
|
||||
(
|
||||
getattr(self, f"mhc_alpha_post_{branch}"),
|
||||
getattr(self, f"mhc_bias_post_{branch}"),
|
||||
post_logits,
|
||||
),
|
||||
(
|
||||
getattr(self, f"mhc_alpha_res_{branch}"),
|
||||
getattr(self, f"mhc_bias_res_{branch}"),
|
||||
residual_logits,
|
||||
),
|
||||
out_dtype=streams.dtype,
|
||||
)
|
||||
return self.mhc_handler.hyper_connect(streams, output, post, residual)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
rope: torch.Tensor,
|
||||
varlen_handler: VarlenHandler,
|
||||
modality_dispatcher: ModalityDispatcher,
|
||||
cp_split_sizes: list[int] | torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
streams = hidden_states.reshape(hidden_states.shape[0], self.config.mhc.num_streams, self.config.hidden_size)
|
||||
attention_logits = self._branch_logits(streams, "attn", modality_dispatcher)
|
||||
attention_input = self._branch_input(streams, "attn", attention_logits)
|
||||
attention_output = self.attention(
|
||||
attention_input,
|
||||
rope,
|
||||
varlen_handler,
|
||||
modality_dispatcher,
|
||||
cp_split_sizes,
|
||||
)
|
||||
streams = self._connect(streams, attention_output, "attn", attention_logits)
|
||||
mlp_logits = self._branch_logits(streams, "mlp", modality_dispatcher)
|
||||
mlp_input = self._branch_input(streams, "mlp", mlp_logits)
|
||||
mlp_output = self.mlp(mlp_input, modality_dispatcher)
|
||||
streams = self._connect(streams, mlp_output, "mlp", mlp_logits)
|
||||
return streams.reshape(streams.shape[0], -1)
|
||||
|
||||
|
||||
class Magi2TransformerBlock(nn.Module):
|
||||
def __init__(self, config: Magi2PreviewConfig) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.layers = nn.ModuleList(Magi2TransformerLayer(config, index) for index in range(config.num_layers))
|
||||
|
||||
def __iter__(self):
|
||||
"""Expose checkpoint-nested layers to the layerwise offload backends."""
|
||||
|
||||
return iter(self.layers)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
rope: torch.Tensor,
|
||||
varlen_handler: VarlenHandler,
|
||||
modality_dispatcher: ModalityDispatcher,
|
||||
cp_split_sizes: list[int] | torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
hidden_states = hidden_states.to(self.config.params_dtype)
|
||||
hidden_states = modality_dispatcher.permute(hidden_states)
|
||||
for layer in self.layers:
|
||||
hidden_states = layer(
|
||||
hidden_states,
|
||||
rope,
|
||||
varlen_handler,
|
||||
modality_dispatcher,
|
||||
cp_split_sizes,
|
||||
)
|
||||
return modality_dispatcher.inverse_permute(hidden_states)
|
||||
|
||||
|
||||
def _is_magi2_transformer_layer(_name: str, module: nn.Module) -> bool:
|
||||
"""Shard one Preview layer at a time under the shared FSDP2/HSDP path."""
|
||||
|
||||
return isinstance(module, Magi2TransformerLayer)
|
||||
|
||||
|
||||
class Magi2PreviewTransformer(nn.Module):
|
||||
"""Native preview DiT with the released checkpoint hierarchy."""
|
||||
|
||||
# ``block`` must remain the registered child name because it is part of the
|
||||
# released checkpoint hierarchy. The ``layers`` property below exposes its
|
||||
# ModuleList through the standard layerwise-offload contract without
|
||||
# changing the registered module names.
|
||||
_layerwise_offload_blocks_attrs = ["layers"]
|
||||
_hsdp_shard_conditions = [_is_magi2_transformer_layer]
|
||||
_hsdp_preserve_parameter_dtypes = True
|
||||
_EP_SHARDED_SUFFIXES = (
|
||||
".moe_mlp.gate",
|
||||
".moe_mlp.W_gate",
|
||||
".moe_mlp.W_up",
|
||||
".moe_mlp.W_down",
|
||||
".moe_mlp.router.expert_bias",
|
||||
".moe_mlp.router.expert_bias_ema",
|
||||
)
|
||||
|
||||
def __init__(self, config: Magi2PreviewConfig | None = None) -> None:
|
||||
super().__init__()
|
||||
self.config = config or Magi2PreviewConfig()
|
||||
self.config.validate()
|
||||
self.pre_adapter = Magi2PreAdapter(self.config)
|
||||
self.post_adapter = Magi2PostAdapter(self.config)
|
||||
self.block = Magi2TransformerBlock(self.config)
|
||||
# SP doubles as MAGI's MoE-head parallel axis. These modules therefore
|
||||
# contain different checkpoint slices on each SP rank and must remain
|
||||
# rank-local while HSDP shards the replicated parameters around them.
|
||||
self._hsdp_ignored_modules = [
|
||||
f"block.layers.{index}.mlp.moe_mlp"
|
||||
for index, layer in enumerate(self.block.layers)
|
||||
if isinstance(layer.mlp, Magi2MultiHeadMoELayer)
|
||||
]
|
||||
|
||||
@property
|
||||
def layers(self) -> nn.ModuleList:
|
||||
"""Expose Preview layers to the shared offload block discovery API."""
|
||||
|
||||
return self.block.layers
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
coords_mapping: torch.Tensor,
|
||||
modality_mapping: torch.Tensor,
|
||||
varlen_handler: VarlenHandler,
|
||||
time_token_sequence: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
dispatcher = Magi2SequenceDispatcher()
|
||||
x = dispatcher.dispatch(x)
|
||||
coords_mapping = dispatcher.dispatch(coords_mapping)
|
||||
modality_mapping = dispatcher.dispatch(modality_mapping)
|
||||
if time_token_sequence is not None:
|
||||
time_token_sequence = dispatcher.dispatch(time_token_sequence)
|
||||
assert dispatcher.split_sizes is not None
|
||||
cp_split_sizes = dispatcher.split_sizes
|
||||
|
||||
time_mask = modality_mapping == int(Modality.TIME)
|
||||
modality_mapping = torch.where(time_mask, int(Modality.TEXT), modality_mapping)
|
||||
modality_dispatcher = ModalityDispatcher(modality_mapping, 3)
|
||||
video_indices = torch.nonzero(modality_mapping == int(Modality.VIDEO)).flatten()
|
||||
audio_indices = torch.nonzero(modality_mapping == int(Modality.AUDIO)).flatten()
|
||||
text_indices = torch.nonzero(modality_mapping == int(Modality.TEXT)).flatten()
|
||||
|
||||
hidden_states, rope = self.pre_adapter(
|
||||
x,
|
||||
coords_mapping,
|
||||
video_indices,
|
||||
audio_indices,
|
||||
text_indices,
|
||||
)
|
||||
if time_token_sequence is not None and time_token_sequence.shape[-1] > 0:
|
||||
hidden_states[:, : time_token_sequence.shape[-1]] = time_token_sequence.to(hidden_states.dtype)
|
||||
hidden_states = self.block(
|
||||
hidden_states,
|
||||
rope,
|
||||
varlen_handler,
|
||||
modality_dispatcher,
|
||||
cp_split_sizes,
|
||||
)
|
||||
output = self.post_adapter(hidden_states, video_indices, audio_indices)
|
||||
return dispatcher.undispatch(output)
|
||||
|
||||
def _moe_for_weight(self, name: str) -> Magi2MultiHeadMoE | None:
|
||||
if ".moe_mlp." not in name:
|
||||
return None
|
||||
module_name = name.split(".moe_mlp.", 1)[0] + ".moe_mlp"
|
||||
module = dict(self.named_modules()).get(module_name)
|
||||
return module if isinstance(module, Magi2MultiHeadMoE) else None
|
||||
|
||||
def validate_loaded_weights(self, loaded_names: set[str]) -> None:
|
||||
"""Fail closed when the mmap loader misses a Preview tensor."""
|
||||
|
||||
loaded_names = {name.removeprefix("transformer.") for name in loaded_names}
|
||||
loaded_names = {f"block.{name}" if name.startswith("layers.") else name for name in loaded_names}
|
||||
expected = {name for name, _ in self.named_parameters()}
|
||||
missing = expected - loaded_names
|
||||
if missing:
|
||||
raise ValueError(f"MAGI-2 Preview mmap loading is missing {len(missing)} weights: {sorted(missing)[:8]}")
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Strictly load and TP/MoE-slice the released Preview checkpoint.
|
||||
|
||||
This is the ordinary loader used by resident and rank-local DLO
|
||||
deployments. DLO+AllGather uses the pipeline's mmap mapping and the
|
||||
same per-parameter transforms before orthogonal DP sharding.
|
||||
"""
|
||||
|
||||
targets = self.state_dict(keep_vars=True)
|
||||
loaded: set[str] = set()
|
||||
for raw_name, checkpoint_tensor in weights:
|
||||
name = raw_name.removeprefix("transformer.")
|
||||
if name not in targets:
|
||||
raise ValueError(f"unexpected MAGI-2 preview checkpoint weight {raw_name!r}")
|
||||
target = targets[name]
|
||||
checkpoint_transform = getattr(target, "checkpoint_weight_transform", None)
|
||||
moe = self._moe_for_weight(name)
|
||||
if callable(checkpoint_transform):
|
||||
checkpoint_tensor = checkpoint_transform(checkpoint_tensor)
|
||||
elif moe is not None and name.endswith(self._EP_SHARDED_SUFFIXES):
|
||||
checkpoint_tensor = moe.ep_slice(checkpoint_tensor)
|
||||
if tuple(target.shape) != tuple(checkpoint_tensor.shape):
|
||||
raise ValueError(
|
||||
f"MAGI-2 weight {name!r} has shape {tuple(checkpoint_tensor.shape)}, expected {tuple(target.shape)}"
|
||||
)
|
||||
with torch.no_grad():
|
||||
target.copy_(checkpoint_tensor.to(device=target.device, dtype=target.dtype))
|
||||
loaded.add(name)
|
||||
|
||||
if (os.environ.get("MAGI2_ROUTER_BIAS_SOURCE") or "ema").strip().lower() != "main":
|
||||
for module_name, module in self.named_modules():
|
||||
if not isinstance(module, Magi2MultiHeadMoE):
|
||||
continue
|
||||
module.router.expert_bias.copy_(module.router.expert_bias_ema)
|
||||
loaded.add(f"{module_name}.router.expert_bias")
|
||||
|
||||
missing = set(targets) - loaded
|
||||
if missing:
|
||||
raise ValueError(f"MAGI-2 preview checkpoint is missing {len(missing)} weights: {sorted(missing)[:8]}")
|
||||
return loaded
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2PreviewTransformer",
|
||||
"Modality",
|
||||
"VarlenHandler",
|
||||
]
|
||||
@@ -0,0 +1,392 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright (c) 2025-2026 SandAI. All Rights Reserved.
|
||||
|
||||
"""Distributed primitives for the native MAGI-2 preview transformer.
|
||||
|
||||
This file is adapted from SandAI's Apache-2.0 MAGI-2 preview context- and
|
||||
expert-parallel primitives. It has been modified to reuse vLLM-Omni's
|
||||
already-initialized sequence-parallel and expert-parallel process groups.
|
||||
|
||||
MAGI-2 supports two equivalent layouts for its two parallel views:
|
||||
|
||||
* Ulysses context parallelism shards tokens and gathers attention heads.
|
||||
* Multi-head expert parallelism shards the *MoE head* axis, not experts.
|
||||
|
||||
With SP-only deployment both views use the SP group. With TP enabled,
|
||||
Ulysses keeps using SP while native column/row tensor parallelism and the MoE
|
||||
heads use TP. This gives TP4 and TP2SP2 independent weight/head and token
|
||||
axes while preserving the released equations and checkpoint hierarchy.
|
||||
|
||||
These helpers intentionally do not create or own process groups.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2ParallelGroup:
|
||||
"""The small process-group surface needed by MAGI-2 kernels."""
|
||||
|
||||
group: dist.ProcessGroup | None
|
||||
world_size: int
|
||||
rank: int
|
||||
replicated_sequence: bool = False
|
||||
|
||||
|
||||
def _dist_group_info(group: dist.ProcessGroup | None) -> Magi2ParallelGroup:
|
||||
if group is None or not dist.is_available() or not dist.is_initialized():
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
return Magi2ParallelGroup(
|
||||
group=group,
|
||||
world_size=dist.get_world_size(group),
|
||||
rank=dist.get_rank(group),
|
||||
)
|
||||
|
||||
|
||||
def get_magi2_ulysses_group() -> Magi2ParallelGroup:
|
||||
"""Return vLLM-Omni's Ulysses subgroup, or a rank-local fallback.
|
||||
|
||||
MAGI-2 does not support Ring or AllGather-KV attention. The caller is
|
||||
expected to validate that ``ulysses_degree == sequence_parallel_size``.
|
||||
"""
|
||||
|
||||
if not dist.is_available() or not dist.is_initialized():
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
try:
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
|
||||
coordinator = get_sp_group()
|
||||
except (AssertionError, RuntimeError):
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
return Magi2ParallelGroup(
|
||||
group=coordinator.ulysses_group,
|
||||
world_size=coordinator.ulysses_world_size,
|
||||
rank=coordinator.ulysses_rank,
|
||||
)
|
||||
|
||||
|
||||
def get_magi2_ep_group() -> Magi2ParallelGroup:
|
||||
"""Return the process group used for MAGI's MoE-head parallelism.
|
||||
|
||||
TP is the explicit MoE-head axis when it is larger than one. Otherwise
|
||||
the released SP-only layout overlaps head parallelism with Ulysses. Both
|
||||
groups are initialized and owned by vLLM-Omni; MAGI creates no ad-hoc
|
||||
process groups.
|
||||
"""
|
||||
|
||||
if not dist.is_available() or not dist.is_initialized():
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
try:
|
||||
from vllm.distributed.parallel_state import get_tp_group
|
||||
|
||||
coordinator = get_tp_group()
|
||||
except (AssertionError, RuntimeError):
|
||||
return get_magi2_ulysses_group()
|
||||
if coordinator.world_size <= 1:
|
||||
return get_magi2_ulysses_group()
|
||||
return Magi2ParallelGroup(
|
||||
group=coordinator.device_group,
|
||||
world_size=coordinator.world_size,
|
||||
rank=coordinator.rank_in_group,
|
||||
replicated_sequence=True,
|
||||
)
|
||||
|
||||
|
||||
def get_magi2_tp_group() -> Magi2ParallelGroup:
|
||||
"""Return vLLM's tensor-parallel group without an SP fallback."""
|
||||
|
||||
if not dist.is_available() or not dist.is_initialized():
|
||||
return Magi2ParallelGroup(None, 1, 0, replicated_sequence=True)
|
||||
try:
|
||||
from vllm.distributed.parallel_state import get_tp_group
|
||||
|
||||
coordinator = get_tp_group()
|
||||
except (AssertionError, RuntimeError):
|
||||
return Magi2ParallelGroup(None, 1, 0, replicated_sequence=True)
|
||||
return Magi2ParallelGroup(
|
||||
group=coordinator.device_group,
|
||||
world_size=coordinator.world_size,
|
||||
rank=coordinator.rank_in_group,
|
||||
replicated_sequence=True,
|
||||
)
|
||||
|
||||
|
||||
def get_magi2_replica_group(data_parallel_size: int) -> Magi2ParallelGroup:
|
||||
"""Return the complete TP x SP group for one data-parallel replica.
|
||||
|
||||
The pipeline uses this group for conditioning broadcasts and output-rank
|
||||
ownership. With DP=1 the diffusion world is exactly one TP x SP replica.
|
||||
With DP>1, MAGI currently requires TP=1, so the existing SP group is the
|
||||
complete per-replica group. CFG x DP is deliberately rejected by
|
||||
``_validate_native_topology``; if that combination is enabled in the
|
||||
future, this helper must select a group fixed to one DP replica and include
|
||||
its CFG ranks rather than returning the SP group alone.
|
||||
"""
|
||||
|
||||
if not dist.is_available() or not dist.is_initialized():
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
if data_parallel_size > 1:
|
||||
try:
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
|
||||
coordinator = get_sp_group()
|
||||
except (AssertionError, RuntimeError):
|
||||
return Magi2ParallelGroup(None, 1, 0)
|
||||
return Magi2ParallelGroup(
|
||||
group=coordinator.device_group,
|
||||
world_size=coordinator.world_size,
|
||||
rank=coordinator.rank_in_group,
|
||||
)
|
||||
return _dist_group_info(dist.group.WORLD)
|
||||
|
||||
|
||||
def balanced_split_sizes(length: int, world_size: int) -> list[int]:
|
||||
"""Split ``length`` contiguously, putting one extra token on early ranks."""
|
||||
|
||||
if length < 0:
|
||||
raise ValueError(f"length must be non-negative, got {length}")
|
||||
if world_size < 1:
|
||||
raise ValueError(f"world_size must be positive, got {world_size}")
|
||||
base, remainder = divmod(length, world_size)
|
||||
return [base + int(rank < remainder) for rank in range(world_size)]
|
||||
|
||||
|
||||
def shard_sequence(
|
||||
tensor: torch.Tensor,
|
||||
split_sizes: list[int] | None = None,
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
) -> tuple[torch.Tensor, list[int]]:
|
||||
"""Return this Ulysses rank's contiguous sequence slice.
|
||||
|
||||
Inputs are replicated at the pipeline boundary, matching the diffusion
|
||||
runner contract, so no collective is needed for dispatch.
|
||||
"""
|
||||
|
||||
group = group or get_magi2_ulysses_group()
|
||||
split_sizes = split_sizes or balanced_split_sizes(tensor.shape[0], group.world_size)
|
||||
if len(split_sizes) != group.world_size or sum(split_sizes) != tensor.shape[0]:
|
||||
raise ValueError(
|
||||
f"split_sizes={split_sizes} do not partition sequence length "
|
||||
f"{tensor.shape[0]} across {group.world_size} ranks"
|
||||
)
|
||||
start = sum(split_sizes[: group.rank])
|
||||
return tensor.narrow(0, start, split_sizes[group.rank]).contiguous(), split_sizes
|
||||
|
||||
|
||||
def gather_sequence(
|
||||
tensor: torch.Tensor,
|
||||
split_sizes: list[int],
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Gather uneven contiguous sequence shards in rank order."""
|
||||
|
||||
group = group or get_magi2_ulysses_group()
|
||||
if group.world_size == 1:
|
||||
return tensor
|
||||
if len(split_sizes) != group.world_size:
|
||||
raise ValueError("split_sizes length must equal the Ulysses world size")
|
||||
if tensor.shape[0] != split_sizes[group.rank]:
|
||||
raise ValueError(f"rank {group.rank} owns {tensor.shape[0]} tokens, expected {split_sizes[group.rank]}")
|
||||
output_shape = (sum(split_sizes), *tensor.shape[1:])
|
||||
output = torch.empty(output_shape, dtype=tensor.dtype, device=tensor.device)
|
||||
chunks = list(torch.split(output, split_sizes, dim=0))
|
||||
dist.all_gather(chunks, tensor.contiguous(), group=group.group)
|
||||
return output
|
||||
|
||||
|
||||
def scatter_seqlen_gather_heads(
|
||||
tensor: torch.Tensor,
|
||||
split_sizes: list[int],
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Ulysses ``[sum(S_r), H, D] -> [S_rank, world*H, D]`` exchange."""
|
||||
|
||||
group = group or get_magi2_ulysses_group()
|
||||
if group.world_size == 1:
|
||||
return tensor
|
||||
if tensor.ndim != 3 or not tensor.is_contiguous():
|
||||
raise ValueError("Ulysses attention input must be contiguous [S,H,D]")
|
||||
if len(split_sizes) != group.world_size or sum(split_sizes) != tensor.shape[0]:
|
||||
raise ValueError("split_sizes must partition the input sequence")
|
||||
|
||||
local_tokens = split_sizes[group.rank]
|
||||
output = torch.empty(
|
||||
(group.world_size * local_tokens, tensor.shape[1], tensor.shape[2]),
|
||||
dtype=tensor.dtype,
|
||||
device=tensor.device,
|
||||
)
|
||||
dist.all_to_all_single(
|
||||
output,
|
||||
tensor,
|
||||
output_split_sizes=[local_tokens] * group.world_size,
|
||||
input_split_sizes=split_sizes,
|
||||
group=group.group,
|
||||
)
|
||||
output = (
|
||||
output.view(group.world_size, local_tokens, tensor.shape[1], tensor.shape[2])
|
||||
.permute(1, 0, 2, 3)
|
||||
.reshape(local_tokens, group.world_size * tensor.shape[1], tensor.shape[2])
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def scatter_heads_gather_seqlen(
|
||||
tensors: Iterable[torch.Tensor],
|
||||
split_sizes: list[int],
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
) -> list[torch.Tensor]:
|
||||
"""Inverse batched Ulysses exchange for Q/K/V.
|
||||
|
||||
Each input is ``[S_rank, world*H_i, D]`` and each output is
|
||||
``[sum(S_r), H_i, D]``. Fusing Q/K/V into one all-to-all preserves the
|
||||
reference communication ordering and avoids three independent collectives.
|
||||
"""
|
||||
|
||||
group = group or get_magi2_ulysses_group()
|
||||
tensors = list(tensors)
|
||||
if group.world_size == 1:
|
||||
return tensors
|
||||
if not tensors:
|
||||
return []
|
||||
local_tokens = split_sizes[group.rank]
|
||||
if len(split_sizes) != group.world_size:
|
||||
raise ValueError("split_sizes length must equal the Ulysses world size")
|
||||
if any(t.ndim != 3 or t.shape[0] != local_tokens for t in tensors):
|
||||
raise ValueError("all Ulysses inputs must be [local_tokens, heads, dim]")
|
||||
|
||||
reshaped: list[torch.Tensor] = []
|
||||
local_head_counts: list[int] = []
|
||||
head_dim = tensors[0].shape[-1]
|
||||
for tensor in tensors:
|
||||
if tensor.shape[-1] != head_dim or tensor.shape[1] % group.world_size:
|
||||
raise ValueError("attention heads must divide evenly across Ulysses ranks")
|
||||
local_heads = tensor.shape[1] // group.world_size
|
||||
local_head_counts.append(local_heads)
|
||||
reshaped.append(
|
||||
tensor.view(local_tokens, group.world_size, local_heads, head_dim)
|
||||
.permute(1, 0, 2, 3)
|
||||
.reshape(group.world_size * local_tokens, local_heads, head_dim)
|
||||
)
|
||||
|
||||
fused = torch.cat(reshaped, dim=1).contiguous()
|
||||
output = torch.empty(
|
||||
(sum(split_sizes), fused.shape[1], head_dim),
|
||||
dtype=fused.dtype,
|
||||
device=fused.device,
|
||||
)
|
||||
dist.all_to_all_single(
|
||||
output,
|
||||
fused,
|
||||
output_split_sizes=split_sizes,
|
||||
input_split_sizes=[local_tokens] * group.world_size,
|
||||
group=group.group,
|
||||
)
|
||||
return list(torch.split(output, local_head_counts, dim=1))
|
||||
|
||||
|
||||
def ep_dispatch(
|
||||
tensor: torch.Tensor,
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
sequence_split_sizes: list[int] | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Dispatch ``[S,H,D]`` so each rank evaluates a contiguous head shard."""
|
||||
|
||||
group = group or get_magi2_ep_group()
|
||||
if group.world_size == 1:
|
||||
return tensor
|
||||
if tensor.ndim != 3 or tensor.shape[1] % group.world_size:
|
||||
raise ValueError(
|
||||
f"MoE head count {tensor.shape[1] if tensor.ndim >= 2 else '?'} must divide by EP size {group.world_size}"
|
||||
)
|
||||
sequence, heads, dim = tensor.shape
|
||||
local_heads = heads // group.world_size
|
||||
if sequence_split_sizes is None:
|
||||
local_size = torch.tensor([sequence], dtype=torch.int64, device=tensor.device)
|
||||
gathered_sizes = [torch.empty_like(local_size) for _ in range(group.world_size)]
|
||||
dist.all_gather(gathered_sizes, local_size, group=group.group)
|
||||
sequence_split_sizes = [int(size.item()) for size in gathered_sizes]
|
||||
if len(sequence_split_sizes) != group.world_size or sequence_split_sizes[group.rank] != sequence:
|
||||
raise ValueError("EP sequence split sizes do not describe the local tensor")
|
||||
send = tensor.contiguous().view(sequence, group.world_size, local_heads, dim).permute(1, 0, 2, 3).contiguous()
|
||||
output = torch.empty(
|
||||
(sum(sequence_split_sizes), local_heads, dim),
|
||||
dtype=tensor.dtype,
|
||||
device=tensor.device,
|
||||
)
|
||||
row_width = local_heads * dim
|
||||
dist.all_to_all_single(
|
||||
output.view(-1),
|
||||
send.view(-1),
|
||||
output_split_sizes=[size * row_width for size in sequence_split_sizes],
|
||||
input_split_sizes=[sequence * row_width] * group.world_size,
|
||||
group=group.group,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def ep_undispatch(
|
||||
tensor: torch.Tensor,
|
||||
group: Magi2ParallelGroup | None = None,
|
||||
sequence_split_sizes: list[int] | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Undo :func:`ep_dispatch` and restore the complete MoE head axis."""
|
||||
|
||||
group = group or get_magi2_ep_group()
|
||||
if group.world_size == 1:
|
||||
return tensor
|
||||
if tensor.ndim != 3:
|
||||
raise ValueError("EP-dispatched tensor must be [global_tokens,local_heads,dim]")
|
||||
if sequence_split_sizes is None:
|
||||
if tensor.shape[0] % group.world_size:
|
||||
raise ValueError("uneven EP sequence requires explicit split sizes")
|
||||
sequence_split_sizes = [tensor.shape[0] // group.world_size] * group.world_size
|
||||
if len(sequence_split_sizes) != group.world_size or sum(sequence_split_sizes) != tensor.shape[0]:
|
||||
raise ValueError("EP sequence split sizes do not partition the global tensor")
|
||||
sequence = sequence_split_sizes[group.rank]
|
||||
local_heads, dim = tensor.shape[1:]
|
||||
send = tensor.contiguous()
|
||||
output = torch.empty(
|
||||
(group.world_size, sequence, local_heads, dim),
|
||||
dtype=tensor.dtype,
|
||||
device=tensor.device,
|
||||
)
|
||||
row_width = local_heads * dim
|
||||
dist.all_to_all_single(
|
||||
output.view(-1),
|
||||
send.view(-1),
|
||||
output_split_sizes=[sequence * row_width] * group.world_size,
|
||||
input_split_sizes=[size * row_width for size in sequence_split_sizes],
|
||||
group=group.group,
|
||||
)
|
||||
return output.permute(1, 0, 2, 3).contiguous().view(sequence, group.world_size * local_heads, dim)
|
||||
|
||||
|
||||
class Magi2SequenceDispatcher:
|
||||
"""Request-scoped dispatcher that enforces one consistent token split."""
|
||||
|
||||
def __init__(self, group: Magi2ParallelGroup | None = None) -> None:
|
||||
self.group = group or get_magi2_ulysses_group()
|
||||
self.split_sizes: list[int] | None = None
|
||||
|
||||
def dispatch(self, tensor: torch.Tensor) -> torch.Tensor:
|
||||
split_sizes = balanced_split_sizes(tensor.shape[0], self.group.world_size)
|
||||
if self.split_sizes is not None and split_sizes != self.split_sizes:
|
||||
raise ValueError(
|
||||
f"all packed inputs must share a sequence length; got {split_sizes}, expected {self.split_sizes}"
|
||||
)
|
||||
self.split_sizes = split_sizes
|
||||
return shard_sequence(tensor, split_sizes, self.group)[0]
|
||||
|
||||
def undispatch(self, tensor: torch.Tensor) -> torch.Tensor:
|
||||
if self.split_sizes is None:
|
||||
raise RuntimeError("dispatch must be called before undispatch")
|
||||
output = gather_sequence(tensor, self.split_sizes, self.group)
|
||||
self.split_sizes = None
|
||||
return output
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,691 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Copyright (c) 2026 SandAI. All Rights Reserved.
|
||||
|
||||
"""Native packing helpers for the MAGI-2 Preview transformer.
|
||||
|
||||
The released model consumes one varlen sequence per sample. Tokens are laid
|
||||
out in VIDEO -> AUDIO -> TEXT order, followed by zero or more reference-image
|
||||
special-token/image-token pairs. This module keeps that layout and its 9-D
|
||||
coordinate metadata independent of any particular distributed topology.
|
||||
|
||||
The packing math is adapted from SandAI's Apache-2.0 MAGI-2 Preview inference
|
||||
implementation. It intentionally has no dependency on that implementation at
|
||||
runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from itertools import chain
|
||||
from typing import Literal
|
||||
|
||||
import torch
|
||||
from einops import rearrange
|
||||
from torch.nn import functional as F
|
||||
|
||||
from .attention import VarlenHandler
|
||||
|
||||
|
||||
class Modality(IntEnum):
|
||||
"""Checkpoint modality IDs used by the pre/post adapters."""
|
||||
|
||||
VIDEO = 0
|
||||
AUDIO = 1
|
||||
TEXT = 2
|
||||
# TIME is remapped to TEXT by the transformer after selecting time tokens.
|
||||
TIME = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Magi2PreviewDataProxyConfig:
|
||||
"""Packing configuration for the released preview checkpoint."""
|
||||
|
||||
t_patch_size: int = 1
|
||||
patch_size: int = 1
|
||||
spatial_rope_interpolation: Literal["inter", "extra"] = "extra"
|
||||
add_time_token: bool = False
|
||||
time_channel_dim: int = 64
|
||||
time_aligned_rope: bool = False
|
||||
audio_latent_fps: float = 25.0
|
||||
time_pos_fps: float = 3.125
|
||||
vae_first_latent_is_image: bool = True
|
||||
video_fps: float = 25.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.t_patch_size <= 0 or self.patch_size <= 0:
|
||||
raise ValueError("MAGI-2 patch sizes must be positive")
|
||||
if self.time_channel_dim < 0:
|
||||
raise ValueError("time_channel_dim must be non-negative")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelInput:
|
||||
"""Unpacked tensors for one native preview-transformer invocation."""
|
||||
|
||||
x_t: torch.Tensor
|
||||
audio_x_t: torch.Tensor
|
||||
audio_feat_len: torch.Tensor | Sequence[int]
|
||||
txt_feat: torch.Tensor
|
||||
txt_feat_len: torch.Tensor | Sequence[int]
|
||||
t: torch.Tensor
|
||||
ref_audio_feat: torch.Tensor | None = None
|
||||
ref_audio_feat_len: torch.Tensor | Sequence[int] | None = None
|
||||
ref_video_feat: torch.Tensor | None = None
|
||||
ref_video_feat_len: torch.Tensor | Sequence[int] | None = None
|
||||
per_token_video_t: torch.Tensor | None = None
|
||||
per_token_audio_t: torch.Tensor | None = None
|
||||
# Images are [B, M, C, 1, H, W], lengths are [B, M, 2] holding
|
||||
# the latent patch-grid (H, W), and special tokens are [B, M, D].
|
||||
ref_image_feat: torch.Tensor | None = None
|
||||
ref_image_feat_len: torch.Tensor | None = None
|
||||
ref_image_special_token_embedding: torch.Tensor | None = None
|
||||
|
||||
|
||||
def _to_int(value: int | torch.Tensor) -> int:
|
||||
if isinstance(value, torch.Tensor):
|
||||
return int(value.detach().reshape(-1)[0].item())
|
||||
return int(value)
|
||||
|
||||
|
||||
def _length_at(value: torch.Tensor | Sequence[int], index: int) -> int:
|
||||
if isinstance(value, torch.Tensor) and value.ndim == 0:
|
||||
if index:
|
||||
raise IndexError("a scalar length only describes one sample")
|
||||
return _to_int(value)
|
||||
return _to_int(value[index])
|
||||
|
||||
|
||||
def _pad_cat(tensors: list[torch.Tensor], device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
||||
if not tensors:
|
||||
return torch.empty(0, 0, device=device, dtype=dtype)
|
||||
max_channel = max(tensor.shape[-1] for tensor in tensors)
|
||||
return torch.cat(
|
||||
[F.pad(tensor.to(device=device, dtype=dtype), (0, max_channel - tensor.shape[-1])) for tensor in tensors],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
|
||||
def _segment_paint(
|
||||
values: list[int],
|
||||
seqlens: list[int],
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
output_size: int,
|
||||
) -> torch.Tensor:
|
||||
mapping = torch.empty(output_size, dtype=dtype, device=device)
|
||||
offset = 0
|
||||
for value, seqlen in zip(values, seqlens, strict=True):
|
||||
mapping[offset : offset + seqlen] = int(value)
|
||||
offset += seqlen
|
||||
if offset != output_size:
|
||||
raise ValueError(f"segment lengths total {offset}, expected {output_size}")
|
||||
return mapping
|
||||
|
||||
|
||||
def _seqlens_to_cu_seqlens(seqlens: list[int], device: torch.device | None = None) -> torch.Tensor:
|
||||
lengths = torch.tensor(seqlens, dtype=torch.int32, device=device)
|
||||
return F.pad(torch.cumsum(lengths, dim=0), (1, 0))
|
||||
|
||||
|
||||
def _len_to_list(value: torch.Tensor) -> list[int]:
|
||||
return [int(item) for item in value.detach().to(torch.long).reshape(-1).tolist()]
|
||||
|
||||
|
||||
def get_coords(
|
||||
shape: tuple[int, int, int],
|
||||
ref_feat_shape: tuple[int, int, int],
|
||||
offset_thw: tuple[int, int, int] = (0, 0, 0),
|
||||
*,
|
||||
device: torch.device = torch.device("cpu"),
|
||||
dtype: torch.dtype = torch.float32,
|
||||
time_positions: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Build MAGI-2's ``(t,h,w,T,H,W,ref_T,ref_H,ref_W)`` rows."""
|
||||
|
||||
ori_t, ori_h, ori_w = shape
|
||||
ref_t, ref_h, ref_w = ref_feat_shape
|
||||
offset_t, offset_h, offset_w = offset_thw
|
||||
|
||||
if time_positions is None:
|
||||
time_range = torch.arange(ori_t, device=device, dtype=dtype) + offset_t
|
||||
else:
|
||||
if time_positions.numel() != ori_t:
|
||||
raise ValueError(f"received {time_positions.numel()} time positions for {ori_t} time steps")
|
||||
time_range = time_positions.to(device=device, dtype=dtype) + offset_t
|
||||
height_range = torch.arange(ori_h, device=device, dtype=dtype) + offset_h
|
||||
width_range = torch.arange(ori_w, device=device, dtype=dtype) + offset_w
|
||||
time_grid, height_grid, width_grid = torch.meshgrid(
|
||||
time_range,
|
||||
height_range,
|
||||
width_range,
|
||||
indexing="ij",
|
||||
)
|
||||
coords = torch.stack((time_grid, height_grid, width_grid), dim=-1).reshape(-1, 3)
|
||||
metadata = torch.tensor(
|
||||
(ori_t, ori_h, ori_w, ref_t, ref_h, ref_w),
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
return torch.cat((coords, metadata.expand(coords.shape[0], -1)), dim=-1)
|
||||
|
||||
|
||||
def sinusoidal_embedding_1d(dim: int, position: torch.Tensor) -> torch.Tensor:
|
||||
"""The preview checkpoint's FP32 diffusion-time channel embedding."""
|
||||
|
||||
position = position.to(torch.float32) * 1000.0
|
||||
half = dim // 2
|
||||
frequencies = torch.exp(
|
||||
-math.log(10000) * torch.arange(0, half, dtype=torch.float32, device=position.device) / half
|
||||
)
|
||||
arguments = position[:, None].float() * frequencies[None]
|
||||
embedding = torch.cat((torch.cos(arguments), torch.sin(arguments)), dim=-1)
|
||||
if dim % 2:
|
||||
embedding = torch.cat((embedding, torch.zeros_like(embedding[:, :1])), dim=-1)
|
||||
return embedding
|
||||
|
||||
|
||||
def _patchify_3d(x: torch.Tensor, t_patch_size: int, patch_size: int) -> torch.Tensor:
|
||||
"""Non-overlapping 3-D unfold with the released channel-major order."""
|
||||
|
||||
kernel = (t_patch_size, patch_size, patch_size)
|
||||
if x.ndim != 5:
|
||||
raise ValueError(f"expected [B,C,T,H,W], got {tuple(x.shape)}")
|
||||
if not all(size >= kernel_size for size, kernel_size in zip(x.shape[2:], kernel, strict=True)):
|
||||
return torch.empty(
|
||||
x.shape[0],
|
||||
0,
|
||||
x.shape[1] * math.prod(kernel),
|
||||
device=x.device,
|
||||
dtype=x.dtype,
|
||||
)
|
||||
|
||||
patches = x.unfold(2, t_patch_size, t_patch_size)
|
||||
patches = patches.unfold(3, patch_size, patch_size)
|
||||
patches = patches.unfold(4, patch_size, patch_size)
|
||||
# [B,C,T,H,W,pT,pH,pW] -> [B,T*H*W,C*pT*pH*pW]
|
||||
patches = patches.permute(0, 2, 3, 4, 1, 5, 6, 7).contiguous()
|
||||
return patches.reshape(x.shape[0], -1, x.shape[1] * math.prod(kernel))
|
||||
|
||||
|
||||
@dataclass
|
||||
class SingleData:
|
||||
"""Packed metadata for one sample."""
|
||||
|
||||
video_x_t: torch.Tensor
|
||||
audio_x_t: torch.Tensor
|
||||
audio_feat_len: int
|
||||
txt_feat: torch.Tensor
|
||||
txt_feat_len: int
|
||||
t: int
|
||||
h: int
|
||||
w: int
|
||||
patch_size: int
|
||||
t_patch_size: int
|
||||
spatial_rope_interpolation: Literal["inter", "extra"]
|
||||
diffusion_t: torch.Tensor | None = None
|
||||
per_token_video_t: torch.Tensor | None = None
|
||||
per_token_audio_t: torch.Tensor | None = None
|
||||
time_channel_dim: int = 0
|
||||
vae_first_latent_is_image: bool = True
|
||||
video_fps: float = 25.0
|
||||
time_pos_fps: float = 3.125
|
||||
ref_image_feats: list[torch.Tensor] | None = None
|
||||
ref_image_feat_lens: list[list[int]] | None = None
|
||||
ref_image_special_tokens: list[torch.Tensor] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.video_token_num = self.video_x_t.shape[0]
|
||||
self.origin_audio_feat_len = self.audio_x_t.shape[0]
|
||||
self.audio_x_t = self.audio_x_t[: self.audio_feat_len]
|
||||
self.txt_feat = self.txt_feat[: self.txt_feat_len]
|
||||
if self.per_token_audio_t is not None:
|
||||
self.per_token_audio_t = self.per_token_audio_t[: self.audio_feat_len]
|
||||
|
||||
self.ref_image_feats = self.ref_image_feats or []
|
||||
self.ref_image_feat_lens = self.ref_image_feat_lens or []
|
||||
self.ref_image_special_tokens = self.ref_image_special_tokens or []
|
||||
if not (len(self.ref_image_feats) == len(self.ref_image_feat_lens) == len(self.ref_image_special_tokens)):
|
||||
raise ValueError("reference-image features, lengths, and special tokens must have equal counts")
|
||||
self.ref_image_token_nums = [math.prod(feat_len) for feat_len in self.ref_image_feat_lens]
|
||||
self.ref_image_feats = [
|
||||
feature[:token_num]
|
||||
for feature, token_num in zip(self.ref_image_feats, self.ref_image_token_nums, strict=True)
|
||||
]
|
||||
self.num_ref_images = len(self.ref_image_feats)
|
||||
self.total_ref_image_feat_len = sum(self.ref_image_token_nums)
|
||||
self.video_channel = self.video_x_t.shape[-1]
|
||||
self.audio_channel = self.audio_x_t.shape[-1]
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return self.video_x_t.device
|
||||
|
||||
@property
|
||||
def default_dtype(self) -> torch.dtype:
|
||||
return self.video_x_t.dtype
|
||||
|
||||
@property
|
||||
def add_time_token(self) -> bool:
|
||||
return self.diffusion_t is not None
|
||||
|
||||
@property
|
||||
def total_token_num(self) -> int:
|
||||
total = self.video_token_num + self.audio_feat_len + self.txt_feat_len
|
||||
total += self.total_ref_image_feat_len + self.num_ref_images
|
||||
return total + int(self.add_time_token)
|
||||
|
||||
@property
|
||||
def feat_to_cat(self) -> list[torch.Tensor]:
|
||||
tensors = [self.video_x_t, self.audio_x_t, self.txt_feat]
|
||||
for index in range(self.num_ref_images):
|
||||
tensors.append(self.ref_image_special_tokens[index])
|
||||
tensors.append(self.ref_image_feats[index])
|
||||
if self.add_time_token:
|
||||
tensors.append(self.diffusion_t.to(device=self.device, dtype=self.default_dtype).reshape(1, 1))
|
||||
return tensors
|
||||
|
||||
@property
|
||||
def token_sequence(self) -> torch.Tensor:
|
||||
return _pad_cat(self.feat_to_cat, self.device, self.default_dtype)
|
||||
|
||||
@property
|
||||
def modality_map_seqlens(self) -> tuple[list[int], list[int]]:
|
||||
seqlens = [self.video_token_num, self.audio_feat_len, self.txt_feat_len]
|
||||
modalities = [Modality.VIDEO, Modality.AUDIO, Modality.TEXT]
|
||||
for index in range(self.num_ref_images):
|
||||
seqlens.extend((1, self.ref_image_token_nums[index]))
|
||||
modalities.extend((Modality.TEXT, Modality.VIDEO))
|
||||
if self.add_time_token:
|
||||
seqlens.append(1)
|
||||
modalities.append(Modality.TIME)
|
||||
return seqlens, modalities
|
||||
|
||||
@property
|
||||
def modality_mapping(self) -> torch.Tensor:
|
||||
seqlens, modalities = self.modality_map_seqlens
|
||||
return _segment_paint(
|
||||
modalities,
|
||||
seqlens,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
output_size=self.total_token_num,
|
||||
)
|
||||
|
||||
def _default_coords(
|
||||
self,
|
||||
shape: tuple[int, int, int],
|
||||
ref_feat_shape: tuple[int, int, int],
|
||||
offset_thw: tuple[int, int, int] = (0, 0, 0),
|
||||
time_positions: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
return get_coords(
|
||||
shape,
|
||||
ref_feat_shape,
|
||||
offset_thw,
|
||||
device=self.device,
|
||||
dtype=self.default_dtype,
|
||||
time_positions=time_positions,
|
||||
)
|
||||
|
||||
@property
|
||||
def coords_to_cat(self) -> list[torch.Tensor]:
|
||||
t_steps = self.t // self.t_patch_size
|
||||
h_steps = self.h // self.patch_size
|
||||
w_steps = self.w // self.patch_size
|
||||
if self.spatial_rope_interpolation == "inter":
|
||||
video_h_ref, video_w_ref = 32, 32
|
||||
else:
|
||||
video_h_ref, video_w_ref = h_steps, w_steps
|
||||
|
||||
video_coords = self._default_coords(
|
||||
(t_steps, h_steps, w_steps),
|
||||
(t_steps, video_h_ref, video_w_ref),
|
||||
)
|
||||
magic_audio_ref_t = (self.audio_feat_len - 1) // 8 + 1
|
||||
audio_coords = self._default_coords(
|
||||
(self.audio_feat_len, 1, 1),
|
||||
(magic_audio_ref_t // self.t_patch_size, 1, 1),
|
||||
)
|
||||
coords = [
|
||||
video_coords,
|
||||
audio_coords,
|
||||
self._default_coords(
|
||||
(self.txt_feat_len, 1, 1),
|
||||
(1, 1, 1),
|
||||
offset_thw=(-self.txt_feat_len, 0, 0),
|
||||
),
|
||||
]
|
||||
|
||||
for index in range(self.num_ref_images):
|
||||
token_len = self.ref_image_token_nums[index]
|
||||
if len(self.ref_image_feat_lens[index]) >= 2:
|
||||
image_h, image_w = self.ref_image_feat_lens[index][:2]
|
||||
else:
|
||||
image_h = image_w = math.ceil(math.sqrt(token_len))
|
||||
# Keep image context after the generated clip with a one-step gap.
|
||||
time_offset = t_steps + 2 + index
|
||||
coords.append(
|
||||
torch.tensor(
|
||||
[[time_offset, -1, -1, 1, image_h, image_w, 1, image_h, image_w]],
|
||||
device=self.device,
|
||||
dtype=self.default_dtype,
|
||||
)
|
||||
)
|
||||
coords.append(
|
||||
self._default_coords(
|
||||
(1, image_h, image_w),
|
||||
(1, image_h, image_w),
|
||||
offset_thw=(time_offset, 0, 0),
|
||||
)[:token_len]
|
||||
)
|
||||
|
||||
if self.add_time_token:
|
||||
coords.append(self._default_coords((1, 1, 1), (1, 1, 1))[:1])
|
||||
return coords
|
||||
|
||||
@property
|
||||
def coords_mapping(self) -> torch.Tensor:
|
||||
return torch.cat(self.coords_to_cat, dim=0)
|
||||
|
||||
@property
|
||||
def time_token_sequence(self) -> torch.Tensor:
|
||||
if self.time_channel_dim == 0:
|
||||
return torch.empty(self.total_token_num, 0, device=self.device)
|
||||
if self.per_token_video_t is None or self.per_token_audio_t is None:
|
||||
raise ValueError("per-token video and audio timesteps are required when time_channel_dim is non-zero")
|
||||
parts = [
|
||||
self.per_token_video_t.squeeze(-1),
|
||||
self.per_token_audio_t.squeeze(-1),
|
||||
torch.zeros(self.txt_feat_len, device=self.device),
|
||||
]
|
||||
for index in range(self.num_ref_images):
|
||||
parts.append(torch.zeros(1, device=self.device))
|
||||
parts.append(torch.zeros(self.ref_image_token_nums[index], device=self.device))
|
||||
if self.add_time_token:
|
||||
parts.append(self.diffusion_t.reshape(1).to(self.device))
|
||||
raw_t = torch.cat(parts, dim=0)
|
||||
if self.time_channel_dim == 1:
|
||||
return raw_t.unsqueeze(-1)
|
||||
return sinusoidal_embedding_1d(self.time_channel_dim, raw_t)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimplePackedData:
|
||||
"""A concatenation of independently addressable packed samples."""
|
||||
|
||||
items: list[SingleData]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.items:
|
||||
raise ValueError("SimplePackedData must contain at least one item")
|
||||
self._total_token_num_list = [item.total_token_num for item in self.items]
|
||||
self._total_token_num_sum = sum(self._total_token_num_list)
|
||||
self._total_token_num_max = max(self._total_token_num_list)
|
||||
self._total_token_cu_seqlens = _seqlens_to_cu_seqlens(self._total_token_num_list)
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return self.items[0].device
|
||||
|
||||
@property
|
||||
def default_dtype(self) -> torch.dtype:
|
||||
return self.items[0].default_dtype
|
||||
|
||||
@property
|
||||
def token_sequence(self) -> torch.Tensor:
|
||||
tensors = list(chain.from_iterable(item.feat_to_cat for item in self.items))
|
||||
return _pad_cat(tensors, self.device, self.default_dtype)
|
||||
|
||||
@property
|
||||
def modality_mapping(self) -> torch.Tensor:
|
||||
seqlens: list[int] = []
|
||||
modalities: list[int] = []
|
||||
for item in self.items:
|
||||
item_seqlens, item_modalities = item.modality_map_seqlens
|
||||
seqlens.extend(item_seqlens)
|
||||
modalities.extend(item_modalities)
|
||||
return _segment_paint(
|
||||
modalities,
|
||||
seqlens,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
output_size=self.total_token_num,
|
||||
)
|
||||
|
||||
@property
|
||||
def coords_mapping(self) -> torch.Tensor:
|
||||
coords = list(chain.from_iterable(item.coords_to_cat for item in self.items))
|
||||
return torch.cat(coords, dim=0)
|
||||
|
||||
@property
|
||||
def time_token_sequence(self) -> torch.Tensor:
|
||||
return torch.cat([item.time_token_sequence for item in self.items], dim=0)
|
||||
|
||||
@property
|
||||
def total_token_num(self) -> int:
|
||||
return self._total_token_num_sum
|
||||
|
||||
@property
|
||||
def cu_seqlen(self) -> torch.Tensor:
|
||||
return self._total_token_cu_seqlens.clone()
|
||||
|
||||
@property
|
||||
def max_seqlen(self) -> int:
|
||||
return self._total_token_num_max
|
||||
|
||||
def __getitem__(self, index: int) -> SingleData:
|
||||
return self.items[index]
|
||||
|
||||
def depack_token_sequence(self, token_sequence: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if token_sequence.shape[0] != self.total_token_num:
|
||||
raise ValueError(f"model returned {token_sequence.shape[0]} tokens, expected {self.total_token_num}")
|
||||
videos: list[torch.Tensor] = []
|
||||
audios: list[torch.Tensor] = []
|
||||
token_slices = torch.split(token_sequence, self._total_token_num_list, dim=0)
|
||||
for item, token_slice in zip(self.items, token_slices, strict=True):
|
||||
video_flat = token_slice[: item.video_token_num, : item.video_channel]
|
||||
output_channels = item.video_channel // (item.t_patch_size * item.patch_size * item.patch_size)
|
||||
video = rearrange(
|
||||
video_flat,
|
||||
"(T H W) (pT pH pW C) -> C (T pT) (H pH) (W pW)",
|
||||
T=item.t // item.t_patch_size,
|
||||
H=item.h // item.patch_size,
|
||||
W=item.w // item.patch_size,
|
||||
pT=item.t_patch_size,
|
||||
pH=item.patch_size,
|
||||
pW=item.patch_size,
|
||||
C=output_channels,
|
||||
).contiguous()
|
||||
audio = torch.zeros(
|
||||
item.origin_audio_feat_len,
|
||||
item.audio_channel,
|
||||
device=token_sequence.device,
|
||||
dtype=token_sequence.dtype,
|
||||
)
|
||||
audio[: item.audio_feat_len] = token_slice[
|
||||
item.video_token_num : item.video_token_num + item.audio_feat_len,
|
||||
: item.audio_channel,
|
||||
]
|
||||
videos.append(video)
|
||||
audios.append(audio)
|
||||
return torch.stack(videos, dim=0), torch.stack(audios, dim=0)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackedModelInput:
|
||||
"""Request-owned packed transformer arguments and output layout."""
|
||||
|
||||
token_sequence: torch.Tensor
|
||||
coords_mapping: torch.Tensor
|
||||
modality_mapping: torch.Tensor
|
||||
varlen_handler: VarlenHandler
|
||||
time_token_sequence: torch.Tensor
|
||||
output_layout: SimplePackedData
|
||||
|
||||
@property
|
||||
def model_args(self) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, VarlenHandler, torch.Tensor]:
|
||||
return (
|
||||
self.token_sequence,
|
||||
self.coords_mapping,
|
||||
self.modality_mapping,
|
||||
self.varlen_handler,
|
||||
self.time_token_sequence,
|
||||
)
|
||||
|
||||
|
||||
class Magi2DataProxy:
|
||||
"""Convert dense modality tensors to/from the transformer's packed ABI.
|
||||
|
||||
The proxy is request-stateless: all packing state needed by
|
||||
``process_output`` is returned as ``PackedModelInput.output_layout`` rather
|
||||
than stored on this shared instance, so concurrent requests cannot
|
||||
overwrite one another.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Magi2PreviewDataProxyConfig | None = None) -> None:
|
||||
self.config = config or Magi2PreviewDataProxyConfig()
|
||||
self.patch_size = self.config.patch_size
|
||||
self.t_patch_size = self.config.t_patch_size
|
||||
|
||||
def img2tokens(self, x_t: torch.Tensor) -> torch.Tensor:
|
||||
return _patchify_3d(x_t, self.t_patch_size, self.patch_size)
|
||||
|
||||
@staticmethod
|
||||
def _validate_batch(data: ModelInput) -> None:
|
||||
if data.x_t.ndim != 5:
|
||||
raise ValueError(f"x_t must be [B,C,T,H,W], got {tuple(data.x_t.shape)}")
|
||||
if data.audio_x_t.ndim != 3 or data.txt_feat.ndim != 3:
|
||||
raise ValueError("audio_x_t and txt_feat must be [B,L,C]")
|
||||
batch_size = data.x_t.shape[0]
|
||||
if data.audio_x_t.shape[0] != batch_size or data.txt_feat.shape[0] != batch_size:
|
||||
raise ValueError("video, audio, and text batch sizes must match")
|
||||
|
||||
ref_values = (
|
||||
data.ref_image_feat,
|
||||
data.ref_image_feat_len,
|
||||
data.ref_image_special_token_embedding,
|
||||
)
|
||||
if any(value is not None for value in ref_values) and not all(value is not None for value in ref_values):
|
||||
raise ValueError("reference-image features, lengths, and special tokens must be provided together")
|
||||
if data.ref_image_feat is not None:
|
||||
if data.ref_image_feat.ndim != 6:
|
||||
raise ValueError("ref_image_feat must be [B,M,C,T,H,W]")
|
||||
if data.ref_image_feat_len.ndim != 3 or data.ref_image_feat_len.shape[-1] < 2:
|
||||
raise ValueError("ref_image_feat_len must be [B,M,2]")
|
||||
if data.ref_image_special_token_embedding.ndim != 3:
|
||||
raise ValueError("ref_image_special_token_embedding must be [B,M,D]")
|
||||
expected = data.ref_image_feat.shape[:2]
|
||||
if data.ref_image_feat_len.shape[:2] != expected:
|
||||
raise ValueError("reference-image feature and length batch/image counts differ")
|
||||
if data.ref_image_special_token_embedding.shape[:2] != expected:
|
||||
raise ValueError("reference-image feature and special-token batch/image counts differ")
|
||||
|
||||
def process_input(
|
||||
self,
|
||||
data: ModelInput,
|
||||
) -> PackedModelInput:
|
||||
self._validate_batch(data)
|
||||
batch_size, _, video_t, video_h, video_w = data.x_t.shape
|
||||
video_tokens = self.img2tokens(data.x_t)
|
||||
audio_tokens = data.audio_x_t.contiguous()
|
||||
text_tokens = data.txt_feat.contiguous()
|
||||
|
||||
per_token_video_tokens = None
|
||||
per_token_audio = None
|
||||
if self.config.time_channel_dim > 0 and data.per_token_video_t is not None:
|
||||
per_token_video_tokens = self.img2tokens(data.per_token_video_t)[:, :, :1]
|
||||
per_token_audio = data.per_token_audio_t
|
||||
|
||||
has_ref_image = data.ref_image_feat is not None
|
||||
num_ref_images = data.ref_image_feat.shape[1] if has_ref_image else 0
|
||||
ref_device, ref_dtype = text_tokens.device, text_tokens.dtype
|
||||
|
||||
items: list[SingleData] = []
|
||||
for batch_index in range(batch_size):
|
||||
ref_image_feats: list[torch.Tensor] = []
|
||||
ref_image_feat_lens: list[list[int]] = []
|
||||
ref_image_special_tokens: list[torch.Tensor] = []
|
||||
for image_index in range(num_ref_images):
|
||||
feature = self.img2tokens(data.ref_image_feat[batch_index, image_index].unsqueeze(0)).squeeze(0)
|
||||
ref_image_feats.append(feature)
|
||||
ref_image_feat_lens.append(_len_to_list(data.ref_image_feat_len[batch_index, image_index]))
|
||||
ref_image_special_tokens.append(
|
||||
data.ref_image_special_token_embedding[batch_index, image_index]
|
||||
.to(device=ref_device, dtype=ref_dtype)
|
||||
.unsqueeze(0)
|
||||
)
|
||||
|
||||
if isinstance(data.t, torch.Tensor) and data.t.ndim == 0:
|
||||
diffusion_t = data.t
|
||||
else:
|
||||
diffusion_t = data.t[batch_index]
|
||||
items.append(
|
||||
SingleData(
|
||||
video_x_t=video_tokens[batch_index],
|
||||
audio_x_t=audio_tokens[batch_index],
|
||||
audio_feat_len=_length_at(data.audio_feat_len, batch_index),
|
||||
txt_feat=text_tokens[batch_index],
|
||||
txt_feat_len=_length_at(data.txt_feat_len, batch_index),
|
||||
t=video_t,
|
||||
h=video_h,
|
||||
w=video_w,
|
||||
patch_size=self.patch_size,
|
||||
t_patch_size=self.t_patch_size,
|
||||
spatial_rope_interpolation=self.config.spatial_rope_interpolation,
|
||||
diffusion_t=diffusion_t if self.config.add_time_token else None,
|
||||
per_token_video_t=(
|
||||
per_token_video_tokens[batch_index] if per_token_video_tokens is not None else None
|
||||
),
|
||||
per_token_audio_t=(per_token_audio[batch_index] if per_token_audio is not None else None),
|
||||
time_channel_dim=self.config.time_channel_dim,
|
||||
time_pos_fps=self.config.time_pos_fps,
|
||||
vae_first_latent_is_image=self.config.vae_first_latent_is_image,
|
||||
video_fps=self.config.video_fps,
|
||||
ref_image_feats=ref_image_feats,
|
||||
ref_image_feat_lens=ref_image_feat_lens,
|
||||
ref_image_special_tokens=ref_image_special_tokens,
|
||||
)
|
||||
)
|
||||
|
||||
packed = SimplePackedData(items)
|
||||
cu_seqlens = packed.cu_seqlen.to(device=data.x_t.device, dtype=torch.int32)
|
||||
varlen_handler = VarlenHandler(
|
||||
cu_seqlens_q=cu_seqlens,
|
||||
cu_seqlens_k=cu_seqlens,
|
||||
max_seqlen_q=packed.max_seqlen,
|
||||
max_seqlen_k=packed.max_seqlen,
|
||||
)
|
||||
return PackedModelInput(
|
||||
token_sequence=packed.token_sequence,
|
||||
coords_mapping=packed.coords_mapping,
|
||||
modality_mapping=packed.modality_mapping,
|
||||
varlen_handler=varlen_handler,
|
||||
time_token_sequence=packed.time_token_sequence,
|
||||
output_layout=packed,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def process_output(
|
||||
x: torch.Tensor,
|
||||
output_layout: SimplePackedData,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return output_layout.depack_token_sequence(x)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2DataProxy",
|
||||
"Magi2PreviewDataProxyConfig",
|
||||
"Modality",
|
||||
"ModelInput",
|
||||
"PackedModelInput",
|
||||
"SimplePackedData",
|
||||
"SingleData",
|
||||
"VarlenHandler",
|
||||
"get_coords",
|
||||
"sinusoidal_embedding_1d",
|
||||
]
|
||||
@@ -0,0 +1,682 @@
|
||||
# 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 Preview classifier-free-guidance sampling.
|
||||
|
||||
The CFG and denoising math is adapted from SandAI's Apache-2.0 MAGI-2
|
||||
Preview inference implementation. Model placement is deliberately not
|
||||
managed here: vLLM-Omni's loader/offloader (including DLO) owns placement,
|
||||
while this sampler only prepares tensors and invokes the already placed model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from diffusers.schedulers.scheduling_utils import SchedulerMixin
|
||||
|
||||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_world_size
|
||||
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
|
||||
|
||||
from .preview_data_proxy import Magi2DataProxy, ModelInput
|
||||
|
||||
|
||||
def _pad_or_trim(
|
||||
tensor: torch.Tensor,
|
||||
target_size: int,
|
||||
dim: int,
|
||||
pad_value: float = 0.0,
|
||||
) -> tuple[torch.Tensor, int]:
|
||||
current_size = tensor.size(dim)
|
||||
if current_size < target_size:
|
||||
padding_amount = target_size - current_size
|
||||
padding = [0] * (2 * tensor.dim())
|
||||
padding_dim_index = tensor.dim() - 1 - dim
|
||||
padding[2 * padding_dim_index + 1] = padding_amount
|
||||
return F.pad(tensor, tuple(padding), "constant", pad_value), current_size
|
||||
slices = [slice(None)] * tensor.dim()
|
||||
slices[dim] = slice(0, target_size)
|
||||
return tensor[tuple(slices)], current_size
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CFGConfig:
|
||||
"""Preview CFG controls, defaulted to the released MAGI-2 recipe."""
|
||||
|
||||
use_cfg_trick: bool = False
|
||||
cfg_trick_start_frame: int = 13
|
||||
cfg_trick_value: float = 2.0
|
||||
use_dynamic_cfg: bool = False
|
||||
dynamic_cfg_start_t: int = 500
|
||||
dynamic_cfg_cutoff_value: float = 2.0
|
||||
video_txt_guidance_scale: float = 5.0
|
||||
audio_txt_guidance_scale: float = 7.0
|
||||
# The released I2V config keeps image context in both CFG branches, so CFG
|
||||
# changes text conditioning without pushing away from the input image.
|
||||
use_ref_for_uncond: bool = True
|
||||
use_skimmed_cfg_linear: bool = False
|
||||
skimmed_cfg_scale: float = 3.0
|
||||
cfg_rescale: float = 0.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
finite_values = {
|
||||
"cfg_trick_value": self.cfg_trick_value,
|
||||
"dynamic_cfg_cutoff_value": self.dynamic_cfg_cutoff_value,
|
||||
"video_txt_guidance_scale": self.video_txt_guidance_scale,
|
||||
"audio_txt_guidance_scale": self.audio_txt_guidance_scale,
|
||||
"skimmed_cfg_scale": self.skimmed_cfg_scale,
|
||||
"cfg_rescale": self.cfg_rescale,
|
||||
}
|
||||
for name, value in finite_values.items():
|
||||
if not math.isfinite(float(value)):
|
||||
raise ValueError(f"{name} must be finite, got {value}")
|
||||
if self.cfg_trick_start_frame < 0:
|
||||
raise ValueError("cfg_trick_start_frame must be non-negative")
|
||||
if self.skimmed_cfg_scale < 0 or self.cfg_rescale < 0:
|
||||
raise ValueError("skimmed_cfg_scale and cfg_rescale must be non-negative")
|
||||
|
||||
|
||||
@dataclass
|
||||
class SamplerInput:
|
||||
video_t_list: Sequence[torch.Tensor]
|
||||
audio_t_list: Sequence[torch.Tensor]
|
||||
latent: torch.Tensor # [B,C,T,H,W]
|
||||
audio_latent: torch.Tensor # [B,L,C]
|
||||
txt_feat: torch.Tensor # [B,L,D]
|
||||
null_txt_feat: torch.Tensor # [B,L,D]
|
||||
ref_audio_feat: torch.Tensor | None
|
||||
ref_video_feat: torch.Tensor | None
|
||||
video_scheduler: SchedulerMixin
|
||||
audio_scheduler: SchedulerMixin
|
||||
cfg_config: CFGConfig
|
||||
ref_image_feat: torch.Tensor | None = None # [B,M,C,1,H,W]
|
||||
ref_image_feat_len: torch.Tensor | None = None # [B,M,2]
|
||||
ref_image_special_token_embedding: torch.Tensor | None = None # [B,M,D]
|
||||
|
||||
|
||||
def build_magi2_preview_schedulers(
|
||||
num_inference_steps: int,
|
||||
*,
|
||||
device: torch.device | str,
|
||||
shift: float = 7.0,
|
||||
) -> tuple[FlowUniPCMultistepScheduler, FlowUniPCMultistepScheduler]:
|
||||
"""Create independent, identically configured video/audio schedulers."""
|
||||
|
||||
if num_inference_steps <= 0:
|
||||
raise ValueError("num_inference_steps must be positive")
|
||||
if not math.isfinite(shift) or shift <= 0:
|
||||
raise ValueError(f"shift must be positive and finite, got {shift}")
|
||||
video_scheduler = FlowUniPCMultistepScheduler()
|
||||
audio_scheduler = FlowUniPCMultistepScheduler()
|
||||
video_scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
|
||||
audio_scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
|
||||
return video_scheduler, audio_scheduler
|
||||
|
||||
|
||||
class Magi2PreviewSampler(CFGParallelMixin):
|
||||
"""Run MAGI-2 Preview's joint video/audio denoising loop."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: torch.nn.Module,
|
||||
data_proxy: Magi2DataProxy | None = None,
|
||||
device: torch.device | str | None = None,
|
||||
dtype: torch.dtype | None = None,
|
||||
) -> None:
|
||||
self.model = model
|
||||
self.data_proxy = data_proxy or Magi2DataProxy()
|
||||
self.device = torch.device(device) if device is not None else None
|
||||
# Retained for the reference-compatible constructor. Model placement
|
||||
# and parameter dtype are owned by the generic loader/offloader.
|
||||
self.dtype = dtype
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, model_input: ModelInput) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
packed_input = self.data_proxy.process_input(model_input)
|
||||
model_output = self.model(*packed_input.model_args)
|
||||
if not isinstance(model_output, torch.Tensor):
|
||||
raise TypeError(f"MAGI-2 preview transformer must return a tensor, got {type(model_output)!r}")
|
||||
return self.data_proxy.process_output(model_output, packed_input.output_layout)
|
||||
|
||||
def predict_noise(self, model_input: ModelInput) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Run one conditional or unconditional branch for shared CFG dispatch."""
|
||||
|
||||
return self.forward(model_input)
|
||||
|
||||
@torch.inference_mode()
|
||||
def sample(self, sampler_input: SamplerInput) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
video_timesteps = list(sampler_input.video_t_list)
|
||||
audio_timesteps = list(sampler_input.audio_t_list)
|
||||
if not video_timesteps:
|
||||
raise ValueError("MAGI-2 sampling needs at least one timestep")
|
||||
if len(video_timesteps) != len(audio_timesteps):
|
||||
raise ValueError("video and audio timestep schedules must have equal lengths")
|
||||
|
||||
video_cfgs, audio_cfgs = self.precalculate_cfg(
|
||||
video_timesteps,
|
||||
sampler_input.latent.shape[2],
|
||||
sampler_input.cfg_config,
|
||||
device=sampler_input.latent.device,
|
||||
)
|
||||
latent = sampler_input.latent.clone()
|
||||
audio_latent = sampler_input.audio_latent.clone()
|
||||
|
||||
for timestep, video_cfg, audio_cfg in zip(
|
||||
video_timesteps,
|
||||
video_cfgs,
|
||||
audio_cfgs,
|
||||
strict=True,
|
||||
):
|
||||
model_input = self.prepare_model_input(
|
||||
latent=latent,
|
||||
audio_latent=audio_latent,
|
||||
txt_feat=sampler_input.txt_feat,
|
||||
null_txt_feat=sampler_input.null_txt_feat,
|
||||
ref_audio_feat=sampler_input.ref_audio_feat,
|
||||
ref_video_feat=sampler_input.ref_video_feat,
|
||||
ref_image_feat=sampler_input.ref_image_feat,
|
||||
ref_image_feat_len=sampler_input.ref_image_feat_len,
|
||||
ref_image_special_token_embedding=(sampler_input.ref_image_special_token_embedding),
|
||||
t=timestep,
|
||||
cfg_config=sampler_input.cfg_config,
|
||||
)
|
||||
if get_classifier_free_guidance_world_size() > 1:
|
||||
positive_input, negative_input = self._split_cfg_model_input(model_input)
|
||||
guided = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=True,
|
||||
true_cfg_scale=1.0,
|
||||
positive_kwargs={"model_input": positive_input},
|
||||
negative_kwargs={"model_input": negative_input},
|
||||
cfg_normalize=False,
|
||||
kwargs={
|
||||
"video_txt_guidance_scale": video_cfg,
|
||||
"audio_txt_guidance_scale": audio_cfg,
|
||||
"cfg_config": sampler_input.cfg_config,
|
||||
"latent": latent,
|
||||
"audio_latent": audio_latent,
|
||||
},
|
||||
)
|
||||
if not isinstance(guided, tuple) or len(guided) != 2:
|
||||
raise RuntimeError("MAGI-2 CFG parallel combine must return video and audio predictions")
|
||||
latent, audio_latent = self._step_guided(
|
||||
guided,
|
||||
latent,
|
||||
audio_latent,
|
||||
sampler_input.video_scheduler,
|
||||
sampler_input.audio_scheduler,
|
||||
timestep,
|
||||
)
|
||||
else:
|
||||
model_pred = self.forward(model_input)
|
||||
latent, audio_latent, _, _ = self.step(
|
||||
model_pred,
|
||||
latent,
|
||||
audio_latent,
|
||||
video_cfg,
|
||||
audio_cfg,
|
||||
sampler_input.video_scheduler,
|
||||
sampler_input.audio_scheduler,
|
||||
timestep,
|
||||
cfg_config=sampler_input.cfg_config,
|
||||
)
|
||||
|
||||
return latent, audio_latent
|
||||
|
||||
@staticmethod
|
||||
def _validate_prepare_inputs(
|
||||
latent: torch.Tensor,
|
||||
audio_latent: torch.Tensor,
|
||||
txt_feat: torch.Tensor,
|
||||
null_txt_feat: torch.Tensor,
|
||||
) -> None:
|
||||
if latent.ndim != 5:
|
||||
raise ValueError("latent must be [B,C,T,H,W]")
|
||||
if audio_latent.ndim != 3 or txt_feat.ndim != 3 or null_txt_feat.ndim != 3:
|
||||
raise ValueError("audio and text features must be [B,L,C]")
|
||||
batch_size = latent.shape[0]
|
||||
if not (audio_latent.shape[0] == txt_feat.shape[0] == null_txt_feat.shape[0] == batch_size):
|
||||
raise ValueError("video, audio, positive-text, and negative-text batch sizes must match")
|
||||
|
||||
def prepare_model_input(
|
||||
self,
|
||||
latent: torch.Tensor,
|
||||
audio_latent: torch.Tensor,
|
||||
txt_feat: torch.Tensor,
|
||||
null_txt_feat: torch.Tensor,
|
||||
ref_audio_feat: torch.Tensor | None = None,
|
||||
ref_video_feat: torch.Tensor | None = None,
|
||||
ref_image_feat: torch.Tensor | None = None,
|
||||
ref_image_feat_len: torch.Tensor | None = None,
|
||||
ref_image_special_token_embedding: torch.Tensor | None = None,
|
||||
t: torch.Tensor | float | None = None,
|
||||
cfg_config: CFGConfig | None = None,
|
||||
) -> ModelInput:
|
||||
self._validate_prepare_inputs(latent, audio_latent, txt_feat, null_txt_feat)
|
||||
batch_size = latent.shape[0]
|
||||
audio_latent_len = audio_latent.shape[1]
|
||||
txt_feat_len = txt_feat.shape[1]
|
||||
null_txt_feat_len = null_txt_feat.shape[1]
|
||||
target_length = max(txt_feat_len, null_txt_feat_len)
|
||||
txt_feat, _ = _pad_or_trim(txt_feat, target_length, 1)
|
||||
null_txt_feat, _ = _pad_or_trim(null_txt_feat, target_length, 1)
|
||||
|
||||
if ref_audio_feat is None:
|
||||
ref_audio_feat = audio_latent.new_empty(batch_size, 0, audio_latent.shape[-1])
|
||||
if ref_audio_feat.ndim != 3 or ref_audio_feat.shape[0] != batch_size:
|
||||
raise ValueError("ref_audio_feat must be [B,L,C] with the same batch size")
|
||||
ref_audio_feat_len = ref_audio_feat.shape[1]
|
||||
|
||||
if ref_video_feat is None:
|
||||
# Preserve the 5-D ABI without allocating or reading a full-size
|
||||
# placeholder that the zero reference length excludes downstream.
|
||||
ref_video_feat = latent[:, :, :0]
|
||||
ref_video_feat_len = 0
|
||||
else:
|
||||
if ref_video_feat.ndim != 5 or ref_video_feat.shape[0] != batch_size:
|
||||
raise ValueError("ref_video_feat must be [B,C,T,H,W] with the same batch size")
|
||||
ref_video_feat_len = (ref_video_feat.shape[3] * ref_video_feat.shape[4]) // 4
|
||||
|
||||
ref_image_feat_cfg, ref_image_feat_len_cfg, ref_image_special_tokens_cfg = self._prepare_ref_image_cfg(
|
||||
ref_image_feat,
|
||||
ref_image_feat_len,
|
||||
ref_image_special_token_embedding,
|
||||
cfg_config,
|
||||
)
|
||||
|
||||
if t is None:
|
||||
t_value = torch.tensor(0.0, device=latent.device, dtype=latent.dtype)
|
||||
elif isinstance(t, torch.Tensor):
|
||||
t_value = t.reshape(-1)[0].to(device=latent.device, dtype=latent.dtype)
|
||||
else:
|
||||
t_value = torch.tensor(float(t), device=latent.device, dtype=latent.dtype)
|
||||
t_normalized = t_value / 1000.0
|
||||
cfg_batch = batch_size * 2
|
||||
t_batch = t_normalized.expand(cfg_batch)
|
||||
|
||||
_, _, video_t, video_h, video_w = latent.shape
|
||||
audio_t = audio_latent.shape[1]
|
||||
# Keep zero-stride views here. Patchification/materialization happens
|
||||
# once in the data proxy, so cloning the full token grids every denoise
|
||||
# step only increases peak memory and allocator pressure.
|
||||
per_token_video_t = t_batch.view(-1, 1, 1, 1, 1).expand(cfg_batch, 1, video_t, video_h, video_w)
|
||||
per_token_audio_t = t_batch.view(-1, 1, 1).expand(cfg_batch, audio_t, 1)
|
||||
|
||||
audio_lengths = torch.full(
|
||||
(cfg_batch,),
|
||||
audio_latent_len,
|
||||
device=latent.device,
|
||||
dtype=torch.long,
|
||||
)
|
||||
text_lengths = torch.tensor(
|
||||
[txt_feat_len] * batch_size + [null_txt_feat_len] * batch_size,
|
||||
device=latent.device,
|
||||
dtype=torch.long,
|
||||
)
|
||||
ref_audio_lengths = torch.full(
|
||||
(cfg_batch,),
|
||||
ref_audio_feat_len,
|
||||
device=latent.device,
|
||||
dtype=torch.long,
|
||||
)
|
||||
ref_video_lengths = torch.full(
|
||||
(cfg_batch,),
|
||||
ref_video_feat_len,
|
||||
device=latent.device,
|
||||
dtype=torch.long,
|
||||
)
|
||||
|
||||
return ModelInput(
|
||||
x_t=torch.cat((latent, latent), dim=0),
|
||||
audio_x_t=torch.cat((audio_latent, audio_latent), dim=0),
|
||||
audio_feat_len=audio_lengths,
|
||||
txt_feat=torch.cat((txt_feat, null_txt_feat), dim=0),
|
||||
txt_feat_len=text_lengths,
|
||||
t=t_batch,
|
||||
per_token_video_t=per_token_video_t,
|
||||
per_token_audio_t=per_token_audio_t,
|
||||
ref_audio_feat=torch.cat((ref_audio_feat, torch.zeros_like(ref_audio_feat)), dim=0),
|
||||
ref_audio_feat_len=ref_audio_lengths,
|
||||
ref_video_feat=torch.cat((ref_video_feat, torch.zeros_like(ref_video_feat)), dim=0),
|
||||
ref_video_feat_len=ref_video_lengths,
|
||||
ref_image_feat=ref_image_feat_cfg,
|
||||
ref_image_feat_len=ref_image_feat_len_cfg,
|
||||
ref_image_special_token_embedding=ref_image_special_tokens_cfg,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _split_cfg_model_input(model_input: ModelInput) -> tuple[ModelInput, ModelInput]:
|
||||
"""Split the packed positive/negative batch for CFG rank dispatch."""
|
||||
|
||||
batch = model_input.x_t.shape[0]
|
||||
if batch % 2:
|
||||
raise ValueError("MAGI-2 CFG model input must contain equal positive and negative halves")
|
||||
half = batch // 2
|
||||
|
||||
def split(value: torch.Tensor | Sequence[int] | None) -> tuple[torch.Tensor | Sequence[int] | None, ...]:
|
||||
if value is None:
|
||||
return None, None
|
||||
if isinstance(value, torch.Tensor):
|
||||
if value.ndim == 0 or value.shape[0] != batch:
|
||||
raise ValueError("MAGI-2 CFG tensor fields must use the packed CFG batch dimension")
|
||||
return value[:half], value[half:]
|
||||
if len(value) != batch:
|
||||
raise ValueError("MAGI-2 CFG sequence fields must use the packed CFG batch dimension")
|
||||
return value[:half], value[half:]
|
||||
|
||||
field_pairs = {
|
||||
name: split(getattr(model_input, name))
|
||||
for name in (
|
||||
"x_t",
|
||||
"audio_x_t",
|
||||
"audio_feat_len",
|
||||
"txt_feat",
|
||||
"txt_feat_len",
|
||||
"t",
|
||||
"ref_audio_feat",
|
||||
"ref_audio_feat_len",
|
||||
"ref_video_feat",
|
||||
"ref_video_feat_len",
|
||||
"per_token_video_t",
|
||||
"per_token_audio_t",
|
||||
"ref_image_feat",
|
||||
"ref_image_feat_len",
|
||||
"ref_image_special_token_embedding",
|
||||
)
|
||||
}
|
||||
branches = []
|
||||
for branch in range(2):
|
||||
branches.append(ModelInput(**{name: pair[branch] for name, pair in field_pairs.items()}))
|
||||
return branches[0], branches[1]
|
||||
|
||||
@staticmethod
|
||||
def _prepare_ref_image_cfg(
|
||||
ref_image_feat: torch.Tensor | None,
|
||||
ref_image_feat_len: torch.Tensor | None,
|
||||
ref_image_special_token_embedding: torch.Tensor | None,
|
||||
cfg_config: CFGConfig | None,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
|
||||
if ref_image_feat is None:
|
||||
if ref_image_feat_len is not None or ref_image_special_token_embedding is not None:
|
||||
raise ValueError("reference-image features, lengths, and special tokens must be provided together")
|
||||
return None, None, None
|
||||
if ref_image_feat_len is None or ref_image_special_token_embedding is None:
|
||||
raise ValueError("reference-image features, lengths, and special tokens must be provided together")
|
||||
|
||||
keep_ref = bool(cfg_config is not None and cfg_config.use_ref_for_uncond)
|
||||
uncond_feature = ref_image_feat if keep_ref else torch.zeros_like(ref_image_feat)
|
||||
return (
|
||||
torch.cat((ref_image_feat, uncond_feature), dim=0),
|
||||
torch.cat((ref_image_feat_len, ref_image_feat_len), dim=0),
|
||||
torch.cat(
|
||||
(ref_image_special_token_embedding, ref_image_special_token_embedding),
|
||||
dim=0,
|
||||
),
|
||||
)
|
||||
|
||||
def precalculate_cfg(
|
||||
self,
|
||||
t_list: Sequence[torch.Tensor],
|
||||
latent_length: int,
|
||||
cfg_config: CFGConfig,
|
||||
*,
|
||||
device: torch.device | str | None = None,
|
||||
) -> tuple[list[torch.Tensor], list[float]]:
|
||||
if latent_length < 0:
|
||||
raise ValueError("latent_length must be non-negative")
|
||||
if device is None:
|
||||
if self.device is not None:
|
||||
device = self.device
|
||||
elif t_list:
|
||||
device = t_list[0].device
|
||||
else:
|
||||
device = torch.device("cpu")
|
||||
|
||||
if cfg_config.use_cfg_trick:
|
||||
video_cfg_base = (
|
||||
torch.tensor(cfg_config.video_txt_guidance_scale, device=device)
|
||||
.expand(1, 1, latent_length, 1, 1)
|
||||
.clone()
|
||||
)
|
||||
video_cfg_base[:, :, : cfg_config.cfg_trick_start_frame] = min(
|
||||
cfg_config.cfg_trick_value,
|
||||
cfg_config.video_txt_guidance_scale,
|
||||
)
|
||||
else:
|
||||
video_cfg_base = torch.tensor([cfg_config.video_txt_guidance_scale], device=device)
|
||||
|
||||
all_video_cfgs: list[torch.Tensor] = []
|
||||
all_audio_cfgs: list[float] = []
|
||||
for timestep in t_list:
|
||||
video_cfg = video_cfg_base.clone()
|
||||
if cfg_config.use_dynamic_cfg:
|
||||
timestep_value = float(timestep.detach().reshape(-1)[0].item())
|
||||
if timestep_value < cfg_config.dynamic_cfg_start_t:
|
||||
video_cfg[video_cfg > cfg_config.dynamic_cfg_cutoff_value] = cfg_config.dynamic_cfg_cutoff_value
|
||||
all_video_cfgs.append(video_cfg)
|
||||
all_audio_cfgs.append(cfg_config.audio_txt_guidance_scale)
|
||||
return all_video_cfgs, all_audio_cfgs
|
||||
|
||||
@staticmethod
|
||||
def _cfg_scale_tensor(
|
||||
guidance_scale: torch.Tensor | float,
|
||||
like: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if isinstance(guidance_scale, torch.Tensor):
|
||||
return guidance_scale.to(device=like.device, dtype=like.dtype)
|
||||
return torch.tensor(float(guidance_scale), device=like.device, dtype=like.dtype)
|
||||
|
||||
@classmethod
|
||||
def _get_skimming_mask(
|
||||
cls,
|
||||
x_orig: torch.Tensor,
|
||||
cond: torch.Tensor,
|
||||
uncond: torch.Tensor,
|
||||
guidance_scale: torch.Tensor | float,
|
||||
) -> torch.Tensor:
|
||||
guidance_scale = cls._cfg_scale_tensor(guidance_scale, cond)
|
||||
x_orig = x_orig.to(device=cond.device, dtype=cond.dtype)
|
||||
denoised = x_orig - ((x_orig - uncond) + guidance_scale * ((x_orig - cond) - (x_orig - uncond)))
|
||||
matching_pred_signs = (cond - uncond).sign() == cond.sign()
|
||||
matching_diff_after = cond.sign() == (cond * guidance_scale - uncond * (guidance_scale - 1)).sign()
|
||||
deviation_influence = denoised.sign() == (denoised - x_orig).sign()
|
||||
return matching_pred_signs & matching_diff_after & deviation_influence
|
||||
|
||||
@classmethod
|
||||
def _apply_skimmed_cfg_linear(
|
||||
cls,
|
||||
x_orig: torch.Tensor | None,
|
||||
cond: torch.Tensor,
|
||||
uncond: torch.Tensor,
|
||||
guidance_scale: torch.Tensor | float,
|
||||
cfg_config: CFGConfig | None,
|
||||
) -> torch.Tensor:
|
||||
if (
|
||||
cfg_config is None
|
||||
or not cfg_config.use_skimmed_cfg_linear
|
||||
or x_orig is None
|
||||
or not torch.any(uncond).item()
|
||||
):
|
||||
return uncond
|
||||
|
||||
guidance_scale = cls._cfg_scale_tensor(guidance_scale, cond)
|
||||
valid_scale = guidance_scale > 1
|
||||
if not torch.any(valid_scale).item():
|
||||
return uncond
|
||||
scale_delta = torch.where(
|
||||
valid_scale,
|
||||
guidance_scale - 1,
|
||||
torch.ones_like(guidance_scale),
|
||||
)
|
||||
fallback_weight = (cfg_config.skimmed_cfg_scale - 1) / scale_delta
|
||||
target_uncond = cond * (1 - fallback_weight) + uncond * fallback_weight
|
||||
skim_mask = cls._get_skimming_mask(x_orig, cond, uncond, guidance_scale)
|
||||
uncond = torch.where(skim_mask & valid_scale, target_uncond, uncond)
|
||||
|
||||
target_uncond = cond * (1 - fallback_weight) + uncond * fallback_weight
|
||||
skim_mask = cls._get_skimming_mask(x_orig, uncond, cond, guidance_scale)
|
||||
return torch.where(skim_mask & valid_scale, target_uncond, uncond)
|
||||
|
||||
@staticmethod
|
||||
def _cfg_rescale_dims(tensor: torch.Tensor) -> tuple[int, ...]:
|
||||
if tensor.ndim <= 1:
|
||||
return ()
|
||||
if tensor.ndim in (2, 3):
|
||||
return (1,)
|
||||
return tuple(range(2, tensor.ndim))
|
||||
|
||||
@classmethod
|
||||
def _apply_cfg_rescale(
|
||||
cls,
|
||||
pos: torch.Tensor,
|
||||
cfg: torch.Tensor,
|
||||
cfg_config: CFGConfig | None,
|
||||
) -> torch.Tensor:
|
||||
if cfg_config is None or cfg_config.cfg_rescale <= 0 or cfg.numel() == 0:
|
||||
return cfg
|
||||
spatial_dims = cls._cfg_rescale_dims(cfg)
|
||||
if not spatial_dims:
|
||||
return cfg
|
||||
pos_std = pos.float().std(dim=spatial_dims, keepdim=True, unbiased=False)
|
||||
cfg_std = cfg.float().std(dim=spatial_dims, keepdim=True, unbiased=False).clamp_min(1e-6)
|
||||
factor = cfg_config.cfg_rescale * (pos_std / cfg_std) + (1 - cfg_config.cfg_rescale)
|
||||
return cfg * factor.to(device=cfg.device, dtype=cfg.dtype)
|
||||
|
||||
def cfg_velocity(
|
||||
self,
|
||||
model_output: tuple[torch.Tensor, torch.Tensor],
|
||||
video_txt_guidance_scale: torch.Tensor | float,
|
||||
audio_txt_guidance_scale: torch.Tensor | float,
|
||||
cfg_config: CFGConfig | None = None,
|
||||
latent: torch.Tensor | None = None,
|
||||
audio_latent: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
||||
cond_uncond_video, cond_uncond_audio = model_output
|
||||
if cond_uncond_video.shape[0] % 2 or cond_uncond_audio.shape[0] % 2:
|
||||
raise ValueError("MAGI-2 CFG outputs must contain equal conditional/unconditional halves")
|
||||
video_batch = cond_uncond_video.shape[0] // 2
|
||||
audio_batch = cond_uncond_audio.shape[0] // 2
|
||||
if video_batch != audio_batch:
|
||||
raise ValueError("video and audio CFG output batch sizes differ")
|
||||
|
||||
if cond_uncond_video.numel() > 0:
|
||||
cond_video = cond_uncond_video[:video_batch]
|
||||
uncond_video = cond_uncond_video[video_batch:]
|
||||
uncond_video = self._apply_skimmed_cfg_linear(
|
||||
latent,
|
||||
cond_video,
|
||||
uncond_video,
|
||||
video_txt_guidance_scale,
|
||||
cfg_config,
|
||||
)
|
||||
cfg_video = uncond_video + video_txt_guidance_scale * (cond_video - uncond_video)
|
||||
cfg_video = self._apply_cfg_rescale(cond_video, cfg_video, cfg_config)
|
||||
else:
|
||||
cfg_video = None
|
||||
|
||||
if cond_uncond_audio.numel() > 0:
|
||||
cond_audio = cond_uncond_audio[:audio_batch]
|
||||
uncond_audio = cond_uncond_audio[audio_batch:]
|
||||
uncond_audio = self._apply_skimmed_cfg_linear(
|
||||
audio_latent,
|
||||
cond_audio,
|
||||
uncond_audio,
|
||||
audio_txt_guidance_scale,
|
||||
cfg_config,
|
||||
)
|
||||
cfg_audio = uncond_audio + audio_txt_guidance_scale * (cond_audio - uncond_audio)
|
||||
cfg_audio = self._apply_cfg_rescale(cond_audio, cfg_audio, cfg_config)
|
||||
else:
|
||||
cfg_audio = None
|
||||
return cfg_video, cfg_audio
|
||||
|
||||
def combine_cfg_noise(
|
||||
self,
|
||||
positive_noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
negative_noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
true_cfg_scale: float,
|
||||
cfg_normalize: bool = False,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Preserve MAGI-2's dual-modality guidance under shared CFG dispatch."""
|
||||
|
||||
del true_cfg_scale, cfg_normalize
|
||||
if not isinstance(positive_noise_pred, tuple) or not isinstance(negative_noise_pred, tuple):
|
||||
raise TypeError("MAGI-2 CFG predictions must contain video and audio tensors")
|
||||
if len(positive_noise_pred) != 2 or len(negative_noise_pred) != 2:
|
||||
raise ValueError("MAGI-2 CFG predictions must contain exactly two tensors")
|
||||
if kwargs is None:
|
||||
raise ValueError("MAGI-2 CFG combine requires guidance and latent context")
|
||||
cfg_video, cfg_audio = self.cfg_velocity(
|
||||
(
|
||||
torch.cat((positive_noise_pred[0], negative_noise_pred[0]), dim=0),
|
||||
torch.cat((positive_noise_pred[1], negative_noise_pred[1]), dim=0),
|
||||
),
|
||||
kwargs["video_txt_guidance_scale"],
|
||||
kwargs["audio_txt_guidance_scale"],
|
||||
kwargs.get("cfg_config"),
|
||||
kwargs.get("latent"),
|
||||
kwargs.get("audio_latent"),
|
||||
)
|
||||
if cfg_video is None or cfg_audio is None:
|
||||
raise RuntimeError("MAGI-2 Preview must produce both video and audio CFG predictions")
|
||||
return cfg_video, cfg_audio
|
||||
|
||||
@staticmethod
|
||||
def _step_guided(
|
||||
guided_output: tuple[torch.Tensor, torch.Tensor],
|
||||
latent: torch.Tensor,
|
||||
audio_latent: torch.Tensor,
|
||||
video_scheduler: SchedulerMixin,
|
||||
audio_scheduler: SchedulerMixin,
|
||||
t: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
cfg_video, cfg_audio = guided_output
|
||||
latent = video_scheduler.step(cfg_video, t, latent, return_dict=False)[0]
|
||||
audio_latent = audio_scheduler.step(cfg_audio, t, audio_latent, return_dict=False)[0]
|
||||
return latent, audio_latent
|
||||
|
||||
def step(
|
||||
self,
|
||||
model_output: tuple[torch.Tensor, torch.Tensor],
|
||||
latent: torch.Tensor,
|
||||
audio_latent: torch.Tensor,
|
||||
video_txt_guidance_scale: torch.Tensor | float,
|
||||
audio_txt_guidance_scale: torch.Tensor | float,
|
||||
video_scheduler: SchedulerMixin,
|
||||
audio_scheduler: SchedulerMixin,
|
||||
t: torch.Tensor,
|
||||
cfg_config: CFGConfig | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
||||
cfg_video, cfg_audio = self.cfg_velocity(
|
||||
model_output,
|
||||
video_txt_guidance_scale,
|
||||
audio_txt_guidance_scale,
|
||||
cfg_config,
|
||||
latent,
|
||||
audio_latent,
|
||||
)
|
||||
if cfg_video is not None:
|
||||
latent = video_scheduler.step(cfg_video, t, latent, return_dict=False)[0]
|
||||
if cfg_audio is not None:
|
||||
audio_latent = audio_scheduler.step(
|
||||
cfg_audio,
|
||||
t,
|
||||
audio_latent,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
return latent, audio_latent, cfg_video, cfg_audio
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CFGConfig",
|
||||
"Magi2PreviewSampler",
|
||||
"SamplerInput",
|
||||
"build_magi2_preview_schedulers",
|
||||
]
|
||||
@@ -0,0 +1,582 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) 2025-2026 SandAI. All Rights Reserved.
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Native decoder for the TurboVAED checkpoint shipped with MAGI-2.
|
||||
|
||||
Adapted from SandAI's MAGI-2 Preview inference implementation. Training-only
|
||||
modules and MagiCompiler integration are intentionally omitted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
from einops import rearrange
|
||||
|
||||
from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
|
||||
DistributedOperator,
|
||||
DistributedVaeMixin,
|
||||
GridSpec,
|
||||
TileTask,
|
||||
)
|
||||
|
||||
_WAN22_LATENT_MEAN = (
|
||||
-0.2289,
|
||||
-0.0052,
|
||||
-0.1323,
|
||||
-0.2339,
|
||||
-0.2799,
|
||||
0.0174,
|
||||
0.1838,
|
||||
0.1557,
|
||||
-0.1382,
|
||||
0.0542,
|
||||
0.2813,
|
||||
0.0891,
|
||||
0.1570,
|
||||
-0.0098,
|
||||
0.0375,
|
||||
-0.1825,
|
||||
-0.2246,
|
||||
-0.1207,
|
||||
-0.0698,
|
||||
0.5109,
|
||||
0.2665,
|
||||
-0.2108,
|
||||
-0.2158,
|
||||
0.2502,
|
||||
-0.2055,
|
||||
-0.0322,
|
||||
0.1109,
|
||||
0.1567,
|
||||
-0.0729,
|
||||
0.0899,
|
||||
-0.2799,
|
||||
-0.1230,
|
||||
-0.0313,
|
||||
-0.1649,
|
||||
0.0117,
|
||||
0.0723,
|
||||
-0.2839,
|
||||
-0.2083,
|
||||
-0.0520,
|
||||
0.3748,
|
||||
0.0152,
|
||||
0.1957,
|
||||
0.1433,
|
||||
-0.2944,
|
||||
0.3573,
|
||||
-0.0548,
|
||||
-0.1681,
|
||||
-0.0667,
|
||||
)
|
||||
_WAN22_LATENT_STD = (
|
||||
0.4765,
|
||||
1.0364,
|
||||
0.4514,
|
||||
1.1677,
|
||||
0.5313,
|
||||
0.4990,
|
||||
0.4818,
|
||||
0.5013,
|
||||
0.8158,
|
||||
1.0344,
|
||||
0.5894,
|
||||
1.0901,
|
||||
0.6885,
|
||||
0.6165,
|
||||
0.8454,
|
||||
0.4978,
|
||||
0.5759,
|
||||
0.3523,
|
||||
0.7135,
|
||||
0.6804,
|
||||
0.5833,
|
||||
1.4146,
|
||||
0.8986,
|
||||
0.5659,
|
||||
0.7069,
|
||||
0.5338,
|
||||
0.4889,
|
||||
0.4917,
|
||||
0.4069,
|
||||
0.4999,
|
||||
0.6866,
|
||||
0.4093,
|
||||
0.5709,
|
||||
0.6065,
|
||||
0.6415,
|
||||
0.4944,
|
||||
0.5726,
|
||||
1.2042,
|
||||
0.5458,
|
||||
1.6887,
|
||||
0.3971,
|
||||
1.0600,
|
||||
0.3943,
|
||||
0.5537,
|
||||
0.5444,
|
||||
0.4089,
|
||||
0.7468,
|
||||
0.7744,
|
||||
)
|
||||
|
||||
|
||||
def turbo_unpatchify(x: torch.Tensor, patch_size: int) -> torch.Tensor:
|
||||
if patch_size == 1:
|
||||
return x
|
||||
if x.ndim != 5:
|
||||
raise ValueError(f"Expected a 5-D TurboVAED output, got {tuple(x.shape)}")
|
||||
batch, patched_channels, frames, height, width = x.shape
|
||||
patch_area = patch_size * patch_size
|
||||
if patched_channels % patch_area:
|
||||
raise ValueError("TurboVAED output channels must be divisible by patch_size squared")
|
||||
channels = patched_channels // patch_area
|
||||
x = x.view(batch, channels, patch_size, patch_size, frames, height, width)
|
||||
x = x.permute(0, 1, 4, 5, 3, 6, 2).contiguous()
|
||||
return x.view(batch, channels, frames, height * patch_size, width * patch_size)
|
||||
|
||||
|
||||
class _RMSNorm(nn.Module):
|
||||
def __init__(self, eps: float = 1e-8) -> None:
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
dtype = x.dtype
|
||||
variance = x.float().pow(2).mean(1, keepdim=True)
|
||||
return (x * torch.rsqrt(variance + self.eps)).to(dtype)
|
||||
|
||||
|
||||
class _CausalConv3d(nn.Module):
|
||||
"""Checkpoint-compatible non-causal 3-D convolution with edge time padding."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: int,
|
||||
kernel_size: int | tuple[int, int, int] = 3,
|
||||
stride: int | tuple[int, int, int] = 1,
|
||||
*,
|
||||
is_causal: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if is_causal:
|
||||
raise ValueError("The released MAGI-2 TurboVAED checkpoint is non-causal")
|
||||
self.kernel_size = kernel_size if isinstance(kernel_size, tuple) else (kernel_size,) * 3
|
||||
stride = stride if isinstance(stride, tuple) else (stride,) * 3
|
||||
self.conv = nn.Conv3d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
self.kernel_size,
|
||||
stride=stride,
|
||||
padding=(0, self.kernel_size[1] // 2, self.kernel_size[2] // 2),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
temporal_pad = (self.kernel_size[0] - 1) // 2
|
||||
if temporal_pad:
|
||||
x = torch.cat(
|
||||
[
|
||||
x[:, :, :1].repeat(1, 1, temporal_pad, 1, 1),
|
||||
x,
|
||||
x[:, :, -1:].repeat(1, 1, temporal_pad, 1, 1),
|
||||
],
|
||||
dim=2,
|
||||
)
|
||||
return self.conv(x)
|
||||
|
||||
|
||||
class _ResNetBlock3d(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: int | None = None,
|
||||
*,
|
||||
is_upsampler_modified: bool = False,
|
||||
is_causal: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
out_channels = out_channels or in_channels
|
||||
self.nonlinearity = nn.SiLU()
|
||||
self.replace_nonlinearity = nn.ReLU()
|
||||
self.is_upsampler_modified = is_upsampler_modified
|
||||
self.norm1 = _RMSNorm()
|
||||
self.conv1 = _CausalConv3d(in_channels, out_channels, is_causal=is_causal)
|
||||
self.norm2 = _RMSNorm()
|
||||
self.conv2 = _CausalConv3d(out_channels, out_channels, is_causal=is_causal)
|
||||
self.norm3 = _RMSNorm(eps=1e-6) if in_channels != out_channels else None
|
||||
self.conv_shortcut = (
|
||||
_CausalConv3d(in_channels, out_channels, kernel_size=1, is_causal=is_causal)
|
||||
if in_channels != out_channels
|
||||
else None
|
||||
)
|
||||
|
||||
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
|
||||
x = self.norm1(inputs)
|
||||
x = self.replace_nonlinearity(x) if self.is_upsampler_modified else self.nonlinearity(x)
|
||||
x = self.conv1(x)
|
||||
x = self.conv2(self.nonlinearity(self.norm2(x)))
|
||||
residual = self.norm3(inputs) if self.norm3 is not None else inputs
|
||||
if self.conv_shortcut is not None:
|
||||
residual = self.conv_shortcut(residual)
|
||||
return x + residual
|
||||
|
||||
|
||||
class _WanUpsample(nn.Upsample):
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return super().forward(x.float()).type_as(x)
|
||||
|
||||
|
||||
class _WanResample(nn.Module):
|
||||
def __init__(self, channels: int, mode: str) -> None:
|
||||
super().__init__()
|
||||
self.mode = mode
|
||||
if mode in ("upsample2d", "upsample3d"):
|
||||
self.resample = nn.Sequential(
|
||||
_WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
|
||||
nn.Conv2d(channels, channels, 3, padding=1),
|
||||
)
|
||||
else:
|
||||
self.resample = nn.Identity()
|
||||
if mode == "upsample3d":
|
||||
self.time_conv = _CausalConv3d(channels, channels * 2, (3, 1, 1))
|
||||
|
||||
def forward(self, x: torch.Tensor, *, is_first_chunk: bool) -> torch.Tensor:
|
||||
batch, channels, frames, _height, _width = x.shape
|
||||
if self.mode == "upsample3d":
|
||||
x = self.time_conv(x)
|
||||
x = rearrange(x, "b (split c) t h w -> b c (t split) h w", split=2)
|
||||
if x.shape[1:3] != (channels, frames * 2):
|
||||
raise RuntimeError("Unexpected temporal upsample shape")
|
||||
if is_first_chunk:
|
||||
x = x[:, :, 1:]
|
||||
x = rearrange(x, "b c t h w -> (b t) c h w")
|
||||
x = self.resample(x)
|
||||
return rearrange(x, "(b t) c h w -> b c t h w", b=batch)
|
||||
|
||||
|
||||
class _MidBlock3d(nn.Module):
|
||||
def __init__(self, channels: int, num_layers: int, *, is_causal: bool) -> None:
|
||||
super().__init__()
|
||||
self.resnets = nn.ModuleList([_ResNetBlock3d(channels, is_causal=is_causal) for _ in range(num_layers)])
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
for resnet in self.resnets:
|
||||
x = resnet(x)
|
||||
return x
|
||||
|
||||
|
||||
class _UpBlock3d(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: int,
|
||||
num_layers: int,
|
||||
*,
|
||||
spatio_temporal_scale: bool,
|
||||
spatio_only: bool,
|
||||
is_causal: bool,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.conv_in = (
|
||||
_ResNetBlock3d(in_channels, out_channels, is_causal=is_causal) if in_channels != out_channels else None
|
||||
)
|
||||
self.upsamplers = (
|
||||
nn.ModuleList([_WanResample(out_channels, "upsample2d" if spatio_only else "upsample3d")])
|
||||
if spatio_temporal_scale
|
||||
else None
|
||||
)
|
||||
self.resnets = nn.ModuleList(
|
||||
[
|
||||
_ResNetBlock3d(
|
||||
out_channels,
|
||||
is_upsampler_modified=spatio_temporal_scale,
|
||||
is_causal=is_causal,
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor, *, is_first_chunk: bool) -> torch.Tensor:
|
||||
if self.conv_in is not None:
|
||||
x = self.conv_in(x)
|
||||
if self.upsamplers is not None:
|
||||
for upsampler in self.upsamplers:
|
||||
x = upsampler(x, is_first_chunk=is_first_chunk)
|
||||
for resnet in self.resnets:
|
||||
x = resnet(x)
|
||||
return x
|
||||
|
||||
|
||||
class _TurboDecoder3d(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
latent_channels: int,
|
||||
out_channels: int,
|
||||
block_out_channels: tuple[int, ...],
|
||||
layers_per_block: tuple[int, ...],
|
||||
spatio_temporal_scaling: tuple[bool, ...],
|
||||
spatio_only: tuple[bool, ...],
|
||||
patch_size: int,
|
||||
decoder_causal: bool,
|
||||
use_unpatchify: bool,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if patch_size != 2 or not use_unpatchify:
|
||||
raise ValueError("The released MAGI-2 TurboVAED requires patch_size=2 and use_unpatchify=true")
|
||||
self.patch_size = patch_size
|
||||
blocks = tuple(reversed(block_out_channels))
|
||||
scaling = tuple(reversed(spatio_temporal_scaling))
|
||||
layers = tuple(reversed(layers_per_block))
|
||||
spatial_only = tuple(reversed(spatio_only))
|
||||
current = blocks[0]
|
||||
|
||||
self.conv_in = _CausalConv3d(latent_channels, current, is_causal=decoder_causal)
|
||||
self.mid_block = _MidBlock3d(current, layers[0], is_causal=decoder_causal)
|
||||
self.up_blocks = nn.ModuleList()
|
||||
for index, output in enumerate(blocks):
|
||||
self.up_blocks.append(
|
||||
_UpBlock3d(
|
||||
current,
|
||||
output,
|
||||
layers[index + 1],
|
||||
spatio_temporal_scale=scaling[index],
|
||||
spatio_only=spatial_only[index],
|
||||
is_causal=decoder_causal,
|
||||
)
|
||||
)
|
||||
current = output
|
||||
self.conv_act = nn.SiLU()
|
||||
self.conv_out = _CausalConv3d(
|
||||
current,
|
||||
out_channels * patch_size * patch_size,
|
||||
is_causal=decoder_causal,
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor, *, is_first_chunk: bool) -> torch.Tensor:
|
||||
x = self.mid_block(self.conv_in(x))
|
||||
for block in self.up_blocks:
|
||||
x = block(x, is_first_chunk=is_first_chunk)
|
||||
dtype = x.dtype
|
||||
x = (x * torch.rsqrt(x.float().pow(2).mean(1, keepdim=True) + 1e-8)).to(dtype)
|
||||
return turbo_unpatchify(self.conv_out(self.conv_act(x)), self.patch_size)
|
||||
|
||||
|
||||
def _load_turbo_config(config_path: Path) -> dict[str, Any]:
|
||||
if not config_path.is_file():
|
||||
raise FileNotFoundError(f"TurboVAED config does not exist: {config_path}")
|
||||
with config_path.open(encoding="utf-8") as file:
|
||||
config = json.load(file)
|
||||
unsupported = {
|
||||
"decoder_causal": config.get("decoder_causal", False),
|
||||
"decoder_is_dw_conv": any(config.get("decoder_is_dw_conv", [])),
|
||||
"use_unpatchify": not config.get("use_unpatchify", False),
|
||||
}
|
||||
enabled = [name for name, value in unsupported.items() if value]
|
||||
if enabled:
|
||||
raise ValueError(f"Unsupported TurboVAED configuration: {', '.join(enabled)}")
|
||||
return config
|
||||
|
||||
|
||||
def extract_turbo_decoder_state_dict(checkpoint: Mapping[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Select decoder EMA tensors and strip the training module prefix."""
|
||||
|
||||
state: Any = checkpoint
|
||||
if "ema_state_dict" in state:
|
||||
state = state["ema_state_dict"]
|
||||
elif "state_dict" in state:
|
||||
state = state["state_dict"]
|
||||
if not isinstance(state, dict):
|
||||
raise ValueError("TurboVAED checkpoint does not contain a state dictionary")
|
||||
result = {}
|
||||
for key, value in state.items():
|
||||
target = key[7:] if key.startswith("module.") else key
|
||||
if target.startswith("decoder."):
|
||||
result[target] = value
|
||||
if not result:
|
||||
raise ValueError("TurboVAED checkpoint contains no decoder tensors")
|
||||
return result
|
||||
|
||||
|
||||
class Magi2TurboVAEDecoder(nn.Module, DistributedVaeMixin):
|
||||
"""Load and run MAGI-2's default distilled video decoder."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_path: str | Path,
|
||||
checkpoint_path: str | Path,
|
||||
*,
|
||||
device: str | torch.device = "cpu",
|
||||
dtype: torch.dtype = torch.float32,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
config = _load_turbo_config(Path(config_path))
|
||||
self.z_dim = int(config["latent_channels"])
|
||||
self.first_chunk_size = int(config["first_chunk_size"])
|
||||
self.step_size = int(config["step_size"])
|
||||
self.temporal_compression_ratio = int(config["temporal_compression_ratio"])
|
||||
self.spatial_compression_ratio = int(config["spatial_compression_ratio"])
|
||||
self.decoder = _TurboDecoder3d(
|
||||
latent_channels=self.z_dim,
|
||||
out_channels=int(config["out_channels"]),
|
||||
block_out_channels=tuple(config["decoder_block_out_channels"]),
|
||||
layers_per_block=tuple(config["decoder_layers_per_block"]),
|
||||
spatio_temporal_scaling=tuple(config["decoder_spatio_temporal_scaling"]),
|
||||
spatio_only=tuple(config["decoder_spatio_only"]),
|
||||
patch_size=int(config["patch_size"]),
|
||||
decoder_causal=bool(config["decoder_causal"]),
|
||||
use_unpatchify=bool(config["use_unpatchify"]),
|
||||
).to(device=device, dtype=dtype)
|
||||
self.register_buffer(
|
||||
"latent_mean",
|
||||
torch.tensor(_WAN22_LATENT_MEAN, dtype=torch.float32, device=device),
|
||||
persistent=False,
|
||||
)
|
||||
self.register_buffer(
|
||||
"latent_std",
|
||||
torch.tensor(_WAN22_LATENT_STD, dtype=torch.float32, device=device),
|
||||
persistent=False,
|
||||
)
|
||||
|
||||
checkpoint_path = Path(checkpoint_path)
|
||||
if not checkpoint_path.is_file():
|
||||
raise FileNotFoundError(f"TurboVAED checkpoint does not exist: {checkpoint_path}")
|
||||
checkpoint = torch.load(
|
||||
checkpoint_path,
|
||||
map_location="cpu",
|
||||
weights_only=True,
|
||||
mmap=True,
|
||||
)
|
||||
state = extract_turbo_decoder_state_dict(checkpoint)
|
||||
self.load_state_dict(state, strict=True)
|
||||
self.eval().requires_grad_(False)
|
||||
self.use_tiling = False
|
||||
self.use_slicing = False
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
self.init_distributed()
|
||||
|
||||
def set_parallel_size(self, parallel_size: int, mode: str = "tile") -> None:
|
||||
if mode != "tile":
|
||||
raise ValueError(f"MAGI-2 TurboVAE supports temporal tile parallelism only, got {mode!r}")
|
||||
if not hasattr(self, "distributed_executor"):
|
||||
if parallel_size > 1:
|
||||
raise RuntimeError("TurboVAE patch parallelism requires initialized torch.distributed")
|
||||
return
|
||||
super().set_parallel_size(parallel_size, mode=mode)
|
||||
|
||||
def is_distributed_enabled(self) -> bool:
|
||||
return hasattr(self, "distributed_executor") and super().is_distributed_enabled()
|
||||
|
||||
def _prepare_latent(self, z: torch.Tensor) -> tuple[torch.Tensor, int]:
|
||||
z = z.to(device=self.latent_std.device)
|
||||
dtype = z.dtype
|
||||
z = z * self.latent_std.view(1, self.z_dim, 1, 1, 1) + self.latent_mean.view(1, self.z_dim, 1, 1, 1)
|
||||
z = z.to(dtype)
|
||||
first = self.first_chunk_size
|
||||
step = self.step_size
|
||||
frames = z.shape[2]
|
||||
if frames < first:
|
||||
padding = first - frames
|
||||
elif (frames - first) % step:
|
||||
padding = step - (frames - first) % step
|
||||
else:
|
||||
padding = 0
|
||||
if padding:
|
||||
z = torch.cat([z, z[:, :, -1:].repeat(1, 1, padding, 1, 1)], dim=2)
|
||||
return z, padding
|
||||
|
||||
def _chunk_tasks(self, z: torch.Tensor, padding: int) -> tuple[list[TileTask], GridSpec]:
|
||||
first = self.first_chunk_size
|
||||
step = self.step_size
|
||||
overlap = self.temporal_compression_ratio
|
||||
frames = z.shape[2]
|
||||
descriptors: list[tuple[int, int, bool, int, int]] = []
|
||||
if frames == first:
|
||||
descriptors.append((0, frames, True, 0, 0))
|
||||
else:
|
||||
descriptors.append((0, first + 1, True, 0, overlap))
|
||||
for index in range(first, frames, step):
|
||||
last = index + step == frames
|
||||
descriptors.append(
|
||||
(
|
||||
index - 1,
|
||||
index + step if last else index + step + 1,
|
||||
False,
|
||||
overlap,
|
||||
0 if last else overlap,
|
||||
)
|
||||
)
|
||||
|
||||
tasks = [
|
||||
TileTask(
|
||||
tile_id=chunk_index,
|
||||
grid_coord=(chunk_index, int(is_first), crop_left, crop_right),
|
||||
tensor=z[:, :, left:right],
|
||||
workload=(right - left) * z.shape[3] * z.shape[4],
|
||||
)
|
||||
for chunk_index, (left, right, is_first, crop_left, crop_right) in enumerate(descriptors)
|
||||
]
|
||||
return tasks, GridSpec(
|
||||
split_dims=(2,),
|
||||
grid_shape=(len(tasks),),
|
||||
tile_spec={"padding": padding},
|
||||
output_dtype=z.dtype,
|
||||
)
|
||||
|
||||
def _decode_chunk(self, task: TileTask) -> torch.Tensor:
|
||||
_index, is_first, crop_left, crop_right = task.grid_coord
|
||||
assert isinstance(task.tensor, torch.Tensor)
|
||||
output = self.decoder(task.tensor, is_first_chunk=bool(is_first))
|
||||
right = output.shape[2] - crop_right if crop_right else output.shape[2]
|
||||
return output[:, :, crop_left:right]
|
||||
|
||||
def _merge_chunks(
|
||||
self,
|
||||
chunks: dict[tuple[int, ...], torch.Tensor],
|
||||
grid_spec: GridSpec,
|
||||
) -> torch.Tensor:
|
||||
output = torch.cat([tensor for _coord, tensor in sorted(chunks.items())], dim=2)
|
||||
padding = int(grid_spec.tile_spec["padding"])
|
||||
if padding:
|
||||
output = output[:, :, : -padding * self.temporal_compression_ratio]
|
||||
return output
|
||||
|
||||
@torch.inference_mode()
|
||||
def decode(self, z: torch.Tensor, *, output_offload: bool = False) -> torch.Tensor:
|
||||
z, padding = self._prepare_latent(z)
|
||||
tasks, grid_spec = self._chunk_tasks(z, padding)
|
||||
if self.is_distributed_enabled():
|
||||
output = self.distributed_executor.execute(
|
||||
z,
|
||||
DistributedOperator(
|
||||
split=lambda _z: (tasks, grid_spec),
|
||||
exec=self._decode_chunk,
|
||||
merge=self._merge_chunks,
|
||||
),
|
||||
broadcast_result=False,
|
||||
)
|
||||
return output.cpu() if output_offload and output.numel() else output
|
||||
|
||||
chunks = []
|
||||
for task in tasks:
|
||||
tensor = self._decode_chunk(task)
|
||||
chunks.append((task.grid_coord, tensor.cpu() if output_offload else tensor))
|
||||
return self._merge_chunks(dict(chunks), grid_spec)
|
||||
|
||||
forward = decode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Magi2TurboVAEDecoder",
|
||||
"extract_turbo_decoder_state_dict",
|
||||
"turbo_unpatchify",
|
||||
]
|
||||
Reference in new issue
Block a user