Skip to content

feat(generation): restore the SDXL Refiner in Generate and Canvas - #9810

Merged
lstein merged 9 commits into
mainfrom
feat/parity-sdxl-refiner
Oct 18, 2026
Merged

lstein merged 9 commits into
mainfrom
feat/parity-sdxl-refiner

Conversation

@joshistoast

@joshistoast joshistoast commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The SDXL Refiner had no path through webv2 Generate or Canvas, so legacy SDXL + refiner recipes could not be reproduced.

A Refiner section (SDXL main models only) offers a refiner model picker limited to sdxl-refiner models (which remain unselectable as main models), scheduler, steps, CFG, start fraction and positive/negative aesthetic scores with the legacy defaults. When a refiner is picked, the graph adds the refiner loader, refiner conditioning (aesthetic scores, prompts wired by edge), a second denoise stage from the start fraction to the end with the base stage ending at the hand-over, and routes the refined latents into decode; seamless is applied to the refiner too. Canvas image-to-image, inpaint and outpaint use start = min(refiner start, 1 − strength) and keep masks, control, IP-adapter and regional guidance on the base stage. The refiner pass runs without the gradient denoise mask to avoid double-noising the coherence band; final paste-back confines the output. PiD + refiner is rejected. A start fraction of 1 means off: inactive model availability and tuning do not block generation, tuning controls are disabled, and the start control and model picker remain available. The refiner is recorded in metadata and restored by Recall All and Remix (key, then hash, then name+base); an uninstalled, ambiguous or unreadable refiner preserves the current refiner selection and tuning and is reported in the recall notice; when the image's main model is unavailable the refiner settings are kept, with a notice only if the image recorded a refiner. The Recall Parameters API also applies its existing refiner fields, including refiner_denoise_start, and resets them in strict mode. Applying an older preset without a refiner clears the slot and names it in the clearing notice.

Two deliberate deviations from the current legacy builder were confirmed correct by review against the backend scheduler: the base pass ends at the hand-over (current legacy regressed this for text-to-image), and the refiner's style conditioning receives the prompt by edge (legacy copied an undefined value).

Related Issues / Discussions

Follows up the legacy parity audit behind #9803.

QA Instructions

From invokeai/frontend/webv2 (Node 22.14, pnpm 10.12): pnpm check:release passes. Mock fixtures now cycle main models over sd-1 / sdxl / sdxl-refiner, so the UI can be exercised against the mock (representative profile):

  1. Home → Generate from text → model picker → SDXL chip → pick an SDXL fixture: a Refiner section appears before Advanced with its controls disabled.
  2. Open "Refiner model": only refiner fixtures are listed; pick one: the badge shows its name and the controls enable.
  3. Switch the main model to an SD 1.x fixture: the confirm dialog lists "Refiner" and the section disappears.
  4. Set Refiner start to 1: the section shows Off, tuning controls disable, and the start control remains editable. Lower it to reactivate the refiner. Apply a preset saved without a refiner: the slot clears and the notice names Refiner. Both flows were inspected in the browser.
  5. Submitted graphs cannot be inspected on the mock (no enqueue route); graph shape is covered by compile tests. A real SDXL model plus refiner is required to see output.

Review

Resolved the Canvas coherence-edge issue by removing the refiner denoise-mask edge while retaining base-pass masking and final paste-back. Recall All and Remix now preserve the whole refiner configuration when the recorded main model or refiner cannot be restored, and distinguish unreadable metadata from missing models. The API recall path supports the six existing refiner parameters and strict resets. Start = 1 consistently disables the refiner, and presets accurately report clearing it.

No material implementation findings remain. GPU generation and the hardware A/B comparison were not repeated.

Compatibility / Rollout

New generate settings are optional with defaults and normalize on load; no project-document migration. No backend API or generated-schema changes; the frontend now consumes the existing refiner recall parameters. Existing API behavior that drops unresolved main-model names and applies remaining parameters to the current model is unchanged.

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. Autofix is disabled.

Add refiner settings (model, steps, CFG, scheduler, start, aesthetic scores) with legacy defaults, a Refiner section shown for SDXL mains, and a refiner denoise stage grafted before the decode for Generate txt2img and Canvas img2img/inpaint/outpaint, with PiD rejected alongside it.
Record refiner metadata and restore it through Recall All and Remix, resolving the model by key, hash or name and base.
Extend the mock fixture with SDXL and refiner mains, the backend graph contract, and the Generate and SDXL docs.
…r states

The base denoise range has one owner, so Canvas grafts keep the hand-over, and inpaint/outpaint feed the gradient mask to the refiner pass too.
A start of 1 skips the refiner instead of loading it for an empty pass; recall reports an uninstalled or ambiguous refiner instead of silently keeping the current one.
The refiner picker owns its empty and loading states, steps/CFG use the legacy input limits, and docs describe the Canvas strength cap.
…nd section

Request counts and chunk sets are unchanged; the refiner settings and graph cores join the
editor's generation source set and the section joins the Generate widget's.
@github-actions github-actions Bot added frontend PRs that change frontend files docs PRs that change docs labels Oct 11, 2026
@lstein lstein self-assigned this Oct 16, 2026
@lstein lstein added the 7.0.0 label Oct 16, 2026
@lstein lstein moved this to 7.0 Theme: Modular Design in Invoke - Community Roadmap Oct 16, 2026

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this. The graph wiring, metadata keys, hand-over split (it matches legacy min(refinerStart, 1 - strength)), LoRA/control isolation and settings normalization all held up under review. Three issues need fixing before merge, plus a few smaller ones.

Blocking

1. Canvas inpaint/outpaint: feeding the gradient mask to the refiner pass leaves seams at the coherence edge. (inline on compileCanvasGraph.ts)

InpaintExt treats the refiner's own latents input as orig_latents, and that input is the base pass output, still noisy at the hand-over timestep. Coherence-band pixels with mask value above ~refinerStart were never generated during the base pass. In the refiner, apply_mask_to_initial_latents adds fresh noise to those already-noisy latents, and they are only released for the last few low-t steps.

I measured this on a W7900 with SDXL base 1.0 + refiner 1.0 at defaults (refiner start 0.8, coherence edge 16, minimum denoise 0, strength 0.75), 3 seeds, 1024² with a circular mask. I compared the graph this PR compiles (A) against the identical graph with only the create_gradient_mask.denoise_mask → refiner_denoise_latents.denoise_mask edge removed (B, the legacy wiring). Mean |A−B| (0–255) by distance from the mask edge:

seed interior −16..0 px 0..8 px 8..16 px 16..32 px 32..64 px
11 5.7 8.3 11.0 16.8 10.0 0.3
22 4.9 7.8 11.6 17.0 11.6 0.5
33 9.5 9.7 12.8 17.4 11.4 0.5

The difference peaks exactly in the outer part of the band. Mean |Laplacian| over the ring is 14–23% higher in A than in B, and B stays close to the source and to a no-refiner run. Visually, A shows hard seams across smooth surfaces and small bright glints at the boundary, where B blends smoothly.

Suggested fix: drop that edge, and the test that asserts it. Paste-back already confines the output to the masked region, so the bleed the comment guards against doesn't reach the final image.

2. Recall All changes the refiner when the image's main model isn't installed. (inline on imageRecall.ts)

getRefinerRecall is evaluated against values.model. When the recorded model can't be resolved, that is still the user's current model. The concept/LoRA path guards this case (getRecord(metadata, 'model') && sourceModel?.key !== model.key → keep current, report modelUnavailable); the refiner path doesn't.

  • With an SDXL model + refiner R selected, Recall All on any generation image whose model isn't installed and which has no refiner_model silently clears R. LoRAs from the same recall are kept.
  • With an SDXL + refiner image whose main model isn't installed, that image's refiner and tuning are written onto the user's unrelated SDXL model.

Suggested fix: pass supportedMetadataModel through and apply the same guard.

3. The Recall Parameters API (recallParameters.ts) doesn't handle the refiner.

The backend RecallParameter accepts refiner_model, refiner_steps, refiner_cfg_scale, refiner_denoise_start (note: not refiner_start), refiner_positive_aesthetic_score and refiner_negative_aesthetic_score. None of them are in HANDLED_KEYS, so they're all reported as "not supported by the Generate panel", which is no longer true. In strict mode a previously selected refiner and its tuning are left in place rather than reset, so the result doesn't match the requested parameters.

Non-blocking

  • When the recorded refiner isn't installed, the skip notice says the current refiner was kept, but steps/CFG/start/scheduler/aesthetic scores from the image are still applied (the test at imageRecall.test.ts ~L1298 asserts this). With no refiner selected, "the current refiner was kept" also reads oddly.
  • An unreadable refiner_model (string, {}) is reported as unresolved ("not installed"). The concept path uses invalid for this.
  • With the refiner off (start = 1), deleting its model still blocks Invoke through the availability check, and the step/CFG/score validators still run. The section also shows the badge and enabled controls at start 1, so it looks active.
  • Applying a preset saved before this PR normalizes refinerModel to null without the "Refiner" cleared-label notice.

Comment thread invokeai/frontend/webv2/src/features/generation/core/canvas/compileCanvasGraph.ts Outdated
Comment thread invokeai/frontend/webv2/src/workbench/image-actions/imageRecall.ts Outdated
Preserve refiner settings on failed image recall and support API recall fields.

Treat disabled refiners consistently and report preset clearing accurately.
@joshistoast
joshistoast requested a review from lstein October 17, 2026 06:50
Recall All on an image whose main model is uninstalled reported the refiner as not restored
even when the record ran without one, so every such recall carried a spurious notice.
The notice now appears only when the record names a refiner or its tuning, as for concepts.

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks. All three blockers and the non-blockers from the previous round are fixed:

  • Canvas seam: the gradient mask no longer reaches the refiner pass. This is the same wiring as the variant that blended cleanly in my GPU A/B, and the test now asserts the edge is absent and that paste-back is still wired.
  • Recall All: refiner recall now shares the concept path's unresolved-model guard, and the notice only appears when the record names a refiner.
  • Recall Parameters API: all six refiner fields are handled, including refiner_denoise_start. Unresolved or incompatible main/refiner lookups skip the tuning instead of retuning the current pair.
  • The skip notice, the invalid reason, the refiner-off behavior and the preset cleared notice are all addressed.

The focused refiner/recall suites and lint:tsc pass. I reverted the recall guard and the API fixes one at a time to check the new tests catch them, and they do.

One optional nit: in recallParameters.ts, a refiner_model name that matches more than one installed refiner silently resolves to the first match. Image recall reports the same case as ambiguous, so the API could skip it with a reason for consistency. Not blocking.

@lstein
lstein merged commit cd8a7e6 into main Oct 18, 2026
16 checks passed
@lstein
lstein deleted the feat/parity-sdxl-refiner branch October 18, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 docs PRs that change docs frontend PRs that change frontend files

Projects

Status: 7.0 Theme: Modular Design

Development

Successfully merging this pull request may close these issues.

2 participants