[CI] Fix and extend PR/issue auto-labeling (#51459)

Signed-off-by: Joe Cotant <joe@inferact.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
jcotant-inferact
2026-08-19 21:45:44 +00:00
committed by GitHub
co-authored by Claude Opus 5 mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent d591d1d511
commit c205726108
6 changed files with 528 additions and 62 deletions
-40
View File
@@ -1,40 +0,0 @@
name: 🤗 Support request for a new model from huggingface
description: Submit a proposal/request for a new model from huggingface
title: "[New Model]: "
labels: ["new model"]
body:
- type: markdown
attributes:
value: >
#### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/vllm-project/vllm/issues?q=is%3Aissue+sort%3Acreated-desc+).
#### We also highly recommend you read https://docs.vllm.ai/en/latest/contributing/model/index.html first to understand how to add a new model.
- type: textarea
attributes:
label: The model to consider.
description: >
A huggingface url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 .
validations:
required: true
- type: textarea
attributes:
label: The closest model vllm already supports.
description: >
Here is the list of models already supported by vllm: https://github.com/vllm-project/vllm/tree/main/vllm/model_executor/models . Which model is the most similar to the model you want to add support for?
- type: textarea
attributes:
label: What's your difficulty of supporting the model you want?
description: >
For example, any new operators or new architecture?
- type: markdown
attributes:
value: >
Thanks for contributing 🎉!
- type: checkboxes
id: askllm
attributes:
label: Before submitting a new issue...
options:
- label: Make sure you already searched for relevant issues, and asked the chatbot living at the bottom right corner of the [documentation page](https://docs.vllm.ai/en/latest/), which can answer lots of frequently asked questions.
required: true
+176 -22
View File
@@ -93,6 +93,7 @@ pull_request_rules:
- files~=^examples/.*deepseek.*\.py
- files~=^tests/.*deepseek.*\.py
- files~=^vllm/model_executor/models/.*deepseek.*\.py
- files~=^vllm/models/deepseek.*/
- files~=^vllm/tool_parsers/.*deepseek.*\.py
- files~=^vllm/reasoning/.*deepseek.*\.py
- files~=^vllm/transformers_utils/.*deepseek.*\.py
@@ -133,9 +134,7 @@ pull_request_rules:
- files~=^examples/.*llama.*\.py
- files~=^tests/.*llama.*\.py
- files~=^vllm/model_executor/models/.*llama.*\.py
- files~=^vllm/reasoning/.*llama.*\.py
- files~=^vllm/tool_parsers/.*llama.*\.py
- files~=^vllm/transformers_utils/.*llama.*\.py
- title~=(?i)llama
actions:
label:
@@ -177,9 +176,17 @@ pull_request_rules:
description: Automatically apply new-model label
conditions:
- label != stale
- and:
- files~=^vllm/model_executor/models/
- files=vllm/model_executor/models/registry.py
- or:
# A model file is added alongside a registry entry. added-files rather
# than files: touching an existing model file is a fix or a refactor,
# and a PR that removes a model is not a new one.
- and:
- added-files~=^vllm/model_executor/models/.*\.py$
- files=vllm/model_executor/models/registry.py
# A new top-level package under vllm/models/. common/ is shared kernel
# code rather than a model, and nested packages belong to a model that
# already exists.
- added-files~=^vllm/models/(?!common/)[^/]+/__init__\.py$
actions:
label:
add:
@@ -233,16 +240,12 @@ pull_request_rules:
conditions:
- label != stale
- or:
- files~=^examples/.*gpt[-_]?oss.*\.py
- files~=^tests/.*gpt[-_]?oss.*\.py
- files~=^tests/entrypoints/openai/test_response_api_with_harmony.py
- files~=^tests/entrypoints/test_context.py
- files~=^vllm/model_executor/models/.*gpt[-_]?oss.*\.py
- files~=^vllm/model_executor/layers/.*gpt[-_]?oss.*\.py
- files~=^vllm/entrypoints/openai/parser/harmony_utils.py
- files~=^vllm/entrypoints/tool_server.py
- files~=^vllm/entrypoints/tool.py
- files~=^vllm/entrypoints/context.py
# Harmony rendering now lives in the Rust frontend.
- files~=^rust/src/chat/src/.*/harmony/
- title~=(?i)gpt[-_]?oss
- title~=(?i)harmony
actions:
@@ -256,7 +259,6 @@ pull_request_rules:
- label != stale
- or:
- files~=(?i)kimi
- files~=(?i)moonshot
- title~=(?i)(?:kimi|moonshot)
actions:
label:
@@ -275,12 +277,73 @@ pull_request_rules:
add:
- k3
- name: label-glm
description: Automatically apply glm label
conditions:
- label != stale
- or:
# Anchored to the start of the filename so midashenglm.py (an unrelated
# model) does not match.
- files~=^vllm/model_executor/models/(?:chat)?glm[^/]*\.py
- files~=^tests/.*[/_](?:chat)?glm[^/]*\.py
- files~=^vllm/tool_parsers/.*glm.*\.py
- files~=^vllm/transformers_utils/.*/(?:chat)?glm[^/]*\.py
- title~=(?i)\b(?:chat)?glm
actions:
label:
add:
- glm
- name: label-minimax
description: Automatically apply minimax label
conditions:
- label != stale
- or:
- files~=^vllm/models/minimax.*/
- files~=^vllm/model_executor/models/.*minimax.*\.py
- files~=^vllm/tool_parsers/.*minimax.*\.py
- files~=^tests/.*minimax.*\.py
- title~=(?i)minimax
actions:
label:
add:
- minimax
- name: label-inkling
description: Automatically apply inkling label
conditions:
- label != stale
- or:
- files~=^vllm/models/inkling/
- files~=^vllm/tool_parsers/inkling.*\.py
- files~=^vllm/renderers/inkling.*\.py
- files~=^tests/.*inkling.*\.py
- title~=(?i)inkling
actions:
label:
add:
- inkling
- name: label-dsv4
description: Automatically apply DSv4 label
conditions:
- label != stale
- or:
- files~=^vllm/models/deepseek_v4/
- title~=(?i)(?:\bDSv4\b|deepseek[-\s]?v4)
actions:
label:
add:
- DSv4
- name: label-nvidia
description: Automatically apply nvidia label
conditions:
- label != stale
- or:
- files~=cuda
# Exclude requirements/: a dependency bump touching requirements/cuda.txt
# is not NVIDIA backend work.
- files~=^(?!requirements/).*cuda
- files~=cutlass
- files~=flashinfer
- files~=trtllm
@@ -300,7 +363,7 @@ pull_request_rules:
- files~=^csrc/rocm/
- files~=^docker/Dockerfile.rocm
- files~=^requirements/rocm.*\.txt
- files~=^vllm/model_executor/layers/fused_moe/rocm.*\.py
- files~=^vllm/model_executor/layers/fused_moe/.*rocm.*\.py
- files~=^vllm/v1/attention/backends/rocm.*\.py
- files~=^vllm/v1/attention/backends/mla/rocm.*\.py
- files~=^vllm/v1/attention/ops/rocm.*\.py
@@ -319,8 +382,8 @@ pull_request_rules:
- label != stale
- or:
- files~=^docker/Dockerfile.xpu
- files~=^\\.buildkite/intel_jobs/
- files=\.buildkite/ci_config_intel.yaml
- files~=^\.buildkite/intel_jobs/
- files=.buildkite/ci_config_intel.yaml
- files=vllm/model_executor/layers/fused_moe/experts/xpu_moe.py
- files=vllm/model_executor/kernels/linear/mixed_precision/xpu.py
- files=vllm/model_executor/kernels/linear/mxfp8/xpu.py
@@ -331,7 +394,6 @@ pull_request_rules:
- files=vllm/v1/worker/xpu_worker.py
- files=vllm/v1/worker/xpu_model_runner.py
- files=vllm/_xpu_ops.py
- files=vllm/kernels/xpu_ops.py
- files~=^vllm/lora/ops/xpu_ops
- files=vllm/lora/punica_wrapper/punica_xpu.py
- files=vllm/platforms/xpu.py
@@ -367,7 +429,7 @@ pull_request_rules:
- files=benchmarks/benchmark_serving_structured_output.py
- files=benchmarks/run_structured_output_benchmark.sh
- files=docs/features/structured_outputs.md
- files=^examples/features/structured_outputs/
- files~=^examples/features/structured_outputs/
- files~=^tests/v1/structured_output/
- files=tests/entrypoints/llm/test_struct_output_generate.py
- files~=^vllm/v1/structured_output/
@@ -382,8 +444,11 @@ pull_request_rules:
- label != stale
- or:
- files~=^vllm/v1/spec_decode/
# Model Runner V2 keeps its speculators in a separate subtree.
- files~=^vllm/v1/worker/gpu/spec_decode/
- files~=^tests/v1/spec_decode/
- files=^examples/features/speculative_decoding/
- files~=^tests/v1/e2e/spec_decode/
- files~=^examples/features/speculative_decoding/
- files~=^vllm/model_executor/models/.*eagle.*\.py
- files=vllm/model_executor/models/mlp_speculator.py
- files~=^vllm/transformers_utils/configs/(eagle|medusa|mlp_speculator)\.py
@@ -392,6 +457,33 @@ pull_request_rules:
add:
- speculative-decoding
# dflash covers the DFlash and DSpark speculative decoding techniques as one
# workstream, matching how they are tracked in the sprint board. It is applied
# in addition to speculative-decoding, never instead of it.
- name: label-dflash
description: Automatically apply dflash label
conditions:
- label != stale
- or:
- files~=^vllm/v1/spec_decode/dflash\.py
- files~=^vllm/v1/worker/gpu/spec_decode/dflash/
- files~=^vllm/model_executor/models/.*dflash.*\.py
- files~=^tests/.*dflash.*\.py
- title~=(?i)dflash
- files~=^vllm/v1/worker/gpu/spec_decode/dspark/
- files~=^vllm/model_executor/models/.*dspark.*\.py
- files~=^vllm/models/deepseek_v4/nvidia/dspark\.py
- files~=^vllm/models/deepseek_v4/amd/dspark\.py
- files~=^vllm/models/deepseek_v4/xpu/dspark\.py
- files~=^vllm/models/kimi_k3/nvidia/dspark_mla\.py
- files~=^vllm/transformers_utils/configs/k3_dspark\.py
- files~=^tests/.*dspark.*\.py
- title~=(?i)dspark
actions:
label:
add:
- dflash
- name: label-mrv2
description: Automatically apply mrv2 label
conditions:
@@ -416,7 +508,6 @@ pull_request_rules:
- files~=_tpu
- files~=tpu_
- files~=/tpu/
- files~=pallas
actions:
label:
add:
@@ -432,7 +523,6 @@ pull_request_rules:
- -files~=_tpu
- -files~=tpu_
- -files~=/tpu/
- -files~=pallas
actions:
label:
remove:
@@ -448,7 +538,6 @@ pull_request_rules:
- files~=^tests/parser/
- files~=^tests/reasoning/
- files~=^tests/entrypoints/openai/.*tool.*
- files~=^tests/entrypoints/anthropic/.*tool.*
- files~=^vllm/tool_parsers/
- files~=^vllm/parser/
- files~=^vllm/reasoning/
@@ -552,3 +641,68 @@ pull_request_rules:
label:
add:
- kv-connector
- name: label-scheduler
description: Automatically apply scheduler label
conditions:
- label != stale
- or:
- files~=^vllm/v1/core/sched/
- files~=^tests/v1/core/test_scheduler.*\.py
actions:
label:
add:
- scheduler
- name: label-kv-cache-manager
description: Automatically apply kv-cache-manager label
conditions:
- label != stale
- or:
# Scoped to the block/cache allocator in v1/core. KV transfer between
# instances is kv-connector, not this.
- files~=^vllm/v1/core/(?:kv_cache|block_pool|single_type_kv_cache)
- files~=^tests/v1/core/test_(?:kv_cache|single_type_kv_cache|prefix_caching).*\.py
actions:
label:
add:
- kv-cache-manager
- name: label-torch-compile
description: Automatically apply torch.compile label
conditions:
- label != stale
- or:
- files~=^vllm/compilation/
- files~=^tests/compile/
- title~=(?i)torch\.compile
actions:
label:
add:
- torch.compile
- name: label-ray
description: Automatically apply ray label
conditions:
- label != stale
- or:
- files~=^vllm/v1/executor/ray_
- files~=^tests/.*ray_.*\.py
- title~=(?i)\bray\b
actions:
label:
add:
- ray
- name: label-vllm-ir
description: Automatically apply vllm-ir label
conditions:
- label != stale
- or:
- files~=^vllm/ir/
- files~=^tests/ir/
- title~=(?i)vllm[-\s]?ir
actions:
label:
add:
- vllm-ir
+153
View File
@@ -190,6 +190,159 @@ jobs:
},
],
},
// Model labels. Title-only by design: issue bodies carry pasted
// collect_env output, tracebacks and configs that name unrelated
// models, hardware and libraries.
deepseek: {
keywords: [{ term: "DeepSeek", searchIn: "title" }],
},
DSv4: {
keywords: [{ term: "DSv4", searchIn: "title" }],
substrings: [{ term: "deepseek-v4", searchIn: "title" }],
},
llama: {
keywords: [{ term: "Llama", searchIn: "title" }],
},
mistral: {
keywords: [
{ term: "Mistral", searchIn: "title" },
{ term: "Ministral", searchIn: "title" },
{ term: "Voxtral", searchIn: "title" },
{ term: "Mixtral", searchIn: "title" },
{ term: "Pixtral", searchIn: "title" },
],
},
qwen: {
keywords: [{ term: "Qwen", searchIn: "title" }],
},
"gpt-oss": {
keywords: [{ term: "harmony", searchIn: "title" }],
substrings: [{ term: "gpt-oss", searchIn: "title" }],
},
glm: {
keywords: [
{ term: "GLM", searchIn: "title" },
{ term: "ChatGLM", searchIn: "title" },
],
},
minimax: {
substrings: [{ term: "minimax", searchIn: "title" }],
},
inkling: {
keywords: [{ term: "Inkling", searchIn: "title" }],
},
// Hardware backends, mirroring the cpu/rocm/intel-gpu rules above.
nvidia: {
keywords: [
{ term: "NVIDIA", searchIn: "title" },
{ term: "CUTLASS", searchIn: "title" },
],
},
tpu: {
keywords: [
{ term: "TPU", searchIn: "title" },
{ term: "XLA", searchIn: "title" },
],
},
// Subsystems.
//
// Single distinctive words go in `keywords` so the word-boundary
// match keeps them from firing inside longer words. Multi-word
// phrases go in `substrings`: they are specific enough on their
// own, and a boundary match would miss ordinary variations --
// "structured outputs", "tool parsers", "prefix caching".
"speculative-decoding": {
keywords: [
{ term: "eagle", searchIn: "title" },
{ term: "medusa", searchIn: "title" },
],
substrings: [
{ term: "speculative decoding", searchIn: "title" },
{ term: "speculative decode", searchIn: "title" },
{ term: "draft model", searchIn: "title" },
],
},
"structured-output": {
substrings: [
{ term: "structured output", searchIn: "title" },
{ term: "guided decoding", searchIn: "title" },
{ term: "JSON schema", searchIn: "title" },
],
},
"tool-calling": {
substrings: [
{ term: "tool calling", searchIn: "title" },
{ term: "tool parser", searchIn: "title" },
{ term: "function calling", searchIn: "title" },
],
},
"kv-connector": {
keywords: [
{ term: "NIXL", searchIn: "title" },
{ term: "LMCache", searchIn: "title" },
],
substrings: [
{ term: "KV transfer", searchIn: "title" },
{ term: "disaggregat", searchIn: "title" },
],
},
scheduler: {
keywords: [{ term: "scheduler", searchIn: "title" }],
},
"kv-cache-manager": {
substrings: [
{ term: "kv cache manager", searchIn: "title" },
{ term: "block manager", searchIn: "title" },
{ term: "prefix cache", searchIn: "title" },
{ term: "prefix caching", searchIn: "title" },
],
},
"torch.compile": {
substrings: [{ term: "torch.compile", searchIn: "title" }],
},
"multi-modality": {
keywords: [
{ term: "multimodal", searchIn: "title" },
{ term: "multi-modal", searchIn: "title" },
],
substrings: [
{ term: "image input", searchIn: "title" },
{ term: "audio input", searchIn: "title" },
],
},
"ci-failure": {
keywords: [{ term: "flaky", searchIn: "title" }],
substrings: [
{ term: "CI failure", searchIn: "title" },
{ term: "test failure", searchIn: "title" },
],
},
rl: {
keywords: [{ term: "RLHF", searchIn: "title" }],
substrings: [
{ term: "reinforcement learning", searchIn: "title" },
{ term: "weight update", searchIn: "title" },
],
},
ray: {
substrings: [
{ term: "Ray cluster", searchIn: "title" },
{ term: "Ray executor", searchIn: "title" },
{ term: "placement group", searchIn: "title" },
],
},
"vllm-ir": {
substrings: [
{ term: "vllm-ir", searchIn: "title" },
{ term: "vllm ir", searchIn: "title" },
],
},
rust: {
keywords: [
{ term: "Rust", searchIn: "title" },
{ term: "PyO3", searchIn: "title" },
],
},
// Add more label configurations here as needed
// example: {
// keywords: [...],
+7
View File
@@ -165,6 +165,13 @@ repos:
language: python
entry: python tools/pre_commit/generate_nightly_torch_test.py
files: ^requirements/test/cuda\.(in|txt)$
- id: check-label-rules
name: Check auto-label rules still match real files
language: python
entry: python tools/pre_commit/check_label_rules.py
files: ^\.github/mergify\.yml$
pass_filenames: false
additional_dependencies: [PyYAML, regex]
- id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
name: Run mypy for Python 3.10
entry: python tools/pre_commit/mypy.py "3.10"
+95
View File
@@ -0,0 +1,95 @@
# Labels
vLLM applies most labels automatically. Three systems do the work:
| | Config | Applies to |
| --- | --- | --- |
| Mergify | `.github/mergify.yml` | pull requests, by changed file path or title |
| GitHub Action | `.github/workflows/issue_autolabel.yml` | issues, by title keyword |
| Issue templates | `.github/ISSUE_TEMPLATE/*.yml` | issues, by which template was used |
A label that none of them applies has to be applied by hand, and in practice
that means it mostly doesn't get applied at all.
A template's `labels:` field must name a label that already exists — GitHub
silently skips one that doesn't, with no error anywhere.
`mergify.yml` also drives reviewer auto-assignment; see
[Collaboration](../governance/collaboration.md) for adding yourself as a
maintainer of an area.
## Adding a label
A new label needs three things. If it can't have all three, don't create it.
1. **An audience.** Someone has to want to filter on it. "It would be nice to
know" isn't enough.
2. **An owner.** A person or SIG who triages that queue. Without one the label
accumulates issues nobody reads.
3. **A rule, in the same PR.** If you can't write a precise rule, the label
depends on humans remembering it exists.
## Measure before you argue
Volume claims should come from the repo, not from intuition. vLLM squash-merges,
so one commit on `main` is one PR:
```bash
git log --since="6 months ago" --oneline -- <path> | wc -l
```
For reference, at the time of writing: `speculative-decoding` 81, `llama` 85,
`mistral` 72, `tpu` 14. A subsystem in that range clears the bar comfortably.
Something in the low teens needs a stronger argument than volume.
The same applies to claiming a rule is too broad. Check what it actually labels
before rewriting it:
```bash
gh pr list --repo vllm-project/vllm --state merged --limit 200 \
--json number,title,labels
```
Several rules in this repo have been called over-broad on inspection and turned
out to be accurate when measured.
## Writing conditions
**`files=` is exact equality. `files~=` is a regex.** Writing a pattern after
`=` compares the pattern text to each filename, so it can never match:
```yaml
- files=^examples/features/speculative_decoding/ # never fires
- files~=^examples/features/speculative_decoding/ # correct
```
**Anchor file patterns.** An unanchored fragment matches far more than intended
— `files~=cuda` also matches `requirements/cuda.txt`.
**Watch for names that contain other names.** `midashenglm.py` contains "glm"
but is unrelated to GLM, so the `glm` rule anchors to the start of the filename.
**Keep an eye on where model code lives.** Newer models live in
`vllm/models/<model>/` rather than `vllm/model_executor/models/`. A rule that
only knows the old location silently stops matching.
**Prefer titles for issue keywords.** Issue bodies contain pasted `collect_env`
output, tracebacks and configs that mention unrelated hardware and libraries.
Body matching tags the reporter's environment rather than the topic.
**Single words go in `keywords`, phrases in `substrings`.** `keywords` matches on
word boundaries, which stops a term firing inside a longer word but also misses
ordinary variations — `structured output` will not match "structured outputs".
## Keeping rules honest
`tools/pre_commit/check_label_rules.py` fails if any file condition in
`mergify.yml` matches nothing in the tree. It runs via pre-commit when that file
changes. This catches conditions that were correct when written and rotted when
the code moved.
## Not everything needs a label
Triage and workflow labels — `bug`, `stale`, `ready`, `RFC`, `needs-rebase` —
and the bot provenance tags are applied by other mechanisms or by hand, and are
intentionally outside this system.
+97
View File
@@ -0,0 +1,97 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Check that auto-labeling rules still match real files.
Mergify silently ignores a condition that can never match, so a file-path
condition keeps passing review long after the file it names has moved or been
deleted. This walks every `files=` / `files~=` condition in `.github/mergify.yml`
and fails if one matches nothing in the tree.
Usage:
python tools/pre_commit/check_label_rules.py
"""
import subprocess
import sys
import regex as re
import yaml
MERGIFY = ".github/mergify.yml"
def tracked_files() -> list[str]:
out = subprocess.run(
["git", "ls-files"], capture_output=True, text=True, check=True
).stdout
return [line for line in out.split("\n") if line]
def file_conditions(node, found: list[str]) -> list[str]:
"""Collect every string condition nested under a rule's `conditions`."""
if isinstance(node, dict):
for value in node.values():
file_conditions(value, found)
elif isinstance(node, list):
for value in node:
file_conditions(value, found)
elif isinstance(node, str):
found.append(node)
return found
def check(files: list[str]) -> list[tuple[str, str, str]]:
with open(MERGIFY) as f:
rules = yaml.safe_load(f)["pull_request_rules"]
# removed-files is excluded: it names paths the tree no longer has.
attrs = ("files", "added-files", "modified-files")
dead = []
for rule in rules:
for cond in file_conditions(rule.get("conditions", []), []):
cond = cond.strip()
# Negated conditions (label-tpu-remove) are expected to match nothing.
if cond.startswith("-"):
continue
attr = next(
(
a
for a in attrs
if cond.startswith(f"{a}~=") or cond.startswith(f"{a}=")
),
None,
)
if attr is None:
continue
if cond.startswith(f"{attr}~="):
pattern = cond[len(attr) + 2 :]
try:
regex = re.compile(pattern)
except re.error as exc:
dead.append((rule["name"], cond, f"invalid regex: {exc}"))
continue
matched = any(regex.search(f) for f in files)
else:
matched = cond[len(attr) + 1 :] in files
if not matched:
dead.append((rule["name"], cond, "matches no tracked file"))
return dead
def main() -> int:
dead = check(tracked_files())
if not dead:
return 0
print(f"{len(dead)} label condition(s) match nothing:\n", file=sys.stderr)
for name, cond, why in dead:
print(f" [{name}] {cond}\n {why}", file=sys.stderr)
print(
"\nRepoint the condition at the file's new location, or remove it.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())