[Perf] [MiniMax H3] Precompute AdaLN modulation schedules
Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
1 parent
c9f2e5ad94
commit
6ba5bdfb15
8 files changed
+976
-38
No files matched your search
@@ -0,0 +1,98 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Exact schedule identity and policy for MiniMax-H3 AdaLN reuse."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
MINIMAX_H3_ADALN_SCHEDULE_CACHE_ENV = "VLLM_OMNI_H3_ADALN_SCHEDULE_CACHE"
|
||||
MINIMAX_H3_ADALN_OFFLOAD_WEIGHTS_ENV = "VLLM_OMNI_H3_ADALN_OFFLOAD_WEIGHTS"
|
||||
_TRUTHY_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
|
||||
|
||||
MiniMaxH3AdalnScheduleKey = tuple[tuple[int, ...], ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MiniMaxH3AdalnScheduleCache:
|
||||
"""One model-local exact modulation schedule."""
|
||||
|
||||
key: MiniMaxH3AdalnScheduleKey
|
||||
t_embs: tuple[torch.Tensor, ...]
|
||||
step_cursor: torch.Tensor
|
||||
steps: int
|
||||
max_unique_timesteps: int
|
||||
table_bytes: int
|
||||
projection_bytes: int = 0
|
||||
offloaded_projection_bytes: int = 0
|
||||
net_memory_saved_bytes: int = 0
|
||||
|
||||
|
||||
def minimax_h3_adaln_schedule_cache_enabled() -> bool:
|
||||
"""Return whether persistent exact-schedule AdaLN reuse is enabled."""
|
||||
return os.getenv(MINIMAX_H3_ADALN_SCHEDULE_CACHE_ENV, "").strip().lower() in _TRUTHY_ENV_VALUES
|
||||
|
||||
|
||||
def minimax_h3_adaln_weight_offload_enabled() -> bool:
|
||||
"""Return whether exact tables should take ownership of AdaLN weights."""
|
||||
return minimax_h3_adaln_schedule_cache_enabled() and (
|
||||
os.getenv(MINIMAX_H3_ADALN_OFFLOAD_WEIGHTS_ENV, "").strip().lower() in _TRUTHY_ENV_VALUES
|
||||
)
|
||||
|
||||
|
||||
def minimax_h3_float32_bits(values: Any) -> tuple[int, ...]:
|
||||
"""Return the signed int32 bit patterns of a CPU float32 tensor."""
|
||||
if not isinstance(values, torch.Tensor) or values.device.type != "cpu" or values.dtype != torch.float32:
|
||||
raise ValueError("schedule key values must be a CPU float32 tensor")
|
||||
return tuple(int(value) for value in values.contiguous().view(torch.int32).tolist())
|
||||
|
||||
|
||||
def minimax_h3_adaln_schedule_key(
|
||||
metadata: Iterable[Any],
|
||||
) -> MiniMaxH3AdalnScheduleKey:
|
||||
"""Build an immutable schedule key without reading a device tensor."""
|
||||
key: list[tuple[int, ...]] = []
|
||||
for step in metadata:
|
||||
bits = getattr(step, "timestep_bits", None)
|
||||
if bits is None:
|
||||
raise ValueError("timestep metadata is missing host timestep_bits")
|
||||
key.append(tuple(int(value) for value in bits))
|
||||
if not key:
|
||||
raise ValueError("AdaLN schedule must contain at least one denoise step")
|
||||
return tuple(key)
|
||||
|
||||
|
||||
def minimax_h3_build_adaln_table(
|
||||
projection: Any,
|
||||
t_embs: tuple[torch.Tensor, ...],
|
||||
) -> torch.Tensor:
|
||||
"""Project each step at its original shape and pad only unused rows."""
|
||||
rows_by_step = tuple(projection.compute_flat(t_emb) for t_emb in t_embs)
|
||||
max_rows = max(int(rows.shape[0]) for rows in rows_by_step)
|
||||
padded: list[torch.Tensor] = []
|
||||
for rows in rows_by_step:
|
||||
missing = max_rows - int(rows.shape[0])
|
||||
if missing:
|
||||
rows = torch.cat(
|
||||
[rows, rows.new_zeros((missing, rows.shape[1]))],
|
||||
dim=0,
|
||||
)
|
||||
padded.append(rows)
|
||||
return torch.stack(padded, dim=0)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MINIMAX_H3_ADALN_OFFLOAD_WEIGHTS_ENV",
|
||||
"MINIMAX_H3_ADALN_SCHEDULE_CACHE_ENV",
|
||||
"MiniMaxH3AdalnScheduleCache",
|
||||
"MiniMaxH3AdalnScheduleKey",
|
||||
"minimax_h3_build_adaln_table",
|
||||
"minimax_h3_adaln_schedule_cache_enabled",
|
||||
"minimax_h3_adaln_weight_offload_enabled",
|
||||
"minimax_h3_adaln_schedule_key",
|
||||
"minimax_h3_float32_bits",
|
||||
]
|
||||
@@ -10,10 +10,17 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from .adaln_schedule_cache import (
|
||||
minimax_h3_adaln_schedule_cache_enabled,
|
||||
minimax_h3_adaln_schedule_key,
|
||||
minimax_h3_adaln_weight_offload_enabled,
|
||||
minimax_h3_float32_bits,
|
||||
)
|
||||
from .scheduling_minimax_h3_euler_ancestral import (
|
||||
minimax_h3_euler_eta0_step,
|
||||
minimax_h3_rf_v_to_x0,
|
||||
@@ -28,6 +35,15 @@ MINIMAX_H3_VIDEO_ROW_WIDTH = 96
|
||||
MINIMAX_H3_AUDIO_ROW_WIDTH = 32
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MiniMaxH3StepTimestepMetadata:
|
||||
"""Device-ready timestep routing for one denoise forward."""
|
||||
|
||||
unique_timesteps: torch.Tensor
|
||||
inverse_indices: torch.Tensor
|
||||
timestep_bits: tuple[int, ...]
|
||||
|
||||
|
||||
class MiniMaxH3DenoiseBranch:
|
||||
"""Static per-branch state: packed layout + fixed forward kwargs.
|
||||
|
||||
@@ -45,6 +61,7 @@ class MiniMaxH3DenoiseBranch:
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
seq_len = int(packed["seq_len"])
|
||||
self.device = device
|
||||
self.seq_len = seq_len
|
||||
self.img_pos = packed["img_pos"].view(-1).to(torch.long)
|
||||
self.audio_pos = packed["audio_pos"].view(-1).to(torch.long)
|
||||
@@ -65,6 +82,12 @@ class MiniMaxH3DenoiseBranch:
|
||||
self.audio_pos_dev = self.audio_pos.to(device)
|
||||
self.update_mask_dev = self.update_mask.to(device)
|
||||
self.audio_update_mask_dev = self.audio_update_mask.to(device)
|
||||
timestep_category_ids = torch.zeros(seq_len, dtype=torch.long)
|
||||
timestep_category_ids[self.img_pos[~self.update_mask]] = 1
|
||||
timestep_category_ids[self.audio_pos[self.audio_update_mask]] = 2
|
||||
timestep_category_ids[self.audio_pos[~self.audio_update_mask]] = 3
|
||||
self.timestep_category_ids = timestep_category_ids
|
||||
self.used_timestep_categories = torch.unique(timestep_category_ids, sorted=True)
|
||||
self.x_base = torch.zeros(1, seq_len, MINIMAX_H3_VIDEO_ROW_WIDTH, dtype=torch.float32, device=device)
|
||||
self.audio_x_base = torch.zeros(1, seq_len, MINIMAX_H3_AUDIO_ROW_WIDTH, dtype=torch.float32, device=device)
|
||||
text_pos_dev = packed["text_pos"].view(-1).to(torch.long).to(device)
|
||||
@@ -87,43 +110,103 @@ class MiniMaxH3DenoiseBranch:
|
||||
"max_seqlen_q": text_len,
|
||||
},
|
||||
}
|
||||
self._adaln_schedule_cache_active = False
|
||||
|
||||
def prepare_timestep_metadata(
|
||||
self,
|
||||
*,
|
||||
sigmas_video: list[float],
|
||||
sigmas_audio: list[float],
|
||||
imgvid_cond_noise_aug_for_inference: float,
|
||||
audio_cond_noise_aug_for_inference: float,
|
||||
) -> list[MiniMaxH3StepTimestepMetadata]:
|
||||
"""Precompute the full schedule's packed-row timestep routing on CPU."""
|
||||
if len(sigmas_video) != len(sigmas_audio):
|
||||
raise ValueError("video/audio sigma schedules must have equal length")
|
||||
if len(sigmas_video) < 2:
|
||||
raise ValueError("sigma schedules need at least 2 entries")
|
||||
|
||||
result: list[MiniMaxH3StepTimestepMetadata] = []
|
||||
for step in range(len(sigmas_video) - 1):
|
||||
t_video = 1.0 - float(sigmas_video[step])
|
||||
t_audio = 1.0 - float(sigmas_audio[step])
|
||||
category_timesteps = torch.tensor(
|
||||
[
|
||||
t_video,
|
||||
max(t_video, float(imgvid_cond_noise_aug_for_inference)),
|
||||
t_audio,
|
||||
max(t_audio, float(audio_cond_noise_aug_for_inference)),
|
||||
],
|
||||
dtype=torch.float32,
|
||||
)
|
||||
active_timesteps = category_timesteps.index_select(0, self.used_timestep_categories)
|
||||
unique_timesteps, active_inverse = torch.unique(
|
||||
active_timesteps,
|
||||
sorted=True,
|
||||
return_inverse=True,
|
||||
)
|
||||
category_inverse = torch.zeros(4, dtype=torch.long)
|
||||
category_inverse.index_copy_(0, self.used_timestep_categories, active_inverse)
|
||||
inverse_indices = category_inverse.index_select(0, self.timestep_category_ids)
|
||||
result.append(
|
||||
MiniMaxH3StepTimestepMetadata(
|
||||
unique_timesteps=unique_timesteps.to(self.device),
|
||||
inverse_indices=inverse_indices.to(self.device),
|
||||
timestep_bits=minimax_h3_float32_bits(unique_timesteps),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def prepare_adaln_schedule_cache(
|
||||
self,
|
||||
model: Any,
|
||||
timestep_metadata: list[MiniMaxH3StepTimestepMetadata],
|
||||
*,
|
||||
enabled: bool,
|
||||
) -> Any | None:
|
||||
"""Prepare exact model-local modulation tables for this schedule."""
|
||||
self._adaln_schedule_cache_active = False
|
||||
if not enabled:
|
||||
clear = getattr(model, "clear_adaln_schedule_cache", None)
|
||||
if clear is not None:
|
||||
clear()
|
||||
return None
|
||||
prepare = getattr(model, "prepare_adaln_schedule_cache", None)
|
||||
if prepare is None:
|
||||
return None
|
||||
with torch.inference_mode():
|
||||
cache = prepare(
|
||||
unique_timestep_plan=tuple(step.unique_timesteps for step in timestep_metadata),
|
||||
schedule_key=minimax_h3_adaln_schedule_key(timestep_metadata),
|
||||
offload_weights=minimax_h3_adaln_weight_offload_enabled(),
|
||||
)
|
||||
self._adaln_schedule_cache_active = cache is not None
|
||||
return cache
|
||||
|
||||
def forward_kwargs(
|
||||
self,
|
||||
*,
|
||||
video_rows: torch.Tensor,
|
||||
audio_rows: torch.Tensor,
|
||||
t_video: float,
|
||||
t_audio: float,
|
||||
imgvid_cond_timestep: float,
|
||||
audio_ref_cond_timestep: float,
|
||||
timestep_metadata: MiniMaxH3StepTimestepMetadata,
|
||||
step_index: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
x = self.x_base.clone()
|
||||
x[0].index_copy_(0, self.img_pos_dev, video_rows)
|
||||
audio_x = self.audio_x_base.clone()
|
||||
audio_x[0].index_copy_(0, self.audio_pos_dev, audio_rows)
|
||||
# Packed-sequence timestep semantics: non-media rows (text and
|
||||
# padding) inherit the current video timestep. Later steps must reuse
|
||||
# the previous step's updated rows; re-initializing from zeros is only
|
||||
# valid at step 0.
|
||||
timesteps = torch.full(
|
||||
(self.seq_len,),
|
||||
float(t_video),
|
||||
dtype=torch.float32,
|
||||
device=x.device,
|
||||
)
|
||||
timesteps[self.img_pos_dev[self.update_mask_dev]] = t_video
|
||||
timesteps[self.img_pos_dev[~self.update_mask_dev]] = imgvid_cond_timestep
|
||||
timesteps[self.audio_pos_dev[self.audio_update_mask_dev]] = t_audio
|
||||
timesteps[self.audio_pos_dev[~self.audio_update_mask_dev]] = audio_ref_cond_timestep
|
||||
unique_timesteps, inverse_indices = torch.unique(timesteps, sorted=True, return_inverse=True)
|
||||
return {
|
||||
dynamic_kwargs = {
|
||||
**self.static_kwargs,
|
||||
"x": x,
|
||||
"audio_x": audio_x,
|
||||
"unique_timesteps": unique_timesteps,
|
||||
"inverse_indices": inverse_indices,
|
||||
"unique_timesteps": timestep_metadata.unique_timesteps,
|
||||
"inverse_indices": timestep_metadata.inverse_indices,
|
||||
}
|
||||
if self._adaln_schedule_cache_active:
|
||||
if step_index is None:
|
||||
raise ValueError("step_index is required while the AdaLN schedule cache is active")
|
||||
dynamic_kwargs["adaln_step_index"] = step_index
|
||||
return dynamic_kwargs
|
||||
|
||||
|
||||
def minimax_h3_denoise_loop(
|
||||
@@ -153,6 +236,12 @@ def minimax_h3_denoise_loop(
|
||||
raise ValueError("video/audio sigma schedules must have equal length")
|
||||
if len(sigmas_video) < 2:
|
||||
raise ValueError("sigma schedules need at least 2 entries")
|
||||
timestep_metadata = positive.prepare_timestep_metadata(
|
||||
sigmas_video=sigmas_video,
|
||||
sigmas_audio=sigmas_audio,
|
||||
imgvid_cond_noise_aug_for_inference=imgvid_cond_noise_aug_for_inference,
|
||||
audio_cond_noise_aug_for_inference=audio_cond_noise_aug_for_inference,
|
||||
)
|
||||
n_cond = int((~positive.update_mask).sum())
|
||||
if keyframe_cond_rows is None:
|
||||
if n_cond != 0:
|
||||
@@ -187,6 +276,16 @@ def minimax_h3_denoise_loop(
|
||||
if audio_anchor is not None:
|
||||
audio_rows[~audio_update] = audio_anchor
|
||||
|
||||
prepare_for_request_caches = getattr(model, "prepare_for_request_caches", None)
|
||||
if prepare_for_request_caches is not None:
|
||||
with torch.inference_mode():
|
||||
prepare_for_request_caches()
|
||||
positive.prepare_adaln_schedule_cache(
|
||||
model,
|
||||
timestep_metadata,
|
||||
enabled=minimax_h3_adaln_schedule_cache_enabled(),
|
||||
)
|
||||
|
||||
num_steps = len(sigmas_video) - 1
|
||||
for step in range(num_steps):
|
||||
step_cm = step_profiler(step) if step_profiler is not None else nullcontext()
|
||||
@@ -194,16 +293,12 @@ def minimax_h3_denoise_loop(
|
||||
s_v, s_v_next = sigmas_video[step], sigmas_video[step + 1]
|
||||
s_a, s_a_next = sigmas_audio[step], sigmas_audio[step + 1]
|
||||
t_v, t_a = 1.0 - s_v, 1.0 - s_a
|
||||
imgvid_cond_t = max(t_v, float(imgvid_cond_noise_aug_for_inference))
|
||||
audio_ref_cond_t = max(t_a, float(audio_cond_noise_aug_for_inference))
|
||||
|
||||
fk = positive.forward_kwargs(
|
||||
video_rows=video_rows,
|
||||
audio_rows=audio_rows,
|
||||
t_video=t_v,
|
||||
t_audio=t_a,
|
||||
imgvid_cond_timestep=imgvid_cond_t,
|
||||
audio_ref_cond_timestep=audio_ref_cond_t,
|
||||
timestep_metadata=timestep_metadata[step],
|
||||
step_index=step,
|
||||
)
|
||||
with torch.inference_mode():
|
||||
v_video, v_audio = model(**fk)
|
||||
@@ -245,5 +340,6 @@ __all__ = [
|
||||
"MINIMAX_H3_IMGVID_COND_TIMESTEP",
|
||||
"MINIMAX_H3_VIDEO_ROW_WIDTH",
|
||||
"MiniMaxH3DenoiseBranch",
|
||||
"MiniMaxH3StepTimestepMetadata",
|
||||
"minimax_h3_denoise_loop",
|
||||
]
|
||||
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from cache_dit import ForwardPattern
|
||||
from torch.distributed.tensor import DTensor
|
||||
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.linear import (
|
||||
@@ -33,6 +34,14 @@ from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput,
|
||||
SequenceParallelOutput,
|
||||
)
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
from .adaln_schedule_cache import (
|
||||
MiniMaxH3AdalnScheduleCache,
|
||||
MiniMaxH3AdalnScheduleKey,
|
||||
minimax_h3_adaln_schedule_cache_enabled,
|
||||
minimax_h3_build_adaln_table,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.model_executor.layers.quantization.base_config import (
|
||||
@@ -132,6 +141,7 @@ _FORWARD_SUPPORTED_KWARGS = frozenset(
|
||||
"img_pos_for_infer_output_info",
|
||||
"packed_seq_params",
|
||||
"refiner_packed_seq_params",
|
||||
"adaln_step_index",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -528,14 +538,118 @@ class MiniMaxH3AdalnProj(nn.Module):
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.linear",
|
||||
)
|
||||
self.register_buffer("_precomputed_table", None, persistent=False)
|
||||
self._precomputed_step: torch.Tensor | None = None
|
||||
self._offloaded_projection_state: dict[str, tuple[torch.Tensor, torch.device, torch.Size]] | None = None
|
||||
|
||||
@property
|
||||
def projection_weights_offloaded(self) -> bool:
|
||||
return self._offloaded_projection_state is not None
|
||||
|
||||
@property
|
||||
def projection_nbytes(self) -> int:
|
||||
state = self._offloaded_projection_state
|
||||
if state is not None:
|
||||
return sum(tensor.numel() * tensor.element_size() for tensor, _, _ in state.values())
|
||||
total = 0
|
||||
for parameter in self.linear.parameters():
|
||||
local = parameter.to_local() if isinstance(parameter, DTensor) else parameter
|
||||
total += local.numel() * local.element_size()
|
||||
return total
|
||||
|
||||
def can_offload_projection(self) -> bool:
|
||||
"""Whether this projection can safely own ordinary parameter storage."""
|
||||
if self.projection_weights_offloaded:
|
||||
return True
|
||||
parameters = tuple(self.linear.parameters())
|
||||
return bool(parameters) and all(
|
||||
not isinstance(parameter, DTensor) and not parameter.is_meta and parameter.numel() > 0
|
||||
for parameter in parameters
|
||||
)
|
||||
|
||||
def offload_projection(self) -> int:
|
||||
"""Move exact projection parameters to CPU and release device storage."""
|
||||
if self.projection_weights_offloaded:
|
||||
return 0
|
||||
if self._precomputed_table is None:
|
||||
raise RuntimeError("AdaLN projection weights require an installed precomputed table before offload")
|
||||
if not self.can_offload_projection():
|
||||
raise RuntimeError("AdaLN projection weight offload does not support DTensor, meta, or empty parameters")
|
||||
|
||||
state: dict[str, tuple[torch.Tensor, torch.device, torch.Size]] = {}
|
||||
try:
|
||||
for name, parameter in self.linear.named_parameters():
|
||||
cpu_tensor = parameter.detach().to(device="cpu", copy=True)
|
||||
state[name] = (cpu_tensor, parameter.device, parameter.shape)
|
||||
parameter.data = torch.empty((0,), device=parameter.device, dtype=parameter.dtype)
|
||||
except Exception:
|
||||
for name, (cpu_tensor, device, shape) in state.items():
|
||||
self.linear.get_parameter(name).data = cpu_tensor.to(device=device).view(shape)
|
||||
raise
|
||||
self._offloaded_projection_state = state
|
||||
return sum(tensor.numel() * tensor.element_size() for tensor, _, _ in state.values())
|
||||
|
||||
def restore_projection(self) -> None:
|
||||
"""Restore a previously offloaded projection to its original devices."""
|
||||
state = self._offloaded_projection_state
|
||||
if state is None:
|
||||
return
|
||||
restored = {
|
||||
name: cpu_tensor.to(device=device).view(shape) for name, (cpu_tensor, device, shape) in state.items()
|
||||
}
|
||||
for name, tensor in restored.items():
|
||||
self.linear.get_parameter(name).data = tensor
|
||||
self._offloaded_projection_state = None
|
||||
|
||||
def compute_flat(self, t_emb: torch.Tensor) -> torch.Tensor:
|
||||
"""Run the original projection and return `[M*modalities, ratio*H]`."""
|
||||
x = nn.functional.silu(t_emb)
|
||||
x, _ = self.linear(x.to(self.linear.weight.dtype))
|
||||
m = x.shape[0]
|
||||
return x.view(m * self.modality_num, self.expand_ratio * self.hidden_size)
|
||||
|
||||
def install_precomputed(
|
||||
self,
|
||||
table: torch.Tensor,
|
||||
step_cursor: torch.Tensor,
|
||||
) -> None:
|
||||
"""Atomically select a non-persistent exact-schedule modulation table."""
|
||||
expected_width = self.expand_ratio * self.hidden_size
|
||||
if table.dim() != 3 or table.shape[-1] != expected_width:
|
||||
raise ValueError(
|
||||
f"precomputed AdaLN table must be [steps, rows, {expected_width}], got {list(table.shape)}"
|
||||
)
|
||||
if table.dtype != self.linear.weight.dtype:
|
||||
raise ValueError(f"precomputed AdaLN table dtype mismatch: {table.dtype} != {self.linear.weight.dtype}")
|
||||
if step_cursor.dtype != torch.long or step_cursor.dim() != 0:
|
||||
raise ValueError("precomputed AdaLN step cursor must be a scalar torch.long tensor")
|
||||
if table.device != step_cursor.device:
|
||||
raise ValueError(f"precomputed AdaLN table/cursor device mismatch: {table.device} != {step_cursor.device}")
|
||||
self._buffers["_precomputed_table"] = table
|
||||
self._precomputed_step = step_cursor
|
||||
|
||||
def clear_precomputed(self) -> None:
|
||||
"""Restore direct projection without changing checkpoint parameters."""
|
||||
self.restore_projection()
|
||||
self._buffers["_precomputed_table"] = None
|
||||
self._precomputed_step = None
|
||||
|
||||
@torch.compiler.disable
|
||||
def forward(self, t_emb: torch.Tensor) -> tuple[torch.Tensor, ...]:
|
||||
"""t_emb: [M, t_dim] -> expand_ratio tensors of [M*modality_num, H]."""
|
||||
x = nn.functional.silu(t_emb)
|
||||
x, _ = self.linear(x.to(_BF16_DTYPE))
|
||||
m = x.shape[0]
|
||||
x = x.view(m * self.modality_num, self.expand_ratio * self.hidden_size)
|
||||
return tuple(x.chunk(self.expand_ratio, dim=-1))
|
||||
table = self._precomputed_table
|
||||
if table is None:
|
||||
rows = self.compute_flat(t_emb)
|
||||
return tuple(rows.chunk(self.expand_ratio, dim=-1))
|
||||
|
||||
step_cursor = self._precomputed_step
|
||||
if step_cursor is None:
|
||||
raise RuntimeError("precomputed AdaLN table is installed without a step cursor")
|
||||
row_count = t_emb.shape[0] * self.modality_num
|
||||
if row_count > table.shape[1]:
|
||||
raise ValueError(f"precomputed AdaLN table row capacity is too small: {table.shape[1]} < {row_count}")
|
||||
rows = table.index_select(0, step_cursor.reshape(1))[0].narrow(0, 0, row_count)
|
||||
return tuple(rows.chunk(self.expand_ratio, dim=-1))
|
||||
|
||||
|
||||
class MiniMaxH3TokenRefinerBlock(nn.Module):
|
||||
@@ -781,6 +895,13 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
_repeated_blocks = ["MiniMaxH3DiTBlock"]
|
||||
_layerwise_offload_blocks_attrs = ["blocks"]
|
||||
|
||||
@staticmethod
|
||||
def layerwise_offload_excluded_parameter_prefixes() -> tuple[str, ...]:
|
||||
"""Parameters owned by the exact-schedule cache instead of block hooks."""
|
||||
if minimax_h3_adaln_schedule_cache_enabled():
|
||||
return ("adaln_proj.linear.",)
|
||||
return ()
|
||||
|
||||
@staticmethod
|
||||
def _is_transformer_block(name: str, module: nn.Module) -> bool:
|
||||
del module
|
||||
@@ -921,6 +1042,8 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
quant_config,
|
||||
prefix="final_layer",
|
||||
)
|
||||
self._adaln_projections = tuple(block.adaln_proj for block in self.blocks) + (self.final_layer.adaln_proj,)
|
||||
self._adaln_schedule_cache: MiniMaxH3AdalnScheduleCache | None = None
|
||||
self._mark_missing_params_required()
|
||||
|
||||
def _mark_missing_params_required(self) -> None:
|
||||
@@ -935,6 +1058,126 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
if name in MINIMAX_H3_FP32_BUFFER_NAMES and buffer.dtype != _FP32_DTYPE:
|
||||
raise ValueError(f"{name} must stay fp32 after load, got {buffer.dtype}.")
|
||||
|
||||
@torch.inference_mode()
|
||||
def prepare_for_request_caches(self) -> None:
|
||||
"""Materialize a sequentially offloaded DiT before auxiliary cache work."""
|
||||
registry = getattr(self, "_hook_registry", None)
|
||||
if registry is None:
|
||||
return
|
||||
hook = registry.get_hook("sequential_offload")
|
||||
if hook is not None:
|
||||
hook.pre_forward(self)
|
||||
|
||||
@torch.inference_mode()
|
||||
def prepare_adaln_schedule_cache(
|
||||
self,
|
||||
*,
|
||||
unique_timestep_plan: tuple[torch.Tensor, ...],
|
||||
schedule_key: MiniMaxH3AdalnScheduleKey,
|
||||
offload_weights: bool = False,
|
||||
) -> MiniMaxH3AdalnScheduleCache | None:
|
||||
"""Build or reuse the single exact AdaLN schedule held by this model."""
|
||||
if not unique_timestep_plan:
|
||||
raise ValueError("AdaLN schedule must contain at least one denoise step")
|
||||
normalized_key = tuple(tuple(int(value) for value in step) for step in schedule_key)
|
||||
if len(normalized_key) != len(unique_timestep_plan):
|
||||
raise ValueError(
|
||||
f"AdaLN schedule key/plan length mismatch: {len(normalized_key)} != {len(unique_timestep_plan)}"
|
||||
)
|
||||
existing = self._adaln_schedule_cache
|
||||
if existing is not None and existing.key == normalized_key:
|
||||
return existing
|
||||
|
||||
for projection in self._adaln_projections:
|
||||
projection.restore_projection()
|
||||
|
||||
t_embs = tuple(self.time_embedder(timestep.view(-1)) for timestep in unique_timestep_plan)
|
||||
device = t_embs[0].device
|
||||
if any(t_emb.device != device for t_emb in t_embs):
|
||||
raise ValueError("AdaLN timestep embeddings must share one device")
|
||||
step_cursor = torch.zeros((), dtype=torch.long, device=device)
|
||||
tables = tuple(minimax_h3_build_adaln_table(projection, t_embs) for projection in self._adaln_projections)
|
||||
table_bytes = sum(table.numel() * table.element_size() for table in tables)
|
||||
projection_bytes = sum(projection.projection_nbytes for projection in self._adaln_projections)
|
||||
offload_supported = offload_weights and all(
|
||||
projection.can_offload_projection() for projection in self._adaln_projections
|
||||
)
|
||||
if offload_supported and table_bytes >= projection_bytes:
|
||||
for projection in self._adaln_projections:
|
||||
projection.clear_precomputed()
|
||||
self._adaln_schedule_cache = None
|
||||
logger.warning(
|
||||
"MiniMax H3 AdaLN weight offload skipped because table bytes (%d) "
|
||||
"would not save memory versus projection bytes (%d)",
|
||||
table_bytes,
|
||||
projection_bytes,
|
||||
)
|
||||
return None
|
||||
|
||||
# Build every table before changing the live projection path.
|
||||
for projection, table in zip(self._adaln_projections, tables):
|
||||
projection.install_precomputed(table, step_cursor)
|
||||
|
||||
offloaded_projection_bytes = 0
|
||||
if offload_supported:
|
||||
try:
|
||||
offloaded_projection_bytes = sum(
|
||||
projection.offload_projection() for projection in self._adaln_projections
|
||||
)
|
||||
except Exception:
|
||||
for projection in self._adaln_projections:
|
||||
projection.restore_projection()
|
||||
logger.exception("MiniMax H3 AdaLN projection offload failed; retaining exact tables and weights")
|
||||
elif offload_weights:
|
||||
logger.warning("MiniMax H3 AdaLN projection offload is unsupported for the active parameter layout")
|
||||
|
||||
if offloaded_projection_bytes:
|
||||
current_omni_platform.empty_cache()
|
||||
|
||||
cache = MiniMaxH3AdalnScheduleCache(
|
||||
key=normalized_key,
|
||||
t_embs=t_embs,
|
||||
step_cursor=step_cursor,
|
||||
steps=len(t_embs),
|
||||
max_unique_timesteps=max(int(t_emb.shape[0]) for t_emb in t_embs),
|
||||
table_bytes=table_bytes,
|
||||
projection_bytes=projection_bytes,
|
||||
offloaded_projection_bytes=offloaded_projection_bytes,
|
||||
net_memory_saved_bytes=offloaded_projection_bytes - table_bytes,
|
||||
)
|
||||
self._adaln_schedule_cache = cache
|
||||
if offloaded_projection_bytes:
|
||||
gib = 1024**3
|
||||
logger.info(
|
||||
"MiniMax H3 AdaLN projection weights offloaded: %.3f GiB weights, "
|
||||
"%.3f GiB tables, %.3f GiB net device-memory reduction",
|
||||
offloaded_projection_bytes / gib,
|
||||
table_bytes / gib,
|
||||
cache.net_memory_saved_bytes / gib,
|
||||
)
|
||||
return cache
|
||||
|
||||
def clear_adaln_schedule_cache(self) -> None:
|
||||
"""Drop the model-local table and restore direct AdaLN projections."""
|
||||
if self._adaln_schedule_cache is None:
|
||||
return
|
||||
for projection in self._adaln_projections:
|
||||
projection.clear_precomputed()
|
||||
self._adaln_schedule_cache = None
|
||||
|
||||
def _select_adaln_schedule_step(self, step_index: Any) -> torch.Tensor | None:
|
||||
cache = self._adaln_schedule_cache
|
||||
if cache is None:
|
||||
if step_index is not None:
|
||||
raise ValueError("adaln_step_index was supplied without a prepared AdaLN schedule")
|
||||
return None
|
||||
if isinstance(step_index, bool) or not isinstance(step_index, int):
|
||||
raise ValueError("adaln_step_index must be an integer while the AdaLN schedule is active")
|
||||
if step_index < 0 or step_index >= cache.steps:
|
||||
raise ValueError(f"adaln_step_index {step_index} is outside [0, {cache.steps})")
|
||||
cache.step_cursor.fill_(step_index)
|
||||
return cache.t_embs[step_index]
|
||||
|
||||
def load_weights(
|
||||
self,
|
||||
weights: Iterable[tuple[str, torch.Tensor]],
|
||||
@@ -1007,6 +1250,7 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
refiner_max_seqlen: int,
|
||||
seq_len: int,
|
||||
device: torch.device,
|
||||
precomputed_t_emb: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Build packed multimodal embeddings for the TP=1/SP=1 inference path.
|
||||
|
||||
@@ -1032,7 +1276,7 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
embeddings.index_add_(0, img_pos, video_embed.to(_BF16_DTYPE)[: img_pos.shape[0]])
|
||||
embeddings.index_add_(0, audio_pos, audio_embed.to(_BF16_DTYPE)[: audio_pos.shape[0]])
|
||||
|
||||
t_emb = self.time_embedder(unique_timesteps)
|
||||
t_emb = self.time_embedder(unique_timesteps) if precomputed_t_emb is None else precomputed_t_emb
|
||||
return embeddings, t_emb
|
||||
|
||||
def forward(self, **kwargs: Any) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
@@ -1052,6 +1296,8 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
f"{sorted(_FORWARD_SUPPORTED_KWARGS)}"
|
||||
)
|
||||
|
||||
precomputed_t_emb = self._select_adaln_schedule_step(kwargs.get("adaln_step_index"))
|
||||
|
||||
x = _required_kwarg(kwargs, "x")
|
||||
audio_x = _required_kwarg(kwargs, "audio_x")
|
||||
img_position_ids = _required_kwarg(kwargs, "img_position_ids")
|
||||
@@ -1104,6 +1350,7 @@ class MiniMaxH3DiTModel(nn.Module):
|
||||
refiner_max_seqlen=refiner_max,
|
||||
seq_len=seq_len,
|
||||
device=device,
|
||||
precomputed_t_emb=precomputed_t_emb,
|
||||
)
|
||||
|
||||
combined_indices = (inverse_indices * MINIMAX_H3_ADALN_MODALITY_NUM + token_tags.clamp(min=0)).to(device)
|
||||
|
||||
Reference in new issue
Block a user