做推荐算法的同学应该都有这种体验:改了一版特征或者换了个损失函数,代码里偷偷加了几行实验标记,然后趁午休把训练任务塞进集群,下午开会的时候偷偷刷新页面看loss曲线。模型跑完还要手动记metric、截图存在聊天记录里,隔两周想复盘的时候翻遍聊天记录也找不到当时到底跑了哪个版本的数据、哪份Embedding才把GAUC提了这么多。
我在某公司做搜推广算法的时候,就是被这套“一键启动、手动记账、凭记忆复盘”的糊涂账流程搞得烦了,才决定自己做了一套内部平台,后来逐步打磨成了我这边一直在用的工业推荐实验基础设施,代号叫Auto-RecSys。这名字听起来有点唬人,但说白了就三件事:让实验异步跑起来不相互阻塞、把每次实验的所有记忆永久存下来、让离线实验和在线反馈形成两个环路的持续进化。这篇文章不聊理论,就聊聊我做这套系统时遇到的真实问题、关键设计取舍,以及落地之后踩过的那些坑。如果你也在折腾推荐实验平台、特征工程管理、模型迭代效率,应该能从中找到点可复用的东西。
1. 为什么工业推荐实验一定要异步化:从"轮流上车"到"各跑各的"
推荐算法团队最常见的场景是:五六个算法同学共用一组GPU或者一套训练集群,每个人手里都有两三个实验想法等着跑。如果实验是同步串行执行的——一个人的任务占用着卡,其他人在那里排队等——整个团队的迭代效率就被卡死在最慢的那个任务上了,这是最典型、也最容易被忽略的问题。
1.1 同步执行模式下,资源和人力到底被浪费在哪
同步执行的痛点是层层叠加的,不是表面上“排队等GPU”那么简单。首先,训练任务本身就分阶段:读取样本、特征工程、模型训练、评估验证,这几个阶段对资源的需求差异非常大。读取样本和特征工程多半是I/O密集加少量CPU计算,真正吃GPU的是模型训练那一小段。如果串行执行,那些I/O密集阶段的碎片时间全被浪费了,GPU利用率可能只有30%到50%,团队里谁都做不了别的事。
更隐蔽的问题在评估阶段。一个模型训练完了,还得在验证集上做批量预测,然后算一堆指标。这个评估过程用的是CPU和内存,但同步串行的架构下,它仍然占着训练分配的GPU资源不释放——名义上你在等“训练任务结束”,实际上训练早就结束了,卡被评估流程白白占着。
我做过一次排查:有个同学训练一个DeepFM模型用了45分钟,其中纯训练过程只占28分钟,剩下的时间全部耗在特征预处理和评估上。也就是GPU真正干活的时间不到三分之二。这个数字在好几个任务里反复出现,所以我当时就意识到,先能把训练流程拆细,本身就值一个系统级的优化。
1.2 异步执行的关键:把一个大任务切成可独立调度的四段
我做Auto-RecSys第一版架构的时候,没有去搞雷声大雨点小的大数据平台,而是先把训练链路按四个阶段切成子任务,每个子任务独立申请资源、独立调度:
采样和样本拼接、特征生成、模型训练、评估与指标产出。这四段可以流水线式衔接,也可以让多个实验的不同阶段同时跑在集群的不同节点上。
举一个实际运行的例子:实验A已经跑完了特征生成,进入模型训练阶段;这时候实验B刚刚提交,它可以立刻在另一台节点上启动样本采样和特征生成,不需要等A的卡释放。实验A结束训练,进入评估阶段,它可以调度到CPU节点上跑;同时实验B的模型训练就可以抢占训练资源。异步化之后,同样的一批GPU,单位时间能跑完的实验数量大概是同步执行时的2.3倍左右。这个数字是我们的batch调度日志里统计出来的,不是直觉估算。
1.3 异步调度不只是队列,我需要一套"状态机"来管理实验生命周期
异步执行光靠一个“提交任务然后轮询日志”的简单队列会出很大问题——一旦某个阶段失败,整个实验要从头开始吗?卡在中间等待资源,任务被更高优先级的实验挤掉了怎么办?这些都迫使我把实验的生命周期显式建模成一套状态机。
在Auto-RecSys里,每个实验的状态至少包含:submitted(已提交)、data_ready(样本就绪)、features_ready(特征就绪)、training(训练中)、evaluating(评估中)、finished(完成)、failed(失败)、killed(被终止)。每个状态都有对应的超时阈值,比如training超过预计耗时2倍就触发告警,evaluating超过6小时自动判死。状态保存在数据库里而不是内存里,这样集群重启、调度器宕机,都不会丢实验进度。
这个设计帮我解决了两个一直很头痛的运维问题:第一,实验到底跑到哪一步了,不用再盯着GPU进程猜,直接看状态板;第二,中途失败可以不重头跑,从失败状态的依赖上游开始恢复。比如特征已经生成好了,训练网络崩了,重启之后直接从training开始,省掉重算特征的时间。这些都是看起来很“基础”但实际运行中极其救命的能力。
1.4 资源争抢的调和:优先级、配额、抢占
异步执行带来了一个新问题:多个实验同时跑,资源怎么分?我用的不是复杂的公平调度算法,而是两个朴素的机制。
一个是队列优先级。每个实验提交的时候会打标签:试验性(exploration)和主力性(exploitation)。试验性实验的优先级低,资源空闲时跑,一旦主力实验进来就要让位。这个策略是因为主实验通常是线上迭代的关键路径,不能让一堆探索实验把它堵死。
另一个是资源配额。每个算法同学有一个配额上限,比如最多同时占用16个CPU核心和2块GPU。这样某个人提交了连环实验,也不会把全团队的资源都吃干净。这套东西比想象中难做的点在抢占上:一个低优先级实验训练到一半,被高优先级任务抢占,保险的做法是先把模型checkpoint落盘,再释放资源,不要让被抢占方从头训练。我花了两个迭代版本才把这个临时保存与恢复的流程做稳。
2. 持久记忆:为什么你的实验总是"做完就忘"、无法复盘
聊完异步执行,接下来要说的“持久记忆”是我觉得Auto-RecSys里真正能拉开和随便写脚本跑实验的做法之间差距的地方。很多团队其实不缺“跑得快”的引擎,缺的是“记得住”的能力。
2.1 实验记忆的三个层次:代码、数据、结论的全面存档
推荐实验的复盘成本,往往不在于代码找不到,而在于三样东西同时丢失:代码对应的数据版本、训练超参数、最终的评估指标和样本外表现。我见过太多实验在复盘时只能找回部分信息——代码在Git仓库里,但训练时用的样本快照是哪个日期生成的,特征工程代码对应哪个commit,模型输出路径是什么,早就找不到了。
Auto-RecSys做持久记忆,就是把这三层信息统一固化到一套“实验档案”(Experiment Profile)里。每次实验完成,系统自动生成一个档案:
代码层:记录训练脚本、模型结构定义、特征配置的源码commit哈希,以及运行时的环境依赖锁定文件(相当于把整个虚拟环境也记下来)。
数据层:记录本次实验使用的样本起始日期段、特征版本号、数据采样比例、随机种子。
结论层:记录模型在验证集上的所有评估指标、导出模型的路径URL、预测结果的文件索引,甚至包括线上AB实验是否开启、观察窗口多长。
这套档案是自动写入的,不需要工程师自己动手填写,只要实验跑完,就永远存在。这一点对比原来“手动在聊天群里记录实验效果”的方式,简直是从蛮荒时代一步跨入了文明时代。
2.2 为什么不能只靠MySQL一张表完成任务
做第一版的时候,我以为把实验元数据和指标存进MySQL就够了。跑了两个礼拜就发现撑不住:
第一,模型文件本身没法塞进MySQL,只能存路径。但路径是挂在分布式文件系统上的,如果存储节点被清理或者回收站被清空,路径就成了死链。第二,指标和参数的形态太丰富了——有的实验记录的是GAUC、AUC、召回率、精确率,有的还要记录预算消耗、平均响应延迟。如果都塞进一张宽表,表结构会越来越臃肿,改一次要动Schema,非常痛苦。
后来我把存储拆成了三层:关系型数据库存实验元数据与状态;对象存储存模型文件、特征快照、评估报告这类大文件;实验档案对应一条JSON格式的“指标摘要”塞进文档型数据库。每次想查某个实验组的历史效果,不需要去翻文件系统,直接在文档库里按指标名过滤就行。
为了加快查询,我还把实验档案做了一层布隆过滤器索引。比如你要找“所有用特征版本V3跑过、GAUC超过0.67的DeepFM实验”,布隆索引能在几毫秒内筛掉90%以上的无关实验,再对剩下的几十个做精确查询。这在实验积累到几千个之后特别有用。
2.3 回溯与复现:不只保存参数,还要保存"当时的运行环境"
只是把参数和指标存下来,还不足以让实验“复活”。我在实际复盘时遇到的惨痛教训是:有一条看起来效果很好的实验记录,But参数倒是都在,代码commit也锁定了,但当时训练用的Python环境版本和现在完全不一致——依赖库升级后,代码已经跑不起来了,数据也不一定能兼容。
所以Auto-RecSys在做持久记忆时,额外固化了两样东西。
一样是运行环境镜像。每次实验启动前,系统把当前依赖环境打包成一个容器镜像,实验结束后这个镜像留一个只读快照。镜像的哈希值会和实验档案关联起来。这样一来,任何时刻想重新做这个实验,只要拉对应镜像,代码和依赖版本100%一致。
另一样是数据版本引用的完整性校验。实验开始前,系统会计算训练数据集的哈希值,和这个数据集关联的特征工程脚本哈希一起写入档案。复盘的时候可以先用哈希校验原始数据还在不在、有没有被篡改过。这个设计灵感来自数据版本控制的思路,但我不需要每次全量复制数据,只要数据源没变,引用关系就能一直有效。
这套机制让我在后面做“双循环进化”的时候省了无数力气,因为每一次进化迭代都站在可回溯的确定状态上,而不是踩在流沙上。
3. 双循环进化:离线优化循环和在线反馈循环怎么联动
很多人以为推荐系统的自动进化是一个“离线训练模型、上线效果好就赢、效果差就回滚”的朴素过程。其实在工业界,理想的迭代不是单线程的“改模型→上线→看效果”,而应该拆成两个同时运转、互相喂数据的闭环:离线优化循环和在线反馈循环。Auto-RecSys把这两个环接到了一个叫做“进化通道”的机制里。
3.1 离线优化循环:批量的实验评估与候选模型池的生成
离线循环处理的是“在没有风险的情况下,不断尝试新的推荐策略”。它的输入是所有已保存的历史实验档案和离线评估指标,输出则是一个“候选模型池”。
Auto-RecSys每周会自动从实验档案里筛出满足硬件条件下线的候选模型,条件是:在验证集上GAUC提升超过当前线上模型0.5个百分点以上,稳定性通过(多个随机种子下指标方差低于阈值),并且通过了C端延迟压测。这个池子的建立不需要人工干预,系统会为池里的每个模型重新完整跑一遍统一的评估协议,确保指标口径一致。
在这个循环里,异步执行和持久记忆的价值完全体现出来了。因为有记忆,系统可以追溯每个实验在历史上的真实效果,而不是靠某个人说“我记得当时好像涨了点”;因为有异步执行,每周自动评估几十个候选模型不会把集群资源占死,评估任务的调度可以穿插在正常训练任务的间隙里。
3.2 在线反馈循环:AB实验和流式反馈的采集
在线反馈循环是和线上流量真正打交道的那个环。跑AB实验的时候,模型预测日志、用户实际点击、曝光、转化行为,每一刻都在产生数据。Auto-RecSys的在线管道做的是实时收集这些反馈,按实验维度做统计,并且实时更新一个“当前线上趋势指标”看板。
这个环的关键设计是“反馈的即时性”。AB实验的sample比例、观察窗口、最小样本量都被系统自动控制。比如某个实验等到累计样本达到50万才开始算统计显著性,而实验开跑的前48小时只允许观察趋势、不允许人工拍板,避免早期抖动干扰判断。
在线反馈不只是用来判断“要不要全量”,它还要回流到离线循环里当新的训练信号。线上用户的实时行为日志会被周期性地整理成新的训练样本集,这些新样本再被离线训练下一个版本。这就是双循环闭环的核心数据通路:离线产出的模型,在线验证;在线的行为反馈,又变成离线更新的原料。
3.3 双循环耦合时的防震荡机制
双循环一听很美好,真正落到系统里,最怕的是震荡。离线模型更新太频繁,在线模型就来回跳,用户感受的推荐结果会忽变;反过来,在线反馈信号抖动剧烈,下一轮离线训练就会被脏数据带偏。
我给Auto-RecSys加了两个防震荡的缓冲闸。
一个是发布闸门:候选池里出来的模型不能直接全量上线,必须先走10%流量观察24小时,再走30%流量观察48小时,最后才能摸到100%。整个过程中每个阶段都有明确的指标阈值,任何一个阶段不达标就自动回滚。这个机制很钝,但足够稳。它把“进化”的效率牺牲了一点,换来了线上系统调度的确定性。
另一个是反馈去噪:在线行为日志在做离线训练数据之前,会经过一个延迟归因校正模块。比如一个曝光在第三天产生了转化,这个信号需要窗口回补到训练集里。如果系统不做延迟归因,频繁拉训练集只覆盖当天的实时转化,模型的预估就会偏低(转化被截断),导致下一轮迭代方向整体偏移。这个是我吃过亏才加上的,不加之前离线模型越训越保守,点击率居然一再掉点。
这两个缓冲闸让双循环能比较健康地转起来,不会变成一个互相扰动、最终发散的混沌系统。
3.4 双循环进化的一个实际案例结果
说一个我们内部很典型的例子:有一个基于注意力权重的粗排模型,通过离线循环自动评估进去候选池,然后走在线AB,第一轮10%流量时GAUC和线上持平,但响应延迟多出4毫秒,发布闸门拦住了它。我们没有人工干预,系统自动把这次候选标记为“延迟敏感,需优化”,把这份信息反馈回离线循环。
下一个迭代版本里,离线侧自动降低了注意力层的维度,把模型推理图做了算子融合,再次进入候选池。第二次AB只跑了50万样本,CTR提升0.4%,延迟恢复到差距小于1毫秒,自动放量到30%,24小时后平稳,最终全量。整个过程里我做的工作就是查看自动生成的实验档案、看了一眼前置条件是否满足,剩下的流程系统自己走完了。
可能有人觉得全自动进化听着有点唬人,但实际上每一步都在记录、在存档、在可控阈值内做有限度的调整。
4. 落地过程中踩过的坑与最终的架构取舍
写到这里,我想把整套系统从设计变成可运行工程的过程中,那些后来回想起来特别有价值的问题再展开讲一讲。很多设计说的时候头头是道,一旦面对真实的并发、脏数据、集群抖动,马上就会露出马脚。
4.1 异步任务优先级被抢占导致的连锁问题
异步调度上线第三周就出过一次事故。一个主力模型训练任务在抢占一个实验任务的GPU时,被抢占的实验没有做checkpoint保存的避让逻辑,直接被杀掉了。因为这个实验已经跑完了特征生成了整个数据集,它的“上游产物”全部失效,重新提交差不多要重新花一个小时做特征工程,而且那部分特征计算的成本也不便宜。
那次事故之后,我给调度器增加了一条规则:任何抢占动作发生前,被抢占方有最多60秒的“善后时间”用来落盘checkpoint和状态标记。同时,每个阶段之间的中间产物统一放在共享对象存储里,编号加上计算哈希,保证即使原任务没了,别的任务如果依赖同样的特征产物,可以直接复用。
异步化首先追求的不该是“挤多少效率”,而是“抢占时不要砍断依赖链”。把中间产物视作第一公民来管理,这一点我觉得是异步执行能够健康运转的底座。
4.2 持久记忆的数据膨胀与淘汰策略
实验记忆一旦开始累积,存储量增长得非常快。几千个实验每个都存一个容器镜像和特征快照,几百GB是很轻松的。如果不干预,很快存储成本就会高到让平台失业。
Auto-RecSys的解法分两层。
一层是分级存储:热实验档案(最近90天内活跃的)放在高速对象存储,冷实验档案(超过180天没被引用的)自动归档到低成本的冷存储,并且删除模型镜像本体,仅保留代码锁定信息和元数据。保留“复现路径”但不保留“复现全量”,对大多数实验来说足够了——毕竟你很少会回头重跑一个半年前失败的探索实验。
另一层是档案压缩:实验的日志文件按天做增量合并,计算指标时只保留主要统计量(均值、方差、分位数),抛弃原始时间序列明细。这样查询历史结论依然很快,但存储量从每月几十GB降到几百MB。压缩策略实施之后,整个记忆模块的存储增速降低了70%左右。
4.3 全自动进化里唯一需要人工把关的节点
虽然前面强调了很多自动化能力,但有一件事我自始至终没有做“全自动”——就是模型的流量发布。线上推荐的任何改动本质上影响的是真实用户的行为,所以即便双循环已经把候选测试过一轮又一轮,最终发布到100%流量之前,仍然需要一次人工确认。
Auto-RecSys的做法是停下来在系统里生成一份“发布审计单”,列出候选模型和线上模型在最近一周内的指标对比、离线评估的实验档案链接、候选模型依赖的数据集版本和特征版本哈希。算法负责人确认这份审计单无误后,系统才会继续执行放量流程。
这个设计不是技术上的妥协,而是工程治理上的取舍。自动化可以把效率推进到极致,但“责任”这个环节没法自动化。倒不是说机器不能做判断,而是万一线上出了问题,团队需要有一个明确的决策节点,知道“这一步是谁放过的”。对任何做工业系统的团队来说,可追溯性比追求极致自动化的边界感要重要得多。
4.4 关于模型评估口径统一:不统一口径,双循环就成了两套话语体系
最后说一个最容易忽视、却影响深远的坑:离线评估和在线反馈的指标口径如果不统一,双循环的联动就会断裂。我在早期犯过这样的错——离线评估用的是全局随机抽样的验证集,线上反馈用的是用户维度的时间切片,两者算出来的GAUC基准都不一样,候选池的筛选标准和在线AB的评判体系对不上,结果就是离线觉得“这个模型很好”,在线AB却把它毙了。
Auto-RecSys后来的做法是把离线评估协议模仿在线环境:验证集按用户和时间的联合分布抽出,评估流程固定用和线上一致的特征算子,连预估延迟都做成仿真模式。每次实验离线评估完,系统还会额外输出一个“在线预估值”,用来推算AB实验里大概能观察到的效果上限。这么做之后,候选池和线上反馈的结论才逐渐开始互相印证。
如果你准备搭一套类似系统,我建议把“评估口径一致”放在第一位去设计,它直接影响着你那两套循环能不能对上话、进化机制有没有意义。
5. 这套系统能给你带来什么,以及有那么一两个时刻的小窍门
讲了这么多架构和机制,最后还是想分享几个实际操作中的体会,以及这套系统落到日常研发流程之后的变化感。
以前一个实验从提交到拿到完整结论,顺利的话一天左右,不顺利的折腾三五天也是常事。现在Auto-RecSys里从提交实验到自动生成实验档案,平均耗时在6小时左右,这个时间不依赖任何人力盯守,半夜提交第二天早上醒来就能看到指标和候选资格评估。释放掉的时间和心力,才是这套基础设施真正值钱的地方。
双循环进化的机制我也不能说它有多么惊艳的AI自动调参效果——它本质上更像是一套纪律严明的流程自动化,把一个无序迭代变成有序积累。经历过的人才会明白,算法团队真正稀缺的不是灵光一现的点子,而是把点子变成线上效果的那条稳定的流水线。
如果只让我给一个“小窍门”——那就是在实验档案里一定要存“失败原因”字段。不要只存指标结果,把实验为什么失败(比如欠拟合、特征泄漏、延迟敏感、评估口径差异、数据质量问题)用标签形式记录下来。我加了这个字段之后,三个月内就积累了二十几种失败模式,这些失败模式反过来成了候选池筛选项的重要参考,也帮你少走重复探索的弯路。
这套系统我还会持续打磨下去,双循环的离线侧目前还在探索更优的自动化搜索空间方式,在线侧想做更细粒度的分人群反馈回流。如果这篇经验能帮你把推荐实验的流程往前推进那么一小步,那就已经很值得了。