Commit Graph
23 Commits
Author SHA1 Message Date
EthanfelandClaude Fable 5 ea6561feb2 feat(inline): use stamped filenames for real names, exact attribution, and file-based community reconciliation
execute_inline now reads the loraopt_chain_names attachment off the incoming
model/clip and aligns each stamped filename to its reconstructed chain group
by order + strength (_resolve_stamp_names, per branch: strength_model for the
model side, strength_clip for the clip side). A matched group:
  - displays its REAL filename in the fingerprint (instead of "chain lora #N");
  - carries a _resolved_file_name flag that gives the virtual item FILE
    identity — _persistent_lora_key returns the file-name key and
    _lora_content_hash hashes the file bytes, so an inline-named run and a
    Stack run of the same LoRA share the memory/community key.

Unstamped groups (custom loaders, or no attachment) keep the exact previous
behavior: generic "chain lora #N [salt]" display + captured-content-hash
identity. The salted display/merge-cache name stays separate from the
identity name. File-based (Stack) items are untouched (no _resolved_file_name
-> both helpers behave byte-identically).

Matching degrades gracefully: fewer/more stamps than groups, strength
mismatch, and malformed entries all leave the affected group unnamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:35:21 +02:00
EthanfelandClaude Fable 5 106f036da6 feat(inline): stamp LoRA filenames onto the model via a LoraLoader wrapper (stock + rgthree Power Loader)
Add _install_lora_name_stamp(), which idempotently wraps
nodes.LoraLoader.load_lora so every stock/rgthree LoRA load records its
real filename + strengths as an ordered "loraopt_chain_names" attachment
on the returned model (and clip.patcher). One method covers all three
loaders: stock LoraLoader (direct), LoraLoaderModelOnly (via self.load_lora),
and rgthree PowerLoraLoader (via LoraLoader().load_lora, whose LoraLoader
IS nodes.LoraLoader).

The accumulator builds a NEW list every call because ModelPatcher.clone
copies the attachment list by reference; appending in place would corrupt
the upstream (pre-clone) model's chain. Stamping is fully fail-safe: any
error returns the original load result unchanged. __init__ installs the
wrapper at import (idempotent, graceful when comfy's `nodes` is absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:28:50 +02:00
EthanfelandClaude Fable 5 0b961f25b2 fix(inline): pin cache_patches off for the AutoTuner delegate + honest memory/community docs
The delegate's in-node patch cache keys on id() of the fresh stripped
clone, so it can never usefully hit for inline and only pins a stale
merged-patch set in RAM (the same id()-reuse staleness the node already
pins off for its own optimize_merge path). Stop forwarding
settings["cache_patches"] to the delegate and pin it "disabled".

Docs: correct the commit-B overclaim. Inline memory now persists across
sessions via captured-content identity, but inline community is a SEPARATE
namespace — captured-weight hashes never collide with file-byte hashes, so
inline chains share configs only with other inline chains, not with
file-based LoRA-Stack recordings — and sharing assumes the same ComfyUI
version (captured factors/keys depend on comfy's key-mapping / QKV-fusion).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:00:39 +02:00
EthanfelandClaude Fable 5 1e1cce54b0 fix(inline): key memory + analysis caches on captured-content hash so persistence works across sessions
Captured chain items have no file and a per-session salted name
(`chain lora #N [uuid]`), so every persistent key derived from the name
changed every ComfyUI session — memory saved under a fresh key each run
and never read back, and the per-LoRA/pair/whole-stack analysis disk
caches never cross-session hit. The content hash from the prior commit
was also only computed behind the community_cache gate, so with the
Settings default (memory auto, community disabled) nothing persisted.

Add _persistent_lora_key(item): file items key on their name (unchanged);
captured items (_precomputed_diffs) key on `captured:<content_hash>`,
memoized on the item dict via _memo_content_hash (content-hashing captured
factors is expensive; the same active_loras dicts are reused across every
candidate + memory + community lookup in a run). Route it through the
persistent-memory key (_mem_key), the names-only fallback scan, the
persisted source_loras, the settings-hash signature
(_per_lora_merge_signature), and both analysis-cache keys
(_lora_identity_hash, _compute_names_only_hash). Reuse the memo in the
community loop. Every change is guarded on _precomputed_diffs — the
file-based path is byte-identical.

Gating tests: a memory entry saved in "session 1" (one salt) is found in
"session 2" (different salt, identical captured content) via the real
auto_tune; persistent/identity/names-only/signature keys stable across
salts for captured, unchanged for file items. The cross-session test fails
pre-fix (session 2 re-sweeps).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:59:40 +02:00
EthanfelandClaude Fable 5 9ce78a8aa0 feat(inline): run the AutoTuner (search/memory/community) on captured chains
execute_inline's autotuner-mode settings previously set autotuner_fallback
and merged with optimizer defaults ("not supported inline"). Now that
captured items have a stable content-based identity (prior commit),
delegate mode=="autotuner" to a LoRAAutoTuner exactly like
LoRAOptimizerSimple — but on the inline-prepared STRIPPED model/clip and
the VIRTUAL stack (never the raw model / original chain), with
normalize_keys forced "disabled" (captured keys are already canonical).
auto_tune runs candidates through optimize_merge, which already handles
_precomputed_diffs, and applies the winner off the passed stack (no file
reload). The 6-tuple return is reconciled to the inline 5-tuple with the
chain fingerprint prepended. Advanced-mode and no-settings paths are
unchanged; the fallback message now only fires for genuinely unknown modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:34:58 +02:00
EthanfelandClaude Fable 5 c2392aa216 feat(inline): stable content hash for captured adapters (enables inline memory/community identity)
The file-less fallback in _lora_content_hash crashed on tuple keys
(k.encode()) and hashed captured adapter/diff objects via repr() — their
memory address — so the persistent memory/community key was unstable
across runs (and even collided across GC address reuse). Hash the actual
factor VALUES instead: iterate keys in stringified-sorted order, and for
each value recurse into tensors (shape + float32 bytes), adapter .weights
(LoRAAdapter/LoKr/LoHa), and diff/tuple/list payloads. Plain-tensor dicts
(LoRAExtractFromModel) and the file-based path stay byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:28:57 +02:00
EthanfelandClaude Fable 5 cbe1a0e76f fix(inline): read true LoRA rank from captured adapters in analysis (was always 1, over-compressed high-rank inline merges)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:18:01 +02:00
EthanfelandClaude Fable 5 93ba7b03c1 fix(inline): detect architecture from captured model-space keys (LTX/DiT no longer mis-detected as sd_unet)
Inline-captured chains are 100% virtual (_precomputed_diffs) items, which
_normalize_stack's architecture detection skipped entirely -> _detected_arch
stayed None -> _resolve_arch_preset("auto","unknown") fell through to sd_unet.
LTX/Z-Image/other DiT merges then got SD/SDXL conflict thresholds, density
floors, and per-prefix gates applied.

Virtual items are keyed by MODEL-SPACE target keys, which still carry
structural architecture markers (diffusion_model.transformer_blocks... for LTX,
diffusion_model.layers.N.attention... for Z-Image). Add a detection FALLBACK in
_normalize_stack: file-based items are tried first exactly as before; only if
still unknown do we detect on the virtual items' stringified key view (fused-QKV
tuple keys handled, real lora dict never mutated). The normalization loop's
virtual-skip is unchanged. execute_inline now detects the arch from the captured
keys too and surfaces it in the fingerprint ("architecture: ltx (detected from
captured keys)"), only warning "unknown" when detection genuinely fails and no
preset is pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:22:31 +02:00
EthanfelandClaude Fable 5 45189919e6 refactor(inline): move per-LoRA option widgets to the chain_options input
Slim LoRAOptimizerInline down to model/output_strength/
clip_strength_multiplier + optional clip/settings/chain_options sockets.
The per-LoRA options (settings_visibility, lora_count, and the 7 per-slot
widgets) now arrive through the LoRAInlineChainOptions side node as a
LORA_CHAIN_OPTIONS payload; unconnected merges every captured LoRA with
default options.

- execute_inline derives visibility/slots from chain_options (or
  ''simple''/[] when unconnected) instead of building slots from
  per-slot kwargs.
- The ''slots vs LoRAs detected'' fingerprint note only fires when an
  options node is connected (fp_lora_count = len(slots) if connected else
  the group count).
- IS_CHANGED keys on chain_options content instead of the per-slot loop.

_chain_groups_to_stack, the name salt, the pinned normalize_keys /
cache_patches, strip/merge/re-apply flow, _SLOT_DEFAULTS, MAX_LORAS,
_payload_rank, _chain_fingerprint, and _prepend_report are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:05:07 +02:00
EthanfelandClaude Fable 5 293f2d2b1a fix(inline): treat alpha=None adapters (PEFT/diffusers LoRAs) as scale 1.0 in _expand_patch_to_diff
A captured comfy LoRAAdapter has alpha=None when the source LoRA file
carries no .alpha key (PEFT / diffusers / AI-Toolkit format). The
plain-LoRA branch of _expand_patch_to_diff divided alpha/rank
unconditionally, crashing with 'unsupported operand type(s) for /:
NoneType and int' the first time two such LoRAs merged through the
inline node. Comfy (weight_adapter/lora.py) and our own sibling
LoKr/LoHa branches already treat None alpha as scale 1.0; this aligns
the plain-LoRA branch.

Only the inline capture path feeds comfy's raw None-alpha adapters into
this shared code (via _precomputed_diffs -> _expand_patch_to_diff); the
normal Stack path defaults alpha to rank upstream, so it never hit this.
Grep of all alpha/rank sites confirmed no other unguarded division is
reachable with a raw captured adapter (the single-LoRA fast path and
_build_exact_linear_patch are both skipped for _precomputed_diffs, and
_get_lora_key_info cannot resolve their model-target keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:59:56 +02:00
EthanfelandClaude Fable 5 a93aa6a131 feat(inline): add LoRAInlineChainOptions side node
Introduce a pure widget->data node that carries the per-LoRA option
widgets (settings_visibility, lora_count, and the 7 per-slot widgets)
and emits a LORA_CHAIN_OPTIONS payload {visibility, slots}. Reuses
LoRAOptimizerInline._SLOT_DEFAULTS as the single source for the 7 slot
keys + defaults so the two nodes cannot drift. Registered in both node
mappings. The inline node still carries its own widgets for now; the
next commit slims it to consume this input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:56:00 +02:00
EthanfelandClaude Fable 5 8f923b17c0 test(inline): harden e2e assertions (pre-run snapshot, merge direction, output_strength separation)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:50:15 +02:00
EthanfelandClaude Fable 5 8176112751 feat(inline): register LoRAOptimizerInline node
Adds the node to NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS next to
the other optimizer entries as "LoRA Optimizer (Inline Chain)", with a
registration test (class identity + display name). Also clarifies the
IS_CHANGED docstring: under live ComfyUI, linked inputs (model/clip/
settings) arrive as None — upstream changes invalidate via the executor's
ancestor cache keys; the patches_uuid terms matter for harness/direct
callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:30:05 +02:00
EthanfelandClaude Fable 5 0518924077 test(inline): end-to-end multi-LoRA chain capture through real optimize_merge
Closes the >= 2-LoRA e2e gap left after Task 5 (single-active-LoRA was
already covered by TestSingleLoraVirtualPath): two rank-4 adapters on the
same 16x16 layer key at loader strengths 1.0 / 0.7, captured off a fake
ModelPatcher chain and driven through execute_inline into the REAL
optimize_merge (Pass 1 conflict analysis + Pass 2 group merge on virtual
items).

- Numeric golden: with a weighted_sum override via an advanced Settings
  dict (the node's supported surface for engine kwargs — execute_inline
  deliberately forwards no loose kwargs), the applied patch is verified
  exactly equal to 1.0*(upA@downA) + 0.7*(upB@downB); also asserts the
  fingerprint report, add_patches on a clone (returned model IS that
  clone), and the untouched original chain entries on the input model.
- Conflict e2e: deliberately near-antiparallel up/down matrices exercise
  the per_prefix conflict-analysis path; asserts completion, finite
  nonzero merged patch, fingerprints.
- Test-only refactor: extracted TestSingleLoraVirtualPath's fake-model
  threading into a shared _pipeline_model helper and fixed its clone
  fidelity (clones now copy the CURRENT patch lists, like the real
  ModelPatcher.clone, so a clone of the stripped clone stays stripped).
- _adapter now accepts explicit up/down matrices for deterministic diffs.

No engine changes were needed — the virtual-item pipeline handled the
multi-LoRA path as-is. Determinism knobs (cache_patches/patch_compression
disabled, CPU SVD) ride through the existing settings delegation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:29:11 +02:00
EthanfelandClaude Fable 5 637dccfea7 fix(inline): dedicated IS_CHANGED + honest passthrough/arch reporting
- IS_CHANGED: the inherited LoRAOptimizer signature expects lora_stack and
  raises on inline's inputs -> ComfyUI treated the node as always-changed
  and re-merged on every queue press (the in-node merge cache cannot
  rescue it: its key folds in id() of the fresh stripped clone). The new
  classmethod keys on model/clip patches_uuid + consulted widgets (slots
  1..lora_count only) + settings content, so ComfyUI's execution cache
  skips unchanged re-queues entirely. Salt comment corrected to
  defense-in-depth (S3).
- Passthrough branch: patches present but zero capturable groups now
  reports "N non-mergeable patch entries ... passed through" instead of
  the wrong "place this node AFTER your loaders" advice (I1).
- Passthrough count includes clip-side remainder (S1).
- Fingerprint warns "architecture: unknown (inline capture)" whenever the
  effective preset is auto — virtual items skip key-based detection; a
  Settings-node preset suppresses the warning (I2).
- _payload_rank returns None for LoKr (weights[1] is the w2 Kronecker
  factor, not a rank) so the fingerprint says "dense" instead of lying (S2).
- New execute-level test: advanced-visibility per-slot widgets reach the
  stack items through execute_inline (S4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:14:31 +02:00
EthanfelandClaude Fable 5 28f2ba7a00 fix(inline): route single-LoRA virtual stacks through the full merge pipeline
optimize_merge's single-LoRA fast path handed the item's lora dict to
comfy.sd.load_lora_for_models, which resolves TRAINER-format keys
({x}.lora_up.weight). Virtual chain items carry model-target keys, so it
loaded nothing, silently — and since the inline node had already stripped
the originals, a 1-LoRA chain (or N LoRAs with all but one disabled) came
out with NO LoRA at all. Guard the fast path on _precomputed_diffs; the
diff pipeline handles 1-item virtual stacks (single-contributor
weighted_sum + native low-rank patch emission).

Tests use a realistic load_lora_for_models stand-in that mimics comfy's
key resolution (records calls, matches only trainer-format keys) — the
default pass-the-model-through stub could never catch this failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:10:25 +02:00
EthanfelandClaude Fable 5 75aafa9319 feat(inline): LoRAOptimizerInline node — chain capture, strip, merge, re-apply
INPUT_TYPES (widgets in required, clip/settings as optional sockets,
clip_strength_multiplier required for a predictable JS control-widget
count), execute_inline flow (reconstruct chain groups -> build salted
virtual stack -> strip captured entries from clones -> optimize_merge
with normalize_keys pinned to "disabled" -> prepend fingerprint report),
per-group fingerprint report with slot-count and model/clip group-count
mismatch warnings plus passthrough-entry count, and advanced-settings
delegation (AutoTuner settings fall back to defaults with a report line).

Virtual item names are salted with the source patcher patches_uuid
digests (model + clip when connected) so two different LoRA files loaded
at identical strengths cannot false-hit the in-node merge cache; the
salt rides an optional name_salt parameter on _chain_groups_to_stack so
existing callers/tests are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:19:53 +02:00
EthanfelandClaude Fable 5 a45421aa87 fix(inline): keep clip-side chain contributions alive through the zero-strength filter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:06:26 +02:00
EthanfelandClaude Fable 5 2e8a4efd37 feat(inline): build virtual stack items from chain groups with slot options
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:49:15 +02:00
EthanfelandClaude Fable 5 40f30b3303 fix(inline): harden chain-capture classification and collision repair
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:42:17 +02:00
EthanfelandClaude Fable 5 1aaa3dedff feat(inline): strip captured LoRA entries from patcher clones
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:21:21 +02:00
EthanfelandClaude Fable 5 cc9bf7485d feat(inline): reconstruct ordered per-LoRA groups from ModelPatcher patches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:19:50 +02:00
EthanfelandClaude Fable 5 dadb629ea2 feat(inline): classify capturable LoRA-family patch entries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:17:52 +02:00