vLLM Explained

model_executor/models/registry.py: the model registry

IntermediateCode mapabout 9 min read + 3 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/model_executor/models/registry.py maps an architecture name from config.json to a model class, and tells the rest of vLLM what that class supports, without an import of the model in the front end.

Filevllm/model_executor/models/registry.py, 1,623 lines of Python. About 820 lines are data tables (lines 73-887).
Main objectsModelRegistry, the one _ModelRegistry object (vllm/model_executor/models/registry.py:1565); classes _ModelInfo, _RegisteredModel, _LazyRegisteredModel, _ModelRegistry.
Runs inEvery process that imports it. In vllm serve: the front end inspects (in ModelConfig); each worker resolves and imports the class; plugins call register_model everywhere. The file is also a script: python -m vllm.model_executor.models.registry is the inspection subprocess.
Called byModelConfig through its registry property (vllm/config/model.py:1165-1168), the model loader (vllm/model_executor/model_loader/utils.py:310), the speculative config for draft models (vllm/config/speculative.py:1779), some platform and KV-planning code, and general plugins through from vllm import ModelRegistry (vllm/__init__.py:26).
CallsThe detector functions of interfaces.py and interfaces_base.py (vllm/model_executor/models/registry.py:47-69), importlib, subprocess with cloudpickle, transformers (Transformers backend checks), safe_hash, current_platform.verify_model_arch.
Not in this fileThe model classes, the capability protocols (interfaces.py, page Model capability protocols (SupportsPP, SupportsLoRA, ...)), the conversion adapters for --convert (adapters.py), and the plugin loader.

The page Out-of-tree models and the model registry explains the registry from the user's side, with a plugin example. This page reads the file section by section.

Where it sits#

Figure: the three jobs of this file: hold entries, look them up, and inspect lazy entries.
Open full size ↗

Map of the file#

LinesSection
vllm/model_executor/models/registry.py:1-69Docstring (update tests/models/registry.py with each new model), imports. Line 26 imports vllm.config, so the inspection subprocess resolves current_platform early.
vllm/model_executor/models/registry.py:73-804Ten tables of built-in architectures (summary below).
vllm/model_executor/models/registry.py:806-817_VLLM_MODELS: all ten merged.
vllm/model_executor/models/registry.py:819-823_SUBPROCESS_COMMAND.
vllm/model_executor/models/registry.py:825-887_PREVIOUSLY_SUPPORTED_MODELS (50) and _OOT_SUPPORTED_MODELS (5): only for error texts.
vllm/model_executor/models/registry.py:890-954_ModelInfo and from_model_cls.
vllm/model_executor/models/registry.py:957-986_BaseRegisteredModel and the eager _RegisteredModel.
vllm/model_executor/models/registry.py:989-1122The lazy _LazyRegisteredModel with its disk cache.
vllm/model_executor/models/registry.py:1125-1150Module-level _try_load_model_cls / _try_inspect_model_cls with lru_cache.
vllm/model_executor/models/registry.py:1152-1553_ModelRegistry: register, look up, error texts, Transformers fallback, twelve one-field helpers.
vllm/model_executor/models/registry.py:1556-1573_resolve_module_name and the ModelRegistry object.
vllm/model_executor/models/registry.py:1575-1623_run_in_subprocess, _run and the __main__ block.

Who calls it#

What it calls#

The tables, summarized#

TableLinesEntriesExamples
_TEXT_GENERATION_MODELSvllm/model_executor/models/registry.py:73-215121LlamaForCausalLM, Qwen3MoeForCausalLM
_EMBEDDING_MODELSvllm/model_executor/models/registry.py:217-26838BertModel; a comprehension copies all LlamaForCausalLM-class names (lines 232-237)
_LATE_INTERACTION_MODELSvllm/model_executor/models/registry.py:270-28713HF_ColBERT
_REWARD_MODELSvllm/model_executor/models/registry.py:289-2933Qwen2ForRewardModel
_TOKEN_CLASSIFICATION_MODELSvllm/model_executor/models/registry.py:295-3168BertForTokenClassification
_SEQUENCE_CLASSIFICATION_MODELSvllm/model_executor/models/registry.py:318-34611cross-encoders
_MULTIMODAL_MODELSvllm/model_executor/models/registry.py:348-645126Qwen2VLForConditionalGeneration
_SPECULATIVE_DECODING_MODELSvllm/model_executor/models/registry.py:647-73266EagleLlamaForCausalLM, MTP heads
_TRANSFORMERS_SUPPORTED_MODELSvllm/model_executor/models/registry.py:734-76923names served by the Transformers backend
_TRANSFORMERS_BACKEND_MODELSvllm/model_executor/models/registry.py:771-80410TransformersForCausalLM

The merged dict has 406 unique names. The counts come from an evaluation of the literals at this commit (derived); 13 names occur in two tables with the same value, so the 10 tables hold 419 entries in total; the later table wins in the merge. 36 entries point to vllm.models.* packages (What vLLM is today).

Key classes and functions#

NameLineWhat it does
_ModelInfovllm/model_executor/models/registry.py:890-915Frozen record of 24 capability fields.
_ModelInfo.from_model_clsvllm/model_executor/models/registry.py:917-954Runs the detectors on a class.
_RegisteredModelvllm/model_executor/models/registry.py:967-986Eager entry: the class and its record.
_LazyRegisteredModelvllm/model_executor/models/registry.py:988-1122Lazy entry: module and class names; cached inspection.
_try_load_model_cls, _try_inspect_model_clsvllm/model_executor/models/registry.py:1125-1150Cached wrappers that log an exception and return None.
_ModelRegistry.register_modelvllm/model_executor/models/registry.py:1160-1203Adds or replaces an entry.
_ModelRegistry._raise_for_unsupportedvllm/model_executor/models/registry.py:1205-1236Four ValueError texts.
_ModelRegistry._try_resolve_transformersvllm/model_executor/models/registry.py:1250-1323Name of a Transformers backend class, or None.
_ModelRegistry._normalize_archvllm/model_executor/models/registry.py:1325-1349Maps a converted name to its base name.
_ModelRegistry.inspect_model_clsvllm/model_executor/models/registry.py:1351-1402Lookup order → (record, name).
_ModelRegistry.resolve_model_clsvllm/model_executor/models/registry.py:1404-1456Same order → (class, name).
is_text_generation_model ... is_transcription_only_modelvllm/model_executor/models/registry.py:1458-1553Twelve one-field helpers over inspect_model_cls (one of them is model_has_inner_state).
ModelRegistryvllm/model_executor/models/registry.py:1565-1573The object, filled with lazy entries.
_run_in_subprocess, _runvllm/model_executor/models/registry.py:1578-1619Parent and child of the inspection subprocess.

Walk through the main methods#

_LazyRegisteredModel: the disk cache#

    @staticmethod
    def _get_cache_dir() -> Path:
        return Path(envs.VLLM_CACHE_ROOT) / "modelinfos"

    def _get_cache_filename(self) -> str:
        cls_name = f"{self.module_name}-{self.class_name}".replace(".", "-")
        return f"{cls_name}.json"

    @staticmethod
    def _get_modelinfo_module_hash(model_path: Path) -> str:
        if model_path.name == "__init__.py":
            # Package entry points often re-export classes implemented in
            # submodules, so include the package contents in the cache key.
            module_paths = sorted(model_path.parent.rglob("*.py"))
            root_path = model_path.parent
        else:
            module_paths = [model_path]
            root_path = model_path.parent

        hasher = safe_hash(b"", usedforsecurity=False)
        for path in module_paths:
            hasher.update(path.relative_to(root_path).as_posix().encode("utf-8"))
            hasher.update(b"\0")
            hasher.update(path.read_bytes())
            hasher.update(b"\0")
        return hasher.hexdigest()

    def _load_modelinfo_from_cache(self, module_hash: str) -> _ModelInfo | None:
        try:
            try:
                modelinfo_path = self._get_cache_dir() / self._get_cache_filename()
                with open(modelinfo_path, encoding="utf-8") as file:
                    mi_dict = json.load(file)
            except FileNotFoundError:
                logger.debug(
                    "Cached model info file for class %s.%s not found",
                    self.module_name,
                    self.class_name,
                )
                return None

            if mi_dict["hash"] != module_hash:
                logger.debug(
                    "Cached model info file for class %s.%s is stale",
                    self.module_name,
                    self.class_name,
                )
                return None

            # file not changed, use cached _ModelInfo properties
            return _ModelInfo(**mi_dict["modelinfo"])
        except Exception:
            logger.debug(
                "Cached model info for class %s.%s error. ",
                self.module_name,
                self.class_name,
            )
            return None
  1. Line 997. The directory is $VLLM_CACHE_ROOT/modelinfos.
  2. Lines 1000-1001. The file name is module-Class.json with every dot replaced by "-".
  3. Lines 1005-1009. For a package __init__.py, all .py files below the package are hashed, sorted; else only the one file.
  4. Lines 1014-1020. The hash takes the relative path, a zero byte, the file bytes and a zero byte, for each file. safe_hash is md5, or sha256 where md5 is not allowed (vllm/utils/hashing.py:118-121).
  5. Lines 1036-1042. A different hash means "stale": DEBUG log, None.
  6. Line 1045. A hit rebuilds _ModelInfo from the JSON dict. Any other error also returns None with a DEBUG log (lines 1046-1052).

The save writes {"hash": ..., "modelinfo": asdict(mi)} through atomic_writer, and only logs an exception on failure (vllm/model_executor/models/registry.py:1054-1069). The method that uses both is inspect_model_cls (vllm/model_executor/models/registry.py:1071-1118), walked on Out-of-tree models and the model registry.

_try_inspect_model_cls: the error filter#

@lru_cache(maxsize=128)
def _try_load_model_cls(
    model_arch: str,
    model: _BaseRegisteredModel,
) -> type[nn.Module] | None:
    from vllm.platforms import current_platform

    current_platform.verify_model_arch(model_arch)
    try:
        return model.load_model_cls()
    except Exception:
        logger.exception("Error in loading model architecture '%s'", model_arch)
        return None


@lru_cache(maxsize=128)
def _try_inspect_model_cls(
    model_arch: str,
    model: _BaseRegisteredModel,
) -> _ModelInfo | None:
    try:
        return model.inspect_model_cls()
    except Exception:
        logger.exception("Error in inspecting model architecture '%s'", model_arch)
        return None
  1. Lines 1125, 1140. lru_cache(maxsize=128) keyed by (name, entry). Entries are frozen dataclasses, so they are hashable and compare by value. A new registration with another target is a new key; the same module and class strings give an equal key.
  2. Line 1132. Before an import, the platform may reject the architecture.
  3. Lines 1135-1137, 1147-1149. Any exception is logged with its traceback ("Error in loading/inspecting model architecture"), and the result is None. The caller then tries the next step of the lookup.

resolve_model_cls: the worker's lookup#

    def resolve_model_cls(
        self,
        architectures: str | list[str],
        model_config: ModelConfig,
    ) -> tuple[type[nn.Module], str]:
        if isinstance(architectures, str):
            architectures = [architectures]
        if not architectures:
            raise ValueError("No model architectures are specified")

        # Require transformers impl
        if model_config.model_impl == "transformers":
            arch = self._try_resolve_transformers(architectures[0], model_config)
            if arch is not None:
                model_cls = self._try_load_model_cls(arch)
                if model_cls is not None:
                    return (model_cls, arch)
        elif model_config.model_impl == "terratorch":
            arch = "Terratorch"
            model_cls = self._try_load_model_cls(arch)
            if model_cls is not None:
                return (model_cls, arch)

        # Fallback to transformers impl (after resolving convert_type)
        if (
            all(arch not in self.models for arch in architectures)
            and model_config.model_impl == "auto"
            and getattr(model_config, "convert_type", "none") == "none"
        ):
            arch = self._try_resolve_transformers(architectures[0], model_config)
            if arch is not None:
                model_cls = self._try_load_model_cls(arch)
                if model_cls is not None:
                    return (model_cls, arch)

        for arch in architectures:
            normalized_arch = self._normalize_arch(arch, model_config)
            model_cls = self._try_load_model_cls(normalized_arch)
            if model_cls is not None:
                return (model_cls, arch)

        # Fallback to transformers impl (before resolving runner_type)
        if (
            all(arch not in self.models for arch in architectures)
            and model_config.model_impl == "auto"
        ):
            arch = self._try_resolve_transformers(architectures[0], model_config)
            if arch is not None:
                model_cls = self._try_load_model_cls(arch)
                if model_cls is not None:
                    return (model_cls, arch)

        return self._raise_for_unsupported(architectures)
  1. Lines 1414-1425. transformers or terratorch implementation first.
  2. Lines 1427-1437. Transformers fallback when no name is registered and no conversion is set.
  3. Lines 1439-1443. Each name, normalized; the first class that imports wins. The returned name is the original one, not the normalized one.
  4. Lines 1445-1454. The fallback again, without the conversion condition.
  5. Line 1456. _raise_for_unsupported.

inspect_model_cls (lines 1351-1402) is the same with _try_inspect_model_cls, except one detail: for terratorch it asserts that inspection worked (line 1370).

_raise_for_unsupported: four messages#

    def _raise_for_unsupported(self, architectures: list[str]):
        all_supported_archs = self.get_supported_archs()

        if any(arch in all_supported_archs for arch in architectures):
            raise ValueError(
                f"Model architectures {architectures} failed "
                "to be inspected. Please check the logs for more details."
            )

        for arch in architectures:
            if arch in _PREVIOUSLY_SUPPORTED_MODELS:
                previous_version = _PREVIOUSLY_SUPPORTED_MODELS[arch]

                raise ValueError(
                    f"Model architecture {arch} was supported in vLLM until "
                    f"v{previous_version}, and is not supported anymore. "
                    "Please use an older version of vLLM if you want to "
                    "use this model architecture."
                )
            if arch in _OOT_SUPPORTED_MODELS:
                plugin_url = _OOT_SUPPORTED_MODELS[arch]

                raise ValueError(
                    f"Model architecture {arch} is not supported in-tree anymore. "
                    f"Please install the plugin at {plugin_url} if you want to "
                    "use this model architecture."
                )

        raise ValueError(
            f"Model architectures {architectures} are not supported for now. "
            f"Supported architectures: {all_supported_archs}"
        )

The order matters: a registered name that failed always gets "failed to be inspected" (lines 1208-1212), even when it is also in a removed-models table. The last message lists all supported names, so it can be long (lines 1233-1236).

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Data flow: types in and out

ValueTypeWhere
table valuetuple[str, str]: (module, class); a module without vllm. is relative to vllm.model_executor.modelsvllm/model_executor/models/registry.py:1556-1562
_ModelRegistry.modelsdict[str, _BaseRegisteredModel]vllm/model_executor/models/registry.py:1154-1155
get_supported_archs()a live view of the dict keys (Set[str])vllm/model_executor/models/registry.py:1157-1158
inspect_model_cls argumentstr | list[str]; empty → ValueErrorvllm/model_executor/models/registry.py:1356-1359
inspect_model_cls result(_ModelInfo, str)vllm/model_executor/models/registry.py:1355
subprocess stdincloudpickle of (fn, output_path)vllm/model_executor/models/registry.py:1587
subprocess outputpickle of _ModelInfo in registry_output.tmp of a temporary directoryvllm/model_executor/models/registry.py:1581-1582, vllm/model_executor/models/registry.py:1618-1619
Deep dive

Processes: who runs what

  • Every process builds ModelRegistry at import, with 406 lazy entries and no model import (vllm/model_executor/models/registry.py:1565-1573).
  • Every process that loads general plugins runs the plugins' register_model calls. The registry is a module global, so each process has its own copy.
  • The front end inspects. On a cache miss it starts one extra Python process per lazy entry; that child loads the general plugins too (vllm/model_executor/models/registry.py:1608-1612).
  • Each worker imports the chosen class through resolve_model_cls.

The child gets its command from _SUBPROCESS_COMMAND, a module list that the comment allows other packagers to change (vllm/model_executor/models/registry.py:819-823). The comment at lines 1589-1590 says why it uses -m: the module uses relative imports.

Deep dive

A worked example: the cache file of an out-of-tree package

A plugin registers register_model("MyLlava", "vllm_add_dummy_model.my_llava:MyLlava") (tests/plugins/vllm_add_dummy_model/vllm_add_dummy_model/__init__.py:26-27).

  1. The module name does not start with vllm.model_executor.models., so find_spec("vllm_add_dummy_model.my_llava") finds the file (lines 1080-1085). This imports the package vllm_add_dummy_model, but not my_llava (Python rule).
  2. The file is my_llava.py, not __init__.py, so the hash covers one file, with the relative path "my_llava.py".
  3. The cache file is $VLLM_CACHE_ROOT/modelinfos/vllm_add_dummy_model-my_llava-MyLlava.json (dots replaced; underscores stay).
  4. On a miss, the child imports my_llava and returns the record. supports_multimodal is then true if MyLlava inherits the multimodal protocol. (The class content is not shown here.)
Deep dive

Error paths: what raises, what only logs

SituationResultWhere
register_model with a non-string nameTypeErrorvllm/model_executor/models/registry.py:1175-1177
A string without exactly one colonValueErrorvllm/model_executor/models/registry.py:1187-1191
Neither string nor nn.Module classTypeErrorvllm/model_executor/models/registry.py:1196-1201
Name already registeredoverwrite, DEBUG logvllm/model_executor/models/registry.py:1179-1185
Cache file missing, stale or brokenDEBUG log; subprocessvllm/model_executor/models/registry.py:1028-1052
Cache write failslogger.exception; start-up goes onvllm/model_executor/models/registry.py:1068-1069
Subprocess exits with an errorRuntimeError with the child's stderr, caught and logged by the wrappervllm/model_executor/models/registry.py:1596-1602, vllm/model_executor/models/registry.py:1147-1149
No step finds a classValueError (four texts)vllm/model_executor/models/registry.py:1205-1236
--model-impl transformers with no Transformers classValueError "Cannot find model module"vllm/model_executor/models/registry.py:1297-1307
Deep dive

Environment variables

VariableDefaultRead atEffect
VLLM_CACHE_ROOT~/.cache/vllm, or $XDG_CACHE_HOME/vllm (vllm/envs.py:713-718)vllm/model_executor/models/registry.py:997Root of the modelinfos cache.
VLLM_PLUGINSnot setin the child, through load_general_plugins (vllm/model_executor/models/registry.py:1612)The child sees the same plugin filter as its parent (it inherits the environment).

Check yourself#

  1. 1
    How many names does the built-in registry hold at this commit, and what kind of entry is each?
    easy
    Show answer

    406 names in _VLLM_MODELS (derived count). Each becomes a _LazyRegisteredModel (vllm/model_executor/models/registry.py:1565-1573).

  2. 2
    Why does _try_inspect_model_cls return None instead of raising?
    medium
    Show answer

    So that the lookup can try the next step (another name, or the Transformers fallback). The exception is logged with its traceback (vllm/model_executor/models/registry.py:1141-1150); if all steps fail, _raise_for_unsupported raises "failed to be inspected" for a registered name.

  3. 3
    What is the cache file name for vllm.models.deepseek_v4:DeepseekV4ForCausalLM, and what does its hash cover?
    medium
    Show answer

    vllm-models-deepseek_v4-DeepseekV4ForCausalLM.json (vllm/model_executor/models/registry.py:999-1001). The source is the package __init__.py, so the hash covers every .py file under vllm/models/deepseek_v4/ (vllm/model_executor/models/registry.py:1005-1009).

  4. 4
    Config says ["MyForSequenceClassification"], only MyForCausalLM is registered, and the user passes --convert classify. Which class do the workers build?
    hard
    Show answer

    The Transformers fallback before the loop is skipped because a conversion is set (vllm/model_executor/models/registry.py:1427-1437). In the loop, _normalize_arch matches the suffix ForSequenceClassification and finds MyForCausalLM (vllm/model_executor/models/registry.py:1335-1347). The loader then wraps it with as_seq_cls_model (vllm/model_executor/model_loader/utils.py:331-333).