外部 Drafter 投机解码验证方案技术文档
1. 需求分析
1.1 用户需求描述
用户希望实现一种"外部 Drafter + 内部 Target 验证"的投机解码变体方案:
-
客户端运行独立的推理模型(例如 ASR drafter),生成草稿 token
-
将草稿 token 发送到 vLLM 服务端
-
vLLM 服务端使用目标模型(
Qwen3NioAsrModel)对草稿 token 进行前向验证 -
通过 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 核心挑战
-
API 层缺少入口 :
EngineCoreRequest没有spec_token_ids字段 -
调度器依赖内部 proposer :
update_draft_token_ids()只接收内部 proposer 输出 -
请求生命周期管理:需要处理连续请求的 draft token 累积
3.3 可行性结论
方案可行,但需要进行如下修改:
-
扩展 OpenAI API 接口,支持传递
draft_token_ids -
修改请求处理链路,将外部 draft tokens 注入到
Request.spec_token_ids -
禁用或跳过内部 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.pyChatCompletionRequest支持vllm_xargs.draft_token_ids
-
vllm/v1/request.pyRequest.from_engine_core_request()读取draft_token_ids并设置spec_token_ids
-
vllm/v1/core/sched/scheduler.pyadd_request()时如果有外部 draft tokens,直接使用
9.2 扩展修改 (P1)
-
vllm/v1/engine/__init__.pyEngineCoreRequest添加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 重要注意事项
-
Tokenizer 一致性:客户端 drafter 和服务端 target 必须使用完全相同的 tokenizer
-
概率分布 :外部 drafter 无法提供
draft_probs,rejection sampling 需要适配 -
请求追踪 :需要确保
request_id在多轮验证中保持一致
11. 总结
11.1 可行性结论
✅ 方案可行
用户的需求本质上是将投机解码的 drafter 外置,而 vLLM 的验证链路(Request.spec_token_ids → Scheduler → GPUModelRunner → RejectionSampler)是完整的,只需要添加一个外部数据注入点。
11.2 推荐实现路径
-
短期 :使用
SamplingParams.extra_args传递 draft tokens(改动最小) -
中期 :扩展
EngineCoreRequest正式支持外部 draft tokens -
长期:考虑支持 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