拿到“算法-infra协同”这个题目我第一反应是这俩词放在一起本身就是一线研发团队每天都在经历的拉扯。我在算法岗和基础设施岗都待过见过太多因为“算法说infra太慢、infra说算法不会用”导致的无效加班。这篇就把我实际踩过的坑、验证过的打法、沉淀下来的协作框架一次说透。无论你是刚入门的新人还是被跨团队协作折磨的研发这篇都能给你一套能直接落地的思路。1. 内容整体设计与思路拆解1.1 “算法”和“infra”为什么总在吵架先讲一个我遇到的真实场景。线上有个推荐服务算法同学迭代了一版新模型离线AUC涨了2个点特别兴奋地上线。结果呢服务端p99延迟直接从120ms飙到800ms线上指标不升反降。算法同学的第一反应是“infra的机器又抖了”infra同学的排查结论是“你这模型结构太离谱一个请求跑50次循环神经网络谁扛得住”。这种互相甩锅的场面但凡在大厂待过的人都见过。根源在于算法同学习惯把模型当做一个数学函数输入特征输出分数至于这个函数在机器上怎么跑、吃多少显存、耗时多少是“infra的事”而infra同学习惯从资源维度看问题CPU要降、内存要省、吞吐要稳至于模型精度降了0.5个点是不是天塌了是“算法的事”。两边都没错但缺的是一个共同语言。算法-infra协同要做的事说白了就是建立一套“从模型结构到资源消耗”的翻译机制让算法知道每一次结构改动背后的算力成本让infra知道每一次资源限制背后的精度损失。没有这个机制协同只是开会时喊的口号。1.2 协同的本质是围着三类资源转我在实际推动协同落地时习惯把问题收敛到三个维度计算资源、存储资源、带宽资源。任何一个算法模型跑在infra上消耗的无非就是这三样不存在第四种。计算资源好理解CPU的算力、GPU的算力。存储资源包括内存、显存、磁盘模型参数、中间激活、特征缓存都要占地方。带宽资源最容易被忽视但往往是真正的瓶颈——数据从磁盘读到内存、从内存拷到显存、多机之间通信同步梯度每一步都在消费带宽。很多算法同学设计的模型在单机单卡上跑得飞快一到分布式训练就慢成蜗牛问题几乎都出在带宽上梯度同步的通信量把网络打满了。把问题收敛到这三个维度之后协同就从“感觉谁有问题”变成了“量化看哪里紧”。比如算法说模型推理慢了infra不要急着加机器先看是算力不够还是数据加载太慢。如果GPU利用率只有20%加再多卡也没用瓶颈在数据管道如果GPU利用率95%以上那才是真算力不够。这一套量化思维是整个协同模式的地基。2. 核心细节解析与实操要点2.1 性能剖析先量化再优化别凭感觉做事我见过太多团队一上来就优化代码结果忙活一周发现GPU利用率从20%涨到22%毫无意义。真正的第一步一定是性能剖析把时间花在哪、资源用在哪摸清楚。CPU上的利器是perfGPU上是Nsight Systems或者PyTorch自带的torch.profiler。用torch.profiler你很快就能看到一张表里面有每个算子的耗时、显存占用、CUDA kernel执行时间。我印象很深的一次一个注意力模块的耗时占了整个模型推理的43%但参数占比不到2%。我们花了一个下午把它换成了FlashAttention的变体推理时间直接砍掉35%。如果没有profiler数据我根本不会知道注意力是这个模型的命门。做剖析要注意采样条件。不要用太小的batch去测也不要开各种优化开关去测最好的方式是贴近线上真实流量分布。我一般会录一段线上真实请求的副本回放来做剖析这样得出的结论才有参考价值。另外剖析本身会引入性能开销尤其是细粒度kernel追踪会对测量结果产生干扰建议先用粗粒度跑一遍锁定嫌疑区域再对局部做细粒度分析。2.2 算子融合与访存优化少搬一次数据快一大截这里有个很反直觉的经验很多时候GPU计算速度足够快真正拖后腿的是数据搬运。GPU的算力是过剩的但显存带宽是有限的。一个算子把数据从显存读进寄存器算完写回显存下一个算子再读一遍这一来一回的时间可能比计算本身还久。算子融合的思路就是把多个算子合并成一个让中间结果直接留在寄存器或者共享内存里不用反复读写显存。最常见的是把卷积后面的激活函数融进去比如ConvReLU合成一个kernel还有LayerNorm前面那几个均值方差计算也可以合并。PyTorch里可以用torch.compile开启后它会自动做算子融合和图优化我实测在BERT类模型上能拿到1.3到1.6倍的加速改动量几乎为零。就算你没有用编译器的条件手动融合也能做。把多个小矩阵乘法合并成一个大矩阵乘法把BatchNorm在推理时折算进前面卷积的权重里这些手工优化虽然土但在生产环境里稳定可靠。核心原则就一句话数据少搬一次性能就进一步。2.3 数据管道的隐藏瓶颈GPU在等饭还是饭在等锅有一次我在排查一个训练任务GPU利用率只有12%所有人都以为模型结构有问题排查了三天最后发现数据加载线程只有两个磁盘读取队列整天都是空的。模型再强也敌不过“等数据”的命这就像厨师再快备菜的也就一个人菜不上来锅就得空烧。数据管道的设计有几个关键参数预取的数量、加载线程数、数据增强是否放在GPU上做。我的习惯是先用nvidia-smi dmon看GPU利用率和显存拷贝情况如果发现GPU计算和拷贝的吞吐有间隙优先加大预取缓冲其次增加worker数。PyTorch的DataLoader里面num_workers不要盲目调大这个参数要根据磁盘IOPS和内存带宽来定。我有一次把num_workers从8调到32性能反而下降因为线程切换开销盖过了并行收益。更现代的做法是让GPU自己处理数据增强比如用NVIDIA DALI把解码、裁剪、归一化全放在GPU上执行CPU只做最轻量的IO调度。这套方案在图像类任务上效果极好数据管道彻底不再是瓶颈GPU利用率能稳定到90%以上。2.4 模型量化与混合精度用最小精度损失换最大性能提升聊推理优化绕不开量化。FP32的模型直接跑FP16显存占用砍半速度提升一截最重要的是精度损失通常小到可以忽略。这里要提醒一个坑不是所有算子都适合FP16。我碰到过BatchNorm在FP16下数值不稳定最后把BatchNorm留在FP32精度下跑问题才解决。混合精度不是一刀切而是逐层看敏感度敏感层保留高精度其余层用低精度。再进一步是INT8量化。这个步子迈大了容易扯着蛋训练后量化做不好精度能掉3到5个点。我的建议是优先用量化感知训练在训练阶段就模拟量化误差让模型自己适应低精度表示。权重和激活的分布也要提前摸清楚如果发现某些层的激活值范围特别大就得先做截断处理否则量化后的误差会失控。量化和上面说的算子融合不冲突可以先合并算子再用低精度跑效果叠加。我也试过把融合后的模型进一步做知识蒸馏让小模型逼近大模型的精度再对这个精炼后的小模型做量化整个流程跑下来模型体积能缩小到原来的十分之一推理速度提升5倍以上线上精度和原版只差了0.1个点的AUC。这应该算是我在算法-infra协同上做得最漂亮的一个案例了。3. 实操过程与核心环节实现3.1 从零搭建一个可观测的推理服务光讲原理不够我拿一个文本分类服务来走一遍真实流程。需求是模型已经训练好要给线上提供一个HTTP接口要求p99延迟小于100msQPS先按500设计。模型本身是一个中型Transformer直接上肯定超时需要做一系列配合优化。第一步是“裸奔”测基准。我先把模型用最简单的方式包装成一个HTTP服务不做任何优化然后压测得到基线数据。基线大概是单请求延迟210msQPS只能到80GPU利用率最高冲到45%。这个基线数据是整个优化工作的坐标原点后续每一步改动都要拿它来对比。第二步上动态批处理。Transformer这类模型最怕batch为1GPU的并行能力完全发挥不出来。我在服务层加了动态批处理队列请求进来先等1.5ms凑够一批或者在窗口时间到期后一起推理。单这一项QPS从80涨到了320p99延迟反而降到了95ms因为单位时间处理的请求多了排队时间自然短了。第三步做模型量化。把权重从FP32换成FP16延迟再次下降GPU利用率稳定到了70%。到这里p99已经在70ms附近QPS可以打到500了。整个过程用到的工具就是PyTorch的JIT、TorchServe的动态批处理配置以及一个自定义的HTTP网关。3.2 动态批处理的关键参数选择动态批处理听起来简单参数没调好会翻车。第一个参数是最大batch size设太小发挥不出GPU并行能力设太大单个请求要等很久才能凑满反而把延迟拉高。我一般通过压测找拐点把batch从4、8、16、32、64一路测过去找到延迟和吞吐的平衡点。这个模型实测下来batch 32是个甜点。第二个参数是最大等待时间。等待时间设太长交互式场景的体验会崩。我用过一个策略先看线上请求到达的分布如果流量有明显的波峰波谷我就在高峰期把等待时间调低优先保证延迟低峰期调高尽量凑大batch。这个动态策略用下来整体吞吐比固定等待时间高了12%左右。还有一个容易忽略的点动态批处理框架内部要做好padding。因为batch里每个请求的文本长度不一样模型需要统一到相同长度才能并行padding到最大长度会浪费算力。我用的是按长度分桶的策略把长度相近的请求放进同一个batchpadding的比例从45%降到了12%总体延迟又优化了一截。3.3 缓存设计与特征复用把算过的结果存下来推理服务有大量重复计算。同一个用户短时间内多次请求很多中间结果其实是一样的。我做了两层缓存第一层是最终结果缓存同一个输入内容的请求直接返回历史结果用LRU策略控制缓存大小命中率大概28%第二层是特征缓存模型中间的特征向量存下来遇到相似请求直接复用这一个操作让每次请求的推理计算量又少掉15%。缓存不是随便加个字典就行。并发环境要考虑缓存击穿和雪崩。我在网关层给缓存加了分布式锁同一个key同时只有一个请求去真正计算其他请求等待结果。缓存过期时间加了随机扰动避免大批key同时失效把数据库和模型服务打垮。这套东西上线后服务的整体QPS上限从500冲到了900很多请求直接从缓存返回了根本没走到模型那一层。当然缓存会带来一致性问题。模型更新后旧缓存的结果要不要作废我的做法是缓存key里带上模型版本号模型一旦切换所有新旧缓存自动隔离流量灰度到新版本时让旧缓存自然过期不影响新模型的判断逻辑。3.4 弹性伸缩和资源预算Infra的算盘怎么打作为infra这一侧我最关心的是资源成本和稳定性算法同学关心的是效果上限这中间需要一个“资源预算”机制来对齐。我给每个模型服务设了三个档位最低保障、日常水位、峰值上限。最低保障是保证服务不挂的资源日常水位是应对正常流量波动的资源峰值上限是宁可牺牲一点延迟也不允许超出的资源红线。这三个档位的数值不是拍脑袋定的是通过压测和容量预估算出来的。压测得出单个实例能扛的QPS再拿日常和峰值流量除以单实例能力就可以算出最低需要的实例数。我给这个模型服务预留了冗余日常水位是16个实例每秒能扛800QPS峰值流量再翻一倍到1600QPS时有备用资源池可以调度。弹性伸缩的自愈逻辑也很关键。我写了一个简单的算法根据队列积压量而不是CPU使用率来触发扩容。因为模型服务的特点是CPU可能不高但请求已经堵在队列里了。队列积压超过阈值就扩容低于阈值持续几分钟就缩容伸缩的冷却时间设在3分钟避免频繁抖动。这套机制上线后大促期间的资源成本比往年降了大概三成服务稳定性反而更好了一次都没有因为容量不足挂掉。4. 常见问题与排查技巧实录4.1 显存不够用不是加卡先看谁在吃很多算法同学一遇到“CUDA out of memory”第一反应就是换大显存的卡或者开模型并行但其实多数情况下是这个坑显存碎片化或者中间激活没有及时释放。我排查过一个案例模型本身只有2GB参数但显存报错说需要20GB一看就是中间激活的峰值太高。排查工具我推荐nvidia-smi盯实时显存再用torch.cuda.max_memory_allocated看峰值然后用torch.profiler定位到具体是哪个算子把显存顶上去的。常见的解法有几个减小batch size但增加微批次数量用梯度累积来保持整体训练效果不变把中间激活用checkpoint技术重新计算而不是全部保存显存占用能大幅下降代价是训练时间会多出约20%。另外要注意显存碎片。PyTorch缓存分配器有时会出现这样的情况总量足够但空隙太多凑不出一块连续的。遇到这种情况torch.cuda.empty_cache()可以缓解更彻底的是在代码里显式用torch.cuda.memory_reserved()监控发现碎片率过高就把缓存重置一下。实在不行才考虑换卡顺序别搞反。4.2 模型速度时快时慢排查CPU与GPU之间的等待链线上推理服务最恶心的一个问题时快时慢飘忽不定。同样的请求上一秒延迟80ms下一秒变成200ms。这种抖动大多数时候不是模型本身的问题而是CPU与GPU之间的协作节奏乱了。我一般按这个顺序排查先看CPU占用率如果CPU持续打满说明预处理和后处理是瓶颈数据在CPU侧排队再看GPU利用率的波动情况如果GPU利用率一会儿90%一会儿30%说明数据供给不稳定问题在IO或者数据管道如果GPU利用率一直很高但延迟依然抖动那可能是多个实例共享同一块GPU算力被邻居抢占了这时候要么限制每个实例的算力配额要么换独立GPU部署。还要看CPU和GPU的工作深度是不是匹配。CPU负责数据加载和预处理GPU负责计算两者之间需要一个异步队列做缓冲。PyTorch的DataLoader用prefetch_factor来设置预取深度num_workers决定了并行加载能力。调优的目标是让CPU永远比GPU快半拍GPU永远有活干一旦出现CPU等GPU或者GPU等CPU的情况延迟就会开始飘。4.3 推理结果和离线评测不一致精度差异从哪来这是最隐蔽的坑。算法同学离线评测AUC是0.85线上推理结果一验证只有0.81怎么找都找不到原因。这种情况十有八九是训练和推理的数据处理逻辑没对齐或者精度和数值环境变了。数据处理逻辑不一致的例子训练时做了归一化用的是全量数据的均值和方差但线上推理时用的是batch内的统计量训练时用了数据增强线上推理时没关分词器的版本号线上和离线不一样导致同一个句子被切成不同的token序列。这些问题排查起来极其费时间我的建议是从入口开始把线上一个个请求的输入特征dump下来和离线同一批请求的特征做逐字段比对差异会很快浮出水面。数值精度也是一样训练用FP32、推理用FP16大部分case没问题但在极端数值分布下会有偏差。可以分层做精度对比逐层计算输出的余弦相似度找到差异最大的layer再决定是调整量化策略还是把这层保留高精度。我处理过的一个BERT模型就是因为LayerNorm在FP16下精度损失过大导致线上和离线差了0.4个点把那层单独切回FP32就完全解决了。4.4 超时重试导致雪崩一个请求变100个最后一个要重点提醒的问题超时重试引发的雪崩效应。这是个我亲历的事故。线上服务偶发超时客户端加了重试机制超时就重新发一个请求。结果某个实例因为磁盘IO抖动响应慢了1秒触发了大量重试瞬间10倍的流量打过来把服务直接打垮了紧接着雪崩又引发了集群层面的故障。到现在我仍然坚持这样的设计原则重试一定要有上限通常不超过2次且每次重试要有递增的退避时间最好再加一点随机扰动防止所有客户端在同一时间重试。但更关键的是重试必须限制在“不安全的失败”上。如果请求已经到达服务端只是响应超时服务端可能已经执行完逻辑了这个时候客户端重试就可能导致重复写入。要在接口层面做幂等设计用请求ID做去重这个ID在客户端生成服务端在窗口期内只处理一次。脑海里的那根弦永远是任何系统都可能出故障关键是故障出现时不要让小问题滚成大灾难。我见过太多服务不是因为本身能力不够挂掉的而是被各种重试、补偿、回调打了个措手不及。协同工作做得再好如果连最基本的流量治理都没做扎实就像建了一栋装修精美的房子地基却不稳早晚要出事。聊到这里我心里还是有很多感触的。算法和infra的协同说到底不是技术问题是人的问题。我在两边都待过知道算法同学背负着指标压力也理解infra同学肩负着稳定性责任。这两个目标天然有冲突但真正优秀的团队不是让一方迁就另一方而是建立一个能持续翻译和转译的共同框架。我常用的一个笨办法是每周固定一次“性能晨会”两边坐在一起把上周上线的模型跑一遍profiler逐层过耗时和资源谁的问题谁认领下周二来复盘。这个会议每次都很有价值很多冲突都被消灭在萌芽里了。再分享一个小技巧给算法同学一个自助的性能诊断面板让他们在上线前自己就能看到自己模型的资源画像不用每次来问infra。这个面板后来成了我们团队的标配算法同学上线的自觉性高了很多很多明显的问题在开发阶段就被发现和优化掉了根本走不到上线的环节。我觉得这就是协同的最高境界——不是解决问题而是让问题根本不会出现。