Skip to content

Commit 4a5d5ce

Browse files
committed
[bugfix]: fix lazy-load, LoRA, and Ray NIC issues from the #1803 review
Keep deferral, compile, and later generate() from fighting each other, stop LoRA bookkeeping from pinning a released DiT, and leave per-node NCCL/Gloo interface names alone.
1 parent eba7e02 commit 4a5d5ce

17 files changed

Lines changed: 506 additions & 225 deletions

File tree

docs/getting_started/installation/spark_pair.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ sm_100a VSA kernel is not on this chip, so denoise is slower than a GB200
191191
| `RayDistributedExecutor` TypeError / abstract `set_log_queue` | Use a FastVideo build that implements those methods on the Ray executor (this page). |
192192
| Worker SIGTERM during DiT shard 11/14 | `RAY_memory_monitor_refresh_ms=0` **before** `ray start`. Do not leave Ray's default 30% object store. |
193193
| NCCL hangs or uses Wi-Fi | `source spark_pair_env.sh`. Confirm `NCCL_SOCKET_IFNAME` is the QSFP NIC. |
194-
| Gloo `connectFullMesh` / `remote=[127.0.0.1]` | Two 1-GPU nodes must not use loopback as the Gloo store. Source `spark_pair_env.sh` so `GLOO_SOCKET_IFNAME` is the QSFP NIC. Use a FastVideo build that keys loopback on unique worker IPs. |
194+
| Gloo `connectFullMesh` / `remote=[127.0.0.1]` | Two 1-GPU nodes must not use loopback as the Gloo store. Source `spark_pair_env.sh` so `GLOO_SOCKET_IFNAME` is the QSFP NIC on **each** box. FastVideo no longer copies that NIC name from the driver onto workers. |
195195
| Second `generate()` crashes `NoneType.parameters` | Sequential load used to drop the text encoder without reloading it. This branch reloads Qwen for later requests so `--warmup --repeats N` works. |
196196
| OOM / `earlyoom` prefers Python | Lazy module load must stay on (do not pass `--no-lazy-module-load`). Peak GPU during 345-frame denoise is ~90 GiB/node. |
197197
| `num_gpus=2` on one Spark | Each Spark has one GPU. Use Ray across two nodes, or `num_gpus=1` on one box. |

docs/inference/offloading.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ when it enables block-sparse attention, and reading a component's attributes
173173
while stages are built, as the shared denoising stage does to pick an attention
174174
backend. A pipeline therefore lists the components it has checked in
175175
`_lazy_module_names`, which is empty in the base class. MiniMax-H3 opts in. On
176-
a pipeline that has not, the flag logs a warning and changes nothing.
176+
a pipeline that has not, the flag is a no-op: hooks are not installed and no
177+
warning is logged. Sequential MiniMax-H3 (`h3_sequential_load`) reloads the
178+
text encoder for a later `generate()` on the same worker; you do not need to
179+
start a new generator.
177180

178181
## General Recommendations
179182

examples/inference/basic/basic_fasth3.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def build_parser(description: str | None = None) -> argparse.ArgumentParser:
5353
default=None,
5454
help="load each heavy component on first use and free it after the last stage that "
5555
"needs it, so peak memory is the largest overlapping set instead of the sum of every "
56-
"component. Default: on when --num-gpus is 1; FastVideo also auto-enables on unified "
57-
"memory. Costs a reload per generation; pass --no-lazy-module-load to keep every "
56+
"component. Omit for auto (on for unified-memory devices such as GB10; off on discrete "
57+
"GPUs). Costs a reload per generation; pass --no-lazy-module-load to keep every "
5858
"component resident")
5959
parser.add_argument("--profile",
6060
choices=("all", "strict"),
@@ -282,8 +282,7 @@ def build_generator_config(args: argparse.Namespace) -> GeneratorConfig:
282282
text_encoder=True,
283283
vae=True,
284284
pin_cpu_memory=args.pin_cpu_memory,
285-
lazy_module_load=(True if args.lazy_module_load is None and args.num_gpus == 1 else
286-
args.lazy_module_load),
285+
lazy_module_load=args.lazy_module_load,
287286
),
288287
compile=CompileConfig(
289288
enabled=args.torch_compile,

examples/inference/basic/basic_minimax_h3_t2v.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def parse_args() -> argparse.Namespace:
6666
default=None,
6767
help="load each heavy component on first use and free it after the last stage that "
6868
"needs it, so peak memory is the largest overlapping set instead of the sum of every "
69-
"component. Default: on when --num-gpus is 1; FastVideo also auto-enables on unified "
70-
"memory. Costs a reload per generation; pass --no-lazy-module-load to keep every "
69+
"component. Omit for auto (on for unified-memory devices such as GB10; off on discrete "
70+
"GPUs). Costs a reload per generation; pass --no-lazy-module-load to keep every "
7171
"component resident")
7272
parser.add_argument("--repeats",
7373
type=int,
@@ -104,8 +104,7 @@ def main() -> None:
104104
text_encoder=True,
105105
vae=True,
106106
pin_cpu_memory=False,
107-
lazy_module_load=(True if args.lazy_module_load is None and args.num_gpus == 1 else
108-
args.lazy_module_load),
107+
lazy_module_load=args.lazy_module_load,
109108
),
110109
compile=CompileConfig(
111110
enabled=args.torch_compile,

fastvideo/attention/backends/video_sparse_attn_h3.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,6 @@ def __init__(
452452
# request-time env/probe/fallback behavior; only Dynamo capture reads
453453
# the prepared, static route.
454454
self._regional_compile_sm100a_enabled: bool | None = None
455-
self._regional_compile_layer_idx: torch.Tensor | None = None
456455

457456
def prepare_for_compile(self, device: torch.device) -> None:
458457
"""Tensorize per-layer state shared by every torch.compile route."""
@@ -468,7 +467,8 @@ def prepare_for_regional_compile(self, device: torch.device) -> str | None:
468467
the loaded model's device now, then let ``forward`` specialize on the
469468
resulting plain bool while Dynamo is compiling.
470469
"""
471-
self.prepare_for_compile(device)
470+
if self._compile_layer_idx is None:
471+
self.prepare_for_compile(device)
472472
requested = os.environ.get(VSA_SM100A_ENV, "0") == "1"
473473
enabled = False
474474
reason = None if requested else f"{VSA_SM100A_ENV}=1 is required for compile-safe VSA-H3 attention"
@@ -495,10 +495,6 @@ def prepare_for_regional_compile(self, device: torch.device) -> str | None:
495495
enabled = reason is None
496496

497497
self._regional_compile_sm100a_enabled = enabled
498-
# Keep this marker unset when preparation fails. Generic/training
499-
# torch.compile must retain the established Triton attention route.
500-
self._regional_compile_layer_idx = (torch.tensor(self.layer_idx, device=device, dtype=torch.int64)
501-
if enabled else None)
502498
if enabled:
503499
route = ("native fastvideo-kernel mask entry" if callable(
504500
getattr(_sm100a, "block_sparse_attn_sm100a_from_mask", None)) else
@@ -524,7 +520,7 @@ def tile(self, x: torch.Tensor, attn_metadata: MiniMaxH3VSAMetadata) -> torch.Te
524520
n_tiles = attn_metadata.variable_block_sizes.numel()
525521
grad_mode = torch.is_grad_enabled() and x.requires_grad
526522
compiling = torch.compiler.is_compiling()
527-
regional_compiling = compiling and self._regional_compile_layer_idx is not None
523+
regional_compiling = compiling and self._regional_compile_sm100a_enabled is True
528524
if regional_compiling:
529525
sm100a_requested = bool(self._regional_compile_sm100a_enabled)
530526
elif compiling:
@@ -570,7 +566,7 @@ def forward( # type: ignore[override]
570566
attn_metadata: MiniMaxH3VSAMetadata,
571567
) -> torch.Tensor:
572568
compiling = torch.compiler.is_compiling()
573-
regional_compiling = compiling and self._regional_compile_layer_idx is not None
569+
regional_compiling = compiling and self._regional_compile_sm100a_enabled is True
574570

575571
tile_elems = attn_metadata.tile_elems
576572
if regional_compiling and tile_elems != 64:

fastvideo/pipelines/basic/minimax_h3/minimax_h3_pipeline.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,26 @@ def _apply_h3_checkpoint_arch_configs(model_path: str, fastvideo_args: FastVideo
6363
extra_config_module_map: dict[str, str]) -> None:
6464
"""Overlay checkpoint config.json onto pipeline configs without loading weights."""
6565
root = Path(model_path)
66-
vae_dir = root / "vae"
66+
vae_dir = root / extra_config_module_map.get("vae", "vae")
6767
if (vae_dir / "config.json").is_file():
6868
fastvideo_args.pipeline_config.vae_config.update_model_arch(get_diffusers_config(str(vae_dir)))
69+
audio_vae_dir = root / extra_config_module_map.get("audio_vae", "audio_vae")
70+
audio_vae_config = getattr(fastvideo_args.pipeline_config, "audio_vae_config", None)
71+
if audio_vae_config is not None and (audio_vae_dir / "config.json").is_file():
72+
audio_vae_config.update_model_arch(get_diffusers_config(str(audio_vae_dir)))
6973
transformer_dir = root / extra_config_module_map.get("transformer", "transformer")
7074
if (transformer_dir / "config.json").is_file():
7175
fastvideo_args.pipeline_config.dit_config.update_model_arch(get_diffusers_config(str(transformer_dir)))
72-
logger.info(
73-
"MiniMax-H3 geometry from config: patch_size=%s spatial_compression_ratio=%s latent_channels=%s",
74-
tuple(fastvideo_args.pipeline_config.dit_config.patch_size),
75-
int(fastvideo_args.pipeline_config.vae_config.arch_config.spatial_compression_ratio),
76-
int(fastvideo_args.pipeline_config.vae_config.arch_config.latent_channels),
77-
)
76+
dit_config = fastvideo_args.pipeline_config.dit_config
77+
vae_arch = getattr(fastvideo_args.pipeline_config.vae_config, "arch_config", None)
78+
patch_size = getattr(dit_config, "patch_size", None)
79+
if patch_size is not None and vae_arch is not None:
80+
logger.info(
81+
"MiniMax-H3 geometry from config: patch_size=%s spatial_compression_ratio=%s latent_channels=%s",
82+
tuple(patch_size),
83+
int(getattr(vae_arch, "spatial_compression_ratio", 0)),
84+
int(getattr(vae_arch, "latent_channels", 0)),
85+
)
7886

7987

8088
class MiniMaxH3BasePipeline(LoRAPipeline, ComposedPipelineBase):
@@ -195,6 +203,7 @@ def _load_denoise_modules(self, fastvideo_args: FastVideoArgs) -> None:
195203
loaded = super().load_modules(fastvideo_args, loaded_modules=self.modules)
196204
for name, module in loaded.items():
197205
self.add_module(name, module)
206+
self._apply_inference_compile(tuple(name for name in loaded if name in _DENOISE_MODULE_NAMES))
198207
finally:
199208
self._required_config_modules = saved
200209

@@ -226,6 +235,7 @@ def _ensure_text_encoder(self, fastvideo_args: FastVideoArgs) -> None:
226235
loaded = super().load_modules(fastvideo_args, loaded_modules=self.modules)
227236
for name, module in loaded.items():
228237
self.add_module(name, module)
238+
self._apply_inference_compile(("text_encoder", ))
229239
finally:
230240
self._required_config_modules = saved
231241
if stage is not None:
@@ -254,17 +264,20 @@ def _input_video_geometry(self, fastvideo_args: FastVideoArgs) -> Any:
254264
return arch
255265
return _default_video_geometry()
256266

257-
def _input_audio_vae(self, *, ref2va: bool) -> Any | None:
267+
def _input_audio_vae(self, fastvideo_args: FastVideoArgs, *, ref2va: bool) -> Any | None:
258268
if not ref2va:
259269
return None
270+
arch = getattr(getattr(fastvideo_args.pipeline_config, "audio_vae_config", None), "arch_config", None)
271+
if arch is not None:
272+
return arch
260273
return _default_audio_geometry()
261274

262275
def _add_condition_stages(self, fastvideo_args: FastVideoArgs, *, ref2va: bool) -> None:
263276
self.add_stage(
264277
"input_preparation_stage",
265278
MiniMaxH3InputPreparationStage(
266279
vae=self._input_video_geometry(fastvideo_args),
267-
audio_vae=self._input_audio_vae(ref2va=ref2va),
280+
audio_vae=self._input_audio_vae(fastvideo_args, ref2va=ref2va),
268281
ref2va=ref2va,
269282
),
270283
)
@@ -317,12 +330,23 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
317330
if not self.post_init_called:
318331
self.post_init()
319332

320-
self._ensure_text_encoder(fastvideo_args)
321-
if self._denoise_stages_ready:
322-
logger.info("Running MiniMax-H3 condition stages before denoise (subsequent request)")
323-
else:
324-
logger.info("Running MiniMax-H3 condition stages before loading DiT/VAE weights")
325-
return self._run_condition_then_denoise(batch, fastvideo_args)
333+
# Sequential encode-then-release is the H3-only fallback. Lazy and the
334+
# fully-resident discrete-GPU path both keep a complete stage list and
335+
# must use the base forward so abort cleanup and text_encoder_cpu_offload
336+
# still apply. Releasing Qwen on every request was re-reading it from disk
337+
# when neither deferral flag was on.
338+
if self._defer_denoise_modules(fastvideo_args):
339+
try:
340+
self._ensure_text_encoder(fastvideo_args)
341+
if self._denoise_stages_ready:
342+
logger.info("Running MiniMax-H3 condition stages before denoise (subsequent request)")
343+
else:
344+
logger.info("Running MiniMax-H3 condition stages before loading DiT/VAE weights")
345+
return self._run_condition_then_denoise(batch, fastvideo_args)
346+
except BaseException:
347+
self._release_all_lazy_modules()
348+
raise
349+
return super().forward(batch, fastvideo_args)
326350

327351

328352
class MiniMaxH3Pipeline(MiniMaxH3BasePipeline):

fastvideo/pipelines/basic/minimax_h3/packing.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,36 @@
3838
MINIMAX_H3_KEYFRAME_NOISE_AUG = 0.999
3939
MINIMAX_H3_KEYFRAME_ENCODE_SEED = 42
4040

41+
_PATCH_SIZE_CACHE: dict[int, tuple[int, int, int]] = {}
42+
4143

4244
def h3_dit_patch_size(fastvideo_args: Any) -> tuple[int, int, int]:
4345
"""Read DiT patch size from pipeline config, not live transformer weights."""
4446
dit_config = getattr(getattr(fastvideo_args, "pipeline_config", None), "dit_config", None)
47+
cached = _PATCH_SIZE_CACHE.get(id(dit_config)) if dit_config is not None else None
48+
if cached is not None:
49+
return cached
4550
patch_size = getattr(dit_config, "patch_size", None)
4651
if patch_size is None:
4752
raise ValueError("MiniMax-H3 requires pipeline_config.dit_config.patch_size.")
48-
values = tuple(int(axis) for axis in patch_size)
49-
if len(values) != 3 or min(values) <= 0:
53+
axes = tuple(int(axis) for axis in patch_size)
54+
if len(axes) != 3 or min(axes) <= 0:
5055
raise ValueError(f"MiniMax-H3 patch_size must be three positive ints, got {patch_size!r}.")
56+
values = (axes[0], axes[1], axes[2])
57+
if dit_config is not None:
58+
_PATCH_SIZE_CACHE[id(dit_config)] = values
5159
return values
5260

5361

62+
def h3_latent_channels(model_config: Any, name: str) -> int:
63+
"""Read VAE latent width from arch config, not a live VAE proxy."""
64+
arch = getattr(model_config, "arch_config", None)
65+
value = getattr(arch, "latent_channels", None)
66+
if value is None:
67+
raise ValueError(f"MiniMax-H3 requires {name}.arch_config.latent_channels")
68+
return int(value)
69+
70+
5471
MINIMAX_H3_ROPE_FRAME_RESCALE = 5.0 / 3.0
5572
MINIMAX_H3_ROPE_FRAMES_PER_LATENT = (1, 4, 4, 4, 4)
5673
_ROPE_SPATIAL_SCALE = 32

fastvideo/pipelines/basic/minimax_h3/stages/minimax_h3_latent_preparation.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
build_packed_sequence,
2323
build_ref2va_packed_sequence,
2424
h3_dit_patch_size,
25+
h3_latent_channels,
2526
keyframe_condition_noise,
2627
patchify_video_latents,
2728
)
@@ -59,6 +60,17 @@ def _sample_visual_posterior(posterior: Any) -> torch.Tensor:
5960
return posterior.sample(generator=generator)
6061

6162

63+
def _video_latent_channels(fastvideo_args: FastVideoArgs) -> int:
64+
return h3_latent_channels(fastvideo_args.pipeline_config.vae_config, "vae_config")
65+
66+
67+
def _audio_latent_channels(fastvideo_args: FastVideoArgs) -> int:
68+
return h3_latent_channels(
69+
getattr(fastvideo_args.pipeline_config, "audio_vae_config", None),
70+
"audio_vae_config",
71+
)
72+
73+
6274
class MiniMaxH3LatentPreparationStage(PipelineStage):
6375
"""Encode fixed conditions, build the row layout, then draw target noise."""
6476

@@ -193,7 +205,7 @@ def _encode_fl2va_conditions(
193205
noise = keyframe_condition_noise(
194206
shapes,
195207
h3_dit_patch_size(fastvideo_args),
196-
self.vae.latent_channels,
208+
_video_latent_channels(fastvideo_args),
197209
generator=batch.generator,
198210
device=device,
199211
)
@@ -241,7 +253,7 @@ def _encode_ref2va_conditions(
241253
noise = keyframe_condition_noise(
242254
shapes,
243255
h3_dit_patch_size(fastvideo_args),
244-
self.vae.latent_channels,
256+
_video_latent_channels(fastvideo_args),
245257
generator=batch.generator,
246258
device=device,
247259
)
@@ -317,10 +329,11 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
317329
h3_dit_patch_size(fastvideo_args))
318330

319331
num_audio_latents = layout.num_audio_latents
320-
expected_audio_shape = (MINIMAX_H3_AUDIO_CHANNELS, self.audio_vae.latent_channels, num_audio_latents)
332+
audio_channels = _audio_latent_channels(fastvideo_args)
333+
expected_audio_shape = (MINIMAX_H3_AUDIO_CHANNELS, audio_channels, num_audio_latents)
321334
if audio_noise is None:
322335
audio_rows = randn_tensor(
323-
(num_audio_latents * MINIMAX_H3_AUDIO_CHANNELS, self.audio_vae.latent_channels),
336+
(num_audio_latents * MINIMAX_H3_AUDIO_CHANNELS, audio_channels),
324337
generator=batch.generator,
325338
device=device,
326339
dtype=torch.float32,
@@ -329,9 +342,7 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
329342
if tuple(audio_noise.shape) != expected_audio_shape:
330343
raise ValueError(f"MiniMax-H3 injected audio latents must have shape {expected_audio_shape}, "
331344
f"got {tuple(audio_noise.shape)}.")
332-
audio_rows = audio_noise.to(device=device,
333-
dtype=torch.float32).permute(0, 2,
334-
1).reshape(-1, self.audio_vae.latent_channels)
345+
audio_rows = audio_noise.to(device=device, dtype=torch.float32).permute(0, 2, 1).reshape(-1, audio_channels)
335346

336347
if condition_video is not None:
337348
video_rows = torch.cat((condition_video.to(device), video_rows))

0 commit comments

Comments
 (0)