invoke-ai/InvokeAI · error · ValueError

Must provide the same number of `block_out_channels` as `dow

Error message

Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}.

What it means

The hotfixed ControlNetModel.__init__ in invokeai/backend/util/hotfixes.py requires block_out_channels and down_block_types to be equal-length tuples, mirroring diffusers' UNet/ControlNet config validation. Each down_block_type (e.g. 'CrossAttnDownBlock2D', 'DownBlock2D') needs a matching channel count. Mismatched lengths mean the block stack cannot be constructed, so a ValueError is raised at model instantiation.

Source

Thrown at invokeai/backend/util/hotfixes.py:162

        conditioning_embedding_out_channels: Optional[Tuple[int]] = (16, 32, 96, 256),
        global_pool_conditions: bool = False,
        addition_embed_type_num_heads=64,
    ):
        super().__init__()

        # If `num_attention_heads` is not defined (which is the case for most models)
        # it will default to `attention_head_dim`. This looks weird upon first reading it and it is.
        # The reason for this behavior is to correct for incorrectly named variables that were introduced
        # when this library was created...
        # The incorrect naming was only discovered much ...
        # later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131
        # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking
        # which is why we correct for the naming here.
        num_attention_heads = num_attention_heads or attention_head_dim

        # Check inputs
        if len(block_out_channels) != len(down_block_types):
            raise ValueError(
                f"Must provide the same number of `block_out_channels` as `down_block_types`. \
                    `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}."
            )

        if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types):
            raise ValueError(
                f"Must provide the same number of `only_cross_attention` as `down_block_types`. \
                    `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}."
            )

        if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types):
            raise ValueError(
                f"Must provide the same number of `num_attention_heads` as `down_block_types`. \
                    `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}."
            )

        if isinstance(transformer_layers_per_block, int):
            transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types)

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Make len(block_out_channels) equal len(down_block_types) — add or remove entries so every down block has a channel count.
  2. Load the original model's config.json and use its exact block_out_channels/down_block_types rather than editing by hand.
  3. Build the config programmatically, e.g. derive block_out_channels from down_block_types length.
  4. Re-download the model config if the file was truncated or corrupted.

Example fix

// before
ControlNetModel(
    down_block_types=("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D", "DownBlock2D"),
    block_out_channels=(320, 640, 1280),
)
// after
ControlNetModel(
    down_block_types=("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D", "DownBlock2D"),
    block_out_channels=(320, 640, 1280, 1280),
)
Defensive patterns

Strategy: validation

Validate before calling

def validate_controlnet_config(config: dict) -> None:
    d = len(config['down_block_types'])
    o = len(config['block_out_channels'])
    if d != o:
        raise ValueError(
            f'block_out_channels ({o}) must match down_block_types ({d})'
        )

validate_controlnet_config(config_dict)
ControlNetModel.from_config(config_dict)

Type guard

def has_matching_block_lengths(config: dict) -> bool:
    n = len(config.get('down_block_types', []))
    return (
        isinstance(config.get('block_out_channels'), (list, tuple))
        and len(config['block_out_channels']) == n
    )

Try / catch

try:
    model = ControlNetModel.from_config(config_dict)
except ValueError as e:
    if 'block_out_channels' in str(e) and 'down_block_types' in str(e):
        n = len(config_dict['down_block_types'])
        config_dict['block_out_channels'] = (
            list(config_dict['block_out_channels']) + [1280] * (n - len(config_dict['block_out_channels']))
        )[:n]
        model = ControlNetModel.from_config(config_dict)
    else:
        raise

Prevention

When it happens

Trigger: Instantiating ControlNetModel (or ControlNetModel.from_config) with block_out_channels=(320,640,1280) but down_block_types of length 4 (or vice versa), typically from a hand-edited config.json or a programmatically built config dict.

Common situations: Hand-writing a ControlNet config for a custom architecture; copying config fields from one model into another with a different depth; omitting entries when extending down_block_types; a corrupted/partially downloaded config.json for a model.

Related errors


AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29). Data as JSON: /api/errors/c82933bf04370b1d. Report an issue: GitHub.