loading

Loading

探索与发现

首页 AI 开发实战

请求合并与去重:省算力与抖延迟之间的账

字数: (2331)
阅读: (4)
0
摘要:从批量推理的吞吐收益曲线写到幂等键去重,讨论合批与去重在延迟、显存和重试幂等上的取舍,并给出只有部分前提下的工程判断门槛。

合批与去重:先想清楚你到底在合什么

请求合并与去重在推理服务架构里常被当成一个词说,其实是两条完全不同的路径。合批(batching)解决的是“把同一次模型前向能算的东西放在一起算”,它提升的是 GPU 的利用效率,换来的是单条请求多等几毫秒到几十毫秒;去重则反过来,同类请求只算一次,后面的直接复用第一次的结果。判断能不能合并,看的是延迟预算;判断能不能去重,看的是业务对“完全一致答案”的容忍度。

线上经常混在一起做的情形是这样:数十个客户端在同一秒内拿着同一段系统提示、只差最后一个字的用户消息请求生成,这时自然想到能不能把 prompt 部分复用、把只差的那几位放进同一个 batch。vLLM 文档里写得很直接,PagedAttention 的核心动机之一就是面对“请求长度高度不均、并发持续变化”的负载时,把每个请求各自增长的 KV Cache 从粗放预分配转成可按块共享、可回收的资源。这是写少而固定的块级共享,不是自动按内容免费去重。如果直接在 prompt 层拿前缀比较做 hash 查表,就成了业务侧的额外功能,必须自己控制。

合批的收益曲线:5-8 倍和 32 条一组的条件

资料里给出的一个代表性数字是 batch size 从 1 提到 32,吞吐量增长 5-8 倍。这个量级在 decode 阶段并不离谱,注意力部分的收益主要来自 matrix-vector 推进变成 matrix-matrix 的分块计算。但这串数字很容易被误读:增长的前提是同样的请求被同时凑齐,而且没有任何一个被等待拖得过了头。

静态批处理的问题在这里暴露得最彻底。哪怕模型参数已经固定,padding to longest sequence 会让短请求一直趴在 batch 里等效于空跑,GPU 的算力有相当一部分打在 PAD token 上;更长的影响是延迟上界变成无界的——只要一条超长请求进了同批,剩下所有短请求都要等到最长那条完成。现在的框架都是动态批处理,vLLM、SGLang、NVIDIA Dynamo 都实现了,触发条件有两种:一条是等你把批次凑满到给定 token 数,另一条是计时器到期 max_batch_delay_ms。后者给出一条有限的延迟上界,但反过来,QPS 不够高的峰值口段,计时器每次到期触发都是小 batch,GPU 实际利用率也就那样。

这是合并这件事没法无偿支付的部分。真实情形里该关心的是 p95/p99 会不会因为定时器多出来的每批 2 到 10 毫秒出现平台台阶,而不是抄批量写到多大的参数表。我自己更倾向让写侧明确上报等待耗时,让监控看请求在批缓冲区里到底驻留了多久。

去重不是免费的:它在一条调用链上有多重身份

请求去重的直接起因不在于性能,在于请求真的会重复。腾讯云开发者社区那篇文章里列出的“四大典型重复场景”——重复提问、超时重发、批量任务重试、同一会话内重复提交——恰好对应 idempotency-key 系列实现里经常走的坑。最常被提到的一个数字是计时不同步导致的概率性穿透,这里更需要注意的是推理耗时通常从几百毫秒到几秒,客户端 timeout 往往只有 60 秒甚至更短,在超时宽限窗口内它会重发同一段内容。

对这种情形正确的做法是引入带状态的幂等键,并且把生命周期拆成 INIT 、PROCESSING、SUCCESS、FAILED 四段:请求带着 X-Idempotency-Key 到达,服务端先用 SETNX 占住状态;命中 PROCESSING 等状态一律不进入新一轮推理,否则并发两条相同的请求会各自拿走一份 GPU 的同时,先后两次写后槽;命中 SUCCESS 就直接返回缓存的完整生成结果。资料给出的一个参考是结果缓存只要带 expiration 到覆盖客户端重试周期,就不再触发计数。

跨实例部署时不能走进程内字典,Redis 是能拿来做单个逻辑锁和共享状态的结果。不过分布式锁也要注意:常见预设“锁超时见设置成 30s、比最慢一条推理还久”意味着更大的 tail 请求会在推理仍未收回时把这条锁续给下一次重试。我现在更倾向同时保留短锁作请求对齐、长过期作为写入屏障。批量场景使用子任务 ID 二层键也是来自同一个模板:batch_id 下 seq_id 的表,“已成功 + 已有输入才唯一”可以逐条重入,执行调度跳过 SUCCESS 的子请求。

把两类变更收敛到统一调度器来做

实际落地时最常见的失败情形是业务侧用一个模块负责去重键,而拼批发生在另一个 vLLM / Triton 层甚至上游网关。这样被幂等标记拦下来的请求等待结果时会占到批内存的消耗而不是重入推理。我们的折中是反着来:让去重表只作为合成后的公共池增量索引,也就是说没有用统一键 token 匹配时不做状态推进;一旦命中就直接在批执行器一侧塞进重用引用位置,不让业务侧对提示文本做 sql_variant 之类的库检索。维护窗口更被接入了连续的指标:命中和错过的比率、命中后时延是否包括超时,都打 trace。

合还是不合,比的是一个成本等式

合并增加等待、提升吞吐;去重跳过重复的预填充成本,但会产生缓存存储带出的不可重算责任,还会因过期时间不适合碰上结果不再有效的时刻。评测的价码很容易写:如果 max_batch_delay_ms 上的成本边际划算就开,如果显存吃紧就退回只保留 int8 缓存回退。对我来说先把事件打开、比对每条命中,再看合批实际以 merge_weight × prefix_match_percentage − normalized_load 打相对分,比读一整套机制简单。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/ai-dev/672.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图