Files
Seunghyun JiandClaude Fable 5 679592ffb0 speed up extended attention: auto flash_attn + mask-free SDPA fallback
Two changes to _extended_self_attn dispatch (per-block, per-step hot path):

- The flash-attn LSE path now auto-enables whenever flash_attn is
  importable, instead of requiring ComfyUI's --use-flash-attention launch
  flag (that flag only governs comfy's own attention calls; gating our
  internal path on it left the fast path dark on stock `comfy launch`).
  A runtime kernel failure permanently drops to the SDPA fallback.

- The SDPA fallback no longer passes a float attn_mask (which forced the
  ~2.6x slower mem-efficient backend). The uniform b_cond bias is folded
  into one appended head dimension (q extra = sqrt(D), target-k extra = 0,
  cond-k extra = b_cond, explicit scale=1/sqrt(D)), head dim zero-padded
  to a multiple of 8, padded value channels sliced off the output — exact,
  and flash-backend eligible.

Measured on RTX 5070 Ti at ~1MP shapes (B=2, 16h, 4200+4200 tokens, bf16),
per call: old float-mask 17.3 ms -> fallback 11.2 ms / flash 6.9 ms
(native target-only attention: 3.3 ms). x28 blocks this was ~300 ms/step
of recoverable overhead. Cross-path agreement at the bf16 noise floor;
gate-off invariant unchanged (7.55e-08 in test_smoke).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSkULyWmisdHfrxHNc7J37
2026-08-21 18:39:07 +09:00

270 lines
14 KiB
Markdown

# ComfyUI-EasyControl-KSamplerCompat
KSampler-compatible **Anima EasyControl** image conditioning for ComfyUI.
The main node — **`Anima EasyControl (KSampler)`** — takes `MODEL + VAE + IMAGE`
and returns a `MODEL` plus an empty `LATENT` sized to the conditioning
resolution.
The reference-image conditioning rides on the returned model as a set of
ModelPatcher object-patches, so it flows straight into the **stock KSampler** and
composes with the rest of your graph (schedulers, ControlNet, other adapters).
Wire the `LATENT` output into the KSampler's `latent_image` and the generation
matches the (aspect-preserved, ~1MP) cond grid automatically — no manual
`EmptyLatentImage` sizing, and the two streams share a grid for clean spatial
alignment on tasks like colorization.
This is deliberately unlike the upstream Flux / Qwen EasyControl ComfyUI nodes,
which make you load a model through a dedicated loader and sample with a
**dedicated sampler node**. Here there is no custom sampler — just a model patch.
Two CPU-only helper nodes ride along for **EasyEdit**, the instruction-editing
use of the same path — see [EasyEdit](#easyedit--edit-by-instruction-no-inversion).
## What it's for
EasyControl is a frozen-DiT reference-image adapter for the Anima model. The
flagship use case is **colorization**: feed a grayscale / manga line page into
the `IMAGE` socket and a color prompt into your normal text encode, and the DiT
colorizes the reference while respecting its structure.
The second use is **EasyEdit**: with the `subject_edit` checkpoint the same node
becomes a reference + instruction editor — give it a character and a list of
changes, get that character re-rendered with the changes applied, in one pass
and with no inversion.
## Install
1. Clone into `ComfyUI/custom_nodes/`:
```
git clone https://github.com/sorryhyun/ComfyUI-EasyControl-KSamplerCompat
```
2. Put your EasyControl checkpoint (`*.safetensors` trained with
`networks.methods.easycontrol`) in `ComfyUI/models/loras/`. Published
adapters live in
[`sorryhyun/anima-easycontrol-adapters`](https://huggingface.co/sorryhyun/anima-easycontrol-adapters):
`anima_colorize_v2.safetensors` (colorization) and
`anima_subject_edit_alpha.safetensors` (EasyEdit).
3. Restart ComfyUI.
## Wiring
```
UNETLoader ──► MODEL ─┐ MODEL ──► KSampler ──► ...
├─► Anima EasyControl ────┤ ▲
VAELoader ───► VAE ───┤ (KSampler) └─ LATENT ──┘
LoadImage ──► IMAGE ──┘ ▲
easycontrol_lora = <your ckpt>
strength = 1.0, target_megapixels = 1.0
```
- **`image`** — the conditioning image (the grayscale / lineart page for
colorization). It is resized to at most `target_megapixels` (aspect-preserving,
VAE/patch-snapped) and VAE-encoded once; the returned `LATENT` is sized to that
same grid, so the cond stream and the sampled target share a resolution.
- **`strength`** — scales the trained cond effect. `1.0` = as trained. Raise to
push harder toward the reference, lower to loosen.
- **`mask`** (optional) — an inpaint mask for the `inpaint` adapter. Wire the
**original** image into `image` and a mask here; the node fills the white
(selected) region with mid-gray and encodes that as the conditioning image —
reproducing what the `inpaint` adapter trained on (cond = original image with a
free-form gray hole). The DiT then regenerates the hole consistent with the
surrounding pixels and the prompt. Leave unconnected for adapters that take a
pre-built cond image (e.g. colorize).
- **`target_megapixels`** (optional) — max output size in megapixels for the
returned latent and the cond encode. `1.0` (default) keeps generation on the
~1MP distribution Anima was trained at by downscaling anything larger; a source
already below the cap is left at its native resolution (never upscaled). `0`
keeps the input's native resolution (still snapped to the VAE/patch grid). Feed
the `LATENT` output into the KSampler's `latent_image`.
- **`cond_scale_override`** (optional) — replace the checkpoint's trained
`cond_scale` outright (before `strength`); `0` = keep the trained value.
Text prompt: encode it the normal way and feed the KSampler's positive/negative
as usual. For the colorize adapter, the prompt carries **color** facts the
lineart can't (`"pink hair, blue eyes, white dress"`); structure comes from the
reference image.
## EasyEdit — edit by instruction, no inversion
**EasyEdit is a checkpoint plus a prompting convention, not a new node.** It
rides the exact path above: reference image into `image`, an *edit instruction*
into your positive text encode, one plain KSampler run. No inversion pass, no
anchor, no mask, no offset tuning.
- **Checkpoint**: `anima_subject_edit_alpha.safetensors` from
[`sorryhyun/anima-easycontrol-adapters`](https://huggingface.co/sorryhyun/anima-easycontrol-adapters)
→ `ComfyUI/models/loras/`.
- **Workflow**: [`workflows/easyedit.json`](workflows/easyedit.json) — drag it
into ComfyUI. Core nodes plus this repo only.
### What it does
Give it a picture of a character and a list of changes; it re-renders that
character with the changes applied. It preserves **identity and appearance**,
not composition — expect a new pose/framing that is recognizably the same
character. That is the opposite trade from the
[DirectEdit node](https://github.com/sorryhyun/anima_lora), which preserves the
original pixels and edits in place at the cost of a second (inversion) pass:
| | DirectEdit | EasyEdit |
|---|---|---|
| Semantics | edit THIS image in place | re-render with these changes |
| Preserves | composition + unchanged pixels | identity / appearance |
| Cost | 2 passes | 1 generation |
| Inputs | image + source/target captions | image + delta instruction |
| Weak spot | hard images | object removals |
### The prompting convention
The prompt is a **tag delta**, not a caption. Additions first as bare tags, then
removals prefixed with `-`:
```
wading in water, night, smile, -chair, -ramune
```
Rules that matter:
- **Instruction, never a full caption.** Only what *changes* goes in the prompt.
Tags the reference already satisfies must be left out — including the
character's name. Identity comes from the image, not the text; naming the
character in the prompt is the failure mode the training objective was
designed to starve.
- **Anima tag vocabulary**, comma-separated (`blue eyes`, not `blue_eyes` — the
builder nodes normalize this for you).
- **The rating band participates.** `safe` / `sensitive` / `nsfw` / `explicit`
are ordinary tags, so `nsfw, -safe` converts the rating. That is a real
capability, and it means an accidental rating tag in your instruction will
move the output.
- **Do not add quality tags** (`masterpiece`, `best quality`) to the
instruction. Training saw deltas only; anything else is off-convention.
Quality *negatives* on the negative prompt are fine and are what the workflow
ships with.
### Building the instruction
Two CPU-only helper nodes ship here so you don't hand-compose the string:
- **`Anima EasyEdit Instruction (add / remove)`** — two text fields, one
correctly formatted instruction out. Use this when you know what you want to
change.
- **`Anima EasyEdit Delta (caption diff)`** — two full captions in
(`source_caption`, `target_caption`), the delta out. Wire the
[Anima Tagger](https://github.com/sorryhyun/anima_lora) node's caption output
into `source_caption`, paste an edited copy into `target_caption`, and you get
"edit the image by editing its caption" — the DirectEdit UX without the
inversion pass. Shared tags cancel automatically, so you cannot accidentally
leave the character name in. It also outputs the computed `additions` and
`removals` separately, which is the quickest way to see why an edit did
something you didn't ask for.
Both emit the same format the training-time pair miner produced (there is a test
pinning that equivalence), and both accept comma- **or** newline-separated tags.
### Settings
Defaults are the validated operating point — the workflow ships with them:
| | |
|---|---|
| `strength` | `1.0` |
| `target_megapixels` | `1.0` |
| `cond_scale_override` | `0` (use the trained value) |
| sampler / scheduler | `euler` / `simple` |
| steps / cfg | `28` / `4.0` |
ComfyUI's built-in Anima config already uses `shift = 3.0`, which is the shift
the adapter was validated at, so no extra sampling-shift node is needed.
**There is deliberately no `b_cond` offset knob.** This node applies the
checkpoint's trained gate verbatim — equivalent to `b_offset 0` in the
anima_lora CLI — and that *is* EasyEdit's operating point. The engaged band is
narrow and centered there: raising the gate by +2 pulls the output back toward
the reference's composition and +3 is a near-verbatim copy of the reference.
If an edit isn't landing, change the instruction, not the gate.
### Limits (alpha)
The checkpoint is published as `_alpha` for these reasons; know them before you
file a bug:
- **Removals of objects that are physically present in the reference mostly
fail.** `-ramune`, `-inflatable orca`, `-hair beads` largely survive the
instruction. Additions and *state changes* ("jacket partially removed") are
where the adapter is strong. Negation is adapter-side only — the text encoder
reads `-ramune` as "ramune", so a weak adapter response leaves you with an
attractor, not a removal. If a removal matters, phrase the change as an
addition where you can.
- **Validation is narrow.** The published evidence is a render-judged
instruction probe on three characters at a single seed, drawn from the
training pair set — an upper bound, not a held-out score. Off-corpus images
(non-anime, or styles the base model covers thinly) are untested.
- **Composition is not preserved by design.** If you need the original framing
and background pixels intact, use DirectEdit, not this.
## How it works
EasyControl extends each DiT block's self-attention to attend over a
**reference-image key/value stream** in addition to the target tokens, gated by
a trained per-block scalar bias `b_cond`. On apply:
1. The reference image is VAE-encoded and mapped into the DiT input space
(`process_latent_in`).
2. On the first sampling step, the cond stream is walked once through all blocks
to build a per-block `(cond_k, cond_v)` cache (deterministic across steps —
the cond t-embedding is fixed at `t=0`). Reused for every step and CFG
branch.
3. Each block's `forward` is replaced (via reversible object-patch) with one
that runs extended self-attention `[target_k ; cond_k]` with the `b_cond`
bias on the cond columns. Cross-attention and MLP run baseline.
It targets ComfyUI's native Anima/Cosmos backbone
(`comfy/ldm/cosmos/predict2.py`) directly — a from-scratch reimplementation of
anima_lora's inference path against ComfyUI's **split** `q_proj/k_proj/v_proj`
layout. The trained cond-LoRA delta (a fused `D→3D` tensor) is sliced into q/k/v
thirds and added onto the split projections; the base weights are numerically
identical between the two DiTs (same pretrained model, fused-vs-split layout),
so this reproduces what training saw. No anima_lora vendoring required — it uses
only ComfyUI's own modules plus the checkpoint tensors.
Checkpoints trained with **`train_adaln`** (target-stream AdaLN LoRA — per-block
deltas on the `adaln_modulation_{self_attn,cross_attn,mlp}` up-projections) are
fully supported: the deltas are merged as ordinary ModelPatcher weight patches
(scaled by `strength`, composing with lowvram loading, block compile, and other
LoRAs), while the cond-stream prefill subtracts them again so the reference
stream sees the frozen modulation exactly as training did. Channel-scaled
checkpoints (`*.inv_scale` tensors from anima_lora's per-channel gradient
rebalance) are also applied faithfully. The loader is **strict**: a checkpoint
carrying tensors this node doesn't implement is refused with an error instead of
silently dropping them (a dropped trained feature degrades output with no
warning — if you hit this, update the node).
## Known limitations / notes
- **Latent space.** The node assumes `VAE.encode` → `process_latent_in` yields
the same latent the DiT receives for the noisy target. This is the principled
match for the native Anima model; if a future build changes the latent
pipeline, the cond stream would need the same change.
- **Block-compile ordering.** If you also use a node that *rebuilds* the DiT
(e.g. Anima block-compile), apply **this node after** it in the chain. The
per-block patches resolve their block from the live diffusion_model each
forward (rebuild-tolerant), but as with the other Anima nodes, mixing
DiT-rebuilding patches is best avoided.
- **Attention.** Extended attention runs on `flash_attn` (exact LSE
decomposition, same as anima_lora's training-time path) whenever the library
is installed — no launch flag needed. Without it, a mask-free
`scaled_dot_product_attention` formulation is used: the `b_cond` bias is
folded into one appended head dimension so the call stays on SDPA's fast
flash backend instead of the ~2.6x slower mem-efficient kernel a float
`attn_mask` would force. Both paths differ from each other numerically only
at the ulp level.
- **One reference image.** If a batch is fed to `IMAGE`, the first image is used.
## Relationship to the Anima Adapter Loader
This is a separate repo from
[`ComfyUI-Anima_lora-Adapter`](https://github.com/sorryhyun/ComfyUI-Anima_lora-Adapter)
(LoRA / HydraLoRA / ReFT / FeRA / Soft Tokens). Those add residuals to whole
Linear/block outputs and can ride generic forward hooks; EasyControl reaches
inside self-attention, so it needs this dedicated reimplementation.