如果只为了提升高并发下的 speculative decoding 吞吐量,num_lookahead_slots 不是先调大的参数,而是先要确认它是否匹配当前批量调度和显存预算。它控制草稿模型一次为每个序列生成多少个候选 token;在高并发下,这个值既影响投机序列的潜在收益,也影响每条序列占用的 KV cache 和参与调度的序列数。没有固定最优值,同一套参数在不同模型、不同并发数和不同草稿模型下表现可能完全不同。
先确认瓶颈:观测指标
调整前先判断,当前吞吐量受限是因为投机接受率不足,还是因为实际批大小不够大。建议记录四类指标:
- 端到端吞吐量:每秒完成的请求数,或每秒输出的 token 数;
- 投机接受率:草稿 token 被目标模型接受的比例;
- 实际参与推理的序列批大小,不等于客户端并发数;
- 显存占用,尤其是 KV cache 部分。
如果并发数很高,但服务日志显示每步合批的序列数被压得很低,瓶颈往往在 KV cache 或调度器;如果接受率本来就不高,继续加 num_lookahead_slots 只会增加每轮的无效计算和显存占用,吞吐量不会跟着上升。
基线测量:固定其他变量
先跑一轮基线,其他参数保持线上配置不变。vLLM 的启动参数名称会随版本变化,启动前先用 vllm serve `--help` 确认当前版本是否支持 `--num-lookahead-slots`。用一个相同请求集,每次只改这个值。
# 基线:lookahead 设为 0
vllm serve /path/to/target-model `--speculative-model` /path/to/draft-model `--num-lookahead-slots` 0 `--max-num-seqs` 128 `--port` 8000
然后依次把 num-lookahead-slots 改成 4、8、16、32,跑同一组请求。每次启动后先等显存稳定,再开始压测。并发数、输入长度、输出长度都保持一致,否则无法对比。
最小观测脚本
如果只想看端到端吞吐,可以用下面的 Python 脚本对公开的 OpenAI 兼容接口发请求。注意它只用于对比趋势,不是压测工具。
import threading
import time
import requests
from concurrent.futures import ThreadPoolExecutor
url = 'http://127.0.0.1:8000/v1/completions'
payload = {
'model': 'local-model',
'prompt': 'Write a short story about a lighthouse.',
'max_tokens': 128
}
def one_request(_):
start = time.time()
try:
requests.post(url, json=payload, timeout=30)
except requests.exceptions.RequestException:
pass
return time.time() - start
with ThreadPoolExecutor(max_workers=32) as executor:
latencies = list(executor.map(one_request, range(256)))
print('requests:', len(latencies))
print('avg_latency:', sum(latencies) / len(latencies))
调整顺序:一次只改一个变量
- 先确认基线可用,再用较小值起步。建议从 0 到 32 或 64,按 4 或 8 的步长逐步增加。
- 每跑一档都同时观察显存和吞吐量。如果显存占用上升明显,且每秒完成的请求数没有同步上升,就该停在当前位置。
- 如果发现批大小下降,先降低
`--max-num-seqs`,而不是继续加大 lookahead slots。 - 如果多档之间吞吐量几乎没有差异,说明接受率或模型解码策略是主要瓶颈,调这个参数不是下一步动作。
风险边界:这个参数不单独决定吞吐量
num_lookahead_slots 调大,意味着目标模型在每一步要并行验证更多位置,KV cache 的预分配会跟着增加。高并发下,这会让可同时驻留的序列数下降,反而拖低吞吐量。它不是越大越好。
同时,投机解码的效果依赖采样的微小差异。如果目标模型使用温度较高或采样算法变化较大,草稿模型的接受率可能很低,此时调大 lookahead slots 相当于做更多无效工作。需要结合环境确认,没有跨模型通用的推荐值。
判断是否落地:验证清单
参数调整是否真正有效,至少满足这几条:
- 同一组请求跑多次,端到端吞吐量的波动在可接受范围内,而不是偶然一次高值;
- 调参后请求失败率没有升高,超时等待没有变长;
- 显存峰值不超过当前实例的可用上限,并且为 KV cache 预留了余量;
- 并发数进一步提高时,吞吐量仍能继续增长,而不是立刻回落。