大模型-vllm 自投机解码可行性分析

外部 Drafter 投机解码验证方案技术文档

1. 需求分析

1.1 用户需求描述

用户希望实现一种"外部 Drafter + 内部 Target 验证"的投机解码变体方案:

  1. 客户端运行独立的推理模型(例如 ASR drafter),生成草稿 token

  2. 将草稿 token 发送到 vLLM 服务端

  3. vLLM 服务端使用目标模型(Qwen3NioAsrModel)对草稿 token 进行前向验证

  4. 通过 rejection sampling 接受或拒绝草稿 token

1.2 技术本质

这本质上是将投机解码的 drafter 推理 外置到客户端,而在 vLLM 服务端只保留 target 验证 功能。

复制代码
传统投机解码:
┌─────────────────────────────────────────────────────┐
│                    vLLM Server                       │
│  ┌────────────┐    ┌─────────────┐    ┌──────────┐  │
│  │   Drafter  │───▶│   Target    │───▶│ Sampler  │  │
│  │  (Proposer)│    │ (Verifier)  │    │(Rejection)│  │
│  └────────────┘    └─────────────┘    └──────────┘  │
└─────────────────────────────────────────────────────┘
​
外部 Drafter 方案:
┌─────────────────┐         ┌───────────────────────────────────┐
│    Client       │         │          vLLM Server               │
│  ┌───────────┐  │ draft   │  ┌─────────────┐    ┌──────────┐  │
│  │ External  │──┼─tokens──┼─▶│   Target    │───▶│ Sampler  │  │
│  │  Drafter  │  │         │  │ (Verifier)  │    │(Rejection)│  │
│  └───────────┘  │         │  └─────────────┘    └──────────┘  │
└─────────────────┘         └───────────────────────────────────┘

2. 用户模型分析:qwen3_audio_asr.py

2.1 模型架构

复制代码
@MULTIMODAL_REGISTRY.register_processor(...)
class Qwen3NioAsrModel(nn.Module, SupportsLoRA, SupportsPP, SupportsEagle3, SupportsMultiModal):
    """
    基于 Qwen3 的 ASR (Automatic Speech Recognition) 模型
    
    关键组件:
    - audio_adaptor: Qwen3AsrMultiModelProjector (音频特征投影)
    - model: Qwen3Model (Qwen3 解码器)
    - lm_head: ParallelLMHead (语言模型头)
    - logits_processor: LogitsProcessor
    """

2.2 核心组件

组件 作用 代码位置
Qwen3AsrMultiModelProjector 音频 embedding 下采样投影 (encoder_dim=1280 → llm_dim=2048) Line 106-130
Qwen3Model Qwen3 Transformer 解码器,处理文本生成 Line 556
get_multimodal_embeddings() 处理音频输入,返回投影后的 embeddings Line 671
forward() 模型前向传播,合并音频和文本 embeddings Line 687
compute_logits() 计算输出 logits Line 706

2.3 模型接口支持

复制代码
class Qwen3NioAsrModel(..., SupportsEagle3, ...):
    """
    注意:模型已声明支持 SupportsEagle3 接口
    这意味着模型架构上已经兼容投机解码的验证逻辑
    """

3. 可行性分析

3.1 技术可行性评估

维度 评估 说明
架构兼容性 ✅ 可行 模型支持 SupportsEagle3,验证逻辑可复用
数据格式 ⚠️ 需扩展 当前 API 不支持传递 draft tokens
验证链路 ✅ 存在 RejectionSampler 和验证逻辑完整
改动范围 ⚠️ 中等 需要修改 API 层和调度器

3.2 核心挑战

  1. API 层缺少入口EngineCoreRequest 没有 spec_token_ids 字段

  2. 调度器依赖内部 proposerupdate_draft_token_ids() 只接收内部 proposer 输出

  3. 请求生命周期管理:需要处理连续请求的 draft token 累积

3.3 可行性结论

方案可行,但需要进行如下修改:

  1. 扩展 OpenAI API 接口,支持传递 draft_token_ids

  2. 修改请求处理链路,将外部 draft tokens 注入到 Request.spec_token_ids

  3. 禁用或跳过内部 proposer,直接使用外部 draft tokens


4. 当前投机解码数据流分析

4.1 内部 Proposer 数据流

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        vLLM v1 投机解码数据流                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────────────┐                                                │
│  │ GPUModelRunner   │                                                │
│  │ .take_draft_     │───┐                                            │
│  │  token_ids()     │   │                                            │
│  └──────────────────┘   │  DraftTokenIds                             │
│                          │  {req_ids, draft_token_ids}               │
│                          ▼                                            │
│  ┌──────────────────┐   ┌─────────────────────────────────┐          │
│  │   EngineCore     │   │        Scheduler                │          │
│  │                  │──▶│  .update_draft_token_ids()      │          │
│  │ (core.py:298)    │   │  (scheduler.py:1069)            │          │
│  └──────────────────┘   └─────────────────────────────────┘          │
│                                      │                                │
│                                      ▼                                │
│                          ┌─────────────────────────────────┐          │
│                          │         Request                 │          │
│                          │  .spec_token_ids = [...]        │          │
│                          │  (request.py:94)                │          │
│                          └─────────────────────────────────┘          │
│                                      │                                │
│                                      ▼                                │
│  ┌───────────────────────────────────────────────────────────┐       │
│  │                   Scheduler.schedule()                     │       │
│  │  • scheduled_spec_decode_tokens[req_id] = spec_token_ids   │       │
│  │  (scheduler.py:303-311)                                    │       │
│  └───────────────────────────────────────────────────────────┘       │
│                                      │                                │
│                                      ▼                                │
│  ┌───────────────────────────────────────────────────────────┐       │
│  │                 GPUModelRunner                             │       │
│  │  ._update_states()                                         │       │
│  │  • token_ids_cpu[req_index, ...] = spec_token_ids          │       │
│  │  (gpu_model_runner.py:680-688)                             │       │
│  └───────────────────────────────────────────────────────────┘       │
│                                      │                                │
│                                      ▼                                │
│  ┌───────────────────────────────────────────────────────────┐       │
│  │              Target Model Forward + Verification           │       │
│  │  • _calc_spec_decode_metadata()                            │       │
│  │  • RejectionSampler.forward()                              │       │
│  └───────────────────────────────────────────────────────────┘       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

4.2 关键代码位置

文件 函数/类 作用
vllm/v1/outputs.py:147 DraftTokenIds Draft token 数据结构
vllm/v1/request.py:94 Request.spec_token_ids 请求中存储的草稿 token
vllm/v1/core/sched/scheduler.py:1069 update_draft_token_ids() 更新请求的草稿 token
vllm/v1/core/sched/scheduler.py:303 schedule() 调度时处理 spec tokens
vllm/v1/worker/gpu_model_runner.py:680 _update_states() 将 spec tokens 加入批次
vllm/v1/sample/rejection_sampler.py RejectionSampler 验证草稿 token

5. 外部 Draft Token 注入链路设计

5.1 方案概述

复制代码
┌────────────────────────────────────────────────────────────────────────┐
│                      外部 Draft Token 注入方案                          │
├────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────┐                                                       │
│  │   Client     │                                                       │
│  │ (ASR Model)  │                                                       │
│  └──────┬───────┘                                                       │
│         │ POST /v1/chat/completions                                     │
│         │ {                                                             │
│         │   "messages": [...],                                          │
│         │   "extra_body": {                                             │
│         │     "draft_token_ids": [token1, token2, ...]  ◄─── 新增字段   │
│         │   }                                                           │
│         │ }                                                             │
│         ▼                                                               │
│  ┌──────────────────────────────────────────────────────┐               │
│  │              OpenAI API Server                        │               │
│  │  protocol.py: ChatCompletionRequest                   │               │
│  │    + draft_token_ids: Optional[list[int]]             │               │
│  └──────────────────────────────────────────────────────┘               │
│         │                                                               │
│         ▼                                                               │
│  ┌──────────────────────────────────────────────────────┐               │
│  │              SamplingParams                           │               │
│  │    extra_args["draft_token_ids"] = [...]              │               │
│  └──────────────────────────────────────────────────────┘               │
│         │                                                               │
│         ▼                                                               │
│  ┌──────────────────────────────────────────────────────┐               │
│  │              EngineCoreRequest                        │               │
│  │    + draft_token_ids: Optional[list[int]]  ◄─── 新增  │               │
│  └──────────────────────────────────────────────────────┘               │
│         │                                                               │
│         ▼                                                               │
│  ┌──────────────────────────────────────────────────────┐               │
│  │              Request (v1/request.py)                  │               │
│  │    spec_token_ids = draft_token_ids  ◄─── 直接赋值    │               │
│  └──────────────────────────────────────────────────────┘               │
│         │                                                               │
│         ▼                                                               │
│  ┌──────────────────────────────────────────────────────┐               │
│  │         [复用现有验证链路]                             │               │
│  │    Scheduler.schedule()                               │               │
│  │        ↓                                              │               │
│  │    GPUModelRunner._update_states()                    │               │
│  │        ↓                                              │               │
│  │    Target Forward + RejectionSampler                  │               │
│  └──────────────────────────────────────────────────────┘               │
│                                                                         │
└────────────────────────────────────────────────────────────────────────┘

5.2 需要修改的文件

文件 修改内容 优先级
vllm/entrypoints/openai/protocol.py ChatCompletionRequest 添加 draft_token_ids 字段 P0
vllm/sampling_params.py 通过 extra_args 传递 draft tokens P0
vllm/v1/engine/__init__.py EngineCoreRequest 添加 draft_token_ids 字段 P0
vllm/v1/request.py Request.from_engine_core_request() 读取 draft tokens P0
vllm/v1/core/sched/scheduler.py add_request() 时设置 spec_token_ids P1
vllm/v1/engine/core.py 跳过内部 proposer 的调用 P2

6. 详细实现方案

6.1 方案 A:通过 extra_args 传递(推荐,改动最小)

步骤 1:客户端请求格式
复制代码
from openai import OpenAI
​
client = OpenAI(base_url="http://localhost:8000/v1")
​
response = client.chat.completions.create(
    model="Qwen3-ASR",
    messages=[{"role": "user", "content": "<speech>"}],
    extra_body={
        "vllm_xargs": {
            "draft_token_ids": [1234, 5678, 9012, ...]  # 外部 drafter 生成的 token IDs
        }
    }
)
步骤 2:修改 ChatCompletionRequest.to_sampling_params()
复制代码
# vllm/entrypoints/openai/protocol.py
​
class ChatCompletionRequest(OpenAIBaseModel):
    # ... 现有字段 ...
    
    def to_sampling_params(
        self,
        default_sampling_params: Optional[dict] = None
    ) -> SamplingParams:
        extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
        
        # 新增: 传递 draft_token_ids
        if extra_args.get("draft_token_ids"):
            # 保持在 extra_args 中,后续由 Request 处理
            pass
        
        return SamplingParams.from_optional(
            # ... 现有参数 ...
            extra_args=extra_args or None,
        )
步骤 3:修改 Request.from_engine_core_request()
复制代码
# vllm/v1/request.py
​
@classmethod
def from_engine_core_request(
    cls, request: EngineCoreRequest,
    block_hasher: Optional[Callable[["Request"], list["BlockHash"]]]
) -> "Request":
    req = cls(
        # ... 现有参数 ...
    )
    
    # 新增: 从 extra_args 读取 draft_token_ids
    if request.sampling_params and request.sampling_params.extra_args:
        draft_token_ids = request.sampling_params.extra_args.get("draft_token_ids")
        if draft_token_ids:
            req.spec_token_ids = list(draft_token_ids)
    
    return req
步骤 4:修改调度器,跳过内部 proposer 更新
复制代码
# vllm/v1/core/sched/scheduler.py
​
def update_draft_token_ids(
    self,
    draft_token_ids: DraftTokenIds,
) -> None:
    for req_id, spec_token_ids in zip(
            draft_token_ids.req_ids,
            draft_token_ids.draft_token_ids,
    ):
        request = self.requests.get(req_id)
        if request is None or request.is_finished():
            continue
​
        # 新增: 如果请求已有外部 draft tokens,跳过内部更新
        if request.spec_token_ids and self._is_external_draft(request):
            continue
        
        # ... 现有逻辑 ...

6.2 方案 B:扩展 EngineCoreRequest(更规范但改动较大)

步骤 1:扩展数据结构
复制代码
# vllm/v1/engine/__init__.py
​
class EngineCoreRequest(
        msgspec.Struct,
        array_like=True,
        omit_defaults=True,
        gc=False):
    
    # ... 现有字段 ...
    
    # 新增: 外部 draft token IDs
    draft_token_ids: Optional[list[int]] = None
步骤 2:修改请求创建逻辑
复制代码
# vllm/v1/engine/llm_engine.py 或相关文件
​
def create_engine_core_request(...) -> EngineCoreRequest:
    # 从 sampling_params.extra_args 提取 draft_token_ids
    draft_token_ids = None
    if sampling_params and sampling_params.extra_args:
        draft_token_ids = sampling_params.extra_args.pop("draft_token_ids", None)
    
    return EngineCoreRequest(
        # ... 现有参数 ...
        draft_token_ids=draft_token_ids,
    )

7. 验证链路详解

7.1 Request.spec_token_ids 如何被使用

复制代码
# vllm/v1/core/sched/scheduler.py (Line 303-311)
​
def _schedule_running_reqs(self, ...):
    for request in self.running:
        # 检查是否有 spec tokens
        if request.spec_token_ids:
            num_scheduled_spec_tokens = (num_new_tokens +
                                         request.num_computed_tokens -
                                         request.num_tokens)
            if num_scheduled_spec_tokens > 0:
                # 裁剪并加入调度
                del request.spec_token_ids[num_scheduled_spec_tokens:]
                scheduled_spec_decode_tokens[request.request_id] = (
                    request.spec_token_ids)

7.2 验证在 GPUModelRunner 中执行

复制代码
# vllm/v1/worker/gpu_model_runner.py (Line 680-688)
​
def _update_states(self, scheduler_output: SchedulerOutput):
    for req_id, ... in ...:
        # 获取调度的 spec tokens
        spec_token_ids = (
            scheduler_output.scheduled_spec_decode_tokens.get(req_id, ()))
        if spec_token_ids:
            # 添加到输入批次
            self.input_batch.token_ids_cpu[
                req_index, start_index:end_token_index] = spec_token_ids
            self.input_batch.num_tokens[req_index] += num_spec_tokens

7.3 RejectionSampler 验证逻辑

复制代码
# vllm/v1/sample/rejection_sampler.py
​
class RejectionSampler:
    def forward(
        self,
        target_probs: torch.Tensor,      # 目标模型概率
        draft_probs: torch.Tensor,        # 草稿模型概率 (外部时可为 None)
        draft_token_ids: torch.Tensor,    # 待验证的 draft tokens
        ...
    ) -> torch.Tensor:
        """
        执行 rejection sampling:
        1. 计算接受概率: p_accept = min(1, p_target / p_draft)
        2. 对每个 draft token 进行采样决定是否接受
        3. 返回接受的 token IDs
        """

8. 使用示例

8.1 客户端代码示例

复制代码
import torch
from openai import OpenAI
from transformers import AutoModelForCausalLM, AutoTokenizer
​
# ============ 外部 Drafter (客户端运行) ============
class ExternalASRDrafter:
    def __init__(self, model_path: str):
        self.model = AutoModelForCausalLM.from_pretrained(model_path)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
    
    def generate_draft_tokens(
        self, 
        audio_features: torch.Tensor,
        num_tokens: int = 5
    ) -> list[int]:
        """生成草稿 token IDs"""
        with torch.no_grad():
            outputs = self.model.generate(
                inputs_embeds=audio_features,
                max_new_tokens=num_tokens,
                do_sample=True,
                temperature=0.7
            )
        return outputs[0].tolist()
​
​
# ============ 调用 vLLM 服务进行验证 ============
def verify_with_vllm(
    audio_features: torch.Tensor,
    draft_token_ids: list[int],
    base_url: str = "http://localhost:8000/v1"
):
    client = OpenAI(base_url=base_url, api_key="dummy")
    
    response = client.chat.completions.create(
        model="Qwen3-ASR",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "audio", "audio_url": "..."},  # 或传递 audio embeddings
                    {"type": "text", "text": "<speech>"}
                ]
            }
        ],
        max_tokens=50,
        extra_body={
            "vllm_xargs": {
                "draft_token_ids": draft_token_ids  # 传递外部生成的 draft tokens
            }
        }
    )
    
    return response.choices[0].message.content
​
​
# ============ 主流程 ============
if __name__ == "__main__":
    # 1. 初始化外部 drafter
    drafter = ExternalASRDrafter("path/to/small-asr-model")
    
    # 2. 处理音频输入
    audio_features = load_audio_features("audio.wav")  # 用户自定义加载函数
    
    # 3. 外部 drafter 生成草稿 tokens
    draft_tokens = drafter.generate_draft_tokens(audio_features, num_tokens=5)
    print(f"Draft tokens: {draft_tokens}")
    
    # 4. 发送到 vLLM 进行验证
    result = verify_with_vllm(audio_features, draft_tokens)
    print(f"Verified result: {result}")

8.2 服务端启动命令

复制代码
# 启动 vLLM 服务,启用投机解码验证
python -m vllm.entrypoints.openai.api_server \
    --model /path/to/Qwen3-ASR \
    --port 8000 \
    --trust-remote-code \
    --speculative-config '{"method": "external"}'  # 需要新增配置项

9. 实现清单

9.1 核心修改 (P0)

  • vllm/entrypoints/openai/protocol.py

    • ChatCompletionRequest 支持 vllm_xargs.draft_token_ids
  • vllm/v1/request.py

    • Request.from_engine_core_request() 读取 draft_token_ids 并设置 spec_token_ids
  • vllm/v1/core/sched/scheduler.py

    • add_request() 时如果有外部 draft tokens,直接使用

9.2 扩展修改 (P1)

  • vllm/v1/engine/__init__.py

    • EngineCoreRequest 添加 draft_token_ids 字段
  • vllm/v1/engine/core.py

    • 区分外部/内部 draft tokens 来源

9.3 可选优化 (P2)

  • vllm/config/speculative.py

    • 添加 method="external" 配置选项
  • 性能监控

    • 记录外部 draft tokens 的接受率

10. 风险与注意事项

10.1 潜在风险

风险 影响 缓解措施
Token ID 不匹配 验证失败 确保客户端和服务端使用相同 tokenizer
网络延迟 抵消投机解码收益 考虑批量发送或异步验证
内存管理 KV Cache 碎片 及时清理未接受的 draft tokens

10.2 重要注意事项

  1. Tokenizer 一致性:客户端 drafter 和服务端 target 必须使用完全相同的 tokenizer

  2. 概率分布 :外部 drafter 无法提供 draft_probs,rejection sampling 需要适配

  3. 请求追踪 :需要确保 request_id 在多轮验证中保持一致


11. 总结

11.1 可行性结论

方案可行

用户的需求本质上是将投机解码的 drafter 外置,而 vLLM 的验证链路(Request.spec_token_idsSchedulerGPUModelRunnerRejectionSampler)是完整的,只需要添加一个外部数据注入点。

11.2 推荐实现路径

  1. 短期 :使用 SamplingParams.extra_args 传递 draft tokens(改动最小)

  2. 中期 :扩展 EngineCoreRequest 正式支持外部 draft tokens

  3. 长期:考虑支持 streaming 模式的连续 draft token 验证

11.3 预期收益

指标 传统方案 外部 Drafter 方案
GPU 利用率 100% (draft + target) ~50% (仅 target)
部署灵活性 低 (捆绑部署) 高 (独立部署)
模型选择 受限于 vLLM 支持 任意模型
网络开销 有 (可优化)

附录:核心数据结构参考

复制代码
# vllm/v1/outputs.py
@dataclass
class DraftTokenIds:
    req_ids: list[str]              # 请求 ID 列表
    draft_token_ids: list[list[int]] # 每个请求的 draft tokens
​
# vllm/v1/request.py
class Request:
    spec_token_ids: list[int] = []  # 存储 draft tokens 的地方
​
# vllm/v1/core/sched/output.py
class SchedulerOutput:
    scheduled_spec_decode_tokens: dict[str, list[int]]  # req_id -> spec_token_ids
相关推荐
大模型推理2 小时前
《Nano-vLLM 源码解读》第 12 篇 · ModelRunner:从 prompt 到 token(二)
vllm
清风lsq1 天前
大模型-解析vllm lora 模块
人工智能·vllm·大模型推理
大模型推理1 天前
《Nano-vLLM 源码解读》第 11 篇 · ModelRunner:从 prompt 到 token
vllm
zhangfeng11332 天前
vLLM + AWQ 是什么,为什么有算力架构要求 为什么v100默认不支持
人工智能·语言模型·显卡·vllm
SpikeKing3 天前
LLM - 支持 Hermes 智能体的 vLLM 部署 Qwen3.5 与 Qwen3.6 方案
llm·vllm·qwen3.5·hermes·qwen3.6
zhojiew3 天前
在Ray集群中使用vLLM部署LLM模型并集成Prometheus和Grafana进行指标观测的实践
grafana·prometheus·vllm
不吃天鹅肉3 天前
PaddleOCR-VL + vLLM 高性能推理实践:踩坑与调优全记录
人工智能·语言模型·svm·vllm
张忠琳3 天前
【vllm】(vllm kv_offload)vLLM V1 KV Offload—(二)核心业务逻辑逐行解析
ai·架构·vllm
张忠琳4 天前
【vllm】(v1 Attention)vLLM V1 Attention—Part1 架构总览与核心调度
ai·架构·vllm