FEATURED · 精选文章

Agentic RL后训练资源调度:Libra方案详解与吞吐优化

发布时间 / 2026/8/29 19:56:01
来源 / 创域科博编辑部
栏目 / 资讯中心
Agentic RL后训练资源调度:Libra方案详解与吞吐优化 Agentic RL 后训练越来越热但很多团队的瓶颈不是算法本身而是资源分配。最近港中文和恒生大学提出的 Libra 方案把“后训练资源怎么分”这个问题重新搬上台面在多个 Agentic RL 任务并行、多个模型角色共享 GPU 集群的场景下通过更细粒度的资源调度吞吐最高可以提升 3 倍。这篇文章会从概念讲起再拆解资源调度的核心思路最后给出一套可落地的观测与调度实验帮助你在自己的后训练任务里复制这套优化方法。1. Agentic RL 后训练为什么“卡”在资源上1.1 什么是 Agentic RL 后训练Agentic RL 是“智能体强化学习”的简称。它和传统 RLHF 最大的区别在于模型的动作空间不只是在几个候选回复里选一个而是可以调用工具、写代码、搜索网页、操作环境甚至多步推理后才给出最终答案。后训练是指在大模型预训练完成之后再用强化学习或监督微调的方式继续训练模型。预训练解决的是“模型懂不懂语言”后训练解决的是“模型会不会按人的意图做事”。Agentic RL 后训练的目标是让模型学会在复杂任务中自主决策。一个典型的 Agentic RL 训练循环包含Actor 模型当前正在训练的模型负责生成动作或回复。Critic 模型评估当前状态的价值帮助 Actor 计算优势函数。Reward Model给模型输出打分。Rollout 生成器让 Actor 与环境交互生成一批经验数据。环境代码解释器、搜索引擎或业务仿真系统。每个角色都可能占一块 GPU 或一组 GPU。也就是说一个后训练任务不是“一个模型在训练”而是“一组模型在协作”。1.2 后训练过程比预训练更“吃资源”预训练阶段流程相对固定数据 → 前向 → 反向 → 更新参数。虽然计算量大但资源分配很规整。后训练阶段流程变成生成阶段Actor 模型做推理产出动作序列。交互阶段把动作交给环境执行可能跑代码、查数据库。评估阶段Reward Model 给结果打分。学习阶段用生成的样本更新 Actor 和 Critic。推理和训练交替进行而且推理阶段的 batch size、序列长度、动作长度都在动态变化。这会导致几个问题显存使用波动大。推理阶段要缓存 KV Cache训练阶段要存优化器状态两者峰值往往不在同一时刻。有些 GPU 忙着推理有些 GPU 在等反向传播算力闲置。多任务并行时如果平均分配 GPU可能出现“一个任务吃不下另一个任务吃不满”的情况。这些问题本质上是资源分配问题不是模型算法问题。1.3 吞吐后训练最该盯住的核心指标在后训练场景里有两个吞吐指标非常关键Token 吞吐单位时间生成的 token 数单位通常是 tokens/s主要影响 Rollout 生成速度。训练吞吐单位时间处理的训练样本数单位可以是 samples/s 或 episodes/s。为什么说吞吐是核心指标因为 Agentic RL 需要大量与环境交互的数据。一个任务往往要跑几万甚至几十万轮 episode每轮都有多次工具调用。如果吞吐上不去训练效率会直线下降。GPU 利用率高不等于吞吐高。很多时候显存占用率接近 100%但 GPU 计算单元在空转因为数据加载、环境交互、Reward 打分都在等。真正的调度优化是让整条数据流水线每个环节都不掉队而不是只看某一块 GPU 的利用率。2. Libra面向 Agentic RL 后训练的资源分配方案2.1 Libra 是谁提出的解决什么问题根据公开资料Libra 由香港中文大学和香港恒生大学研究团队提出是一个面向 Agentic RL 后训练的资源分配方案。它的核心目标是解决“多任务、多角色共享 GPU 集群时资源怎么分才更高效”的问题。注意Libra 不是新一代模型也不是新的强化学习算法它更偏向系统层和调度层的研究。用业内常见的话说这是在“训推一体、多任务混部”的大背景下把 GPU 资源调度做得更细。在 Agentic RL 后训练场景里一个集群可能同时跑多个任务任务 A 在微调一个新的 Agent 模型需要大量生成 Rollout 数据。任务 B 在评估上一个 checkpoint 的效果需要独立环境。任务 C 在跑 Reward Model 的重新训练。这些任务对 GPU 算力、显存、CPU、内存、网络带宽的需求完全不同。用统一的资源池去调度比让每个任务独占一组 GPU 要高效得多。2.2 资源分配的“分”字怎么理解“资源怎么分”可以从四个维度理解资源类型维度资源作用后训练中的瓶颈点GPU 算力前向和反向计算Actor 推理和 Critic 训练争抢算力显存模型参数、优化器状态、KV Cache推理和训练切换时显存波动大CPU 与内存数据预处理、环境交互Rollout 数据量大时 CPU 容易打满网络带宽多卡通信、数据分发全量 checkpoint 同步容易卡顿分配粒度维度任务级哪个任务先跑哪个任务排队。角色级Actor、Critic、Reward 各占多少卡。批次级同一个任务内部batch size 是否动态调整。时间维度静态分配启动时分配运行中不变。动态分配根据实时负载调整 GPU 和显存配额。调度策略维度吞吐优先让所有任务整体吞吐最高。延迟优先保证关键推理任务的响应时间。公平优先每个任务按权重分资源。Libra 这类方案的核心价值就是把以上维度放在一个统一资源视图里做优化。2.3 吞吐最高提升 3 倍该怎么解读“吞吐最高提升 3 倍”听起来很夸张但需要理性看待。这个数字一般是实验环境下的最好结果而且取决于对比基线。如果基线是“每个任务独占固定 GPU空闲资源不能共享”那么引入动态调度后空闲时隙被利用起来吞吐翻倍甚至 3 倍都很正常。实际落地时提升幅度会受几个因素影响任务本身的资源需求是否互补。一个偏推理、一个偏训练互补效果最好。调度开销是否可控。如果调度器本身反复迁移模型通信开销会吃掉收益。显存是否允许动态迁移。大模型显存占用高迁移成本也高。所以更准确的说法是在资源需求差异大、空闲碎片多的场景里精细化调度能带来显著吞吐收益。如果所有任务资源需求完全一样调度优化空间就小很多。3. 复现前的环境准备与指标观测3.1 软硬件环境说明要做类似 Libra 的资源调度实验不一定要先复现完整论文。更务实的做法是先搭建一套资源观测系统把自己的后训练集群的“资源使用画像”画出来再考虑调度策略。软硬件环境建议如下GPUNVIDIA 显卡建议 4 卡以上显存 24GB 或更高。CUDA 与驱动CUDA 11.8 或 12.x驱动版本要匹配。Python3.9 或 3.10。深度学习框架PyTorch 2.x具体版本根据你的模型要求安装。调度与任务管理可以先用 shell 脚本或 Python 脚本模拟不强制引入 Kubernetes。需要注意的是Libra 的具体复现方式要以官方仓库和论文为准。我在本文中给的是通用资源调度思路代码是演示逻辑不是 Libra 官方实现。3.2 搭建资源观测脚本在优化资源分配之前先要有数据。推荐用一段轻量脚本周期采集 GPU 状态和进程状态输出到结构化日志里。下面是一个资源观测脚本示例核心思路是读取nvidia-smi并关联到当前 Python 进程统计显存与利用率趋势。 文件路径monitor/gpu_monitor.py 这是一个资源观测脚本示例用于记录 GPU 利用率和显存变化。 可根据实际环境调整采集间隔和输出格式。 import json import subprocess import time import os from datetime import datetime def get_gpu_info(): # 使用 nvidia-smi 查询 GPU 状态 cmd [ nvidia-smi, --query-gpuindex,utilization.gpu,memory.used,memory.total, --formatcsv,noheader,nounits ] result subprocess.run(cmd, capture_outputTrue, textTrue) lines result.stdout.strip().split(\n) gpu_list [] for line in lines: parts [item.strip() for item in line.split(,)] gpu_list.append({ gpu_index: int(parts[0]), gpu_util: int(parts[1]), memory_used_mb: int(parts[2]), memory_total_mb: int(parts[3]) }) return gpu_list def get_process_info(): # 可以按需扩展查询当前进程 PID、显存占用 cmd [ nvidia-smi, --query-compute-appspid,used_memory, --formatcsv,noheader,nounits ] result subprocess.run(cmd, capture_outputTrue, textTrue) lines result.stdout.strip().split(\n) process_map {} for line in lines: if not line.strip(): continue parts [item.strip() for item in line.split(,)] pid parts[0] used_memory parts[1] if len(parts) 1 else 0 process_map[pid] used_memory return process_map def record_metrics(interval5, duration300): # 按固定间隔采集写入 JSONL 文件 output_file metrics/gpu_metrics.jsonl os.makedirs(metrics, exist_okTrue) start_time time.time() while time.time() - start_time duration: record { timestamp: datetime.now().isoformat(), gpus: get_gpu_info(), processes: get_process_info() } with open(output_file, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) print(record) time.sleep(interval) if __name__ __main__: # 示例每秒采集一次持续 60 秒 record_metrics(interval1, duration60)这个脚本的用途是帮助你了解哪些 GPU 在跑什么任务显存是否长期处于高位GPU 利用率是否存在周期性空转。后训练的资源调度优化第一步就是把这些问题量化。3.3 记录训练吞吐的两种口径除了 GPU 指标还要记录训练吞吐。推荐同时记录两个口径模型训练速度每秒处理多少样本通常在看训练主循环里累计样本数。Rollout 生成速度每秒生成多少 token通常在看采样循环里累计生成 token 数。下面是一个简单的吞吐计数器示例 文件路径monitor/throughput_counter.py 用于在训练或推理循环中累计吞吐指标。 import time class ThroughputCounter: def __init__(self): self.total_samples 0 self.total_tokens 0 self.start_time time.time() def add_samples(self, n): self.total_samples n def add_tokens(self, n): self.total_tokens n def get_samples_per_second(self): elapsed time.time() - self.start_time if elapsed 0: return 0.0 return self.total_samples / elapsed def get_tokens_per_second(self): elapsed time.time() - self.start_time if elapsed 0: return 0.0 return self.total_tokens / elapsed def reset(self): self.total_samples 0 self.total_tokens 0 self.start_time time.time() # 使用示例 if __name__ __main__: counter ThroughputCounter() for step in range(100): # 模拟每个 step 处理 8 个 sample counter.add_samples(8) # 模拟每个 sample 生成 512 token counter.add_tokens(8 * 512) time.sleep(0.05) print(samples/s:, counter.get_samples_per_second()) print(tokens/s:, counter.get_tokens_per_second())记录吞吐时建议按时间窗口分段统计而不是只记全程平均值。因为 Agentic RL 后训练的生成阶段长训练阶段短全程平均值会掩盖局部波动。4. 核心原理拆解资源调度的三个层级4.1 任务级调度任务级调度是最高层的资源分配。它要回答的问题是当前有多个 Agentic RL 后训练任务谁先执行、谁可以暂停、谁需要排队。常见策略有两种先来先服务实现简单但容易导致长任务饿死短任务。优先级调度根据任务重要性和预期耗时动态调整。在 Libra 这类方案里任务级调度通常会考虑“资源互补性”。假设任务 A 需要大量 GPU 做推理任务 B 需要大量 GPU 做训练如果两个任务排在同一组 GPU 上交替执行比各自独占更合理。实现任务级调度的关键点每个任务要能安全暂停和恢复这要求模型状态能落盘。任务队列要支持动态插入高优任务。调度决策要有日志方便回放分析。4.2 角色级资源分配在单个 Agentic RL 后训练任务内部还可以按照角色拆分资源。以 4 卡 GPU 为例常见分配方式角色固定分配方案动态分配方案Actor 训练2 卡1-3 卡动态调整Rollout 推理1 卡1-2 卡动态调整Reward Model1 卡0-1 卡动态调整动态分配的依据是流水线状态如果 Rollout 数据堆积严重暂时多分 GPU 给推理。如果经验池已经足够减少推理资源把资源让给训练。如果 Reward Model 打分太慢需要单独给它扩容。这种角色级调度要求每个角色可以被独立启停。实践中通常会为每个角色单独启动一个服务进程通过共享队列传递数据。4.3 批次级动态伸缩批次级调度是更细的调整通常发生在单个 GPU 卡内部。核心手段是动态调整 batch size、序列长度切分、梯度累积步数。在 Agentic RL 后训练里Rollout 生成的序列长度波动很大。有的动作序列很短有的动作序列带几十轮工具调用。如果固定 batch size可能出现两类问题短序列场景显存浪费。长序列场景显存溢出。动态 batch size 的思路是根据当前任务里序列长度的分布动态决定一次前向推理放多少个样本。伪代码如下 文件路径scheduler/dynamic_batcher.py 示例思路用于演示动态 batch size 的调度逻辑。 def compute_dynamic_batch_size( free_memory_mb, max_seq_len, per_token_memory_mb, min_batch_size, max_batch_size ): 根据当前显存余量计算可容纳的 batch size。 注意实际训练中还需要考虑优化器状态、梯度显存等。 max_batch_by_memory free_memory_mb / (max_seq_len * per_token_memory_mb) batch_size int(max_batch_by_memory) batch_size max(min_batch_size, min(batch_size, max_batch_size)) return batch_size if __name__ __main__: # 示例参数 free_memory_mb 24000 max_seq_len 4096 per_token_memory_mb 0.01 # 根据模型结构估算 batch_size compute_dynamic_batch_size( free_memory_mbfree_memory_mb, max_seq_lenmax_seq_len, per_token_memory_mbper_token_memory_mb, min_batch_size1, max_batch_size16 ) print(动态 batch size:, batch_size)批次级调度的收益在于减少显存碎片。但要注意batch size 频繁变化可能导致训练不稳定所以在实际工程中通常用“阶梯式调整”而不是每步都变。5. 实战案例实现一个简易的“类 Libra”调度器5.1 示例场景假设我们有 4 张 GPU 卡两个后训练任务需要共享集群任务 1Actor RL 训练需要较多算力训练吞吐要求高。任务 2Rollout 数据生成需要较多显存做长序列推理但对算力要求不高。固定分配方案是把 4 卡对半分任务 1 用 0、1 号卡任务 2 用 2、3 号卡。问题是任务 2 如果中间出现长序列生成显存会吃紧任务 1 在反向传播阶段算力需求高但显存需求相对低。我们可以写一个简单的调度器实时采集 GPU 状态根据规则动态调整两个任务的进程数量或 batch size。5.2 项目结构与调度逻辑项目结构如下agentic_rl_scheduler/ ├── monitor/ │ ├── gpu_monitor.py │ └── throughput_counter.py ├── scheduler/ │ ├── simple_scheduler.py │ └── dynamic_batcher.py ├── tasks/ │ ├── task_train.py # 任务1训练 │ └── task_rollout.py # 任务2生成数据 └── metrics/ └── gpu_metrics.jsonlsimple_scheduler.py是调度核心。它根据 GPU 利用率和显存余量决定给任务 2 分配多少并发 worker。 文件路径scheduler/simple_scheduler.py 示例思路一个基于规则的简易调度器不是 Libra 官方实现。 它根据 GPU 空闲情况动态调整 Rollout 任务的 worker 数量。 import time import subprocess def get_gpu_stats(): cmd [ nvidia-smi, --query-gpuindex,utilization.gpu,memory.used,memory.total, --formatcsv,noheader,nounits ] result subprocess.run(cmd, capture_outputTrue, textTrue) gpu_stats [] for line in result.stdout.strip().split(\n): parts [item.strip() for item in line.split(,)] gpu_stats.append({ index: int(parts[0]), gpu_util: int(parts[1]), memory_used_mb: int(parts[2]), memory_total_mb: int(parts[3]) }) return gpu_stats def decide_rollout_workers(gpu_stats, current_workers): 规则 1. 如果所有 GPU 平均利用率低于 50%且显存余量较大增加 rollout worker。 2. 如果任一 GPU 显存使用率超过 90%减少 rollout worker。 3. 其他情况保持当前 worker 数。 if not gpu_stats: return current_workers avg_util sum(item[gpu_util] for item in gpu_stats) / len(gpu_stats) max_mem_ratio max( item[memory_used_mb] / item[memory_total_mb] for item in gpu_stats ) if max_mem_ratio 0.9: return max(1, current_workers - 1) if avg_util 50 and max_mem_ratio 0.8: return min(8, current_workers 1) return current_workers def main(): current_workers 2 while True: stats get_gpu_stats() new_workers decide_rollout_workers(stats, current_workers) if new_workers ! current_workers: print(f[scheduler] rollout workers: {current_workers} - {new_workers}) # 实际项目中这里应该调用进程管理接口动态扩容或缩容。 current_workers new_workers time.sleep(10) if __name__ __main__: main()这个调度器很简单但它体现的核心思想是调度决策基于实时资源状态而不是启动时一次性分配。真实系统里还需要加入任务队列、模型迁移、优先级权重但骨架是一样的。5.3 运行与验证第一步启动资源观测cd agentic_rl_scheduler python monitor/gpu_monitor.py第二步启动调度器python scheduler/simple_scheduler.py第三步分别启动任务 1 和任务 2 的模拟脚本。由于篇幅原因这里用一段简单循环模拟真实训练任务。 文件路径tasks/task_train.py 模拟 Agentic RL 后训练中的 Actor 训练循环。 import time import sys sys.path.append(.) from monitor.throughput_counter import ThroughputCounter def simulate_training(duration120): counter ThroughputCounter() start time.time() while time.time() - start duration: # 模拟训练每步处理 16 个样本 counter.add_samples(16) counter.add_tokens(16 * 1024) # 模拟反向传播耗时 time.sleep(0.1) print(train samples/s:, counter.get_samples_per_second()) print(train tokens/s:, counter.get_tokens_per_second()) if __name__ __main__: simulate_training() 文件路径tasks/task_rollout.py 模拟 Agentic RL 后训练中的 Rollout 生成。 import time import sys sys.path.append(.) from monitor.throughput_counter import ThroughputCounter def simulate_rollout(duration120): counter ThroughputCounter() start time.time() while time.time() - start duration: # 模拟 Rollout 生成每步生成 8 个样本序列较长 counter.add_samples(8) counter.add_tokens(8 * 2048) # 模拟推理耗时比训练更慢 time.sleep(0.2) print(rollout samples/s:, counter.get_samples_per_second()) print(rollout tokens/s:, counter.get_tokens_per_second()) if __name__ __main__: simulate_rollout()5.4 结果说明运行后你可以对比两种方案的吞吐差异固定分配方案两个任务各占固定资源不互相感知总吞吐等于两者之和。动态调度方案调度器根据 GPU 利用率波动把空闲资源临时让给更需要的任务。在模拟场景里任务 1 反向传播阶段 GPU 计算密集任务 2 可以在显存充足时多做生成当任务 2 显存吃紧时调度器自动减少其并发避免 OOM。观察metrics/gpu_metrics.jsonl你会看到 GPU 利用率的波峰波谷被抚平。这就是吞吐提升的来源让原本空闲的 GPU 时隙被利用起来。需要说明的是这个示例只是演示调度思想。真实的 Libra 方案会涉及更复杂的约束比如模型分片、通信拓扑、checkpoint 迁移等。建议先跑通这个最小闭环理解资源调度的收益再对照论文或官方代码深入。6. 常见问题与排查思路6.1 常见问题表问题现象常见原因解决思路GPU 利用率长期低于 30%Rollout 生成慢训练等数据增加推理并发优化数据队列显存溢出 OOM长序列场景下 batch size 过大引入动态 batch size 或梯度累积调度器频繁迁移任务吞吐反而下降迁移开销大于资源收益增加迁移阈值降低调度频次多个任务互相抢 CPU数据预处理慢CPU 资源不足为每个任务设置 CPU 配额训练吞吐波动大环境交互耗时不稳定增加经验池缓冲平滑生成速度动态调整 batch size 后训练效果变差batch 分布变化过大使用阶梯调整控制变化幅度6.2 排查清单如果后训练吞吐上不去建议按以下顺序排查先检查 Rollout 生成速度是否满足训练消耗速度。再检查 GPU 利用率和显存时序图定位空转窗口。然后检查 CPU 和内存是否存在瓶颈。最后检查网络通信多卡间梯度同步是否成为瓶颈。如果以上都不是再考虑调度策略问题。排查时不要凭感觉。把 GPU 监控和吞吐记录打开至少采集 30 分钟数据再下结论。6.3 为什么要避免“一刀切”调度有的团队会简单粗暴地规定“每 10 分钟重新分配一次 GPU”。这种策略在小集群里可能有效但大模型训练场景里模型参数和优化器状态都在显存中迁移成本很高。更务实的做法是优先调整数据流和 batch size而不是迁移模型。跨卡迁移只发生在任务级调度不发生在批次级调度。每次调度前计算预期收益收益低于阈值就不动。7. 最佳实践与工程建议7.1 资源观测先行没有观测就没有优化。在做任何调度改造之前先把 GPU、显存、吞吐、队列长度这些指标记录下来。建议至少保留两周的数据再分析资源画像。推荐指标GPU 利用率均值与峰值。显存利用率均值与峰值。训练吞吐、Rollout 吞吐。经验池队列长度。任务等待时间。7.2 调度策略设计建议设计调度策略时注意以下原则最小权限与安全在后训练集群里调度器对任务的操作要有限制。不要随意 kill 任务尤其不要在生产环境直接强制释放显存。如果需要重启任务先保存 checkpoint再进行操作。控制调度频次调度过于频繁会带来抖动。建议设置最小调度间隔例如 30 秒或 1 分钟。只有在资源状态持续超过阈值时才触发迁移或扩缩容。优先级可配置不同任务的重要性不同。引入优先级队列让关键任务在资源争抢时优先获得资源。7.3 训练完成后如何导出模型部署资源调度做完之后另一个常被问到的问题是Agentic RL 后训练得到的模型怎么导出给实际业务使用这个问题和 YOLO 模型训练后导出给 Qt 调用是同一个思路。常见的做法是用 ONNX 或 TorchScript 导出先确认模型处于推理模式去掉训练专属的优化器状态。用torch.onnx.export导出为 ONNX 格式。在 C/Qt 项目里加载 ONNX Runtime 或 OpenCV DNN 模块进行推理。以 PyTorch 导出 ONNX 为例 文件路径export/export_onnx.py 示例思路导出 PyTorch 模型为 ONNX便于 C 或 Qt 程序调用。 import torch def export_to_onnx(model, dummy_input, output_path): model.eval() # 这里需要按实际模型结构构造一个 dummy input with torch.no_grad(): torch.onnx.export( model, dummy_input, output_path, opset_version17, input_names[input], output_names[output], dynamic_axes{ input: {0: batch_size, 1: sequence_length}, output: {0: batch_size} } ) print(export done:, output_path) if __name__ __main__: # 以一个小型 transformer 为例实际使用时替换为自己的模型 model torch.nn.Transformer( d_model128, nhead4, num_encoder_layers2, num_decoder_layers2 ) dummy_input torch.randn(1, 16, 128) export_to_onnx(model, dummy_input, model.onnx)导出后再配合 ONNX Runtime 或 TensorRT 做推理加速就可以在 Qt 等 C 应用里调用。需要注意的是RL 模型导出时要特别注意动作头和后处理逻辑这些通常不能直接用 ONNX 算子表示需要留在业务代码里。7.4 从单机实验到集群实践的过渡本文的实验在单机 4 卡环境就能跑通但生产环境往往是多机多卡。从单机到集群需要考虑更多问题资源调度器与 Kubernetes/Yarn 的联动。多机通信带宽对迁移成本的影响。不同任务的隔离与配额管理。调度决策的审计日志。建议先在单机集群里跑通最小闭环积累资源和吞吐数据再逐步引入更复杂的调度组件。不要一上来就直接在生产环境做动态调度。如果要在生产环境引入类似 Libra 的调度策略我的建议是从监控和记录开始先把吞吐瓶颈量化再迭代调度规则。资源调度不是一次性的改造而是一个持续优化的过程。先把基础观测做好把每一次调度决策都记录下来你就能慢慢找到最适合自己业务场景的资源分配方案。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻