Model capability protocols (SupportsPP, SupportsLoRA, ...)
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#
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)
)- Line 66.
__init__must accept the keywordvllm_config. - Lines 70-76.
embed_input_idsmust exist. A missing method logs a WARNING. - Lines 86-87.
forwardmust acceptinput_idsandpositions. - 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] = Falsedef 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)- Line 763. For a class: the flag
supports_loraand the attributespacked_modules_mappingandembedding_modulesmust exist (lines 792-793 use the helper protocol). - Lines 772-779. Flag set, attributes missing: WARNING "sets
supports_lora=True, but is missing LoRA-specific attributes", and the result is false. - 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")- Lines 867-875. The attribute test passes, but
forwardhas no keywordintermediate_tensors: WARNING, and the result is false (line 897). - Line 902. For a class,
SupportsPPin the MRO passes the attribute test. The helper protocol (withmake_empty_intermediate_tensors) is the second way. - 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.
| Protocol | Flag / test | Who reads it | If missing |
|---|---|---|---|
VllmModelForTextGeneration | is_text_generation_model: base checks + compute_logits | runner check vllm/config/model.py:705-728; MRV2 tasks vllm/v1/worker/gpu/model_states/interface.py:111-112 | ValueError "does not support --runner generate" |
VllmModelForPooling | is_pooling_model flag | runner check vllm/config/model.py:706-722 | "does not support --runner pooling" unless --convert |
SupportsMultiModal | supports_multimodal flag | multimodal config set-up vllm/config/model.py:830-834 | no multimodal config: the model takes text only |
SupportsLoRA | flag + 2 attributes, warnings | vllm/config/lora.py:137-143; worker vllm/v1/worker/lora_model_runner_mixin.py:49-50 | ValueError with --enable-lora |
SupportsPP | MRO or attribute + forward keyword | vllm/config/model.py:1569-1576 | NotImplementedError when PP > 1 |
SupportsHCSP | supports_hc_sp flag | vllm/config/model.py:1549-1553 | ValueError with --enable-hc-sp and TP > 1 |
HasInnerState | has_inner_state flag | CPU platform vllm/platforms/cpu.py:53-56 | treated as a model without recurrent state |
IsAttentionFree | is_attention_free flag | attention type vllm/config/model.py:2185-2186; MRV2 model state vllm/v1/worker/gpu/model_states/__init__.py:52-55 | vLLM plans attention KV cache for it |
IsHybrid | is_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) |
HasNoOps | has_noops flag | layer count vllm/config/model.py:1748-1750 | no-op layers count as attention layers |
SupportsReplaySSM | supports_replayssm flag | vllm/config/vllm.py:3435-3439 | ValueError with --use-replayssm |
SupportsKVCacheGrouping | isinstance (method get_kv_cache_groups) | vllm/v1/core/kv_cache_planning.py:1669-1670 | default KV cache grouping |
SupportsTranscription | supports_transcription flag | MRV2 tasks vllm/v1/worker/gpu/model_states/interface.py:113-116 | no transcription task |
SupportsEagle3 | isinstance | vllm/v1/worker/gpu/spec_decode/eagle/eagle3_utils.py:29-30 | RuntimeError "does not support EAGLE3 interface" |
SupportsMRoPE | isinstance | MRV2 vllm/v1/worker/gpu/mm/rope.py:134 | AssertionError for an M-RoPE model |
MixtureOfExperts | isinstance and num_moe_layers > 0 | EPLB vllm/model_executor/models/interfaces.py:1170-1173 | not 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#
SupportsQuantis a plain class, not a protocol. Its__new__finds the quantization config in the constructor arguments and stores it on the object asquant_config(vllm/model_executor/models/interfaces.py:1260-1290). It also applies the model's name mapping andpacked_modules_mappingto that config (vllm/model_executor/models/interfaces.py:1292-1299).EagleModelMixingoes on the inner decoder, not on the top class. It collects auxiliary hidden states for EAGLE-3 (vllm/model_executor/models/interfaces.py:1608-1680).SupportsEagle3.set_aux_hidden_state_layersasserts that the inner model has it (vllm/model_executor/models/interfaces.py:1745-1748).SupportsEagle(EAGLE-1/2) is in the MRO of Llama, but at this commitgit grepfinds no call ofsupports_eagle()outsidevllm/model_executor/models/.
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 diveWorked 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):
| Field | Value | Why |
|---|---|---|
architecture | "LlamaForCausalLM" | model.__name__ |
is_text_generation_model | true | keyword-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_model | false | no 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_lora | true | flag + both mappings (lines 475-482) |
supports_pp | true | SupportsPP 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, ... | false | no 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 diveThe 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: boolEvery 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 diveFront 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_configruns inVllmConfig.__post_init__(vllm/config/vllm.py:1572). The worker'sload_lora_modelcallssupports_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 inheritsSupportsLoRA, 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 nolora_manager, so the worker check fails. (Derived.)
Deep diveEdge cases of the detectors
- A flag without the base class. Works for every flag-only detector (Step 2). It does not work for
isinstancedetectors unless every protocol member exists. - A wrong flag.
supports_multimodal = Trueon a class withoutembed_input_ids(..., multimodal_embeddings)passes the detector. The failure comes later, at run time. (Derived from line 589.) SupportsPPwithoutmake_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_hybridreturns false when everylayer_typesentry is attention, even with the flag set (vllm/config/model.py:2040-2049). - Two score types.
get_score_typeasserts that the MRO has at most one non-default score type (vllm/model_executor/models/interfaces_base.py:279-287).
What can go wrong#
- "Pipeline parallelism is not supported for this model". The class lacks
SupportsPP, or itsforwardlacksintermediate_tensors. Look for the PP WARNING in the log (vllm/model_executor/models/interfaces.py:870-875). - "LoRA is not supported for ...". The class lacks
SupportsLoRAor the two mappings (vllm/config/lora.py:137-143). - "This model does not support
--runner generate". The base checks failed: a missingembed_input_ids,compute_logits, or keyword in__init__/forward(vllm/config/model.py:727-728).
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#
- 1Where does vLLM store the results of the capability detectors?easy
Show answer
In a frozen
_ModelInforecord that the registry computes from the class (vllm/model_executor/models/registry.py:917-954).ModelConfigkeeps it as_model_info(vllm/config/model.py:741-742). - 2A class setsmedium
supports_lora = Truebut has nopacked_modules_mapping. What happens with--enable-lora?Show answer
supports_lorareturns false and logs "setssupports_lora=True, but is missing LoRA-specific attributes" (vllm/model_executor/models/interfaces.py:772-779). ThenLoRAConfigraisesValueError(vllm/config/lora.py:137-143). - 3Why doesmedium
supports_multimodalaccept a class that only sets the flag?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. - 4Why dohard
supports_loraandsupports_ppuse helper protocols (_SupportsLoRAType,_SupportsPPType) for a class?Show answer
A runtime
isinstancecheck on a class object needs every protocol member on that object.SupportsLoRAonly annotateslora_manager, so a class does not have it, andisinstance(cls, SupportsLoRA)is false (derived)._SupportsLoRATypelists only the flag and the two mappings (vllm/model_executor/models/interfaces.py:744-749). The comment atvllm/model_executor/models/interfaces.py:742-743adds thatissubclassdoes not work with these protocols, so the code callsisinstanceon the class.