Skip to content

fix: dedup duplicate controlnet model ids - #46

Open
forkni wants to merge 2 commits into
dotsimulate:SDTD_040_beta_releasefrom
forkni:pr/controlnet-dedup
Open

fix: dedup duplicate controlnet model ids#46
forkni wants to merge 2 commits into
dotsimulate:SDTD_040_beta_releasefrom
forkni:pr/controlnet-dedup

Conversation

@forkni

@forkni forkni commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What

Two ControlNet blocks pointing at the same model_id were not deduplicated
at startup — only the TouchDesigner live-update path (Cnblock()) applied
"highest weight wins". Startup duplicates loaded the model twice (double
VRAM) and conditioned the generated image additively, and the live path
could not repair what startup produced.

Root cause

_update_controlnet_config's current_models was index-keyed
({0: canny, 1: canny}) while desired_models was model_id-keyed
({canny: cfg}). The removal predicate (model_id not in desired_models)
never matched a duplicate whose model_id was still desired, so at most the
first duplicate index ever got updated.

Fix

  • New dedupe_controlnet_configs() helper in config.py — collapses
    duplicate model_ids to the highest conditioning_scale, first-occurrence
    order, logging every drop at INFO.
  • _prepare_controlnet_configs now dedups on load, closing the startup gap
    for every config source (TD, demo/realtime-img2img, examples/*).
  • _update_controlnet_config now (1) dedups the incoming desired_config
    through the same helper — this also closes a second creation vector: a
    stale, unrefreshed current_models snapshot could otherwise add the same
    not-yet-loaded model twice; (2) removes pre-existing duplicate indices from
    the already-loaded pipeline before the reorder/diff pass, so an
    already-running stream self-heals on its next live update; (3) recomputes
    current_models/current_config after those removals, fixing a latent
    stale-index/IndexError risk.

Blast radius

Reached via find_connections on the changed function: 22 symbols across 13
files, all pass-through consumers requiring no change — demo/realtime-img2img,
examples/benchmark/ab_bench.py, examples/config/config_video_test.py,
examples/config/config_ipadapter_stream_test.py, and the TouchDesigner
component's td_manager.py.

Tests

New tests/unit/test_controlnet_duplicate_dedup.py (8 tests): dedup keeps
highest scale + winner's preprocessor, pass-through when clean, pre-existing
duplicate removal on an already-loaded stream, the stale-current_models
creation-vector case (asserted on call count), non-duplicated setup left
intact, tie-breaks-to-first-occurrence, and a full startup→live end-to-end
collapse. Neighbor suites (test_cn_cache_decay.py,
test_controlnet_residual_merge.py, test_config_extraction_golden.py,
test_param_updater_binding.py — 31 tests) unaffected.

forkni added 2 commits August 4, 2026 14:52
ControlNet configs with duplicate model_ids were not deduplicated at
startup (only the live-update path deduped, and asymmetrically). Adds
a shared dedupe_controlnet_configs() helper: _prepare_controlnet_configs
now dedups on load, and _update_controlnet_config dedups the incoming
desired_config plus self-heals any pre-existing duplicate indices on an
already-running stream. Root cause: current_models was index-keyed while
desired_models was model_id-keyed, so the removal predicate never matched
a duplicate whose model_id was still desired.
pyrefly flagged order/best_by_model (typed str-keyed) receiving an int
from id(cfg) for the no-model_id fallback path in
dedupe_controlnet_configs. Wrap it in a string sentinel; behavior is
unchanged, type-checks clean now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant