面向 vLLM 推理服务的 Speculative Decoding

简介

Speculative decoding 允许 vLLM server 在每个 decode step 中提出多个 token,并通过目标模型的一次 forward pass 对其进行验证,从而在不改变输出分布的前提下降低交互式工作负载的单 token 延迟。

本页重点介绍如何在 Alauda AI 上运行的 InferenceService 中启用、配置、验证和回滚 speculative decoding。关于上游技术本身以及 vLLM 支持的完整方法列表,请参见 vLLM speculative decoding 文档

WARNING

Speculative decoding 依赖于运行时版本敏感的标志位。下面引用的 --speculative-config JSON keys、支持的 method 值以及指标名称,都取决于你运行时镜像中的 vLLM 版本。请将此处的所有示例视为起点,并结合你实际发布的 vLLM 版本进行确认。

在决定之前

每个请求的 decode loop 主导端到端延迟,且提议的 token 被足够频繁地接受,以至于可以摊薄提议开销时,speculative decoding 才能发挥作用。

它通常更适合以下场景:

  • 交互式聊天 / 智能体循环,且后续生成较为可预测。
  • 总结、RAG 回答和代码补全,这些场景中输出与 prompt 重叠较多。

它在以下场景中可能带来负面影响,或者收益不明显:

  • 高温度采样,此时接受率会显著下降。
  • 高 QPS / 批量饱和的服务,此时 decode 容量已经不再空闲。vLLM 团队 2024 年的 V0 engine 基准测试在高 QPS 下对相同数据集报告了 1.4×–1.8× 的变慢。V1 engine 的调度方式不同,因此在你的运行时上幅度可能不同,但风险方向相同。
  • 很小的目标模型,此时验证步骤本身已经很便宜。

在将 speculative decoding 设为默认之前,请先针对具有代表性的工作负载进行测试。请参见 验证并衡量影响

本指南在 Alauda AI 上验证的方法

下面两种方法是本指南覆盖的内容,并且已经在 Alauda AI 上完成端到端验证。vLLM 上游还支持其他方法(例如适用于自带 multi-token-prediction heads 的模型的 MTP、Medusa、MLP Speculator、Suffix、Draft Model),这些方法也可能通过相同的 --speculative-config 标志在 Alauda AI 上使用。它们不在本页范围内,因此请参考上游文档,并在你自己的环境中完成验证后再投入生产。

方法你需要提供的内容权衡
N-gram仅目标模型不需要额外权重,也无需训练。收益取决于 prompt-output token 重叠程度。
EAGLE-3目标模型 以及 与之匹配的 EAGLE-3 draft head需要针对精确的目标模型训练 draft head。会额外占用少量 GPU 内存。

说明:

  • vLLM 上游将 N-gram 描述为“适用于总结和问答等场景,这些场景中 prompt 与答案之间存在显著重叠”。
  • vLLM 上游将 EAGLE-3 描述为“speculative decoding 算法当前的 SOTA”(摘自最新 feature 页面快照;请随 release 重新确认)。

没有一种方法适用于所有工作负载。以下是一些保守的起点,用于降低试错成本。在将其推广到生产之前,请始终结合你自己的流量进行验证。

如果你有...从这里开始
一个通用聊天 / 指令模型,并且有可用的 EAGLE-3 head先用 EAGLE-3,初始 num_speculative_tokens: 3
大量 prompt-output 重叠(RAG、总结、代码补全)且没有 EAGLE-3 head先用 N-gram,初始 num_speculative_tokens: 5
以上都不满足暂缓启用 speculative decoding,直到满足上述条件之一。

内部验证快照 — N-gram

上面的起点是指导建议,不是保证。下面的测量结果来自 Alauda AI 内部实验室的一个具体数据点,旨在帮助你在类似的单 GPU serving 环境中校准预期。你的模型、GPU、运行时版本和流量都会产生不同结果——在投入生产之前务必先做基准测试。

  • 硬件: NVIDIA A30 24 GB × 1
  • 模型: Qwen3-8B(BF16,HuggingFace Qwen/Qwen3-8B
  • 运行时: vLLM 0.19.1(V1 engine)
  • 请求参数: temperature=0seed=42max_tokens=1024enable_thinking=false、单并发请求、1 次 warmup 丢弃 + 3 次计时运行(报告中位数)

基线命令(不使用 spec decode):

python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name t-ng \
  --model /mnt/models \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42

N-gram 命令(仅 --speculative-config 不同):

python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name t-ng \
  --model /mnt/models \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42 \
  --speculative-config '{"method":"ngram","num_speculative_tokens":5,"prompt_lookup_max":4,"prompt_lookup_min":2}'

工作负载:

  • 代码重构(高 prompt-output 重叠): 让模型为一个 30 行的 Python class 添加 docstring 和 type annotation,并返回更新后的完整 class
  • 通用聊天(无 prompt-output 重叠): 让模型用不少于 800 个词解释一个概念

结果:

工作负载基线 tok/sN-gram tok/s加速比墙钟时间差
代码重构(高重叠)47.0245.920.98×+524 ms
通用聊天(无重叠)47.1339.940.85×+3914 ms

解读:

  • 在这个单 GPU 8B 环境中,N-gram 在代码重构工作负载上表现为轻微回退,在聊天场景上则是明显的约 15% 回退。提议器的 CPU 开销、每步对 5 个候选 token 的验证,以及 vLLM 在 N-gram 下会关闭 async scheduling 这一事实,共同消耗的时间比接受 token 节省的时间还要多。
  • 高重叠代码工作负载的接受率是健康的(早期非正式探测中的平均接受长度约为 3),但仅有接受率并不能预测端到端加速效果——每步开销必须相对于目标模型的实际 decode 时间来摊销。对于单 GPU 上的小目标模型,decode 本身已经很便宜,几乎没有可用于摊销的空间。
  • 聊天结果再次印证了 注意事项 中关于没有 prompt-output 重叠的工作负载的说明。

相同方法在更大的目标模型上(此时每次 verify step 成本更高)、在多 GPU tensor parallelism 下,或者在更高并发下,表现可能会非常不同。请将这个快照视为“需要测量”的提醒,而不是对 N-gram 本身的最终结论。

内部验证快照 — EAGLE-3

上面的起点是指导建议,不是保证。下面的测量结果来自 Alauda AI 内部实验室的一个具体数据点,旨在帮助你在类似的单 GPU EAGLE-3 环境中校准预期。你的模型、GPU、运行时版本和流量都会产生不同结果——在投入生产之前务必先做基准测试。

  • 硬件: NVIDIA A30 24 GB × 1
  • 模型: Meta-Llama-3.1-8B-Instruct(BF16,HuggingFace meta-llama/Meta-Llama-3.1-8B-Instruct),EAGLE-3 draft 为 yuhuili/EAGLE3-LLaMA3.1-Instruct-8B
  • 运行时: vLLM 0.19.1(V1 engine)
  • 请求参数: temperature=0seed=42max_tokens=1024、单并发请求、1 次 warmup 丢弃 + 3 次计时运行(报告中位数)

基线命令(不使用 spec decode):

python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name eagle \
  --model /mnt/models/Meta-Llama-3.1-8B-Instruct \
  --dtype auto \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42

EAGLE-3 命令(仅 --speculative-config 不同):

python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name eagle \
  --model /mnt/models/Meta-Llama-3.1-8B-Instruct \
  --dtype auto \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42 \
  --speculative-config '{"method":"eagle3","model":"/mnt/models/EAGLE3-LLaMA3.1-Instruct-8B","num_speculative_tokens":3}'

工作负载:

  • 代码重构(高 prompt-output 重叠): 让模型为一个 30 行的 Python class 添加 docstring 和 type annotation,并返回更新后的完整 class
  • 通用聊天(无 prompt-output 重叠): 让模型用不少于 800 个词解释一个概念

结果:

工作负载基线 tok/sEAGLE-3 tok/s加速比墙钟时间差(中位数)
代码重构(高重叠)47.8488.251.84×−6171 ms
通用聊天(无重叠)47.8747.450.99×+2416 ms

Speedup 是 tok/s 比值(与 completion 长度无关)。墙钟时间差直接比较中位数墙钟时间;聊天运行生成的输出量不同(基线 588 个 token,而 EAGLE-3 为 709 个 token),因此在该场景下 Speedup 是更可靠的指标。

speculative-decoding 行为(EAGLE-3 侧,来自 SpecDecoding metrics 日志窗口):

工作负载平均接受长度平均 Draft 接受率按位置接受率
代码重构(高重叠)≈ 2.54≈ 51%0.50 / 0.40 / 0.33
通用聊天(无重叠)≈ 1.19≈ 6%0.16 / 0.02 / 0.01

平均接受长度和接受率是在覆盖每次基准测试运行的 SpecDecoding metrics 日志窗口中按 draft 加权统计得到的;按位置的数值来自每次运行中的持续负载窗口。

解读:

  • 在这个单 GPU 8B 环境中,EAGLE-3 在代码重构上实现了 约 1.84× 的加速,而在通用聊天上则基本持平(约 0.99×)。两个基线运行都稳定在约 47.8 tok/s,这与预期一致——基础 decode 速率是模型和硬件属性,不依赖 prompt 内容。所有可观察到的差异都来自 EAGLE-3 侧。
  • 为什么代码场景收益明显,而聊天场景不明显——接受率数据直接揭示了机制。在代码场景中,draft head 每个 decode step 平均输出约 2.54 个 token,接受率约 51%,因此大多数 step 都会发出多个 token;按位置的接受率下降较慢(0.50 / 0.40 / 0.33),因此即使是第 3 个 speculative slot 也仍然在约三分之一的时间里有收益。在聊天场景中,平均接受长度只有约 1.19,接受率只有约 6%,并且按位置接受率在第 2 个 slot 就迅速崩塌(0.16 / 0.02 / 0.01)——几乎每一步都只会输出经过验证的 token,而 draft 的 token 会被丢弃。
  • 实际收益 vs 理论上限。 在提议器开销为 0 的情况下,平均接受长度是 speedup 的理论上限。代码场景最终实现了 1.84×,相对于 2.54× 的上限约实现了 72%,也就是说提议器 CPU 工作、对被拒绝提议的验证,以及 async scheduling 成本消耗掉了大约四分之一的余量。聊天场景的 1.19× 理论上限则完全被开销吞掉,并进一步滑落为轻微回退。这与注意事项一致:在单 GPU 上的小模型中,每步开销几乎没有空闲 decode 容量可供隐藏。

相同方法在更大的目标模型上(此时每次 verify step 成本更高)、在多 GPU tensor parallelism 下,或者在更高并发下,表现可能会非常不同。请将这个快照视为“需要测量”的提醒,而不是对 EAGLE-3 本身的最终结论。

前提条件

  • 一个已安装 KServe 的 Kubernetes cluster,以及一个你可以创建 InferenceService 资源的 namespace。
  • 平台上已注册一个 vLLM serving runtime,其 vLLM 版本支持你计划使用的 speculative 方法。要检查版本,请进入使用该 runtime 的正在运行的 pod:kubectl exec <pod> -- python3 -c "import vllm; print(vllm.__version__)"
  • 目标模型可通过其存储来源(model repository、PVC 或 OCI image)供服务访问。
  • 对于 EAGLE-3:一个 draft head,其架构、tokenizer 和 base version 与精确的目标模型匹配。与目标模型不匹配的 head 往往只会悄悄降低接受率,而不会在启动时显式报错。
  • 对于 EAGLE-3:一种能够将 target 和 draft 同时送入同一个 pod 的模型工件加载机制。请参见 在 Alauda AI 上提供模型工件

配置面

在 vLLM v1 中,speculative decoding 通过一个参数启用:

--speculative-config '{"method": "<method>", "num_speculative_tokens": <k>, ...}'

常见 key:

  • method:要使用的 proposer。本指南中使用的值:ngrameagle3。上游还存在其他值(例如 medusa,或模型特定的 MTP 名称,如 deepseek_mtp)——请参考 vLLM speculative decoding 文档,确认你所用方法的准确值。
  • num_speculative_tokens:每一步要提议多少个 token。更高的值可能带来更高的 speedup,但也会在被拒绝的提议上浪费更多计算。
  • model:对于需要加载独立 draft 工件的方法(例如 EAGLE-3),这是该工件在容器内的路径。
  • 方法特定的 key,例如 N-gram 的 prompt_lookup_max / prompt_lookup_min。这些名称在不同 vLLM release 中发生过变化——请结合你发布版本进行确认。

其他所有 vLLM 参数(--model--tensor-parallel-size--gpu-memory-utilization,等等)都与非 speculative 部署中的用法相同。

在 Alauda AI 上提供模型工件

不同方法需要 predictor pod 内部的不同文件。

单工件模式(N-gram)

对于 N-gram,只需要目标模型。像其他 inference service 一样直接使用 storageUri 即可:

spec:
  predictor:
    model:
      storageUri: hf://<your-model-path>

模型会落到 /mnt/models,并通过 --model 传递给 vLLM。

双工件模式(EAGLE-3 及类似方法)

EAGLE-3 需要目标模型以及一个匹配的 draft head,并且两者都加载到同一个 pod 中。支持三种交付方式。请根据你的平台版本、网络访问条件和运维偏好进行选择。

选项 A — KServe storageUris(可用时优先)

storageUris 是一个 KServe 字段,支持多个存储位置,并将每个位置挂载到声明的路径。当你的平台 KServe 版本支持它时,这是最简洁的选项(KServe 0.16 及之后版本)。

spec:
  predictor:
    model:
      storageUris:
        - uri: hf://<your-target-model-path>
          mountPath: /mnt/models/target
        - uri: hf://<your-draft-head-path>
          mountPath: /mnt/models/draft

然后将 vLLM 指向这两个路径:

--model /mnt/models/target \
--speculative-config '{"method":"eagle3","model":"/mnt/models/draft","num_speculative_tokens":3}'

需要注意的约束:

  • storageUri(单数)和 storageUris(复数)互斥。
  • 所有 mountPath 值都必须是绝对路径,并且共享一个公共父目录(例如 /mnt/models/target/mnt/models/draft)。
  • 对于私有仓库,请将相应的凭据 secret 挂到 predictor pod 使用的 service account 上。

如果你平台上的 KServe 版本尚不支持 storageUris,请使用选项 B 或选项 C。

选项 B — 单个 OCI Modelcar,其中包含两个工件

将目标模型和 draft head 打包到一个 OCI image 中,并放在可预测的子目录下(例如 /models/target/models/draft),然后使用 storageUri: oci://... 进行部署。打包步骤请参见 使用 KServe Modelcar 进行模型存储。要写入镜像的示例磁盘布局如下:

/models/
├── target/
│   └── ... target model files ...
└── draft/
    └── ... EAGLE-3 head files ...

随后 vLLM 命令引用相同路径:

--model /mnt/models/target \
--speculative-config '{"method":"eagle3","model":"/mnt/models/draft","num_speculative_tokens":3}'

此选项非常适合离线 / air-gapped cluster,因为这些工件会被一起版本化并从你自己的 registry 拉取。

选项 C — 预先放置在共享 PVC 上

将两个工件预先放置到某个 PVC 的已知目录结构中,挂载该 PVC,并在 vLLM 命令中引用本地路径。如果你已经在共享文件系统上管理模型文件,这是最简单的选项。

在 A / B / C 之间选择

约束使用
在线 cluster,KServe ≥ 0.16,希望使用声明式 manifest选项 A
离线 / air-gapped,希望只有一个版本化工件选项 B
已经在共享 PVC 上管理模型文件选项 C

端到端示例

下面两个示例覆盖了 Alauda AI 上验证的方法 中列出的方法。请将 <your-namespace><your-vllm-runtime> 和 storage URI 替换为你环境中的实际值。

示例 1 — N-gram

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  annotations:
    aml-model-repo: Qwen2.5-7B-Instruct
    serving.knative.dev/progress-deadline: 1800s
    serving.kserve.io/deploymentMode: Standard
  labels:
    aml.cpaas.io/runtime-type: vllm
  name: qwen-ngram-spec
  namespace: <your-namespace>
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 1
    model:
      command:
        - bash
        - -c
        - |
          set -ex

          MODEL_PATH="/mnt/models/${MODEL_NAME}"
          if [ ! -d "${MODEL_PATH}" ]; then
            MODEL_PATH="/mnt/models"
          fi

          python3 -m vllm.entrypoints.openai.api_server \
            --port 8080 \
            --served-model-name {{.Name}} {{.Namespace}}/{{.Name}} \
            --model "${MODEL_PATH}" \
            --dtype ${DTYPE} \
            --gpu-memory-utilization ${GPU_MEMORY_UTILIZATION} \
            --speculative-config '{"method":"ngram","num_speculative_tokens":5,"prompt_lookup_max":4,"prompt_lookup_min":2}' 
        - bash
      env:
        - name: DTYPE
          value: half
        - name: GPU_MEMORY_UTILIZATION
          value: '0.85'
        - name: MODEL_NAME
          value: '{{ index .Annotations "aml-model-repo" }}'
      modelFormat:
        name: transformers
      protocolVersion: v2
      resources:
        limits:
          cpu: '8'
          memory: 32Gi
          nvidia.com/gpu: '1'
        requests:
          cpu: '4'
          memory: 16Gi
      runtime: <your-vllm-runtime>
      storageUri: hf://<your-model-path>
    securityContext:
      seccompProfile:
        type: RuntimeDefault
  1. 替换为你的实际模型名称;平台会使用该注解进行展示。
  2. prompt_lookup_* keys 属于 n-gram proposer。它们的名称在不同 vLLM release 之间发生过变化——请结合你运行时镜像中的版本进行确认。

示例 2 — 在共享 PVC 上同时提供 target + draft 的 EAGLE-3

此 manifest 与上文 内部验证快照 — EAGLE-3 中使用的配置一致。目标模型和 EAGLE-3 draft head 都预先放置在同一个 PVC 的可预测子目录下;PVC 通过 storageUri: pvc://... 挂载到 /mnt/models/,而 vLLM 命令直接引用这两个子目录。

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  annotations:
    aml-model-repo: Meta-Llama-3.1-8B-Instruct
    serving.knative.dev/progress-deadline: 1800s
    serving.kserve.io/deploymentMode: Standard
  labels:
    aml.cpaas.io/runtime-type: vllm
  name: llama-eagle3-spec
  namespace: <your-namespace>
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 1
    model:
      command:
        - bash
        - -c
        - |
          set -ex

          python3 -m vllm.entrypoints.openai.api_server \
            --port 8080 \
            --served-model-name {{.Name}} {{.Namespace}}/{{.Name}} \
            --model /mnt/models/Meta-Llama-3.1-8B-Instruct \
            --dtype ${DTYPE} \
            --gpu-memory-utilization ${GPU_MEMORY_UTILIZATION} \
            --max-model-len 4096 \
            --max-num-seqs 8 \
            --seed 42 \
            --speculative-config '{"method":"eagle3","model":"/mnt/models/EAGLE3-LLaMA3.1-Instruct-8B","num_speculative_tokens":3}' 
        - bash
      env:
        - name: DTYPE
          value: auto
        - name: GPU_MEMORY_UTILIZATION
          value: '0.8'
      modelFormat:
        name: transformers
      protocolVersion: v2
      resources:
        limits:
          cpu: '8'
          memory: 48Gi
          nvidia.com/gpu: '1'
        requests:
          cpu: '4'
          memory: 24Gi
      runtime: <your-vllm-runtime>
      storageUri: pvc://<your-pvc-name>/
    securityContext:
      seccompProfile:
        type: RuntimeDefault
  1. vLLM 命令中的两个路径(--model--speculative-config 内的 model key)必须与 PVC 内部的目录名完全一致。如果你的 PVC 以不同名称存放这些工件,请同时调整这两个路径。
  2. EAGLE-3 head 会占用 --gpu-memory-utilization 预算之外的 GPU 内存。保留余量(这里使用 0.8 而不是 0.9)可以降低在两个工件都加载时发生 OOM 的概率。
  3. pvc://<your-pvc-name>/ 期望一个同时包含目标模型和 EAGLE-3 draft head 的预先放置的 PVC;PVC root 会挂载到 /mnt/models/,因此两个工件必须分别位于 /mnt/models/<target-subdir>//mnt/models/<draft-subdir>/。请参见下方预期布局。如果你更倾向于声明式的多 URI 挂载(KServe 0.16+)或改为将 target + draft 打包进单个 OCI image,请参见 在提供模型工件中的选项 A 或选项 B

PVC 内的预期布局(在 pod 中挂载到 /mnt/models/):

<PVC root>/
├── Meta-Llama-3.1-8B-Instruct/
│   └── ... target model files ...
└── EAGLE3-LLaMA3.1-Instruct-8B/
    └── ... EAGLE-3 draft head files ...

pod 启动后,请在 predictor pod 内验证布局:

kubectl exec -n <your-namespace> <pod> -- ls /mnt/models/
# Expected: EAGLE3-LLaMA3.1-Instruct-8B/  Meta-Llama-3.1-8B-Instruct/

使用以下命令应用上述任一 manifest:

kubectl apply -f <manifest>.yaml -n <your-namespace>

验证并衡量影响

确认 speculative decoding 已配置好,是一个步骤。确认它确实对你的工作负载有帮助,则是另一个步骤。

1. 确认配置已应用

kubectl get inferenceservice <name> -n <your-namespace> -o yaml

在 predictor 命令中查找 --speculative-config,并确认就绪状态:

kubectl get pods -n <your-namespace> -l serving.kserve.io/inferenceservice=<name>

2. 确认 speculative decoding 正在实际运行

启动时最先出现的信号是 engine-config 日志行;它会打印 engine 解析后的 speculative_config,因此你可以验证 method 和 draft 路径是否已经生效:

kubectl logs -n <your-namespace> -l serving.kserve.io/inferenceservice=<name> \
  | grep -m1 'Initializing a V1 LLM engine'
# Expected to contain: speculative_config=SpeculativeConfig(method='eagle3', model='...', num_spec_tokens=3)

对于实时计数器,vLLM 会在 /metrics 上暴露 Prometheus metrics。具体指标名称取决于 vLLM 版本,因此建议先进行广泛匹配:

kubectl exec <pod> -n <your-namespace> -- curl -s localhost:8080/metrics | grep -iE 'spec_decode|draft|acceptance'

如果没有返回结果,说明该 pod 要么还没有服务过任何请求(计数器只会在第一次生成完成后才开始发布),要么你所用 vLLM 构建中的指标名称不同——这种情况下请回退查看 predictor 日志。

vLLM 会打印按窗口汇总的结果行,这是最容易阅读的实时视图。下面是 vLLM 0.19.1 在 num_speculative_tokens=3 时的真实行格式:

SpecDecoding metrics: Mean acceptance length: 2.68, Accepted throughput: 65.69 tokens/s,
Drafted throughput: 116.98 tokens/s, Accepted: 657 tokens, Drafted: 1170 tokens,
Per-position acceptance rate: 0.664, 0.559, 0.462, Avg Draft acceptance rate: 56.2%

如何解读:

  • Mean acceptance length —— 每个 decode step 平均交付的 token 数。基线为 1。这是在该工作负载上你可能获得的 speedup 的实际上限。
  • Avg Draft acceptance rate —— 所有提议 token 中被接受的总体比例。可以用一个数字回答“提议器整体是在帮忙,还是大多在浪费算力?”。
  • Per-position acceptance rate —— 对于位置 1..num_speculative_tokens 的逐槽接受率。你会看到恰好 num_speculative_tokens 个值——上面的示例有 3 个,因为运行时使用的是 num_speculative_tokens=3;使用 num_speculative_tokens=5ngram 运行会打印 5 个值。健康的曲线会缓慢下降;如果曲线在第 2 个槽就接近 0,说明该工作负载并不适合这个 proposer。

3. 衡量端到端影响

对同一个具有代表性的工作负载运行两次:

  1. 移除 --speculative-config(基线)。
  2. 启用该配置(其余完全相同,包括 --seed)。

每次运行都记录三个数值:

  • 首 token 时间(TTFT)。
  • 单 token 延迟(或固定输出长度下的端到端延迟)。
  • 在你实际服务的 QPS 下的吞吐量(tokens/second)。

如果这三项在你的目标 QPS 下都得到改善,那么 speculative decoding 就值得保留。一个常见失败模式是在低 QPS 时表现更好,但在生产 QPS 下反而回退——因此一定要在你实际运行的场景中测量。

4. 如何报告或比较数值

没有上下文的性能数字是无法复现或信任的。每当你发布比较结果——无论是在内部、客户报告中,还是反馈给平台团队——都应包含下面五个字段。缺少其中任何一项的数字,都应被视为轶事,而不是证据。

**Hardware:** <GPU model and count, e.g. NVIDIA A30 24 GB × 1>
**Model:** <model identifier and dtype, e.g. Qwen3-8B (BF16)>
**Runtime:** <vLLM version and runtime image name, e.g. vLLM 0.19.1 inside aml-vllm-x.y.z>
**Request parameters:** <temperature, max_tokens, concurrency, sampling toggles, runs per prompt>

**Baseline command (no spec decode):**
```text
python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name <name> \
  --model /mnt/models \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42

spec-decode 命令(仅 --speculative-config 不同):

python3 -m vllm.entrypoints.openai.api_server \
  --port 8080 \
  --served-model-name <name> \
  --model /mnt/models \
  --gpu-memory-utilization 0.8 \
  --max-model-len 4096 \
  --max-num-seqs 8 \
  --seed 42 \
  --speculative-config '{"method":"ngram","num_speculative_tokens":5,"prompt_lookup_max":4,"prompt_lookup_min":2}'

结果:

工作负载基线 TTFTSpec TTFT基线 tok/sSpec tok/s平均接受长度平均接受率加速比(tok/s)
chat
code
rag

进行比较测试时,有两条实用规则:

  • 两边使用相同的 --seedtemperature=0,并在计时前先对每个服务做 3 次丢弃的 warmup 请求——否则采样噪声和 compile-cache 噪声会主导你测量到的差异。
  • 基线和 spec-decode 必须针对同一份固定 prompt 列表、同一顺序进行,每个 prompt 至少运行 5–10 次,并比较中位数而不是平均值。

回滚

要在不修改其他内容的情况下禁用 speculative decoding,只需从 predictor 命令中删除 --speculative-config 这一行并重新应用:

kubectl edit inferenceservice <name> -n <your-namespace>
# delete the --speculative-config line, save, exit

或者重新应用一个不包含该标志位的 manifest:

kubectl apply -f <manifest-without-spec-config>.yaml -n <your-namespace>

服务会滚动到一个不带 speculative proposer 的新 revision。对于 N-gram,不需要更改模型工件。对于 EAGLE-3,draft head 仍会被挂载,但不会被使用——如果你希望回收磁盘空间,请在下一次变更时移除 draft head 工件(对于选项 A,删除匹配的 storageUris 条目;对于选项 B,重新构建不含 draft 目录的 OCI image;对于选项 C,从 PVC 中删除 draft 子目录)。

故障排查

症状可能原因检查项
pod 启动失败,vLLM 参数错误中提到 speculative 或未知 JSON keys--speculative-config 中的 keys 与运行时镜像中的 vLLM 版本不匹配kubectl exec <pod> -- python3 -c "import vllm; print(vllm.__version__)",并将参数与该版本对齐
pod 启动失败,出现未知的 methodmethod 拼写错误,或者该值不被你的 vLLM 版本支持(例如使用 eagle 而不是 eagle3在上游 speculative decoding 文档中确认你所使用 vLLM release 支持的 method
启用 EAGLE-3 后在模型加载期间 OOM没有为 EAGLE-3 head 的内存预留预算--gpu-memory-utilization 下调 0.05–0.10,或者减少 GPU 上的其他工作负载
服务已 Ready,但接受率接近 0target 与 draft 之间的 tokenizer / architecture 不匹配,或者采样温度过高重新确认 draft head 与目标模型精确匹配;在评估时降低采样温度
在生产 QPS 下 TTFT 或延迟回退提议开销不再被空闲 decode 容量掩盖在该服务上禁用,或者降低 num_speculative_tokens;请参见 回滚
API server 拒绝 storageUris平台上的 KServe 版本早于 storageUris 的引入改用选项 B(Modelcar)或选项 C(PVC)
Knative 在 rollout 期间由于 progress-deadline 超时将 revision 标记为 NotReady带 draft 工件的冷启动比不带 draft 更慢——backbone 和 EAGLE head 都要进行 torch.compile + engine profiling,可能会超过默认的 progress deadline提高 serving.knative.dev/progress-deadline(我们在 A30 + Llama-3.1-8B 上的 EAGLE-3 冷启动约为 5 分钟;本页的示例 1 和示例 2 manifest 因此将其设为 1800s
客户端在 spec decode 下使用 min_plogit_bias 时观察到意外的采样行为启用 speculative decoding 时,vLLM 会静默忽略这两个参数(在 engine init 时打印警告)从请求中删除该参数,或者在依赖这些参数的服务上关闭 speculative decoding

对于 pod 级别的问题,标准的 inference-service 故障排查命令同样适用:

kubectl describe inferenceservice <name> -n <your-namespace>
kubectl logs -n <your-namespace> -l serving.kserve.io/inferenceservice=<name>

注意事项和已知限制

  • 结果会随着工作负载形态大幅波动——回退和加速都是真实存在的。 上游 V0 基准测试在高 QPS 下报告了 1.4×–1.8× 的变慢。我们在 A30 + Qwen3-8B 上做的 N-gram 测试(见 内部验证快照 — N-gram)即使在高重叠代码工作负载上也观察到了轻微回退。在相同硬件上,Llama-3.1-8B 上的 EAGLE-3(见 内部验证快照 — EAGLE-3)在代码重构上达到了 1.84× 加速,但在聊天上基本持平(约 0.99×)——同一模型、同一方法、同一个 pod,仅 prompt 形态不同,最终收益却相差 2 倍。请始终结合你的生产流量特征进行验证。
  • N-gram 会禁用 async scheduling。 在较新的 vLLM 版本中,启用 ngram method 会强制关闭 async scheduling(predictor 日志会显示 Async scheduling not supported with ngram-based speculative decoding and will be disabled)。如果你的服务依赖 async scheduling 来提高吞吐量,请优先选择 EAGLE-3,或者明确测量这种权衡。
  • storageUris 的可用性。 该字段从 KServe 0.16 开始提供。较旧的平台版本必须使用 Modelcar 或 PVC 方案。
  • draft head 不匹配不会报错。 与目标模型不完全匹配的 draft head 通常仍然能够启动并正常提供流量,但接受率会非常低。启用后请务必检查接受率。
  • 采样参数会影响接受率。 高温度会降低接受率;请使用能反映生产使用情况的采样设置进行基准测试。
  • gpu-memory-utilization 预算。 draft 工件(EAGLE-3 head、MLP speculator、draft model)不包含在 --gpu-memory-utilization 预算内;在添加 draft 工件时应降低该值。
  • 镜像依赖。 运行时镜像必须包含所选方法所需的库。如果某个方法初始化失败,请重建或替换运行时镜像——参见 扩展 Inference Runtimes
  • min_plogit_bias 会被静默忽略。 在 speculative decoding 下,vLLM 会在 engine init 期间记录警告 min_p and logit_bias parameters won't work with speculative decoding.。即使请求携带了这两个采样参数,仍然会收到 200 响应,但这些参数不会生效——如果你的流量依赖它们,请结合客户端预期进行验证。
  • 与其他特性的组合。 Speculative decoding 可以与 tensor parallelism 和 continuous batching 组合使用,但它与 autoscaling 以及 EP / advanced parallelism 的交互会随 vLLM 版本而变化。带有 draft 工件时,冷启动的成本会明显更高:在我们基于 A30 + Llama-3.1-8B + EAGLE-3 head 的实验室环境中,predictor 从 container-ready 到 Application startup complete 约耗时 5 分钟(weight load 约 45 s、draft weights 约 5 s、torch.compile backbone 约 48 s、torch.compile EAGLE head 约 17 s、CUDA-graph capture 和 warmup 约 10 s,再加上约 2 分钟的 engine profiling 和 KV-cache sizing)。请将你的 Knative progress-deadline 注解以及任何 autoscaling 从零扩容的 SLO 按这个时间设置,而不是按非 speculative 基线来设置。
  • 输出等价性。 vLLM 声明 speculative decoding 不会改变输出分布。这是 vLLM 的属性,而不是 Alauda AI 的保证——如果你的运行时镜像要求严格等价,请将其作为验收测试的一部分进行验证。

参考资料