Files
vllm-omni/vllm_omni/diffusion/models/dreamzero/causal_wan_model.py
T

1107 lines
44 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CausalWanModel — 40-layer DiT with causal attention and KV cache.
Key differences from WanTransformer3DModel:
- Causal self-attention (new frames only see history)
- KV cache for streaming inference
- Action/state token support (appended after video tokens)
- Extended RoPE with action/state-specific frequencies
- Inference-only forward with KV cache
"""
from __future__ import annotations
import math
from typing import Any
import torch
import torch.nn as nn
from vllm.distributed import (
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_reduce,
)
from vllm.model_executor.layers.conv import Conv3dLayer
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.utils import set_weight_attrs
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.models.dreamzero.action_encoder import (
CategorySpecificMLP,
MultiEmbodimentActionEncoder,
)
# AR-Diffusion paged self-attention (in-tree experimental engine). Import at
# module level so the isinstance check + custom-op call trace cleanly inside
# the fullgraph-compiled DiT block (an import inside the traced region would
# graph-break). The model still works without the engine: the payload type is
# only ever constructed by the AR-Diffusion runner.
try:
from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import (
ARDiffusionPagedLayerInputs,
paged_write_attn,
)
except ImportError: # pragma: no cover - experimental package always ships in-tree
ARDiffusionPagedLayerInputs = None
paged_write_attn = None
# ── RoPE utilities ──────────────────────────────────────────────────
def sinusoidal_embedding_1d(dim: int, position: torch.Tensor) -> torch.Tensor:
"""Sinusoidal positional embedding for timesteps."""
if dim % 2 != 0:
raise ValueError(f"dim must be even, got {dim}.")
half = dim // 2
position = position.type(torch.float64)
sinusoid = torch.outer(
position,
torch.pow(10000, -torch.arange(half, dtype=position.dtype, device=position.device).div(half)),
)
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
return x
def rope_params(max_seq_len: int, dim: int) -> torch.Tensor:
"""Precompute complex-valued RoPE frequencies (polar form).
Returns: complex tensor [max_seq_len, dim // 2]
"""
if dim % 2 != 0:
raise ValueError(f"dim must be even, got {dim}.")
freqs = torch.outer(
torch.arange(max_seq_len),
1.0 / torch.pow(10000, torch.arange(0, dim, 2).to(torch.float64).div(dim)),
)
freqs = torch.polar(torch.ones_like(freqs), freqs)
return freqs
def rope_apply(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
"""Apply RoPE to x using precomputed complex freqs."""
B, seq_len, n, _ = x.shape
x = torch.view_as_complex(x.to(torch.float64).reshape(B, seq_len, n, -1, 2))
freqs = freqs.unsqueeze(0)
x = torch.view_as_real(x * freqs).flatten(3)
return x
def rope_action_apply(
x: torch.Tensor,
freqs: torch.Tensor,
freqs_action: torch.Tensor,
freqs_state: torch.Tensor,
action_register_length: int | None,
num_action_per_block: int = 32,
num_state_per_block: int = 1,
) -> torch.Tensor:
"""RoPE with action/state frequency tables for multi-step sequences."""
B, seq_len, n, _ = x.shape
x = torch.view_as_complex(x.to(torch.float64).reshape(B, seq_len, n, -1, 2))
if action_register_length is not None:
if num_action_per_block is None:
raise ValueError("num_action_per_block is required when action_register_length is set.")
if num_state_per_block is None:
raise ValueError("num_state_per_block is required when action_register_length is set.")
chunk_size = action_register_length // (num_action_per_block + num_state_per_block)
freqs_1d_action = freqs_action[: chunk_size * num_action_per_block].view(
chunk_size * num_action_per_block, 1, -1
)
freqs_1d_state = freqs_state[: chunk_size * num_state_per_block].view(chunk_size * num_state_per_block, 1, -1)
freqs = torch.cat([freqs, freqs_1d_action, freqs_1d_state], dim=0)
freqs = freqs.unsqueeze(0)
x = torch.view_as_real(x * freqs).flatten(3)
return x
def causal_rope_action_apply(
x: torch.Tensor,
freqs: torch.Tensor,
freqs_action: torch.Tensor,
freqs_state: torch.Tensor,
action_register_length: int | None,
num_action_per_block: int,
num_state_per_block: int,
action_state_index: int,
) -> torch.Tensor:
"""RoPE for single inference step (causal / KV-cache mode)."""
B, seq_len, n, _ = x.shape
x = torch.view_as_complex(x.to(torch.float64).reshape(B, seq_len, n, -1, 2))
if action_register_length is not None:
expected_length = num_action_per_block + num_state_per_block
if action_register_length != expected_length:
raise ValueError(
f"action_register_length must equal num_action_per_block + num_state_per_block "
f"({expected_length}), got {action_register_length}."
)
freqs_action = freqs_action[
action_state_index * num_action_per_block : (action_state_index + 1) * num_action_per_block
]
freqs_state = freqs_state[
action_state_index * num_state_per_block : (action_state_index + 1) * num_state_per_block
]
freqs_1d = torch.cat([freqs_action, freqs_state], dim=0).view(action_register_length, 1, -1)
freqs = torch.cat([freqs, freqs_1d], dim=0)
freqs = freqs.unsqueeze(0)
x = torch.view_as_real(x * freqs).flatten(3)
return x
# ── Normalization ───────────────────────────────────────────────────
class WanLayerNorm(nn.LayerNorm):
"""LayerNorm wrapper used by DreamZero blocks."""
def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine: bool = False) -> None:
super().__init__(dim, eps=eps, elementwise_affine=elementwise_affine)
class DistributedRMSNorm(nn.Module):
"""RMSNorm that computes global RMS across tensor parallel ranks."""
def __init__(self, hidden_size: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(hidden_size))
set_weight_attrs(self.weight, {"weight_loader": self.weight_loader})
def weight_loader(self, param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
if param.shape == loaded_weight.shape:
param.data.copy_(loaded_weight)
return
tp_size = get_tensor_model_parallel_world_size()
if loaded_weight.shape[0] % tp_size != 0:
raise ValueError(
f"Cannot shard RMSNorm weight of shape {tuple(loaded_weight.shape)} across tp_size={tp_size}."
)
shard_size = loaded_weight.shape[0] // tp_size
start_idx = get_tensor_model_parallel_rank() * shard_size
shard = loaded_weight.narrow(0, start_idx, shard_size)
if param.shape != shard.shape:
raise ValueError(f"RMSNorm shard shape mismatch: param={tuple(param.shape)}, shard={tuple(shard.shape)}.")
param.data.copy_(shard)
def _local_sum_sq(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, int]:
"""Per-rank float32 activation, local sum-of-squares, and local width."""
x_float = x.float()
local_sum_sq = x_float.pow(2).sum(dim=-1, keepdim=True)
return x_float, local_sum_sq, x.shape[-1]
def _scale(
self,
x_float: torch.Tensor,
global_sum_sq: torch.Tensor,
global_count: int,
x: torch.Tensor,
) -> torch.Tensor:
"""Apply the (already reduced) RMS and the per-rank weight shard."""
mean_sq = global_sum_sq / global_count
return (x_float * torch.rsqrt(mean_sq + self.eps)).type_as(x) * self.weight
def forward(self, x: torch.Tensor) -> torch.Tensor:
tp_size = get_tensor_model_parallel_world_size()
x_float, local_sum_sq, local_count = self._local_sum_sq(x)
if tp_size > 1:
# Use vLLM's collective (custom all-reduce / symmetric-mem fast path
# for small tensors) instead of raw torch.distributed.all_reduce, and
# take the return value so the custom-AR path (which may return a new
# buffer) is handled correctly. No .clone() needed.
global_sum_sq = tensor_model_parallel_all_reduce(local_sum_sq)
global_count = local_count * tp_size
else:
global_sum_sq = local_sum_sq
global_count = local_count
return self._scale(x_float, global_sum_sq, global_count, x)
def fused_qk_rms_norm(
norm_q: nn.Module,
norm_k: nn.Module,
q: torch.Tensor,
k: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply q/k :class:`DistributedRMSNorm` with a SINGLE fused TP all-reduce.
WHY: self-attention norms q and k every step; on TP each norm issues its own
tiny all-reduce of the per-token sum-of-squares. For DreamZero's decode-like
per-step forward these latency-bound micro-collectives dominate, so we pack
both sum-of-squares into one tensor and reduce once (2 collectives → 1).
NUMERICALLY IDENTICAL to ``norm_q(q), norm_k(k)``: all-reduce is elementwise,
so packing along the last dim reduces each slice independently with the same
fp32 accumulation. Requires q and k to share the same shape (true for
self-attention — both come from the same hidden states). Falls back to
independent application when either norm is not a DistributedRMSNorm
(e.g. nn.Identity when qk_norm=False).
"""
if not (isinstance(norm_q, DistributedRMSNorm) and isinstance(norm_k, DistributedRMSNorm)):
return norm_q(q), norm_k(k)
# The fused path reduces one packed sum-of-squares and reuses q's token width
# as the RMS count for BOTH q and k, so q and k must share the same shape.
# (Self-attention guarantees this: q and k are projected from the same x.)
assert q.shape == k.shape, "fused_qk_rms_norm requires q and k to have the same shape."
tp_size = get_tensor_model_parallel_world_size()
q_float, q_sum_sq, count = norm_q._local_sum_sq(q)
k_float, k_sum_sq, _ = norm_k._local_sum_sq(k)
if tp_size > 1:
packed = torch.cat([q_sum_sq, k_sum_sq], dim=-1)
packed = tensor_model_parallel_all_reduce(packed)
q_sum_sq, k_sum_sq = packed[..., 0:1], packed[..., 1:2]
count = count * tp_size
q_out = norm_q._scale(q_float, q_sum_sq, count, q)
k_out = norm_k._scale(k_float, k_sum_sq, count, k)
return q_out, k_out
# ── Projections ─────────────────────────────────────────────────────
class MLPProj(nn.Module):
"""CLIP feature projection for i2v.
Uses ColumnParallelLinear + RowParallelLinear (Qwen3_VisionMLP pattern).
"""
def __init__(self, in_dim: int, out_dim: int) -> None:
super().__init__()
self.norm1 = nn.LayerNorm(in_dim)
self.fc1 = ColumnParallelLinear(
in_dim,
in_dim,
bias=True,
return_bias=False,
)
self.act = nn.GELU()
self.fc2 = RowParallelLinear(
in_dim,
out_dim,
bias=True,
return_bias=False,
)
self.norm2 = nn.LayerNorm(out_dim)
def forward(self, image_embeds: torch.Tensor) -> torch.Tensor:
x = self.norm1(image_embeds)
x = self.fc1(x)
x = self.act(x)
x = self.fc2(x)
x = self.norm2(x)
return x
# ── Cross-Attention ─────────────────────────────────────────────────
# T2V and I2V cross-attention variants
class WanT2VCrossAttention(nn.Module):
"""Text-to-video cross-attention.
Uses vllm-omni Attention for FlashAttn backend.
"""
def __init__(self, dim: int, num_heads: int, window_size=(-1, -1), qk_norm: bool = True, eps: float = 1e-6) -> None:
super().__init__()
if dim % num_heads != 0:
raise ValueError(f"dim={dim} must be divisible by num_heads={num_heads}.")
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
tp_size = get_tensor_model_parallel_world_size()
if num_heads % tp_size != 0:
raise ValueError(f"num_heads={num_heads} must be divisible by tp_size={tp_size}.")
self.tp_num_heads = num_heads // tp_size
self.tp_inner_dim = self.tp_num_heads * self.head_dim
self.q = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.k = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.v = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.o = RowParallelLinear(dim, dim, bias=True, input_is_parallel=True, return_bias=False)
self.norm_q = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.attn = Attention(
self.tp_num_heads,
self.head_dim,
causal=False,
softmax_scale=self.head_dim**-0.5,
skip_sequence_parallel=True,
)
def forward(
self,
x: torch.Tensor,
context: torch.Tensor,
context_lens: torch.Tensor | None = None,
crossattn_cache: dict | None = None,
) -> torch.Tensor:
del context_lens
n, d = self.tp_num_heads, self.head_dim
q = self.norm_q(self.q(x)).unflatten(2, (n, d))
if crossattn_cache is not None:
if not crossattn_cache["is_init"]:
crossattn_cache["is_init"] = True
k = self.norm_k(self.k(context)).unflatten(2, (n, d))
v = self.v(context).unflatten(2, (n, d))
crossattn_cache["k"] = k
crossattn_cache["v"] = v
else:
k = crossattn_cache["k"]
v = crossattn_cache["v"]
else:
k = self.norm_k(self.k(context)).unflatten(2, (n, d))
v = self.v(context).unflatten(2, (n, d))
x = self.attn(q, k, v)
x = x.flatten(2)
x = self.o(x)
return x
class WanI2VCrossAttention(nn.Module):
"""Image-to-video cross-attention (splits first 257 image tokens).
Uses vllm-omni Attention for FlashAttn backend.
"""
def __init__(self, dim: int, num_heads: int, window_size=(-1, -1), qk_norm: bool = True, eps: float = 1e-6) -> None:
super().__init__()
if dim % num_heads != 0:
raise ValueError(f"dim={dim} must be divisible by num_heads={num_heads}.")
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
tp_size = get_tensor_model_parallel_world_size()
if num_heads % tp_size != 0:
raise ValueError(f"num_heads={num_heads} must be divisible by tp_size={tp_size}.")
self.tp_num_heads = num_heads // tp_size
self.tp_inner_dim = self.tp_num_heads * self.head_dim
self.q = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.k = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.v = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.o = RowParallelLinear(dim, dim, bias=True, input_is_parallel=True, return_bias=False)
self.norm_q = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.k_img = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.v_img = ColumnParallelLinear(dim, dim, bias=True, gather_output=False, return_bias=False)
self.norm_k_img = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.attn = Attention(
self.tp_num_heads,
self.head_dim,
causal=False,
softmax_scale=self.head_dim**-0.5,
skip_sequence_parallel=True,
)
def forward(
self,
x: torch.Tensor,
context: torch.Tensor,
context_lens: torch.Tensor | None = None,
crossattn_cache: dict | None = None,
) -> torch.Tensor:
del context_lens
context_img = context[:, :257]
context = context[:, 257:]
n, d = self.tp_num_heads, self.head_dim
q = self.norm_q(self.q(x)).unflatten(2, (n, d))
# context (text) and context_img (clip features) are constant within a
# session, so k/v and k_img/v_img are cached on first call and reused.
# context_img == img_emb(clip feature) is session-invariant (state.clip_feas
# is set once at current_start_frame==0 and only cleared on reset, which also
# rebuilds crossattn_cache). Caching k_img/v_img removes a per-step
# norm_k_img all-reduce plus the k_img/v_img projection GEMMs in steady state.
# Only q depends on the per-step x and is always recomputed.
if crossattn_cache is not None:
if not crossattn_cache["is_init"]:
crossattn_cache["is_init"] = True
k = self.norm_k(self.k(context)).unflatten(2, (n, d))
v = self.v(context).unflatten(2, (n, d))
k_img = self.norm_k_img(self.k_img(context_img)).unflatten(2, (n, d))
v_img = self.v_img(context_img).unflatten(2, (n, d))
crossattn_cache["k"] = k
crossattn_cache["v"] = v
crossattn_cache["k_img"] = k_img
crossattn_cache["v_img"] = v_img
else:
k = crossattn_cache["k"]
v = crossattn_cache["v"]
k_img = crossattn_cache["k_img"]
v_img = crossattn_cache["v_img"]
else:
k = self.norm_k(self.k(context)).unflatten(2, (n, d))
v = self.v(context).unflatten(2, (n, d))
k_img = self.norm_k_img(self.k_img(context_img)).unflatten(2, (n, d))
v_img = self.v_img(context_img).unflatten(2, (n, d))
x = self.attn(q, k, v)
img_x = self.attn(q, k_img, v_img)
x = x.flatten(2)
img_x = img_x.flatten(2)
x = x + img_x
x = self.o(x)
return x
WAN_CROSSATTENTION_CLASSES = {
"t2v_cross_attn": WanT2VCrossAttention,
"i2v_cross_attn": WanI2VCrossAttention,
}
# ── Self-Attention with causal masking + KV cache ───────────────────
class CausalWanSelfAttention(nn.Module):
"""Causal self-attention with KV cache + action/state tokens."""
def __init__(
self,
dim: int,
num_heads: int,
frame_seqlen: int,
local_attn_size: int = -1,
sink_size: int = 0,
num_frame_per_block: int = 1,
qk_norm: bool = True,
eps: float = 1e-6,
num_action_per_block: int = 32,
num_state_per_block: int = 1,
) -> None:
super().__init__()
if dim % num_heads != 0:
raise ValueError(f"dim={dim} must be divisible by num_heads={num_heads}.")
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
tp_size = get_tensor_model_parallel_world_size()
if num_heads % tp_size != 0:
raise ValueError(f"num_heads={num_heads} must be divisible by tp_size={tp_size}.")
self.tp_num_heads = num_heads // tp_size
self.tp_inner_dim = self.tp_num_heads * self.head_dim
self.local_attn_size = local_attn_size
self.num_frame_per_block = num_frame_per_block
self.frame_seqlen = frame_seqlen
self.num_action_per_block = num_action_per_block
self.num_state_per_block = num_state_per_block
self.max_attention_size = 21 * frame_seqlen if local_attn_size == -1 else local_attn_size * frame_seqlen
# Fused QKV projection: q/k/v all come from x, so a single column-parallel
# GEMM replaces three (fewer launches / better GEMM util in the decode-like
# per-step forward). No GQA here: total_num_kv_heads defaults to num_heads.
self.qkv = QKVParallelLinear(
hidden_size=dim,
head_size=self.head_dim,
total_num_heads=num_heads,
bias=True,
)
# The forward splits qkv into three equal q/k/v shards, which is only
# correct without GQA. total_num_kv_heads defaults to total_num_heads
# here; fail loud if a future checkpoint introduces GQA so the equal
# split does not silently misalign k/v.
if self.qkv.total_num_kv_heads != self.qkv.total_num_heads:
raise ValueError("Self-attn QKV fusion requires no GQA (total_num_kv_heads == total_num_heads).")
self.o = RowParallelLinear(dim, dim, bias=True, input_is_parallel=True, return_bias=False)
self.norm_q = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = DistributedRMSNorm(self.tp_inner_dim, eps=eps) if qk_norm else nn.Identity()
self.attn = Attention(
self.tp_num_heads,
self.head_dim,
causal=False,
softmax_scale=self.head_dim**-0.5,
skip_sequence_parallel=True,
)
def forward(
self,
x: torch.Tensor,
freqs: torch.Tensor,
freqs_action: torch.Tensor,
freqs_state: torch.Tensor,
action_register_length: int | None,
kv_cache: torch.Tensor | Any | None = None,
current_start_frame: int = 0,
is_tf: bool = True,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Inference-only forward (KV cache path)."""
n, d = self.tp_num_heads, self.head_dim
# Single fused QKV GEMM, then split into the per-rank q/k/v shards.
qkv, _ = self.qkv(x)
qk_size = self.tp_num_heads * self.head_dim
q, k, v = qkv.split([qk_size, qk_size, qk_size], dim=-1)
# Fuse q/k qk-norm into a single TP all-reduce (both come from x, same shape).
q, k = fused_qk_rms_norm(self.norm_q, self.norm_k, q, k)
q = q.unflatten(2, (n, d))
k = k.unflatten(2, (n, d))
v = v.unflatten(2, (n, d))
updated_kv_cache: torch.Tensor | None = None
if kv_cache is None:
raise RuntimeError("Inference only: kv_cache is required.")
action_state_index = max(0, (current_start_frame - 1) // self.num_frame_per_block)
roped_query = causal_rope_action_apply(
q,
freqs,
freqs_action,
freqs_state,
action_register_length,
self.num_action_per_block,
self.num_state_per_block,
action_state_index,
).type_as(v)
roped_key = causal_rope_action_apply(
k,
freqs,
freqs_action,
freqs_state,
action_register_length,
self.num_action_per_block,
self.num_state_per_block,
action_state_index,
).type_as(v)
roped_action_query = None
roped_action_key = None
action_v = None
if action_register_length is not None:
roped_action_query = roped_query[:, -action_register_length:]
roped_query = roped_query[:, :-action_register_length]
roped_action_key = roped_key[:, -action_register_length:]
roped_key = roped_key[:, :-action_register_length]
action_v = v[:, -action_register_length:]
v = v[:, :-action_register_length]
if ARDiffusionPagedLayerInputs is not None and isinstance(kv_cache, ARDiffusionPagedLayerInputs):
# Fused write+attend custom op: one opaque node in the compiled
# graph (slot writes + FlashAttention block-table kernel inside).
# Metadata tensors were prepared once per forward in _forward_blocks.
if action_register_length is not None:
q_cat = torch.cat([roped_query, roped_action_query], dim=1)
k_act, v_act = roped_action_key[0], action_v[0]
else:
q_cat = roped_query
k_act = v_act = None
x = paged_write_attn(
kv_cache,
q_cat[0],
roped_key[0],
v[0],
k_act,
v_act,
self.head_dim**-0.5,
).unsqueeze(0)
else:
updated_k = kv_cache[0]
updated_v = kv_cache[1]
new_k = torch.cat([updated_k, roped_key], dim=1)
new_v = torch.cat([updated_v, v], dim=1)
new_k = new_k[:, -self.max_attention_size :]
new_v = new_v[:, -self.max_attention_size :]
if action_register_length is not None:
q_cat = torch.cat([roped_query, roped_action_query], dim=1)
k_cat = torch.cat([new_k, roped_action_key], dim=1)
v_cat = torch.cat([new_v, action_v], dim=1)
else:
q_cat = roped_query
k_cat = new_k
v_cat = new_v
x = self.attn(q_cat, k_cat, v_cat)
updated_kv_cache = torch.stack([new_k, new_v], dim=0)
x = x.flatten(2)
x = self.o(x)
return x, updated_kv_cache
# ── Attention Block ─────────────────────────────────────────────────
class CausalWanAttentionBlock(nn.Module):
"""Transformer block: self-attn + cross-attn + FFN with 6-param modulation."""
def __init__(
self,
cross_attn_type: str,
dim: int,
ffn_dim: int,
num_heads: int,
frame_seqlen: int,
local_attn_size: int = -1,
sink_size: int = 0,
num_frame_per_block: int = 1,
qk_norm: bool = True,
cross_attn_norm: bool = False,
eps: float = 1e-6,
num_action_per_block: int = 32,
num_state_per_block: int = 1,
) -> None:
super().__init__()
self.norm1 = WanLayerNorm(dim, eps)
self.self_attn = CausalWanSelfAttention(
dim=dim,
num_heads=num_heads,
frame_seqlen=frame_seqlen,
local_attn_size=local_attn_size,
sink_size=sink_size,
num_frame_per_block=num_frame_per_block,
qk_norm=qk_norm,
eps=eps,
num_action_per_block=num_action_per_block,
num_state_per_block=num_state_per_block,
)
self.norm3 = WanLayerNorm(dim, eps, elementwise_affine=True) if cross_attn_norm else nn.Identity()
self.cross_attn = WAN_CROSSATTENTION_CLASSES[cross_attn_type](dim, num_heads, (-1, -1), qk_norm, eps)
self.norm2 = WanLayerNorm(dim, eps)
self.ffn = nn.Sequential(
ColumnParallelLinear(dim, ffn_dim, bias=True, gather_output=False, return_bias=False),
nn.GELU(approximate="tanh"),
RowParallelLinear(ffn_dim, dim, bias=True, input_is_parallel=True, return_bias=False),
)
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
def forward(
self,
x: torch.Tensor,
e: torch.Tensor,
freqs: torch.Tensor,
freqs_action: torch.Tensor,
freqs_state: torch.Tensor,
context: torch.Tensor,
action_register_length: int | None = None,
kv_cache: torch.Tensor | Any | None = None,
crossattn_cache: dict | None = None,
current_start_frame: int = 0,
is_tf: bool = True,
) -> tuple[torch.Tensor, torch.Tensor | None]:
e = (self.modulation.unsqueeze(1) + e).chunk(6, dim=2)
y, updated_kv_cache = self.self_attn(
x=(self.norm1(x) * (1 + e[1].squeeze(2)) + e[0].squeeze(2)),
freqs=freqs,
freqs_action=freqs_action,
freqs_state=freqs_state,
action_register_length=action_register_length,
kv_cache=kv_cache,
is_tf=is_tf,
current_start_frame=current_start_frame,
)
x = x + (y * e[2].squeeze(2))
x = x + self.cross_attn(self.norm3(x), context, crossattn_cache=crossattn_cache)
y = self.ffn(self.norm2(x) * (1 + e[4].squeeze(2)) + e[3].squeeze(2))
x = x + (y * e[5].squeeze(2))
return x, updated_kv_cache
# ── Output Head ─────────────────────────────────────────────────────
class CausalHead(nn.Module):
"""Output norm + linear with 2-param modulation.
Runs once per step (not TP-critical), uses nn.Linear.
"""
def __init__(self, dim: int, out_dim: int, patch_size: tuple, eps: float = 1e-6) -> None:
super().__init__()
self.dim = dim
self.out_dim = out_dim
self.patch_size = patch_size
out_channels = math.prod(patch_size) * out_dim
self.norm = WanLayerNorm(dim, eps)
self.head = nn.Linear(dim, out_channels)
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
def forward(self, x: torch.Tensor, e: torch.Tensor) -> torch.Tensor:
"""
Args:
x: [B, L1, C]
e: [B, F, 1, C] (time embedding, unsqueezed)
"""
e = (self.modulation.unsqueeze(1) + e).chunk(2, dim=2)
x = self.head(self.norm(x) * (1 + e[1].squeeze(2)) + e[0].squeeze(2))
return x
# ── Main Model ──────────────────────────────────────────────────────
class CausalWanModel(nn.Module):
"""Causal video diffusion transformer for DreamZero.
Architecture (14B): 40 layers, dim=5120, heads=40, ffn=13824
"""
_layerwise_offload_blocks_attrs = ["blocks"]
def __init__(
self,
model_type: str = "t2v",
patch_size: tuple[int, int, int] = (1, 2, 2),
frame_seqlen: int = 220,
text_len: int = 512,
in_dim: int = 16,
dim: int = 2048,
ffn_dim: int = 8192,
freq_dim: int = 256,
text_dim: int = 4096,
out_dim: int = 16,
num_heads: int = 16,
num_layers: int = 32,
max_chunk_size: int = -1,
sink_size: int = 0,
qk_norm: bool = True,
cross_attn_norm: bool = True,
eps: float = 1e-6,
num_frame_per_block: int = 1,
action_dim: int = 32,
num_registers: int = 8,
max_state_dim: int = 64,
max_num_embodiments: int = 32,
hidden_size: int = 1024,
diffusion_model_pretrained_path: str | None = None,
num_action_per_block: int = 32,
num_state_per_block: int = 1,
) -> None:
super().__init__()
if model_type not in ["t2v", "i2v", "ti2v"]:
raise ValueError(f"Unsupported model_type={model_type!r}; expected one of ['t2v', 'i2v', 'ti2v'].")
self.model_type = model_type
self.patch_size = patch_size
self.frame_seqlen = frame_seqlen
self.text_len = text_len
self.dim = dim
self.freq_dim = freq_dim
self.out_dim = out_dim
self.num_heads = num_heads
self.num_layers = num_layers
self.local_attn_size = max_chunk_size * num_frame_per_block + 1 if max_chunk_size != -1 else -1
self.num_frame_per_block = num_frame_per_block
self.action_dim = action_dim
self.num_action_per_block = num_action_per_block
self.num_state_per_block = num_state_per_block
max_num_embodiments_local = 1
self.state_encoder = CategorySpecificMLP(
num_categories=max_num_embodiments_local,
input_dim=max_state_dim,
hidden_dim=hidden_size,
output_dim=dim,
)
self.action_encoder = MultiEmbodimentActionEncoder(
action_dim=action_dim,
hidden_size=dim,
num_embodiments=max_num_embodiments_local,
)
self.action_decoder = CategorySpecificMLP(
num_categories=max_num_embodiments_local,
input_dim=dim,
hidden_dim=hidden_size,
output_dim=action_dim,
)
# Disable the Conv3d GEMM rewrite for patch embedding.
self.patch_embedding = Conv3dLayer(
in_dim,
dim,
kernel_size=patch_size,
stride=patch_size,
)
self.patch_embedding.enable_linear = False
self.text_embedding = nn.Sequential(
nn.Linear(text_dim, dim),
nn.GELU(approximate="tanh"),
nn.Linear(dim, dim),
)
self.time_embedding = nn.Sequential(
nn.Linear(freq_dim, dim),
nn.SiLU(),
nn.Linear(dim, dim),
)
self.time_projection = nn.Sequential(
nn.SiLU(),
nn.Linear(dim, dim * 6),
)
cross_attn_type = "t2v_cross_attn" if model_type == "t2v" else "i2v_cross_attn"
self.blocks = nn.ModuleList(
[
CausalWanAttentionBlock(
cross_attn_type,
dim,
ffn_dim,
num_heads,
frame_seqlen,
self.local_attn_size,
sink_size,
num_frame_per_block,
qk_norm,
cross_attn_norm,
eps,
num_action_per_block,
num_state_per_block,
)
for _ in range(num_layers)
]
)
self.head = CausalHead(dim, out_dim, patch_size, eps)
if dim % num_heads != 0:
raise ValueError(f"dim={dim} must be divisible by num_heads={num_heads}.")
if (dim // num_heads) % 2 != 0:
raise ValueError(f"dim // num_heads must be even, got {dim // num_heads}.")
d = dim // num_heads
self.freqs_action = rope_params(1024 * 10, d)
self.freqs_state = rope_params(1024, d)
self.freqs = [
rope_params(1024, d - 4 * (d // 6)),
rope_params(1024, 2 * (d // 6)),
rope_params(1024, 2 * (d // 6)),
]
if model_type == "i2v":
self.img_emb = MLPProj(1280, dim)
self.init_weights()
def init_weights(self) -> None:
"""Initialize parameters."""
def _init_linear_like(module: nn.Module) -> None:
if isinstance(module, nn.Linear):
nn.init.xavier_uniform_(module.weight)
if module.bias is not None:
nn.init.zeros_(module.bias)
return
if isinstance(module, (ColumnParallelLinear, RowParallelLinear)):
fan_in = module.input_size
fan_out = module.output_size
bound = math.sqrt(6.0 / float(fan_in + fan_out))
nn.init.uniform_(module.weight, -bound, bound)
if module.bias is not None:
nn.init.zeros_(module.bias)
for module in self.modules():
_init_linear_like(module)
nn.init.xavier_uniform_(self.patch_embedding.weight.flatten(1))
if self.patch_embedding.bias is not None:
fan_in = self.patch_embedding.in_channels * math.prod(self.patch_embedding.kernel_size)
bound = 1 / math.sqrt(fan_in)
nn.init.uniform_(self.patch_embedding.bias, -bound, bound)
for module in self.text_embedding.modules():
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, std=0.02)
for module in self.time_embedding.modules():
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, std=0.02)
nn.init.zeros_(self.head.head.weight)
def _create_freqs(self, grid_size: torch.Tensor, start_frame: int) -> torch.Tensor:
"""Create 3D RoPE frequency tensor."""
device = self.patch_embedding.weight.device
if any(freq.device != device for freq in self.freqs):
self.freqs = [freq.to(device) for freq in self.freqs]
if self.freqs_action.device != device:
self.freqs_action = self.freqs_action.to(device)
if self.freqs_state.device != device:
self.freqs_state = self.freqs_state.to(device)
f, h, w = grid_size.tolist()
freqs = torch.cat(
[
self.freqs[0][start_frame : start_frame + f].view(f, 1, 1, -1).expand(f, h, w, -1),
self.freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
self.freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1),
],
dim=-1,
).reshape(f * h * w, 1, -1)
return freqs
def unpatchify(self, x: torch.Tensor, grid_size: torch.Tensor) -> torch.Tensor:
"""Reconstruct video from patch embeddings."""
B = x.shape[0]
c = self.out_dim
grid_size = grid_size.tolist()
expected_seq_len = math.prod(grid_size)
if x.shape[1] != expected_seq_len:
raise ValueError(f"x sequence length must equal product(grid_size)={expected_seq_len}, got {x.shape[1]}.")
x = x.view(B, *grid_size, *self.patch_size, c)
x = torch.einsum("bfhwpqrc->bcfphqwr", x)
x = x.reshape(B, c, *[i * j for i, j in zip(grid_size, self.patch_size)])
return x
def _forward_blocks(
self,
x: torch.Tensor,
seq_len: int,
freqs: torch.Tensor,
timestep: torch.Tensor,
context: torch.Tensor,
clip_feature: torch.Tensor | None,
embodiment_id: torch.Tensor | None,
action: torch.Tensor | None,
timestep_action: torch.Tensor | None,
state: torch.Tensor | None,
kv_cache: list[Any],
current_start_frame: int,
crossattn_cache: list[dict] | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor | None]]:
x = x.flatten(start_dim=2).transpose(1, 2)
B = x.shape[0]
F_t = timestep.shape[1]
if action is not None:
# Current DreamZero checkpoints have one local action/state adapter.
# Global embodiment IDs are used by transforms and normalization.
adapter_category_id = torch.zeros(B, dtype=torch.long, device=x.device)
action_features = self.action_encoder(action, timestep_action, adapter_category_id)
state_features = self.state_encoder(state, adapter_category_id)
action_register = torch.cat([action_features, state_features], dim=1)
action_length = action_features.shape[1]
action_register_length = action_register.shape[1]
x = torch.cat([x, action_register], dim=1)
else:
action_length = 0
action_register_length = None
timestep = timestep.unsqueeze(-1).expand(B, F_t, seq_len // F_t).reshape(B, -1)
if action is not None:
if timestep_action is None or state is None:
raise RuntimeError("timestep_action and state are required when action is provided.")
state_features_t = self.state_encoder(state, adapter_category_id)
stride = timestep_action.shape[1] // state_features_t.shape[1]
timestep_state = timestep_action[:, ::stride]
timestep = torch.cat([timestep, timestep_action, timestep_state], dim=1)
e = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, timestep.flatten()).type_as(x))
e = e.unflatten(dim=0, sizes=(B, -1))
e0 = self.time_projection(e)
e0 = e0.unflatten(dim=2, sizes=(6, self.dim))
context = self.text_embedding(context)
if clip_feature is not None:
clip_embedding = self.img_emb(clip_feature)
context = torch.cat([clip_embedding, context], dim=1)
# AR-Diffusion paged path: host-side prep ONCE per branch forward, outside
# the compiled blocks — allocate current video/action slots, build the
# padded block-table metadata all 40 layers share, and hand the compiled
# region plain tensors (ARDiffusionPagedLayerInputs) instead of a Python
# context object. Lazy-allocation contract preserved: only the branch this
# CFG-parallel rank executes reaches its _forward_blocks.
if kv_cache and getattr(kv_cache[0], "is_ar_diffusion_paged_context", False):
if B != 1:
raise RuntimeError("AR-Diffusion paged self-attention currently expects batch_size=1")
fctx = kv_cache[0].forward_ctx
if seq_len != fctx.seq_len:
raise RuntimeError(
f"AR-Diffusion paged context seq_len={fctx.seq_len} but current video KV has {seq_len} tokens"
)
fctx.prepare(
device=x.device,
action_len=int(action_register_length or 0),
query_len=int(x.shape[1]),
)
kv_cache = [c.to_layer_inputs() for c in kv_cache]
updated_kv_caches: list[torch.Tensor | None] = []
for block_index, block in enumerate(self.blocks):
x, updated_kv_cache = block(
x=x,
e=e0,
freqs=freqs,
freqs_action=self.freqs_action,
freqs_state=self.freqs_state,
context=context,
action_register_length=action_register_length,
kv_cache=kv_cache[block_index] if kv_cache else None,
crossattn_cache=crossattn_cache[block_index] if crossattn_cache else None,
current_start_frame=current_start_frame,
)
updated_kv_caches.append(updated_kv_cache)
if action is not None:
action_noise_pred = x[:, seq_len : seq_len + action_length]
action_noise_pred = self.action_decoder(action_noise_pred, adapter_category_id)
else:
action_noise_pred = None
x_video = x[:, :seq_len]
e_video = e[:, :seq_len]
x_video = self.head(x_video, e_video.unsqueeze(2))
return x_video, action_noise_pred, updated_kv_caches
def _forward_inference(
self,
x: torch.Tensor,
timestep: torch.Tensor,
context: torch.Tensor,
seq_len: int,
kv_cache: list[Any],
current_start_frame: int,
crossattn_cache: list[dict] | None = None,
y: torch.Tensor | None = None,
clip_feature: torch.Tensor | None = None,
action: torch.Tensor | None = None,
timestep_action: torch.Tensor | None = None,
state: torch.Tensor | None = None,
embodiment_id: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor]]:
if self.model_type == "i2v":
if clip_feature is None or y is None:
raise RuntimeError("clip_feature and y are required for i2v inference.")
if context.shape[1] != self.text_len:
raise ValueError(f"context length must be {self.text_len}, got {context.shape[1]}.")
if y is not None:
x = torch.cat([x, y.to(dtype=x.dtype)], dim=1)
x = self.patch_embedding(x)
grid_size = torch.tensor(x.shape[2:], dtype=torch.long)
freqs = self._create_freqs(grid_size, current_start_frame)
x_video, action_noise_pred, updated_kv_caches = self._forward_blocks(
x=x,
seq_len=seq_len,
freqs=freqs,
timestep=timestep,
context=context,
clip_feature=clip_feature,
embodiment_id=embodiment_id,
action=action,
timestep_action=timestep_action,
state=state,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_frame=current_start_frame,
)
x_video = x_video.clone()
if action_noise_pred is not None:
action_noise_pred = action_noise_pred.clone()
video_noise_pred = self.unpatchify(x_video, grid_size)
return video_noise_pred, action_noise_pred, updated_kv_caches
def forward(self, *args: Any, **kwargs: Any):
"""Inference only. Requires kv_cache."""
return self._forward_inference(*args, **kwargs)