Model registration: the AFD<arch> wrappers
Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857
In one sentence
AFD registers each supported model under an AFD<arch> name. At build time it swaps the model config to that AFD identity.
The idea#
AFD needs a role-aware version of each model, so the attention side can replace the FFN
stage with a remote proxy. It supplies one AFD<arch> class per architecture. It
registers these classes with vLLM's ModelRegistry, as lazy
"module:Class" strings, so vLLM imports a class only when it is needed.
The model config names a plain architecture, for example DeepseekV2ForCausalLM. At
build time, the AFD worker swaps that name to AFDDeepseekV2ForCausalLM, so vLLM builds
the AFD class instead of the native one. The swap copies the config, so the native config is left
unchanged.
An everyday comparison. Think of a catalogue with an "AFD" edition of each item. You order by the plain name; the clerk swaps in the AFD edition. The comparison stops here: the swap keeps one hidden link intact, so vLLM Ascend can still tell a text model from a multimodal one.
The picture#
Read panel 1 as load time: register the names. Read panel 2 as build time: detect and swap the identity. Read panel 3 as the exact mappings. The Model wrappers: the role-aware decoder layer page shows what the wrappers do inside.
In the code#
Step 1: Register each AFD name#
At the end of register_afd, a loop registers each architecture under the name
"AFD" + arch (afd_plugin/__init__.py:211-214). The source map holds eight
architectures (afd_plugin/__init__.py:96-139).
from vllm.model_executor.models import ModelRegistry
for model_arch, model_cls in _MODEL_REGISTRATIONS.items():
ModelRegistry.register_model(f"AFD{model_arch}", model_cls)Step 2: The lazy "module:Class" form#
The registered value is a string, not a class. vLLM stores it and imports the class only when the
model is built (vllm:vllm/model_executor/models/registry.py:1126-1141). So loading the
plugin does not import every model file.
Step 3: Detect an AFD model#
has_afd_model_registration returns true when the model's architecture, with any
AFD prefix removed, is in the map (afd_plugin/model_executor/models/model_utils.py:16-22).
The runtime validator uses it to require a registered AFD model
(afd_plugin/validation.py:136-141).
def has_afd_model_registration(model_config: ModelConfig) -> bool:
"""Return whether the model resolves to a registered AFD implementation."""
return any(
model_arch.removeprefix("AFD") in _MODEL_REGISTRATIONS
for model_arch in model_config.hf_config.architectures
)Step 4: Swap the model identity#
get_afd_model_config finds the architecture in the map, deep-copies the model config,
and sets its architecture to AFD<arch>
(afd_plugin/model_executor/models/model_utils.py:25-46). The worker then uses the returned
config (afd_plugin/v1/worker/attention_worker.py:75-80).
for model_arch in model_config.hf_config.architectures:
if model_arch in _MODEL_REGISTRATIONS:
if model_arch in _QWEN3_5_MODEL_REGISTRATIONS and device_type != "cuda":
raise ValueError(
"AFD Qwen3.5/3.6 supports CUDA execution only; "
f"got device_type={device_type!r}",
)
# deepcopy preserves aliasing within the copied object graph, so
# the pure-text identity hf_text_config is hf_config is retained
# automatically. vLLM Ascend uses that identity to distinguish
# text models from multimodal models.
afd_model_config = deepcopy(model_config)
afd_model_config.hf_config.architectures = [f"AFD{model_arch}"]
return afd_model_config
return model_configStep 5: Which architectures map to which wrapper#
The map registers these exact strings (afd_plugin/__init__.py:96-131).
| Architecture | AFD wrapper class | Note |
|---|---|---|
DeepseekV2ForCausalLM | AFDDeepseekV2ForCausalLM | |
DeepseekV3ForCausalLM | AFDDeepseekV3ForCausalLM | |
DeepseekV32ForCausalLM | AFDDeepseekV3ForCausalLM | V3.2 reuses the V3 class (afd_plugin/__init__.py:106-108). |
DeepseekV4ForCausalLM | NPU or GPU V4 class | NPU class when torch_npu is importable, else the GPU class (afd_plugin/__init__.py:109-113). |
Qwen3MoeForCausalLM | AFDQwen3MoeForCausalLM | |
Qwen3_5MoeForConditionalGeneration | AFDQwen3_5MoeForConditionalGeneration | CUDA execution only (afd_plugin/model_executor/models/model_utils.py:34-38). |
The map also registers DeepseekForCausalLM and GlmMoeDsaForCausalLM
(afd_plugin/__init__.py:96-116).
Deep dive#
Deep diveWhy deepcopy, and the hf_text_config identity
get_afd_model_config does not change the original config. It deep-copies it, then sets
the architecture on the copy (afd_plugin/model_executor/models/model_utils.py:43-45).
- deepcopy keeps the inner aliasing. A code comment states the reason: the
pure-text identity
hf_text_config is hf_configis kept automatically, because deepcopy preserves aliasing within the copied object graph (afd_plugin/model_executor/models/model_utils.py:39-43). - vLLM Ascend reads that identity. It uses the same-object check to tell a text model from a multimodal model. So the swap must not break it.
- Only the architecture name changes. The rest of the config is the copy of the
original (
afd_plugin/model_executor/models/model_utils.py:44).
Deep diveREADME names a family; the code registers one string
The README names model families loosely. The code registers exact architecture strings.
- The README lists "Qwen3.5 / Qwen3.6 MoE" (
README.md:51), but the code registers one architecture,Qwen3_5MoeForConditionalGeneration(afd_plugin/__init__.py:125-131). get_afd_model_configraises for that architecture when the device type is notcuda(afd_plugin/model_executor/models/model_utils.py:34-38). So Qwen3.5 and Qwen3.6 run on CUDA only.DeepseekV4ForCausalLMresolves to the NPU class or the GPU class, chosen at import bytorch_npu(afd_plugin/__init__.py:109-113).
What can go wrong#
- Model not registered. A model whose architecture is not in the map fails the
runtime check: "AFD ModelRunnerV2 requires a registered AFD model"
(
afd_plugin/validation.py:136-141). - Qwen3.5 on NPU.
get_afd_model_configraises for a non-CUDA device (afd_plugin/model_executor/models/model_utils.py:34-38). - Model file import error. Because the registration is a lazy string, an import
error in the model file surfaces when vLLM builds the model, not when the plugin loads
(
vllm:vllm/model_executor/models/registry.py:1126-1141).
Why it matters. The worker runs an AFD model only after the identity swap. If the architecture is not registered, the swap does nothing and the runtime check stops start-up.
Check yourself#
- 1Under what name iseasy
Qwen3MoeForCausalLMregistered?Show answer
AFDQwen3MoeForCausalLM. The loop registers each key under"AFD" + key(afd_plugin/__init__.py:211-214). - 2Why is the registered value a string, not a class?easy
Show answer
So vLLM imports the model class only when it is needed, not when the plugin loads (
vllm:vllm/model_executor/models/registry.py:1126-1141). - 3What doesmedium
get_afd_model_configchange, and what does it keep?Show answer
It deep-copies the config and changes only the architecture name to
AFD<arch>(afd_plugin/model_executor/models/model_utils.py:43-45). It keeps thehf_text_config is hf_configidentity (afd_plugin/model_executor/models/model_utils.py:39-43). - 4The README says "Qwen3.5 / Qwen3.6". Which architecture string does the code register?medium
Show answer
One:
Qwen3_5MoeForConditionalGeneration(afd_plugin/__init__.py:125-131). - 5Why doeshard
get_afd_model_configusedeepcopyinstead of editing the architecture in place?Show answer
To leave the original config unchanged, and because deepcopy preserves the inner aliasing so
hf_text_config is hf_configstays true. vLLM Ascend uses that identity to tell a text model from a multimodal one (afd_plugin/model_executor/models/model_utils.py:39-45).