Repository navigation
feat(generation): restore the SDXL Refiner in Generate and Canvas - #9810
Conversation
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.
lstein
left a comment
There was a problem hiding this comment.
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_modelsilently 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 asunresolved("not installed"). The concept path usesinvalidfor 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
refinerModelto null without the "Refiner" cleared-label notice.
Preserve refiner settings on failed image recall and support API recall fields. Treat disabled refiners consistently and report preset clearing accurately.
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
left a comment
There was a problem hiding this comment.
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
invalidreason, 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.
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-refinermodels (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, includingrefiner_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:releasepasses. Mock fixtures now cycle main models over sd-1 / sdxl / sdxl-refiner, so the UI can be exercised against the mock (representative profile):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
What's Newcopy (if doing a release after this PR)Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.