Files
Kaiqin Kongandmergify[bot] 773d44b875 [misc] Rename MatrixGame to MatrixGame2 (#1357)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-17 22:57:03 -07:00

437 lines
15 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Matrix-Game 2.0 causal training model plugin."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal
import torch
from fastvideo.forward_context import set_forward_context
from fastvideo.train.models.matrixgame2.matrixgame2 import MatrixGame2Model
from fastvideo.train.models.wan.wan_causal import WanCausalModel
@dataclass(slots=True)
class _StreamingCaches:
kv_cache: list[dict[str, Any]]
kv_cache_mouse: list[dict[str, Any] | None]
kv_cache_keyboard: list[dict[str, Any] | None]
crossattn_cache: list[dict[str, Any]] | None
frame_seq_length: int
local_attn_size: int
sliding_window_num_frames: int
batch_size: int
dtype: torch.dtype
device: torch.device
class MatrixGame2CausalModel(MatrixGame2Model, WanCausalModel):
"""Matrix-Game 2.0 per-role model with causal/streaming primitives."""
_transformer_cls_name: str = "CausalMatrixGame2WanModel"
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._streaming_caches: dict[tuple[int, str], _StreamingCaches] = {}
def clear_caches(self, *, cache_tag: str = "pos") -> None:
self._streaming_caches.pop((id(self), str(cache_tag)), None)
def predict_noise_streaming(
self,
noisy_latents: torch.Tensor,
timestep: torch.Tensor,
batch: Any,
*,
conditional: bool,
cache_tag: str = "pos",
store_kv: bool = False,
cur_start_frame: int = 0,
cfg_uncond: dict[str, Any] | None = None,
attn_kind: Literal["dense", "vsa"] = "dense",
) -> torch.Tensor | None:
del cfg_uncond
if attn_kind == "dense":
attn_metadata = batch.attn_metadata
elif attn_kind == "vsa":
attn_metadata = batch.attn_metadata_vsa
else:
raise ValueError(f"Unknown attn_kind: {attn_kind!r}")
cache_tag = str(cache_tag)
cur_start_frame = int(cur_start_frame)
if cur_start_frame < 0:
raise ValueError("cur_start_frame must be >= 0")
batch_size = int(noisy_latents.shape[0])
num_frames = int(noisy_latents.shape[1])
timestep_full = self._ensure_per_frame_timestep(
timestep=timestep,
batch_size=batch_size,
num_frames=num_frames,
device=noisy_latents.device,
)
transformer = self._get_transformer(timestep_full)
caches = self._get_or_init_streaming_caches(
cache_tag=cache_tag,
transformer=transformer,
noisy_latents=noisy_latents,
)
frame_seq_length = int(caches.frame_seq_length)
kv_cache = caches.kv_cache
kv_cache_mouse = caches.kv_cache_mouse
kv_cache_keyboard = caches.kv_cache_keyboard
crossattn_cache = caches.crossattn_cache
if self._should_snapshot_streaming_cache() and torch.is_grad_enabled():
kv_cache = self._snapshot_kv_cache_indices(kv_cache)
kv_cache_mouse = self._snapshot_action_kv_cache_indices(kv_cache_mouse)
kv_cache_keyboard = self._snapshot_action_kv_cache_indices(kv_cache_keyboard)
if conditional:
cond_dict = batch.conditional_dict
if cond_dict is None:
raise RuntimeError("Missing conditional_dict in TrainingBatch")
else:
cond_dict = batch.unconditional_dict or batch.conditional_dict
if cond_dict is None:
raise RuntimeError("Missing conditional_dict in TrainingBatch")
device_type = self.device.type
dtype = noisy_latents.dtype
with (
torch.autocast(device_type, dtype=dtype),
set_forward_context(
current_timestep=batch.timesteps,
attn_metadata=attn_metadata,
),
):
input_kwargs = self._build_streaming_input_kwargs(
noisy_latents=noisy_latents,
timestep=timestep_full,
cond_dict=cond_dict,
batch=batch,
cur_start_frame=cur_start_frame,
)
input_kwargs["timestep"] = timestep_full.to(
device=self.device,
dtype=torch.long,
)
input_kwargs.update({
"kv_cache": kv_cache,
"kv_cache_mouse": kv_cache_mouse,
"kv_cache_keyboard": kv_cache_keyboard,
"crossattn_cache": crossattn_cache,
"current_start": cur_start_frame * frame_seq_length,
"start_frame": cur_start_frame,
"is_cache": bool(store_kv),
"num_frame_per_block": num_frames,
})
if store_kv:
with torch.no_grad():
_ = transformer(**input_kwargs)
return None
pred_noise = transformer(**input_kwargs).permute(0, 2, 1, 3, 4)
return pred_noise
def _get_or_init_streaming_caches(
self,
*,
cache_tag: str,
transformer: torch.nn.Module,
noisy_latents: torch.Tensor,
) -> _StreamingCaches:
key = (id(self), cache_tag)
cached = self._streaming_caches.get(key)
batch_size = int(noisy_latents.shape[0])
dtype = noisy_latents.dtype
device = noisy_latents.device
frame_seq_length = self._compute_frame_seq_length(
transformer,
noisy_latents,
)
local_attn_size = self._get_local_attn_size(transformer)
sliding_window_num_frames = self._get_sliding_window_num_frames(transformer)
meta = (
frame_seq_length,
local_attn_size,
sliding_window_num_frames,
batch_size,
dtype,
device,
)
if cached is not None:
cached_meta = (
cached.frame_seq_length,
cached.local_attn_size,
cached.sliding_window_num_frames,
cached.batch_size,
cached.dtype,
cached.device,
)
if cached_meta == meta:
return cached
kv_cache = self._initialize_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
sliding_window_num_frames=sliding_window_num_frames,
checkpoint_safe=self._should_use_checkpoint_safe_kv_cache(),
)
kv_cache_mouse = self._initialize_action_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
channel="mouse",
)
kv_cache_keyboard = self._initialize_action_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
channel="keyboard",
)
crossattn_cache = self._initialize_crossattn_cache(
transformer=transformer,
device=device,
)
caches = _StreamingCaches(
kv_cache=kv_cache,
kv_cache_mouse=kv_cache_mouse,
kv_cache_keyboard=kv_cache_keyboard,
crossattn_cache=crossattn_cache,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
sliding_window_num_frames=sliding_window_num_frames,
batch_size=batch_size,
dtype=dtype,
device=device,
)
self._streaming_caches[key] = caches
return caches
def _build_streaming_input_kwargs(
self,
*,
noisy_latents: torch.Tensor,
timestep: torch.Tensor,
cond_dict: dict[str, Any],
batch: Any,
cur_start_frame: int,
) -> dict[str, Any]:
if batch.image_latents is None:
raise RuntimeError("Matrix-Game 2.0 causal rollout requires image_latents")
if batch.image_embeds is None:
raise RuntimeError("Matrix-Game 2.0 causal rollout requires image_embeds")
num_frames = int(noisy_latents.shape[1])
frame_end = cur_start_frame + num_frames
cond_latents = self._slice_image_latents_for_chunk(
batch.image_latents,
start=cur_start_frame,
end=frame_end,
)
hidden_states = torch.cat(
[noisy_latents.permute(0, 2, 1, 3, 4), cond_latents],
dim=1,
)
return {
"hidden_states": hidden_states,
"encoder_hidden_states": None,
"timestep": timestep,
"encoder_hidden_states_image": cond_dict["encoder_hidden_states_image"],
"keyboard_cond": self._slice_action_prefix(
cond_dict.get("keyboard_cond"),
frame_end=frame_end,
),
"mouse_cond": self._slice_action_prefix(
cond_dict.get("mouse_cond"),
frame_end=frame_end,
),
"return_dict": False,
}
def _slice_image_latents_for_chunk(
self,
image_latents: torch.Tensor,
*,
start: int,
end: int,
) -> torch.Tensor:
num_frames = end - start
if image_latents.ndim != 5:
raise ValueError("image_latents must have shape [B, C, T, H, W], "
f"got {tuple(image_latents.shape)}")
if image_latents.shape[2] >= end:
return image_latents[:, :, start:end]
if image_latents.shape[2] > start:
cond = image_latents[:, :, start:]
pad_frames = num_frames - int(cond.shape[2])
if pad_frames <= 0:
return cond
pad = torch.zeros(
cond.shape[0],
cond.shape[1],
pad_frames,
cond.shape[3],
cond.shape[4],
device=cond.device,
dtype=cond.dtype,
)
return torch.cat([cond, pad], dim=2)
return torch.zeros(
image_latents.shape[0],
image_latents.shape[1],
num_frames,
image_latents.shape[3],
image_latents.shape[4],
device=image_latents.device,
dtype=image_latents.dtype,
)
def _slice_action_prefix(
self,
action: torch.Tensor | None,
*,
frame_end: int,
) -> torch.Tensor | None:
if action is None:
return None
action_frame_end = ((frame_end - 1) * self._temporal_compression_ratio()) + 1
if action.shape[1] < action_frame_end:
raise ValueError("Action tensor is shorter than required for causal rollout: "
f"got={action.shape[1]}, required>={action_frame_end}")
return action[:, :action_frame_end]
def _initialize_action_kv_cache(
self,
*,
transformer: torch.nn.Module,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
frame_seq_length: int,
local_attn_size: int,
channel: Literal["mouse", "keyboard"],
) -> list[dict[str, Any] | None]:
blocks = list(getattr(transformer, "blocks", []))
num_blocks = len(blocks)
if num_blocks <= 0:
return []
action_config = getattr(transformer, "action_config", {}) or {}
action_blocks = {int(block_idx) for block_idx in action_config.get("blocks", [])}
if local_attn_size <= 0:
raise ValueError("Matrix-Game 2.0 causal streaming requires "
"transformer.local_attn_size > 0")
action_heads_num = int(action_config.get("heads_num", 0) or 0)
if action_blocks and action_heads_num <= 0:
raise ValueError("Matrix-Game 2.0 causal action_config.heads_num must be > 0")
if channel == "mouse":
enabled = bool(action_config.get("enable_mouse", False))
hidden_dim = int(action_config.get("mouse_hidden_dim", 0) or 0)
batch_dim = batch_size * frame_seq_length
else:
enabled = bool(action_config.get("enable_keyboard", False))
hidden_dim = int(action_config.get("keyboard_hidden_dim", 0) or 0)
batch_dim = batch_size
caches: list[dict[str, Any] | None] = []
for block_idx in range(num_blocks):
if block_idx not in action_blocks or not enabled:
caches.append(None)
continue
if hidden_dim <= 0 or hidden_dim % action_heads_num != 0:
raise ValueError(f"Invalid {channel} action hidden size for causal "
f"cache initialization: hidden_dim={hidden_dim}, "
f"heads={action_heads_num}")
head_dim = hidden_dim // action_heads_num
caches.append({
"k":
torch.zeros(
[
batch_dim,
local_attn_size,
action_heads_num,
head_dim,
],
dtype=dtype,
device=device,
),
"v":
torch.zeros(
[
batch_dim,
local_attn_size,
action_heads_num,
head_dim,
],
dtype=dtype,
device=device,
),
"global_end_index":
torch.tensor(
[0],
dtype=torch.long,
device=device,
),
"local_end_index":
torch.tensor(
[0],
dtype=torch.long,
device=device,
),
})
return caches
def _snapshot_action_kv_cache_indices(
self,
caches: list[dict[str, Any] | None],
) -> list[dict[str, Any] | None]:
snapshot: list[dict[str, Any] | None] = []
for cache in caches:
if cache is None:
snapshot.append(None)
continue
global_end_index = cache.get("global_end_index")
local_end_index = cache.get("local_end_index")
if (not isinstance(global_end_index, torch.Tensor) or not isinstance(local_end_index, torch.Tensor)):
raise ValueError("Unexpected action kv_cache index tensors; expected "
"tensors at kv_cache_*[*].{global_end_index, "
"local_end_index}")
copied = dict(cache)
copied["global_end_index"] = global_end_index.detach().clone()
copied["local_end_index"] = local_end_index.detach().clone()
snapshot.append(copied)
return snapshot