[Bugfix] Skip VllmConfig re-validation for with_hf_config submodel views (#58212)

Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Kimi Code <noreply@moonshot.ai> Co-authored-by: Roger Wang <rogerw@inferact.ai>

Nick Hill committed Sep 22, 2026 at 17:50 UTC 826e300cde4c512670bf2905cfd03c87451da36c
3 files changed +55 -1
tests/test_config.py
+40
@@ -289,6 +289,46 @@ def test_kv_offloading_does_not_skip_dcp_interleave_validation():
289 VllmConfig.validate_block_size(config)
290
291
292 +def test_nixl_dcp_check_skipped_for_submodel_config():
293 + # with_hf_config() builds a submodule view of the config (e.g. a
294 + # multimodal model's text stack) whose architecture list is empty.
295 + # Re-running VllmConfig validation on it is unsafe: the NIXL DCP check
296 + # reads use_mla, which resolves the architecture registry.
297 + model_config = ModelConfig("Qwen/Qwen2-VL-2B-Instruct", max_model_len=2048)
298 + vllm_config = VllmConfig(
299 + model_config=model_config,
300 + device_config=DeviceConfig(device="cpu"),
301 + kv_transfer_config=KVTransferConfig(
302 + kv_connector="NixlConnector",
303 + kv_role="kv_both",
304 + ),
305 + )
306 + submodel_config = vllm_config.with_hf_config(model_config.hf_text_config)
307 + assert submodel_config.model_config.is_submodel_config
308 + assert submodel_config.model_config.architectures == []
309 + assert submodel_config.model_config.use_mla is False
310 +
311 +
312 +def test_nixl_dcp_check_rejects_non_mla_model_with_dcp(monkeypatch):
313 + # Pretend the model has a single KV head so the DCP feasibility checks
314 + # pass and the MLA-only assert is what actually fires.
315 + monkeypatch.setattr(ModelConfig, "get_total_num_kv_heads", lambda self: 1)
316 + with pytest.raises(ValidationError, match="only supported for MLA models"):
317 + VllmConfig(
318 + model_config=ModelConfig("Qwen/Qwen3-0.6B", max_model_len=2048),
319 + device_config=DeviceConfig(device="cpu"),
320 + parallel_config=ParallelConfig(
321 + tensor_parallel_size=2,
322 + decode_context_parallel_size=2,
323 + distributed_executor_backend="mp",
324 + ),
325 + kv_transfer_config=KVTransferConfig(
326 + kv_connector="NixlConnector",
327 + kv_role="kv_both",
328 + ),
329 + )
330 +
331 +
332 def test_compile_config_repr_succeeds():
333 # setup: VllmBackend mutates the config object
334 config = VllmConfig()
vllm/config/model.py
+8 -1
@@ -187,6 +187,10 @@ class ModelConfig:
187 """The Hugging Face config of the model."""
188 hf_text_config: PretrainedConfig = field(init=False)
189 """The Hugging Face config of the text model (same as hf_config for text models)."""
190 + is_submodel_config: bool = field(default=False, init=False)
191 + """Whether this is a submodule view derived by `VllmConfig.with_hf_config`
192 + (e.g. a multimodal model's text stack). Its architecture list is empty, so
193 + deployment-level validation must not run against it."""
194 word_embeddings_untied_by_checkpoint: bool = field(default=False, init=False)
195 """Whether `tie_word_embeddings` was overridden to `False` because the checkpoint
196 contains an `lm_head` of its own. The two may still turn out to be identical, in
@@ -1082,7 +1086,10 @@ class ModelConfig:
1086 # Check if the architecture we're wrapping has defaults
1087 runner = None
1088 task = None
1085 - if defaults := try_match_architecture_defaults(self.architectures[0]):
1089 + # architectures is empty for with_hf_config() submodel views.
1090 + if self.architectures and (
1091 + defaults := try_match_architecture_defaults(self.architectures[0])
1092 + ):
1093 _, (runner, task) = defaults
1094 # User specified value take precedence
1095 if self.runner != "auto":
vllm/config/vllm.py
+7
@@ -943,6 +943,7 @@ class VllmConfig:
943
944 model_config.hf_config = hf_config
945 model_config.model_arch_config = model_config.get_model_arch_config()
946 + model_config.is_submodel_config = True
947
948 return replace(self, model_config=model_config)
949
@@ -1331,6 +1332,12 @@ class VllmConfig:
1332 # To give each torch profile run a unique instance name.
1333 self.instance_id = f"{time.time_ns()}"
1334
1335 + if self.model_config is not None and self.model_config.is_submodel_config:
1336 + # with_hf_config() view: the parent config was already validated,
1337 + # and this view's empty architecture list makes the model-dependent checks
1338 + # below unsafe (e.g. use_mla resolves the architecture registry).
1339 + return
1340 +
1341 self._resolve_mm_encoder_only()
1342
1343 if self.performance_mode != "balanced":