Prhub

#41277 Fix error in Dynamic NTK scaling

原始 PR 作者 maxdebayser 合并时间 2026-05-20 05:27 文件变更 4 提交数 10 评论 11 代码增减 +45 / -152

执行摘要

修复 Dynamic NTK RoPE 缩放公式为变量而非常量

Issue #41236 指出 Dynamic NTK 缩放公式错误:max_len = self.max_position_embeddings * self.scaling_factor 代入后变成常量,失去了长度依赖。PR body 引用社区公式 (α * current length / original length) - (α - 1),要求根据实际序列长度动态缩放。此外,Nomic 模型需要正确支持从训练长度向 n_positions 的扩展(如 2048→8192)。

建议合并。核心贡献是修正了一个长期存在的公式错误,并使 Nomic 模型的默认行为与社区对齐。推荐阅读 dynamic_ntk_scaling_rope.py 中的核心公式修正和 config.py 中简化后的配置逻辑。开发者在升级后应检查自家 Nomic 模型是否通过 rope_parameters 自定义了缩放参数。

讨论亮点
  1. 默认值安全性:gemini-code-assist[bot] 提出 __init__.pymax_trained_positions 默认取 max_position 会导致缩放比率为 1.0,对未显式提供该参数的模型禁用动态缩放,可能构成回归。作者 maxdebayser 回应这是有意为之,因为如果没有上下文扩展(序列长度不大于训练长度),不需要动态缩放,且该默认不会破坏原有行为。最终未修改默认值。
  2. None 解包风险:gemini-code-assist[bot] 指出 config.rope_parameters 可能为 None,导致 ** 解包 TypeError。该建议被采纳,最终代码改为 (config.rope_parameters or {})
  3. 对齐验证:noooop 请求核心维护者 double-check 修改,并建议更新模型代码版本以对齐最新 transformers。同时 ieBoytsov 建议增加 embedding 相似度测试,确认与 sentence-transformers 一致。最终 mgoin 表示认可修改并批准合并。

实现拆解

  1. 修正核心公式:修改 dynamic_ntk_scaling_rope.py 中的 _compute_cos_sin_cache 方法,删除错误的最大长度 max_len,直接使用 self.max_position_embeddings 作为分母,用 max_trained_positions 参数替代原来的常量缩放分子,使 base 计算依赖 self.max_position_embeddings / self.max_trained_positions,变量实际来自 self.max_position_embeddings
  2. 扩展参数传递:在 __init__.pyget_rope 构建函数中,从 rope_parameters 读取 max_trained_positions(默认值为 max_position),并传递给 DynamicNTKScalingRotaryEmbedding 构造函数,同时添加 max_trained_positions 属性。
  3. 简化配置逻辑:在 config.pyNomicBertModel 配置类中,删除原有复杂的上下文扩展分支(约70行替换为10行),不再手动修改 max_model_len 限制到训练长度,而是直接设置 rotary_kwargs 中的 max_positionmodel_config.max_model_len,并将 max_trained_positions 注入 rope_parameters,同时修复 config.rope_parameters 可能为 None 的潜在异常。
  4. 更新测试覆盖:调整 test_nomic_max_model_len.py,删除 test_set_max_model_len_illegaltest_use_rope_scaling_illegal 测试(因新逻辑不再主动限制长度),修改 test_default 断言 nomic-embed-text-v1 默认 max_model_len 为 8192,并扩展 test_set_max_model_len_legal 支持设置超过 2048 的长度(如 4096)。
文件 模块 状态 重要度
vllm/model_executor/layers/rotary_embedding/dynamic_ntk_scaling_rope.py 旋转嵌入 modified 6.75
vllm/model_executor/models/config.py 配置处理 modified 7.57
vllm/model_executor/layers/rotary_embedding/__init__.py 旋转嵌入 modified 5.68
tests/models/language/pooling/test_nomic_max_model_len.py 测试 modified 6.28

关键符号

DynamicNTKScalingRotaryEmbedding.__init__ DynamicNTKScalingRotaryEmbedding._compute_cos_sin_cache NomicBertModel.verify_and_update_model_config get_rope

关键源码片段

vllm/model_executor/layers/rotary_embedding/dynamic_ntk_scaling_rope.py core-logic

核心修复所在:修改 `_compute_cos_sin_cache` 方法,修正 base 计算公式,引入 `max_trained_positions` 参数。

# vllm/model_executor/layers/rotary_embedding/dynamic_ntk_scaling_rope.pyclass DynamicNTKScalingRotaryEmbedding(RotaryEmbedding):
    """RotaryEmbedding extended with Dynamic NTK scaling."""
​
    def __init__(
        self,
        head_size: int,
        rotary_dim: int,
        max_position_embeddings: int,
        max_trained_positions: int, # <-- 新增参数:模型训练的原始长度
        base: float,
        is_neox_style: bool,
        scaling_factor: float,
        dtype: torch.dtype,
    ) -> None:
        self.scaling_factor = scaling_factor
        self.max_trained_positions = max_trained_positions # 保存训练长度
        super().__init__(
            head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype
        )
​
    def _compute_cos_sin_cache(self) -> torch.Tensor:
        # 正确公式:base' = base * (( α * cur_len / trained_len ) - (α - 1))^(dim/(dim-2))
        # 其中 cur_len = self.max_position_embeddings # 当前推理长度
        # 之前的错误:使用了 max_len = cur_len * α,导致公式变成常量
        base = self.base * (
            (
                self.scaling_factor
                * self.max_position_embeddings
                / self.max_trained_positions
            )
            - (self.scaling_factor - 1)
        ) ** (self.rotary_dim / (self.rotary_dim - 2))
        inv_freq = self._compute_inv_freq(base)
        # 注意:t 的范围基于当前 max_position_embeddings,而非缩放后的长度
        t = torch.arange(self.max_position_embeddings, dtype=torch.float)
        freqs = torch.einsum("i,j -> ij", t, inv_freq)
        cos = freqs.cos()
        sin = freqs.sin()
        cache = torch.cat((cos, sin), dim=-1)
        return cache
vllm/model_executor/models/config.py data-contract

重构 Nomic 模型配置,简化 context extension 逻辑,移除约 70 行条件分支,改为直接传递 max_trained_positions 和 max_model_len。

# vllm/model_executor/models/config.py
# NomicBertModel 的 verify_and_update_model_config 新实现(关键片段)@staticmethod
def verify_and_update_model_config(model_config: "ModelConfig") -> None:
    config = model_config.hf_config
    # ... 前面的断言和属性映射省略 ...
​
    head_dim = config.hidden_size // config.num_attention_heads
    # 优先取 max_position_embeddings,兜底 2048
    max_position_embeddings = getattr(config, "max_position_embeddings", 2048)
    max_trained_positions = getattr(
        config, "max_trained_positions", max_position_embeddings
    )
​
    # 将 max_trained_positions 注入 rope_parameters,供后续构建使用
    rope_parameters = {
        "max_trained_positions": max_trained_positions,
        **(config.rope_parameters or {}), # 安全解包,防止 None
    }
​
    # rotary_kwargs 直接使用 model_config.max_model_len(用户可自由设置)
    config.rotary_kwargs = {
        "head_size": head_dim,
        "max_position": model_config.max_model_len,
        "rope_parameters": rope_parameters,
    }
    # 不再覆盖 max_model_len,删除警告,简化逻辑

评论区精华

默认 max_trained_positions 为 max_position 的影响 正确性

gemini-code-assist[bot] 指出默认值会导致缩放比率 1.0,对未显式提供 max_trained_positions 的模型可能禁用 NTK 缩放。作者回复这是有意的,没有扩展时无需缩放。

结论:保持默认值不变,无修改。 · 已解决

rope_parameters 可能为 None 导致 TypeError 正确性

gemini-code-assist[bot] 指出在 config.py 中解包 config.rope_parameters 可能为 None,应加空字典保护。

结论:建议被采纳,最终使用 `(config.rope_parameters or {})`。 · 已解决

请求核心维护者 double-check 公式修改 设计

noooop 在 dynamic_ntk_scaling_rope.py 评论上 @tjtanaa, WoosukKwon, mgoin, Isotr0py,请求确认修改正确性。

结论:mgoin 表示希望合入,认为改动合理。 · 已解决

增加 embedding 数值比对测试 测试

ieBoytsov 建议添加与 sentence-transformers 直接比对 embedding 的端到端测试,以验证修复效果。

结论:未在本次 PR 中实现,但存在其他测试(如 test_embeddings)可能部分覆盖,未被强制要求。 · unresolved

风险与影响

  1. 兼容性风险:未显式设置 max_trained_positions 的模型默认使用 max_position,缩放比率为 1.0,这符合没有上下文扩展的场景,不会引起数值错误。但对于已通过自定义 rope_parameters 依赖原错误的模型,升级后行为可能发生非兼容变化。需要用户确认配置。
  2. 测试覆盖不足:虽然测试已更新,但未增加直接验证 embedding 数值一致性的端到端测试(如与 sentence-transformers 比较),仅验证了 max_model_len 配置。潜在的实际推理输出差异可能未被捕获。
  3. 简化影响:config.py 中删除了大量上下文扩展的警告和条件逻辑,对依赖原有 max_model_len 自动裁剪的用户可能带来意外改变。但新逻辑更简单透明,通过配置明确启用扩展。

影响范围:直接修正 Nomic 系列 Embedding 模型(如 nomic-embed-text-v1, v1.5, CodeRankEmbed, Snowflake/snowflake-arctic-embed-m-long)的动态 NTK 缩放行为,使 vLLM 可与 sentence-transformers 对齐。影响程度:中等。对使用这些模型且需要上下文扩展的用户是 bugfix;其他用户无影响。团队影响:简化了 config.py 中历史遗留逻辑,降低维护成本。

配置行为变化 核心路径变更 缺少端到端数值验证

关联 Issue

#41236 [Bug]: Dynamic NTK RoPE scaling is wrong

完整报告

参与讨论