Prhub

#49343 [BugFix] eagle draft max position embeddings

原始 PR 作者 JaredforReal 合并时间 2026-07-29 16:24 文件变更 4 提交数 10 评论 4 代码增减 +288 / -4

执行摘要

修复 EAGLE draft 位置编码越界与 proposer block_size 不对齐

用户使用 Llama-3.1-8B-Instruct 作为目标模型、yuhuili/EAGLE3-LLaMA3.1-Instruct-8B 作为 draft 模型时,draft 的 max_position_embeddings 为 2048,而目标 max_model_len 为 8192,导致 rotary cache 越界。在 torch.compile 模式下触发设备端断言失败(CUDA error: device-side assert triggered),在 eager 模式下静默读取垃圾值(但被 rejection sampling 掩盖)。因此需要将 draft 的 max_position_embeddings 提升至 target 的 max_model_len。

建议所有参与 speculative decoding 开发的同学精读本 PR。实现思路清晰:配置层修复 draft 模型不对齐问题,运行时层修复 block_size 和确定性,测试覆盖充分,为类似 bug 的修复提供了模板。

讨论亮点

类型注解问题:Copilot 指出 attention_groups 字典的键类型标注为 dict[tuple[str, str], AttentionGroup] 但实际使用字符串作为键,存在类型不匹配。该问题未在 PR 中进一步处理,但可能不影响运行时正确性。

Docstring 准确性争议:Copilot 质疑测试文档声称 eager 模式下产生“垃圾读取”可能不准确,因为 issue 报告 eager 模式输出正常。贡献者 manojarulmurugan 提供详细实验数据证明:在 eager 模式下,超出 2047 位置的回调确实返回错误值(误差约 3e19),但最终推断结果被 rejection sampling 掩盖,因此 docstring 描述正确。结论是保留原始文档。

实现拆解

  1. 新增配置层 override 逻辑:在 vllm/config/speculative.py 中添加静态方法 _maybe_override_draft_max_position_embeddings,当 draft 的 max_position_embeddings 不存在或已大于等于 target 的 max_model_len 时跳过;否则将其提升至 target 值并记录一条 INFO 日志。在 SpeculativeConfig.__post_init__ 中对 eagle 和 eagle3 方法(在替换 hf_config 为 EAGLEConfig 之前)调用此方法。

  2. 修复 proposer block_size 来源及确定性:在 vllm/v1/spec_decode/llm_base_proposer.py 的 initialize_attn_backend 中,将 _draft_attn_layer_names 的迭代改为 sorted() 以保证注意力组构建顺序跨进程确定;同时将 self.block_size 的赋值改为优先从 kernel_block_sizes 获取对应组的 kernel_block_size,仅在缺失时回退到 kv_cache_spec.block_size。此修正确保 slot-mapping 计算使用的块大小与 kernel 级 block table 一致。

  3. 测试覆盖:新增 tests/config/test_speculative_draft_max_position_embeddings.py,通过五个用例(小值覆盖、足够值不覆盖、缺失属性、EAGLE 继承、独立 draft 保持自身 limit)验证 override 行为及日志;新增 tests/v1/spec_decode/test_llm_base_proposer.py,通过三个用例验证 proposer 的 block_size 选择、回退行为以及注意力组确定性。

  4. 日志捕获辅助:测试中使用 vllm_caplog fixture 开启 vLLM 日志传播,以便断言 _maybe_override_draft_max_position_embeddings 的日志记录。

文件 模块 状态 重要度
vllm/config/speculative.py 配置层 modified 7.36
vllm/v1/spec_decode/llm_base_proposer.py 推测解码器 modified 6.46
tests/config/test_speculative_draft_max_position_embeddings.py 测试配置 added 7.5
tests/v1/spec_decode/test_llm_base_proposer.py 测试推测解码 added 7.2

关键符号

_maybe_override_draft_max_position_embeddings initialize_attn_backend _override_logged test_override_raises_smaller_value test_override_keeps_sufficient_value test_override_ignores_missing_attribute test_eagle_draft_inherits_target_max_model_len test_independent_draft_model_keeps_its_own_limit test_block_size_uses_kernel_block_size test_block_size_falls_back_to_kv_cache_spec test_draft_layer_iteration_is_deterministic

关键源码片段

vllm/config/speculative.py core-logic

核心 bug 修复:新增 override 方法,在配置初始化时将 draft 的 max_position_embeddings 提升至 target 的 max_model_len,仅用于 eagle/eagle3 方法。

# vllm/config/speculative.py (line 913-950, head)
# __post_init__ 中插入 override 调用
if self.method in ("eagle", "eagle3"):
    # EAGLE drafts share the target's positional space; a
    # draft checkpoint with a smaller max_position_embeddings
    # than the target under-sizes its rotary cache (#48894).
    SpeculativeConfig._maybe_override_draft_max_position_embeddings(
        self.draft_model_config.hf_config,
        self.target_model_config.max_model_len,
    )
​
@staticmethod
def _maybe_override_draft_max_position_embeddings(
    draft_hf_config: PretrainedConfig,
    target_max_model_len: int,
) -> None:
    """Raise an EAGLE draft's max_position_embeddings up to the target's.
​
    The proposer feeds the draft positions up to the target's
    max_model_len, while max_position_embeddings sizes the draft's
    rotary cos_sin_cache. A smaller checkpoint value (e.g. 2048 for
    yuhuili/EAGLE3-LLaMA3.1-Instruct-8B) makes that cache gather go
    out of bounds (#48894).
​
    Args:
        draft_hf_config: The draft model's HF config, mutated in place.
        target_max_model_len: The target model's max_model_len.
    """
    draft_max_position_embeddings = getattr(
        draft_hf_config, "max_position_embeddings", None
    )
    if (
        draft_max_position_embeddings is None
        or draft_max_position_embeddings >= target_max_model_len
    ):
        return
    logger.info(
        "Overriding draft model max_position_embeddings from %d to the "
        "target model's max_model_len (%d); EAGLE drafts share the "
        "target's positional space.",
        draft_max_position_embeddings,
        target_max_model_len,
    )
    draft_hf_config.max_position_embeddings = target_max_model_len
vllm/v1/spec_decode/llm_base_proposer.py core-logic

修复 proposer 初始化中的 block_size 来源和确定性:使用 kernel block size 并排序迭代。

# vllm/v1/spec_decode/llm_base_proposer.py (line 1741-1792, head)
def initialize_attn_backend(
    self,
    kv_cache_config: KVCacheConfig,
    kernel_block_sizes: list[int] | None = None,
) -> None:
    # ... 前面代码找到 kv_cache_spec ...
    attention_groups: dict[tuple[str, str], AttentionGroup] = {}
    if kv_cache_spec is not None:
        # _draft_attn_layer_names is a set; iterate in sorted order so
        # that attention_groups (and anything derived from its first
        # element) is deterministic across processes.
        for layer_name in sorted(self._draft_attn_layer_names):
            attn_backend = all_attn_layers[layer_name].get_attn_backend()
            backend_key = attn_backend.full_cls_name()
            if backend_key not in attention_groups:
                # ... 创建 AttentionGroup ...
                attn_group.create_metadata_builders(
                    self.vllm_config,
                    self.device,
                    kernel_block_size=kernel_block_size,
                )
                attention_groups[backend_key] = attn_group
            else:
                attention_groups[backend_key].layer_names.append(layer_name)
​
    self.draft_attn_groups = list(attention_groups.values())
    if kernel_block_sizes is not None and 0 <= self.kv_cache_gid < len(
        kernel_block_sizes
    ):
        # Slot mappings are computed against the block table, which is
        # stored at kernel-block granularity. Use the kernel block size
        # rather than the KV cache manager's block size; the two differ
        # when manager blocks are split for the attention kernel.
        self.block_size = kernel_block_sizes[self.kv_cache_gid]
    else:
        self.block_size = (
            self.draft_attn_groups[0]
            .get_metadata_builder()
            .kv_cache_spec.block_size
        )
    logger.debug("Using block size %d for drafting layers", self.block_size)

评论区精华

类型注解不匹配 style

Copilot 指出 attention_groups 字典的键类型标注为 dict[tuple[str,str], AttentionGroup],但实际代码使用字符串 backend_key 作为键。

结论:未在 PR 中进一步处理,PR 已合并。该类型问题不影响运行时,但可能影响静态类型检查。 · unresolved

Docstring 中 eager 模式下垃圾读的准确性 正确性

Copilot 质疑 docstring 声称 eager 模式有垃圾读取,因为 issue 报告 eager 模式输出正常。manojarulmurugan 提供实验数据:在 A100 上测试发现 eager 模式确实存在 OOB 读取,cos/sin 值从 position 2048 起错误,但最终输出被 rejection sampling 掩盖。

结论:docstring 描述准确,保留原始文本。 · 已解决

风险与影响

  1. override 范围限制:当前 override 仅针对 eagle 和 eagle3,未来其他 speculative 方法(如 dflash)也可能遇到相似问题,需开发者留意。
  2. block_size 依赖:kernel_block_sizes 参数由外部传入,若调用方未提供正确值可能导致回退到 scheduler block size,引入隐蔽的 slot-mapping 错误。测试覆盖了回退场景,但仍需调用链保证。
  3. 类型注解未修复:attention_groups 的键类型注解与实际使用不符,可能触发类型检查器误报,影响开发体验。
  4. 确定性排序范围:initialize_attn_backend 中仅对迭代进行了排序,但其他引用 _draft_attn_layer_names 的代码可能仍受集合顺序影响,需全局检查。

用户影响:所有使用 EAGLE/EAGLE3 推测解码且目标模型上下文长度大于 2048 的用户将避免崩溃或错误。系统影响:修改了配置初始化和 proposer 初始化流程,不影响未使用 speculative decoding 的现有用户。团队影响:新增的测试套件提高了代码健壮性,但需维护。

类型注解不匹配 override 仅覆盖 eagle/eagle3 block_size 依赖外部参数 确定性仅修复部分路径

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论