Visual Model Merge SDXL
Merge two SDXL models by dragging a curve, not 21 sliders
- model1
- model2
- MODEL
ComfyUI has always been able to merge two checkpoints, but the built-in path makes you babysit a wall of sliders. Visual Model Merge SDXL swaps that for a curve you drag with your mouse - one smooth line across the 21 blocks of an SDXL UNet instead of 21 numbers you have to hold in your head. The name is a little fancy, but the pitch is simple: you can finally see the merge you're building.
The catch up front: this is SDXL-only. The node maps weights to input_blocks.0 through input_blocks.8, middle_block.0 through 2, and output_blocks.0 through 8 - hardcoded SDXL key names. Drop an SD 1.5 or Flux checkpoint in and nothing lines up. It's a tool for blending two SDXL models you already like, not a universal merge gadget.
How it works
Under the hood it does exactly what ComfyUI's ModelMergeSimple and ModelMergeBlocks do. It clones model1, pulls the diffusion-model key patches off model2, then calls add_patches() with a blend ratio per block: 100 means model1's weights win outright, 0 means model2's do, and anything between is a proportional blend. No training, no extra data - it's linear interpolation over existing weights, which is why a merge can be built in seconds.
What's genuinely different is the frontend. Drag on the curve and a Gaussian smoothing kernel spreads your change across neighboring blocks - grab the "Smoothing Radius" slider (1 = tight, 8 = wide) to control how far it ripples. Shift+drag edits only the single block under the cursor, and "Reset Weights" snaps all 21 back to 100. The canvas is color-coded: blue for input, orange for middle, green for output, so you can see at a glance where the style is coming from.
One behavior worth knowing: the numbers are inverted from what you might guess. 100 = model 1's weights, 0 = model 2's. If you want mostly model 2, drag the curve down.
Inputs and outputs that matter
- model1 / model2 - the two
MODELcheckpoints you're blending (wire them out of two Checkpoint Loaders). - time_embed, label_emb, out - the SDXL conditioning embedders most merge nodes ignore. These control the timestep embedding, the pooled-text embedding, and the output projection. If you're blending one model for prompt adherence and one for aesthetics, these are the dials that actually move behavior.
- weights_json - a 21-element array of ints that's the machine-readable version of your curve. It's hidden in the UI once you start dragging, but it's there if you want to paste in a known-good merge.
Output is a single MODEL - wire it straight into a KSampler. This is a runtime merge: nothing gets saved to disk, so it's perfect for A/B experimenting but not a replacement for merging once and saving a .safetensors with a dedicated tool if you'll reuse the result.
Installation
Via ComfyUI Manager, search "Visual Model Merge", or just clone it:
cd ComfyUI/custom_nodes
git clone https://github.com/jeremy-strang/ComfyUI-VisualModelMerge.git
Then restart ComfyUI. There's no requirements.txt and no model to download - it's one Python file plus a JS extension using only the standard library, so installs don't break your environment. It shows up under advanced/model_merging/visual.
Where people get burned
The silent-fallback trap: weights_json is parsed with a try/except that snaps to all-100s (i.e. plain model 1) if the JSON is malformed - and the JS curve editor won't load the array at all unless it has exactly 21 values, so a 20-number paste just leaves the curve at 100. If a merge "isn't working," count your array: valid JSON with the wrong length won't throw in the editor, it'll quietly hand you model 1.
And the bigger, community-proven one: merging incompatible families dilutes both. Blend a realism model with a Pony/Illustrious anime model and you get a checkpoint weaker at each - smoother curves don't fix incompatible training distributions. Use this to fine-tune a blend of two models that already share a base. For that job, the curve is a genuinely pleasant way to work.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| model1 | MODEL | — | |
| model2 | MODEL | — | |
| time_embed | INT | 1000–100 | — |
| label_emb | INT | 1000–100 | — |
| out | INT | 1000–100 | — |
| weights_jsonopt | STRING | [100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100] | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| MODEL | MODEL | — |