Skip to content

fix(qwen-image): hold the dynamic shift at the end of its fit - #9892

Merged
joshistoast merged 5 commits into
invoke-ai:mainfrom
Pfannkuchensack:fix/qwen-image-shift-hold
Oct 19, 2026
Merged

joshistoast merged 5 commits into
invoke-ai:mainfrom
Pfannkuchensack:fix/qwen-image-shift-hold

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Qwen-Image sets its noise-schedule shift (mu) from the image's token count. It draws a line through two points from the scheduler config: 256 tokens give 0.5, and 8192 tokens (about 1448×1448 of area, 2.1 MP) give 0.9. Above 8192 tokens the line kept climbing.

Image mu before mu after 40-step img2img at strength 0.5 starts at sigma
1024² 0.69 0.69 unchanged
2048² 1.31 0.90 0.77 → 0.70
4096² 3.79 0.90 0.95 → 0.70

qwen_image_denoise now holds mu at the end of the fit, as the FLUX.1, Z-Image and LTX-2 fits already do (Krea-2 Raw: #9887).

  • The end of the fit comes from the checkpoint's own scheduler/scheduler_config.json.
  • An explicit shift (Lightning LoRAs) still wins.
  • Edit models share the node. Their reference tokens never counted toward mu, and still don't.

Why now: at 4096², an image-to-image pass at strength 0.5 starts from almost pure noise (sigma 0.95), so it replaces the image instead of refining it. The Upscale widget's Qwen-Image entry, which comes next, needs mid-range starts at 2048²–4096².

How other implementations handle this:

  • ComfyUI samples Qwen-Image at a constant mu of 1.15 (supported_models.py, QwenImage.sampling_settings).
  • diffusers QwenImagePipeline extrapolates like the old code.

Other changes:

  • The fit values are read as attributes of scheduler.config instead of .get(key, default). The scheduler registers every init argument, so those defaults never applied, and the max_shift default (0.9) did not match the scheduler's real one (1.15). The comment now names the real defaults.
  • The node goes from 1.0.0 to 1.0.1; its inputs are unchanged. The shift description now says where the hold starts.

Related Issues / Discussions

Same fix as #9887 (Krea-2 Raw) and the Z-Image shift hold. It is a prerequisite for Qwen-Image in the Upscale widget (follow-up to #9825 and #9889).

Qwen-Image-2.1 (#9784) has its own calculate_mu with the same fit, and it still extrapolates. Not touched here.

QA Instructions

Automated

  • pytest tests/app/invocations/test_qwen_image_denoise.py tests/app/invocations/test_qwen_image_packed_models.py: 34 passed.
    • The new tests run _run_diffusion with the real FlowMatchEulerDiscreteScheduler and read mu where it reaches set_timesteps. Expected values are hand-computed.
    • Stock config: 1024², 2048×1008 (8064 tokens, below the hold), 2048×1024, 2048² and 4096².
    • A checkpoint's own scheduler_config.json holds at its own end.
    • An explicit shift gives mu = ln(shift) at 4096².
    • Each of these breaks at least one test:
      • removing the hold;
      • hard-coding the hold at 8192 or at 0.9;
      • an off-by-one at the boundary;
      • counting tokens from the width only;
      • skipping the scheduler_config.json path.
  • ruff@0.11.2 check / format --check: pass. openapi.json and schema.ts are regenerated; only the field description and the version change.

Text-to-image A/B (RTX 4090, Qwen Image 2512 Q4_K_M GGUF, Qwen2.5-VL fp8 encoder, 2048², 40 steps, CFG 4, model cache emptied before each run, held and old mu interleaved per seed; the old mu is passed through shift = e^1.3129 = 3.717):

Seed Result Time per image
1234 Same composition, lettering, lighthouse and house; the waves differ in detail 413 s held / 409 s old
99 Same lighthouse and lettering; the keeper's house changes shape and position 411 s / 409 s
7 Near-identical 411 s / 411 s

No image shows doubled subjects, broken lettering or a damaged composition.

Review

No material findings. Resolved: the docs now say that Shift on this node is e^mu, not mu as on Krea-2, and which value gives back the old schedule.

Remaining: the low end still extrapolates below 256 tokens, as the FLUX.1, Z-Image and Krea-2 fits do.

Compatibility / Rollout

  • Same seed, different image: Qwen-Image and Qwen-Image-Edit images above about 1448×1448 of area (8192 tokens) no longer reproduce with the same seed. Image metadata does not record mu.
    • To get the old schedule back, set Shift on Denoise - Qwen Image in a workflow to e^(old mu): about 3.72 at 2048×2048, about 44 at 4096×4096.
    • Generate and Canvas don't send Shift.
  • Unchanged: images at or below that area, and any explicit shift.
  • Saved workflows: the 1.0.1 node has the same fields as 1.0.0, so older workflows still load.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Above the scheduler config's max_image_seq_len (about 1448x1448 for the stock config) mu stays at max_shift instead of extrapolating.
The Qwen-Image docs explain the hold and how Shift reproduces the previous schedule.
@github-actions github-actions Bot added python PRs that change python files invocations PRs that change invocations python-tests PRs that change python tests docs PRs that change docs labels Oct 16, 2026
@joshistoast joshistoast self-assigned this Oct 19, 2026
@joshistoast
joshistoast enabled auto-merge (squash) October 19, 2026 01:24
@joshistoast
joshistoast merged commit df9ebad into invoke-ai:main Oct 19, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs PRs that change docs invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants