Prhub

#42538 [ModelRunner V2] Share identical MTP weights

原始 PR 作者 njhill 合并时间 2026-05-14 02:57 文件变更 1 提交数 2 评论 4 代码增减 +54 / -36

执行摘要

共享 MTP 相同权重,减少显存占用

V1 ModelRunner 已经实现了共享相同 MTP 权重,但 V2 没有覆盖。PR body 明确指出:"This is already done in V1 but wasn't covered in V2. - Dedup identical layer weights and topk_indices_buffer - Skip in PP case",且 CI 中一个 eagle 测试因 GPU 内存紧张而失败。

建议精读此 PR,尤其是 _should_share 函数的实现和权重共享的完整流程。该设计体现了“资源不足时 fallback”和“避免重复计算”的优良风格,值得学习。同时也应关注 PP 场景下的潜在风险,建议作者在后续 PR 中补充更多测试并回应 review 中的疑虑。

讨论亮点
  • gemini-code-assist[bot] 指出 _should_share 中当 draft is None 且 flag 为 True 时返回 False 与文档字符串矛盾,应返回 True 以允许从 target 赋值;同时建议优先在 GPU 上比较权重以减少 CPU 传输开销。但作者最终实现了“显存不足时 fallback 到 CPU”的策略。
  • gemini-code-assist[bot] 还指出 PP 检查仅用于 embedding 共享,但 lm_head 共享同样需要 PP 保护。作者未在 PR 中回应这一建议(可能因 lm_head 在 PP 下每个 rank 存在性不同但共享逻辑不影响)。
  • yewentao256 询问 .weight 是否存在,作者确认“afaik yes”。

实现拆解

  1. 引入 _should_share 工具函数:在 vllm/v1/worker/gpu/spec_decode/eagle/utils.py 中新增,根据 draft 模型是否拥有独立副本以及其与 target 权重是否相等来决定是否共享。该函数内部使用 torch.equal 比较,并在 GPU 显存紧张时自动将张量移至 CPU 比较以减少显存开销。

  2. 重构权重重快逻辑:将原来分散的 share_embeddings / share_lm_head 逻辑替换为统一的 _should_share 调用。对 embedding 和 lm_head 分别检查 draft 是否拥有独立副本及权重是否与 target 相同,若相同则删除 draft 中的参数并替换为 target 的引用。

  3. 添加 PP 保护:通过 get_pp_group().world_size == 1 判断只在非 PP 环境下共享 embedding,避免在 PP 切分下各 rank 错误共享不属于自己的部分。

  4. 新增 topk_indices_buffer 共享:对于 MTP 模型,目标模型的 topk_indices_buffer 也被共享给 draft 模型。

  5. 整理代码结构:提取公共变量 target_language_modeltarget_innerdraft_inner,使代码更清晰。

文件 模块 状态 重要度
vllm/v1/worker/gpu/spec_decode/eagle/utils.py 推测解码 modified 7.62

关键符号

_should_share load_eagle_model

关键源码片段

vllm/v1/worker/gpu/spec_decode/eagle/utils.py core-logic

核心变更文件:重写了权重共享逻辑,新增 `_should_share` 函数,并添加 `topk_indices_buffer` 共享。

# vllm/v1/worker/gpu/spec_decode/eagle/utils.pyimport torch
import torch.nn as nnfrom vllm.config import VllmConfig
from vllm.distributed.parallel_state import get_pp_group
from vllm.model_executor.model_loader import get_model
​
​
def _should_share(eagle: nn.Module, flag: str, draft, target) -> bool:
    """Share when the draft has no own copy, or its copy matches the target."""
​
    # If the model doesn't have the flag (e.g. 'has_own_embed_tokens') or
    # there is no draft param, always share from target (avoid uninit).
    if not getattr(eagle, flag, False) or draft is None:
        return True
​
    if target is None:
        return False
​
    # torch.equal on GPU allocates a bool mask the size of the input.
    # Use the faster GPU path when there is plenty of headroom;
    # otherwise compare on CPU to avoid OOM.
    w = draft.weight
    if w.is_cuda and torch.cuda.mem_get_info(w.device)[0] < w.numel() * 2:
        return torch.equal(w.cpu(), target.weight.cpu())
    return torch.equal(w, target.weight)
​
​
def load_eagle_model(target_model: nn.Module, vllm_config: VllmConfig) -> nn.Module:
    # ... (setup and model creation) ...
    target_language_model = (
        target_model.get_language_model()
        if hasattr(target_model, "get_language_model")
        else target_model
    )
    target_inner = target_language_model.model
    draft_inner = eagle_model.model
​
    # Skip embedding sharing under PP — each rank owns its own embedding.
    if get_pp_group().world_size == 1:
        target_embed = getattr(target_inner, "embed_tokens", None) or getattr(
            target_inner, "embedding", None
        )
        draft_embed = getattr(draft_inner, "embed_tokens", None)
        if target_embed is not None and _should_share(
            eagle_model, "has_own_embed_tokens", draft_embed, target_embed
        ):
            if draft_embed is not None:
                del draft_inner.embed_tokens
            draft_inner.embed_tokens = target_embed
​
    target_lm_head = getattr(target_model, "lm_head", None)
    draft_lm_head = getattr(eagle_model, "lm_head", None)
    if target_lm_head is not None and _should_share(
        eagle_model, "has_own_lm_head", draft_lm_head, target_lm_head
    ):
        if draft_lm_head is not None:
            del eagle_model.lm_head
        eagle_model.lm_head = target_lm_head
​
        # Fix per-layer lm_head in MTP layers
        layers = getattr(draft_inner, "layers", None)
        if layers is not None:
            items = layers.values() if isinstance(layers, nn.ModuleDict) else layers
            for layer in items:
                sh = getattr(layer, "shared_head", None)
                if sh is not None and hasattr(sh, "head"):
                    del sh.head
                    sh.head = target_lm_head
​
    # Share topk_indices_buffer for MTP models
    if hasattr(target_inner, "topk_indices_buffer"):
        if hasattr(draft_inner, "topk_indices_buffer"):
            del draft_inner.topk_indices_buffer
        draft_inner.topk_indices_buffer = target_inner.topk_indices_buffer
​
    return eagle_model

评论区精华

_should_share 逻辑矛盾 正确性

当 draft 为 None 且 flag 为 True 时,代码返回 False,但文档字符串说“Share when the draft has no own copy”,应返回 True。

结论:未明确处理,当前代码保留返回 False 的行为(当 flag=True 且 draft=None),但实际调用场景中 draft 通常不会为 None(因为 draft 已有权重),风险较低。 · 已解决

权重比较的 GPU/CPU 选择 性能

建议在 GPU 上比较以减少 CPU 传输开销,但当前实现在 GPU 显存紧张时 fallback 到 CPU。

结论:接受当前策略:GPU 有空闲时用 GPU,否则 fallback 到 CPU。 · 已解决

lm_head 共享缺少 PP 保护 设计

gemini-code-assist 指出 embedding 共享有 PP 检查,但 lm_head 共享没有。

结论:未回应,但实际调用中 `target_model.lm_head` 在 PP 下可能为 None 从而跳过,风险有限。 · unresolved

.weight 属性是否存在 question

yewentao256 询问 `.weight` 是否总是存在,作者确认。

结论:确认存在,适用所有 Linear 层。 · 已解决

风险与影响

  1. 回归风险:重构了权重共享逻辑,若前 draft 模型有特殊参数(如 has_own_embed_tokens 但权重恰好等于 target),可能错误地删除自有权重。_should_share 中对 flag 为 False 或 draft 为 None 时直接返回 True 可能导致意料之外共享。
  2. PP 兼容性:lm_head 共享未加 PP 检查,在 PP 场景下某些 rank 可能没有 lm_head,但 getattr(target_model, "lm_head", None) 会返回 None,从而跳过共享,实际影响有限。
  3. 性能影响:比较权重时可能引入 GPU 显存临时分配(torch.equal 分配 bool mask),已通过 fallback 到 CPU 缓解,但仍需关注对大型权重的比较开销。
  4. 缺少测试覆盖:本次变更未包含测试文件,若后续 CI 未覆盖相关场景,可能引入隐蔽 bug。

用户影响:对使用 EAGLE 推测解码的用户,V2 ModelRunner 将自动共享相同 MTP 权重,减少显存占用,可能使一些原本 OOM 的场景(如 eagle 测试)得以运行。系统影响:仅影响 vllm/v1/worker/gpu/spec_decode/eagle/utils.py 一个文件。团队影响:统一 V1/V2 行为,降低维护成本。

缺少测试覆盖 核心路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论