| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Cold start and warm start tests for vLLM-compile. |
| 4 | |
| 5 | Cold start runs in a separate child process which |
| 6 | populates on-disk caches and asserts cold-start counters. Warm start |
| 7 | then runs in the parent with clean in-memory state but populated caches. |
| 8 | """ |
| 9 | |
| 10 | import multiprocessing as mp |
| 11 | from typing import NamedTuple |
| 12 | |
| 13 | import pytest |
| 14 | from torch._dynamo.utils import counters |
| 15 | |
| 16 | import vllm.envs as envs |
| 17 | from vllm.compilation.counter import compilation_counter |
| 18 | from vllm.config import CompilationConfig, CompilationMode, CUDAGraphMode, PassConfig |
| 19 | from vllm.utils.torch_utils import is_torch_equal_or_newer |
| 20 | |
| 21 | from ...utils import create_new_process_for_each_test, requires_spawn_multiprocessing |
| 22 | |
| 23 | MODEL = "microsoft/Phi-tiny-MoE-instruct" |
| 24 | |
| 25 | |
| 26 | def _run_vllm(vllm_runner): |
| 27 | with vllm_runner( |
| 28 | MODEL, |
| 29 | trust_remote_code=False, |
| 30 | max_model_len=256, |
| 31 | max_num_batched_tokens=1024, |
| 32 | load_format="dummy", |
| 33 | compilation_config=CompilationConfig( |
| 34 | mode=CompilationMode.VLLM_COMPILE, |
| 35 | cudagraph_mode=CUDAGraphMode.NONE, |
| 36 | ), |
| 37 | # Phi-tiny-MoE uses SWA, whose admission cap is `cdiv(L, block_size) + 1` |
| 38 | # at default block_size=16 — i.e. 17 blocks for max_model_len=256. Use |
| 39 | # 32 for headroom. |
| 40 | num_gpu_blocks_override=32, |
| 41 | ): |
| 42 | pass |
| 43 | |
| 44 | |
| 45 | def _cold_start(vllm_runner): |
| 46 | counters.clear() |
| 47 | with compilation_counter.expect( |
| 48 | num_compiled_artifacts_saved=3, |
| 49 | num_compiled_artifacts_loaded=0, |
| 50 | ): |
| 51 | _run_vllm(vllm_runner) |
| 52 | assert counters["aot_autograd"]["total"] == 33 |
| 53 | assert counters["aot_autograd"]["autograd_cache_miss"] == 3 |
| 54 | assert counters["aot_autograd"]["autograd_cache_hit"] == 0 |
| 55 | |
| 56 | |
| 57 | @create_new_process_for_each_test() |
| 58 | @pytest.mark.parametrize("mega_aot_artifact", ["0", "1"]) |
| 59 | def test_moe_startup(monkeypatch, vllm_runner, fresh_vllm_cache, mega_aot_artifact): |
| 60 | monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0") |
| 61 | monkeypatch.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", mega_aot_artifact) |
| 62 | monkeypatch.setenv("VLLM_DEEP_GEMM_WARMUP", "skip") |
| 63 | |
| 64 | # ROCm and XPU cannot fork safely after device discovery. |
| 65 | # This model has 32 identical transformer layers which produce |
| 66 | # 33 subgraphs after splitting on attention — only 3 are unique. |
| 67 | ctx = ( |
| 68 | mp.get_context("spawn") |
| 69 | if requires_spawn_multiprocessing() |
| 70 | else mp.get_context("fork") |
| 71 | ) |
| 72 | p = ctx.Process(target=_cold_start, args=(vllm_runner,)) |
| 73 | p.start() |
| 74 | p.join() |
| 75 | assert p.exitcode == 0, "Cold-start child failed" |
| 76 | |
| 77 | # Warm start — compiled artifacts loaded from disk cache. |
| 78 | counters.clear() |
| 79 | with compilation_counter.expect( |
| 80 | num_compiled_artifacts_loaded=3, |
| 81 | num_compiled_artifacts_saved=0, |
| 82 | ): |
| 83 | _run_vllm(vllm_runner) |
| 84 | mega_aot_active = envs.VLLM_USE_MEGA_AOT_ARTIFACT and is_torch_equal_or_newer( |
| 85 | "2.10.0" |
| 86 | ) |
| 87 | if mega_aot_active: |
| 88 | # MEGA_AOT_ARTIFACT is enabled, so we expect no aot_autograd running on |
| 89 | # subgraphs. |
| 90 | assert counters["aot_autograd"]["total"] == 0 |
| 91 | else: |
| 92 | assert counters["aot_autograd"]["total"] == 30 |
| 93 | assert counters["aot_autograd"]["autograd_cache_miss"] == 0 |
| 94 | assert ( |
| 95 | counters["aot_autograd"]["autograd_cache_hit"] == 0 |
| 96 | ) # No miss at aot_autograd level causing disk I/O. |
| 97 | |
| 98 | |
| 99 | # --------------------------------------------------------------------------- |
| 100 | # Parametrized model startup tests |
| 101 | # --------------------------------------------------------------------------- |
| 102 | |
| 103 | |
| 104 | class ModelStartupSpec(NamedTuple): |
| 105 | model: str |
| 106 | hf_overrides: dict |
| 107 | cold_artifacts_saved: int |
| 108 | warm_artifacts_saved: int |
| 109 | warm_artifacts_loaded: int |
| 110 | |
| 111 | |
| 112 | _SMALL_MOE_OVERRIDES = { |
| 113 | "num_hidden_layers": 8, |
| 114 | "hidden_size": 256, |
| 115 | "intermediate_size": 512, |
| 116 | "num_attention_heads": 8, |
| 117 | "num_key_value_heads": 1, |
| 118 | "n_routed_experts": 8, |
| 119 | } |
| 120 | |
| 121 | MODEL_SPECS = [ |
| 122 | pytest.param( |
| 123 | ModelStartupSpec( |
| 124 | model="openai/gpt-oss-120b", |
| 125 | hf_overrides={ |
| 126 | "num_hidden_layers": 8, |
| 127 | "hidden_size": 256, |
| 128 | "intermediate_size": 512, |
| 129 | "num_attention_heads": 8, |
| 130 | "num_key_value_heads": 1, |
| 131 | "num_local_experts": 8, |
| 132 | }, |
| 133 | cold_artifacts_saved=3, |
| 134 | warm_artifacts_saved=0, |
| 135 | warm_artifacts_loaded=3, |
| 136 | ), |
| 137 | id="gpt_oss_120b", |
| 138 | ), |
| 139 | pytest.param( |
| 140 | ModelStartupSpec( |
| 141 | model="moonshotai/Kimi-K2.5", |
| 142 | hf_overrides={"text_config": _SMALL_MOE_OVERRIDES}, |
| 143 | cold_artifacts_saved=4, |
| 144 | # https://github.com/vllm-project/vllm/issues/38051 |
| 145 | warm_artifacts_saved=0 if is_torch_equal_or_newer("2.12.0") else 4, |
| 146 | warm_artifacts_loaded=4 if is_torch_equal_or_newer("2.12.0") else 0, |
| 147 | ), |
| 148 | id="kimi_k2.5", |
| 149 | ), |
| 150 | pytest.param( |
| 151 | ModelStartupSpec( |
| 152 | model="zai-org/GLM-4.5", |
| 153 | hf_overrides=_SMALL_MOE_OVERRIDES, |
| 154 | cold_artifacts_saved=4, |
| 155 | warm_artifacts_saved=0, |
| 156 | warm_artifacts_loaded=4, |
| 157 | ), |
| 158 | id="glm_4.5", |
| 159 | ), |
| 160 | pytest.param( |
| 161 | ModelStartupSpec( |
| 162 | model="MiniMaxAI/MiniMax-M2.5", |
| 163 | hf_overrides=_SMALL_MOE_OVERRIDES, |
| 164 | cold_artifacts_saved=3, |
| 165 | warm_artifacts_saved=0, |
| 166 | warm_artifacts_loaded=3, |
| 167 | ), |
| 168 | id="minimax_m2.5", |
| 169 | ), |
| 170 | ] |
| 171 | |
| 172 | |
| 173 | def _run_model(vllm_runner, spec: ModelStartupSpec): |
| 174 | with vllm_runner( |
| 175 | spec.model, |
| 176 | trust_remote_code=True, |
| 177 | max_model_len=256, |
| 178 | max_num_batched_tokens=1024, |
| 179 | block_size=64, |
| 180 | load_format="dummy", |
| 181 | hf_overrides=spec.hf_overrides, |
| 182 | compilation_config=CompilationConfig( |
| 183 | mode=CompilationMode.VLLM_COMPILE, |
| 184 | cudagraph_mode=CUDAGraphMode.NONE, |
| 185 | pass_config=PassConfig(fuse_allreduce_rms=False), |
| 186 | ), |
| 187 | num_gpu_blocks_override=16, |
| 188 | ): |
| 189 | pass |
| 190 | |
| 191 | |
| 192 | def _check_model_run(vllm_runner, spec: ModelStartupSpec, is_cold_start: bool): |
| 193 | """Runs a model and checks the number of compiled artifacts.""" |
| 194 | old = compilation_counter.clone() |
| 195 | _run_model(vllm_runner, spec) |
| 196 | saved = ( |
| 197 | compilation_counter.num_compiled_artifacts_saved |
| 198 | - old.num_compiled_artifacts_saved |
| 199 | ) |
| 200 | loaded = ( |
| 201 | compilation_counter.num_compiled_artifacts_loaded |
| 202 | - old.num_compiled_artifacts_loaded |
| 203 | ) |
| 204 | |
| 205 | start_type = "COLD" if is_cold_start else "WARM" |
| 206 | # Print actual values for debugging — intentional, helps diagnose |
| 207 | # failures and calibrate expected counts when adding new models. |
| 208 | print(f"\n=== {start_type} START for {spec.model} ===") |
| 209 | print(f" num_compiled_artifacts_saved={saved}") |
| 210 | print(f" num_compiled_artifacts_loaded={loaded}") |
| 211 | |
| 212 | if is_cold_start: |
| 213 | expected_saved = spec.cold_artifacts_saved |
| 214 | expected_loaded = 0 |
| 215 | else: |
| 216 | expected_saved = spec.warm_artifacts_saved |
| 217 | expected_loaded = spec.warm_artifacts_loaded |
| 218 | |
| 219 | assert saved == expected_saved, f"{start_type.lower()}_artifacts_saved: got {saved}" |
| 220 | assert loaded == expected_loaded, ( |
| 221 | f"{start_type.lower()}_artifacts_loaded: got {loaded}" |
| 222 | ) |
| 223 | |
| 224 | |
| 225 | def _cold_start_model(vllm_runner, spec: ModelStartupSpec): |
| 226 | _check_model_run(vllm_runner, spec, is_cold_start=True) |
| 227 | |
| 228 | |
| 229 | @pytest.mark.parametrize("spec", MODEL_SPECS) |
| 230 | @create_new_process_for_each_test() |
| 231 | def test_model_startup(monkeypatch, vllm_runner, fresh_vllm_cache, spec): |
| 232 | monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0") |
| 233 | monkeypatch.setenv("VLLM_DEEP_GEMM_WARMUP", "skip") |
| 234 | |
| 235 | # ROCm and XPU cannot fork safely after device discovery. |
| 236 | ctx = ( |
| 237 | mp.get_context("spawn") |
| 238 | if requires_spawn_multiprocessing() |
| 239 | else mp.get_context("fork") |
| 240 | ) |
| 241 | p = ctx.Process(target=_cold_start_model, args=(vllm_runner, spec)) |
| 242 | p.start() |
| 243 | p.join() |
| 244 | assert p.exitcode == 0, "Cold-start child failed" |
| 245 | |
| 246 | # Warm start — compiled artifacts loaded from disk cache. |
| 247 | _check_model_run(vllm_runner, spec, is_cold_start=False) |