前途科技前途科技
  • 服务
  • 关于
  • AI
    • AI 大模型
    • 具身智能
    • 算力芯片
  • 科技
    • 智能终端
    • 软件·互联网
    • 汽车·出行
    • 科学前沿
  • 资源中心
    • 深度研究
      • AI 前沿
      • 教程
      • AI 知识库
      • 案例研究
    • 行业报告
      • 白皮书
      • 行业报告
      • 研究报告
      • 技术分享
      • 专题报告
    • 精选案例
      • 金融行业
      • 医疗行业
      • 教育行业
      • 零售行业
      • 制造行业
  • 服务
  • 关于
联系我们
教程/09.18 · 00:00/14 MIN/作者:苏晚/0 阅读

LoRA 让异步 RL 跨机只传几 MB:一个桶加一个代理,再靠三处设置提速 3.9 倍

Hugging Face 用 TRL v1.14 的 AsyncGRPOTrainer 只同步 rank-1 LoRA 适配器,靠一个挂载进每个 Job 的存储桶和一个按 KV 前缀路由的代理,在没有 NCCL 的 HF Jobs 上跨机跑通异步 GRPO。架构只决定能不能跑:500 步从 3 小时 27 分缩到 53 分钟,靠的是打包、关梯度检查点、放开在途上限这三处设置。

本文基于 Hugging Face 的 Amine Dirhoussi、Quentin Gallouédec、Kashif Rasul、Sergio Paniego 于 2026 年 9 月 10 日发表在 Hugging Face Blog 的《Async GRPO with LoRA across HF Jobs: a bucket, a proxy, and no NCCL》,原文链接:https://huggingface.co/blog/asyncgrpo-lora-hfjobs。复现代码在 https://github.com/AmineDiro/hfjobs-lora-buckets。对照部分引用了同一团队 5 月的《Delta Weight Sync》:https://huggingface.co/blog/delta-weight-sync。

一个 Job 只有一台机器,异步 RL 怎么跑

TRL 的 AsyncGRPOTrainer 把训练和生成拆开:训练器和 vLLM 推理服务可以放在不同机器上,各跑各的速度。这在单机或集群里很好办,两边要么共享文件系统,要么能组一个 NCCL 通信组。

Hugging Face Jobs 不一样。一个 Job 本质上是一台虚拟机上的一个容器,单节点最多 8 张 H200,而且一个 Job 没法再拉起多个节点。Job 之间不能跨节点通信,没有共享本地盘,更没有共享的 localhost。团队给自己出的题目是:如果不再要求训练器和推理服务在同一个节点上,能走多远?

如果每次同步都要搬整套权重,答案是"走不远"——每次更新都是几个 GB 的数据跨机器搬运,这本来就是密集集群里 NCCL 的活。转机来自 LoRA。随着 PR #7017 合入、TRL v1.14 发布,AsyncGRPOTrainer 可以只训练一个 LoRA 适配器,并且只把适配器同步给 vLLM。一个 1.5B 模型的 rank-1 适配器只有几 MB,而完整模型约 3 GB。

rank-1 够不够用?原文引用了 Thinking Machines 的《LoRA Without Regret》:在策略梯度类 RL 里,LoRA 能追平全参微调,rank 1 也可以。理由是优势函数每个 episode 只提供大约 O(1) 比特的信息,从信息总量看,每一步本来就没多少东西可学,rank-1 的容量足够装下。

另一个系统层面的好处是:vLLM 能同时挂多个适配器。旧的 rollout 用它开始时的策略跑完,新的 rollout 用最新策略,两者互不干扰。

最后搭出来的系统很小:

  • 一个训练器 Job,跑带 LoRA 的 AsyncGRPOTrainer(外加 FSDP);
  • 两个 vLLM Job,各自服务基座模型加上训练器最新发布的适配器;
  • 一个 Storage Bucket,以同一路径挂载进三个 Job,适配器靠它从训练器走到推理端;
  • 一个代理,负责按 KV 缓存路由 rollout 请求,并把适配器更新广播给所有副本。

同步通道:一个桶,加一次原子重命名

新的"只同步适配器"路径是这样工作的:训练器不向 vLLM 发送任何张量。每隔几个优化器步,它把适配器存到 <output_dir>/.vllm_lora/trl-policy-v{N},用一次原子重命名把目录发布出去,再把这个路径 POST 给 vLLM 的 /v1/load_lora_adapter。vLLM 从磁盘读文件,之后 rollout 就可以请求 model="trl-policy-v{N}"。

这其实就是 vLLM 运行时加载适配器的原生方式:接口收的是路径,不是张量,所以本来就默认训练器和推理服务共享文件系统。在 Slurm 集群上,那是网络文件系统;在 Jobs 上,团队用 hf-mount 把 Storage Bucket 以 POSIX 文件系统的形式挂进每个容器,而且挂在相同的绝对路径:

hf jobs run ... -v hf://buckets/aminediroHF/asyncgrpo-lora-buckets:/lora ...

TRL 和 vLLM 一行都没改。训练器往 /lora/<run>/.vllm_lora/ 写,推理端从同一路径读,POST 请求里的路径在每个容器里都有效。检查点和最终适配器也存在桶里,Job 是一次性的,但被抢占的训练器可以从桶里恢复。

为什么每个版本单独起名。 另一种设计是只保留最新适配器、永远用同一个名字发布。团队没这么做,因为 vLLM 的前缀缓存是按适配器名做键的。名字不变,旧权重算出来的 KV 块在换权重之后仍然能命中,prefill 不会重算,一条 rollout 可能前缀来自上一个策略、解码来自下一个。训练器察觉不到,唯一的症状是 ratio 慢慢偏离 1。带版本号的名字从根上排除了这种情况:一个名字永远只对应一套权重。

适配器槽位怎么算。 槽位数由 max_staleness 决定。每次权重同步,策略版本号加一;max_staleness 是一个样本最多能落后当前策略几个版本还不被丢弃。设为 4 时,训练器在 v7 仍会用 v3 生成的样本,而在 v3 下开始的 rollout 也必须能在 v3 下跑完。所以 vLLM 任何时刻都要同时服务当前版本加前面四个,共 5 个。每次同步先加载新版本、再卸载最旧的,交接瞬间还要多一个槽,于是 。 换成一般规律:。

苏晚
苏晚Su Wan

前途科技 · 前沿主笔

关注 AI 与前沿科技的观察者,前途科技前沿内容主笔。

查看全部文章

想了解 AI 如何助力您的企业?

免费获取企业 AI 成熟度诊断报告,发现转型机会

置顶文章

LoRA 让异步 RL 跨机只传几 MB:一个桶加一个代理,再靠三处设置提速 3.9 倍
置顶

LoRA 让异步 RL 跨机只传几 MB:一个桶加一个代理,再靠三处设置提速 3.9 倍

//

24小时热榜

OpenAI发布Astra for Law:面向法律行业的GPT-6模型
TOP1

OpenAI发布Astra for Law:面向法律行业的GPT-6模型

英王查尔斯召集AI巨头闭门会谈
前途科技前途科技
服务关于快讯技术商业报告
前途科技微信公众号

微信公众号

扫码关注

Copyright © 2026 AccessPath.com, 前途国际科技咨询(北京)有限公司,版权所有。|京ICP备17045010号-1|京公网安备 11010502033860号|隐私政策|服务条款
--max-loras 6
只给 5 个的话,vLLM 会在每次同步时悄悄驱逐一个还有 rollout 在跑的策略。
max-loras = max_staleness + 2

推理端用的是原版 vllm/vllm-openai 镜像,版本钉在 v0.27.1,需要打开两个环境变量:VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 启用运行时加载接口,VLLM_SERVER_DEV_MODE=1 启用 TRL 需要的 /pause、/resume、/server_info。原文特意提醒,vLLM 迭代很快,版本号应当视为配方的一部分。

还有一个容易漏的检查点:TRL 初始化时会调 /server_info,看到 lora_config 才走只同步适配器的路径。DoRA、modules_to_save、rank 超过 --max-lora-rank 这类 vLLM 不能直接服务的配置,会带着一条警告退回到合并权重同步。日志里应该能看到 Adapter-only vLLM sync enabled——没有这行,后面所有"几 MB"的前提都不成立。

代理:数据并行得往上挪一层

训练器和 vLLM Job 之间需要一个代理,原因有两个。

第一个很朴素:Job 暴露的端口要求每个请求带 Authorization: Bearer <HF token>,代理负责加上,TRL 不用知道。

第二个更关键。想让多张 GPU 一起生成,单个 vLLM 的常规做法是 --data-parallel-size > 1,但 TRL 在这种模式下拒绝只同步适配器,而且理由充分:一次 /v1/load_lora_adapter 调用只会到达应答它的那个 DP rank,其他 rank 会继续拿基座模型冒充新策略。所以数据并行只能挪到上一层,由一个能把适配器加载"扇出"到每个副本的组件来做。

团队在训练器 Job 里起了一个跑在 127.0.0.1:8000 的代理,让 TRL 把它当成一台 vLLM。从 TRL 看过去,这是一台 data_parallel_size=1 的服务器,所以它会选择只同步适配器。

广播是全有或全无。 每个副本有自己的桶挂载,看到新目录的时间不一定相同。No adapter found for <path> 通常意味着某个副本的挂载还没跟上,只重试这一个副本;遇到其他错误,就把已经接受适配器的副本全部卸载回滚,保证一个策略名不会只存在于部分副本上。/pause、/resume 和卸载请求同样广播;/health 要所有副本都健康才返回 200。

按 KV 前缀路由。 GRPO 对同一个 prompt 会发 G 个请求,这里 G=8。如果它们落在同一副本,第一个请求算完 prefill,后面七个直接复用;轮询分发的话,一半请求会落到没缓存的副本上,白白重算。

代理的路由器模仿 vLLM 的做法,把 token 切成 16 个一块,只对完整块做链式 blake2b 哈希:第 3 块的哈希代表第 1 到 3 块的整体,这正对应因果注意力下 KV 的依赖关系。哈希链的种子是适配器名,所以同一个 prompt 在 v4 下永远匹配不到 v3 留下的缓存。

难点在"公共前缀"。所有请求都以相同的聊天模板和系统提示开头,这个数据集里是前 23 个 token,1,460 道题都一样。原文提到,换到 Agent 场景,公共前缀是工具描述;多轮环境里则是共享的对话历史。这些块几秒内就会进入每个副本的缓存,拿它们做匹配,每个新 prompt 看起来都像命中。路由器的办法是看"扇出":一个块如果被所有副本服务过,或者后面接过不止一种后继块,它就是公共的,不参与路由判断。去掉公共前缀之后:

  • 某个副本有这个 prompt 专属的块,且它比最空闲的副本多出的在途请求不超过 8 个 → 发给它(affinity 命中);
  • 有专属块但超出 8 个 → 放弃缓存,发给最空闲的副本(spill);
  • 没有副本有专属块 → 新 prompt,发给最空闲的副本,平局轮询(unmatched)。

跑完 64,728 次 rollout,代理计数是 affinity 54,712、spill 820、unmatched 9,196。每个 prompt 8 次 rollout,至少有 1 次必然是冷的,理论下限 12.5%;实测 unmatched 14.2%,affinity 84.5%,spill 1.3%,已经很接近天花板。

团队原本担心 Python asyncio 写的代理会成为瓶颈,实际没有:在途请求最多 128 个,每次路由只算几个哈希,一个线程绰绰有余。

252 次加载,没有一次一步到位

全量跑的同步耗时,原文给了一张拆分表:整段同步中位数 8.5 秒(早先的版本是 30.8 秒),其中暂停两个副本 0.3 秒,适配器 all-gather 加写入桶 1.1 秒,两个副本接受适配器约 7 秒。126 次同步乘以 2 个副本,252 次加载全部成功——其中 6 次在第二次尝试时成功,246 次在第三次。

这组数字原文没有展开,但我认为它是整篇最值得多看一眼的地方:**没有一次加载是第一次就成功的。**适配器只有几 MB,写入桶只要 1 秒出头;真正的时间花在等各副本的挂载"看见"新目录上。换句话说,用存储桶当传输通道,贵的不是带宽,而是可见性延迟。

旁证来自第四轮实验:代理的重试间隔从 2 秒降到 0.5 秒之后,同步从 7.6 秒降到 5.8 秒,这和"时间主要耗在等待重试"的解释吻合。但要说清楚,第四轮同时改了三处——加了第三个副本、缩短重试间隔、关掉 FSDP 的 reshard——并不是一次单变量对照。把同步提速归到重试间隔上,是原作者的判断,也是合理的判断,但不是被隔离验证过的结论。

把它和同一团队 5 月的 Delta Weight Sync 放在一起看会更清楚。那条路线不用 LoRA,同步的是全参数:相邻两步之间约 99% 的 bf16 权重逐位不变,于是只把变化的元素编码成稀疏 safetensors 上传到 Bucket,在 Qwen3-0.6B 上把每步负载从 1.2 GB 压到 20 到 35 MB。那篇的日志显示,推理只暂停了 1.1 秒,剩下的上传时间在后台完成、不阻塞生成。

两条路线的模型、同步方式都不同,秒数不能直接比。但它们指向同一件事:一旦每次同步的数据量压到 MB 级,"跨机通信"就退化成"文件出现在存储里",瓶颈从带宽转移到存储一致性。 选哪条路线取决于训练方式:LoRA 路线负载最小,但被锁在 vLLM 能直接服务的适配器配置里,DoRA 或 modules_to_save 都会退回合并权重;Delta 路线对全参微调都适用,代价是要维护锚点和 CPU 侧的 bf16 快照。我的判断是,规模再往上走,决定同步耗时的会是挂载层多快能让所有副本看到新文件,而不是文件本身多大。

3.9 倍提速:三处默认值,而不是架构

架构证明了"能跑":第一轮 500 步里奖励从前 20 步均值 0.145 升到后 20 步 0.438,ratio 每一步都保持在 1.000,126 次同步中 vLLM 服务的策略始终和训练器打分用的策略一致,平均陈旧度 1.5 个版本(上限 4)。

第一轮 r1-dp2 的奖励曲线与 ratio,500 步内奖励从 0.145 升至 0.438,ratio 始终贴着 1.000。图片来源:Hugging Face / Hugging Face Blog
第一轮 r1-dp2 的奖励与 ratio 曲线。图片来源:Hugging Face / Hugging Face Blog

但这一轮跑了 3 小时 27 分钟,而且问题很明显。原文后半篇是一次"瓶颈乒乓":每轮只改一处,看仪表盘把瓶颈推到哪一侧。异步 RL 是训练和生成之间的一条流水线,一侧提速,另一侧跟不上就白费。判断方法只需要盯四组指标:

  • perf/step_s 与 perf/fwd_bwd_s:一步里前向加反向占多少,两者接近说明训练器算力受限;
  • perf/rollout_wait_s:训练器等样本等了多久,接近 0 说明生成跑在前面;
  • sample/rollout_queue_size 与 queue_maxsize:两侧之间的缓冲区,满了说明生成被节流,空了说明训练器在挨饿;
  • rollout/backpressure_s 与 rollout/score_block_s:缓冲区满时 rollout worker 被卡住的时长,先卡在打分阶段,再传回生成阶段。

诊断规则一句话:队列满、等待为零、背压高,是训练器慢;队列空、等待上升、没有背压,是生成慢。

**第一轮 r1-dp2:训练器跟不上。**一步 22.9 秒,前向加反向 21.9 秒,占 96%;训练器等样本只等 0.02 秒,队列停在 512 容量中的 476,rollout worker 每组被背压卡 15 秒。MFU 只有 3.9%。原因在批次指标里:每步 64 个 micro-batch,每行只有 1 条样本,每个 rank 一次只处理一条约 1.2k token 的序列,重复 64 次。根源是参考配方里的 per_device_train_batch_size=1,对 H200 上的 1.5B 模型来说完全是延迟受限。第二个推理副本在这一轮基本是闲置的。

**第二轮 r1-dp2-tb16k:把 micro-batch 塞满。**不改批大小,每个优化器步仍是 128 条补全,只改它们在 GPU 上的排布:开启 token 预算打包,token_budget=16384、gradient_accumulation_steps=6,把多条样本无填充地塞进同一行。每行样本数从 1 升到约 12.7,micro-batch 从 64 降到 6,行填充率 95%。前向加反向从 21.9 秒降到 5.6 秒,MFU 从 3.9% 升到 19%。

第一轮与第二轮对比:打包后每行样本数、micro-batch 数、步时、MFU 与生成吞吐的变化。图片来源:Hugging Face / Hugging Face Blog
开启 token 预算打包前后的六项指标对比。图片来源:Hugging Face / Hugging Face Blog

更有意思的是,生成吞吐也从 4.6k 跳到 25k token/s,而 vLLM 那边什么都没改。队列不再一直满,副本终于能放开跑。原文由此强调不要孤立地优化流水线的某一段:慢的一段会把它前面所有环节的真实性能都掩盖掉。

**第三轮 r1-dp2-tb16k-nockpt:别再重算前向。**前向 1.34 秒,前向加反向 5.6 秒。正常反向约是前向的两倍,基座权重冻结时应该更接近一倍,3.2 倍的比例可疑。原因是 AsyncGRPOConfig 默认 gradient_checkpointing=True,每个 micro-batch 在反向时都要重算一遍前向——这也解释了为什么一行 16k token 在 141 GB 的 H200 上只用了 25 GB。模型够小,激活值完全放得下。关掉后前向加反向降到 4.6 秒,几乎正好少了一次前向,MFU 到 23%。这时队列跌到 71,训练器等样本的时间从 0.04 秒升到 0.6 秒:瓶颈被成功推到了生成侧。

这也暴露出两个新成本。每四步一次、每次 7.6 秒的权重同步,现在占了墙钟时间的 25%,而在每步 23 秒的时候只占 8%。另外反向仍比前向慢 2.5 倍,每步约有 2 秒不像正常的模型计算——这一点原文到最后也没有找到答案。

**第四轮 r1-dp3-tb16k-nockpt:加了第三张卡,结果白加。**既然生成慢,就加第三个副本。同步从 7.6 秒降到 5.8 秒;前向加反向纹丝不动,排除了 FSDP reshard 的嫌疑;生成吞吐只从 25k 升到 26k token/s,第三个副本几乎没干活。

第三轮与第四轮对比:加第三个副本后生成吞吐几乎不变,rollout/inflight 两轮都卡在 128。图片来源:Hugging Face / Hugging Face Blog
第三轮与第四轮的同步耗时、生成吞吐、在途请求与前向加反向对比。图片来源:Hugging Face / Hugging Face Blog

答案在 rollout/inflight 里:每一轮都是 128。max_inflight_tasks 限制的是整个 rollout worker 的并发,不是每个副本的。代理显示这 128 个请求被分成 44、43、41 摊给三个副本,而 H200 上的 1.5B 模型处理 43 条和 130 条并发序列,每 token 成本几乎一样。128 个请求分给三张卡和分给两张卡,吞吐差不多。卡住系统的不是 vLLM,是自己客户端里的一个常数。团队当初设得保守,是因为不确定几百条长 HTTPS 请求穿过 Jobs 的公网代理会怎样——而到这时,已经有 13 万次 rollout 穿过去,没有一次传输错误。

**第五轮 r1-dp3-inflight384:放开在途上限。**只改两个值:max_inflight_tasks=384、queue_maxsize=768。每个副本分到 128 个请求,队列很快填到 768 中的约 690,背压回到 5 秒,等样本降到 0.03 秒——瓶颈又回到训练器。步时中位数 4.8 秒,其中前向加反向 4.6 秒,权重同步摊到每步 1.5 秒。队列变大,样本等得更久,平均陈旧度从 1.5 升到 2.0,仍在上限 4 以内,ratio 依然贴着 1.000。

500 步第一轮 r1-dp2第五轮 r1-dp3-inflight384
墙钟时间3 小时 27 分53 分钟
步时中位数22.9 秒4.8 秒
前向加反向中位数21.9 秒4.6 秒
MFU(前向加反向)3.9%23.5%
每步样本数128168
训练样本总数64,00084,078
权重同步中位数8.5 秒6.2 秒
平均陈旧度1.52.0
奖励(前 20 步 → 后 20 步)0.145 → 0.4380.145 → 0.416

第一轮与第五轮的奖励随墙钟时间变化:同一配方、同样 500 步,第五轮 52 分钟到达,第一轮用了 3 小时 26 分。图片来源:Hugging Face / Hugging Face Blog
第一轮与第五轮按墙钟时间对比奖励曲线。图片来源:Hugging Face / Hugging Face Blog

最终快了 3.9 倍,多训了 31% 的样本,奖励曲线基本重合。回头看这三处改动:per_device_train_batch_size=1 是照抄参考配方带来的,gradient_checkpointing=True 是训练器的默认值,max_inflight_tasks=128 是团队自己保守设的。**没有一处是架构问题。**桶和代理决定了这套系统能不能跑,这三处设置决定了它跑多快。

自建集群能直接搬走什么

国内团队多数不会把训练放在 HF Jobs 上,但这篇的做法并不绑定 HF。按可迁移程度排序:

**第一,指标诊断法可以原样照搬。**无论用的是哪个异步 RL 框架,只要能拿到"训练器等样本的时间""缓冲队列占用"和"生成端被背压卡住的时间"这三类数,就能用上面那条规则判断瓶颈在哪一侧。加 GPU 之前,先看在途请求有没有被一个全局常数卡死——第四轮就是反例:多花一张 H200,换来 4% 的吞吐。

**第二,三处默认值值得逐条对一遍。**参考配方里的 per_device_train_batch_size=1 照搬到大显存卡上跑小模型,GPU 大部分时间在空等;梯度检查点是显存优化,模型放得下时它就是一笔白付的前向;客户端并发上限是全局还是按副本计算,决定了加卡有没有用。

**第三,用对象存储当通道时,先确认 rename 语义。**这套方案依赖"原子重命名发布目录",而 hf-mount 提供的是 POSIX 文件系统语义。如果换成阿里云 OSS 的 ossfs、腾讯云 COS 的 COSFS 这类把对象存储挂成本地目录的 FUSE 工具,目录重命名通常是逐个对象复制再删除,不是原子操作,推理端可能读到写了一半的适配器。搬之前要先测清楚自己挂载工具的行为;用 NAS 这类真正的网络文件系统则没有这个问题。无论哪种,都要保留"No adapter found 只重试该副本、其他错误整体回滚"的逻辑——252 次加载没有一次首发成功,说明可见性延迟是常态而不是意外。

第四,槽位和命名两条规则通用。--max-loras 至少设为 max_staleness + 2,少一个就会在每次同步时悄悄驱逐在用的策略;每个版本用独立的适配器名,否则前缀缓存会把旧权重的 KV 拼到新权重的解码上,唯一的症状是 ratio 缓慢漂移。

想先摸一遍流程,作者给了一条约 15 分钟、20 步的冒烟测试命令:MAX_STEPS=20 RUN_TAG=smoke ./run_all.sh --wait;第五轮的完整配方约 55 分钟。按原文的报价,三个 Job 同时运行大约每小时 20 美元。

TOP2

英王查尔斯召集AI巨头闭门会谈

3

模式的终局:算法如何驯化了我们的创作

2小时前
模式的终局:算法如何驯化了我们的创作
4

Uber用"错误归属"机制遏制重试风暴,单次事件挡下950万请求

10小时前
Uber用"错误归属"机制遏制重试风暴,单次事件挡下950万请求
5

华为将发布昇腾960芯片,对标英伟达加速算力破局

22小时前
华为将发布昇腾960芯片,对标英伟达加速算力破局
6

Gemini对决GPT-6:差价超90%,开发者该选谁?

2小时前
Gemini对决GPT-6:差价超90%,开发者该选谁?
7

Anthropic发布生命科学计划 解锁生物研发AI限制

2小时前
Anthropic发布生命科学计划 解锁生物研发AI限制
8

AI末日究竟会是什么样:从一封辞职信到三类现实场景

10小时前
AI末日究竟会是什么样:从一封辞职信到三类现实场景
热门标签
大模型AgentRAG微调私有化部署Prompt EngineeringChatGPTClaudeDeepSeek智能客服知识管理内容生成代码辅助数据分析金融零售制造医疗教育AI 战略数字化转型ROI 分析OpenAIAnthropicGoogle

关注公众号

前途科技微信公众号

扫码关注,获取最新 AI 资讯

免费获取 AI 落地指南

3 步完成企业诊断,获取专属转型建议

已有 200+ 企业完成诊断