invoke-ai/InvokeAI · info · NotAMatchError

directory looks like a complete causal LM (config.json and t

Error message

directory looks like a complete causal LM (config.json and tokenizer files at root), not a standalone Qwen3 encoder

What it means

NotAMatchError raised in Qwen3Encoder_Qwen3Encoder_Config.from_model_on_disk (qwen3_encoder.py:318). The directory has config.json at root plus tokenizer files (tokenizer.json, tokenizer.model, or tokenizer_config.json). A standalone Qwen3 text-encoder download never bundles tokenizer files; their presence indicates a complete causal LM (TextLLM), so the config rejects the folder so the TextLLM config can match it.

Source

Thrown at invokeai/backend/model_manager/configs/qwen3_encoder.py:318

            raise NotAMatchError(
                "directory looks like a full diffusers pipeline (has model_index.json or transformer folder), "
                "not a standalone Qwen3 encoder"
            )

        # Check for text_encoder config - support both:
        # 1. Full model structure: model_root/text_encoder/config.json
        # 2. Standalone text_encoder download: model_root/config.json (when text_encoder subfolder is downloaded separately)
        config_path_nested = mod.path / "text_encoder" / "config.json"
        config_path_direct = mod.path / "config.json"

        if config_path_nested.exists():
            expected_config_path = config_path_nested
        elif config_path_direct.exists():
            # Standalone text_encoder downloads do not bundle tokenizer files. If we see tokenizer files at the
            # root next to config.json, this is a complete causal LM (TextLLM), not a Qwen3 encoder subfolder.
            tokenizer_files = ("tokenizer.json", "tokenizer.model", "tokenizer_config.json")
            if any((mod.path / f).exists() for f in tokenizer_files):
                raise NotAMatchError(
                    "directory looks like a complete causal LM (config.json and tokenizer files at root), "
                    "not a standalone Qwen3 encoder"
                )
            expected_config_path = config_path_direct
        else:
            raise NotAMatchError(
                f"unable to load config file(s): {{PosixPath('{config_path_nested}'): 'file does not exist'}}"
            )

        # Qwen3 uses Qwen2VLForConditionalGeneration or similar
        raise_for_class_name(expected_config_path, _QWEN3_ENCODER_ARCHITECTURES)

        # Reject SDNQ-quantized encoders so Qwen3Encoder_SDNQ_Folder_Config matches them instead.
        # A real SDNQ Qwen3 encoder has the same Qwen3 config class name as an unquantized one, so
        # without this guard both configs accept the folder — and since they share the Qwen3Encoder
        # type, the factory tiebreak is non-deterministic. If it picked this (unquantized) config,
        # the non-SDNQ loader would then mis-read the packed uint8 weights.
        cls._reject_if_sdnq_quantized(mod)

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Install the folder as a TextLLM / main model — that is what it is.
  2. If you need a Qwen3 encoder, download only the text_encoder subfolder contents (config.json + model weights, no tokenizer) into a dedicated directory.
  3. If the tokenizer files are accidental leftovers, remove them and rescan.
  4. Place the complete LM outside the directory the encoder scanner walks.

Example fix

// before
models/qwen3-encoder/  // config.json + tokenizer.json + safetensors (a full LM)
// after
models/qwen3-textllm/  // full LM, registered as TextLLM
models/qwen3-encoder/  // only config.json + model.safetensors
Defensive patterns

Strategy: validation

Validate before calling

def is_complete_causal_lm(path) -> bool:
    if not (path / 'config.json').exists():
        return False
    return any((path / f).exists() for f in ('tokenizer.json', 'tokenizer.model', 'tokenizer_config.json'))

Type guard

def is_standalone_qwen3_encoder_dir(path) -> bool:
    has_cfg = (path / 'config.json').exists() or (path / 'text_encoder' / 'config.json').exists()
    has_tokenizer = any((path / f).exists() for f in ('tokenizer.json', 'tokenizer.model', 'tokenizer_config.json'))
    return has_cfg and not has_tokenizer

Try / catch

if is_complete_causal_lm(model_dir):
    register_model(model_dir, model_type='TextLLM')
else:
    try:
        register_model(model_dir, model_type='Qwen3Encoder')
    except NotAMatchError as e:
        logger.warning('Not a standalone encoder: %s', e)

Prevention

When it happens

Trigger: from_model_on_disk scanning a directory where mod.path/config.json exists AND any of tokenizer.json / tokenizer.model / tokenizer_config.json exists at the root.

Common situations: Downloading a full Qwen3-4B/8B causal LM repo (which always ships tokenizer files) and expecting it to register as a Qwen3 text encoder; confusing Qwen3ForCausalLM checkpoints with the Z-Image text_encoder subfolder.

Related errors


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