1. 从一次"盲训"经历说起:为什么训练监控不是可选项
刚接触 MindSpore Transformers 那会儿,我干过一件现在想起来都后怕的事:把一个大模型训练脚本挂到服务器上,nohup一丢,人就下班了。第二天回来一看,loss 曲线在前 200 步就炸成了 NaN,白白烧了一整晚的算力。更离谱的是,我连它什么时候炸的、炸之前学习率是多少、梯度范数有没有异常,全都不知道——因为整个训练过程没有任何可视化记录,只有终端里滚过去的一堆日志,翻都翻不回来。
这件事之后我就给自己定了个规矩:任何超过半小时的训练任务,必须挂上 TensorBoard 监控。MindSpore 本身对 TensorBoard 的支持是相当完整的,mindspore.train.callback里直接提供了SummaryCollector和TensorBoard相关的回调接口,配合 MindSpore Transformers(也就是mindformers这套高层封装),可以在几乎不改动训练主逻辑的前提下,把 loss、学习率、梯度、权重分布、甚至计算图都记录下来。
这篇内容我想聊的就是:在 MindSpore Transformers 的训练流程里,怎么把 TensorBoard 这套监控真正用起来,而不是停留在"加个 callback 就完事"的层面。我会从监控指标该记什么、SummaryCollector和TensorBoard回调的区别、日志目录的组织方式、到实际排查训练异常时怎么读这些曲线,一步步拆开讲。适合已经能跑通 MindSpore Transformers 基础训练、但还没建立起系统监控习惯的朋友,也适合那些被"训练炸了却找不到原因"折磨过的同行。
先说一个核心结论:TensorBoard 的价值不在于"好看",而在于它把训练过程从黑盒变成了可回溯的时间序列。你记录的每一个标量,都是未来排查问题时的一条线索。
2. MindSpore 里记录 TensorBoard 日志的两条技术路线
很多人第一次用 MindSpore 记 TensorBoard,会卡在一个地方:到底该用SummaryCollector还是mindspore.train.callback.TensorBoard?这两个东西名字都跟 TensorBoard 有关,但职责完全不同,混用会出问题。我先把这两条路线的定位讲清楚。
2.1 SummaryCollector:负责"写",把数据落成 event 文件
SummaryCollector是 MindSpore 里真正干活的写入器。它的作用是在训练过程中收集各类 summary 数据,然后以 TensorBoard 能识别的 event 文件格式写到磁盘上。你调用它的时候需要指定一个summary_dir,所有 event 文件都会落在这个目录下。
from mindspore.train.callback import SummaryCollector summary_collector = SummaryCollector( summary_dir="./summary_dir", collect_freq=10, # 每 10 个 step 收集一次 collect_specified_data={ 'collect_metric': True, # 记录评估指标 'collect_train_lineage': True,# 记录训练血缘 'collect_graph': True, # 记录计算图 'histogram_regular': None, # 权重直方图,默认不记 }, keep_default_action=False, # 是否保留默认收集行为 )这里有几个参数值得单独说。collect_freq控制采样频率,默认是 10,意思是每 10 个 step 记一次。这个值不是越小越好——如果你训练步数上万,collect_freq=1会产生巨量 event 文件,磁盘 IO 会成为瓶颈,TensorBoard 加载也会卡到怀疑人生。我的经验是:短训练(几千步以内)用 1 到 10,长训练(几万步以上)用 50 到 100,既能看出趋势,又不会拖慢训练。
collect_specified_data里的collect_graph要特别小心。记录计算图对排查模型结构问题很有用,但它会显著增加启动时的开销,而且生成的图文件可能很大。如果你只是想做常规的 loss 监控,完全可以先关掉,等真需要看网络结构时再单独开一次。
2.2 TensorBoard 回调:负责"读",在训练中实时展示
mindspore.train.callback.TensorBoard这个回调,很多人误以为它是用来写日志的,其实它的核心职责是在训练过程中读取已经写好的 event 文件,并在终端或指定端口上做实时展示。它依赖tensorboard这个 Python 包,需要提前装好。
from mindspore.train.callback import TensorBoard tb_callback = TensorBoard( summary_dir="./summary_dir", log_dir="./tb_logs", batch_size=1, flush_interval=10, )注意summary_dir要和SummaryCollector的目录一致,这样它才能读到数据。log_dir是它自己输出展示相关文件的地方。flush_interval控制刷新频率,设太小会频繁 IO,设太大又看不到实时效果,10 到 30 之间比较合适。
提示:如果你只是想在训练结束后用浏览器打开 TensorBoard 看曲线,其实不需要
TensorBoard这个回调,只要SummaryCollector把 event 文件写好了,事后用tensorboard --logdir=./summary_dir启动就行。TensorBoard回调主要用在需要训练中实时观察的场景。
2.3 两条路线的组合策略
实际项目里我一般这么配:训练脚本里只挂SummaryCollector,把数据老老实实写下来;实时展示交给独立的 TensorBoard 进程。这样做的好处是训练进程和展示进程解耦,展示端崩了不影响训练,训练端也不用为了展示做额外 IO。
如果你确实需要训练中回调展示,那就两个都挂上,但要注意SummaryCollector必须排在TensorBoard回调前面,否则TensorBoard读不到刚写的数据。这个顺序问题我踩过一次,当时排查了半天才发现是 callback 列表顺序反了。
3. 在 MindSpore Transformers 训练流程里接入监控
MindSpore Transformers 的训练入口通常是通过配置文件驱动的,比如run_mindformer.py配合 YAML 配置。监控的接入点就在 callback 的组装环节。这一节我按实际项目里的接入顺序来讲。
3.1 找到 callback 的组装位置
在mindformers的训练流程里,callback 一般是在build_callback这类函数里组装的,或者直接在训练脚本里手动拼一个 callback 列表传给model.train()。你要做的是在这个列表里插入SummaryCollector。
from mindspore.train.callback import Callback, SummaryCollector, LossMonitor, TimeMonitor callbacks = [ LossMonitor(per_print_times=10), TimeMonitor(), SummaryCollector(summary_dir="./summary_dir", collect_freq=10), ]顺序上,LossMonitor和TimeMonitor负责终端打印,SummaryCollector负责落盘,互不干扰。但如果你还用了CheckpointConfig做权重保存,建议把 checkpoint 相关的 callback 放在最后,避免保存大文件时阻塞 summary 写入。
3.2 自定义指标:把 loss scale、学习率也记进去
默认的SummaryCollector会记录 loss 和部分评估指标,但训练大模型时,loss scale 和学习率这两个量往往比 loss 本身更能说明问题。loss scale 突然掉到 1 或者学习率没按预期衰减,都是训练异常的早期信号。这些量默认不一定被记录,需要你自己通过自定义 callback 补上。
from mindspore.train.callback import Callback from mindspore import SummaryRecord class CustomMetricsCallback(Callback): def __init__(self, summary_dir): super().__init__() self.summary_writer = SummaryRecord(summary_dir) def step_end(self, run_context): cb_params = run_context.original_args() cur_step = cb_params.cur_step_num # 从优化器或网络里取出当前学习率 lr = cb_params.train_network.optimizer.learning_rate self.summary_writer.add_value("learning_rate", lr, cur_step) self.summary_writer.record(cur_step) def end(self, run_context): self.summary_writer.close()这里用的是SummaryRecord,它是比SummaryCollector更底层的接口,适合做精细化的自定义记录。注意record(cur_step)这一步不能漏,否则数据不会真正落盘。end里要记得close(),不然最后一段数据可能丢。
3.3 日志目录的组织:别把所有实验堆在一个文件夹
这是我最想强调的一点。很多人图省事,所有实验都往./summary_dir里写,结果 TensorBoard 一打开,几十条曲线叠在一起,根本分不清哪条是哪次实验。正确的做法是按实验维度建目录,比如用时间戳或实验名做后缀。
import os import time exp_name = f"exp_lr5e-5_bs32_{time.strftime('%m%d_%H%M')}" summary_dir = os.path.join("./summary_dir", exp_name) os.makedirs(summary_dir, exist_ok=True)这样每次实验的数据独立存放,TensorBoard 启动时指定父目录./summary_dir,它会自动把子目录当成不同的 run 并列展示,对比起来非常直观。我现在的习惯是实验名里带上关键超参,比如学习率、batch size,这样光看曲线名字就能回忆起配置。
4. 训练异常时,TensorBoard 曲线到底该怎么读
监控搭起来只是第一步,真正体现价值的是训练出问题时,你能不能从曲线里读出原因。这一节我结合自己踩过的几个典型场景,讲讲不同异常在 TensorBoard 上的表现。
4.1 loss 曲线突然变 NaN:先看学习率和 loss scale
loss 变 NaN 是最常见的训练崩溃。光看 loss 曲线,你只能知道"它炸了",但不知道"为什么炸"。这时候要同时打开学习率和 loss scale 两条曲线对照看。
如果 loss 炸之前学习率有一个明显的尖峰,那基本可以确定是学习率调度出了问题,比如 warmup 阶段配置错误,或者衰减策略在某一步算出了异常值。如果学习率平稳但 loss scale 突然从几万掉到 1,那说明梯度里出现了 inf 或 nan,动态 loss scale 机制在自我保护。这时候要往梯度裁剪和数值稳定性方向查。
我遇到过一次,loss 在 3000 步左右周期性变 NaN,最后发现是某个 batch 的数据里有异常样本,导致梯度爆炸。这种问题光看 loss 是看不出来的,但把 loss scale 曲线拉出来,能看到它在崩溃前有明显的抖动,这就是线索。
4.2 loss 不降反升:区分"没学好"和"学过头"
loss 下降缓慢或者震荡,有两种可能:一种是学习率太小、模型没学动;另一种是学习率太大、在最优解附近来回跳。这两种在曲线上的形态不一样。
学习率太小的时候,loss 曲线是一条缓慢下降的平滑线,斜率很小但方向稳定。学习率太大的时候,loss 会上下震荡,甚至出现周期性起伏。这时候可以对照学习率曲线,如果学习率一直很小,那就调大;如果学习率在某个区间反复横跳,那就考虑加 warmup 或者降低峰值学习率。
注意:不要只看 loss 的绝对值判断好坏。不同 batch size、不同损失函数下,loss 的合理范围差别很大。关键是看趋势和相对变化。
4.3 梯度范数异常:大模型训练的隐形杀手
梯度范数(grad norm)是排查大模型训练问题最有用的指标之一,但它默认不被记录,需要你自己加。梯度范数突然飙升,往往预示着梯度爆炸;长期维持在极低值,则可能是梯度消失或者某些层根本没参与训练。
我在训练一个多层 Transformer 时,发现中间某几层的梯度范数长期接近 0,而首尾层正常。查下来是初始化方式的问题,中间层的参数初始化方差太小,导致前向输出被压制。这种问题如果不看梯度范数,光看 loss 是发现不了的——因为 loss 还在缓慢下降,看起来"正常"。
记录梯度范数的方法是在自定义 callback 里遍历网络参数求范数,或者用 MindSpore 提供的梯度监控接口。要注意的是,遍历所有参数求范数本身有开销,建议只在关键 step 记录,比如每 100 步一次。
4.4 权重直方图:看参数分布有没有跑偏
SummaryCollector支持记录权重直方图,通过histogram_regular参数控制记录哪些参数。这个功能开销较大,一般只在调试阶段开。权重直方图能告诉你参数分布是否健康:如果某一层的权重全部挤在一个极窄的区间,说明它几乎没更新;如果分布越来越宽甚至出现极端值,说明可能过拟合或者数值不稳定。
我一般会在训练初期开几次直方图,确认各层参数都在正常更新,之后就关掉,避免影响训练速度。
5. 让监控真正好用的几个工程细节
监控这东西,搭起来容易,用好难。下面这些细节都是我在实际项目里一点点磨出来的,分享出来能帮你少走弯路。
5.1 采样频率和训练速度的平衡
前面提过collect_freq的取值,这里再展开说。TensorBoard 的 event 文件写入是同步 IO,如果采样太频繁,会明显拖慢训练。我做过一个粗略测试:在同样的硬件上,collect_freq=1相比collect_freq=100,整体训练时间增加了大约 8% 到 15%,具体取决于磁盘性能。
所以我的建议是分阶段设置:训练前 1000 步用较小的collect_freq(比如 10),因为初期最容易出问题,需要密集监控;之后调大到 50 或 100,降低开销。这个切换可以通过自定义 callback 动态调整,或者干脆分两次训练。
5.2 event 文件过大怎么办
长时间训练会产生大量 event 文件,单个文件可能达到几个 GB,TensorBoard 加载时会非常慢甚至卡死。解决办法有两个:一是控制采样频率,从源头减少数据量;二是定期归档旧数据,只保留最近一段时间的日志。
# 查看 summary 目录大小 du -sh ./summary_dir/* # 归档超过 7 天的实验目录 find ./summary_dir -maxdepth 1 -type d -mtime +7 -exec tar -czf {}.tar.gz {} \; -exec rm -rf {} \;归档而不是直接删除,是因为有些实验的曲线过段时间可能还要回看。压缩后的 event 文件体积能小很多,需要时解压再加载即可。
5.3 远程训练时怎么看 TensorBoard
训练在远程服务器上跑,本地想看 TensorBoard,最稳妥的方式是通过端口转发把远程的 6006 端口映射到本地。这个操作在各类终端工具里都有对应功能,配置好之后本地浏览器访问localhost:6006就能看到远程的实时曲线。
如果不想做端口转发,也可以让训练把 event 文件同步到本地再加载,但这样就没有实时性了。我一般用端口转发,配置一次就行,后面所有实验都能复用。
5.4 把关键指标同时打到终端
TensorBoard 虽好,但不是所有时候都方便打开浏览器。我习惯在自定义 callback 里,把 loss、学习率、梯度范数这几个关键指标同时用logger.info打到终端日志里。这样即使没有 TensorBoard,翻日志也能大致判断训练状态。终端日志和 TensorBoard 曲线互为补充,一个适合快速扫一眼,一个适合精细分析。
6. 几个我踩过的坑和对应的排查思路
这一节单独拿出来讲坑,因为监控本身的坑往往比训练本身的坑更隐蔽——你以为你在监控,其实数据根本没记上。
6.1 曲线是空的:先查目录和 flush
最常见的问题是 TensorBoard 打开后什么都没有。排查顺序是这样的:先确认summary_dir路径对不对,event 文件有没有真的生成;再确认SummaryCollector有没有被正确加入 callback 列表;最后检查是不是数据写了但没 flush。
SummaryCollector默认会在一定步数后 flush,但如果训练异常退出,最后一段数据可能没落盘。这时候可以在end回调里手动触发一次 flush。另外,TensorBoard 启动时如果logdir指错了层级,也会显示为空——它只认包含 event 文件的目录,不认空目录。
6.2 曲线对不上:step 编号错位
有时候曲线能显示,但横轴 step 编号和实际训练步数对不上,比如实际跑了 5000 步,曲线只到 3000。这通常是采样频率和 step 计数方式不一致导致的。SummaryCollector记录的是它实际采样的 step,如果collect_freq=10,那它只在 10、20、30……这些 step 记录,中间的点是插值出来的。如果你同时用了多个记录源(比如自定义 callback 和SummaryCollector),它们的 step 基准可能不同,叠在一起就会错位。
解决办法是统一 step 来源,所有记录都用cb_params.cur_step_num作为横轴,不要自己另算一套。
6.3 训练变慢:定位是不是监控的锅
如果加了监控之后训练明显变慢,先做个对照实验:把SummaryCollector去掉跑 100 步,记下耗时;再加回来跑 100 步,对比差异。如果确实是监控导致的,优先调大collect_freq,其次关掉collect_graph和直方图,最后检查磁盘 IO 是不是瓶颈。
我遇到过一次训练变慢,排查半天发现不是监控本身,而是 summary 目录和 checkpoint 目录在同一个慢速磁盘上,两者 IO 互相抢占。把 summary 目录挪到本地 SSD 后就正常了。所以监控的性能问题,有时候根子在存储布局上。
6.4 多卡训练时日志混乱
多卡训练时,如果每张卡都往同一个summary_dir写,event 文件会互相覆盖或者混在一起,曲线完全没法看。正确做法是每张卡写各自的子目录,用 rank id 区分。
import mindspore.communication as comm rank_id = comm.get_rank() summary_dir = os.path.join(base_dir, f"rank_{rank_id}")这样每张卡的曲线独立,需要对比时在 TensorBoard 里勾选不同 rank 即可。如果只想看整体趋势,通常看 rank 0 的就够了,其他卡作为参考。
7. 把监控变成训练流程的固定环节
聊了这么多,最后说说我现在的做法。我已经把 TensorBoard 监控固化成了训练脚本的标配:每个训练任务启动时自动创建带时间戳和关键超参的 summary 目录,SummaryCollector默认挂上,自定义 callback 负责记录学习率、loss scale 和梯度范数,训练结束后自动归档旧日志。
这套流程跑下来,最大的感受是排查问题的效率完全不一样了。以前训练炸了只能靠猜,现在打开 TensorBoard,对着 loss、学习率、loss scale、梯度范数四条曲线一看,问题范围基本能锁定到具体方向。省下来的时间,远比搭监控花的时间多。
如果你现在还没养成记录训练指标的习惯,我的建议是从最小配置开始:先只记 loss 和学习率,跑通一次完整训练,感受一下有曲线和没曲线的区别。等你习惯了这种"看得见"的训练方式,再逐步加上梯度范数、权重直方图这些进阶指标。监控这件事,起步越早,后面越省心。