File size: 20,108 Bytes
c3da9f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | """Keep a long (up to 60 s), full-canvas MiniMax-H3 pass inside one card — without lowering the quality.
A 28 s 768x1344 request is ~200k rows in the packed sequence and 60 s at the same canvas is ~430k. Stock
`MiniMaxH3TransformerBlock.forward` materializes, for *every* row at once, the modulated norm, six per-row AdaLN
tensors (`index_select` over the table), the feed-forward's hidden layer (SwiGLU, several times the model width) and
the LoRA branch next to it. That is what ran the allocator dry (`NVML_SUCCESS == r INTERNAL ASSERT FAILED` in
`CUDACachingAllocator` is how an exhausted card surfaces under ZeroGPU), inside `ff`.
Everything in a block except the attention core is **per row**: RMSNorm, the AdaLN modulation, the q/k/v projections,
q/k norms, the rotary rotation, the output projection, the gates and the whole feed-forward. So they are run here in
chunks of rows written into preallocated buffers. Only the attention core itself sees the full sequence, and the
fused cuDNN kernel needs no `S x S` matrix. The result is the same computation row for row — same weights, same
LoRA, same dtypes, same order of operations — just not all rows at the same time.
The same goes for the two ends of the model and for the decode:
* `norm_out` is computed chunk by chunk straight into the float32 buffer the output heads read.
* The video decode streams: the VAE already decodes in temporal clips; each finished clip is denormalized,
converted to uint8 and moved to the CPU immediately, instead of the whole float32 video being built on the card
(18 GB for 60 s at 768x1344 before the clamp copies it again).
`patched(pipe)` installs all of it for the duration of one window and restores the stock methods afterwards; the
one-shot `/generate` path never sees it.
**The decode can be split too.** A ZeroGPU visitor who is not signed in has 2 minutes a day — about 60 s of real
`xlarge` time — so even the video decode may not fit next to the setup a window pays. The decode already runs clip by
clip; `DecodeControl` lets a window stop on a clip boundary and a later window pick up at the next clip. The loop's
whole state at a clip boundary is the frames emitted so far (uint8, on the CPU) and the cross-fade tail `overlap`, so
that is all that is handed over.
"""
from __future__ import annotations
import contextlib
import os
import types
CHUNK_ROWS = int(os.environ.get("H3_CHUNK_ROWS", "16384"))
def _chunks(total: int):
for start in range(0, total, CHUNK_ROWS):
yield start, min(total, start + CHUNK_ROWS)
class A2VBlockStop(BaseException):
"""Raised from inside a transformer forward, on a *block* boundary, when the window is out of time.
A `BaseException` on purpose: diffusers' `loop_step` logs every `Exception` passing through it as a block error
with a full traceback, and this is a planned stop, not an error.
`block` is the index of the first block NOT yet run; `hidden` is the packed sequence entering it (on the CPU),
or None when this window had not run a single block of the step yet (nothing worth keeping).
"""
def __init__(self, block: int, hidden):
super().__init__(f"window stopped before transformer block {block}")
self.block = int(block)
self.hidden = hidden
class A2VDecodeStop(BaseException):
"""Raised from inside the streaming video decode, on a *clip* boundary, when the window is out of time.
A `BaseException` for the same reason as `A2VBlockStop`: it is a planned stop, not an error.
`clip` is the first clip NOT yet decoded; `frames` the uint8 frames THIS window decoded and has not handed over,
`(F, H, W, 3)` on the CPU, or None when it decoded none; `overlap` the cross-fade tail the next clip blends with
(on the CPU), or None.
"""
def __init__(self, clip: int, frames, overlap):
super().__init__(f"window stopped before video decode clip {clip}")
self.clip = int(clip)
self.frames = frames
self.overlap = overlap
class DecodeControl:
"""Lets the VIDEO DECODE be split across ZeroGPU windows, clip by clip.
* `resume_clip` / `resume_overlap` — start at that clip with that cross-fade tail instead of at clip 0.
* `should_stop(clip, pending_frames)` — asked before every clip that would actually run; `pending_frames` is how
many frames this window has decoded so far (they still have to be written out if it stops).
After the decode, `num_clips` is how many clips the whole video has and `pad_frames` how many trailing frames the
last clip's latent padding produced. A resumable decode never trims them itself — the caller joins the parts of
all windows and trims once, because the tail may live in a different window than the padding's origin.
"""
def __init__(self, resume_clip: int = 0, resume_overlap=None, should_stop=None):
self.resume_clip = int(resume_clip or 0)
self.resume_overlap = resume_overlap if self.resume_clip > 0 else None
self.should_stop = should_stop
self.num_clips = 0
self.pad_frames = 0
self.decoded = 0 # clips this window actually decoded
class BlockControl:
"""Lets ONE denoising step be split across ZeroGPU windows, block by block.
A 60 s full-canvas step costs more GPU time than a single ZeroGPU booking can hold, so a step can no longer be
the smallest unit of work. Nothing changes numerically: the packed sequence entering block `k` is exactly what the
uninterrupted forward would have handed it, so resuming there with it gives the same step, bit for bit.
* `resume_block` / `resume_hidden` — skip blocks `< resume_block` of the first forward and start from the saved
sequence instead. The embeddings in front of block 0 are recomputed (cheap, deterministic) and discarded.
* `should_stop(index)` — asked before every block that would actually run.
"""
def __init__(self, resume_block: int = 0, resume_hidden=None, should_stop=None, decode: "DecodeControl | None" = None):
self.resume_block = int(resume_block or 0)
self.resume_hidden = resume_hidden if self.resume_block > 0 else None
self.should_stop = should_stop
self.decode = decode # the same window's control over the video decode, if it may stop there too
self.first_block = self.resume_block # the first block this window will actually compute
self.computed = 0
CONTROL: BlockControl | None = None
def _control_gate(block, hidden_states):
"""`(skip, hidden_states)` for one block under `CONTROL`: skip a block an earlier window already ran (handing
over the saved sequence at block 0), or raise `A2VBlockStop` when the window is out of time."""
control = CONTROL
if control is None:
return False, hidden_states
index = int(getattr(block, "_a2v_index", -1))
if control.resume_block > 0 and 0 <= index < control.resume_block:
if index == 0:
saved = control.resume_hidden
if tuple(saved.shape) != tuple(hidden_states.shape):
raise ValueError(
f"The saved mid-step sequence {tuple(saved.shape)} does not match {tuple(hidden_states.shape)}."
)
hidden_states = saved.to(device=hidden_states.device, dtype=hidden_states.dtype)
if index == control.resume_block - 1:
control.resume_block, control.resume_hidden = 0, None
return True, hidden_states
if control.should_stop is not None and control.should_stop(index):
# Block 0's input is only the embeddings (recomputed for free), and stopping at the very block this window
# resumed at, before computing anything, has nothing new to keep either.
keep = index > 0 and not (index == control.first_block and control.computed == 0)
raise A2VBlockStop(index, hidden_states.detach().to("cpu") if keep else None)
control.computed += 1
return False, hidden_states
def _head_major_backend(backend) -> bool:
"""Whether this attention backend re-lays q/k/v out as `(batch, heads, rows, dim)` before the kernel.
diffusers' native backends (`_native_cudnn` here, SDPA in general) do `x.permute(0, 2, 1, 3).contiguous()` on
each of q, k and v. On a `(batch, rows, heads, dim)` buffer that is a full COPY — at 60 s full canvas three extra
~6.3 GB tensors, alive next to the originals, right at the memory peak. That copy is what ran the card out of
memory (`NVML_SUCCESS == r INTERNAL ASSERT FAILED` in `CUDACachingAllocator`, raised inside that `contiguous`).
"""
name = str(getattr(backend, "value", backend) or "").lower()
return name == "" or "native" in name
def _attention_buffer(like, batch: int, total: int, head_major: bool):
"""A full-sequence q/k/v buffer, returned as a `(batch, rows, heads, dim)` view.
With `head_major` the storage itself is `(batch, heads, rows, dim)`, so the backend's permute lands on a tensor
that is already contiguous and `.contiguous()` returns it without copying. Same values, same kernel, same
result — only the copy is gone.
"""
heads, dim = like.shape[2], like.shape[3]
if head_major:
return like.new_empty((batch, heads, total, dim)).transpose(1, 2)
return like.new_empty((batch, total, heads, dim))
def _block_forward(self, hidden_states, temb, adaln_indices, rotary_emb, attention_mask=None):
import torch
from diffusers.models.attention_dispatch import dispatch_attention_fn
from diffusers.models.transformers.transformer_minimax_h3 import _apply_rotary_emb
skip, hidden_states = _control_gate(self, hidden_states)
if skip:
return hidden_states
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaln_proj(temb)
attn = self.attn
batch, total, _ = hidden_states.shape
# --- attention inputs, row chunk by row chunk -------------------------------------------------------------
processor = attn.processor
head_major = _head_major_backend(getattr(processor, "_attention_backend", None))
query = key = value = None
for start, end in _chunks(total):
idx = adaln_indices[start:end]
norm = self.norm1(hidden_states[:, start:end])
norm = norm * (1.0 + scale_msa.index_select(0, idx)) + shift_msa.index_select(0, idx)
if getattr(attn, "fused_projections", False):
q, k, v = attn.to_qkv(norm).chunk(3, dim=-1)
else:
q, k, v = attn.to_q(norm), attn.to_k(norm), attn.to_v(norm)
del norm
q = attn.norm_q(q.unflatten(-1, (attn.heads, -1)))
k = attn.norm_k(k.unflatten(-1, (attn.heads, -1)))
v = v.unflatten(-1, (attn.heads, -1))
if rotary_emb is not None:
cos, sin = (t[start:end] for t in rotary_emb)
q = _apply_rotary_emb(q, cos, sin)
k = _apply_rotary_emb(k, cos, sin)
if query is None:
query, key, value = (_attention_buffer(t, batch, total, head_major) for t in (q, k, v))
query[:, start:end], key[:, start:end], value[:, start:end] = q, k, v
del q, k, v
# --- the one full-sequence operation ----------------------------------------------------------------------
out = dispatch_attention_fn(
query,
key,
value,
attn_mask=attention_mask,
dropout_p=0.0,
is_causal=False,
backend=getattr(processor, "_attention_backend", None),
parallel_config=getattr(processor, "_parallel_config", None),
)
dtype = query.dtype
del query, key, value
# --- attention output + residual, then the feed-forward + residual, row chunk by row chunk ----------------
# In place: under no_grad nothing else holds the block's input, and the model rebinds it to the return value.
for start, end in _chunks(total):
idx = adaln_indices[start:end]
projected = attn.to_out[1](attn.to_out[0](out[:, start:end].flatten(2, 3).type(dtype)))
hidden_states[:, start:end] += gate_msa.index_select(0, idx) * projected
del projected
del out
for start, end in _chunks(total):
idx = adaln_indices[start:end]
norm = self.norm2(hidden_states[:, start:end])
norm = norm * (1.0 + scale_mlp.index_select(0, idx)) + shift_mlp.index_select(0, idx)
hidden_states[:, start:end] += gate_mlp.index_select(0, idx) * self.ff(norm)
del norm
return hidden_states
def _norm_out_forward(self, hidden_states, temb, timestep_indices):
"""`norm_out.forward` (released or pruned), chunked, returning float32 — what the model casts it to next anyway."""
import torch
from diffusers.models.modeling_utils import get_parameter_dtype
dtype = get_parameter_dtype(self.linear)
if hasattr(self, "lora_adaln_bias"):
# The pruned checkpoint (`MiniMaxH3PrunedAdaLayerNormOut`, multimodalart/MiniMax-H3-Pruned): `temb` is already
# the table's timestep coordinates, so there is NO `silu`, and the modulation is `linear(temb)` plus the float32
# `folded_bias` — which carries almost the whole modulation — plus the turbo LoRA's AdaLN constant term.
# Leaving those out (the stock formula below) breaks the last layer on every step: that is what burned the
# colours/contrast of every `animate_a2v` render while the one-shot `/generate` path stayed clean.
projected = self.linear(temb.to(dtype))
shift, scale = (projected.float() + self.lora_adaln_bias()).to(dtype).chunk(2, dim=-1)
else:
# The released checkpoint's `MiniMaxH3AdaLayerNormOut`.
shift, scale = self.linear(torch.nn.functional.silu(temb).to(dtype)).chunk(2, dim=-1)
out = torch.empty(hidden_states.shape, device=hidden_states.device, dtype=torch.float32)
for start, end in _chunks(hidden_states.shape[1]):
idx = timestep_indices[start:end]
chunk = self.norm(hidden_states[:, start:end])
out[:, start:end] = (chunk * (1.0 + scale.index_select(0, idx)) + shift.index_select(0, idx)).float()
return out
def _streaming_video_decode(self, components, state):
"""`MiniMaxH3VideoDecodeStep.__call__` with the VAE's own clip loop, emitting uint8 frames to the CPU per clip.
With a `DecodeControl` installed (resumable windows) the loop can start at any clip and stop before any clip; the
returned video then holds only the frames THIS window decoded and the trailing padding frames are NOT trimmed —
`control.pad_frames` says how many to cut once the parts are joined. Without one it is the stock decode, trimmed.
"""
import torch
from diffusers.models.modeling_utils import get_parameter_dtype
block_state = self.get_block_state(state)
device = components._execution_device
vae = components.vae
control = getattr(CONTROL, "decode", None) if CONTROL is not None else None
latents_mean = torch.tensor(vae.config.latents_mean, device=device).view(1, -1, 1, 1, 1)
latents_std = torch.tensor(vae.config.latents_std, device=device).view(1, -1, 1, 1, 1)
z = (block_state.latents * latents_std + latents_mean).to(get_parameter_dtype(vae.decoder))
pixel_mean = torch.tensor(components.pixel_mean, device=device).view(1, -1, 1, 1, 1)
pixel_std = torch.tensor(components.pixel_std, device=device).view(1, -1, 1, 1, 1)
frames = []
def emit(chunk):
video = (chunk.float() * pixel_std + pixel_mean).clamp(0, 1)
frames.append((video[0] * 255).round().to(torch.uint8).permute(1, 2, 3, 0).contiguous().cpu())
# The loop below is `AutoencoderKLMiniMaxH3._decode`, line for line, with `decoded_chunks.append` -> `emit`.
tokens_chunk_size = vae.tokens_chunk_size
token_drop = vae.config.token_drop
temporal_ratio = vae.temporal_compression_ratio
chunk_num_frames = tokens_chunk_size * temporal_ratio
num_tokens = z.shape[2] + token_drop
pad_tokens = (-num_tokens) % tokens_chunk_size
num_chunks = (num_tokens + pad_tokens) // tokens_chunk_size - int(token_drop > 0)
if pad_tokens > 0:
z = torch.cat([z, z[:, :, -1:].repeat(1, 1, pad_tokens, 1, 1)], dim=2)
# The frames the latent padding produced past the last requested one (`_decode` cuts them off at the very end).
pad_frames = 0
if pad_tokens > 0:
intra_tail = vae.config.clip_length % temporal_ratio
before_pad = z.shape[2] - pad_tokens
pad_frames = sum(
intra_tail if intra_tail and (before_pad + k) % tokens_chunk_size == 0 else temporal_ratio
for k in range(pad_tokens)
)
first_clip, overlap = 0, None
if control is not None:
control.num_clips, control.pad_frames, control.decoded = int(num_chunks), int(pad_frames), 0
first_clip = min(int(control.resume_clip), int(num_chunks))
if control.resume_overlap is not None:
overlap = control.resume_overlap.to(device)
with torch.autocast(device_type=device.type, dtype=torch.float16, enabled=device.type == "cuda"):
for i in range(first_clip, num_chunks):
if control is not None and control.should_stop is not None:
if control.should_stop(i, sum(int(f.shape[0]) for f in frames)):
raise A2VDecodeStop(
i,
torch.cat(frames) if frames else None,
overlap.detach().to("cpu") if overlap is not None else None,
)
start = i * tokens_chunk_size
clip = vae._decode_clip(z[:, :, start:start + tokens_chunk_size + vae.token_overlap])
for j in range(int(token_drop > 0) + 1):
frame_start = j * chunk_num_frames
chunk = clip[:, :, frame_start:frame_start + chunk_num_frames]
chunk = chunk[:, :, vae.frame_pre_padding:]
if j == 0:
if overlap is not None:
chunk = vae._blend(overlap, chunk, vae.frame_overlap, dim=-3)
emit(chunk)
else:
overlap = chunk
del clip
if control is not None:
control.decoded += 1
if overlap is not None:
emit(overlap)
video = torch.cat(frames)
if control is None and pad_frames > 0:
video = video[:-pad_frames]
block_state.videos = [video] # (F, H, W, 3) uint8 on the CPU
self.set_block_state(state, block_state)
return components, state
@contextlib.contextmanager
def patched(pipe, control: BlockControl | None = None):
"""Install the chunked block/norm forwards on this transformer and the streaming decode, then undo them.
`control`, when given, lets the window stop between two transformer blocks and resume there later."""
global CONTROL
from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3VideoDecodeStep
transformer = pipe.transformer
undo = []
for index, block in enumerate(getattr(transformer, "transformer_blocks", [])):
if all(hasattr(block, name) for name in ("adaln_proj", "attn", "ff", "norm1", "norm2")):
block._a2v_index = index
block.forward = types.MethodType(_block_forward, block)
undo.append(lambda block=block: block.__dict__.pop("forward", None))
norm_out = getattr(transformer, "norm_out", None)
if norm_out is not None and hasattr(norm_out, "linear") and hasattr(norm_out, "norm"):
norm_out.forward = types.MethodType(_norm_out_forward, norm_out)
undo.append(lambda: norm_out.__dict__.pop("forward", None))
original_decode = MiniMaxH3VideoDecodeStep.__call__
MiniMaxH3VideoDecodeStep.__call__ = _streaming_video_decode
undo.append(lambda: setattr(MiniMaxH3VideoDecodeStep, "__call__", original_decode))
print(f"[a2v-memory] chunked {len(undo) - 2} blocks at {CHUNK_ROWS} rows, streaming decode on", flush=True)
CONTROL = control
try:
yield
finally:
CONTROL = None
for step in reversed(undo):
step()
|