[Model Runner V2] Support randomized dummy inputs (#58411)

Signed-off-by: Robert Shaw <robshaw@redhat.com> Co-authored-by: Robert Shaw <robshaw@redhat.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>

Robert Shaw committed Oct 1, 2026 at 03:52 UTC e5e38ba9b7d18f9746d989e389a51a94b0f96f6f
4 files changed +43 -9
vllm/distributed/elastic_ep/elastic_execute.py
+6 -1
@@ -721,7 +721,12 @@ class ElasticEPScalingExecutor:
721 self._suppress_eplb(),
722 serving_state,
723 ):
724 - runner._dummy_run(runner.max_num_tokens, is_profile=True, skip_eplb=True)
724 + runner._dummy_run(
725 + runner.max_num_tokens,
726 + is_profile=True,
727 + skip_eplb=True,
728 + randomize_inputs=self.worker.randomize_dummy_inputs,
729 + )
730 self.worker.compile_or_warm_up_model()
731
732 lock_workspace()
vllm/v1/worker/gpu/model_runner.py
+11 -2
@@ -805,6 +805,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
805 skip_eplb: bool = False,
806 is_profile: bool = False,
807 valid_dummy_state_slots: bool = False,
808 + randomize_inputs: bool = False,
809 **kwargs,
810 ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
811 if skip_attn and not is_profile:
@@ -878,6 +879,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
879 is_profile=is_profile,
880 context_len=context_len,
881 valid_dummy_state_slots=valid_dummy_state_slots,
882 + randomize_inputs=randomize_inputs,
883 )
884 self.kv_connector.set_disabled(False)
885
@@ -968,7 +970,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
970 self.pooling_runner.dummy_pooler_run(hidden_states)
971
972 @torch.inference_mode()
971 - def profile_run(self) -> None:
973 + def profile_run(self, randomize_inputs: bool = False) -> None:
974 if self.supports_mm_inputs and self.is_first_pp_rank:
975 mm_config = self.model_config.multimodal_config
976 if mm_config is not None and not mm_config.skip_mm_profiling:
@@ -981,7 +983,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
983 )
984
985 hidden_states, sample_hidden_states = self._dummy_run(
984 - self.max_num_tokens, skip_attn=True, is_profile=True
986 + self.max_num_tokens,
987 + skip_attn=True,
988 + is_profile=True,
989 + randomize_inputs=randomize_inputs,
990 )
991
992 # Only run sampler/pooler on last PP rank (non-last ranks return None).
@@ -1696,6 +1701,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1701 is_profile: bool = False,
1702 context_len: int = 0,
1703 valid_dummy_state_slots: bool = False,
1704 + randomize_inputs: bool = False,
1705 ) -> ModelRunnerOutput | IntermediateTensors | None:
1706 if not dummy_run:
1707 # Update the request states.
@@ -1814,6 +1820,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1820 # so MoE memory is measured and MoE kernels are exercised.
1821 is_padding=not is_profile,
1822 )
1823 + if randomize_inputs:
1824 + # All-zero input_ids route every token to the same experts.
1825 + input_batch.input_ids.random_(0, self.vocab_size)
1826 if self.pcp_manager is not None:
1827 input_batch = self.pcp_manager.prepare_inputs_to_capture(input_batch)
1828 if skip_attn_for_dummy_run:
vllm/v1/worker/gpu_model_runner.py
+5 -2
@@ -5735,6 +5735,9 @@ class GPUModelRunner(
5735 - during profile_run
5736 - during DP rank dummy run
5737 """
5738 + # The worker also resolves this flag and passes randomize_inputs, but
5739 + # V1 has internal dummy runs (e.g. CUDA graph capture) that don't go
5740 + # through the worker, so keep the check here too.
5741 dp_size = self.vllm_config.parallel_config.data_parallel_size
5742 randomize_inputs = randomize_inputs or (
5743 envs.VLLM_RANDOMIZE_DP_DUMMY_INPUTS and dp_size > 1
@@ -6420,7 +6423,7 @@ class GPUModelRunner(
6423 max_task = max(output_size.items(), key=lambda x: x[1])[0]
6424 return self._dummy_pooler_run_task(hidden_states, max_task)
6425
6423 - def profile_run(self) -> None:
6426 + def profile_run(self, randomize_inputs: bool = False) -> None:
6427 # Profile with multimodal encoder & encoder cache.
6428 if self.supports_mm_inputs:
6429 mm_config = self.model_config.multimodal_config
@@ -6481,7 +6484,7 @@ class GPUModelRunner(
6484
6485 # Add `is_profile` here to pre-allocate communication buffers
6486 hidden_states, last_hidden_states = self._dummy_run(
6484 - self.max_num_tokens, is_profile=True
6487 + self.max_num_tokens, is_profile=True, randomize_inputs=randomize_inputs
6488 )
6489 if get_pp_group().is_last_rank:
6490 if self.is_pooling_model:
vllm/v1/worker/gpu_worker.py
+21 -4
@@ -613,7 +613,7 @@ class Worker(WorkerBase):
613 if kv_cache_memory_bytes := self.cache_config.kv_cache_memory_bytes:
614 # still need a profile run which compiles the model for
615 # max_num_batched_tokens
616 - self.model_runner.profile_run()
616 + self.model_runner.profile_run(randomize_inputs=self.randomize_dummy_inputs)
617
618 msg = (
619 f"Initial free memory {format_gib(self.init_snapshot.free_memory)} "
@@ -650,7 +650,7 @@ class Worker(WorkerBase):
650 # memory_profiling measures, restoring the original limit.
651 self._scoped_allocator_max_split(max_split_size_mb=20),
652 ):
653 - self.model_runner.profile_run()
653 + self.model_runner.profile_run(randomize_inputs=self.randomize_dummy_inputs)
654
655 # Profile CUDA graph memory if graphs will be captured.
656 # ROCm is included: #44825 moved the profiler to
@@ -885,7 +885,12 @@ class Worker(WorkerBase):
885 # We skip EPLB here since we don't want to record dummy metrics
886 for size in sorted(warmup_sizes, reverse=True):
887 logger.info("Compile and warming up model for size %d", size)
888 - self.model_runner._dummy_run(size, skip_eplb=True, remove_lora=False)
888 + self.model_runner._dummy_run(
889 + size,
890 + skip_eplb=True,
891 + remove_lora=False,
892 + randomize_inputs=self.randomize_dummy_inputs,
893 + )
894 self.model_runner.maybe_remove_all_loras(self.model_runner.lora_config)
895
896 # Warmup and tune the kernels used during model execution before
@@ -1387,9 +1392,21 @@ class Worker(WorkerBase):
1392 # Recreate it so the next profile_prefix is honored.
1393 self.profiler = None
1394
1395 + @property
1396 + def randomize_dummy_inputs(self) -> bool:
1397 + # Not cached: elastic EP rewrites parallel_config in place on reconfigure.
1398 + return (
1399 + envs.VLLM_RANDOMIZE_DP_DUMMY_INPUTS
1400 + and self.parallel_config.data_parallel_size > 1
1401 + )
1402 +
1403 def execute_dummy_batch(self) -> None:
1404 num_tokens = getattr(self.model_runner, "uniform_decode_query_len", 1)
1392 - self.model_runner._dummy_run(num_tokens, uniform_decode=True)
1405 + self.model_runner._dummy_run(
1406 + num_tokens,
1407 + uniform_decode=True,
1408 + randomize_inputs=self.randomize_dummy_inputs,
1409 + )
1410
1411 def add_lora(self, lora_request: LoRARequest) -> bool:
1412 return self.model_runner.add_lora(lora_request)