fix(flux2): stop the sampler from advancing reference image tokens - #9510
Open
Pfannkuchensack wants to merge 4 commits into
Open
fix(flux2): stop the sampler from advancing reference image tokens#9510Pfannkuchensack wants to merge 4 commits into
Pfannkuchensack wants to merge 4 commits into
Conversation
Reference image latents were concatenated onto the sampled tensor once before the denoising loop, and the model prediction was never sliced back to the generated tokens. Every sampler step therefore integrated the reference tokens along the model's velocity field, so the reference drifted away from the encoded image over the schedule and dragged the generated image with it - reported as a reproducible ~20% spatial shift of the edited result, independent of the prompt. Build the model input per forward pass instead and slice both the positive and the negative prediction to the generated sequence length, matching the FLUX.1 Kontext path and diffusers' Flux2KleinPipeline. The latents that the sampler advances now contain only generated tokens, which also removes the split/recombine dances around the inpaint merge and the step previews. Adds a regression test that asserts the reference part of the model input is bit-identical to the encoded latents at every step, across both the scheduler and the manual Euler path, with and without CFG.
Pfannkuchensack
requested review from
JPPhoto,
blessedcoolant,
dunkeroni and
lstein
as code owners
August 15, 2026 18:54
|
Ahhh, I guess this probably explains why reference likeness in Klein was better preserved with fewer steps. Great find! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fix(flux2): stop the sampler from advancing reference image tokens
FLUX.2 reference image conditioning (
kontext_conditioning) is context: the encoded reference latents are concatenated onto the image stream for every forward pass, but the sampler must never advance them.invokeai/backend/flux2/denoise.pyconcatenated them ontoimgonce, before the denoising loop, and never sliced the model prediction back to the generated tokens. Both sampler paths then integrated the whole sequence:scheduler.step(model_output=pred, sample=img)img = img + (t_prev - t_curr) * predSo the reference latents were denoised along with the generated ones. After the first step the reference no longer is the encoded image — it drifts along the model's velocity field for the rest of the schedule, and feeds that drift back into the generated image at every step. The tensor shapes stay consistent throughout, so nothing ever failed; the conditioning just silently degraded.
Both reference implementations do it differently, and one of them is our own code:
Flux2KleinPipelinerebuildstorch.cat([latents, image_latents])inside the loop, slicesnoise_pred = noise_pred[:, : latents.size(1)], and steps onlylatents.image_latentsstay pristine.invokeai/backend/flux/denoise.py) builds a temporaryimg_inputinside the loop and slicespred = pred[:, :original_seq_len].The FLUX.2 port lost that pattern. This PR restores it:
img_input, used by both the positive and the negative (CFG) forward passpredandneg_predare sliced tooriginal_seq_lenbefore CFG and before the sampler updateimgtherefore only ever holds generated tokens, which also removes the split/recombine dances around the inpaint merge, the step previews, and the final sliceBehavior with no reference image is unchanged. With a single reference image the position-ID geometry is unchanged — only the contamination of the conditioning is gone.
One drive-by in the same block: in the manual Euler path the inpaint merge for the preview was written into a dead local (
preview_gen) and thrown away. It now applies topreview_img, matching the scheduler path.Related Issues / Discussions
Community report of FLUX.2 Klein reference-image edits coming out spatially displaced. This PR is not claimed to fix that report — see QA Instructions. It fixes a defect found while investigating it.
QA Instructions
Unit tests —
tests/backend/flux2/test_denoise_ref_image_conditioning.py(new):A fake transformer records its inputs. The core assertion is that the reference slice of
hidden_statesis bit-identical to the encoded latents at every step. Parametrized over both sampler paths, plus a CFG case (which also covers the negative-prediction slice) and a control case without reference images.Verified the tests actually pin the bug: with
denoise.pyreverted tomain, 4 of the 6 fail (all reference cases, both paths); the 2 no-reference control cases stay green.End-to-end — FLUX.2 Klein 9B FP8 + FLUX.2 VAE (diffusers) + Qwen3
text_encoder, single reference image (1200×800), target 1200×800, 9 steps Euler, prompt asking for a haze/color change and explicitly no reframing. Four seeds (424242–424245), run with and without the fix, compared against the reference image by phase correlation (high-pass filtered, brute-forced over scale 0.90–1.20):No measurable difference in image quality, in either direction — the delta is smaller than its own spread and flips sign across seeds. Geometry is identical in all eight images (dx = 0, dy = 0/+1). This is a correctness fix, not a quality fix: the reference conditioning is supposed to stay constant across the schedule, and it now does.
What this PR does not establish: the reported ~20% horizontal displacement did not reproduce in any configuration tried — matching reference/target size, an oversized 1.88 MP reference, and an aspect-ratio mismatch (3:2 reference into a 16:9 target) all came out with < 1% horizontal offset. So the fix is justified on correctness grounds, not as a repro-and-fix of that report.
Merge Plan
Nothing special. Backend only, no schema or API changes, no frontend impact.
Out of scope, noted while investigating and worth a separate PR: with two or more reference images,
invokeai/backend/flux2/ref_image_extension.pygives each additional reference a spatialh_offset/w_offseton top of the per-reference T offset. diffusers places every reference at H/W 0 and separates them by T alone; BFL instead concatenates the images into one picture and uses one latent. Our hybrid matches neither training convention. Untouched here because it changes output for every multi-reference user and needs a visual A/B, not a unit test. The single-reference path is unaffected (idx == 0means both offsets are 0).Checklist
What's Newcopy (if doing a release after this PR)