Repository navigation
feat: --flow-shift for the diffusion scheduler; validate the Wan 2.2 low-noise budget - #57
Conversation
…e low-noise budget --flow-shift (env DYN_TRTLLM_FLOW_SHIFT) rebuilds the loaded scheduler with a new flow_shift (or shift, for FlowMatchEulerDiscreteScheduler) after the pipeline loads. Unset keeps the checkpoint's scheduler config. Wan2.2-T2V-A14B-Diffusers ships a UniPC scheduler with flow_shift 3.0, which VisualGen uses as-is, while Wan's official A14B T2V config is 40 steps at sample_shift 12.0. At shift 3 the high-noise expert gets 16 of 50 steps; at 12 it gets 26 of 40. In a 12-prompt review on 1xB200 the official settings looked better in 10 of 12 clips (less oversaturated) at 163 s/clip vs 225 s. Also the two deferred #56 review nits: --cache-dit-low-noise-max-cached-steps rejects values below -1, and logs a warning when it cannot apply (non-cache_dit backend or a single-transformer pipeline). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
temirulan
left a comment
There was a problem hiding this comment.
Approving. The production target, UniPC on Wan2.2-T2V-A14B, works correctly. There is one latent bug on another scheduler type that's worth a quick follow-up (inline).
What I verified against TRT-LLM v1.3.0rc14
- Swapping the scheduler after load is safe. The Wan T2V and I2V pipelines only read
self.schedulerper request:set_timesteps,config.num_train_timestepsfor the expert boundary, and the denoise loop. cache_dit'ssplit_wan22_inference_steps()also readspipeline.schedulerat refresh time, so the per-expert step split follows the new shift. Warmup runs with the old scheduler, but the timesteps don't change compiled shapes, so that's fine. - The UniPC path rebuilds correctly.
type(s).from_config(dict(s.config), flow_shift=x)is what VisualGen itself uses to build the scheduler, and every other config key is kept. - The two #56 follow-ups are fixed: the budget must be
>= -1, and there's now a warning when the flag can't apply (wrong backend, or notransformer_2).
Finding (inline, should fix in a follow-up)
- On a
FlowMatchEulerDiscreteSchedulerwhose config also hasflow_shift, the PR overridesflow_shift. FlowMatchEuler ignores that key, soshiftstays the same and the log falsely reports the change. I reproduced it with diffusers. VisualGen's Wan loader explicitly expects such configs. The fix is to pick the key the scheduler's__init__accepts, or to check the result after the rebuild.
Nit
- The low-noise range check only runs on the cache_dit path. With another backend,
-5passes and only produces the "ignored" warning. That's harmless, so this is just a note.
On rollout: as the description says, the 10/12 quality result also depends on deepapi sending 40 steps and on retuning the cache args. Shift 12 with 50 steps and the budget-24 settings was not measured, so I wouldn't flip --flow-shift alone.
I couldn't run the tests locally because they need the dynamo.llm Rust extension. I'm relying on your 47/47 result from the #55 image.
| """Rebuild the pipeline's scheduler with a different flow-matching shift.""" | ||
| scheduler = pipeline.scheduler | ||
| config = dict(scheduler.config) | ||
| keys = [key for key in ("flow_shift", "shift") if key in config] |
There was a problem hiding this comment.
--flow-shift can silently do nothing on a FlowMatchEulerDiscreteScheduler whose config also has a flow_shift key.
diffusers keeps unexpected config attributes in scheduler.config; it logs "will be ignored" but still stores them. VisualGen's Wan loader expects FlowMatchEuler configs to carry a stray flow_shift: pipeline_wan.py:249-250 in rc14 copies it into shift. With that config, keys[0] is flow_shift, from_config ignores it again, and shift never changes. The info log still prints flow_shift 3.0 -> 12.0.
Repro with diffusers:
s = FlowMatchEulerDiscreteScheduler.from_config({"shift": 5.0, "flow_shift": 3.0})
config = dict(s.config) # still contains flow_shift
n = type(s).from_config(config, flow_shift=12.0)
n.config.shift # 5.0, unchangedThis doesn't affect the Wan2.2-T2V-A14B rollout, because UniPC takes flow_shift as a real init arg. Suggested fix: choose the key the scheduler's __init__ accepts ("flow_shift" in inspect.signature(type(scheduler).__init__).parameters, else "shift"). Alternatively, after the rebuild, assert that the new scheduler's config holds shift under the chosen key and raise if not. A test with both keys on a FlowMatchEuler-like fake would cover it.
There was a problem hiding this comment.
Fixed in 41cfbaf. I checked it with real diffusers. inspect.signature sees through @register_to_config, so FlowMatchEulerDiscreteScheduler exposes shift but not flow_shift. For a config with both keys, the code now picks shift, and the rebuilt scheduler has shift == 12.0. Upstream UniPCMultistepScheduler.__init__ takes flow_shift and no shift, so the A14B path picks flow_shift as before. The new test_stray_flow_shift_key_does_not_hide_the_real_shift covers this case.
diffusers keeps unknown config keys, and Wan's FlowMatchEuler configs carry
a stray flow_shift beside shift. Choosing the key by config presence made
--flow-shift override the ignored flow_shift and leave shift unchanged while
logging the change. Verified against real diffusers: FlowMatchEuler with
{shift: 5, flow_shift: 3} now ends at shift 12; UniPC unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The range check sat in the cache_dit-only setter, so another backend let -5 through with just the "ignored" warning. It now runs while building the VisualGen args, before the backend branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
temirulan
left a comment
There was a problem hiding this comment.
Round 2: still approving at 450251e. Both follow-ups look correct.
- 41cfbaf (flow-shift key choice): I checked it against real diffusers.
inspect.signature(type(s).__init__)sees through@register_to_config, so the code now finds the parameter each scheduler actually accepts. For FlowMatchEuler that isshift; for current upstream UniPC it isflow_shift. My repro, a FlowMatchEuler with bothshiftand a strayflow_shift, now ends up withshift == 12.0.config.get()in the log line also stops aKeyErrorwhen the key is accepted but missing from the config. - 450251e (budget check): the
>= -1check now runs at the top of_build_diffusion_args()for every cache backend. The parametrized test coverscache_ditandnone.
No new findings. I couldn't run the tests locally because they need the dynamo.llm Rust extension.
What
--flow-shift(envDYN_TRTLLM_FLOW_SHIFT): after the pipeline loads, rebuild its scheduler from the same config with the shift replaced, under whichever offlow_shift(UniPC) orshift(FlowMatchEulerDiscreteScheduler) the scheduler's__init__accepts. Choosing by__init__rather than by config presence matters because diffusers keeps unknown keys, and Wan's FlowMatchEuler configs carry an ignoredflow_shiftbesideshift. Unset keeps the checkpoint's scheduler config. A scheduler with neither key raises.--cache-dit-low-noise-max-cached-stepsrejects values below -1 on every cache backend (-1 = unlimited, as inCacheDiTConfig), and logs a warning when it cannot apply (backend is not cache_dit, or the pipeline has notransformer_2).Why
Wan2.2-T2V-A14B-Diffusersships a UniPC scheduler withflow_shift: 3.0, which VisualGen uses as-is. Wan's official A14B T2V config (wan_t2v_A14B.py) usessample_steps = 40,sample_shift = 12.0. The shift decides how the steps split across the two experts at boundary 0.875: production gives the high-noise expert 16 of 50 steps; the official settings give it 26 of 40.Measured on 1x B200 (1280x720, 81 frames, guidance 4.0/3.0, cache_dit, image from #55), 12 public prompts:
Side-by-side review: official looked better in 10 of 12 clips, generally much less oversaturated; two clips regressed (latte: an extra milk jug and a third hand; ballerina: an impossible leg movement). An uncached official render of those two is running to tell the settings from cache_dit's reuse (17 of 26 high-noise steps reused at shift 12).
Not enabled by this PR. Shipping the official settings also needs deepapi to send 40 steps (
Wan22T2VIn.num_inference_stepsis hard-coded to 50), and dropping the budget-24 / threshold-0.32 cache args.Tests
TestFlowShift: the shift key is replaced and the rest of the config kept (UniPCflow_shiftand FlowMatchEulershift); a FlowMatchEuler config with a strayflow_shiftstill getsshiftchanged; a scheduler without either raises; budget below -1 raises with cache_dit and with no cache backend (mutation-checked). 49/49 non-handler tests pass in the #56 image; against real diffusers, UniPC ends atflow_shift12 and FlowMatchEuler{shift: 5, flow_shift: 3}atshift12.🤖 Generated with Claude Code