Skip to content

[perf]: stop holding the whole checkpoint during DiT load, unblocking MiniMax H3 on one GB10 - #1714

Merged
SolitaryThinker merged 2 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:release-checkpoint-tensors-during-load
Aug 26, 2026
Merged

SolitaryThinker merged 2 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:release-checkpoint-tensors-during-load

Conversation

@KyleNeverGivesUp

@KyleNeverGivesUp KyleNeverGivesUp commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Purpose

load_model_from_full_model_state_dict retained every source tensor until the
entire model copy completed. For MiniMax-H3 this kept the checkpoint storage and
the progressively materialized DiT resident together, exceeding the practical
working set of a single 121 GiB GB10.

This change drains the converted state dictionary as parameters are consumed.
It is generic loader behavior: discrete-GPU systems reduce host-memory pressure,
while unified-memory systems reduce pressure on the shared physical pool.

Changes

  • Iterate over a snapshot of parameter names and pop each source tensor before
    copying it into the model.
  • Preserve mapping order, dtype selection, custom loader behavior, DTensor
    distribution, strict/non-strict handling, and unused-key accounting.
  • Add regression tests that retain an external reference to the internal state
    dictionary and prove that entries are released during the load, including
    continue paths and mixed-dtype placement.
  • Describe the lifetime reduction in storage-neutral terms. Inputs may be views
    backed by safetensors mappings or ordinary allocated tensors; the optimization
    is valid for both.

Dependency

This PR introduces fastvideo/tests/loader/. Merged PR #1710 owns the shared
unit-lane collection entry for that directory, so this branch is rebased onto
its merge commit rather than duplicating the CI wiring. Final head:
54c7652b3245bb5add9ee950949f8dd9a57457b8.

Verification

fastvideo/tests/loader/test_fsdp_load_releases_checkpoint.py: 5 passed
tests/local_tests/models/test_fsdp_load_mixed_dtype.py: 3 passed, 1 skipped
explicit GB10 nested-FSDP mixed-dtype case: 1 passed
focused stack on merged #1710: 21 passed, 1 skipped
equivalent exact unit lane with #1710: 962 passed, 7 skipped
pre-commit: passed for every applicable changed path

The original GB10 measurement remains representative of the motivating working
set: draining the dictionary allowed all 20.17B transformer parameters to load,
where retaining the source set repeatedly exhausted memory partway through the
copy. This PR treats that as memory-lifecycle evidence, not as a performance or
quality baseline.

Checklist

  • Tests cover source release and parameter placement
  • Applicable pre-commit hooks pass
  • Memory impact was reviewed on GB10
  • No model weights, inference numerics, or output-quality policy changes

@mergify mergify Bot added type: perf Performance improvement scope: infra CI, tests, Docker, build scope: model Model architecture (DiTs, encoders, VAEs) labels Aug 17, 2026
@mergify

mergify Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI

Protection Waiting on
🔴 PR merge requirements 👀 reviews and 🤖 CI

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=full-suite-passed
  • check-success=fastcheck-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@KyleNeverGivesUp KyleNeverGivesUp changed the title [perf]: release checkpoint tensors as the loader copies them [perf]: stop holding the whole checkpoint during DiT load, unblocking MiniMax H3 on one GB10 Aug 17, 2026
KyleNeverGivesUp added a commit to KyleNeverGivesUp/FastVideo that referenced this pull request Aug 18, 2026
…ader's

Every offload flag moves weights to host memory so the device can drop them.
That trade only pays when the two are separate pools. On a unified-memory device
they are one, so the move frees nothing, the copy is a pure loss, and the peak
becomes the sum instead of the max.

hao-ai-lab#1710 added `Platform.has_unified_memory()` and used it at one site: the text
encoder's FSDP offload during loading. That is not enough, because a single flag
acts in more than one place. `text_encoder_cpu_offload` does three things:

  component_loader.py:353   picks the load-time target device, so the model is
                            placed on the host
  component_loader.py:407   gates the FSDP offload path, which hao-ai-lab#1710 covers
  minimax_h3_conditioning.py:292-302
                            gates a `.to(device)` before the conditioning
                            forward and a `.to("cpu")` after it

Gating only the loader leaves the model on the host and moves the copy to
inference time. On a DGX Spark loading MiniMax H3 that is a 48 GB move in the
middle of generation with about 11 GiB free, and the worker is killed.

Decide it once, in `check_fastvideo_args`, so every call site sees the same
answer. `UNIFIED_MEMORY_OFFLOAD_FLAGS` names the five flags and a test asserts
the tuple still matches the dataclass, because missing one is silent: the run
works and quietly pays twice, which is exactly how the text encoder survived the
first pass at this.

`use_fsdp_inference` is deliberately left alone. Sharding across ranks is a
separate decision from where the weights live, and a unified-memory host can
have more than one of these devices. MPS keeps its own branch, which does
disable it, and reaches this one through `elif`.

hao-ai-lab#1710's loader guard is not made redundant. `TextEncoderLoader.load` takes an
explicit `cpu_offload` argument that bypasses the args-level decision, and that
path still needs it.

Measured on a DGX Spark, GB10, 121 GiB unified memory, one GPU, with hao-ai-lab#1710,
hao-ai-lab#1711 and hao-ai-lab#1714 also applied. Before this change the conditioning stage is
terminated 1.5 s in, every time, with nothing reported. After it, the run gets
through input preparation, conditioning, latent preparation and the full
denoising loop, and is terminated in the video decode instead.

Getting past that last step also needs the video VAE decoder in fp16 rather than
the fp32 it is pinned to at `models/vaes/minimax_h3_video.py:565`. With that as a
local patch on top, MiniMax H3 completes a text to video generation on one GB10:
320 by 192, 124 frames, video and audio, 11.69 s. It could not load at all before
this series. The VAE change is not in this PR because the encode path has to stay
fp32 for FL2VA and Ref2VA, so it needs its own design and its own measurements.

fastvideo/tests/platforms/test_unified_memory_offload.py: 11 passed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SolitaryThinker
SolitaryThinker force-pushed the release-checkpoint-tensors-during-load branch 2 times, most recently from 281245b to c57c3b1 Compare August 23, 2026 22:55
@KyleNeverGivesUp
KyleNeverGivesUp force-pushed the release-checkpoint-tensors-during-load branch from c57c3b1 to 93bcee6 Compare August 26, 2026 10:29
KyleNeverGivesUp and others added 2 commits August 26, 2026 12:10
`load_model_from_full_model_state_dict` holds the whole checkpoint alive while
it copies it, so peak memory during a load is checkpoint plus model rather than
just model.

`hf_to_custom_state_dict` drains the weight iterator into one dict before a
single parameter is placed, and every value in that dict is a zero-copy view
into a mmap'd safetensors shard: `weight_utils.py:177-180` opens each shard with
`safe_open` on device `cpu` and hands out `f.get_tensor` views. The dict is
therefore holding the file, not 639 small objects. As the loop touches each
tensor the pages it faults in stay referenced for the rest of the load and the
kernel cannot reclaim them, while the copy allocates the same bytes again on the
device.

Drain the dict instead of iterating it. The loop reads each entry once and
`custom_param_sd` is not used after it, so nothing needs it to stay whole. `pop`
rather than a trailing `del` because the loop continues in two places and the
release has to happen on every path.

Measured on a DGX Spark, GB10, 121 GiB unified memory, loading the transformer
of noctuashap/MiniMax-H3-pruned-r16: 20.17 B parameters, 639 tensors, 37.6 GiB.
Logging `torch.cuda.memory_allocated`, `VmRSS` and `MemAvailable` every ten
parameters:

  before   20.8 GiB of parameters placed cost 41.9 GiB of MemAvailable, 2.01x,
           and the worker was killed at about 380 of 639
  after    37.1 GiB of parameters placed cost 39.5 GiB of MemAvailable, 1.06x,
           and the load completed

`VmRSS` shows it directly. Before it climbs monotonically with the device
allocation and never comes down. After it stays between 61.4 and 64.5 GiB for
the whole load, dropping back each time a shard's mappings are released.

Load time is unchanged: 0.38 s per parameter before, 0.37 s after. The bytes are
still read off disk, only the retention goes away.

Not device specific and not model specific. On a discrete GPU the duplicate is
host RAM rather than one shared pool, so the win is a halved host-side peak
instead of the run completing.

fastvideo/tests/loader/test_fsdp_load_releases_checkpoint.py: 3 passed, and
reverting this change fails two of them.
@SolitaryThinker
SolitaryThinker force-pushed the release-checkpoint-tensors-during-load branch from 93bcee6 to 54c7652 Compare August 26, 2026 19:11
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

/merge

@github-actions github-actions Bot added the ready PR is ready to merge label Aug 26, 2026
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

/test full

@SolitaryThinker
SolitaryThinker merged commit 9bfa585 into hao-ai-lab:main Aug 26, 2026
31 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready PR is ready to merge scope: infra CI, tests, Docker, build scope: model Model architecture (DiTs, encoders, VAEs) type: perf Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants