Skip to content

Commit 9a8d5b9

Browse files
committed
[bugfix] Fix the training data preprocess pipeline of h3 ref2va
1 parent dc8cc41 commit 9a8d5b9

1 file changed

Lines changed: 272 additions & 12 deletions

File tree

fastvideo/pipelines/preprocess/preprocess_minimax_h3_ref2va.py

Lines changed: 272 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,68 @@
44
from __future__ import annotations
55

66
import argparse
7+
import gc
78
from pathlib import Path
89
import shutil
910
from typing import Any
1011

12+
import numpy as np
1113
import pyarrow as pa
1214
import pyarrow.parquet as pq
15+
import torch
1316

1417
from fastvideo.configs.pipelines.minimax_h3 import MiniMaxH3PipelineConfig
15-
from fastvideo.dataset.minimax_h3_ref2va_dataset import pyarrow_schema_minimax_h3_ref2va
18+
from fastvideo.dataset.minimax_h3_ref2va_dataset import (
19+
MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH,
20+
MINIMAX_H3_REF2VA_SCHEMA_VERSION,
21+
MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH,
22+
collate_minimax_h3_ref2va_rows,
23+
pyarrow_schema_minimax_h3_ref2va,
24+
)
1625
from fastvideo.fastvideo_args import FastVideoArgs
26+
from fastvideo.models.schedulers.scheduling_minimax_h3 import MiniMaxH3Scheduler
27+
from fastvideo.pipelines import ForwardBatch
28+
from fastvideo.pipelines.basic.minimax_h3.packing import (
29+
MINIMAX_H3_KEYFRAME_ENCODE_SEED,
30+
MINIMAX_H3_KEYFRAME_NOISE_AUG,
31+
audio_latent_num_frames,
32+
keyframe_condition_noise,
33+
patchify_video_latents,
34+
video_latent_num_frames,
35+
)
1736
from fastvideo.pipelines.basic.minimax_h3.ref2va_manifest import (
1837
MiniMaxH3RawReference,
1938
MiniMaxH3Ref2VARawSample,
2039
build_minimax_h3_references,
2140
load_minimax_h3_ref2va_raw_samples,
2241
)
23-
from fastvideo.pipelines.basic.minimax_h3.reference import MiniMaxH3PreparedReference, prepare_reference
42+
from fastvideo.pipelines.basic.minimax_h3.reference import (
43+
MiniMaxH3PreparedReference,
44+
prepare_reference,
45+
trim_reference_num_frames,
46+
)
47+
from fastvideo.pipelines.basic.minimax_h3.stages.minimax_h3_conditioning import (
48+
MINIMAX_H3_TEXT_TOKEN_TAGS_KEY,
49+
MiniMaxH3ConditioningStage,
50+
)
51+
from fastvideo.pipelines.basic.minimax_h3.stages.minimax_h3_input_preparation import MINIMAX_H3_KEYFRAMES_KEY
2452
from fastvideo.pipelines.preprocess.preprocess_minimax_h3_overfit import (
2553
AUDIO_SAMPLE_RATE,
2654
MODEL_PATH,
2755
NUM_FRAMES,
56+
VIDEO_HEIGHT,
57+
VIDEO_WIDTH,
2858
_init_single_process_distributed,
59+
_load_component,
60+
build_parquet_record,
2961
encode_audio_latents,
3062
encode_video_latents,
3163
load_training_media,
3264
)
33-
from fastvideo.pipelines.preprocess.preprocess_minimax_h3_ref2va_overfit import (
34-
build_ref2va_parquet_record,
35-
encode_ref2va_conditioning,
36-
encode_ref_audio_anchor,
37-
encode_ref_visual_anchor,
38-
validate_record_contract,
39-
)
4065
from fastvideo.utils import verify_model_config_and_directory
4166

42-
DEFAULT_MANIFEST = Path("data/my_openhumanvid/train.jsonl")
43-
DEFAULT_OUTPUT_DIR = Path("data/my_openhumanvid_h3_ref2va_preprocessed")
67+
DEFAULT_MANIFEST = Path("examples/training/finetune/minimax-h3/openhumanvid/train.jsonl")
68+
DEFAULT_OUTPUT_DIR = Path("data/openhumanvid_h3_ref2va_single_sample_preprocessed")
4469
_PATCH_SIZE = (1, 2, 2)
4570

4671

@@ -54,6 +79,241 @@ def parse_args() -> argparse.Namespace:
5479
return parser.parse_args()
5580

5681

82+
def _sample_visual_posterior(posterior: Any) -> torch.Tensor:
83+
generator = torch.Generator("cpu").manual_seed(MINIMAX_H3_KEYFRAME_ENCODE_SEED)
84+
return posterior.sample(generator=generator)
85+
86+
87+
def encode_ref_visual_anchor(
88+
references: list[MiniMaxH3PreparedReference],
89+
model_path: Path,
90+
model_index: dict[str, Any],
91+
fastvideo_args: FastVideoArgs,
92+
patch_size: tuple[int, int, int],
93+
) -> torch.Tensor:
94+
"""Encode and cache official 0.999-noised ordered visual condition rows."""
95+
visual_references = [reference for reference in references if reference.media_type != "audio"]
96+
if not visual_references:
97+
return torch.empty((0, MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH), dtype=torch.float32)
98+
99+
print("Loading MiniMax H3 video VAE for Ref2VA visual anchors")
100+
vae = _load_component("vae", model_path, model_index, fastvideo_args)
101+
device = torch.device("cuda:0")
102+
clean_rows: list[torch.Tensor] = []
103+
latent_channels = int(vae.latent_channels)
104+
with torch.no_grad():
105+
for reference in references:
106+
if reference.media_type == "audio":
107+
continue
108+
if reference.media_type == "image":
109+
if reference.image is None:
110+
raise ValueError("Prepared image reference is missing pixels")
111+
pixels = torch.from_numpy(np.asarray(reference.image).copy()).permute(2, 0, 1)[None, :, None]
112+
pixels = pixels.to(device=device, dtype=torch.float32).div_(255.0)
113+
posterior = vae.encode_keyframe(vae.normalize_pixels(pixels)).latent_dist
114+
else:
115+
if reference.frames is None:
116+
raise ValueError("Prepared video reference is missing frames")
117+
frames = reference.frames[:trim_reference_num_frames(reference.frames.shape[0])]
118+
pixels = torch.from_numpy(frames.copy()).permute(3, 0, 1, 2)[None]
119+
pixels = pixels.to(device=device, dtype=torch.float32).div_(255.0)
120+
posterior = vae.encode(vae.normalize_pixels(pixels)).latent_dist
121+
122+
# The fp16 round trip before latent normalization is part of the
123+
# released Ref2VA condition encoding path.
124+
latents = vae.normalize_latents(_sample_visual_posterior(posterior).to(torch.float16).float()).cpu()
125+
if latents.ndim != 5 or latents.shape[0] != 1 or latents.shape[1] != latent_channels:
126+
raise ValueError(f"Unexpected reference visual latent shape: {tuple(latents.shape)}")
127+
reference.num_latent_frames = int(latents.shape[2])
128+
reference.latent_height = int(latents.shape[3])
129+
reference.latent_width = int(latents.shape[4])
130+
clean_rows.append(patchify_video_latents(latents, patch_size).float().contiguous())
131+
del posterior, latents, pixels
132+
133+
clean_anchor = torch.cat(clean_rows).to(device=device, dtype=torch.float32)
134+
shapes = tuple((reference.num_latent_frames, reference.latent_height, reference.latent_width)
135+
for reference in references if reference.media_type != "audio")
136+
noise_generator = torch.Generator("cpu").manual_seed(MINIMAX_H3_KEYFRAME_ENCODE_SEED)
137+
noise = keyframe_condition_noise(
138+
shapes,
139+
patch_size,
140+
latent_channels,
141+
generator=noise_generator,
142+
device=device,
143+
dtype=torch.float32,
144+
)
145+
anchor = MiniMaxH3Scheduler(shift=12.0).scale_noise(
146+
clean_anchor,
147+
MINIMAX_H3_KEYFRAME_NOISE_AUG,
148+
noise,
149+
).float().cpu().contiguous()
150+
151+
expected_width = latent_channels * int(np.prod(patch_size))
152+
if expected_width != MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH or anchor.shape[1] != expected_width:
153+
raise ValueError(
154+
f"Ref2VA visual anchor width must be {MINIMAX_H3_REF2VA_VISUAL_ROW_WIDTH}, got {anchor.shape[1]}")
155+
del clean_anchor, clean_rows, noise, vae
156+
gc.collect()
157+
torch.cuda.empty_cache()
158+
print(f"Ref2VA visual anchor shape: {tuple(anchor.shape)} at fixed clean-time 0.999")
159+
return anchor
160+
161+
162+
def encode_ref_audio_anchor(
163+
references: list[MiniMaxH3PreparedReference],
164+
model_path: Path,
165+
model_index: dict[str, Any],
166+
fastvideo_args: FastVideoArgs,
167+
) -> torch.Tensor:
168+
"""Encode clean channel-major audio anchors in ordered-reference order."""
169+
if not any(reference.has_audio for reference in references):
170+
return torch.empty((0, MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH), dtype=torch.float32)
171+
172+
print("Loading MiniMax H3 audio VAE for Ref2VA audio anchors")
173+
audio_vae = _load_component("audio_vae", model_path, model_index, fastvideo_args)
174+
device = torch.device("cuda:0")
175+
latent_channels = int(audio_vae.latent_channels)
176+
if int(audio_vae.sampling_rate) != AUDIO_SAMPLE_RATE:
177+
raise ValueError(f"Audio VAE sampling rate must be {AUDIO_SAMPLE_RATE}, got {audio_vae.sampling_rate}")
178+
rows: list[torch.Tensor] = []
179+
with torch.no_grad():
180+
for reference in references:
181+
if not reference.has_audio:
182+
continue
183+
if reference.waveform is None:
184+
raise ValueError("Audio-bearing reference is missing its prepared waveform")
185+
posterior = audio_vae.encode(reference.waveform.to(device=device, dtype=torch.float32)[:, None]).latent_dist
186+
latents = audio_vae.normalize_latents(posterior.mode().float()).cpu().transpose(1, 2)
187+
if latents.ndim != 3 or latents.shape[0] != 2 or latents.shape[2] != latent_channels:
188+
raise ValueError(f"Unexpected reference audio latent shape: {tuple(latents.shape)}")
189+
reference.num_audio_latents = int(latents.shape[1])
190+
rows.append(latents.reshape(-1, latent_channels).float().contiguous())
191+
del posterior, latents
192+
anchor = torch.cat(rows).float().contiguous()
193+
if latent_channels != MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH or anchor.shape[1] != latent_channels:
194+
raise ValueError(
195+
f"Ref2VA audio anchor width must be {MINIMAX_H3_REF2VA_AUDIO_ROW_WIDTH}, got {anchor.shape[1]}")
196+
del rows, audio_vae
197+
gc.collect()
198+
torch.cuda.empty_cache()
199+
print(f"Ref2VA audio anchor shape: {tuple(anchor.shape)}")
200+
return anchor
201+
202+
203+
def encode_ref2va_conditioning(
204+
caption: str,
205+
references: list[MiniMaxH3PreparedReference],
206+
model_path: Path,
207+
model_index: dict[str, Any],
208+
fastvideo_args: FastVideoArgs,
209+
) -> tuple[torch.Tensor, torch.Tensor]:
210+
"""Encode the exact ordered presentation without padding or truncation."""
211+
print("Loading MiniMax H3 tokenizer, processor, and Qwen3-VL encoder")
212+
tokenizer = _load_component("tokenizer", model_path, model_index, fastvideo_args)
213+
processor = _load_component("processor", model_path, model_index, fastvideo_args)
214+
conditioner = _load_component("text_encoder", model_path, model_index, fastvideo_args)
215+
stage = MiniMaxH3ConditioningStage(
216+
conditioner=conditioner,
217+
tokenizer=tokenizer,
218+
processor=processor,
219+
ref2va=bool(references),
220+
)
221+
batch = ForwardBatch(data_type="video", prompt=caption, references=references)
222+
if not references:
223+
# The Ref stage intentionally rejects an empty list. Prompt-only rows
224+
# use the exact T2VA tokenizer path and contain only text tags.
225+
batch.extra[MINIMAX_H3_KEYFRAMES_KEY] = []
226+
batch = stage.forward(batch, fastvideo_args)
227+
if len(batch.prompt_embeds) != 1:
228+
raise RuntimeError("MiniMax H3 conditioning must return exactly one embedding")
229+
text_embedding = batch.prompt_embeds[0].squeeze(0).float().cpu().contiguous()
230+
text_token_tags = batch.extra.get(MINIMAX_H3_TEXT_TOKEN_TAGS_KEY)
231+
if not isinstance(text_token_tags, torch.Tensor):
232+
raise RuntimeError("MiniMax H3 conditioning did not return text token tags")
233+
text_token_tags = text_token_tags.to(dtype=torch.long, device="cpu").contiguous()
234+
if text_embedding.ndim != 2 or text_embedding.shape[1] != 5120 or text_embedding.shape[0] == 0:
235+
raise ValueError(f"Unexpected Qwen embedding shape: {tuple(text_embedding.shape)}")
236+
if text_token_tags.shape != text_embedding.shape[:1]:
237+
raise ValueError("Qwen text token tags do not align with its hidden states")
238+
if not bool(((text_token_tags == 0) | (text_token_tags == 1)).all()):
239+
raise ValueError("Qwen text token tags may contain only vision=0 and text=1")
240+
241+
dynamic_length = int(text_embedding.shape[0])
242+
print(f"Qwen Ref2VA conditioning shape: {tuple(text_embedding.shape)}; preserving all {dynamic_length} tokens")
243+
del batch, stage, conditioner, processor, tokenizer
244+
gc.collect()
245+
torch.cuda.empty_cache()
246+
return text_embedding, text_token_tags
247+
248+
249+
def _serialize_float32_tensor(record: dict[str, Any], name: str, tensor: torch.Tensor) -> None:
250+
tensor = tensor.detach().float().cpu().contiguous()
251+
record[f"{name}_bytes"] = tensor.numpy().tobytes()
252+
record[f"{name}_shape"] = list(tensor.shape)
253+
record[f"{name}_dtype"] = "float32"
254+
255+
256+
def _canonical_prepared_references(references: list[MiniMaxH3PreparedReference]) -> list[dict[str, Any]]:
257+
canonical: list[dict[str, Any]] = []
258+
for reference in references:
259+
is_audio = reference.media_type == "audio"
260+
canonical.append({
261+
"media_type": reference.media_type,
262+
"has_audio": bool(reference.has_audio),
263+
# MiniMaxH3PreparedReference defaults num_latent_frames to 1, so
264+
# standalone audio must be canonicalized explicitly to zero visual
265+
# geometry rather than copying dataclass defaults.
266+
"num_latent_frames": 0 if is_audio else int(reference.num_latent_frames),
267+
"latent_height": 0 if is_audio else int(reference.latent_height),
268+
"latent_width": 0 if is_audio else int(reference.latent_width),
269+
"num_audio_latents": int(reference.num_audio_latents),
270+
})
271+
return canonical
272+
273+
274+
def build_ref2va_parquet_record(
275+
*,
276+
file_name: str,
277+
caption: str,
278+
video_latents: torch.Tensor,
279+
audio_latents: torch.Tensor,
280+
text_embedding: torch.Tensor,
281+
text_token_tags: torch.Tensor,
282+
ref_visual_anchor: torch.Tensor,
283+
ref_audio_anchor: torch.Tensor,
284+
references: list[MiniMaxH3PreparedReference],
285+
) -> dict[str, Any]:
286+
record = build_parquet_record(
287+
file_name=file_name,
288+
caption=caption,
289+
video_latents=video_latents,
290+
audio_latents=audio_latents,
291+
text_embedding=text_embedding,
292+
)
293+
record["schema_version"] = MINIMAX_H3_REF2VA_SCHEMA_VERSION
294+
record["text_token_tags"] = text_token_tags.to(dtype=torch.long, device="cpu").tolist()
295+
_serialize_float32_tensor(record, "ref_visual_anchor", ref_visual_anchor)
296+
_serialize_float32_tensor(record, "ref_audio_anchor", ref_audio_anchor)
297+
record["references"] = _canonical_prepared_references(references)
298+
return record
299+
300+
301+
def validate_record_contract(record: dict[str, Any]) -> None:
302+
missing = [name for name in pyarrow_schema_minimax_h3_ref2va.names if name not in record]
303+
if missing:
304+
raise ValueError(f"Ref2VA record is missing schema fields: {missing}")
305+
expected_target_shapes = {
306+
"vae_latent_shape": [24, video_latent_num_frames(NUM_FRAMES), VIDEO_HEIGHT // 16, VIDEO_WIDTH // 16],
307+
"audio_latent_shape": [2, 32, audio_latent_num_frames(NUM_FRAMES)],
308+
}
309+
for name, expected in expected_target_shapes.items():
310+
if record[name] != expected:
311+
raise ValueError(f"{name} must be {expected}, got {record[name]}")
312+
# Reuse the actual training collator as the authoritative nested-reference,
313+
# empty-anchor, dtype, row-count, and dynamic-text validation gate.
314+
collate_minimax_h3_ref2va_rows([record])
315+
316+
57317
def _prepare_references(references: tuple[MiniMaxH3RawReference, ...], ) -> list[MiniMaxH3PreparedReference]:
58318
raw_references = build_minimax_h3_references(references)
59319
prepared = [prepare_reference(reference, NUM_FRAMES, AUDIO_SAMPLE_RATE) for reference in raw_references]
@@ -118,6 +378,7 @@ def _build_record(
118378
fastvideo_args,
119379
)
120380
record = build_ref2va_parquet_record(
381+
file_name=sample.target_file,
121382
caption=sample.caption,
122383
video_latents=video_latents,
123384
audio_latents=audio_latents,
@@ -128,7 +389,6 @@ def _build_record(
128389
references=references,
129390
)
130391
record["id"] = sample.sample_id
131-
record["file_name"] = sample.target_file
132392
validate_record_contract(record)
133393
return record
134394

0 commit comments

Comments
 (0)