unslothai/unsloth · error · ValueError

ControlNet '{spec_id}' is for {', '.join(entry.families)}, n

Error message

ControlNet '{spec_id}' is for {', '.join(entry.families)}, not the loaded '{family}' model; pick a ControlNet built for this family.

What it means

ControlNets are architecture-specific: resolve_controlnet() looks the spec_id up in the curated catalog (including full-repo-id matches) and rejects an entry whose families list does not contain the loaded model's family, before any download happens. The family check also catches full-repo-id lookups so a curated entry named by repo id cannot bypass the gate via the bare-repo fallback.

Source

Thrown at studio/backend/core/inference/diffusion_controlnet.py:171

def resolve_controlnet(spec_id: str, *, family: Optional[str] = None) -> ResolvedControlNet:
    """Resolve a ControlNet id to a loadable repo id / local dir.

    Accepts a catalog/local id or a bare HF repo id (``owner/name``); the backend loads it with
    ``from_pretrained``. Raises on an unknown id (caller maps to 400).

    ``family`` enforces compatibility: a ControlNet is architecture-specific, so an entry tagged
    for another family is rejected here rather than loaded through the wrong pipeline later.
    """
    entry = _catalog_by_id().get(spec_id)
    if entry is None:
        # A curated entry named by its full repo id must still hit the family gate below, not slip through the bare-repo fallback.
        entry = next((e for e in _CURATED if e.repo_id and e.repo_id == spec_id), None)
    if entry is not None:
        # A direct API call could send an entry for another family; reject it before any download.
        fam = (family or "").strip().lower()
        if entry.families and fam and fam not in {f.lower() for f in entry.families}:
            raise ValueError(
                f"ControlNet '{spec_id}' is for {', '.join(entry.families)}, not the loaded "
                f"'{family}' model; pick a ControlNet built for this family."
            )
        if entry.source == "local":
            path = entry.local_path or ""
            if not path or not Path(path).is_dir():
                raise FileNotFoundError(f"ControlNet '{spec_id}' is no longer present on disk")
            return ResolvedControlNet(spec_id, path, is_local = True)
        if not entry.repo_id:
            raise ValueError(f"ControlNet '{spec_id}' has no repo")
        return ResolvedControlNet(spec_id, entry.repo_id, is_local = False)

    # A bare HF repo id (owner/name). STRICT shape so a filesystem-looking id can never reach from_pretrained.
    if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*", spec_id):
        return ResolvedControlNet(spec_id, spec_id, is_local = False)

    raise FileNotFoundError(
        f"unknown ControlNet '{spec_id}': not a local model, catalog entry, or HF repo id"

View on GitHub (pinned to 203007d190)

Solutions

  1. Pick a ControlNet whose catalog entry lists the loaded family (check entry.families).
  2. After switching the loaded model, re-resolve the ControlNet for the new family before generating.
  3. Expose family info in your picker UI so CN options filter by the loaded model.

Example fix

# before: SDXL CN with a Flux model loaded
engine.generate(prompt=p, controlnet=("xinsir-controlnet-v1.1-sdxl-canny", img, "canny", 0.8, 0.0, 1.0))
# after: Flux-family CN
engine.generate(prompt=p, controlnet=("instantx/flux-union-controlnet", img, "canny", 0.8, 0.0, 1.0))
Defensive patterns

Strategy: validation

Validate before calling

from core.inference import diffusion_controlnet

def cn_matches_family(spec_id: str, family: str) -> bool:
    entry = diffusion_controlnet._catalog_by_id().get(spec_id)
    if entry is None:
        return False
    fam = (family or "").strip().lower()
    return not entry.families or not fam or fam in {f.lower() for f in entry.families}

Type guard

def controlnet_suitable_for(spec_id: str, family: str) -> bool:
    return cn_matches_family(spec_id, family)

Try / catch

try:
    out = engine.generate(prompt=p, controlnet=(cn_id, img, t, s, gs, ge))
except ValueError as e:
    if "is for" in str(e) and "pick a ControlNet built for this family" in str(e):
        cn_id = default_cn_for(family=state.family.name)  # re-pick from catalog by family
        out = engine.generate(prompt=p, controlnet=(cn_id, img, t, s, gs, ge))
    else:
        raise

Prevention

When it happens

Trigger: Passing a ControlNet id tagged for another family — e.g. an SDXL ControlNet ('xinsir ... sdxl ...') while a FLUX model is loaded, or a Flux Union CN while SDXL is loaded. The loaded family is passed in as family=state.family.name.

Common situations: Switching loaded models without re-picking the ControlNet; curated lists mixing SD/SDXL/Flux entries with similar names; automated pipelines pairing a fixed CN repo with a rotating set of base models.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/e058681eebe0a12b. Report an issue: GitHub.