做高并发AI系统的时候,最容易踩的第一个坑就是——默认把“高并发”当成纯运维问题。大家忙着加机器、上调度、扩副本,折腾几天发现QPS还是上不去,机器倒是吃满了。有一次我们大促前压测,一个模型服务的P99直接冲上800毫秒,我翻监控一看,模型推理本身占了整个接口时延的八成还多。问题根本不在网关,也不在负载均衡,就在模型自己身上。
当时我们的目标很典型:既要顶住高并发流量,又要把模型塞进边缘设备里,还不能牺牲太多准确率。这个目标光靠堆显卡满足不了,因为边缘设备跑不动大模型,单次推理的时间决定了整个链路的极限。于是我们把重心从“买机器”转到了“让模型更轻”,做的核心工作就是量化、剪枝,再加上边缘部署架构的重构。这篇文章,算是把这一整套完整实践过程、选型逻辑和踩过的坑一起写出来。适合正在做AI推理服务、边缘计算、部署优化的团队参考,不管你是刚摸到门路,还是已经上过线,里面都有能直接落地的东西。
先说结论:模型压缩不是单纯“减小体积”的手段,它直接影响的是并发能力、时延水位和部署成本三件事。接下来我按实践顺序讲,从定位瓶颈开始,再到量化剪枝的具体操作,然后是边缘部署,最后是压测和复盘。
1. 先定位瓶颈:高并发AI系统里真正的延迟包袱在哪
1.1 一个典型的高并发推理链路长什么样
我们当时做的是一套面向多路内容请求的智能分析服务,流程大致是:请求进来 → 网关鉴权 → 参数解析/预处理 → 模型推理 → 结果后处理 → 写回缓存。这个链路在高并发下,最容易先出问题的不是模型,而是上游的线程池和下游的连接池。但等我们把线程数调优、连接复用都做完之后,瓶颈依然压得很死——因为无论上游多顺,模型推理这一步是串行的,它算多久,这个请求就得等多久。
那段时间我们做了很多次压测,观察到的现象很典型:并发升到某个值之后,平均时延开始线性上涨,再往后就变成指数上涨,服务直接雪崩。表面看像是流量太大扛不住了,实际上是因为模型单次推理时延太长,线程资源被长时间占住,新请求排队越来越久。所以第一件事不是再扩容,而是把模型单次推理的时间砍下来。
1.2 快速定位瓶颈的方法:看三张曲线
定位瓶颈不需要上太复杂的工具,把三张曲线拉出来就够了。
第一,看GPU利用率。如果GPU利用率持续很低但接口时延还在涨,说明推理没被打满,瓶颈在数据预处理、传输或者服务框架本身。第二,看接口时延拆解。把“CPU预处理耗时”“模型推理耗时”“网络耗时”分开埋点打点,直接看哪一段占比最高。我们当时埋点之后发现模型推理占时延的80%以上,方向就明确了。第三,看显存和内存占用。显存如果长期逼近上限,说明模型太大了,并发多副本都塞不进,这时候光调代码没用,必须让模型本身瘦下来。
在第三代硬件环境或者边缘设备上,还要额外看算力利用率。边缘设备算力有限,很多时候模型算力需求远超设备供给,反而带宽和光靠并发调度也没用,必须做端侧模型轻量化。
1.3 为什么不优先选择堆机器
有人会问:既然模型这么大,多加几台带大显存的机器不就完了吗?这个思路在并发量小的时候成立,但到了高并发场景,问题就变成了“单次推理成本”。
举个例子:一个模型单次推理要200毫秒、占显存3GB,一台机器最多同时跑两个副本,QPS天花板就摆在那。你加十台机器,成本和运维复杂度全上去了,但单副本能力没变。而如果把模型体积降一半、推理时延降到80毫秒,同样一台机器可能同时跑四个副本,成本可能省下六成以上。对于边缘部署更是如此,设备是固定的,型号是固定的,能跑什么模型完全由硬件上限决定,不在模型层面压缩,部署方案都不会成立。
所以我们的路线很明确:先做量化,再做剪枝,让模型在单次推理的算力开销和内存占用上都降下来,然后再去做边缘部署的高并发编排。
2. 量化落地:先选位宽再定校准方案,别一上来就签INT8
2.1 位宽选型:FP16、INT8、混合精度怎么取舍
量化本质上是用更少的比特去表示模型里的权重和激活值,从而减少内存带宽占用、提升计算吞吐。很多人一聊量化就直接上INT8,但“尽量低位宽”不一定是最高效的选择。
我整理了三种常用方案的适用场景:
| 方案 | 精度损失 | 加速收益 | 适用场景 |
|---|---|---|---|
| FP16 | 基本无损 | 约10%-20% | 大模型跑在支持半精度算力较强的设备上 |
| INT8动态量化 | 中小模型可接受 | 约1-2倍 | 白天在线服务、边缘设备内存紧张 |
| INT8静态量化+混合精度 | 需要调校 | 约2-3倍 | 高并发服务、延迟敏感场景 |
| 4bit/混合低比特 | 精度风险高 | 约3-5倍 | 实验性质,或配套强蒸馏重建 |
当时我们走的是“FP16先落地,INT8再优化”的两阶段方案。第一步先确认FP16能让模型在现有硬件上完整跑起来,把显存占用压下去。第二步再针对推理热点算子做INT8量化,同时对少数特别敏感的层保持FP16。这种做法比直接全量INT8稳妥得多,尤其在没有大规模调参经验的时候,能少踩很多坑。
2.2 PTQ量化落地的几个关键动作
后训练量化(PTQ)看起来简单——把模型权重替换成低比特表示就行,但实际部署时误差很容易爆炸。我们踩出来的经验是:校准集的选择比量化算法本身更重要。
校准集必须和线上真实输入的分布一致,而且要覆盖数据的“边界情况”。比如内容推荐模型,线上会出现深夜的异常流量模式,如果校准集里全是白天的正常样本,量化模型在深夜流量下就可能输出奇怪结果。
我当时用的是大概1200条样本做校准,覆盖了正常样本、长尾样本和几个极端输入。校准时的统计工作大致如下面这个示意流程:
# 伪代码:PTQ校准的完整思路 model.eval() calib_loader = build_calib_loader(valid_data, count=1200) # 记录每一层的激活值min/max,以及权重分布 observer = collect_activation_and_weight_ranges(model, calib_loader) # 选择per-channel或per-tensor粒度的量化方式 quant_spec = make_quant_spec( activation_bits=8, weight_bits=8, per_channel=True, per_tensor=False ) # 对非sensitive层做INT8,对敏感层保留FP16 quantized_model = apply_ptq_quantization( model, quant_spec=quant_spec, sensitive_layers=get_sensitive_layer_names(model) )还有一个非常容易被忽略的点:量化不是把所有权重一视同仁。某些层对数值扰动特别敏感,比如最后的分类层、embedding层,直接压到INT8可能让指标掉得很明显。那时候我们采用的方式是“混合精度量化”,把敏感层标记出来,维护在一个名单里,用FP16跑,其余层INT8。名单不是猜的,是用敏感度分析跑出来的。
2.3 QAT感知训练:什么时候值得花这个成本
PTQ便宜但不是万能的。如果模型量化后准确率掉得超过3%,又实在没办法替换结构,那就得走量化感知训练(QAT)。
QAT的思路是让模型在训练阶段就适应低比特带来的扰动,把量化误差通过反向传播“消化”掉。它的成本主要在训练时间上,通常要多训练几个epoch,还要改训练脚本里插入伪量化节点。我们判断要不要用QAT,一般看三点:一是PTQ后指标跌幅是否不可接受;二是模型是否需要长期迭代,如果后面还要反复优化,QAT的一次性成本可以被摊薄;三是团队有没有足够的调参时间。
如果决定做QAT,有个经验是初始学习率要降到原来的十分之一左右,否则伪量化节点的梯度噪声很容易让模型震荡。我们实际跑下来,QAT之后模型的准确率能回到PTQ之前的95%-99%,高并发场景下的推理速度还保持量化后的水平。
2.4 量化之后必须做的评测和回滚机制
很多人量化完只看一个指标——模型跑通了没。这远远不够。我们会在量化前后各跑一遍同一套评测集,对比准确率、F1或者线上核心指标。这个评测集不能太小,至少几百条,否则统计噪声会掩盖真实的精度变化。
另外就是做好版本化。我们当时把“原始模型”“FP16模型”“INT8混合精度模型”三个版本都打上了标签,存在模型仓库里,线上出问题时可以一键回退。高并发系统最忌讳的是上线后发现模型坏了,又找不到原始版本,那只能干瞪眼。
在评测过程中我们遇到过一个反直觉的现象:量化后单看准确率只掉了0.2%,但线上用户反馈变差了。后来排查发现,掉的那0.2%集中在长尾用户上,而长尾用户更敏感。所以评测不能只看平均数,要把按用户群拆分的指标也跑一遍。
3. 结构化剪枝:通道和注意力头的取舍逻辑与精度回补
3.1 非结构化剪枝为什么不适合高并发场景
剪枝的目标是去掉模型中的冗余连接。但剪枝还分两种:非结构化和结构化。非结构化剪枝是把权重矩阵里不重要的单个数值清零,模型参数量确实是少了,但它变成一堆稀疏参数。通用推理引擎对稀疏参数的支持很有限,稀疏度上去了,推理速度却不一定提升,甚至因为访问不规则还可能变慢。
高并发和边缘部署要的是“实打实的计算量下降”,而不是“理论参数量下降”。所以我们做的都是结构化剪枝——按通道、按层、按注意力头为单位整块移除,让模型结构和算子在底层天然变少,推理引擎不需要特殊优化也能直接提速。
打个比方说明两者的差别:非结构化剪枝像在墙上每隔几块砖抽掉一两块,墙表面看没什么不同,但你要重新粉刷时根本没法施工;结构化剪枝则像直接拆掉一整段非承重墙,施工效率高,改动也干净。
3.2 剪什么:通道剪枝和注意力头剪枝的判定方法
在CNN结构里,最常用的判定维度是BN层的缩放系数。BN层里的γ参数直观反映了对应通道对输出结果的影响程度,γ越小,说明这个通道的信息量弱,剪掉后影响也相对小。流程我们就用四步:训练一个完整模型 → 统计各通道γ的绝对值 → 按阈值剪掉低贡献通道 → 微调恢复精度。
以我们项目中的一个视觉模型为例,剪枝后直接砍掉约25%的通道,单次推理时延降了32%,准确率只掉了0.4%,微调后基本拉回原水平。
对Transformer结构,注意力头是更值得下手的目标。多头注意力机制本身有冗余,有些头学到的模式和别的高度重合。判断方法是用一组代表性样本跑一遍模型,统计每个注意力头的激活值与输出相关性,把长期低激活、低相关的头整组移除。这里有个关键点:不要只剪“看起来不重要”的头,要看你剪完后剩下头的组合是否还能覆盖足够多的特征。我们曾经一口气剪了30%的头,结果模型精度掉得比预想严重很多,后来改成逐层少量剪、每剪一步就跑一次评测,才稳住。
3.3 剪枝+微调的正确顺序
剪枝之后必须微调,这个没有讨论余地。但顺序问题容易被搞错。我们的正确做法是:
- 用完整训练集训练原始模型到收敛,记录其效果。
- 基于统计指标选出要剪的通道/头。
- 一次性剪枝(尽量避免剪一次调一次,因为每次都改变优化目标很混乱)。
- 接着用原始训练数据进行若干轮微调。
这里还要补充一个经验:微调时不能用太小的学习率,否则模型很难适应结构变化。我一般设置成原始训练学习率的10%-20%,然后观察验证集指标,如果前两轮不降反升,说明学习率合适;如果降了,就停下来把学习率再降。
3.4 用蒸馏补回剪枝损伤的实战方法
剪枝后精度回不来,很多人第一反应是更长时间训练。其实更好的办法是知识蒸馏:让完整模型当“老师”,剪枝后的模型当“学生”,学生不仅学真实标签,还学老师的输出分布,通常几轮就能明显提升。
有一个我们坑过的地方:蒸馏的时候老师模型必须用“剪枝前的原始模型”,而不是“剪枝后再训练复原的模型”。听起来理所应当,但我们的同学在实现时误把老师设成了当前剪枝后的模型,蒸馏了等于没蒸馏,浪费了一整天的训练时间。老师模型的输出要在每轮前固定下来,做成离线张量缓存,能省很多重复前向计算。
量化与剪枝合起来用的时候,顺序也很重要。我们推荐先剪枝再量化。因为剪枝会改变模型中各通道的数值分布,先量化再剪枝会导致量化统计的分布被后续变化破坏。先剪后量,校准分布是最终结构的分布,效果更稳。
4. 边缘部署架构:多设备编排与缓存设计把并发撑起来
4.1 模型压缩之后,部署层面才能谈高并发
模型体积从几百MB降到几十MB,单次推理时延从几百毫秒降到几十毫秒,这才有了做高并发边缘部署的底气。但实际部署时还会遇到另一堆问题:设备多、型号杂、算力分布不均,怎么分配流量、怎么做缓存、怎么处理模型版本更新,这些都是模块级的事情。
我们当时的边缘部署环境是“一台中心调度节点 + 多台边缘推理节点”的结构。中心节点负责任务分发和汇总,边缘节点只做推理。每个边缘节点部署同一个量化剪枝后的模型副本,靠业务侧的负载均衡把请求打到不同节点上。这套架构下,并发能力的上限就是“单副本QPS × 副本数 × 可用率系数”,模型的单次推理越快,同等硬件下的并发天花板就越高。
4.2 批处理与异步推理:把设备利用率拉满
边缘设备的算力不大,但并不意味着必须一次推理一个请求。我们用动态批处理把多个请求合并成一次推理,设备利用率能提升一大截。核心是在“等待攒批”和“及时响应”之间做权衡。延迟太敏感就不能攒太久,我们把攒批窗口控制在5-10毫秒,最多攒16个请求一次推理,效果非常明显。
伪代码如下,思路很容易迁移:
# 示意:动态批处理核心逻辑 async def handle_request(req): batch = await batch_queue.collect( max_wait_ms=8, max_batch_size=16 ) results = await model_infer(batch) return results[req.index]异步推理也对并发帮助很大。请求进来后先入队,推理线程不会阻塞等待每个请求完成,而是批量消费队列,完成一批再统一返写。高并发下用异步消息队列缓冲瞬时洪峰,比每个请求同步占用一个线程要稳得多。
4.3 缓存策略:重复请求的隐性成本
高并发系统里,重复计算是最浪费的一件事。我们在边缘节点上加了缓存层,缓存的是“输入特征 → 模型输出”的一一映射,或者更节省点,只缓存高频top结果。对于内容推荐类的场景,热门内容被反复请求的概率很高,命中率能达到20%-30%,等于凭空省下了这部分推理。
缓存还要注意失效策略。不能因为缓存让用户一直看到旧结果。我们用“时间窗口+分数微调”的做法:给每条缓存设一个有效期,过期后该条请求重新走完整推理,新结果再写回缓存。这样一个渐变刷新的机制,既能扛突发流量,又能保证结果不太旧。
4.4 边缘设备适配:算力、内存、带宽的三重约束
部署阶段还有一个容易低估的环节:模型压缩了,但关键运算算子未必在每个设备上都高效。比如INT8量化在一些芯片上有专门的加速指令,在另一些芯片上反而比FP16还慢。所以模型放到边缘设备前,必须先在目标设备上做基准测试,确认INT8算子的实测速度。
内存上也要盯住。边缘设备的运行内存一般不大,模型文件、运行时、中间结果三者会互相抢内存。我们直接把模型文件通过“轻量化容器”的只读层挂载,避免重复加载和复制。带宽方面,模型更新包不能太大,量化剪枝后我们的更新包从几百MB缩到了二三十MB,边缘设备在做模型热更新时基本感觉不到影响。
还有一个我们觉得很有用的细节:把模型拆成“主模型+辅助分支”两部分,主模型部署在边缘端,需要大量算力的辅助逻辑,比如复杂形态的排序和筛选,尽量放到中心侧。边缘端只做最核心的判断和决策,这样单设备压力小,整个集群吞吐也更容易平衡。
5. 压测与调优:用P99和显存曲线决定能否上线
5.1 压测指标不能只看平均时延
高并发系统压测时,最阴险的是平均值掩盖了大量慢请求。业务上我们最关心两个指标:QPS(每秒能处理的请求数)和P99(99%请求的耗时都低于某个值)。P99反映的是极端条件下的体验,如果P99飙到800毫秒,平均时延再好看,用户流失也挽回不了。
除了时延,还要观察显存曲线在长时间压测下是否缓慢增长。如果显存在不断攀升,多半是推理框架内部有张量或队列没有释放。边缘设备上还会看温度,设备过热降频,QPS会突然掉一截,这类问题在纯云环境压测里根本发现不了。
5.2 一次量化剪枝后压测的完整过程和结果
拿我们当时一个召回模型来举例。压测前先做设备校准:把量化剪枝后的模型部署到边缘节点,跑30分钟小流量,观察日志里有没有异常输入,确认无误后再逐档加压。
压测分成三档:低并发(模拟日常流量)、中并发(模拟活动流量)、高并发(模拟峰值流量)。每档跑10分钟,记录QPS、P99、平均时延和显存峰值,然后休息5分钟观察显存是否回落。最终数据大致如下:
| 模型版本 | 单副本QPS | P99时延 | 显存占用 |
|---|---|---|---|
| 原始FP32模型 | 28 | 620ms | 2.8GB |
| FP16 | 39 | 430ms | 1.6GB |
| INT8混合精度 | 86 | 190ms | 0.9GB |
| 剪枝+INT8 | 121 | 108ms | 0.7GB |
这个结果验证了我们的方向:模型单次推理快,P99显著下跌,相同显存下能跑更多副本。高并发能力不是靠堆机器堆出来的,而是靠单次推理成本降下来的。
5.3 上线之后常见的性能回退排查链路
压测过了不代表上线后永远稳定。我们维护了一份排查手册,遇到性能回退按顺序查:
- 先看时延有没有伴随显存曲线异常,如果有,查推理框架的缓存和连接池。
- 再看是某一台设备变慢还是所有设备变慢,如果是单台,查设备是否过热降频或已被其他任务抢占资源。
- 如果P99上涨但平均时延变化不大,重点查批处理是否失效——请求模式突变导致攒批超时率上升,每次推理都在“等batch”,拖累了尾部时延。
- 最后排查模型输入分布是否和校准集偏差过大,尤其是量化模型,输入分布漂移会引起激活值范围估计不准,结果就是推理结果质量下降。
这个链路我们反复用过很多次,基本上能覆盖九成以上的线上性能问题。
6. 复盘清单:量化与剪枝失败的常见原因和正确顺序
6.1 “量化一定掉点”是最大的误区
好几次分享时有人问我:你们量化后精度不降吗?我的回答是:不一定。量化掉不掉点,主要看你有没有做对三件事。校准集和真实分布是否一致,敏感层是否得到保留处理,以及评测时有没有区分长尾。很多团队量化后掉点,不是量化本身有问题,而是校准集太随意,或者一刀切全INT8。我们做过一次粗颗粒度的分析实验:同一份校准集,只调整覆盖范围,量化后的精度差距就有1.8个百分点。可见“掉点”未必是量化的本质,而是操作细节的代价。
6.2 剪枝前先看算子热点分布
剪枝别按“直观感觉”来。得先知道哪些算子或层真的占了大量计算时间,剪掉它们才能带来可感知的速度提升。我们第一次剪枝时,把精力放在参数量最多的层上,结果参数量掉了很多,时延没怎么降。后来一分析,慢的其实是一些计算密度高的算子,不是因为层参数多,而是因为反复被调用。调整思路后,先做profiling,按算子耗时排序,再针对耗时占比最高的层做剪枝和结构改造,效果立刻不一样。
6.3 高并发边缘部署的容量规划清单
最后给大家一份我们内部在用的容量规划清单,直接按项目套就行:
- 预估峰值QPS:按日常10倍流量以上估算,别太乐观。
- 单副本能力:取压测P99稳定下的最大QPS,别用理论值。
- 副本数:峰值QPS ÷ 单副本QPS,再乘以1.3-1.5的冗余系数。
- 设备型号异构:如果边缘设备有高算力和低算力两档,分别测不同档位的单副本能力。
- 模型更新节奏:预留热更新窗口,更新时对新版本做灰度验证。
- 显存冗余:至少保留20%余量,防止推理中间张量临时上涨导致内存溢出。
这一套下来,从模型压缩到边缘部署,再到并发支撑和压测验证,整条链路就算闭环了。我个人在带这几次项目后最大的体会是:模型压缩和高并发架构不是两件事,而是一件事。量化让模型更快,剪枝让模型更轻,部署架构让流量更均衡,压测让上限更清楚。先搞清楚瓶颈在哪,再决定动哪里,比拿着一堆热门技术名词往上硬套要靠谱得多。如果看完想跑通一遍,建议你先拿一个小模型,把量化、剪枝、部署、压测整个流程走一遍,踩一遍坑以后,对这个体系的体会会完全不一样。