把决策模型推理从 vLLM 换到 Prism:5.3 倍提速的适用边界

ppylox
·10/23/2026·2 阅读

背景:一张工单要判三件事,通用引擎算三遍

客服工单进来,要同时判断三件事:派给哪个团队、多久内必须回复、严重程度几级。三个问题读的是同一份材料,答案都是从有限选项里挑一个。

在 vLLM 这类通用推理引擎上,这三个问题会被当成三个独立请求:同一份材料被分词多次、被调度多次、公共上下文被重复前向计算多次。还有一层更隐蔽的开销在输出端——StartLux-Decision 27B 的词表规模约 24.8 万个 Token,而每题真正需要比较的只有 A、B、C 几个候选项,通用路径却要为整个词表计算 Logits。

量级对得上:单张 H200、27B 模型,同一份材料回答三个问题,vLLM 的 FP8 方案要 92.9 毫秒;一个约 256 Token 的短问题要 53.1 毫秒。工单分类、风险评估、文档审核这类场景调用频次高且高度重复,单次几十毫秒乘上调用量就是实打实的排队时间。据 InfoQ 的报道,StartLux 给出的做法是给决策模型单独配一个推理引擎 Prism,单卡最高提速 5.3 倍。

方案:模型不动,只改推理侧三处

先说否掉的两个方案。

一是不改引擎、只加机器。问题的根源不是算力不够,而是重复计算和无效计算,加卡只会把浪费按比例放大。二是换更小的模型或做蒸馏。决策模型输出的是概率分布,蒸馏直接动到标定,且要重训重评,周期不可控,收益还只是线性的。

Prism 的选择是模型权重完全不动,只改推理流程,共三处:

只算候选答案的分数。 模型仍然完整处理输入材料,但输出层不再对全词表算 Logits,只对有效候选算分数,再在候选集合内归一化。对"选项固定的判断任务",这与全词表打分后重新归一化在数学上等价,省掉的是输出层那部分计算。

共享前缀只算一次。 把一批带公共上下文的任务组织成森林结构:公共指令是根,共同材料是树干,不同问题是分支,配合 Tree Attention 复用公共前缀的 KV。这是加速比的主要来源。

确定性计算。 动态批处理和不同并发下,浮点累加顺序会变,概率出现微小差异;如果最终判定正好卡在决策阈值附近,结果就可能翻转。Prism 把相关 Projection 放到 INT8 Tensor Core 上执行、用 INT32 累加,换取相同请求在不同并发量、批处理方式和 GPU 数量下逐位一致的输出概率。

取舍同样明确:森林结构的收益与问题间共享上下文的比例强相关,共享得少就退化成普通批处理;确定性路径约束了部分数值优化手段;整套优化只对"从有限候选里选一个"的任务成立,开放式长文本生成用不上。部署侧它支持 TP/DP 组合——TP 压单请求时延,DP 抬整体吞吐,按目标配比。

实现要点

第一步是确认你的任务真的只需要候选分数。下面这段可以本地验证等价性,避免上线后才发现概率口径变了(示意代码,非官方 API):

import torch
import torch.nn.functional as F

VOCAB = 248_000                                  # 27B 决策模型词表规模
cand = torch.tensor([id_A, id_B, id_C])          # 取自 tokenizer 的候选 Token

# 通用路径:全词表 softmax,取候选后再归一化
full = F.softmax(h @ W_lm.T, dim=-1)             # [B, 248000]
p_full = full[:, cand] / full[:, cand].sum(-1, keepdim=True)

# 专用路径:只在候选列上计算
sub = h @ W_lm[cand].T                           # [B, 3]
p_sub = F.softmax(sub, dim=-1)

assert torch.allclose(p_full, p_sub, atol=1e-5)  # 恒等式成立才可替换

第二步是把"一个问题一个请求"改造成共享前缀的结构。核心是让公共部分的 KV 只算一次:

root  = tokenize(SYSTEM)                    # 树根:系统指令
trunk = tokenize(ticket_text)               # 树干:公共材料,只前向一次
branches = [tokenize(q) for q in ("负责团队", "回复时限", "严重程度")]

cache = model.prefill(root + trunk)         # 公共前缀的 KV 复用
for br in branches:
    logits = model.decode(cache, br, candidate_ids=cand)

第三步是部署形态。共享比例高、时延敏感的场景优先加 TP;请求量大但对单次时延不敏感的场景优先加 DP。

效果

场景 原 vLLM 方案 Prism 加速
同材料回答三个问题 92.9 毫秒 17.4 毫秒 5.3 倍
约 256 Token 短问题 53.1 毫秒 19 毫秒 约 2.8 倍
单卡整体区间 — — 1.2 至 5.3 倍

精度侧:在覆盖 7 个评测集、共 12,351 次决策的测试中,Prism 平均准确率 92.94%,与 BF16 原始精度下的 92.97% 基本持平。也就是说,这一轮提速没有用可测量的精度损失来换。

复盘

最贵的教训是把 5.3 倍当成常量写进容量规划。这个数字来自三个问题共享同一份材料、且材料占绝对多数 Token 的形态;一旦请求之间没有可复用的公共部分,收益会回落到 1.2 倍这一档。按 5.3 倍报容量、按 1.2 倍兜底,是唯一稳妥的排法。

第二个坑是确定性被当成默认项。决策类服务的判定阈值往往就卡在概率中段,浮点累加顺序一变就可能翻案,事后排查成本极高。上线前应该固定一组请求,在不同并发和不同 GPU 数量下各跑一遍,对比输出概率是否逐位一致,把它做成回归项而不是一次性验证。

第三个坑是候选集管理。候选 Token 从 prompt 里搬到了服务端配置后,选项增删不再经过模型输入,很容易出现线上候选集与评测集不一致。候选集应当版本化,并和模型版本一起发布。

还能怎么改:把候选数设硬上限,超过一定数量的选项直接退回通用路径,避免输出层等价性验证失效;对同一份材料的多任务做合批调度,而不是让业务方各自发请求;把共享前缀命中率、平均候选数、确定性回归结果做成三个常驻指标。加速比不是靠压测报告证明的,是靠这三个指标长期守住。

参考来源

相关文章

评论 (0)

暂无评论,来写第一条吧