BFS LoRA Surgery
Find Which Layers Broke Your LoRA, Without Retraining
- model
- clip
- model
- clip
- report
- rules
Your character LoRA nails the face and then gives you skin that looks like it went through a phone's beauty filter. Or the likeness holds up close and drifts as soon as the subject is small in frame. Retraining is the honest fix, but you don't know what to change, and a rerun costs hours. BFS LoRA Surgery exists for the span between those two facts.
The bet behind it: a defect a LoRA learned doesn't live evenly across the adapter. It lives in one module family, across a range of blocks. Scale that group, or delete it, and regenerate on a fixed seed - you've pinned down where the problem sits in minutes instead of guessing through a retrain. Block-level editing isn't new (sd-webui-lora-block-weight was doing it in 2023); what's new is doing it in-graph, live, with the layer names read out of your actual file.
How it works
It reads the safetensors header first, which is instant, then discovers the structure from the key names rather than assuming an architecture. Diffusers (lora_A/lora_B) and kohya (lora_down/lora_up plus alpha) conventions both work, and families like transformer_blocks or token_refiner are detected automatically.
Two details keep it honest on real files. The block index is whichever numeric slot varies most across the file, not just the last number - otherwise transformer_blocks.12.attn1.to_q looks like block 1. And each module's update size, ‖ΔW‖, comes from QR-ing both factors and taking singular values of the small product, so the full matrix is never materialized.
Scaling multiplies only the up/B factor, since (sB)A = s(BA) - exact, and negative scales work. Scale 0 removes the module's keys.
The inputs that matter
model and lora_name are the obvious two - the second is a dropdown of everything in models/loras. strength_model is overall LoRA strength, as usual. rules is JSON, and the panel writes it for you. One optional input, clip: wire it only if the LoRA carries a text-encoder delta, otherwise it passes through untouched. Outputs: model (into your sampler), clip, report (a one-line string of kept/dropped/scaled counts - wire it to a Show Text node; it also prints to console), and rules, so you can feed the save node without copying JSON.
There's no Load LoRA upstream - this node loads the file itself, so the graph is checkpoint loader → BFS LoRA Surgery → sampler.
Driving the panel
Pick the LoRA and the panel lists every family with its module types, sorted by update size. Click a type's -> to select it, drag across the block ruler for a range, set a scale, press add rule. add as regex writes the same selection as a hand-editable pattern. The rule JSON is saved with the workflow:
[
{"enabled": true, "match": {"type": "attn.*"}, "scale": 1.15},
{"enabled": true, "match": {"type": "img_mlp.gate_up", "blocks": "8-15"}, "scale": 0}
]
Order matters. Later rules override earlier ones where they overlap, so put boosts first and drops last, or a boost quietly undoes a drop. blocks takes 8-15, 0,4,7 or 8-15,24-31; a regex match is tested against the module's full path.
Install
ComfyUI Manager → search the pack title, or:
cd ComfyUI/custom_nodes
git clone https://github.com/alisson-anjos/ComfyUI-BFSNodes.git
# restart ComfyUI
Don't pip install -r requirements.txt on reflex. That file pulls insightface==0.7.3, onnxruntime, opencv-python and librosa, for the pack's head-swap and video nodes, and insightface wants a compiler on some systems. The surgery nodes need torch, safetensors and ComfyUI. The pack README never mentions these nodes - it only covers the LTXV Edit Anything ones. The real notes live in the panel's help box.
Where people get burned
Silent rules that match nothing. Type matching is exact, or a prefix with a trailing * (img_mlp.* works; img_mlp alone matches nothing). The built-in presets use a leading wildcard (*mlp*), which this matcher doesn't honour, so trust the report line over the preset label: 0 dropped, 0 scaled means your rule caught nothing.
Non-LoRA adapters only half-work. Scaling touches the up/B weights, so plain LoRA is exact. LyCORIS LoHa/LoKr modules use different key names, where a nonzero scale is silently a no-op - though scale: 0 still drops them.
The node missing from the menu. The pack imports each module in a try/except, so a failure appears as [BFSNodes] LoRA Surgery node not loaded: … in the console and nowhere else. Check the terminal before blaming Manager.
An empty panel. This is a Vue app in a DOM widget - exactly the class of custom UI that ComfyUI's Nodes 2.0 rewrite broke for other packs. The rules STRING is only hidden once the JS loads, so you can paste JSON by hand. If a loaded workflow shows an empty recipe, hit reload before editing.
One warning worth taking seriously
The author's own: a variant can win every metric you're looking at and still have destroyed what you trained. Dropping the whole MLP path gave the sharpest skin in one of his cases - and made the LoRA stop copying expression from the source image. Check a deliberately hard case with your eyes, not just your numbers. Pruning after training also isn't the same as training without those layers: excluding them up front may fail to converge, because the layer is where the defect lodges, not where it comes from.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| model | MODEL | — | |
| lora_name | COMBO | 0 options: | |
| strength_model | FLOAT | 1.00-20–20 | Overall LoRA strength, as usual. |
| rules | STRING | [] | JSON list of rules. The panel writes this for you. |
| clipopt | CLIP | — |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| model | MODEL | — |
| clip | CLIP | — |
| report | STRING | — |
| rules | STRING | — |