dagloop5 commited on
Commit
8636286
·
verified ·
1 Parent(s): a84218e

Upload h3_momentum.py

Browse files
Files changed (1) hide show
  1. h3_momentum.py +341 -0
h3_momentum.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Video-momentum-conditioned `t2va` for Chunked Generation: the previous chunk's trailing frames, imposed on the
2
+ opening latent frames of the next chunk's own target — real motion continuity at the seam, not a single-frame
3
+ keyframe anchor.
4
+
5
+ Modeled directly on `h3_a2v_blocks.py`'s `MiniMaxH3AudioConditionStep` (from the `multimodalart/minimax-h3-audio-
6
+ to-video` Space): a *given* signal is encoded and imposed on the rows the model would otherwise draw fresh noise
7
+ for, in one of two ways — `locked` (clean from the first forward on, presented at `t = 1.0`) or `blended` (re-
8
+ noised to each step's own sigma, ordinary diffusion inpainting). The difference from that file's audio block is
9
+ scope: audio conditions the *entire* track; this conditions only a *prefix* of the generated video rows — the
10
+ carried head — leaving the rest on the normal free noise-draw path, since a chunk's own new content still needs
11
+ to come from the model, not from what was carried in.
12
+
13
+ Continuation chunks run keyframe-free (`t2va`-shaped, no `image`/`last_image`): the momentum block already
14
+ determines what the opening frames are, more directly than a keyframe ever could, so there is nothing for a
15
+ keyframe to add. Only chunk one, with no prior chunk to carry from, uses the ordinary keyframe-capable generator.
16
+
17
+ Two things this has not been validated against yet, flagged rather than assumed away: whether encoding a short
18
+ trailing clip standalone reproduces what those frames would have encoded to as part of the original longer clip
19
+ (a causal-VAE boundary effect, if the encoder has significant temporal receptive field), and whether the video
20
+ VAE's encoder is tolerant of an arbitrary frame count for a short sub-clip the way a full request's `17 * n + 5`
21
+ alignment matters there. Worth checking directly before trusting this at high momentum durations.
22
+ """
23
+
24
+ import torch
25
+
26
+ from diffusers.models import AutoencoderKLMiniMaxH3
27
+ from diffusers.modular_pipelines.minimax_h3.before_denoise import (
28
+ MiniMaxH3NoKeyframeAnchorsStep,
29
+ MiniMaxH3PrepareLatentsStep,
30
+ MiniMaxH3PrepareLayoutStep,
31
+ MiniMaxH3SetTimestepsStep,
32
+ )
33
+ from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
34
+ from diffusers.modular_pipelines.minimax_h3.denoise import (
35
+ MiniMaxH3DenoiseLoopWrapper,
36
+ MiniMaxH3LoopDenoiser,
37
+ MiniMaxH3LoopSchedulerStep,
38
+ )
39
+ from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import MiniMaxH3DecodeStep
40
+ from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
41
+ from diffusers.modular_pipelines.modular_pipeline import BlockState, ModularPipelineBlocks, PipelineState, SequentialPipelineBlocks
42
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
43
+
44
+
45
+ LOCKED, BLENDED, OFF = "locked", "blended", "off"
46
+ VIDEO_CONDITION_MODES = (LOCKED, BLENDED, OFF)
47
+
48
+
49
+ def pack_video_rows(latents: torch.Tensor, patch_size: tuple[int, int, int], channels: int) -> torch.Tensor:
50
+ """The forward patchify `MiniMaxH3PrepareLayoutStep` applies to fresh noise, run here on a real encoded clip
51
+ instead. The exact inverse of `MiniMaxH3AfterDenoiseStep`'s own unpack (`reshape` -> `permute(0,4,1,5,2,6,3,7)`
52
+ -> `reshape`), so a prefix of the rows this returns lines up with a whole number of leading latent frames —
53
+ with `patch_size`'s temporal component at 1 (no temporal sub-patching), each latent frame maps to an exact,
54
+ contiguous, non-interleaved block of `(latent_height // patch_h) * (latent_width // patch_w)` rows.
55
+ """
56
+ patch_t, patch_h, patch_w = patch_size
57
+ b, c, f, h, w = latents.shape
58
+ rows = latents.reshape(b, c, f // patch_t, patch_t, h // patch_h, patch_h, w // patch_w, patch_w)
59
+ rows = rows.permute(0, 2, 4, 6, 1, 3, 5, 7)
60
+ return rows.reshape(-1, channels * patch_t * patch_h * patch_w)
61
+
62
+
63
+ class MiniMaxH3MomentumConditionStep(ModularPipelineBlocks):
64
+ model_name = "minimax-h3"
65
+
66
+ @property
67
+ def description(self) -> str:
68
+ return (
69
+ "Encodes the given trailing clip into the opening video rows of the packed sequence, and keeps the "
70
+ "noise those rows were drawn from. Runs after the noise draw so the request's generator is "
71
+ "untouched: video noise is still the generator's first draw regardless of this block, which only "
72
+ "overwrites — never skips — that draw for the imposed prefix."
73
+ )
74
+
75
+ @property
76
+ def expected_components(self) -> list[ComponentSpec]:
77
+ return [ComponentSpec("vae", AutoencoderKLMiniMaxH3)]
78
+
79
+ @property
80
+ def inputs(self) -> list[InputParam]:
81
+ return [
82
+ InputParam(
83
+ name="given_video",
84
+ type_hint=torch.Tensor,
85
+ description="The carried clip's pixel frames, `(num_frames, 3, H, W)` in `[0, 1]`. None runs "
86
+ "plain `t2va`.",
87
+ ),
88
+ InputParam(
89
+ name="video_condition_mode",
90
+ type_hint=str,
91
+ default=BLENDED,
92
+ description="`locked`, `blended` or `off`.",
93
+ ),
94
+ InputParam(name="latents", type_hint=torch.Tensor, required=True, description="The video rows of the "
95
+ "packed sequence, as drawn from the request's generator."),
96
+ InputParam(name="num_condition_video_rows", type_hint=int, default=0),
97
+ ]
98
+
99
+ @property
100
+ def intermediate_outputs(self) -> list[OutputParam]:
101
+ return [
102
+ OutputParam("latents", type_hint=torch.Tensor, description="The video rows the loop starts from."),
103
+ OutputParam(
104
+ "given_video_rows", type_hint=torch.Tensor, description="The encoded, packed carried clip's rows, "
105
+ "or None.",
106
+ ),
107
+ OutputParam(
108
+ "video_noise_rows", type_hint=torch.Tensor, description="The noise the imposed rows were drawn "
109
+ "from, which `blended` re-noises against.",
110
+ ),
111
+ OutputParam(
112
+ "num_momentum_rows", type_hint=int, description="How many leading generated video rows are "
113
+ "imposed — 0 when there is nothing to carry.",
114
+ ),
115
+ ]
116
+
117
+ @torch.no_grad()
118
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
119
+ block_state = self.get_block_state(state)
120
+ device = components._execution_device
121
+
122
+ if block_state.video_condition_mode not in VIDEO_CONDITION_MODES:
123
+ raise ValueError(
124
+ f"`video_condition_mode` must be one of {VIDEO_CONDITION_MODES}, got "
125
+ f"{block_state.video_condition_mode!r}."
126
+ )
127
+
128
+ block_state.video_noise_rows = block_state.latents.clone()
129
+ block_state.given_video_rows = None
130
+ block_state.num_momentum_rows = 0
131
+
132
+ if block_state.given_video is not None and block_state.video_condition_mode != OFF:
133
+ channels = components.vae_latent_channels
134
+ patch_size = components.patch_size
135
+
136
+ # (num_frames, 3, H, W) in [0, 1] -> (1, 3, num_frames, H, W), the video VAE's own input convention.
137
+ frames = block_state.given_video.to(device=device, dtype=components.vae.dtype)
138
+ frames = frames.permute(1, 0, 2, 3)[None]
139
+ posterior = components.vae.encode(frames, return_dict=False)[0]
140
+ momentum_latents = posterior.mode() # (1, channels, k, latent_height, latent_width)
141
+
142
+ given_rows = pack_video_rows(momentum_latents, patch_size, channels).to(block_state.latents.dtype)
143
+ num_momentum_rows = given_rows.shape[0]
144
+
145
+ start = block_state.num_condition_video_rows
146
+ available = block_state.latents.shape[0] - start
147
+ if num_momentum_rows > available:
148
+ raise ValueError(
149
+ f"The carried clip encodes to {num_momentum_rows} rows, more than the {available} generated "
150
+ f"rows available to impose on — shorten the momentum duration."
151
+ )
152
+
153
+ block_state.given_video_rows = given_rows
154
+ block_state.num_momentum_rows = num_momentum_rows
155
+
156
+ if block_state.video_condition_mode == LOCKED:
157
+ # Clean from the first forward on, matching the `t = 1.0` the timestep plan will claim for these
158
+ # rows — the same presentation `ref2va` gives a reference, applied here to a target prefix.
159
+ block_state.latents = block_state.latents.clone()
160
+ block_state.latents[start : start + num_momentum_rows] = given_rows.clone()
161
+
162
+ self.set_block_state(state, block_state)
163
+ return components, state
164
+
165
+
166
+ class MiniMaxH3MomentumSetTimestepsStep(MiniMaxH3SetTimestepsStep):
167
+ model_name = "minimax-h3"
168
+
169
+ @property
170
+ def description(self) -> str:
171
+ return (
172
+ "The `t2va` timestep plan, with the imposed video prefix presented as clean (`t = 1.0`) when the "
173
+ "carried clip is locked. `blended` and `off` leave those rows on their own schedule, where their "
174
+ "content really is at the level their timestep claims."
175
+ )
176
+
177
+ @property
178
+ def inputs(self) -> list[InputParam]:
179
+ return super().inputs + [
180
+ InputParam(name="video_condition_mode", type_hint=str, default=BLENDED, description="See the block above."),
181
+ InputParam(name="given_video_rows", type_hint=torch.Tensor, description="The encoded carried clip, or None."),
182
+ InputParam(name="num_momentum_rows", type_hint=int, default=0),
183
+ ]
184
+
185
+ @torch.no_grad()
186
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
187
+ block_state = self.get_block_state(state)
188
+ device = components._execution_device
189
+
190
+ components.scheduler.set_timesteps(block_state.num_inference_steps, device=device)
191
+ components.audio_scheduler.set_timesteps(block_state.num_inference_steps, device=device)
192
+ block_state.timesteps = components.scheduler.timesteps
193
+ block_state.audio_timesteps = components.audio_scheduler.timesteps
194
+
195
+ locked = block_state.video_condition_mode == LOCKED and block_state.given_video_rows is not None
196
+ # The rows are *given*, not *prepended*: `num_condition_video_rows` stays whatever the (keyframe-free)
197
+ # layout already set it to everywhere else, so the loop still steps them and the unpack step still keeps
198
+ # them — only this plan's own clean-row count changes.
199
+ num_clean_video_rows = int(block_state.num_momentum_rows) if locked else 0
200
+
201
+ block_state.row_timestep_plan = [
202
+ tuple(
203
+ tensor.to(device)
204
+ for tensor in self.build_row_timesteps(
205
+ block_state.video_indices,
206
+ block_state.audio_indices,
207
+ num_clean_video_rows,
208
+ block_state.num_condition_audio_rows,
209
+ block_state.text_indices.numel(),
210
+ float(timestep),
211
+ float(audio_timestep),
212
+ 1.0,
213
+ max(float(audio_timestep), components.keyframe_noise_aug),
214
+ )
215
+ )
216
+ for timestep, audio_timestep in zip(block_state.timesteps, block_state.audio_timesteps)
217
+ ]
218
+
219
+ self.set_block_state(state, block_state)
220
+ return components, state
221
+
222
+
223
+ class MiniMaxH3MomentumLoopSchedulerStep(MiniMaxH3LoopSchedulerStep):
224
+ model_name = "minimax-h3"
225
+
226
+ @property
227
+ def description(self) -> str:
228
+ return (
229
+ "Steps both modalities, then imposes the carried clip on the leading video rows: clean when locked, "
230
+ "re-noised to the video schedule's next sigma when blended. The scheduler is still stepped over "
231
+ "those rows so its step index keeps up with the loop; the write-back is what the next forward reads."
232
+ )
233
+
234
+ @property
235
+ def inputs(self) -> list[InputParam]:
236
+ return super().inputs + [
237
+ InputParam(name="video_condition_mode", type_hint=str, default=BLENDED, description="See the block above."),
238
+ InputParam(name="given_video_rows", type_hint=torch.Tensor, description="The encoded carried clip, or None."),
239
+ InputParam(name="video_noise_rows", type_hint=torch.Tensor, description="The noise the imposed rows were drawn from."),
240
+ InputParam(name="num_momentum_rows", type_hint=int, default=0),
241
+ InputParam(name="num_condition_video_rows", type_hint=int, default=0),
242
+ ]
243
+
244
+ @torch.no_grad()
245
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: BlockState, i: int, t: torch.Tensor):
246
+ components, block_state = super().__call__(components, state, i, t)
247
+
248
+ given = block_state.given_video_rows
249
+ if given is None or block_state.video_condition_mode == OFF:
250
+ return components, block_state
251
+
252
+ start = block_state.num_condition_video_rows
253
+ end = start + block_state.num_momentum_rows
254
+
255
+ if block_state.video_condition_mode == LOCKED:
256
+ # `.clone()` is load-bearing — see the identical note on the audio version of this step: without it
257
+ # the imposed slice *is* `given`, and the next step's write overwrites the carried clip itself.
258
+ block_state.latents[start:end] = given.to(block_state.latents).clone()
259
+ else:
260
+ timesteps = block_state.timesteps
261
+ next_timestep = float(timesteps[i + 1]) if i + 1 < timesteps.numel() else 1.0
262
+ block_state.latents[start:end] = components.scheduler.scale_noise(
263
+ given.to(block_state.latents),
264
+ next_timestep,
265
+ block_state.video_noise_rows[start:end].to(block_state.latents),
266
+ )
267
+ return components, block_state
268
+
269
+
270
+ class MiniMaxH3MomentumDenoiseStep(MiniMaxH3DenoiseLoopWrapper):
271
+ model_name = "minimax-h3"
272
+ block_classes = [MiniMaxH3LoopDenoiser, MiniMaxH3MomentumLoopSchedulerStep]
273
+ block_names = ["denoiser", "update"]
274
+
275
+ @property
276
+ def description(self) -> str:
277
+ return "Runs the `t2va` denoising loop with the carried clip imposed on the leading video rows every step."
278
+
279
+
280
+ class MiniMaxH3MomentumCoreDenoiseStep(SequentialPipelineBlocks):
281
+ """`MiniMaxH3CoreDenoiseStep`'s `t2va`-shaped core, with the momentum-conditioning blocks inserted at the
282
+ same points `h3_a2v_blocks.py` inserts its audio ones: after the noise draw, before the timestep plan."""
283
+
284
+ model_name = "minimax-h3"
285
+ block_classes = [
286
+ MiniMaxH3NoKeyframeAnchorsStep,
287
+ MiniMaxH3PrepareLayoutStep,
288
+ MiniMaxH3PrepareLatentsStep,
289
+ MiniMaxH3MomentumConditionStep,
290
+ MiniMaxH3MomentumSetTimestepsStep,
291
+ MiniMaxH3MomentumDenoiseStep,
292
+ MiniMaxH3AfterDenoiseStep,
293
+ ]
294
+ block_names = [
295
+ "no_keyframe_anchors",
296
+ "prepare_layout",
297
+ "prepare_latents",
298
+ "momentum_condition",
299
+ "set_timesteps",
300
+ "denoise",
301
+ "after_denoise",
302
+ ]
303
+
304
+ @property
305
+ def description(self) -> str:
306
+ return (
307
+ "Core denoising workflow for momentum-conditioned `t2va`: `MiniMaxH3CoreDenoiseStep` with the "
308
+ "carried clip encoded into the leading video rows and imposed on every step."
309
+ )
310
+
311
+
312
+ class MiniMaxH3MomentumGeneratorBlocks(SequentialPipelineBlocks):
313
+ """The denoising half of the split deployment, momentum-conditioned — for Chunked Generation continuation
314
+ chunks only. No keyframe branch at all: the momentum block already determines the opening frames more
315
+ directly than a keyframe could, so continuation chunks never pass `image`/`last_image`. Chunk one, with
316
+ nothing to carry, keeps using the ordinary keyframe-capable `MiniMaxH3GeneratorBlocks`.
317
+ """
318
+
319
+ model_name = "minimax-h3"
320
+ block_classes = [MiniMaxH3MomentumCoreDenoiseStep, MiniMaxH3DecodeStep]
321
+ block_names = ["denoise", "decode"]
322
+
323
+ @property
324
+ def description(self) -> str:
325
+ return (
326
+ "The denoising half of a split MiniMax-H3 deployment, conditioned on a carried trailing clip: "
327
+ "always the `t2va` shape, without a text-encoder step, with the leading video rows imposed rather "
328
+ "than generated."
329
+ )
330
+
331
+ @property
332
+ def outputs(self):
333
+ return [
334
+ OutputParam.template("videos", description="The generated video."),
335
+ OutputParam(
336
+ "audio",
337
+ type_hint=torch.Tensor,
338
+ description="The soundtrack of the packed sequence, of shape `(1, 2, num_samples)`.",
339
+ ),
340
+ OutputParam("sampling_rate", type_hint=int, description="Sample rate of the soundtrack in Hz."),
341
+ ]