[CI] Give every Buildkite step an explicit key, and a hook to enforce it (#56329)

Signed-off-by: Tahsin Tunan <tahsintunan@gmail.com>

Tahsin Tunan committed Sep 12, 2026 at 07:08 UTC 80650454dcf1a0b758b31ce59c004f49897963db
12 files changed +139 -1
.buildkite/intel_jobs/basic_correctness_intel.yaml
+1
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-xpu
4 steps:
5 - label: XPU Sleep Mode
6 + key: xpu-sleep-mode
7 timeout_in_minutes: 45
8 device: intel_gpu
9 agent_tags:
.buildkite/intel_jobs/engine_intel.yaml
+2
@@ -41,6 +41,7 @@ steps:
41 pytest -v -s engine/test_arg_utils.py test_sequence.py test_logger.py test_vllm_port.py jit_monitor/test_hooks.py'
42
43 - label: Engine (1 GPU)
44 + key: engine-1-gpu
45 timeout_in_minutes: 30
46 device: intel_gpu
47 agent_tags:
@@ -65,6 +66,7 @@ steps:
66 VLLM_XPU_ENABLE_XPU_GRAPH=1 pytest -v -s test_config.py'
67
68 - label: V1 e2e (2 GPUs)
69 + key: v1-e2e-2-gpus
70 timeout_in_minutes: 30
71 device: intel_gpu
72 agent_tags:
.buildkite/intel_jobs/kernels_intel.yaml
+1
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-xpu
4 steps:
5 - label: vLLM IR Tests
6 + key: vllm-ir-tests
7 timeout_in_minutes: 30
8 device: intel_gpu
9 agent_tags:
.buildkite/intel_jobs/lora_intel.yaml
+6
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-xpu
4 steps:
5 - label: LoRA Runtime + Utils
6 + key: lora-runtime-utils
7 timeout_in_minutes: 45
8 device: intel_gpu
9 agent_tags:
@@ -36,6 +37,7 @@ steps:
37 pytest -v -s lora/test_worker.py'
38
39 - label: LoRA Fused/MoE Kernels
40 + key: lora-fused-moe-kernels
41 timeout_in_minutes: 45
42 device: intel_gpu
43 agent_tags:
@@ -60,6 +62,7 @@ steps:
62 pytest -v -s lora/test_moe_lora_align_sum.py --deselect="tests/lora/test_moe_lora_align_sum.py::test_moe_lora_align_block_size_mixed_base_and_lora[1]"'
63
64 - label: LoRA Punica Kernels
65 + key: lora-punica-kernels
66 timeout_in_minutes: 45
67 device: intel_gpu
68 agent_tags:
@@ -86,6 +89,7 @@ steps:
89 pytest -v -s lora/test_punica_ops.py::test_add_lora_fused_moe_early_exit'
90
91 - label: LoRA Punica FP8/XPU Ops
92 + key: lora-punica-fp8-xpu-ops
93 timeout_in_minutes: 60
94 device: intel_gpu
95 agent_tags:
@@ -110,6 +114,7 @@ steps:
114 pytest -v -s lora/test_punica_xpu_ops.py'
115
116 - label: LoRA Models
117 + key: lora-models
118 timeout_in_minutes: 45
119 device: intel_gpu
120 agent_tags:
@@ -137,6 +142,7 @@ steps:
142 pytest -s -v lora/test_minicpmv_tp.py'
143
144 - label: LoRA Multimodal
145 + key: lora-multimodal
146 timeout_in_minutes: 45
147 device: intel_gpu
148 agent_tags:
.buildkite/intel_jobs/misc_intel.yaml
+7 -1
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-xpu
4 steps:
5 - label: V1 Core + KV + Metrics
6 + key: v1-core-kv-metrics-intel
7 timeout_in_minutes: 45
8 device: intel_gpu
9 agent_tags:
@@ -32,9 +33,10 @@ steps:
33 export VLLM_WORKER_MULTIPROC_METHOD=spawn &&
34 cd tests &&
35 pytest -v -s v1/executor &&
35 - VLLM_BATCH_INVARIANT=1 pytest -v -s -m 'not cpu_test' v1/ec_connector/unit'
36 + VLLM_BATCH_INVARIANT=1 pytest -v -s -m "not cpu_test" v1/ec_connector/unit'
37
38 - label: V1 Sample + Logits
39 + key: v1-sample-logits-intel
40 timeout_in_minutes: 90
41 device: intel_gpu
42 agent_tags:
@@ -80,6 +82,7 @@ steps:
82 parallelism: 4
83
84 - label: Basic Models Tests (Initialization)
85 + key: basic-models-tests-initialization
86 timeout_in_minutes: 60
87 device: intel_gpu
88 agent_tags:
@@ -104,6 +107,7 @@ steps:
107 pytest -v -s models/test_initialization.py::test_can_initialize_large_subset[Eagle3MiniMaxM2ForCausalLM]'
108
109 - label: XPU CPU Offload
110 + key: xpu-cpu-offload
111 timeout_in_minutes: 60
112 device: intel_gpu
113 agent_tags:
@@ -131,6 +135,7 @@ steps:
135 pytest -v -s v1/kv_connector/unit/test_offloading_connector.py'
136
137 - label: NixlConnector PD accuracy (4 GPUs)
138 + key: nixlconnector-pd-accuracy-4-gpus
139 timeout_in_minutes: 60
140 num_devices: 4
141 device: intel_gpu
@@ -269,6 +274,7 @@ steps:
274 pytest -v -s utils_ --ignore=utils_/test_mem_utils.py'
275
276 - label: Fusion Unit Tests
277 + key: fusion-unit-tests
278 timeout_in_minutes: 30
279 device: intel_gpu
280 agent_tags:
.buildkite/intel_jobs/model_runner_v2_intel.yaml
+4
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-xpu
4 steps:
5 - label: Model Runner V2 Core Tests (Intel)
6 + key: model-runner-v2-core-tests-intel
7 timeout_in_minutes: 45
8 device: intel_gpu
9 agent_tags:
@@ -34,6 +35,7 @@ steps:
35 pytest -v -s v1/e2e/general/test_min_tokens.py'
36
37 - label: Model Runner V2 Examples (Intel)
38 + key: model-runner-v2-examples-intel
39 timeout_in_minutes: 45
40 device: intel_gpu
41 agent_tags:
@@ -64,6 +66,7 @@ steps:
66 python3 features/automatic_prefix_caching/prefix_caching_offline.py'
67
68 - label: Model Runner V2 Distributed (2 GPUs)
69 + key: model-runner-v2-distributed-2-gpus
70 timeout_in_minutes: 50
71 device: intel_gpu
72 agent_tags:
@@ -90,6 +93,7 @@ steps:
93 TARGET_TEST_SUITE=L4 pytest -v -s basic_correctness/test_basic_correctness.py -m "distributed\(num_gpus=2\)" -k "not ray and not True"'
94
95 - label: Model Runner V2 Spec Decode
96 + key: model-runner-v2-spec-decode
97 timeout_in_minutes: 50
98 device: intel_gpu
99 agent_tags:
.buildkite/intel_jobs/test-intel.yaml
+7
@@ -15,6 +15,7 @@ steps:
15 - exit_status: -10 # Agent was lost
16 limit: 2
17 - label: "XPU example Test"
18 + key: xpu-example-test
19 depends_on:
20 - image-build-xpu
21 timeout_in_minutes: 50
@@ -50,6 +51,7 @@ steps:
51 python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP4-LLMC --enforce-eager -tp 2 --max-model-len 8192
52 '
53 - label: "XPU W8A8 FP8 Linear Examples"
54 + key: xpu-w8a8-fp8-linear-examples
55 depends_on:
56 - image-build-xpu
57 timeout_in_minutes: 60
@@ -77,6 +79,7 @@ steps:
79 python3 examples/basic/offline_inference/generate.py --linear-backend torch --model meta-llama/Llama-3.2-1B-Instruct --quantization fp8 --enforce-eager --max-model-len 4096
80 '
81 - label: "XPU V1 test"
82 + key: xpu-v1-test
83 depends_on:
84 - image-build-xpu
85 timeout_in_minutes: 70
@@ -107,6 +110,7 @@ steps:
110 pytest -v -s v1/kv_connector/unit --ignore=v1/kv_connector/unit/test_lmcache_integration.py --ignore=v1/kv_connector/unit/test_hf3fs_client.py --ignore=v1/kv_connector/unit/test_hf3fs_connector.py --ignore=v1/kv_connector/unit/test_hf3fs_metadata_server.py --ignore=v1/kv_connector/unit/test_offloading_connector.py'
111 parallelism: 2
112 - label: "XPU server test"
113 + key: xpu-server-test
114 depends_on:
115 - image-build-xpu
116 timeout_in_minutes: 45
@@ -130,6 +134,7 @@ steps:
134 pytest -v -s entrypoints/multimodal/openai/chat_completion/test_audio_in_video.py &&
135 pytest -v -s benchmarks/test_serve_cli.py'
136 - label: "XPU quantization test"
137 + key: xpu-quantization-test
138 depends_on:
139 - image-build-xpu
140 timeout_in_minutes: 30
@@ -158,6 +163,7 @@ steps:
163 pytest -v -s quantization/test_auto_round.py &&
164 pytest -v -s quantization/test_online.py'
165 - label: "XPU compressed tensors FP8 test"
166 + key: xpu-compressed-tensors-fp8-test
167 depends_on:
168 - image-build-xpu
169 timeout_in_minutes: 60
@@ -182,6 +188,7 @@ steps:
188 pytest -v -s quantization/test_compressed_tensors.py::test_compressed_tensors_fp8 &&
189 pytest -v -s quantization/test_compressed_tensors.py::test_compressed_tensors_fp8_block_enabled'
190 - label: "XPU Graph"
191 + key: xpu-graph
192 depends_on:
193 - image-build-xpu
194 timeout_in_minutes: 60
.buildkite/test_areas/docker.yaml
+1
@@ -3,6 +3,7 @@ depends_on:
3 - image-build-cpu
4 steps:
5 - label: ":computer: (CPU) Docker Build Metadata"
6 + key: docker-build-metadata
7 timeout_in_minutes: 20
8 device: cpu-small
9 source_file_dependencies:
.buildkite/test_areas/lm_eval.yaml
+1
@@ -470,6 +470,7 @@ steps:
470 - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "deepseek-v4-flash"
471
472 - label: ":nvidia: (H200 MIG 35GB) MRCR Eval Small Models"
473 + key: mrcr-eval-small-models
474 device: h200_35gb
475 timeout_in_minutes: 25
476 source_file_dependencies:
.buildkite/test_areas/rust_frontend.yaml
+5
@@ -3,6 +3,7 @@ depends_on:
3 - image-build
4 steps:
5 - label: ":nvidia: (H200 MIG 18GB) Rust Frontend OpenAI Coverage"
6 + key: rust-frontend-openai-coverage
7 timeout_in_minutes: 30
8 device: h200_18gb
9 working_dir: "/vllm-workspace/tests"
@@ -67,6 +68,7 @@ steps:
68 - vllm/platforms/rocm.py
69
70 - label: ":nvidia: (H200 MIG 18GB) Rust Frontend Serve/Admin Coverage"
71 + key: rust-frontend-serve-admin-coverage
72 timeout_in_minutes: 25
73 device: h200_18gb
74 working_dir: "/vllm-workspace/tests"
@@ -116,6 +118,7 @@ steps:
118 - vllm/platforms/rocm.py
119
120 - label: ":nvidia: (H200 MIG 18GB) Rust Frontend Core Correctness"
121 + key: rust-frontend-core-correctness
122 timeout_in_minutes: 20
123 device: h200_18gb
124 working_dir: "/vllm-workspace/tests"
@@ -130,6 +133,7 @@ steps:
133 - pytest -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine
134
135 - label: ":nvidia: (H200 MIG 35GB) Rust Frontend Tool Use"
136 + key: rust-frontend-tool-use
137 device: h200_35gb
138 timeout_in_minutes: 25
139 working_dir: "/vllm-workspace/tests"
@@ -145,6 +149,7 @@ steps:
149 - pytest -v -s tool_use --ignore=tool_use/mistral --models llama3.2 -k "not test_response_format_with_tool_choice_required and not test_parallel_tool_calls_false and not test_tool_call_and_choice"
150
151 - label: ":nvidia: (L4) Rust Frontend Distributed"
152 + key: rust-frontend-distributed
153 timeout_in_minutes: 25
154 device: l4
155 num_devices: 4
.pre-commit-config.yaml
+7
@@ -177,6 +177,13 @@ repos:
177 files: ^\.github/mergify\.yml$
178 pass_filenames: false
179 additional_dependencies: [PyYAML, regex]
180 + - id: check-buildkite-step-keys
181 + name: Check every Buildkite step declares an explicit key
182 + language: python
183 + entry: python tools/pre_commit/check_buildkite_step_keys.py
184 + files: ^\.buildkite/.*\.ya?ml$
185 + pass_filenames: false
186 + additional_dependencies: [PyYAML]
187 - id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
188 name: Run mypy for Python 3.10
189 entry: python tools/pre_commit/mypy.py "3.10"
tools/pre_commit/check_buildkite_step_keys.py new
+97
@@ -0,0 +1,97 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Check that every Buildkite step the CI generator reads declares a key.
4 +
5 +A step with no `key:` is published under one the generator derives from its
6 +label, which makes the job's address a function of its title: retitling the
7 +step silently renames it and breaks retries, `depends_on:` and build history.
8 +The generator only reads the directories a pipeline config names in
9 +`job_dirs:`, so this walks the same files and fails on any step without a key.
10 +
11 +Usage:
12 + python tools/pre_commit/check_buildkite_step_keys.py
13 +"""
14 +
15 +import sys
16 +from pathlib import Path
17 +
18 +import yaml
19 +
20 +BUILDKITE = Path(".buildkite")
21 +CONFIG_GLOB = "ci_config*.yaml"
22 +JOB_GLOB = "*.yaml"
23 +
24 +
25 +def job_dirs() -> list[Path]:
26 + """Every directory the pipeline generator reads, per the pipeline configs."""
27 + configs = sorted(BUILDKITE.glob(CONFIG_GLOB))
28 + if not configs:
29 + raise SystemExit(f"no pipeline config matched {BUILDKITE / CONFIG_GLOB}")
30 + dirs: set[Path] = set()
31 + for config in configs:
32 + with open(config, encoding="utf-8") as f:
33 + doc = yaml.safe_load(f)
34 + declared = doc.get("job_dirs") if isinstance(doc, dict) else None
35 + if not isinstance(declared, list) or not declared:
36 + raise SystemExit(f"{config}: `job_dirs` is missing or not a list")
37 + for name in declared:
38 + job_dir = Path(name)
39 + if not job_dir.is_dir():
40 + raise SystemExit(f"{config}: job_dir `{name}` does not exist")
41 + dirs.add(job_dir)
42 + return sorted(dirs)
43 +
44 +
45 +def scan(steps: object, source: Path) -> list[str]:
46 + """Label of every step in a `steps:` list that declares no key."""
47 + if not isinstance(steps, list):
48 + raise SystemExit(f"{source}: `steps:` is not a list")
49 + keyless: list[str] = []
50 + for i, step in enumerate(steps):
51 + # A scalar shorthand such as `- wait` has nowhere to put a key. Every
52 + # other step does, so there are no other exemptions. A `mirror:` block
53 + # is not a step here: the generator publishes it as `<hw>-<parent key>`.
54 + if not isinstance(step, dict):
55 + continue
56 + label = step.get("label") or step.get("group") or f"step {i}"
57 + if "steps" in step:
58 + raise SystemExit(
59 + f"{source}: `{label}` nests its own `steps:`. The generator has "
60 + "no group step and drops the children without an error, so give "
61 + "each one its own entry."
62 + )
63 + key = step.get("key")
64 + if not (isinstance(key, str) and key.strip()):
65 + keyless.append(label)
66 + return keyless
67 +
68 +
69 +def keyless_steps(path: Path) -> list[str]:
70 + with open(path, encoding="utf-8") as f:
71 + doc = yaml.safe_load(f)
72 + if not isinstance(doc, dict) or "steps" not in doc:
73 + return []
74 + return scan(doc["steps"], path)
75 +
76 +
77 +def main() -> int:
78 + files = sorted({p for d in job_dirs() for p in d.rglob(JOB_GLOB)})
79 + if not files:
80 + raise SystemExit("no pipeline files under the declared job_dirs")
81 + keyless = [(path, label) for path in files for label in keyless_steps(path)]
82 + if not keyless:
83 + return 0
84 + print(f"{len(keyless)} Buildkite step(s) have no explicit key:\n", file=sys.stderr)
85 + for path, label in keyless:
86 + print(f" {path}: {label}", file=sys.stderr)
87 + print(
88 + "\nAdd a `key:` naming the test each step runs. Without one the pipeline"
89 + "\ngenerator derives the key from the label, so retitling the step renames"
90 + "\nits job and breaks retries and build history.",
91 + file=sys.stderr,
92 + )
93 + return 1
94 +
95 +
96 +if __name__ == "__main__":
97 + raise SystemExit(main())