Skip to content
23 changes: 18 additions & 5 deletions .agents/skills/transformers-upgrade/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,25 @@ For each deleted config:
Deleted processors follow the same steps in
`vllm/transformers_utils/processors/__init__.py`.

Before merging, rebase and grep again for the old field names: PRs that land
in between still read them.

Done when every line of the inventory output is deleted, or kept with a
reason that meets the bar in step 2. The PR also carries the config diff
results and evals for any changed values.

## Upstreaming hazards

Upstream configs are `@strict` dataclasses. Checkpoint keys upstream does not
declare survive as plain attributes. Defaults that only vLLM sets, and values
vLLM derives, vanish.
declare survive as plain attributes, but only when the checkpoint sets them.
Defaults that only vLLM sets, and values vLLM derives, vanish.

| Hazard | Signal | Fix |
| --- | --- | --- |
| Upstream consumes legacy keys | `__post_init__` pops keys the model reads (`deepseek_v4` `compress_ratios`) | Read the upstream replacement (`layer_types`, `mlp_layer_types`, per-layer `rope_parameters`) |
| Renamed or dropped fields | Diff shows `<MISSING>` for a field the model reads | Read the upstream name; derive dropped flags from what implies them (`enable_moe_block` from `num_experts`) |
| Undeclared field without a default | `config.x` raises `AttributeError` for configs that omit `x`, e.g. minimal test configs (A.X-K1 `scoring_func`). Dumps of real checkpoints miss it | Upstream ignoring `x` usually means vLLM can stop reading it; if vLLM truly needs it, read `getattr(config, "x", <old default>)` |
| Top-level config stops forwarding | The vendored top-level config forwarded reads to `text_config`; upstream does not. `getattr(config, "x", default)` silently takes the default, e.g. on MTP draft configs from `SpeculativeConfig.hf_config_override`, which the dumps do not build | Read from `get_text_config()`; grep `getattr` reads of every field, including speculative-decoding paths |
| Type check on the config | `ctx.get_hf_config(SomeConfig)` relies on the vendored base class | Accept the upstream class too, e.g. a `Cosmos3ProcessingInfo` overriding `get_hf_config` |
| Remote code takes over | Checkpoint `config.json` has `auto_map`; with `--trust-remote-code`, the hub class replaces the native one once vLLM stops registering its own | Check the remote class loads; if not, register the upstream class for that `model_type` |
| Read-only upstream fields | Properties or derived fields break subclasses and `hf_overrides` (`nemotron_h` `hybrid_override_pattern`) | Override the source field (`layers_block_type`) instead |
Expand All @@ -159,10 +164,10 @@ Version gates from the inventory need a decision:
- **Patches citing a Transformers issue or PR:**
- Delete the patch when its fix is in the floor release.
- For an issue, find the PR that closed it and check that PR. A closed
issue is not a fixed one: #43329 was closed while the bug was still in
5.16.1.
issue is not a fixed one: issues get closed while the bug is still in
the release.
- Re-run the patched behaviour before deleting. A fix in the release does
not always cover vLLM's path: #47924 made Emu3's processor add BOS, but
not always cover vLLM's path: Emu3's processor gained BOS upstream, but
vLLM tokenizes without the processor.
- Links that only explain a design choice (e.g. a float32 RoPE note) are
not patches; leave them.
Expand All @@ -176,3 +181,11 @@ Version gates from the inventory need a decision:

Add `tests/models/test_initialization.py -k "<archs>"` where a GPU is
available, and a model eval for any model whose resolved config changed.

Models in `vllm/models/` are high priority: whenever their config or model
code changes, run GSM8k (`tests/evals/gsm8k/`) and GPQA Diamond
(`tests/evals/gpt_oss/`) on `main` and the branch, even when the dumps show
no diff, and put both scores in the PR. FlashInfer autotuning picks kernel
tactics afresh on every server start, which moved DeepSeek-V4-Flash GSM8k by
about 1 point; pass `--kernel-config '{"enable_flashinfer_autotune": false}'`
to both servers so greedy outputs are comparable.
3 changes: 2 additions & 1 deletion tests/config/test_model_arch_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ def test_deepseek_v4_convertor_splits_vision_architecture():
text-only DeepSeek-V4; the convertor routes it to the VL wrapper class
by rewriting hf_config.architectures (model-class resolution reads the
raw attribute)."""
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
from transformers import DeepseekV4Config

from vllm.transformers_utils.model_arch_config_convertor import (
DeepseekV4ModelArchConfigConvertor,
)
Expand Down
4 changes: 2 additions & 2 deletions tests/distributed/test_eplb_spec_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import lm_eval
import pytest
import torch
from transformers import DeepseekV4Config

from tests.utils import large_gpu_mark
from vllm.distributed.eplb.eplb_state import EplbState
from vllm.models.deepseek_v4.common.eplb_util import dspark_draft_supports_eplb
from vllm.platforms import current_platform
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
from vllm.v1.worker.gpu.eplb_utils import (
EPLBController,
draft_model_supports_eplb,
Expand Down Expand Up @@ -105,7 +105,7 @@ def _make_dsv4_dspark_hf_config() -> DeepseekV4Config:
vocab_size=256,
n_mtp_layers=2,
enable_confidence_head=False,
compress_ratios=[1, 1],
layer_types=["sliding_attention"] * 2,
)


Expand Down
3 changes: 2 additions & 1 deletion tests/model_executor/layers/test_fused_shared_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def get_fse_test_model_config(
head_dim=128,
max_position_embeddings=128,
sliding_window=None,
compress_ratios=[1],
layer_types=["sliding_attention"],
compress_rates={},
rope_theta=10000.0,
compress_rope_theta=10000.0,
rope_parameters={"rope_type": "default"},
Expand Down
20 changes: 17 additions & 3 deletions tests/models/multimodal/processing/test_deepseek_v4_vl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,27 @@
"""DeepSeek-V4 VL processing helpers."""

from PIL import Image
from transformers import DeepseekV4Config

from vllm.models.deepseek_v4.common.mm_preprocess import (
DeepseekV4VLProcessingInfo,
DeepseekV4VLProcessor,
build_image_block_pad_free,
)
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config

# Vision fields of deepseek-ai/DeepSeek-V4-Flash-Vision-Exp
VISION_CONFIG = dict(
vision_n_layers=24,
vision_dim=1024,
vision_n_heads=16,
vision_inter_dim=2816,
vision_patch_size=14,
vision_rope_theta=10000.0,
vision_downsample_ratio=3,
vision_max_n_token=384,
vision_min_pixels=147456,
vision_max_wh_ratio=8,
)


class _ConfigCtx:
Expand All @@ -34,7 +48,7 @@ def test_get_image_size_with_most_features_beats_wide_real_image():

A square under-counts because each aligner row pays an IMAGE_NEW_LINE.
"""
config = DeepseekV4Config(vision_n_layers=24)
config = DeepseekV4Config(**VISION_CONFIG)
info = DeepseekV4VLProcessingInfo(_ConfigCtx(config))
processor = DeepseekV4VLProcessor(config)

Expand All @@ -47,7 +61,7 @@ def test_get_image_size_with_most_features_beats_wide_real_image():


def test_get_image_size_with_most_features_respects_max_wh_ratio():
config = DeepseekV4Config(vision_n_layers=24)
config = DeepseekV4Config(**VISION_CONFIG)
info = DeepseekV4VLProcessingInfo(_ConfigCtx(config))
dummy = info.get_image_size_with_most_features()
assert dummy.width / dummy.height <= config.vision_max_wh_ratio
4 changes: 2 additions & 2 deletions tests/models/test_deepseek_v4_mega_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest
import torch
from transformers import DeepseekV4Config

from vllm.model_executor.layers.fused_moe.deep_gemm_mega_moe import (
DeepGemmSm100MegaMoEBackend,
Expand All @@ -28,7 +29,6 @@
from vllm.models.deepseek_v41.common.mm_preprocess import IMAGE_SENTINEL_BASE_ID
from vllm.models.deepseek_v41.nvidia.model import DeepseekV4MoE as DeepseekV41MoE
from vllm.platforms import current_platform
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
from vllm.transformers_utils.configs.deepseek_v41 import DeepseekV41Config
from vllm.utils.deep_gemm import is_deep_gemm_supported
from vllm.utils.torch_utils import set_default_torch_dtype
Expand Down Expand Up @@ -979,7 +979,7 @@ def v4_dspark_config(dist_init):
vocab_size=256,
n_mtp_layers=2,
enable_confidence_head=False,
compress_ratios=[1, 1],
layer_types=["sliding_attention"] * 2,
q_lora_rank=32,
o_lora_rank=32,
qk_rope_head_dim=16,
Expand Down
6 changes: 2 additions & 4 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1983,9 +1983,7 @@ def test_draft_inherits_ep_only_for_moe(
draft_is_moe: bool,
):
"""Validate final draft configs without loading weights or mocking validation."""
from transformers import LlamaConfig, MixtralConfig

from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
from transformers import DeepseekV4Config, LlamaConfig, MixtralConfig

common = dict(
hidden_size=128,
Expand All @@ -2002,7 +2000,7 @@ def test_draft_inherits_ep_only_for_moe(
n_routed_experts=4,
num_experts_per_tok=2,
num_nextn_predict_layers=1,
compress_ratios=[1, 1],
layer_types=["sliding_attention"] * 2,
head_dim=32,
**common,
)
Expand Down
19 changes: 19 additions & 0 deletions tests/transformers_utils/test_deepseek_v4_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from transformers import DeepseekV4Config

from vllm.transformers_utils.deepseek_v4_utils import get_compress_ratios

# Legacy field as shipped by deepseek-ai/DeepSeek-V4-Flash-Vision-Exp
COMPRESS_RATIOS = [0, 0] + [4, 128] * 20 + [4, 0, 0, 0]


def test_compress_ratios_survive_upstream_config():
"""Upstream pops `compress_ratios`; the helper must recover it."""
config = DeepseekV4Config(
num_hidden_layers=43,
num_nextn_predict_layers=3,
compress_ratios=COMPRESS_RATIOS,
)
assert not hasattr(config, "compress_ratios")
assert get_compress_ratios(config) == COMPRESS_RATIOS
29 changes: 15 additions & 14 deletions tests/v1/attention/test_deepseek_v4_swa_visible.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import pytest
import torch
from transformers import DeepseekV4Config
from typing_extensions import TypedDict

from tests.v1.attention.utils import create_vllm_config
Expand Down Expand Up @@ -532,26 +533,26 @@ def make_builder(
builder_cls: type[
DeepseekSparseSWAMetadataBuilder
] = DeepseekSparseSWAMetadataBuilder,
mm_prefix_clamp_sliding_window: bool = True,
deepseek_v4_config: bool = True,
bounded_replay: bool = False,
) -> DeepseekSparseSWAMetadataBuilder:
overrides: dict = {"sliding_window": window}
if vision:
# Emulate the V4 vision config, whose mm_prefix_clamp_sliding_window
# gates the in-kernel SWA widening. V4.1 (causal image tokens) leaves
# it off.
overrides.update(
vision_n_layers=2,
vision_max_n_token=max_image_tokens,
mm_prefix_clamp_sliding_window=mm_prefix_clamp_sliding_window,
)
vision_fields = dict(vision_n_layers=2, vision_max_n_token=max_image_tokens)
vllm_config = create_vllm_config(
model_name=model_name,
max_model_len=4096,
max_num_batched_tokens=max_num_batched_tokens,
max_num_seqs=8,
hf_config_override=overrides,
)
hf_config_override={"sliding_window": window}
| (vision_fields if vision and not deepseek_v4_config else {}),
)
if vision and deepseek_v4_config:
# Only V4 configs widen the SWA window in-kernel over image spans
vllm_config.model_config.hf_config = DeepseekV4Config(
num_hidden_layers=1,
layer_types=["sliding_attention"],
sliding_window=window,
**vision_fields,
)
spec = SlidingWindowMLASpec(
block_size=BLOCK_SIZE,
num_kv_heads=1,
Expand Down Expand Up @@ -686,7 +687,7 @@ def test_v41_image_prefill_uses_causal_swa(compress_ratio, query_len):
max_image_tokens,
query_len,
builder_cls=DeepseekV41SparseSWAMetadataBuilder,
mm_prefix_clamp_sliding_window=False,
deepseek_v4_config=False,
)
assert builder.max_image_tokens == 0
md = build_metadata(builder, [seq_len], [query_len], {0: spans})
Expand Down
3 changes: 2 additions & 1 deletion vllm/model_executor/layers/attention/mla_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
)
from vllm.model_executor.utils import replace_parameter
from vllm.platforms import current_platform
from vllm.transformers_utils.deepseek_v4_utils import get_compress_ratios
from vllm.utils.flashinfer import has_flashinfer
from vllm.utils.math_utils import cdiv, round_down, round_up
from vllm.utils.torch_utils import (
Expand Down Expand Up @@ -1740,7 +1741,7 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
hf_text_config = model_config.hf_text_config

# Check if this is a DeepseekV4 config (uses unified head_dim + rope_head_dim)
if hasattr(hf_text_config, "compress_ratios"):
if get_compress_ratios(hf_text_config):
# DeepseekV4 style config: unified head_dim with rope_head_dim
head_dim = hf_text_config.head_dim
rope_head_dim = hf_text_config.qk_rope_head_dim
Expand Down
5 changes: 4 additions & 1 deletion vllm/models/deepseek_v4/amd/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,10 @@ def __init__(
self.image_sentinel_lo = (
IMAGE_SENTINEL_BASE_ID if getattr(config, "vision_n_layers", 0) > 0 else 0
)
is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
num_hash_layers = getattr(config, "num_hash_layers", None) # Set by V4.1
if num_hash_layers is None:
num_hash_layers = config.mlp_layer_types.count("hash_moe")
is_hash_moe = extract_layer_index(prefix) < num_hash_layers
self.hash_indices_dtype = torch.int32
if is_hash_moe:
# hash MoE doesn't use e_score_correction_bias
Expand Down
3 changes: 2 additions & 1 deletion vllm/models/deepseek_v4/attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ def __init__(
# we do this for because MTP layer is not included
# in the compress ratio list
if layer_id < config.num_hidden_layers:
self.compress_ratio = max(1, config.compress_ratios[layer_id])
layer_type = config.layer_types[layer_id]
self.compress_ratio = max(1, config.compress_rates.get(layer_type, 0))
else:
self.compress_ratio = 1
self.eps = config.rms_norm_eps
Expand Down
3 changes: 1 addition & 2 deletions vllm/models/deepseek_v4/common/mm_preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import numpy as np
import torch
from PIL import Image, ImageOps
from transformers import BatchFeature
from transformers import BatchFeature, DeepseekV4Config

from vllm.config.multimodal import MultiModalDummyOptions
from vllm.inputs import MultiModalDataDict
Expand All @@ -48,7 +48,6 @@
UpdateMode,
_plan_prompt_updates,
)
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config

IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5)
COMPRESS_PAD_TO = 4
Expand Down
11 changes: 7 additions & 4 deletions vllm/models/deepseek_v4/common/ops/cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
triton_scalar_specialization_rep,
)
from vllm.platforms import current_platform
from vllm.transformers_utils.deepseek_v4_utils import get_compress_ratios
from vllm.triton_utils import tl, triton
from vllm.utils.import_utils import has_cutedsl
from vllm.utils.math_utils import next_power_of_2
Expand Down Expand Up @@ -393,7 +394,9 @@ def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:

compress_ratios = frozenset(
max(1, int(compress_ratio))
for compress_ratio in vllm_config.model_config.hf_config.compress_ratios
for compress_ratio in get_compress_ratios(
vllm_config.model_config.hf_config
)
)
return self._trace_dispatch(self.dispatch)(
zip_inputs(
Expand Down Expand Up @@ -658,7 +661,7 @@ def dispatch( # type: ignore[override]

def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
index_topk = vllm_config.model_config.hf_config.index_topk
compress_ratios = vllm_config.model_config.hf_config.compress_ratios
compress_ratios = get_compress_ratios(vllm_config.model_config.hf_config)
cache_block_size = vllm_config.cache_config.block_size
if index_topk <= 0 or cache_block_size <= 0:
return []
Expand Down Expand Up @@ -964,7 +967,7 @@ def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
hf_config = vllm_config.model_config.hf_config
index_topk = int(getattr(hf_config, "index_topk", 0) or 0)
compress_ratios = tuple(
sorted({max(1, int(ratio)) for ratio in hf_config.compress_ratios})
sorted({max(1, int(ratio)) for ratio in get_compress_ratios(hf_config)})
)
max_c128a_topk = (
((vllm_config.model_config.max_model_len + 127) // 128 + 127) // 128 * 128
Expand Down Expand Up @@ -1592,7 +1595,7 @@ def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
image_widths = (0, max_image_tokens) if max_image_tokens > 0 else 0
index_topk = int(getattr(hf_config, "index_topk", 0) or 0)
compress_ratios = tuple(
sorted({max(1, int(ratio)) for ratio in hf_config.compress_ratios})
sorted({max(1, int(ratio)) for ratio in get_compress_ratios(hf_config)})
)
max_c128a_topk = (
((vllm_config.model_config.max_model_len + 127) // 128 + 127) // 128 * 128
Expand Down
5 changes: 5 additions & 0 deletions vllm/models/deepseek_v4/common/vl_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from vllm.multimodal.inputs import MultiModalKwargsItem

from .mm_preprocess import (
COMPRESS_PAD_TO,
IMAGE_PLACEHOLDER,
IMAGE_SENTINEL_BASE_ID,
DeepseekV4VLDummyInputsBuilder,
Expand Down Expand Up @@ -105,6 +106,10 @@ class DeepseekV4ForConditionalGeneration(
# (borrowed reserved ids, see common/mm_preprocess.py) and apply bias_vl.
requires_raw_input_tokens = True
supports_tower_connector_lora = True
# Image spans widen the sparse-SWA window in-kernel and the placeholder
# starts with a compressor-alignment pad
mm_prefix_clamp_sliding_window = True
mm_prefix_span_leading_pad_modulus = COMPRESS_PAD_TO

@classmethod
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
Expand Down
4 changes: 3 additions & 1 deletion vllm/models/deepseek_v4/cpu/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ def __init__(

self.gate.e_score_correction_bias = None
self.gate.tid2eid = None
is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
is_hash_moe = extract_layer_index(prefix) < config.mlp_layer_types.count(
"hash_moe"
)
self.hash_indices_dtype = torch.int32
if is_hash_moe:
# hash MoE doesn't use e_score_correction_bias
Expand Down
Loading
Loading