linoyts HF Staff commited on
Commit
d9863e2
·
verified ·
1 Parent(s): dbc0953

Ship a modular workflow that carries keyframes and references in one run

Browse files
Files changed (1) hide show
  1. 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}")