vLLM 推理服务的推测解码

简介

推测解码允许 vLLM server 在每个 decode 步骤中提出多个 token,并通过目标 model 的单次 forward pass 对其进行验证,从而在不改变输出分布的情况下,降低交互式工作负载的单 token 延迟。

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

WARNING

推测解码涉及对 runtime 版本敏感的 flags。下面提到的 --speculative-config JSON keys、支持的 method 值以及 metric 名称,都取决于你 runtime image 中的 vLLM 版本。本文中的所有示例都应视为起点,并请以你实际发布的 vLLM 版本为准进行确认。

在决定之前

单个请求的 decode loop 主导端到端延迟,并且提议的 token 能够以足够高的接受率被采纳,从而摊薄提议开销时,推测解码才会带来收益。

它通常适用于:

  • 交互式 chat / agent 循环,且后续输出相对可预测。
  • 摘要、RAG answers 和 code completion,此类场景中输出与 prompt 有较高重叠。

在以下场景中,它可能带来负收益或基本无影响:

  • 高温度采样,此时接受率会大幅下降。
  • 高 QPS / batch 饱和的服务,此时 decode 容量已不再空闲。vLLM 团队在 2024 年的 V0-engine benchmarks 中报告,在高 QPS 下,同一数据集上出现了 1.4×–1.8× 的变慢。V1 engine 的调度方式不同,因此在你的 runtime 上幅度可能不同,但风险方向是相同的。
  • 非常小的 target model,此时验证步骤本身已经很便宜。

在将推测解码设为默认值之前,请先在具有代表性的 workload 上进行验证。请参见 验证并衡量影响

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

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

方法你需要提供的内容权衡
N-gram仅 target model无额外 weights,无需训练。收益取决于 prompt-output token 重叠程度。
EAGLE-3target model 以及 匹配的 EAGLE-3 draft head需要一个针对精确 target model 训练的 draft head。额外 GPU memory 占用较小。

说明:

  • vLLM upstream 将 N-gram 描述为“适用于摘要和问答等用例,因为 prompt 与 answer 之间存在显著重叠”。
  • vLLM upstream 将 EAGLE-3 描述为“当前推测解码算法的 SOTA”(摘自最新 features 页;请按每个 release 重新确认)。

不存在一种适用于所有 workload 的最佳方法。以下是保守的起始点,用于降低试错成本。在推广到生产之前,请始终结合你自己的流量进行验证。

如果你有...建议从这里开始
一个通用 chat / instruction model,并且有可用的 EAGLE-3 headEAGLE-3,初始 num_speculative_tokens: 3
prompt-output 重叠很高(RAG、摘要、code completion)且没有 EAGLE-3 headN-gram,初始 num_speculative_tokens: 5
以上都不满足暂缓启用推测解码,直到满足上述条件之一。

内部验证快照 — N-gram

上面的起始点是建议,不是保证。下面的测量结果来自 Alauda AI 内部实验室的一个具体数据点,目的是帮助你对类似的单 GPU serving 场景形成预期。你自己的 model、GPU、runtime 版本和流量会产生不同结果——在推广到生产之前务必进行 benchmark。

  • 硬件: NVIDIA A30 24 GB × 1
  • Model: Qwen3-8B(BF16,HuggingFace Qwen/Qwen3-8B
  • Runtime: 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}'

Workloads:

  • code refactor(高 prompt-output 重叠): 要求 model 为一个 30 行的 Python class 添加 docstring 和类型注解,并返回完整更新后的 class
  • general chat(无 prompt-output 重叠): 要求 model 用不少于 800 个词解释一个概念

结果:

Workload基线 tok/sN-gram tok/s加速比墙钟时间差
Code refactor(高重叠)47.0245.920.98×+524 ms
General chat(无重叠)47.1339.940.85×+3914 ms

解读:

  • 在这个单 GPU 8B 场景中,N-gram 在 code refactor workload 上表现为轻微回退,而在 chat 上则出现了明显的约 15% 回退。提议器的 CPU 工作、每步对 5 个候选 token 的验证,以及 vLLM 在 N-gram 下禁用 async scheduling,这些开销加在一起超过了被接受 token 带来的收益。
  • 对高重叠 code workload 而言,接受率是健康的(在更早的非正式探测中,平均接受长度约为 3),但仅有接受率并不能预测端到端加速——每步开销必须与 target model 的实际 decode 时间摊销后才有意义。在单 GPU 的小型 target model 上,decode 本身已经很便宜,可供摊销的空间很小。
  • chat 结果验证了 注意事项 中关于缺少 prompt-output 重叠的 workload 的判断。

同样的方法在更大的 target model 上(此时每次 verify step 的成本更高)、在 multi-GPU tensor parallelism 下,或在更高并发下,可能表现得非常不同。请把这个快照当作“需要测量”的提醒,而不是对 N-gram 本身的结论。

内部验证快照 — EAGLE-3

上面的起始点是建议,不是保证。下面的测量结果来自 Alauda AI 内部实验室的一个具体数据点,目的是帮助你对类似的单 GPU EAGLE-3 场景形成预期。你自己的 model、GPU、runtime 版本和流量会产生不同结果——在推广到生产之前务必进行 benchmark。

  • 硬件: NVIDIA A30 24 GB × 1
  • Model: 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
  • Runtime: 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}'

Workloads:

  • code refactor(高 prompt-output 重叠): 要求 model 为一个 30 行的 Python class 添加 docstring 和类型注解,并返回完整更新后的 class
  • general chat(无 prompt-output 重叠): 要求 model 用不少于 800 个词解释一个概念

结果:

Workload基线 tok/sEAGLE-3 tok/s加速比墙钟时间差(中位数)
Code refactor(高重叠)47.8488.251.84×−6171 ms
General chat(无重叠)47.8747.450.99×+2416 ms

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

推测解码行为(EAGLE-3 侧,来自 SpecDecoding metrics 日志窗口):

Workload平均接受长度平均 Draft 接受率按位置接受率
Code refactor(高重叠)≈ 2.54≈ 51%0.50 / 0.40 / 0.33
General chat(无重叠)≈ 1.19≈ 6%0.16 / 0.02 / 0.01

平均接受长度和接受率是按各 benchmark 运行所覆盖的 SpecDecoding metrics 日志窗口中的 draft 加权结果;按位置的数值来自每次运行中的持续负载窗口。

解读:

  • 在这个单 GPU 8B 场景中,EAGLE-3 在 code refactor 上带来了 约 1.84× 的加速,而在 general chat 上则基本持平(约 0.99×)。两个基线运行的 tok/s 都稳定在约 47.8,这与预期一致——基础 decode 速率是 model 与硬件的属性,不依赖 prompt 内容。所有可观察到的差异都来自 EAGLE-3 一侧。
  • 为什么 code 有收益而 chat 没有——接受率数据直接揭示了机制。在 code 场景中,draft head 在每个 decode step 平均能命中约 2.54 个 token,接受率约 51%,因此大多数 step 会输出多个 token;按位置接受率下降得很慢(0.50 / 0.40 / 0.33),因此即使第 3 个 speculative slot 仍然有约三分之一的时间产生收益。在 chat 场景中,平均接受长度只有约 1.19,接受率约 6%,并且按位置接受率在第 2 个 slot 就迅速崩塌(0.16 / 0.02 / 0.01)——几乎每一步都只输出经过验证的 token,而 draft 出来的 token 都被丢弃。
  • 实际效果 vs 理论上限。 在没有 proposer 开销的理想情况下,平均接受长度是理论上的最大加速上限。code 场景实际达到 1.84×,而理论上限为 2.54×(约 72% 转化),也就是说 proposer 的 CPU 工作、对被拒绝提议的验证,以及 async scheduling 的成本,吞掉了大约四分之一的可用空间。chat 场景的 1.19× 理论上限则完全被开销吃掉,甚至略微转为回退。这与 Caveats 中的说明一致:在单 GPU 的小模型上,每步开销几乎没有空闲 decode 容量可供隐藏。

同样的方法在更大的 target model 上(此时每次 verify step 的成本更高)、在 multi-GPU tensor parallelism 下,或在更高并发下,可能表现得非常不同。请把这个快照当作“需要测量”的提醒,而不是对 EAGLE-3 本身的结论。

前提条件

  • 一个已安装 KServe 的 Kubernetes 集群,以及可以创建 InferenceService 资源的 namespace。
  • 平台上已注册一个 vLLM serving runtime,并且其 vLLM 版本支持你计划使用的 speculative method。要检查版本,请进入该 runtime 的运行中 pod:kubectl exec <pod> -- python3 -c "import vllm; print(vllm.__version__)"
  • 目标 model 可通过其 storage source(model repository、PVC 或 OCI image)供服务访问。
  • 对于 EAGLE-3:一个 draft head,其 architecture、tokenizer 和 base version 与精确的 target model 匹配。若 head 不匹配,通常会悄悄降低接受率,而且不一定会以启动错误的形式暴露。
  • 对于 EAGLE-3:一种可以将 target 和 draft 同时送入同一 pod 的 model-artifact 加载机制。请参阅 在 Alauda AI 上提供 Model Artifacts

配置面

在 vLLM v1 中,推测解码通过一个参数启用:

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

常见 keys:

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

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

在 Alauda AI 上提供 Model Artifacts

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

单 artifact 模式(N-gram)

N-gram 只需要 target model。像其他 inference service 一样,直接使用 storageUri 即可:

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

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

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

EAGLE-3 需要将 target model 以及 匹配的 draft head 加载到同一个 pod 中。共有三种受支持的交付方式。请根据平台版本、网络访问条件和运维偏好进行选择。

选项 A — KServe storageUris(可用时推荐)

storageUris 是一个 KServe 字段,支持多个 storage location,并将每个 location 挂载到声明的路径。当你的平台 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)。
  • 对于私有仓库,请将相应的 credentials secret 绑定到 predictor pod 使用的 service account 上。

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

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

将 target model 和 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 集群,此选项非常适合,因为这些 artifact 会一起版本化,并从你自己的 registry 拉取。

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

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

在 A / B / C 之间选择

约束选择
在线集群、KServe ≥ 0.16,希望使用声明式 manifests选项 A
离线 / air-gapped,希望使用单个版本化 artifact选项 B
已经在共享 PVC 上管理 model 文件选项 C

端到端示例

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

示例 1 — N-gram

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  annotations:
    aml-model-repo: Qwen2.5-7B-Instruct
    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. 请替换为你的实际 model 名称;该注解由平台用于展示。
  2. prompt_lookup_* keys 属于 n-gram proposer。这些名称在不同 vLLM release 之间发生过变化——请以 runtime image 内的版本为准进行验证。

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

该 manifest 与上文 内部验证快照 — EAGLE-3 中使用的 setup 一致。target model 和 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.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 memory。保留余量(这里用 0.8 而不是 0.9)可以在两个 artifact 同时加载时降低 OOM 风险。
  3. pvc://<your-pvc-name>/ 期望一个已预先放置目标 model 和 EAGLE-3 draft head 的 PVC;PVC 根目录会挂载到 /mnt/models/,因此两个 artifact 必须分别位于 /mnt/models/<target-subdir>//mnt/models/<draft-subdir>/。请参见下方期望布局。如果你更希望使用声明式多 URI 挂载(KServe 0.16+),或者改为将 target + draft 打包进单个 OCI image,请参阅 在提供 Model Artifacts 中的选项 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>

验证并衡量影响

确认已配置推测解码是一回事;确认它确实改善了你的 workload则是另一回事。

1. 确认配置已应用

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

在 predictor command 中查找 --speculative-config,并确认 readiness 状态:

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

2. 确认推测解码正在实际运行

启动时的第一个信号是 engine-config 日志行;它会打印 engine 解析出的 speculative_config,因此你可以据此验证 method 和 draft path 是否生效:

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)

对于实时 counter,vLLM 会在 /metrics 暴露 Prometheus metrics。确切的 metric 名称取决于 vLLM 版本,因此应先进行广泛匹配:

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

如果没有返回任何内容,说明 pod 还没有处理过任何请求(counter 只会在第一次 generation 完成后才发布),或者你所用 vLLM build 中的 metric 名称不同——此时应回退查看 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。这是你在该 workload 上可期望获得的实际加速上限。
  • Avg Draft acceptance rate — 被接受的提议 token 占所有提议 token 的整体比例。它回答的是“proposer 大多在发挥作用,还是大多在浪费计算?”这一问题。
  • Per-position acceptance rate — 第 1..num_speculative_tokens 个 slot 的逐位接受率。你会看到恰好 num_speculative_tokens 个值——上面的示例之所以有 3 个值,是因为运行时使用了 num_speculative_tokens=3;而一个 num_speculative_tokens=5ngram 运行会打印 5 个值。健康的曲线下降应较缓;如果在第 2 个 slot 附近就降到接近 0,说明该 workload 不适合这个 proposer。

3. 衡量端到端影响

对同一个具有代表性的 workload 运行两次:

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

每次运行记录三个数值:

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

如果在目标 QPS 下这三项都得到改善,那么就值得保留推测解码。常见失败模式是:低 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}'

结果:

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

进行对比时,有两个实用原则:

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

回滚

如果要在不改动其他内容的情况下禁用推测解码,只需从 predictor command 中移除 --speculative-config 这一行并重新应用:

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

或者重新应用一个不包含该 flag 的 manifest:

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

服务会以不含 speculative proposer 的新 revision 进行滚动更新。对于 N-gram,不需要更改任何 model artifact。对于 EAGLE-3,draft head 会继续挂载但不会被使用——如果你想回收磁盘空间,请在下一次变更时移除 draft-head artifact(删除选项 A 中对应的 storageUris entry、在不包含 draft 目录的情况下重新构建选项 B 的 OCI image,或者在选项 C 中删除 PVC 的 draft 子目录)。

故障排查

症状可能原因需要检查的内容
Pod 启动失败,并报出包含 speculative 或未知 JSON keys 的 vLLM argument error--speculative-config 中的 keys 与 runtime image 内的 vLLM 版本不匹配kubectl exec <pod> -- python3 -c "import vllm; print(vllm.__version__)",并将 flags 调整为与该版本一致
Pod 启动失败,并提示未知的 methodmethod 拼写错误,或者该 vLLM 版本不支持该值(例如使用 eagle 代替 eagle3在 upstream speculative decoding 文档中确认你所用 vLLM release 支持的 method
启用 EAGLE-3 后在 model load 期间发生 OOM未为 EAGLE-3 head 的 memory 预留预算--gpu-memory-utilization 下调 0.05–0.10,或者减少 GPU 上的其他 workload
Service 已 Ready,但接受率接近 0target 和 draft 之间的 tokenizer / architecture 不匹配,或者采样温度过高重新确认 draft head 是否与精确的 target model 匹配;在评估时降低采样温度
在生产 QPS 下 TTFT 或延迟出现回退提议开销不再被空闲的 decode capacity 所隐藏在该 service 上禁用,或减小 num_speculative_tokens;参见 回滚
storageUris 被 API server 拒绝平台上的 KServe 版本早于 storageUris 的引入改用选项 B(Modelcar)或选项 C(PVC)
rollout 期间 service 一直保持 NotReady,或者 Knative 将 revision 标记为 NotReady 并提示 progress-deadline 超时带有 draft artifact 的冷启动比不带时更慢——backbone 和 EAGLE head 的 torch.compile + engine profiling 可能会超过 readiness deadline。我们在 A30 + Llama-3.1-8B 上的 EAGLE-3 冷启动约为 5 分钟Standard mode 下,提高 predictor readiness probe,使其能够容忍 5 分钟启动,例如设置 initialDelaySeconds: 60periodSeconds: 10failureThreshold: 30。在 Knative mode 下,将 serving.knative.dev/progress-deadline annotation 设为 1800s
客户端在 spec decode 下使用 min_plogit_bias 时观察到意外的采样行为在启用推测解码时,这两个参数会被 vLLM 静默忽略(engine init 时会打印 warning)从请求中移除该参数,或者在依赖这些参数的 service 上禁用推测解码

对于 pod 级别的问题,可以使用标准的 inference-service 故障排查命令:

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

注意事项和已知限制

  • 结果会随 workload 形态大幅波动——回退和加速都是真实存在的。 upstream V0 benchmarks 在高 QPS 下报告了 1.4×–1.8× 的变慢。我们自己的 A30 + Qwen3-8B N-gram 测试(参见 内部验证快照 — N-gram)即使在高重叠的 code workload 上也出现了轻微回退。在相同硬件上,Llama-3.1-8B 的 EAGLE-3(参见 内部验证快照 — EAGLE-3)在 code-refactor 上达到了 1.84× 加速,但在 chat 上基本持平(约 0.99×)——同一个 model、同一种方法、同一个 pod,仅仅因为 prompt 形态不同,实际收益就出现了 2 倍的差异。请始终结合你的生产流量 profile 进行验证。
  • N-gram 会禁用 async scheduling。 在最近的 vLLM 版本中,启用 ngram 方法会强制关闭 async scheduling(predictor 日志会显示 Async scheduling not supported with ngram-based speculative decoding and will be disabled)。如果你的 service 依赖 async scheduling 来提升吞吐量,优先考虑 EAGLE-3,或者明确测量这种权衡。
  • storageUris 的可用性。 该字段从 KServe 0.16 开始提供。较旧的平台版本必须使用 Modelcar 或 PVC 方案。
  • Draft head 不匹配不会报错。 与 target model 不完全匹配的 draft head 通常也能正常启动并提供服务,但接受率会非常低。启用后务必检查接受率。
  • 采样参数会影响接受率。 高温度会降低接受率;请使用与生产使用一致的采样配置进行 benchmark。
  • gpu-memory-utilization 预算。 draft artifact(EAGLE-3 head、MLP speculator、draft model)不计入 --gpu-memory-utilization 预算;在添加 draft artifact 时应降低该值。
  • 镜像依赖。 runtime image 必须包含所选方法所需的库。如果某个方法初始化失败,请重建或替换 runtime image——参见 扩展 Inference Runtimes
  • min_plogit_bias 会被静默忽略。 在推测解码下,vLLM 在 engine init 期间会记录警告 min_p and logit_bias parameters won't work with speculative decoding.。传入这两个采样参数的请求仍会收到 200 响应,但参数不会被真正执行——如果你的流量依赖它们,请验证这一点是否符合客户端预期。
  • 与其他功能的组合。 推测解码可以与 tensor parallelism 和 continuous batching 组合使用,但它与 autoscaling 以及 EP / advanced parallelism 的交互会随 vLLM 版本而变化。带有 draft artifact 时冷启动尤其昂贵:在我们 A30 + Llama-3.1-8B + EAGLE-3 head 的实验环境中,predictor 从 container-ready 到 Application startup complete 约耗时 5 分钟(weights load 约 45 秒,draft weights 约 5 秒,torch.compile backbone 约 48 秒,torch.compile EAGLE head 约 17 秒,CUDA-graph capture 和 warmup 约 10 秒,外加约 2 分钟的 engine profiling 和 KV-cache sizing)。readiness deadline 应按这个时间设置,而不是按非推测解码基线来设置:在 Standard mode 下,predictor readiness probe 必须能容忍 5 分钟启动;在 Knative mode 下,serving.knative.dev/progress-deadline annotation 应设置为 1800s。任何 autoscaling 的 scale-from-zero SLO 也应按同样的时间来设定。
  • 输出等价性。 vLLM 表示,推测解码不会改变输出分布。这是 vLLM 的属性,不是 Alauda AI 的保证——如果你的 runtime image 需要严格的等价性,请将其作为验收测试的一部分进行验证。

参考资料