Worker selection: choosing the role worker
Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857
In one sentence
AFD picks a role-specific worker by rewriting worker_cls, but only when the user left it as "auto".
The idea#
vLLM has no register API for a worker. It reads the worker class name from
parallel_config.worker_cls and imports it by name
(vllm:vllm/v1/worker/worker_base.py:273-277). A plugin changes the worker by setting
this field. AFD does that in its config patch.
AFD must not overwrite a worker the user chose on purpose. So the rewrite runs only when the
field is still "auto". When the user passed --worker-cls, AFD keeps it
and checks it later instead. The right worker also depends on the role and the platform, so AFD
maps the platform's default worker to the matching AFD worker.
An everyday comparison. Think of a form field set to "auto". A helper fills it only when it is still "auto"; a value you typed is left alone, but checked for fit. The comparison stops here: AFD also swaps the model runner later, inside the worker.
The picture#
Read panel 1 top to bottom as the start-up flow. Read panel 2 as the mapping used in step 3. The violet NPU row is named here only; the GPU worker is the one this site covers in depth.
In the code#
Step 1: The config patch records whether worker_cls was "auto"#
The patched create_engine_config first records whether the user left
worker_cls as "auto" (afd_plugin/compat/patches/config_validation.py:60).
It keeps that flag because vLLM fills the field with the platform default while it builds the
config.
# ### PATCH START: AFD config preflight
worker_cls_was_auto = _uses_auto_worker_value(self.worker_cls)
is_afd_npu = _apply_afd_npu_config_patches(self)
needs_engine_args_bypass = not is_afd_npu and _should_relax_engine_args_backend(
self
)
# ### PATCH END: AFD config preflightStep 2: Rewrite only when it was "auto"#
After the config is built, the patch rewrites the worker only when the flag is set
(afd_plugin/compat/patches/config_validation.py:91-93). The rewrite reads the role from
the AFD config and the platform default from the field
(afd_plugin/compat/patches/config_validation.py:179-197).
def _select_afd_worker_for_auto(vllm_config: VllmConfig) -> None:
afd_config = parse_optional_afd_config(vllm_config)
if afd_config is None:
return
from vllm.platforms import current_platform
platform_worker_qualname = vllm_config.parallel_config.worker_cls
if not isinstance(platform_worker_qualname, str):
raise ValueError(
"platform worker_cls must be a qualname string before AFD automatic "
f"selection, got {type(platform_worker_qualname).__name__}",
)
vllm_config.parallel_config.worker_cls = afd_worker_qualname_for_platform_default(
afd_config.role,
platform_worker_qualname,
is_cuda=current_platform.is_cuda(),
device_type=current_platform.device_type,
)Step 3: The platform default maps to an AFD worker#
afd_worker_qualname_for_platform_default normalizes the platform default and maps it
to the AFD worker for the role (afd_plugin/validation.py:280-309). The table lists the
mappings.
| Platform default worker | Role | AFD worker |
|---|---|---|
vllm.v1.worker.gpu_worker.Worker (CUDA) |
attention | AFDAttentionWorker |
| ffn | AFDFFNWorker | |
vllm_ascend.worker.worker.NPUWorker (NPU) |
attention | AFDNPUAttentionWorker |
| ffn | AFDNPUFFNWorker |
The names are the FQCN constants at the top of the module
(afd_plugin/validation.py:26-41). A CUDA default maps by role
(afd_plugin/validation.py:290-291); the standard Ascend default maps to the NPU workers
(afd_plugin/validation.py:292-296). The 310P and Xlite Ascend workers are rejected
(afd_plugin/validation.py:297-305).
Step 4: vLLM imports the chosen class#
Later, in the worker process, vLLM imports worker_cls by its qualified name
(vllm:vllm/v1/worker/worker_base.py:273-277). By then the field holds the AFD worker, so
vLLM builds the AFD worker.
Step 5: The worker swaps the model runner in init_device#
The AFD worker's init_device first checks the stack, then swaps the native model
runner class for the AFD one and delegates to the native init
(afd_plugin/v1/worker/attention_worker.py:60-88). The FFN worker does the same with its
own runner (afd_plugin/v1/worker/ffn_worker.py:81-99).
native_runner_cls = gpu_model_runner_v2.GPUModelRunner
gpu_model_runner_v2.GPUModelRunner = AFDAttentionModelRunnerV2
try:
super().init_device()
finally:
gpu_model_runner_v2.GPUModelRunner = native_runner_cls
# ### PATCH END: select and directly construct the AFD runner.Deep dive#
Deep diveAn explicit wrong worker is rejected, not fixed
When the user passes --worker-cls, the rewrite does not run. The stack check in
init_device then verifies the field. assert_compatible_afd_stack compares
the resolved worker with the expected AFD worker for the role
(afd_plugin/validation.py:312-369).
- If the field is still
"auto", it raises. This means the general plugin did not run before the config was built (afd_plugin/validation.py:343-348). - If the worker does not match the expected AFD worker, it raises and names both, and tells the
user to remove
--worker-clsor pass the right one (afd_plugin/validation.py:357-367). - For the async CAM connector, the expected worker is the NPU worker instead
(
afd_plugin/validation.py:331-335).
So AFD never silently replaces a worker the user set. It either accepts the right one or stops with a clear message.
Deep diveWhy the runner swap is a scoped module-attribute patch
vLLM 0.30.0 builds its model runner inside Worker.init_device, with no runner
factory to inject. The AFD worker works around this with a scoped patch. It replaces the module
attribute GPUModelRunner, calls the native init_device, then restores the
attribute in a finally (afd_plugin/v1/worker/attention_worker.py:82-87).
This works because the native code does a local import of GPUModelRunner from the
same module inside init_device, after the patch is in place
(vllm:vllm/v1/worker/gpu_worker.py:475-482). A module-attribute patch changes only later
attribute lookups, so the swap must be active during that import. The finally keeps
the swap from leaking past the call.
Both roles also need the V2 model runner. The GPU runner check raises when the V2 model
runner is off (afd_plugin/validation.py:90-91). vLLM reads the flag from
VLLM_USE_V2_MODEL_RUNNER; the AFD recipes set it to 1.
What can go wrong#
- Plugin loaded too late. If the config is built before the plugin runs,
worker_clsstays"auto"and the stack check raises (afd_plugin/validation.py:343-348). - Wrong explicit worker. A
--worker-clsthat is not the role's AFD worker is rejected with both names (afd_plugin/validation.py:357-367). - Unsupported platform. A platform whose default worker AFD does not know
raises in the mapping (
afd_plugin/validation.py:306-309). - No V2 model runner. With the V2 model runner off
(
VLLM_USE_V2_MODEL_RUNNER=0) the GPU check raises (afd_plugin/validation.py:90-91).
Why it matters. The two servers must run the matching role workers. AFD picks
them when the field is "auto", and refuses a wrong one, so a mismatch cannot reach the
model.
Check yourself#
- 1When does AFD rewriteeasy
worker_cls?Show answer
Only when it is still
"auto". The patch records this before the config is built (afd_plugin/compat/patches/config_validation.py:60,afd_plugin/compat/patches/config_validation.py:91-93). - 2On CUDA, which worker does the attention role use?easy
Show answer
AFDAttentionWorker. The CUDA default maps to it by role (afd_plugin/validation.py:290-291). - 3The user passesmedium
--worker-cls some.other.Worker. What happens?Show answer
The rewrite is skipped, and
assert_compatible_afd_stackrejects the worker because it does not match the role's AFD worker (afd_plugin/validation.py:357-367). - 4Why does the runner swap use amedium
finallyblock?Show answer
The swap is a module-attribute patch active only during the native
init_device. Thefinallyrestores the native class so the swap does not leak past the call (afd_plugin/v1/worker/attention_worker.py:82-87). - 5The worker class resolves, but the model runner is never swapped to the AFD one. Give one setup that produces this.hard
Show answer
If the plugin loaded after the config was built,
worker_clsstays"auto"and the stack check raises before the swap (afd_plugin/validation.py:343-348). The swap runs only inside the AFD worker'sinit_device(afd_plugin/v1/worker/attention_worker.py:82-87).