Repository navigation
feat: add lanpaint support #1978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chaserhkj
wants to merge
6
commits into
leejet:master
Choose a base branch
from
chaserhkj:lanpaint
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+725
−11
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5b55aad
feat(runtime): add LanPaint Langevin-dynamics inpainting core engine
chaserhkj ea606a5
fix(lanpaint): reject SenseNova U1 and MiniMax H3 AV denoisers
chaserhkj 7383adf
feat(sampling): add LanPaint Langevin-dynamics inpainting support
chaserhkj 59bb5f5
feat(lanpaint): add schedule logging and extract effective_inner_step…
chaserhkj 0017abe
fix(lanpaint): fix mask tensor lifetime issue
chaserhkj b273aa2
docs(lanpaint): add LanPaint inpainting documentation
chaserhkj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| ## LanPaint Inpainting | ||
|
|
||
| LanPaint is a training-free inpainting sampler that gives diffusion models | ||
| "think mode": instead of denoising in one pass, it runs up to N inner Langevin | ||
| steps per sampling step, letting the model reconcile the prompt-driven region | ||
| with the known (kept) region before committing to the update. It is a port of | ||
| the [ComfyUI LanPaint extension](https://github.com/scraed/LanPaint) | ||
| (official implementation of | ||
| ["LanPaint: Training-Free Diffusion Inpainting with Asymptotically Exact and Fast Conditional Sampling"](https://arxiv.org/abs/2502.03491), | ||
| TMLR 2025), with the parameter defaults of the v2.1.0 ComfyUI node. | ||
|
|
||
| In essence, the inpainting problem is modeled as a conditional stochastic | ||
| process, and LanPaint integrates it numerically with | ||
| [Langevin dynamics](https://en.wikipedia.org/wiki/Langevin_dynamics). The | ||
| drift of the process is assembled from the underlying model's own denoising | ||
| solutions, i.e. its inferred solutions of the unconditional version of the | ||
| same process, so the conditional problem is solved without retraining. Each | ||
| inner iteration is one integration step of this process, and each step costs | ||
| one model evaluation; that is the exchange of extra compute for inpainting | ||
| quality: up to `1 + n_steps` model evaluations per sampling step instead of one. | ||
|
|
||
| ### Usage | ||
|
|
||
| Provide an init image and a mask and enable LanPaint: | ||
|
|
||
| ```bash | ||
| sd-cli -m model.safetensors -i images/input.png --mask images/mask.png \ | ||
| -p "a cozy living room, photorealistic" \ | ||
| --strength 1.0 --sampling-method euler --lanpaint -v | ||
| ``` | ||
|
|
||
| Mask convention (same as the rest of `stable-diffusion.cpp`): **white (255) | ||
| marks the region that gets repainted, black (0) marks the region that is kept.** | ||
| Masks are binarized at load time and evaluated at latent resolution. When no | ||
| mask is given, LanPaint is inactive and sampling proceeds like the plain | ||
| sampler. | ||
|
|
||
| The inner loop needs both a conditional and an unconditional model branch. | ||
| Guidance-distilled models without an unconditional branch (for example | ||
| turbo/schnell variants) run, but the keep-region guidance degenerates and the | ||
| LanPaint benefit largely disappears. | ||
|
|
||
| ### Parameters | ||
|
|
||
| | Flag | Description | Default | | ||
| |------|-------------|---------| | ||
| | `--lanpaint` | enable LanPaint | off | | ||
| | `--lanpaint-n-steps` | number of inner Langevin steps per sampling step ("turns of thinking"); the effective count ramps down near the end of the schedule | 5 | | ||
| | `--lanpaint-lambda` | strength of the keep-region (bidirectional) guidance | 5.0 | | ||
| | `--lanpaint-beta` | time-scale ratio of the keep-region branch | 1.0 | | ||
| | `--lanpaint-step-size` | Langevin step size; scaled by the remaining noise fraction of the current step | 0.2 | | ||
| | `--lanpaint-early-stop` | skip the inner loop for the last N sampling steps (they only polish with a plain evaluation) | 1 | | ||
| | `--lanpaint-min-step-frac` | when the remaining noise fraction drops below this value, the step size is pinned there and the inner-step count ramps down to zero | 1.0 | | ||
| | `--lanpaint-prompt-first` | Prompt First mode: sets the BIG guidance scale to -0.5, emphasizing prompt following over mask-boundary coherence | off (Image First) | | ||
| | `--lanpaint-cfg-big` | explicit BIG guidance scale override; by default it resolves to `--cfg-scale` (Image First) or -0.5 (Prompt First) | auto | | ||
|
|
||
| `Image First` (default) and `Prompt First` change how strongly the inner loop | ||
| is anchored to the already-known image versus the prompt. Use Image First for | ||
| seamless object insertion in the known surroundings; use Prompt First when the | ||
| repainted region should follow the prompt even at the cost of boundary | ||
| coherence. | ||
|
|
||
| LanPaint inherits every per-step feature of the outer sampler: conditioning, Control | ||
| Net, IP-Adapter, reference latents, video masks, previews and cancellation. | ||
|
|
||
| ### Cost | ||
|
|
||
| Each inner step costs one model evaluation (conditional + unconditional), so a | ||
| run needs up to `steps x (1 + n_steps)` evaluations. At INFO log level the | ||
| planned count is printed before sampling starts: | ||
|
|
||
| ``` | ||
| LanPaint: 16 outer steps, up to 5 inner steps per outer step, 68 model evaluations planned (16 without LanPaint) | ||
| ``` | ||
|
|
||
| At VERBOSE level every inner step is logged. With the defaults, `early_stop` | ||
| and the `min_step_frac` ramp already keep the tail of the schedule cheap. | ||
| Recommended `--lanpaint-n-steps` range: 2-8 (the upstream default of 5 is a | ||
| good quality/speed balance). | ||
|
|
||
| ### Supported samplers and models | ||
|
|
||
| - Samplers: `euler` (recommended), `euler_a`, `heun`, `dpm2`, `dpm++2m`, | ||
| `dpm++2mv2`. Other samplers are rejected with an error. | ||
| - Models: everything using the standard denoiser noise scaling, including | ||
| SD1.x/SD2.x (eps, v-prediction, EDM), SDXL, SD3/SD3.5, FLUX, Chroma, | ||
| Qwen-Image, Wan, HunyuanVideo, and LTX video models. | ||
| - Not supported (rejected with an error): | ||
| - MiniT2I and SeFi (custom latent scaling / dual time conventions), | ||
| - SenseNova U1.5 (its noise scaling discards the latent, which would | ||
| overwrite the kept region), | ||
| - MiniMax-H3 and LTX-AV (audio rows follow a per-stream noise schedule the | ||
| inner loop does not model). | ||
| - Sampling caches (`--cache-mode`, for example) are disabled under LanPaint: | ||
| the inner loop's repeated evaluations at one step index break their reuse | ||
| accounting. | ||
|
|
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
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 17554
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 41961
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 45072
Persist LanPaint settings in configuration and metadata.
SDGenerationParams::get_optionsonly binds the CLI values tosample_params.lanpaint; it does not serialize them.from_json_stralso ignores asample_params.lanpaintobject, andbuild_sampling_metadata_jsonemits no LanPaint fields. A run with--lanpainttherefore uses the selected values, but a saved configuration or embedded metadata cannot reproduce them. The reload then uses LanPaint defaults or leaves it disabled.Suggested fix
if (sample_json.contains("flow_shift") && sample_json["flow_shift"].is_number()) { target_params.flow_shift = sample_json["flow_shift"]; } + if (sample_json.contains("lanpaint") && sample_json["lanpaint"].is_object()) { + const json& lanpaint_json = sample_json["lanpaint"]; + if (lanpaint_json.contains("enabled") && lanpaint_json["enabled"].is_boolean()) + target_params.lanpaint.enabled = lanpaint_json["enabled"]; + if (lanpaint_json.contains("n_steps") && lanpaint_json["n_steps"].is_number_integer()) + target_params.lanpaint.n_steps = lanpaint_json["n_steps"]; + if (lanpaint_json.contains("early_stop") && lanpaint_json["early_stop"].is_number_integer()) + target_params.lanpaint.early_stop = lanpaint_json["early_stop"]; + if (lanpaint_json.contains("lambda") && lanpaint_json["lambda"].is_number()) + target_params.lanpaint.lambda = lanpaint_json["lambda"]; + if (lanpaint_json.contains("beta") && lanpaint_json["beta"].is_number()) + target_params.lanpaint.beta = lanpaint_json["beta"]; + if (lanpaint_json.contains("step_size") && lanpaint_json["step_size"].is_number()) + target_params.lanpaint.step_size = lanpaint_json["step_size"]; + if (lanpaint_json.contains("min_step_frac") && lanpaint_json["min_step_frac"].is_number()) + target_params.lanpaint.min_step_frac = lanpaint_json["min_step_frac"]; + if (lanpaint_json.contains("prompt_first") && lanpaint_json["prompt_first"].is_boolean()) + target_params.lanpaint.prompt_first = lanpaint_json["prompt_first"]; + if (lanpaint_json.contains("cfg_big") && lanpaint_json["cfg_big"].is_number()) + target_params.lanpaint.cfg_big = lanpaint_json["cfg_big"]; + }{"flow_shift", sample_params.flow_shift}, {"extra_sample_args", safe_json_string(sample_params.extra_sample_args)}, + {"lanpaint", + { + {"enabled", sample_params.lanpaint.enabled}, + {"n_steps", sample_params.lanpaint.n_steps}, + {"early_stop", sample_params.lanpaint.early_stop}, + {"lambda", sample_params.lanpaint.lambda}, + {"beta", sample_params.lanpaint.beta}, + {"step_size", sample_params.lanpaint.step_size}, + {"min_step_frac", sample_params.lanpaint.min_step_frac}, + {"prompt_first", sample_params.lanpaint.prompt_first}, + {"cfg_big", std::isfinite(sample_params.lanpaint.cfg_big) + ? json(sample_params.lanpaint.cfg_big) + : json(nullptr)}, + }}, {"guidance",🤖 Prompt for AI Agents