Skip to content

[perf]: MiniMax H3 on GB10 - load pipeline components on demand instead of all at once - #1761

Open
KyleNeverGivesUp wants to merge 10 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:lazy-module-load
Open

[perf]: MiniMax H3 on GB10 - load pipeline components on demand instead of all at once#1761
KyleNeverGivesUp wants to merge 10 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:lazy-module-load

Conversation

@KyleNeverGivesUp

@KyleNeverGivesUp KyleNeverGivesUp commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Problem

A pipeline materializes every component before the first stage runs, so peak memory is the sum of all components even though no two of them are needed at the same moment. For MiniMax-H3 r16 that sum is 96.3 GiB: text encoder 48.0, DiT 37.8, video VAE 9.7, audio VAE 0.6. For FastH3 it is 124.0 GiB.

The existing CPU offload flags cannot help, for two reasons. They act after loading has already finished, so they do nothing about the load-time peak. And on a unified-memory device such as GB10 there is no second pool to offload into, so moving weights to the host frees nothing.

A GB10 has 121 GiB. FastH3's components do not fit on it at all.

What this changes

Adds lazy_module_load, off by default. Heavy components become a LazyModule proxy that loads on first use. The pipeline derives, from what its stages actually hold, which stage is the last user of each component, and installs a release hook there. Peak becomes the largest overlapping set rather than the sum.

Measured

One GB10, 1 GPU, single prompt, 124 frames. Every number below is
torch.cuda.memory_allocated() sampled where a component is loaded or
released, so it measures component residency rather than the run's true peak,
which also includes activations.

MiniMax-H3 r16, 192x320, 4 steps

moment cuda allocated
after load, before stage 0 ~0
video VAE materialized 9.7 GiB
text encoder materialized 57.7 GiB, peak
text encoder released 9.7 GiB, freed 48.0
DiT materialized 47.5 GiB
audio VAE materialized 48.1 GiB
DiT and video VAE released 0.6 GiB
audio VAE released 0.03 GiB

Peak 57.7 GiB against 96.3 GiB resident without the flag. The same command without the flag was killed by the machine's out-of-memory daemon during the DiT load an hour earlier, at fsdp_load.py:584.

The decisive moment is two adjacent log lines, 4 ms apart:

Released deferred module text_encoder, cuda allocated 57.72 -> 9.74 GiB, freed 47.98 GiB
Loading deferred module transformer

The DiT starts loading from 9.7 GiB rather than from 58.3 GiB.

FastH3 Preview v0.2, VSA-H3 attention on the Triton kernel

resolution packed tokens resident components result
192x320 29,760 75.81 GiB completed, 525 s, video and audio written
384x672 124,992 75.81 GiB completed, 745 s, video and audio written
512x896 222,208 75.82 GiB completed, 930 s, video and audio written
640x1120 347,200 75.83 GiB killed during the first DiT forward
768x1344 499,968 75.83 GiB killed during the first DiT forward

Component residency does not move with resolution, because it is set by the
weights. What moves is activation memory, which this change does not touch.
The last two rows load every component and then die in
minimax_h3_denoising.py:190, so the ceiling on one GB10 sits between 222,208
and 347,200 packed tokens. That ceiling also reflects whatever else the shared
machine was running, since the out-of-memory daemon watches the host rather
than this process, so treat it as a lower bound.

DiT 35.05B parameters, 65.5 GiB. The four components sum to 124.0 GiB against the device's 121 GiB, so without the flag this model cannot load at all and there is no eager baseline to compare against.

Still applies to FastH3 Preview v1

FastVideo has since released FastH3 Preview v1. The tables above were measured on v0.2 and are left as recorded, but nothing this change depends on moved: the two checkpoints ship a byte-identical transformer/config.json, the same 35.05B parameters, and the same 688 student tensors, so the component sum that overflows a 121 GiB device is unchanged.

Measured with this flag on v1, using KyleNeverGivesUp/FastH3-4-step-Preview-v1-r16, which is v1 with the rank-reduced AdaLN converter from #1699 applied:

frames resolution packed tokens FP8 resident weights peak allocated time
124 768x1344 499,968 no 51.70 GiB 69.2 GiB 902 s
345 768x1344 1,403,136 yes 33.06 GiB 83.7 GiB 3096 s

Both peaks match the same conversion applied to v0.2 to the byte, which is what you would expect if peak is set by the architecture and the packed token count rather than by which training run produced the weights.

The 345-frame row is the longest MiniMax-H3 generates, 14.38 s of synchronized video and audio in one pass on a single GB10. It needs FP8 to fit, and FP8 only reaches the feed-forward stack after #1780.

Cost

A released component is read from disk again on the next generation. Within one generation nothing reloads, since the release point is the last stage that holds the component, not the last stage that uses it. Across generations the whole set reloads, which for this model is several minutes. That is why the flag is off by default and documented as something to enable only when the model does not otherwise fit.

Design notes

Four things here would fail silently if done the obvious way.

The release hook lives on PipelineStage.__call__, not in the pipeline's stage loop. Several pipelines override forward, and a loop-based hook would leave those pipelines deferring but never freeing, with no error.

The proxy forwards __class__. Callers branch on isinstance(module, FSDPModule) and similar. A proxy answering False would take the wrong branch and produce a wrong result rather than a crash. The cost is that an isinstance check materializes, which is the correct trade.

Self-returning methods hand back the proxy, not the component. Stages write self.vae = self.vae.to(device) in a dozen places. Returning the component there would replace the proxy with a strong reference the pipeline cannot release, and the run would look completely normal while freeing nothing.

add_stage rebuilds the schedule if it runs after the schedule was built. Every pipeline in the tree builds its stages inside create_pipeline_stages, but nothing enforced it. A stage appended later could hold a component an earlier stage had already been told to free.

Building the schedule also has to avoid isinstance on proxy values, since that would materialize every deferred component before the run starts. There is a test for exactly that.

Training keeps every component resident and warns if the flag is set. If no stage holds a deferred component the pipeline warns rather than silently doing nothing.

Scope

Deferral is opt-in per pipeline. _lazy_module_names is empty in the base class and MiniMax-H3 lists the four components measured above. Every other pipeline is unchanged, and setting the flag there logs a warning and does nothing.

The reason is that releasing a component and loading it again is only correct when nothing outside the loader has changed it, and two habits in this tree break that without raising:

  • Mutating a component after load. LongCatPipeline.initialize_pipeline turns on block-sparse attention and writes parameters into every transformer block. It runs once, so a re-materialized component silently comes back with the feature off.
  • Reading a component's attributes while stages are built. The shared DenoisingStage.__init__ derives the attention backend from transformer.hidden_size, which materializes the DiT during post_init and defeats the deferral it was meant to gain.

A pipeline opts in once someone has checked it against both. LTX2Pipeline, MagiHumanPipeline, and LingbotVideoPipeline additionally override load_modules without delegating to the base implementation, so they would need that too.

The flag lowers weight residency only. It does not change activation memory, so a resolution whose activations do not fit still does not fit. On one GB10, FastH3 runs at 512x896 and is killed at 640x1120, where q, k, v, and the VSA gate alone account for 18.5 GiB against roughly 30 GiB left after the weights. The repository's reference configuration is 768x1344 on four GPUs with sp_size=4, which splits those activations across ranks.

Relationship to the existing MPS special cases

denoising.py:653, decoding.py:343, and sr_denoising.py:279 already do a hard-coded version of this behind if torch.backends.mps.is_available(), with matching reload paths keyed on fastvideo_args.model_loaded. Apple Silicon is unified memory too, so that is the same problem found on a different device.

Those three differ from this change in two ways. They still load everything eagerly first, so they lower the steady-state footprint but not the load-time peak, which is where H3 on GB10 dies. And they never release the text encoder, which for H3 is the largest component at 48 GiB.

This change could subsume all three later. It does not touch them here.

Tests

31 cases in fastvideo/tests/stages/test_lazy_module_load.py, CPU only, covering the proxy contract, the release schedule, the identity rule for self-returning methods, the late-stage rebuild, and the enablement rules. Placed under stages/ because fastvideo/tests/loader/ is not collected by any CI lane on this branch's base.

lazy_module_load is registered in docs/design/inference_schema_parity_inventory.yaml and mapped in api/compat.py, so test_fastvideo_args_fields_are_classified stays green.

@mergify mergify Bot added type: perf Performance improvement scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build scope: docs Documentation labels Aug 26, 2026
@mergify

mergify Bot commented Aug 26, 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)\]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa30a9815b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fastvideo/pipelines/composed_pipeline_base.py Outdated
Comment thread fastvideo/pipelines/stages/base.py Outdated

@aryan5v aryan5v left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the lazy-load path, H3 opt-in, GPU-direct DiT iterator, and the Spark/offload docs against GB10 FastH3 behavior.

The LazyModule contract is in good shape: is_lazy_module avoids __class__ materialization, self-returning .to() keeps the proxy, compile is a materialize transform, LoRA config reads are deferred, nested-stage walk and failure-path release are covered, and the CPU tests hit the production load_modules / H3 stage constructors. The to_cpu=cpu_offload change in fsdp_load.py is the right GB10 load path.

Please fix the docs before merge. They currently describe always-on sequential H3 start (encode, drop the encoder, then load DiT/VAE, arch-config geometry, one prompt per worker). This PR is an opt-in lazy_module_load flag that defaults to off, reloads from disk on the next generate(), and materializes the video VAE on the first input-prep attribute read. Spark operators following those docs will still OOM.

Two residency issues on the motivating model, neither blocked by tests:

  1. MiniMaxH3InputPreparationStage reads vae.spatial_compression_ratio and vae.latent_channels in forward, so the 9.7 GiB video VAE loads before Qwen3-VL. That is the 57.7 GiB encode peak in the PR table. Those values are already on MiniMaxH3VideoVAEArchConfig / MiniMaxH3AudioVAEArchConfig and do not need the weights.
  2. MiniMaxH3VideoDecodingStage holds transformer only for patch_size, so the 65 GiB DiT stays resident through VAE decode (the 75.8 GiB FastH3 row). Stash patch_size on the batch during latent prep / denoise and drop the DiT before decode.

Nit: VSA prepare_for_compile graph sharing is a separate change. Fine if it is required for compile-across-reload, otherwise it is easier to review on its own.

encoder is still resident, the process is a typical `earlyoom` kill (Python is
preferred). The CUDA pipeline now encodes first, releases the encoder, then
loads DiT and VAEs onto the accelerator (`to_cpu` follows `cpu_offload`, which
is off here). See [Offloading](../../inference/offloading.md).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads as if CUDA H3 now sequential-loads by default. In this PR it does not: lazy_module_load defaults to False, and basic_fasth3.py only turns it on with --lazy-module-load.

On one GB10, FastH3 still loads encoder + DiT + VAEs together unless that flag is set, which is the earlyoom case this bullet describes.

Please state the flag, and either auto-enable it on unified memory or show --lazy-module-load in the Spark command. The GPU-direct to_cpu=cpu_offload sentence can stay.

Comment thread docs/inference/offloading.md Outdated
(spatial ratio, latent channels, audio sample rate) comes from the VAE arch
configs until those weights load. A later `generate()` on the same worker
currently re-enters conditioning after the encoder has been released; start a
new generator for a new prompt until prompt-cache reload exists.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph does not match the code in this PR.

  • Sequential encode-then-release is not default CUDA H3 behavior; it is --lazy-module-load.
  • Input-prep geometry is not taken from VAE arch configs here. The first input-prep access of spatial_compression_ratio / latent_channels materializes the full video VAE.
  • A later generate() on a lazy worker re-runs the loader after release(); it does not require a new process. The broken second-prompt behavior and the MLX phase-order sentence belong to a different design.

Please describe lazy_module_load only (the section below already does that) and drop this always-on sequential-start writeup.

help="load each heavy component on first use and free it after the last stage that "
"needs it, so peak memory is the largest overlapping set instead of the sum of every "
"component. Enable when the model does not fit at load time; costs a reload per "
"generation, so leave it off when it does fit")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default all on one GB10 cannot keep FastH3's four components resident (PR body: 124 GiB vs 121 GiB). Leaving this off means the Spark example still dies at load unless the user already knows the flag.

Either auto-enable on unified memory (same place disable_offload_on_unified_memory already runs) or default this on when num_gpus == 1 and document the reload cost. Off-by-default is reasonable on 4×GB200.

# Deferral is safe here: no stage reads a component's attributes while it
# is being constructed, and `initialize_pipeline` only inspects the
# schedulers, which are never deferred.
_lazy_module_names = ("text_encoder", "transformer", "vae", "audio_vae")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt-in list is right, but last-holder scheduling still overlaps more than H3 needs:

  • Input prep holds vae and reads two ints in forward, so the video VAE loads before the text encoder (your r16 table: 9.7 then 57.7 GiB peak).
  • Video decode holds transformer only for patch_size, so the DiT stays through VAE decode (FastH3 75.8 GiB resident).

If those scalars come from arch config / the batch instead of the live modules, encode peak drops the VAE and decode can free the DiT. Construction-time access is already tested; first-forward attribute reads are not.

@mergify

mergify Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@aryan5v

aryan5v commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Addressed the CHANGES_REQUESTED review on a branch you can merge into this PR: KyleNeverGivesUp#2 (aryan5v:aryan/1761-lazy-geometrylazy-module-load). I cannot push your fork (permissions.push: false).

What changed:

  • Input prep holds the checkpoint-updated VAE arch config, not the live VAE proxy, so the first spatial_compression_ratio / latent_channels read no longer materializes 9.7 GiB before Qwen.
  • Decode and latent packing read patch_size from pipeline_config.dit_config (overlayed from transformer/config.json in initialize_pipeline, no weights). Decode no longer holds the DiT, so last-holder scheduling drops it before VAE decode.
  • Docs now describe --lazy-module-load (opt-in / auto), not always-on sequential start. A later generate() reloads from disk.
  • Auto-enable: lazy_module_load=None turns on after the worker binds a unified-memory device. basic_fasth3.py / basic_minimax_h3_t2v.py also default it on when --num-gpus 1. --no-lazy-module-load keeps everything resident (reasonable on 4×GB200).

Spark2 GB10 check, alpine 768×1344×124, VSA-DataFree, seed 2026, --vsa-kernel triton --no-fa4 --no-warmup --repeats 1 --lazy-module-load, full VAE (not TAEH3):

  1. geometry from config: patch_size=(1, 2, 2) spatial_compression_ratio=16 latent_channels=24 — input prep 0.05 ms, no VAE weights
  2. Load Qwen 47.98 GiB → release
  3. Load DiT 66.12 GiB → denoise ~138 s → release DiT (freed 65.51 GiB)
  4. Then load VAE 10.34 GiB and decode
  5. Release VAE 9.71 GiB

E2E 399.3 s. Denoise matches the sequential-start full-VAE run (~138 vs 139 s). The extra wall time vs 336.1 s sequential is cold Qwen load inside generate() and first torch.compile of the VAE at decode, not a slower DiT.

CUDA TAEH3 is not in this follow-up; that is a separate PR against main.

@aryan5v

aryan5v commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

#1803 supersedes this PR for the remaining FastH3-on-Spark work.

Already on main

The GB10 “do not load encoder + DiT + VAE all at once” path landed in #1793 (h3_sequential_load, auto on unified memory, GPU-direct DiT load). That is the overlapping slice of this PR. GitHub marks this branch CONFLICTING against main because of that merge, not because the Spark-pair work failed.

Preserved from this PR

What survived is the behavior, not this branch’s LazyModule machinery:

  • Encode with Qwen3-VL, release the encoder, then load DiT / VAEs so a single 128 GB GB10 can start FastH3.
  • GPU-direct DiT safetensors when host offload is off (also in [perf] Sequential MiniMax H3 start with GPU-direct DiT load #1793).
  • Dual-Spark still needs that split: sequence parallel replicates the DiT (~66 GiB/node), so each box still cannot hold encoder + DiT + VAE together. On main that is --h3-sequential-load / auto, not --lazy-module-load.

Still only on this branch (not on main, not in the Spark-pair commit): generic LazyModule + release-after-last-stage, in-process reload for a second generate(), geometry from checkpoint JSON so decode does not hold a live DiT, compile/LoRA hooks around lazy reload, and test_lazy_module_load.py. Those need a fresh rebase onto main if they are still wanted; they should not merge through this conflicted stack.

What #1803 adds on top

#1803 is the follow-on, stacked on main + #1793, not on this lazy-load implementation:

  • Ray executor: driver-local set_log_queue / clear_log_queue (Queue is not picklable across nodes) and copy NCCL_* onto workers.
  • FastH3 --execution-backend {mp,ray} (auto-ray when RAY_ADDRESS is set).
  • Dual-Spark QSFP / Ray bring-up, cookbook recipe, YAML, env script.
  • Measured pair runs: SP=2 124f 292 s, 345f 587 s vs ~374–393 s on one Spark.

Please review / land Spark-pair on #1803. This PR should not merge as-is on top of #1793.

…last stage

A pipeline materializes every component before the first stage runs, so peak
memory is the sum of all components even though no two are needed at the same
moment. The CPU offload flags cannot help with this: they act after loading,
and on a unified-memory device moving weights to the host frees nothing because
it is the same pool.

Add lazy_module_load, off by default. Heavy components become a LazyModule
proxy that loads on first use, and the pipeline installs a release hook on the
last stage that holds each one. Peak becomes the largest overlapping set
instead of the sum.

Measured on MiniMax-H3 r16, 121 GiB GB10, 1 GPU, 192x320, 4 steps. Peak CUDA
allocated is 57.7 GiB, reached during conditioning where the text encoder and
video VAE overlap. The four deferred components account for 96.3 GiB together,
which is what stays resident without the flag: text encoder 48.0, DiT 37.8,
video VAE 9.7, audio VAE 0.8. Generation completed and wrote a video.

Details worth flagging:

The release hook lives on PipelineStage.__call__, not in the pipeline stage
loop, so pipelines that override forward still free.

The proxy forwards __class__, so isinstance stays honest. A proxy answering
False to isinstance(module, FSDPModule) would take the wrong branch silently.

Self-returning methods hand back the proxy rather than the component. Stages
write self.vae = self.vae.to(device) in a dozen places; returning the component
there would replace the proxy with a reference the pipeline cannot release, and
the run would look normal while freeing nothing.

Releasing is a latency cost, never a correctness one: a released component
reloads on next access. Training keeps everything resident and warns if the
flag is set. If no stage holds a deferred component the pipeline warns rather
than silently doing nothing.
The example builds its OffloadConfig directly rather than going through
FastVideoArgs.add_cli_args, so the new flag needs its own switch to be
reachable from the motivating command line.
Same reason as the MiniMax H3 T2V example: this script builds its OffloadConfig
directly rather than going through FastVideoArgs.add_cli_args, so the flag needs
its own switch here to be reachable. FastH3 is the case the flag exists for,
since its components sum past what a 121 GiB unified-memory device can hold.
The schedule maps each deferred component to the last stage that holds it,
derived once after create_pipeline_stages. Every pipeline in the tree builds
its stages there, but nothing enforced it. A stage appended afterwards could
hold a component an earlier stage had already been told to free, and would
then be handed a released component mid-run with no error.

add_stage now rebuilds the schedule and says so, turning an invariant nothing
checked into a visible self-correction.
…lure

Review of the first version found that the flag was unsafe as a global
default. Releasing a component and loading it again is only correct when
nothing outside the loader has changed it, and two habits in this tree break
that without raising:

  * mutating a component after load. LongCatPipeline.initialize_pipeline turns
    on block-sparse attention and writes parameters into every transformer
    block. That runs once, so a re-materialized component silently comes back
    with the feature off and the second generation quietly degrades.
  * reading a component's attributes while stages are built. The shared
    DenoisingStage.__init__ derives the attention backend from
    transformer.hidden_size, which materializes the DiT during post_init and
    defeats the deferral it was meant to gain.

_lazy_module_names is now empty in the base class, so an unchecked pipeline
gets no deferral and says so. MiniMax-H3 opts in to the four components this
PR measured, and nothing else changes behaviour.

Also from review:

Releasing on the failure path. A stage's hook frees only what that stage is
the last user of, and it ran only after a successful forward. Both the stage
and the whole run now release on the way out, so the retry a memory
constrained caller attempts does not start from a worse position than the
request that just failed. A failing release cannot replace the exception
being propagated.

Walking into nested stages. Cosmos25AutoDenoisingStage keeps the transformer
inside child stages, so a one-level scan called it unreferenced and never
freed it. The scan now recurses through stages and containers with cycle
protection.

Keeping the proxy when torch.compile is skipped. The FSDP check ran after the
proxy had already been replaced by the real module, so an FSDP-wrapped
component lost both the compile and its release hook.

Not attaching the activation trace to a deferred component, since the hook
manager pins every module it wraps.

test_parser.py asserted on a whole serialized config dict, so adding a field
to OffloadConfig broke it. Grepping the field name could not find that; only
running the suite could.
KyleNeverGivesUp and others added 5 commits August 31, 2026 21:59
The existing tests build stages and pipelines by hand, so they would stay
green through both defects review found: a load_modules that never reaches the
deferral, and a stage constructor that reads a component's attributes and
materializes it during post_init.

Three tests now run the production paths. Two call the real
ComposedPipelineBase.load_modules with the component loader stubbed and a
counter on it, asserting that only opted-in names become proxies and that the
loader is never asked for them. The third builds the real MiniMax-H3 stages
over tracked proxies and asserts nothing materialized.

The third one was mutation-checked: adding a single transformer.patch_size
read to MiniMaxH3DenoisingStage.__init__ makes it fail, which is the habit
that defeats deferral in the shared DenoisingStage today.
Register pipeline-level compilation as a lazy materialization transform so every freshly loaded component receives the same compile setup. Keep whole-module compilation behind the proxy, preserve release scheduling, and add regression coverage for conditional and whole-module compile paths.
Defer transformer config access until LoRA conversion is actually requested so base inference keeps lazy DiTs unloaded through conditioning. Initialize transformer bookkeeping per pipeline and cover deferred no-LoRA construction, on-demand exclusion setup, and instance isolation.
…re on

main defers the same four MiniMax-H3 components through h3_sequential_load,
which auto-arms on unified memory, and this branch defers them through
lazy_module_load. With both active they interleave over one set of modules:
the H3 override strips the denoise modules from the load list, calls the base
loader which wraps the text encoder in a proxy, then pops that encoder while
the release schedule still holds it.

lazy_module_load is off by default and has to be asked for, so when it is on
it takes ownership and the H3 sequential path stands down. Passing neither
leaves main's behaviour untouched.
@mergify mergify Bot removed the needs-rebase PR has merge conflicts label Sep 1, 2026
@mergify

mergify Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label Sep 1, 2026
@mergify

mergify Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase PR has merge conflicts scope: attention Attention backends (VSA, STA, Flash, etc.) scope: docs Documentation scope: inference Inference pipeline, serving, CLI 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.

3 participants