vLLM Explained

Model capability protocols (SupportsPP, SupportsLoRA, ...)

AdvancedvLLM conceptsabout 9 min read + 3 min deep dives

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

In one sentence

A model class inherits capability protocols such as SupportsPP; vLLM records them in _ModelInfo and checks them before it starts the workers.

The idea#

vLLM serves hundreds of model classes, and each supports a different set of features. Pipeline parallelism needs a forward that accepts the hidden states of the previous stage. LoRA needs a map of the fused weight names. vLLM must know this before it starts the workers, so that it can reject a bad configuration early, with a clear message.

A model class declares a feature with a capability protocol: it adds a base class such as SupportsPP or SupportsLoRA from vllm/model_executor/models/interfaces.py. The base class brings a class flag, for example supports_pp = True, and names the methods that the feature needs. A detector function, for example supports_pp(), checks the flag and often the methods too.

The registry runs all detectors on the class and stores the results in one model info record (Out-of-tree models and the model registry). The configuration code reads the record. Workers check some capabilities again on the built model object.

An everyday comparison. Think of the badges on a driving licence: "trucks", "motorcycles". A clerk checks the badges before you get the job, not on the road. The comparison stops here. For some badges, vLLM also tests the skill: it looks at the forward signature. And a few features need no badge, only the right attribute.

The picture#

Figure: what the class declares (panel 1), the three kinds of detector (panel 2), and the start-up checks that read the results (panel 3).
Open full size ↗

Panel 2 is the important one. A flag test trusts the flag. A protocol test needs every member of the protocol. A combined test needs the flag and the structure, and it logs a WARNING when the two do not agree.

In the code#

Step 1: the base expectations (VllmModel)#

interfaces_base.py defines the protocol that every model must follow. Unlike the protocols of interfaces.py, it has no class flag. The comment says why: old out-of-tree models do not inherit these classes (vllm/model_executor/models/interfaces_base.py:35-37). So vLLM checks the structure by name:

def _check_vllm_model_init(model: type[object] | object) -> bool:
    model_init = getattr(model, "__init__")  # noqa: B009
    return supports_kw(model_init, "vllm_config")


def _check_vllm_model_embed_input_ids(model: type[object] | object) -> bool:
    model_embed_input_ids = getattr(model, "embed_input_ids", None)
    if not callable(model_embed_input_ids):
        logger.warning(
            "The model (%s) is missing the `embed_input_ids` method.",
            model,
        )
        return False

    return True


def _check_vllm_model_forward(model: type[object] | object) -> bool:
    model_forward = getattr(model, "forward", None)
    if not callable(model_forward):
        return False

    vllm_kws = ("input_ids", "positions")
    missing_kws = tuple(kw for kw in vllm_kws if not supports_kw(model_forward, kw))

    if missing_kws and (isinstance(model, type) and issubclass(model, nn.Module)):
        logger.warning(
            "The model (%s) is missing "
            "vLLM-specific keywords from its `forward` method: %s",
            model,
            missing_kws,
        )

    return len(missing_kws) == 0


@overload
def is_vllm_model(model: type[object]) -> TypeIs[type[VllmModel]]: ...


@overload
def is_vllm_model(model: object) -> TypeIs[VllmModel]: ...


def is_vllm_model(
    model: type[object] | object,
) -> TypeIs[type[VllmModel]] | TypeIs[VllmModel]:
    return (
        _check_vllm_model_init(model)
        and _check_vllm_model_embed_input_ids(model)
        and _check_vllm_model_forward(model)
    )
  1. Line 66. __init__ must accept the keyword vllm_config.
  2. Lines 70-76. embed_input_ids must exist. A missing method logs a WARNING.
  3. Lines 86-87. forward must accept input_ids and positions.
  4. Lines 111-115. All three must pass.

Two derived protocols build on it. A generative model also has a callable compute_logits (vllm/model_executor/models/interfaces_base.py:140-147). A pooling model sets the flag is_pooling_model (vllm/model_executor/models/interfaces_base.py:226-232). A class that is neither fails the runner check in ModelConfig, unless --convert adapts it to a pooling model (vllm/config/model.py:715-737).

Step 2: flag-only detectors#

Most detectors only read the class flag:

@runtime_checkable
class SupportsMultiModal(SupportsMultiModalEmbeddings, Protocol):
    """The interface required for all multi-modal models."""

    supports_multimodal: ClassVar[Literal[True]] = True
    """
    A flag that indicates this model supports multi-modal inputs.

    Note:
        There is no need to redefine this flag if this class is in the
        MRO of your model class.
    """

    supports_multimodal_raw_input_only: ClassVar[bool] = False
The docstring says that a class need not set the flag again when the protocol is in its MRO.
def supports_multimodal(
    model: type[object] | object,
) -> TypeIs[type[SupportsMultiModal]] | TypeIs[SupportsMultiModal]:
    return getattr(model, "supports_multimodal", False)

getattr(model, flag, False) works on a class and on an object. It has a side effect: a class can set the flag without the base class. The test model MyOPTRawInputTokensForCausalLM sets requires_raw_input_tokens = True directly (tests/plugins/vllm_add_dummy_model/vllm_add_dummy_model/my_opt.py:21-24). The detector does not check that the methods of the protocol exist.

Step 3: protocol-member detectors#

Some detectors call isinstance with a runtime-checkable Protocol, for example supports_eagle3:

def supports_eagle3(
    model: type[object] | object,
) -> TypeIs[type[SupportsEagle3]] | TypeIs[SupportsEagle3]:
    return isinstance(model, SupportsEagle3)

For a class object, Python checks that the object has every member of the protocol (general Python rule). Members with a value in the protocol, such as supports_eagle3 and the methods, come with inheritance. A member that is only annotated, such as SupportsLoRA.lora_manager (vllm/model_executor/models/interfaces.py:739), is not on the class. So isinstance(cls, SupportsLoRA) is false for every model class (derived). For a class, LoRA uses the helper protocol _SupportsLoRAType, which lists only the flag and the two mappings (vllm/model_executor/models/interfaces.py:744-749). The comment above it gives another reason: issubclass does not work with these protocols, so the code treats the class as an instance (lines 742-743). PP has a helper of the same kind, _SupportsPPType (Step 4).

Step 4: combined detectors with warnings (LoRA and PP)#

def supports_lora(
    model: type[object] | object,
) -> TypeIs[type[SupportsLoRA]] | TypeIs[SupportsLoRA]:
    result = _supports_lora(model)

    if not result:
        lora_attrs = (
            "packed_modules_mapping",
            "embedding_modules",
        )
        missing_attrs = tuple(attr for attr in lora_attrs if not hasattr(model, attr))

        if getattr(model, "supports_lora", False):
            if missing_attrs:
                logger.warning(
                    "The model (%s) sets `supports_lora=True`, "
                    "but is missing LoRA-specific attributes: %s",
                    model,
                    missing_attrs,
                )
        else:
            if not missing_attrs:
                logger.warning(
                    "The model (%s) contains all LoRA-specific attributes, "
                    "but does not set `supports_lora=True`.",
                    model,
                )

    return result


def _supports_lora(model: type[object] | object) -> bool:
    if isinstance(model, type):
        return isinstance(model, _SupportsLoRAType)

    return isinstance(model, SupportsLoRA)
  1. Line 763. For a class: the flag supports_lora and the attributes packed_modules_mapping and embedding_modules must exist (lines 792-793 use the helper protocol).
  2. Lines 772-779. Flag set, attributes missing: WARNING "sets supports_lora=True, but is missing LoRA-specific attributes", and the result is false.
  3. Lines 781-786. Attributes present, flag missing: WARNING "contains all LoRA-specific attributes, but does not set supports_lora=True". The result is false.

PP is stricter: it also inspects the forward signature.

def supports_pp(
    model: type[object] | object,
) -> bool | TypeIs[type[SupportsPP]] | TypeIs[SupportsPP]:
    supports_attributes = _supports_pp_attributes(model)
    supports_inspect = _supports_pp_inspect(model)

    if supports_attributes and not supports_inspect:
        logger.warning(
            "The model (%s) sets `supports_pp=True`, but does not accept "
            "`intermediate_tensors` in its `forward` method",
            model,
        )

    if not supports_attributes:
        pp_attrs = ("make_empty_intermediate_tensors",)
        missing_attrs = tuple(attr for attr in pp_attrs if not hasattr(model, attr))

        if getattr(model, "supports_pp", False):
            if missing_attrs:
                logger.warning(
                    "The model (%s) sets `supports_pp=True`, "
                    "but is missing PP-specific attributes: %s",
                    model,
                    missing_attrs,
                )
        else:
            if not missing_attrs:
                logger.warning(
                    "The model (%s) contains all PP-specific attributes, "
                    "but does not set `supports_pp=True`.",
                    model,
                )

    return supports_attributes and supports_inspect


def _supports_pp_attributes(model: type[object] | object) -> bool:
    if isinstance(model, type):
        return SupportsPP in model.__mro__ or isinstance(model, _SupportsPPType)

    return isinstance(model, SupportsPP)


def _supports_pp_inspect(model: type[object] | object) -> bool:
    model_forward = getattr(model, "forward", None)
    if not callable(model_forward):
        return False

    return supports_kw(model_forward, "intermediate_tensors")
  1. Lines 867-875. The attribute test passes, but forward has no keyword intermediate_tensors: WARNING, and the result is false (line 897).
  2. Line 902. For a class, SupportsPP in the MRO passes the attribute test. The helper protocol (with make_empty_intermediate_tensors) is the second way.
  3. Lines 877-895. The same two mismatch warnings as for LoRA.

Step 5: from the class to _ModelInfo#

The registry runs the detectors once per class and keeps the results:

    @staticmethod
    def from_model_cls(model: type[nn.Module]) -> "_ModelInfo":
        return _ModelInfo(
            architecture=model.__name__,
            is_text_generation_model=is_text_generation_model(model),
            is_pooling_model=is_pooling_model(model),
            default_seq_pooling_type=get_default_seq_pooling_type(model),
            default_tok_pooling_type=get_default_tok_pooling_type(model),
            attn_type=get_attn_type(model),
            score_type=get_score_type(model),
            supports_multimodal=supports_multimodal(model),
            supports_multimodal_raw_input_only=supports_multimodal_raw_input_only(
                model
            ),
            requires_raw_input_tokens=requires_raw_input_tokens(model),
            supports_multimodal_encoder_tp_data=supports_multimodal_encoder_tp_data(
                model
            ),
            supports_pp=supports_pp(model),
            supports_lora=supports_lora(model),
            supports_hc_sp=supports_hc_sp(model),
            has_inner_state=has_inner_state(model),
            is_attention_free=is_attention_free(model),
            is_hybrid=is_hybrid(model),
            supports_replayssm=supports_replayssm(model),
            supports_transcription=supports_transcription(model),
            supports_transcription_only=(
                supports_transcription(model) and model.supports_transcription_only
            ),
            supports_kv_cache_grouping=supports_kv_cache_grouping(model),
            has_noops=has_noops(model),
            supported_video_pruning_methods=getattr(
                model, "supported_video_pruning_methods", ()
            ),
            supports_mm_device_do_normalize=getattr(
                model, "supports_mm_device_do_normalize", False
            ),
        )

ModelConfig stores the record as _model_info (vllm/config/model.py:741-742) and exposes properties such as is_pp_supported and is_lora_supported (vllm/config/model.py:2028-2034). The table maps each protocol to its reader.

ProtocolFlag / testWho reads itIf missing
VllmModelForTextGenerationis_text_generation_model: base checks + compute_logitsrunner check vllm/config/model.py:705-728; MRV2 tasks vllm/v1/worker/gpu/model_states/interface.py:111-112ValueError "does not support --runner generate"
VllmModelForPoolingis_pooling_model flagrunner check vllm/config/model.py:706-722"does not support --runner pooling" unless --convert
SupportsMultiModalsupports_multimodal flagmultimodal config set-up vllm/config/model.py:830-834no multimodal config: the model takes text only
SupportsLoRAflag + 2 attributes, warningsvllm/config/lora.py:137-143; worker vllm/v1/worker/lora_model_runner_mixin.py:49-50ValueError with --enable-lora
SupportsPPMRO or attribute + forward keywordvllm/config/model.py:1569-1576NotImplementedError when PP > 1
SupportsHCSPsupports_hc_sp flagvllm/config/model.py:1549-1553ValueError with --enable-hc-sp and TP > 1
HasInnerStatehas_inner_state flagCPU platform vllm/platforms/cpu.py:53-56treated as a model without recurrent state
IsAttentionFreeis_attention_free flagattention type vllm/config/model.py:2185-2186; MRV2 model state vllm/v1/worker/gpu/model_states/__init__.py:52-55vLLM plans attention KV cache for it
IsHybridis_hybrid flag (+ layer_types check)vllm/config/model.py:2040-2049; MRV2 model state (same line 52)no Mamba state groups (The hybrid KV cache: specs, groups and one manager per layer type)
HasNoOpshas_noops flaglayer count vllm/config/model.py:1748-1750no-op layers count as attention layers
SupportsReplaySSMsupports_replayssm flagvllm/config/vllm.py:3435-3439ValueError with --use-replayssm
SupportsKVCacheGroupingisinstance (method get_kv_cache_groups)vllm/v1/core/kv_cache_planning.py:1669-1670default KV cache grouping
SupportsTranscriptionsupports_transcription flagMRV2 tasks vllm/v1/worker/gpu/model_states/interface.py:113-116no transcription task
SupportsEagle3isinstancevllm/v1/worker/gpu/spec_decode/eagle/eagle3_utils.py:29-30RuntimeError "does not support EAGLE3 interface"
SupportsMRoPEisinstanceMRV2 vllm/v1/worker/gpu/mm/rope.py:134AssertionError for an M-RoPE model
MixtureOfExpertsisinstance and num_moe_layers > 0EPLB vllm/model_executor/models/interfaces.py:1170-1173not an MoE model for EPLB

The "if missing" column for flag-only rows is derived from the reader code; vLLM gives no error there.

Step 6: mixins that are not detectors#

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

Worked example: the _ModelInfo of LlamaForCausalLM

The class header and its LoRA attributes:

class LlamaForCausalLM(
    LocalArgmaxMixin,
    nn.Module,
    SupportsLoRA,
    SupportsPP,
    SupportsEagle,
    SupportsEagle3,
    SupportsQuant,
):
    hf_to_vllm_mapper = LlamaModel.hf_to_vllm_mapper
    # LoRA specific attributes
    packed_modules_mapping = {
        "qkv_proj": ["q_proj", "k_proj", "v_proj"],
        "gate_up_proj": ["gate_proj", "up_proj"],
    }
    embedding_modules = {
        "embed_tokens": "input_embeddings",
        "lm_head": "output_embeddings",
    }

    def __init__(
        self,
        *,
        vllm_config: VllmConfig,
        prefix: str = "",
        layer_type: type[nn.Module] = LlamaDecoderLayer,
    ):

Applying each detector of from_model_cls to this class gives these values (derived from the code above; the cache file vllm-model_executor-models-llama-LlamaForCausalLM.json holds the same record):

FieldValueWhy
architecture"LlamaForCausalLM"model.__name__
is_text_generation_modeltruekeyword-only vllm_config (line 487), embed_input_ids (vllm/model_executor/models/llama.py:531), forward(input_ids, positions, ...) (534), compute_logits (546)
is_pooling_modelfalseno flag
attn_type, pooling types, score_type"decoder", "LAST", "ALL", "bi-encoder"defaults of the getters (vllm/model_executor/models/interfaces_base.py:253-287)
supports_loratrueflag + both mappings (lines 475-482)
supports_pptrueSupportsPP in MRO; forward has intermediate_tensors (vllm/model_executor/models/llama.py:534-544)
supports_multimodal, is_hybrid, is_attention_free, has_inner_state, supports_transcription, ...falseno flag
supported_video_pruning_methods()getattr default

So vllm serve meta-llama/Llama-3.1-8B -pp 2 --enable-lora passes both start-up checks. A class that drops SupportsPP fails at vllm/config/model.py:1569-1576, before any worker starts.

Deep dive

The 24 fields of _ModelInfo

@dataclass(frozen=True)
class _ModelInfo:
    architecture: str
    is_text_generation_model: bool
    is_pooling_model: bool
    attn_type: AttnTypeStr
    default_seq_pooling_type: SequencePoolingType
    default_tok_pooling_type: TokenPoolingType
    score_type: ScoreType
    supports_multimodal: bool
    supports_multimodal_raw_input_only: bool
    requires_raw_input_tokens: bool
    supports_multimodal_encoder_tp_data: bool
    supports_pp: bool
    supports_lora: bool
    supports_hc_sp: bool
    has_inner_state: bool
    is_attention_free: bool
    is_hybrid: bool
    has_noops: bool
    supports_replayssm: bool
    supports_transcription: bool
    supports_transcription_only: bool
    supports_kv_cache_grouping: bool
    supported_video_pruning_methods: tuple[str, ...]
    supports_mm_device_do_normalize: bool

Every field is a bool, a string literal or a tuple of strings, so the record survives a JSON round trip to the modelinfos cache (vllm/model_executor/models/registry.py:1054-1070). Two fields read plain attributes without a detector: supported_video_pruning_methods and supports_mm_device_do_normalize (lines 948-953). supports_transcription_only reads the attribute only when the transcription flag is set (lines 943-945).

Deep dive

Front end versus worker: two checks of the same capability

The front end checks the class through the cached record. The worker checks the built object again in some places:

  • LoRA. LoRAConfig.verify_with_model_config runs in VllmConfig.__post_init__ (vllm/config/vllm.py:1572). The worker's load_lora_model calls supports_lora(model) on the object (vllm/v1/worker/lora_model_runner_mixin.py:49-50); Model Runner V2 calls it after it loads the model (vllm/v1/worker/gpu/model_runner.py:407-410).
  • EAGLE-3. No front-end check in the record; the worker checks the object (vllm/v1/worker/gpu/model_runner.py:412-414).
  • For an object. isinstance(obj, SupportsLoRA) succeeds by inheritance alone when the object's class names the protocol as a base (general Python rule for protocols). So for a class that inherits SupportsLoRA, the object check cannot fail where the class check passed. A class that only copies the flag and the two mappings passes the class check. But its object has no lora_manager, so the worker check fails. (Derived.)
Deep dive

Edge cases of the detectors

  • A flag without the base class. Works for every flag-only detector (Step 2). It does not work for isinstance detectors unless every protocol member exists.
  • A wrong flag. supports_multimodal = True on a class without embed_input_ids(..., multimodal_embeddings) passes the detector. The failure comes later, at run time. (Derived from line 589.)
  • SupportsPP without make_empty_intermediate_tensors. For a class, the MRO test at line 902 passes alone. The protocol only annotates this member (vllm/model_executor/models/interfaces.py:820-821), so the model must set it itself. Llama sets it on the object in __init__ (vllm/model_executor/models/llama.py:519-521). (Derived.)
  • Hybrid models without non-attention layers. ModelConfig.is_hybrid returns false when every layer_types entry is attention, even with the flag set (vllm/config/model.py:2040-2049).
  • Two score types. get_score_type asserts that the MRO has at most one non-default score type (vllm/model_executor/models/interfaces_base.py:279-287).

What can go wrong#

Why it matters. These protocols are the contract between a model and the rest of vLLM. An out-of-tree model that forgets one does not crash; it loses a feature, and start-up rejects the flag that needs it.

Check yourself#

  1. 1
    Where does vLLM store the results of the capability detectors?
    easy
    Show answer

    In a frozen _ModelInfo record that the registry computes from the class (vllm/model_executor/models/registry.py:917-954). ModelConfig keeps it as _model_info (vllm/config/model.py:741-742).

  2. 2
    A class sets supports_lora = True but has no packed_modules_mapping. What happens with --enable-lora?
    medium
    Show answer

    supports_lora returns false and logs "sets supports_lora=True, but is missing LoRA-specific attributes" (vllm/model_executor/models/interfaces.py:772-779). Then LoRAConfig raises ValueError (vllm/config/lora.py:137-143).

  3. 3
    Why does supports_multimodal accept a class that only sets the flag?
    medium
    Show answer

    The detector is getattr(model, "supports_multimodal", False) (vllm/model_executor/models/interfaces.py:589). It checks only the flag, not the methods.

  4. 4
    Why do supports_lora and supports_pp use helper protocols (_SupportsLoRAType, _SupportsPPType) for a class?
    hard
    Show answer

    A runtime isinstance check on a class object needs every protocol member on that object. SupportsLoRA only annotates lora_manager, so a class does not have it, and isinstance(cls, SupportsLoRA) is false (derived). _SupportsLoRAType lists only the flag and the two mappings (vllm/model_executor/models/interfaces.py:744-749). The comment at vllm/model_executor/models/interfaces.py:742-743 adds that issubclass does not work with these protocols, so the code calls isinstance on the class.