上礼拜五快下班那会儿,我的后台监控突然开始报警,P99 延迟一路从平时的八百毫秒冲到了快六秒。我心想坏了,是不是哪段代码又炸了,赶紧拉日志看。结果日志干干净净,一个错误都没有,就是单纯的慢。我盯着那个数字看了半天,突然反应过来——是我手贱,早上把服务的最大并发数从 8 调到了 64。
先说清楚,我说的这个"服务"是指把大模型封装成一个对外能调的推理服务,比如用 vLLM 或者 Triton 这类推理引擎架起来的。模型本身没变,参数没动,就改了一个并发配置,性能就崩成这样。你可能觉得这事有点反直觉——并发开大点,不是应该吞吐更高、大家更快吗?我也是这么想的,结果现实给我上了一课。
这里头有个特别关键的误区:很多人以为推理服务的瓶颈在"算得快不快",其实瓶颈在"怎么把活排队排好"。模型推理不像普通的 Web 接口,你请求一个就开一个线程去处理就行。GPU 上的计算是串行的,一次只能真正跑一个前向过程,你并发开高了,请求全堆在 GPU 上排队等着,谁也没法插队。这就好比一个只开了一个收费窗口的收费站,你把来车从 8 辆放宽到 64 辆,结果就是排队的车从一长条变成一眼望不到头,单车的通过时间反而被拉长了。
我第一次调这个参数的时候,其实就踩过这个坑,但当时没当回事,以为只是偶发。真正让我疼的,是这次线上事故。我把并发从 8 调到 64,本意是想趁着流量上来之前多扛点并发,结果恰恰相反——吞吐不升反降,因为单请求延迟翻了好几倍,坏请求触发的重试又多了一批,雪球越滚越大。有句话怎么说的来着,你在本地最多测出个寂寞,线上高并发一压,那些你以为稳如老狗的东西全现原形了。
那是不是并发越开越低越好?也不是。并发太低,GPU 喂不饱,白白浪费算力。这里头有个平衡点,跟一个叫批处理(batching)的机制强相关。现代推理引擎会在同一时刻把多个请求拼成一个 batch 一起喂给 GPU,这样虽然单条请求还是串行,但多条请求能共享权重加载、共享显存里的KV 缓存(key-value cache,就是模型算过的中间结果),摊下来每条的成本反而低了。所以并发太低吃不到批处理的甜头,太高又把排队搞炸,中间那一段才是黄金区间。
你可能会问,那到底开多少合适?我没法给你一个万能数字,因为它跟你的模型大小、显存、吞吐目标全绑在一起。但有个笨办法挺管用——直接压测,把并发从 1 往上慢慢加,每次盯着两个指标看:一个是 P99 延迟,一个是每秒钟能跑多少个 token(输出token数/秒)。你会发现一个神奇的拐点:在拐点之前,并发上去吞吐也上去;过了拐点,并发再上,延迟开始断崖式上涨,吞吐反而开始回缩。那个拐点,就是你这个服务现实的承载力。
打岔说一句,我那台机器是双卡 A100,内存 80G,听起来很唬人,实际上并发过 16 就开始明显吃紧了。别被显卡的规格唬住,以为显存大就能扛住一切,显存是能装下模型,但不代表能装下同时排队的一堆请求的中间结果。这也是我这次事故里学到最深刻的一课。
现在回头想,我真正缺的不是并发配置,是一套限流和熔断机制。限流(rate limiting)就是提前规定好这个服务一秒最多接多少个请求,超了就排队或者直接拒绝,宁可让部分请求快速失败,也不让所有请求一起卡死。熔断(circuit breaker)更狠,一旦发现连续失败率超过阈值,直接把流量切走,先保服务不崩。这两样东西听起来像是后端工程师的活儿,但你做 AI 推理服务,早晚都得会,因为模型推理比普通接口更"娇气",一堵就是灾难级的。
还有个小坑我得提一嘴,就是压测的时候别用异步客户端的默认配置去测,很多库的重试策略是"失败就重试",在高延迟下会把请求数又翻个好几倍,测出来的延迟是虚高的。我自己就上过这个当,测出来的曲线惨不忍睹,后来才发现是重试把服务又打了一层。
关于吞吐和延迟这笔账,我现在的态度是:别再盯着"并发开到 64"这种数字自我感动了,先想想你的业务到底要的是低延迟还是高吞吐。如果你的场景是客服问答这种对响应时间敏感、但对量要求没那么极限的,那低并发、稳延迟更实在;如果是批量生成、离线跑任务这种不着急要结果的,那高吞吐、能排队才是你该追求的。
那你现在在用的推理服务,并发到底开的多少?你是靠压测拍出来的,还是拍脑袋定的?评论区聊聊你的拐点在哪,我挺好奇的。