指标跟踪#
AReaL 提供统一的指标跟踪系统,处理分布式训练和 Rollout 工作器的统计信息收集。该系统支持针对其各自使用场景优化的两种不同范式:流式指标用于异步 Rollout 工作流,批量指标用于同步训练更新。
核心组件#
指标系统围绕 areal.utils.stats_tracker 构建,提供:
命名跟踪器:不同组件的隔离指标命名空间
层级作用域:将指标组织成逻辑组
分布式聚合:跨工作器自动归约
多种归约类型:支持平均值、求和、最小/最大值和标量
from areal.utils import stats_tracker
# 默认跟踪器(训练指标)
stats_tracker.scalar(learning_rate=0.001)
# 命名跟踪器(Rollout 指标)
stats_tracker.get("rollout").scalar(reward=0.5)
两种日志范式#
流式指标(Rollout 工作器)#
Rollout 工作器异步执行工作流,每个工作流独立记录指标。这种流式方法自然地处理可变完成时间。
特性:
每个工作流在完成时单独记录标量
指标在工作器进程内的列表中累积
记录期间工作器之间无需同步
归约在导出时通过控制器完成
来自 RLVRWorkflow 的示例:
# areal/workflow/rlvr.py
async def _collect_samples(self, engine, req, prompt_str, task_data):
resp = await engine.agenerate(req)
reward = await self._compute_rewards(resp, prompt_str, task_data)
# 记录单个标量 - 追加到内部列表
# `workflow_context.stat_scope()` 自动区分评估/训练作用域
stats_tracker.get(workflow_context.stat_scope()).scalar(reward=reward)
return resp, reward
你可以在自定义工作流中记录任何其他标量,例如:
async def run(self, data, **extra_kwargs):
# `workflow_context.stat_scope()` 自动区分评估/训练作用域
stats_tracker.get(workflow_context.stat_scope()).scalar(num_turns=num_turns, max_tokens=max_tokens, reward=reward)
return reward
控制器聚合:
RolloutController 从所有工作器收集统计信息并计算加权平均值:
# areal/infra/controller/rollout_controller.py
def export_stats(self) -> dict[str, float]:
all_raw_stats = self._collective_rpc(method="export_stats")
# 使用计数作为权重进行聚合
stats, counts = defaultdict(float), defaultdict(int)
for raw_stats in all_raw_stats:
for k, v in raw_stats.items():
if k.endswith("__count"):
counts[k] += v
else:
stats[k] += v * raw_stats.get(k + "__count", 0)
# 计算加权平均值
return {k: v / counts[k + "__count"] for k, v in stats.items()
if counts.get(k + "__count", 0) > 0}
批量指标(训练引擎)#
训练引擎在数据并行 rank 之间同步处理批次。指标作为带布尔掩码的张量记录,在导出时跨所有 rank 归约。
特性:
记录带分母掩码的完整批次张量
支持每 token 和每序列统计
All-reduce 同步确保各 rank 统计信息一致
多种归约类型:
AVG_MIN_MAX、AVG、SUM、MIN、MAX
来自 PPOActor 的示例:
# areal/trainer/ppo/actor.py
def ppo_update(self, data):
loss_mask = data["loss_mask"].bool()
reward_score = data["rewards"]
# 定义分母(布尔掩码)
stats_tracker.denominator(
n_seqs=torch.ones_like(reward_score, dtype=torch.bool),
n_valid_tokens=loss_mask,
)
# 使用分母引用记录张量指标
stats_tracker.stat(
advantages=data["advantages"], # [batch, seq_len]
kl_rewards=data["kl_rewards"], # [batch, seq_len]
denominator="n_valid_tokens"
)
stats_tracker.stat(
task_reward=reward_score.float(), # [batch]
seq_len=seqlens.float(), # [batch]
denominator="n_seqs"
)
导出行为:
# areal/engine/fsdp_engine.py
def export_stats(self) -> dict[str, float]:
# 跨数据并行组 all-reduce
return stats_tracker.export_all(reduce_group=self.data_parallel_group)
# 所有 DP rank 接收相同的结果
API 参考#
记录方法#
方法 |
使用场景 |
示例 |
|---|---|---|
|
单个浮点值 |
|
|
定义布尔掩码 |
|
|
带掩码的张量指标 |
|
归约类型#
使用 stat() 时,指标默认为 AVG_MIN_MAX,生成三个输出键:
stats_tracker.stat(loss=tensor, denominator="valid")
# 导出:{"loss/avg": 0.5, "loss/min": 0.1, "loss/max": 0.9}
可用的归约类型:
类型 |
输出 |
描述 |
|---|---|---|
|
|
张量统计的默认值 |
|
|
仅加权平均值 |
|
|
所有元素求和 |
|
|
最小值 |
|
|
最大值 |
|
|
用于标量值 |
作用域#
使用层级作用域组织相关指标:
with stats_tracker.scope("ppo_actor"):
with stats_tracker.scope("update"):
stats_tracker.stat(loss=loss_tensor, denominator="valid")
# 键:"ppo_actor/update/loss/avg"
计时#
使用 timeperf/ 下的自动作用域测量执行时间:
with stats_tracker.record_timing("rollout"):
batch = actor.prepare_batch(dataloader, workflow)
# 键:"timeperf/rollout"
命名跟踪器#
为不同组件隔离指标:
# 训练指标(默认跟踪器)
stats_tracker.scalar(grad_norm=1.5)
# Rollout 指标
stats_tracker.get("rollout").scalar(reward=0.8)
# 评估指标
stats_tracker.get("eval-rollout").scalar(reward=0.9)
# 从所有跟踪器导出
all_stats = stats_tracker.export_all(reduce_group=group)
如果设置 evaluator.eval_before_train: true,系统会在第一个训练 step 开始前执行一次评估,以便在微调开始前估计初始模型的性能。
数据流#
从收集到记录的完整指标流程:
Rollout 工作器 训练工作器
─────────────── ───────────────
workflow.arun_episode() actor.ppo_update(batch)
│ │
▼ ▼
get("rollout").scalar(r=0.5) stat(tensor, denom=mask)
│ │
▼ ▼
export_stats(reduce_group=None) export_stats(reduce_group=dp_group)
{reward: 0.5, reward__count: 1} → all_reduce 跨 DP rank
│ │
▼ │
RolloutController.export_stats() │
→ 加权平均跨工作器 │
│ │
└────────────────┬───────────────────────┘
▼
PPOTrainer._export_and_commit_stats()
│
▼
StatsLogger.commit(stats)
│
┌────────────┼────────────┐
▼ ▼ ▼
wandb tensorboard swanlab
StatsLogger:日志后端#
StatsLogger
将聚合指标发送到外部日志后端。它由 PPOTrainer 自动管理,仅在 rank 0 运行以避免重复日志。
支持的后端#
后端 |
配置 |
描述 |
|---|---|---|
Weights & Biases |
|
云端实验跟踪 |
SwanLab |
|
替代实验跟踪 |
TensorBoard |
|
本地可视化 |
与 PPOTrainer 集成#
训练器在每个训练步结束时调用 StatsLogger.commit():
# areal/trainer/rl_trainer.py
def _export_and_commit_stats(self, epoch, epoch_step, global_step):
# 1. 从所有组件收集指标
stats = self.actor.export_stats() # 训练指标(all-reduced)
stats.update(self.rollout.export_stats()) # Rollout 指标(控制器聚合)
stats.update(self.eval_rollout.export_stats()) # 评估指标
# 2. 发送到日志后端(仅 rank 0)
self.stats_logger.commit(epoch, epoch_step, global_step, stats)
StatsLogger.commit()#
commit() 方法过滤掉内部计数键并记录到所有配置的后端:
# areal/utils/stats_logger.py
def commit(self, epoch, step, global_step, data):
if dist.is_initialized() and dist.get_rank() != 0:
return # 仅 rank 0 记录
# 过滤掉 __count 键(用于内部加权平均)
data = {k: v for k, v in data.items() if not k.endswith("__count")}
# 记录到所有后端
wandb.log(data, step=global_step)
swanlab.log(data, step=global_step)
if self.summary_writer:
for key, val in data.items():
self.summary_writer.add_scalar(key, val, global_step)
配置#
在实验配置中配置日志后端:
stats_logger:
experiment_name: "gsm8k_grpo"
trial_name: "run_001"
fileroot: "/path/to/logs"
wandb:
mode: "online" # "online"、"offline" 或 "disabled"
project: "my-project"
entity: "my-team"
swanlab:
mode: "online" # "online"、"local" 或 "disabled"
project: "my-project"
tensorboard:
path: "/path/to/tensorboard/logs" # null 禁用
训练诊断指标#
当 advantage 预处理获得重算的 proximal logprob 时,ppo_actor/train_infer/ 会在标准 PPO 覆盖原始 rollout
logprob 之前比较两者,不增加模型 forward。 不重算 logprob 的运行和纯 MOPD 预处理路径不会产生这组指标。
设对齐后的有效生成 token 上 d = log(p_trainer) - log(p_rollout):
指标 |
定义 |
|---|---|
|
带符号差值 d |
|
绝对差值 abs(d) |
|
d² 的 token 加权均值 |
|
采样 token 负对数概率的均值 |
|
-d、d²/2、exp(d)-1-d 的均值 |
|
abs(d) > log(阈值) 的 token 比例 |
统计使用后续 rejection/advantage masking 之前的生成掩码。陈旧 rollout 的差异包含策略变化, 不能全部归因于引擎数值误差。KL
估计还取决于采样分布、过滤和 logprob 口径;有限样本的 k1 可以为负,k2 是局部近似。聚合后可以用
sqrt(max(0, E[d²] - E[abs(d)]²)) 推导绝对差值的总体标准差。
概率比尾部定义与 R3 论文公式 (3) 相同。 当 token 确实来自返回的 rollout
概率分布且支持集匹配时,k1/k3 估计 KL(rollout || trainer)。 top-p/top-k 过滤或 greedy 采样后的实际分布,不一定等于接口返回
logprob 所描述的分布。
nonfinite_logp_fraction 表示有效 token 中任一侧 logprob 为 NaN 或无穷大的比例。 存在这样的 token 时,概率比尾部指标输出
NaN,不将无效比较误报成零。 掩码之外的 padding 不参与此比例,也不会污染尾部指标。 kl_k3_overflow_fraction 记录有限输入差值经
float64 expm1 后仍溢出的比例。
ppo_actor/advantage_{positive,negative,zero}_fraction 基于塑形后的输入 advantage 和 batch loss
mask,统计位置在 M2/rejection 过滤之前。它表示 token 占比,不表示任务成功率或梯度符号。 GSPO 还可能在 loss 内对 advantage
做序列聚合。
ppo_actor/update/version_stats/sample_staleness_{theta,proximal}_{avg,min,max}
仅统计掩码有效且版本非负的生成 token。均值按 token 加权,极值是聚合组中的全局极值。 theta/proximal 两组兼容字段均使用最近发布的
checkpoint 版本:actor 和 rollout 在 PPO 更新后一起升版, 这不是 optimizer minibatch
计数。stale_token_fraction 表示 behavior 版本落后的比例, future_token_fraction 表示 behavior
版本超前的比例。包括 pure MOPD 在内,诊断在 _ppo_update 中局部左移 raw rollout version 到预测位置, 不修改 loss
的原有近端策略近似。统计总体在 M2/rejection 过滤之前,每次 _ppo_update 计一次,与 minibatch forward 次数无关。
n_valid_generated_tokens 累加这部分 token 观测;不同 update 重用同一轨迹仍会重复计数。 没有有效 token
时省略均值和极值,不填充为零。
新增诊断使用 stat_compact:每个指标在每个 batch 仅保留四个 float64 标量(掩码和、数量、最小、最大) 直到 export,避免原先约 106
字节/padded token 的诊断张量长期留存。已有 PPO 指标仍有自身的全量张量, 诊断计算过程中也会临时分配逐 token
张量。长上下文的峰值内存和运行时间仍需按实际 workload 验证。
可选的 loglinear 近端策略近似仍读取原始 token 位置的 rollout version,且保留原先 current_version - 1
的插值假设。其算法语义需要另行按真实 optimizer step 验证; 这里修正的 checkpoint 年龄指标不能证明 loglinear 路径也已修复。
最佳实践#
选择正确的范式:对标量使用
scalar(),对批量 PyTorch 张量(通常是训练指标)使用带分母的stat()。先定义分母:始终在
stat()之前调用denominator()来建立掩码关系。使用命名跟踪器:使用
stats_tracker.get(workflow_context.stat_scope()).scalar(...)将 Rollout("rollout")和评估("eval-rollout")指标与训练指标隔离。
训练吞吐、预估 FLOPs 与 MoE 均衡度#
Archon、Megatron 和 FSDP 在 train_perf scope 上报训练指标。每个统计窗口包含上次 export 以来的所有 train_batch
调用,包括 PPO 中重复执行的更新。tokens 为原始 attention_mask 的有效 token 数,包含 prompt 和 response,不包含
padding;只沿 DP 维度求和,不重复乘以 TP、CP/SP、PP 或 EP。
计时从梯度清零前开始,覆盖微批准备、前向、反向和 optimizer 工作。CUDA/ROCm 在进入计时区间时捕获训练流, 使用该流上的 events 记录区间,仅在统计导出时对每个设备同步一次,不额外插入批次级同步。 该时间包含流上的等待及起始 event 执行后的主机提交间隙,但不包含结束 event 前未汇合的其他流异步工作, 因此不是设备全局同步后的 wall time。CPU/NPU 保留同步 wall time 计时。不包含 rollout、参考模型/评估前向、读取下一批数据、存盘和统计导出。 分母取所有训练 rank 的累计训练耗时最大值。
指标 |
含义 |
|---|---|
|
当前窗口训练侧总有效 tokens |
|
当前窗口训练耗时 |
|
当前窗口 tokens / 耗时 |
|
对各条序列的前向+反向 FLOPs 估算求和 |
|
当前窗口预估 FLOPs / 耗时 |
|
engine 初始化以来的累计量 |
|
累计 tokens / 累计训练耗时 |
|
累计预估 FLOPs / 累计训练耗时 |
这些指标表示整个训练集群的吞吐,不是单卡吞吐。重建 engine(包括恢复训练)会重新累计。 没有注册 FLOPs 函数的架构仍然上报 tokens 和耗时,省略 FLOPs 指标。
FLOPs 口径与自定义注册#
areal.utils.flops 预置 qwen3_moe 和 qwen3_5_moe[_text] 架构的计算工厂,覆盖 Qwen3-30B-A3B 和
Qwen3.5-35B-A3B。计算使用实际 checkpoint 的 text config,支持本地 模型目录,无须匹配目录名。模型参考配置来自官方
Qwen3 配置和
Qwen3.5 配置。
一次乘加计为 2 FLOPs,训练估算为前向的 3 倍(前向+约 2 倍前向的反向)。包括激活的 专家投影、router、共享专家及 gate、attention 投影和 LM
head。每层因果全注意力的前向 计算包含 2 * query_heads * head_dim * L * (L + 1),所以 packed batch 必须对各条
序列的 estimate(L_i) 求和,不能把总长度传入一次。
Qwen3.5 分别计算全注意力与 GatedDeltaNet 线性注意力。后者包括各个投影和 depthwise 卷积,状态计算采用每个 value head、每个 token 三次 key-by-value 乘加的递归等价估算 (状态预测、更新、读出),随序列长度线性增长,不刻画具体 chunk kernel 的额外操作。
估算不包含激活重计算、optimizer、逐元素操作、softmax/归一化、视觉编码器和额外 MTP。 它表示文本骨干全参数训练的模型数学工作量,不等同于实际硬件执行 FLOPs,也不是 LoRA/冻结参数时的精确反向计算量。树训练按原始序列估算,不扣除共享前缀节省的计算。
在每个训练 worker 的 engine 初始化前注册自定义工厂;仅在 controller 注册不会 自动传到远程 worker。工厂接收模型配置,返回一个只接收单条序列长度、输出训练总 FLOPs 的函数。同名注册会覆盖预置工厂:
from areal.utils.flops import register_flops_estimator
def my_model_factory(config):
active_parameters = config.active_parameters
heads = config.num_attention_heads
head_dim = config.head_dim
layers = config.num_hidden_layers
def total_training_flops(sequence_length: int) -> float:
linear = 6 * active_parameters * sequence_length
attention = 6 * layers * heads * head_dim * sequence_length * (sequence_length + 1)
return float(linear + attention)
return total_training_flops
register_flops_estimator("my_model_type", my_model_factory)
MoE 均衡度与 W&B 可视化#
MoE 使用独立的 moe_balance scope。Archon 支持通用 MoE 模块,Megatron 支持 TopKRouter,FSDP 支持
Transformers 的 Qwen3MoeTopKRouter 和 Qwen3_5MoeTopKRouter 返回值契约。层编号从 0 开始,在 PP
之间保持全局唯一;共享专家 和额外 MTP router 不纳入统计。不支持的 router 不产生均衡度指标。
设一层有 E 个专家,专家 e 的累计路由分配数为 c_e:
每个专家占比为
100 * c_e / sum(c_e),同层合计 100%。理想负载为
sum(c_e) / E,分配数包含 top-k 的重复路由。moe_balance/layer_<id>/max_over_ideal为最大负载除以理想负载,完全均衡时为 1。 没有路由分配的层上报 0。
先跨微批和并行 rank 累加负载,再归一化。只计原始训练前向,不计评估和 backward 重计算。这是实际执行的路由负载:包含 packing/alignment padding;Megatron 使用 capacity/drop 之后的 routing map。因此负载总数可能与吞吐指标中的逻辑 tokens 不同。
W&B 每个日志 step 收到一个 moe_balance/expert_loads Table,列为 layer、expert、
tokens、load_percent。每层 max_over_ideal 保持标量,避免产生上万个专家标量序列。 SwanLab 和 Trackio 保留
moe_balance/layer_<id>/expert_<id>/{tokens,load_percent};TensorBoard 和控制台仅输出每层摘要。
建议用 W&B Custom Charts 制作以下面板:
选定 step 的“层 × 专家”热力图:颜色表示负载百分比,tooltip 显示 tokens。 固定专家顺序;跨模型比较时可二次计算
load_percent / (100 / E),以 1 为颜色中心。“训练步 × 层”热力图:颜色表示
max_over_ideal,定位从何时起、哪些层发生失衡。单层专家柱状图:按层筛选 Table,加入理想占比
100 / E的参考线,观察长期热点 或闲置专家。
单个 Table 是一个 step 的快照。使用 historyTable 和 step 滑块可直接切换快照; 若要同时展示多个 step 的 Table
数据,则需在后处理中合并快照并添加 step 列。 每层最大负载比可直接使用标量历史。分位数、熵、变异系数等额外指标可用原始负载在 W&B 二次计算。
完整操作步骤、可粘贴的 Vega 配置和 10,000 行截断处理见 MoE 专家负载可视化。