Image-Text-to-Video
Diffusers
Safetensors
MiniMax H3
modular-diffusers
ref2va
fl2va
Merge
synchronized-audio-video
experimental
Instructions to use diffusers-modular/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use diffusers-modular/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024 with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("diffusers-modular/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Ship a modular workflow that carries keyframes and references in one run
Browse files- combined_layout.py +260 -0
combined_layout.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Packed layout for a MiniMax-H3 request carrying BOTH keyframes and references.
|
| 2 |
+
|
| 3 |
+
Shipped with the checkpoint so `ModularPipeline.from_pretrained(..., trust_remote_code=True)` can serve a request
|
| 4 |
+
that has keyframes *and* references, which the stock blocks cannot express.
|
| 5 |
+
|
| 6 |
+
diffusers ships two builders and dispatches either/or: `references` wins and keyframes are dropped. ComfyUI packs
|
| 7 |
+
both (`PackedLayout(..., keyframes=..., refs=...)` in `comfy/ldm/minimax/model.py`), so the layout exists — this is
|
| 8 |
+
a port of it, not an invention.
|
| 9 |
+
|
| 10 |
+
Row order: [ text | keyframe cond | reference blocks | target audio | target video ]
|
| 11 |
+
|
| 12 |
+
The coupling that makes naive composition wrong: references pack between the text and the targets, so the *target
|
| 13 |
+
timeline* starts after their spans, and the keyframe anchors — which live on the target timeline — shift by exactly
|
| 14 |
+
that amount. ComfyUI does this with a pre-pass (`cursor = text_len + sum(_ref_t_span(blk))`); so does this.
|
| 15 |
+
|
| 16 |
+
Correctness is pinned by `validate_against_stock()`: with no keyframes this must reproduce diffusers' `ref2va`
|
| 17 |
+
layout bit for bit, and with no references its `fl2va` layout, both including `position_ids` in float64.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
from diffusers.modular_pipelines.minimax_h3.before_denoise import (
|
| 25 |
+
_ROPE_FRAME_RESCALE,
|
| 26 |
+
_ROPE_FRAMES_PER_LATENT,
|
| 27 |
+
_fill_audio_positions,
|
| 28 |
+
_frame_position_grid,
|
| 29 |
+
_temporal_position_grid,
|
| 30 |
+
MiniMaxH3PrepareLayoutStep,
|
| 31 |
+
MiniMaxH3Ref2VAPrepareLayoutStep,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _target_span_sum(num_latent_frames: int) -> float:
|
| 36 |
+
"""Rotary time the generated frames span, by numpy pairwise summation — the order the keyframe anchor uses."""
|
| 37 |
+
spans = np.ones(num_latent_frames, dtype=np.float64) * _ROPE_FRAME_RESCALE
|
| 38 |
+
for offset in range(len(_ROPE_FRAMES_PER_LATENT)):
|
| 39 |
+
spans[offset :: len(_ROPE_FRAMES_PER_LATENT)] *= _ROPE_FRAMES_PER_LATENT[offset]
|
| 40 |
+
return float(spans.sum())
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _video_span_sequential(num_latent_frames: int) -> float:
|
| 44 |
+
"""The same series summed sequentially — the order a reference block advances the clock by. The two differ in
|
| 45 |
+
the last ulp from 16 latent frames on, and the reference implementation keeps both, one per call site."""
|
| 46 |
+
return sum(
|
| 47 |
+
_ROPE_FRAME_RESCALE * _ROPE_FRAMES_PER_LATENT[index % len(_ROPE_FRAMES_PER_LATENT)]
|
| 48 |
+
for index in range(num_latent_frames)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _reference_span(reference, visual_geometry, audio_row_counts, audio_channels) -> float:
|
| 53 |
+
"""`_ref_t_span` from ComfyUI: the time axis a reference block occupies ahead of the target streams."""
|
| 54 |
+
if reference.kind == "image":
|
| 55 |
+
next(visual_geometry)
|
| 56 |
+
return 1.0
|
| 57 |
+
if reference.kind == "audio":
|
| 58 |
+
return float(next(audio_row_counts) // audio_channels)
|
| 59 |
+
if reference.kind == "video":
|
| 60 |
+
audio_latents = (next(audio_row_counts) // audio_channels) if reference.has_audio else 0
|
| 61 |
+
frames, _, _ = next(visual_geometry)
|
| 62 |
+
return max(float(audio_latents), _video_span_sequential(frames))
|
| 63 |
+
raise ValueError(f"A reference must be an 'image', a 'video' or an 'audio', got {reference.kind!r}.")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def build_combined_packed_sequence(
|
| 67 |
+
text_token_tags: torch.Tensor,
|
| 68 |
+
references: list,
|
| 69 |
+
condition_latents: list[torch.Tensor],
|
| 70 |
+
audio_condition_latents: list[torch.Tensor],
|
| 71 |
+
num_latent_frames: int,
|
| 72 |
+
latent_height: int,
|
| 73 |
+
latent_width: int,
|
| 74 |
+
num_audio_latents: int,
|
| 75 |
+
patch_size: tuple[int, int, int],
|
| 76 |
+
audio_channels: int,
|
| 77 |
+
audio_tag: int,
|
| 78 |
+
video_tag: int,
|
| 79 |
+
keyframe_anchors: tuple[str, ...] = (),
|
| 80 |
+
):
|
| 81 |
+
"""`condition_latents` is keyframe latents first (one per `keyframe_anchors` entry), then the reference latents."""
|
| 82 |
+
_, patch_h, patch_w = patch_size
|
| 83 |
+
num_keyframes = len(keyframe_anchors)
|
| 84 |
+
keyframe_latents, reference_latents = condition_latents[:num_keyframes], condition_latents[num_keyframes:]
|
| 85 |
+
|
| 86 |
+
num_text_tokens = text_token_tags.shape[0]
|
| 87 |
+
rows_per_frame = (latent_height // patch_h) * (latent_width // patch_w)
|
| 88 |
+
num_keyframe_rows = num_keyframes * rows_per_frame
|
| 89 |
+
num_reference_video_rows = sum(
|
| 90 |
+
frames * (height // patch_h) * (width // patch_w)
|
| 91 |
+
for frames, height, width in (tuple(latents.shape[2:5]) for latents in reference_latents)
|
| 92 |
+
)
|
| 93 |
+
num_reference_audio_rows = sum(rows.shape[0] for rows in audio_condition_latents)
|
| 94 |
+
num_target_video_rows = num_latent_frames * rows_per_frame
|
| 95 |
+
num_target_audio_rows = num_audio_latents * audio_channels
|
| 96 |
+
sequence_length = (
|
| 97 |
+
num_text_tokens
|
| 98 |
+
+ num_keyframe_rows
|
| 99 |
+
+ num_reference_video_rows
|
| 100 |
+
+ num_reference_audio_rows
|
| 101 |
+
+ num_target_audio_rows
|
| 102 |
+
+ num_target_video_rows
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
position_ids = torch.zeros(sequence_length, 3, dtype=torch.float64)
|
| 106 |
+
position_ids[:num_text_tokens, 0] = torch.arange(num_text_tokens, dtype=torch.float64)
|
| 107 |
+
target_frame_grid, target_width_grid = _frame_position_grid(latent_height, latent_width, patch_h, patch_w)
|
| 108 |
+
|
| 109 |
+
# Pre-pass: how far the references push the target timeline out. The keyframe anchors ride on that timeline.
|
| 110 |
+
span_geometry = iter(tuple(latents.shape[2:5]) for latents in reference_latents)
|
| 111 |
+
span_audio = iter(rows.shape[0] for rows in audio_condition_latents)
|
| 112 |
+
reference_span = sum(
|
| 113 |
+
_reference_span(reference, span_geometry, span_audio, audio_channels) for reference in references
|
| 114 |
+
)
|
| 115 |
+
target_origin = float(num_text_tokens) + reference_span
|
| 116 |
+
|
| 117 |
+
video_indices, audio_indices = [], []
|
| 118 |
+
|
| 119 |
+
# 1. Keyframe conditioning rows, immediately after the text, on the target spatial grid.
|
| 120 |
+
cursor = num_text_tokens
|
| 121 |
+
for index, anchor in enumerate(keyframe_anchors):
|
| 122 |
+
if anchor == "first":
|
| 123 |
+
anchor_time = target_origin
|
| 124 |
+
elif anchor == "last":
|
| 125 |
+
anchor_time = target_origin + _target_span_sum(num_latent_frames) - _ROPE_FRAME_RESCALE
|
| 126 |
+
else:
|
| 127 |
+
raise ValueError(f"A keyframe anchor must be 'first' or 'last', got {anchor!r}.")
|
| 128 |
+
frames = keyframe_latents[index].shape[2]
|
| 129 |
+
rows = slice(cursor, cursor + frames * rows_per_frame)
|
| 130 |
+
cursor = rows.stop
|
| 131 |
+
video_indices.append(torch.arange(rows.start, rows.stop))
|
| 132 |
+
position_ids[rows, 0] = anchor_time
|
| 133 |
+
position_ids[rows, 1:] = target_frame_grid.repeat(frames, 1)
|
| 134 |
+
|
| 135 |
+
# 2. Reference blocks, on their own clock starting where the text ends — exactly the stock `ref2va` walk.
|
| 136 |
+
visual_geometry = iter(tuple(latents.shape[2:5]) for latents in reference_latents)
|
| 137 |
+
audio_row_counts = iter(rows.shape[0] for rows in audio_condition_latents)
|
| 138 |
+
rotary_time = float(num_text_tokens)
|
| 139 |
+
for reference in references:
|
| 140 |
+
if reference.kind == "image":
|
| 141 |
+
frames, height, width = next(visual_geometry)
|
| 142 |
+
rows = slice(cursor, cursor + frames * (height // patch_h) * (width // patch_w))
|
| 143 |
+
cursor = rows.stop
|
| 144 |
+
video_indices.append(torch.arange(rows.start, rows.stop))
|
| 145 |
+
frame_grid, _ = _frame_position_grid(height, width, patch_h, patch_w)
|
| 146 |
+
position_ids[rows, 0] = rotary_time
|
| 147 |
+
position_ids[rows, 1:] = frame_grid
|
| 148 |
+
rotary_time += 1.0
|
| 149 |
+
elif reference.kind == "audio":
|
| 150 |
+
num_rows = next(audio_row_counts)
|
| 151 |
+
latents = num_rows // audio_channels
|
| 152 |
+
rows = slice(cursor, cursor + num_rows)
|
| 153 |
+
cursor = rows.stop
|
| 154 |
+
audio_indices.append(torch.arange(rows.start, rows.stop))
|
| 155 |
+
_fill_audio_positions(position_ids, rows, latents, rotary_time, target_width_grid, audio_channels)
|
| 156 |
+
rotary_time += float(latents)
|
| 157 |
+
elif reference.kind == "video":
|
| 158 |
+
num_rows = next(audio_row_counts) if reference.has_audio else 0
|
| 159 |
+
latents = num_rows // audio_channels
|
| 160 |
+
frames, height, width = next(visual_geometry)
|
| 161 |
+
audio_rows = slice(cursor, cursor + num_rows)
|
| 162 |
+
video_rows = slice(audio_rows.stop, audio_rows.stop + frames * (height // patch_h) * (width // patch_w))
|
| 163 |
+
cursor = video_rows.stop
|
| 164 |
+
audio_indices.append(torch.arange(audio_rows.start, audio_rows.stop))
|
| 165 |
+
video_indices.append(torch.arange(video_rows.start, video_rows.stop))
|
| 166 |
+
frame_grid, width_grid = _frame_position_grid(height, width, patch_h, patch_w)
|
| 167 |
+
_fill_audio_positions(position_ids, audio_rows, latents, rotary_time, width_grid, audio_channels)
|
| 168 |
+
frame_time = _temporal_position_grid(frames, rotary_time)
|
| 169 |
+
position_ids[video_rows, 0] = frame_time.repeat_interleave(frame_grid.shape[0])
|
| 170 |
+
position_ids[video_rows, 1:] = frame_grid.repeat(frames, 1)
|
| 171 |
+
rotary_time += max(float(latents), _video_span_sequential(frames))
|
| 172 |
+
else:
|
| 173 |
+
raise ValueError(f"A reference must be an 'image', a 'video' or an 'audio', got {reference.kind!r}.")
|
| 174 |
+
|
| 175 |
+
# 3. The generated rows, on the timeline the references left behind — the same origin the keyframes anchored to.
|
| 176 |
+
audio_start = cursor
|
| 177 |
+
video_start = audio_start + num_target_audio_rows
|
| 178 |
+
_fill_audio_positions(
|
| 179 |
+
position_ids,
|
| 180 |
+
slice(audio_start, video_start),
|
| 181 |
+
num_audio_latents,
|
| 182 |
+
target_origin,
|
| 183 |
+
target_width_grid,
|
| 184 |
+
audio_channels,
|
| 185 |
+
)
|
| 186 |
+
frame_time = _temporal_position_grid(num_latent_frames, target_origin)
|
| 187 |
+
position_ids[video_start:, 0] = frame_time.repeat_interleave(target_frame_grid.shape[0])
|
| 188 |
+
position_ids[video_start:, 1:] = target_frame_grid.repeat(num_latent_frames, 1)
|
| 189 |
+
|
| 190 |
+
video_indices = torch.cat(video_indices + [torch.arange(video_start, sequence_length)])
|
| 191 |
+
audio_indices = torch.cat(audio_indices + [torch.arange(audio_start, video_start)])
|
| 192 |
+
text_indices = torch.arange(num_text_tokens)
|
| 193 |
+
|
| 194 |
+
token_tags = torch.empty(sequence_length, dtype=torch.long)
|
| 195 |
+
token_tags[text_indices] = text_token_tags.to(torch.long)
|
| 196 |
+
token_tags[audio_indices] = audio_tag
|
| 197 |
+
token_tags[video_indices] = video_tag
|
| 198 |
+
|
| 199 |
+
return (
|
| 200 |
+
position_ids,
|
| 201 |
+
token_tags,
|
| 202 |
+
video_indices,
|
| 203 |
+
audio_indices,
|
| 204 |
+
text_indices,
|
| 205 |
+
num_keyframe_rows + num_reference_video_rows,
|
| 206 |
+
num_reference_audio_rows,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def validate_against_stock(verbose: bool = True) -> dict:
|
| 211 |
+
"""Both degenerate cases must reproduce the shipped builders exactly."""
|
| 212 |
+
|
| 213 |
+
class _Ref:
|
| 214 |
+
def __init__(self, kind, has_audio=False):
|
| 215 |
+
self.kind, self.has_audio = kind, has_audio
|
| 216 |
+
|
| 217 |
+
geometry = dict(num_latent_frames=8, latent_height=34, latent_width=60, num_audio_latents=200,
|
| 218 |
+
patch_size=(1, 2, 2), audio_channels=2, audio_tag=2, video_tag=0)
|
| 219 |
+
tags = torch.randint(0, 2, (57,))
|
| 220 |
+
report = {}
|
| 221 |
+
|
| 222 |
+
# (a) references only -> the stock ref2va layout
|
| 223 |
+
refs = [_Ref("image"), _Ref("video", has_audio=True), _Ref("audio")]
|
| 224 |
+
ref_latents = [torch.zeros(1, 16, 1, 32, 32), torch.zeros(1, 16, 3, 34, 60)]
|
| 225 |
+
ref_audio = [torch.zeros(60, 8), torch.zeros(40, 8)]
|
| 226 |
+
mine = build_combined_packed_sequence(tags, refs, ref_latents, ref_audio, **geometry)
|
| 227 |
+
stock = MiniMaxH3Ref2VAPrepareLayoutStep.build_ref2va_packed_sequence(
|
| 228 |
+
tags, refs, ref_latents, ref_audio, **geometry)
|
| 229 |
+
report["refs_only"] = _compare(mine, stock)
|
| 230 |
+
|
| 231 |
+
# (b) keyframes only -> the stock fl2va layout
|
| 232 |
+
anchors = ("first", "last")
|
| 233 |
+
kf_latents = [torch.zeros(1, 16, 1, 34, 60), torch.zeros(1, 16, 1, 34, 60)]
|
| 234 |
+
mine = build_combined_packed_sequence(tags, [], kf_latents, [], keyframe_anchors=anchors, **geometry)
|
| 235 |
+
stock = MiniMaxH3PrepareLayoutStep.build_packed_sequence(tags, keyframe_anchors=anchors, **geometry)
|
| 236 |
+
report["keyframes_only"] = _compare(mine, stock)
|
| 237 |
+
|
| 238 |
+
if verbose:
|
| 239 |
+
for case, result in report.items():
|
| 240 |
+
print(f"{case}: {result}")
|
| 241 |
+
return report
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _compare(mine, stock) -> dict:
|
| 245 |
+
names = ["position_ids", "token_tags", "video_indices", "audio_indices", "text_indices",
|
| 246 |
+
"num_condition_video_rows", "num_condition_audio_rows"]
|
| 247 |
+
out = {}
|
| 248 |
+
for name, a, b in zip(names, mine, stock):
|
| 249 |
+
if isinstance(a, torch.Tensor):
|
| 250 |
+
out[name] = "identical" if a.shape == b.shape and torch.equal(a, b) else f"DIFFERS {tuple(a.shape)} vs {tuple(b.shape)}"
|
| 251 |
+
else:
|
| 252 |
+
out[name] = "identical" if a == b else f"DIFFERS {a} vs {b}"
|
| 253 |
+
return out
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
report = validate_against_stock()
|
| 258 |
+
bad = {c: {k: v for k, v in r.items() if v != "identical"} for c, r in report.items()}
|
| 259 |
+
bad = {c: v for c, v in bad.items() if v}
|
| 260 |
+
print("\nVALIDATION", "PASSED" if not bad else f"FAILED: {bad}")
|