从vLLM迁移到SGLang后,发现同一模型、同一输入下输出的概率分布不一致,这是推理引擎数值实现差异导致的常见现象。精度对齐验证的目标不是“证明两个框架完全一样”,而是建立一套可重复的比较流程,确认差异是否在业务可接受范围内,并且定位差异来源是否来自可控制的参数设置。
两套推理引擎对同一模型权重的前向计算路径不同,logits出现小幅度差异属于正常现象。验证核心是控制变量:锁定模型权重、输入、dtype、采样参数和随机种子,逐层比较logits、概率和采样结果。若仅是个别浮点尾差,通常不影响指标;若出现明显分布偏移,需要优先检查dtype、batch size、attention后端和cuda graph配置。以下是一个可复现的对比验证流程。
为什么两套引擎会产生差异
vLLM和SGLang都实现了高性能推理,但底层算子融合方式、attention实现(如flash attention版本)、KV cache管理、并行策略以及CUDA kernel的数值累加顺序并不一致。这些差异会导致logits在浮点精度上出现小幅度偏移,尤其是使用FP16或INT8量化时。另外,两套框架对sampling参数的解释和执行顺序在边界情况下也可能不同,比如temperature为0时是否直接走贪心搜索、top_p的过滤逻辑等。因此,不能假设迁移后概率分布能严格逐位一致。
精度对齐验证的具体步骤
- 锁定基线:导出同一份模型权重(确保SHA256一致),固定输入文本和tokenizer版本。
- 统一参数:设置相同的dtype(如float16或bfloat16)、same max_tokens、temperature、top_p、top_k、repetition_penalty等。
- 获取原始logits:在vLLM和SGLang中都使用“logits输出”接口,而不是直接获取采样后的token,因为采样结果受随机种子影响,不能说明分布本身是否一致。
- 计算统计指标:对同一prompt、同一长度下生成的logits矩阵,计算最大绝对差、平均绝对差、KL散度或余弦相似度。
- 重复多组输入:使用不同长度、不同主题的prompt,观察差异是否稳定。
影响一致性的常见因素
- dtype:FP16与BF16的指数位和尾数位不同,差异可能出现明显偏差。建议先统一采用相同dtype。
- batch size:部分推理引擎在batch size不同时,会切换不同的kernel实现,导致结果不同。验证时固定batch size,单独测试。
- attention后端:vLLM使用的flash-attention版本和SGLang的attention实现可能不同,可通过设置后端或禁用flash attention来排查。
- CUDA Graph和torch.compile:这些优化可能触发算子融合,改变数值计算顺序,造成微小差异。
- 张量并行策略:并行时AllReduce的累加顺序不同,也会产生差异。如果使用多卡,先单卡对比。
可复制的对比脚本骨架
以下是核心对比逻辑的Python示例,具体调用方式需要替换为两个引擎实际提供的接口。
import torch
# 伪代码:加载同一权重路径的模型
# model_vllm = create_vllm_model(model_path, dtype="float16")
# model_sglang = create_sglang_model(model_path, dtype="float16")
# 固定输入(实际使用tokenizer将文本转成input_ids)
input_ids = torch.tensor([[1, 2, 3, 4, 5]])
# 获取logits(需使用引擎的logits接口)
logits_vllm = model_vllm.get_logits(input_ids) # shape: [1, seq_len, vocab_size]
logits_sglang = model_sglang.get_logits(input_ids)
# 计算逐元素差异
diff = (logits_vllm - logits_sglang).abs()
print("max_abs_diff:", diff.max().item())
print("mean_abs_diff:", diff.mean().item())
# 计算KL散度
probs_v = torch.softmax(logits_vllm, dim=-1)
probs_s = torch.softmax(logits_sglang, dim=-1)
kl = (probs_v * (probs_v / probs_s).log()).sum(dim=-1).mean()
print("kl_div:", kl.item())
# 还可以比较top-k token是否一致
topk_v = torch.topk(logits_vllm, k=10, dim=-1).indices
topk_s = torch.topk(logits_sglang, k=10, dim=-1).indices
print("topk_equal_ratio:", (topk_v == topk_s).float().mean().item())建议增加一个循环:对至少20条prompt重复执行,记录每次的max_abs_diff和KL散度,观察是否存在偶发或系统性偏差。如果所有输入的max_abs_diff都在0.01以下(具体阈值需根据模型和业务自行设定),且采样结果在实际任务中表现无差异,可以认为对齐通过。
验证清单和风险边界
- 迁移前先备份旧引擎的离线推理日志,作为回归基线。
- 验证时要区分“logits分布不一致”和“采样结果不一致”。采样结果还受随机种子影响,应先对比logits。
- 如果业务依赖温度大于0的随机采样,建议在线上切换前,用新旧引擎分别生成相同数量的样本,统计业务指标(如准确率、人工评分)是否在可接受范围内。
- 不要追求logits逐位一致,更合理的指标是topk token重合率以及下游任务效果差异。
常见问题
差异多大才算正常?
对于FP16推理,不同引擎间的logits平均绝对差在0.01量级通常正常;若达到0.1甚至1以上,基本可以确定存在配置或实现层面的实质性差异。建议观察top-1/top-5 token重合率,若重合率超过95%,一般不影响具体任务。
是否需要让两套引擎完全一致?
一般不需要,也不现实。只要差异不影响业务判断阈值或最终指标,就可以接受。如果发现明显偏差,优先检查dtype、attention后端和采样参数是否对齐,必要时可以禁用vLLM或SGLang的某些优化选项(如enable_cuda_graph=False)做对比,以判断差异来源。