1. 训练监控这件事,为什么值得单独拎出来说
搞深度学习训练的人都有一个共识:模型跑起来只是第一步,真正折磨人的是“它到底学得怎么样”。尤其是用 MindSpore Transformers 跑大模型微调或者预训练的时候,一次训练动辄几个小时甚至几天,你不可能一直盯着终端刷日志。终端里那一行行 loss 数值滚过去,等你发现 loss 变成 NaN 的时候,可能已经白白烧了几个小时的算力。
所以训练在线监控不是一个“锦上添花”的功能,而是刚需。TensorBoard 作为深度学习领域最成熟的训练可视化工具之一,在 PyTorch 和 TensorFlow 生态里几乎是默认选项。MindSpore 这边其实也提供了对 TensorBoard 的支持,只是很多刚接触 MindSpore Transformers 的朋友不太清楚怎么把它接进去、怎么配置、怎么在训练过程中实时看到曲线。
这篇文章就是围绕“MindSpore Transformers 训练在线监控:TensorBoard 效果”这个主题,把整套流程拆开来讲。从为什么要用 TensorBoard、MindSpore 里怎么对接、MindSpore Transformers 框架下具体怎么配置、到实际跑起来之后 TensorBoard 面板上能看到什么效果、遇到问题怎么排查,我都会按实操顺序一步步说清楚。不管你是刚上手 MindSpore 的新手,还是已经从 PyTorch 迁过来的老手,应该都能从里面找到能直接抄作业的东西。
先说一下我自己的场景:我用 MindSpore Transformers 做 LLM 的 LoRA 微调,模型规模在 7B 左右,跑在 Ascend 上。训练过程中我最关心的几个指标是 loss 曲线是否平滑下降、学习率调度是否符合预期、梯度范数有没有异常飙升、以及每个 step 的吞吐量是否稳定。这些指标如果只靠终端日志,排查效率极低,但全部丢到 TensorBoard 上,一眼就能看出问题在哪。下面我把这套东西完整地展开。
2. MindSpore 与 TensorBoard 的对接原理和方案选型
2.1 MindSpore 侧有哪些监控手段,为什么选 TensorBoard
MindSpore 本身提供的训练监控手段其实不少,常见的有这几种:
- 终端日志打印:最原始的方式,通过
print或者 MindSpore 的日志模块输出 loss、lr 等数值。优点是零配置,缺点是只能看当前值,没法看趋势,更没法对比多次实验。 - MindInsight:这是 MindSpore 生态里官方的可视化工具,功能很全,能看计算图、算子耗时、数据下沉、loss 曲线等等。但它的部署相对重一些,需要单独启动一个 Web 服务,而且和团队里用 PyTorch 的同事协作时,大家习惯不统一。
- Callback 机制自定义输出:MindSpore 的
Callback体系允许你在训练的不同阶段插入自定义逻辑,你可以自己写一个 callback 把指标写到文件或者数据库里。灵活但工作量大。 - TensorBoard:通过
SummaryCollector或者summary相关接口,把训练指标写成 TensorBoard 能读的事件文件(event file),然后用 TensorBoard 前端展示。
我选 TensorBoard 的核心理由有三个。第一,生态通用。团队里不管用 PyTorch 还是 TensorFlow,TensorBoard 都是大家最熟悉的界面,不需要额外学习成本。第二,轻量。TensorBoard 就是一个 Python 包,pip install tensorboard之后tensorboard --logdir就能起,不需要额外的服务端组件。第三,对比方便。TensorBoard 天然支持多实验对比,你把不同 run 的日志放在不同子目录下,它能把多条曲线叠在一起看,这对调参阶段的帮助非常大。
提示:MindSpore 从 1.x 版本开始就提供了 TensorBoard 支持,主要通过
mindspore.train.summary.SummaryCollector这个 callback 来实现。不同版本 API 位置可能略有差异,建议先确认自己的 MindSpore 版本。
2.2 SummaryCollector 的工作机制
SummaryCollector本质上是一个 MindSpore 的 Callback,它在训练过程中按照你设定的频率,把指定的标量、图像、计算图等信息序列化写入到磁盘上的 event 文件中。TensorBoard 启动后会读取这些 event 文件并渲染成可视化面板。
它的核心参数有这么几个,我逐个解释一下:
| 参数 | 作用 | 常用取值 |
|---|---|---|
summary_dir | event 文件输出目录 | ./summary/exp01 |
collect_freq | 采集频率(多少个 step 采集一次) | 10 或 100 |
collect_specified_data | 指定采集哪些数据 | 字典形式,控制 loss、lr、计算图等 |
keep_default_action | 是否保留默认采集行为 | True/False |
custom_lineage_data | 自定义实验元信息 | 记录超参用 |
这里最关键的是collect_freq。设太小会导致 event 文件膨胀得很快,尤其是你训练几十万 step 的时候,磁盘很快就满了;设太大又会导致曲线不够平滑,看不出细节。我一般根据总 step 数来定:总 step 在 1 万以内,collect_freq=10;1 万到 10 万,collect_freq=50或100;10 万以上,collect_freq=100到500。
2.3 为什么不用 MindSpore Transformers 自带的日志
MindSpore Transformers(也就是mindformers这个库)本身在训练脚本里是有日志输出的,通过logger打印 loss、lr、吞吐等信息。这些日志对于快速查看是够用的,但它有几个硬伤:一是只能看当前值,没法回溯趋势;二是多卡训练时日志会交错,看起来非常乱;三是没法做实验对比。
所以我的做法是:保留 mindformers 自带的日志输出作为兜底,同时额外挂一个 SummaryCollector 把关键指标写到 TensorBoard。两者不冲突,日志用于快速定位当前状态,TensorBoard 用于看全局趋势和对比实验。
3. 在 MindSpore Transformers 中接入 TensorBoard 的完整实操
3.1 环境准备与依赖确认
在动手之前,先把环境确认清楚。你需要的东西不多,但版本要对上。
# 确认 MindSpore 版本 python -c "import mindspore; print(mindspore.__version__)" # 确认 mindformers 版本 python -c "import mindformers; print(mindformers.__version__)" # 安装 tensorboard(如果还没装) pip install tensorboard这里有个坑我要提前说:TensorBoard 的版本和 MindSpore 的版本之间存在兼容性要求。MindSpore 写 event 文件用的是tensorboardX或者内置的 protobuf 序列化逻辑,如果 TensorBoard 版本太新,有时候会出现 event 文件读不出来、面板空白的情况。我实测下来,TensorBoard 2.9 到 2.14 这个区间和 MindSpore 2.2/2.3 配合比较稳。如果你装的是最新的 2.16+,遇到读不出来的问题,先降版本试试。
另外,如果你用的是 Ascend 环境,确认一下te和topi这些包是否正常,因为 SummaryCollector 在采集计算图信息时会用到它们。如果只采集标量(loss、lr),一般不会触发这些依赖。
3.2 找到训练入口,确定 Callback 注入点
MindSpore Transformers 的训练入口通常在run_mindformer.py或者你自定义的训练脚本里。核心逻辑是构建Trainer或者直接用mindspore.train.Model来包装网络和优化器。
以run_mindformer.py为例,训练流程大致是这样的:
# 伪代码,展示关键结构 from mindformers import Trainer, TrainingArguments from mindformers.trainer import build_trainer training_args = TrainingArguments(...) trainer = build_trainer(training_args) trainer.train()Callback 的注入点就在trainer.train()这一步。MindSpore Transformers 的 Trainer 内部会构建mindspore.train.Model,而Model.train()接受callbacks参数。所以你有两种注入方式:
方式一:修改训练脚本,在构建 Trainer 时传入 callbacks。这种方式最直接,但需要你改源码或者写一个包装脚本。
方式二:通过配置文件或者 TrainingArguments 传入。部分版本的 mindformers 支持在配置里指定 callback,但支持程度不一,需要看具体版本。
我一般用方式一,因为可控性最强。下面给出具体的代码。
3.3 编写并注入 SummaryCollector
假设你的训练脚本里已经构建好了model(mindspore.train.Model实例),那么注入 SummaryCollector 的代码如下:
import os from mindspore.train.summary import SummaryCollector # 为每次实验创建独立的目录,方便对比 summary_dir = os.path.join("./summary", "exp_lora_r8_lr2e5") os.makedirs(summary_dir, exist_ok=True) # 构建 SummaryCollector summary_collector = SummaryCollector( summary_dir=summary_dir, collect_freq=50, # 每 50 个 step 采集一次 collect_specified_data={ 'collect_metric': True, # 采集 loss 等指标 'collect_learning_rate': True, # 采集学习率 'collect_train_lineage': True, # 采集训练溯源信息 'collect_graph': False, # 计算图比较大,按需开启 }, keep_default_action=False, # 关闭默认采集,避免冗余 ) # 注入到 model.train 的 callbacks 里 model.train( epochs, train_dataset, callbacks=[summary_collector], dataset_sink_mode=False, # 注意这个参数,后面会讲 )如果你用的是 mindformers 的 Trainer,注入方式类似,但需要找到 Trainer 内部构建 Model 的地方。一个取巧的办法是继承 Trainer 并重写train方法,在调用父类方法前把 callback 塞进去。或者更简单:直接在run_mindformer.py里找到model.train的调用处,把 callback 加进去。
注意:
dataset_sink_mode这个参数非常关键。当它设为True时,数据会下沉到设备侧,训练循环在设备上执行,此时 SummaryCollector 的采集行为会受到影响,部分指标可能采集不到或者采集频率不准。如果你发现 TensorBoard 上曲线断断续续或者只有几个点,第一件事就是检查这个参数。我一般做监控调试时先设为False,确认曲线正常后再根据性能需要决定是否开启 sink。
3.4 自定义指标采集:把梯度范数和吞吐量也丢进去
SummaryCollector 默认采集的是 loss 和学习率,但实际调参时我还想看梯度范数和吞吐量。这两个指标 SummaryCollector 不直接支持,需要自己写 callback 来采集。
思路是这样的:写一个自定义 Callback,在step_end阶段计算梯度范数,然后通过SummaryRecord手动写入 event 文件。
import mindspore as ms from mindspore.train.callback import Callback from mindspore.train.summary import SummaryRecord import numpy as np class GradNormCallback(Callback): def __init__(self, summary_dir, collect_freq=50): super().__init__() self.summary_dir = summary_dir self.collect_freq = collect_freq self.record = None self.step = 0 def begin(self, run_context): # 注意:SummaryRecord 和 SummaryCollector 不能同时写同一个目录 # 所以这里用子目录 self.record = SummaryRecord( log_dir=os.path.join(self.summary_dir, "custom") ) def step_end(self, run_context): cb_params = run_context.original_args() self.step += 1 if self.step % self.collect_freq != 0: return # 获取网络参数梯度 network = cb_params.train_network grads = network.gradients if hasattr(network, 'gradients') else None if grads is not None: total_norm = 0.0 for g in grads: if g is not None: total_norm += float(ms.ops.sum(g ** 2).asnumpy()) total_norm = np.sqrt(total_norm) self.record.add_value("grad_norm", total_norm, self.step) def end(self, run_context): if self.record: self.record.close()这段代码有几个细节要注意。第一,SummaryRecord和SummaryCollector如果写同一个目录,可能会产生冲突,所以我单独开了一个custom子目录。第二,梯度范数的计算方式我采用的是全局 L2 范数,也就是所有参数梯度平方和的平方根,这是最常用的衡量方式。第三,network.gradients这个属性不是所有网络都有,取决于你的网络构建方式,如果没有,可以通过network.trainable_params()配合ms.grad来手动计算,但那样开销会大一些。
吞吐量(samples/sec 或 tokens/sec)的采集类似,在step_end里记录时间戳,算差值即可。这个指标对于判断训练是否稳定、有没有出现数据加载瓶颈非常有用。
3.5 启动 TensorBoard 并验证
训练跑起来之后,event 文件会写到summary_dir下。启动 TensorBoard:
tensorboard --logdir ./summary --port 6006 --host 0.0.0.0--logdir指向你的 summary 根目录,TensorBoard 会自动扫描子目录,每个子目录作为一个独立的 run 展示。--host 0.0.0.0是为了让同一网络下的其他机器也能访问(比如你在服务器上跑训练,在本地笔记本上看面板)。
启动后浏览器打开http://<服务器IP>:6006,就能看到面板了。如果面板是空的,先检查 event 文件有没有生成:
ls -lh ./summary/exp_lora_r8_lr2e5/ # 应该能看到 events.out.tfevents.xxxxx 这样的文件如果文件存在但 TensorBoard 读不出来,大概率是版本兼容问题,参考前面说的降级 TensorBoard。
4. TensorBoard 面板上到底能看到什么效果
4.1 Loss 曲线的正确打开方式
Loss 曲线是 TensorBoard 上最核心的面板。但很多人只看一条线,其实里面有很多门道。
首先,平滑系数(Smoothing)要调。TensorBoard 右上角有个 Smoothing 滑块,默认 0.6。原始 loss 曲线往往抖动很大,看起来像锯齿,调大平滑系数(0.9 左右)能看出整体趋势。但注意,平滑只是视觉上的,不改变数据本身。我一般会同时看平滑和原始两条,平滑看趋势,原始看异常。
其次,train loss 和 eval loss 要一起看。如果你在训练中插入了验证,SummaryCollector 会把 eval 的 loss 也写进去。两条线放在一起,如果 train loss 持续下降但 eval loss 开始上升,那就是过拟合的信号,该考虑加正则或者早停了。
第三,多 run 对比。这是我用 TensorBoard 最爽的地方。比如我同时跑了三组实验:LoRA rank 分别设为 4、8、16,三个 run 的 loss 曲线叠在一起,哪个收敛快、哪个最终 loss 低,一目了然。这也是为什么我强调每次实验要用独立的 summary 子目录。
4.2 学习率调度曲线:确认你的 scheduler 有没有生效
学习率曲线看起来简单,但它是排查训练异常的重要依据。我遇到过好几次 loss 不下降的情况,最后发现是 warmup 配置写错了,学习率一直是 0。如果当时有 TensorBoard 看学习率曲线,一眼就能发现。
常见的调度策略有 cosine decay、linear decay、constant with warmup 等。在 TensorBoard 上,你应该能看到学习率从 warmup 阶段线性上升,然后按照设定的策略下降。如果曲线形状和你预期的不一样,那就是配置有问题。
提示:SummaryCollector 采集学习率需要
collect_learning_rate=True,而且要求你的优化器是通过 MindSpore 的LearningRateSchedule体系构建的。如果你用的是自定义的学习率更新逻辑,可能采集不到,需要手动通过 SummaryRecord 写入。
4.3 梯度范数曲线:判断训练稳定性的利器
梯度范数是我在微调大模型时最关注的指标之一。正常情况下,梯度范数应该在一个合理的范围内波动,随着训练进行逐渐趋于稳定。如果出现以下情况,就要警惕:
- 梯度范数突然飙升:可能是遇到了异常样本,或者学习率太大导致震荡。这时候可以考虑加梯度裁剪(gradient clipping)。
- 梯度范数持续下降趋近于 0:可能是梯度消失,模型学不动了。对于深层网络,检查一下初始化或者归一化层。
- 梯度范数剧烈震荡:batch size 可能太小,或者数据分布不均匀。
在 TensorBoard 上,梯度范数曲线配合 loss 曲线一起看,能快速定位大部分训练不稳定问题。
4.4 吞吐量曲线:发现性能瓶颈
吞吐量曲线反映的是每秒处理的样本数或 token 数。理想情况下,这条线应该是平稳的。如果出现周期性下降,可能是数据加载跟不上(dataloader 的 num_workers 设小了),或者有其他的 IO 瓶颈。如果吞吐量整体偏低,可能需要检查是否开启了数据下沉、混合精度是否生效、通信是否成为瓶颈(多卡场景)。
我在一次多卡训练中发现吞吐量周期性掉到接近 0,排查后发现是 checkpoint 保存太频繁,每次保存都会阻塞训练。后来把保存间隔从 500 step 改成 2000 step,吞吐量曲线就平稳了。这种问题如果只看终端日志,很难发现规律,但在 TensorBoard 上一眼就能看出周期性。
5. 常见问题与排查技巧实录
5.1 TensorBoard 面板空白或曲线不显示
这是最常见的问题,排查顺序如下:
| 排查项 | 检查方法 | 解决方法 |
|---|---|---|
| event 文件是否生成 | ls summary_dir/ | 检查 summary_dir 路径是否正确,是否有写权限 |
| TensorBoard 版本兼容 | pip show tensorboard | 降级到 2.9-2.14 区间 |
| logdir 路径是否正确 | 确认启动命令的 --logdir | 指向 summary 根目录,不是子目录 |
| 端口是否被占用 | lsof -i:6006 | 换端口或杀掉占用进程 |
| 浏览器缓存 | 强制刷新 Ctrl+Shift+R | 清除缓存或换浏览器 |
我踩过最坑的一次是:summary_dir 设成了相对路径,而训练脚本的工作目录和我以为的不一样,结果 event 文件写到了别的地方。后来我统一改成绝对路径,再也没出过这个问题。
5.2 曲线只有几个点,采集频率不对
如果你发现 TensorBoard 上曲线只有零星几个点,大概率是collect_freq设太大了,或者训练 step 数太少。比如你总共只跑 100 step,collect_freq=100,那只会采集到一个点。这时候把collect_freq调小到 5 或 10。
另一个可能的原因是dataset_sink_mode=True导致采集行为异常。前面说过,sink 模式下训练循环在设备侧执行,callback 的触发时机和 step 计数可能和你预期不一致。调试阶段建议先关掉 sink。
5.3 多卡训练时 event 文件冲突
多卡训练时,如果每个卡都往同一个 summary_dir 写 event 文件,会出现文件冲突或者数据混乱。正确的做法是每张卡写自己的子目录,比如按 rank 区分:
import mindspore.communication as comm rank_id = comm.get_rank() summary_dir = os.path.join("./summary", f"exp01_rank{rank_id}")然后在 TensorBoard 上,你可以选择只看 rank0 的曲线(通常 rank0 的 loss 最具代表性),或者把所有 rank 的曲线都打开对比,看看各卡的 loss 是否同步。如果各卡 loss 差异很大,可能是数据分片不均匀或者通信有问题。
5.4 event 文件过大导致磁盘爆满
这个问题在长训练中很常见。collect_freq设太小、采集项太多(比如开了计算图采集)、训练 step 数很大,这三个因素叠加,event 文件能轻松涨到几十 GB。
我的应对策略是:第一,collect_freq不要低于 10;第二,计算图采集只在调试时开,正式训练关掉;第三,定期清理旧的 summary 目录,或者用脚本只保留最近 N 次实验。另外,TensorBoard 本身也提供了--purge_orphaned_data等参数来控制内存,但对磁盘占用帮助有限,根本还是控制采集频率和采集项。
5.5 自定义指标不显示
如果你按 3.4 节写了自定义 callback,但 TensorBoard 上找不到对应的曲线,检查这几点:SummaryRecord的log_dir是否和 TensorBoard 的--logdir有包含关系;add_value的 tag 名是否和你在面板上找的一致(TensorBoard 会按 tag 分组);record.close()是否被调用(不调用的话数据可能没刷到磁盘)。我遇到过一次是close()没调,训练中途去看面板什么都没有,训练结束后才出现,就是因为数据缓存在内存里没落盘。
6. 一些让监控更顺手的经验配置
6.1 用脚本管理实验目录
手动创建 summary 目录很容易乱,我一般写一个小工具函数,根据实验配置自动生成目录名:
import hashlib import json def make_summary_dir(base_dir, config: dict): # 用配置的 hash 作为目录名后缀,保证可追溯 config_str = json.dumps(config, sort_keys=True) hash_suffix = hashlib.md5(config_str.encode()).hexdigest()[:8] exp_name = config.get("exp_name", "exp") summary_dir = os.path.join(base_dir, f"{exp_name}_{hash_suffix}") os.makedirs(summary_dir, exist_ok=True) # 把配置也存一份,方便回溯 with open(os.path.join(summary_dir, "config.json"), "w") as f: json.dump(config, f, indent=2) return summary_dir这样每次实验的目录名都带配置 hash,不会重复,而且目录里存了完整的配置,过几个月回头看也知道当时跑了什么。
6.2 TensorBoard 的实用启动参数
除了基本的--logdir和--port,还有几个参数很实用:
--reload_interval:event 文件重新加载的间隔,默认 5 秒。训练时想更快看到最新曲线,可以设成 2 或 3。--samples_per_plugin:控制每个面板保留多少个数据点,默认 1000。数据点太多会拖慢浏览器,可以适当调小。--max_reload_threads:多实验对比时,增加加载线程数能加快面板渲染。
6.3 把关键超参记录到 TensorBoard
TensorBoard 有一个 HParams 面板,可以记录超参和指标的关系。MindSpore 的 SummaryCollector 通过custom_lineage_data参数支持记录自定义元信息,但 HParams 面板的支持不如 PyTorch 那边完善。我的替代方案是:把超参写进 summary 目录的 config.json,同时在 TensorBoard 的 TEXT 面板里写一段实验说明。这样虽然不能自动做超参对比,但至少信息是完整的。
6.4 训练中断后如何续接监控
如果训练中断后从 checkpoint 恢复,SummaryCollector 默认会重新开始 step 计数,导致曲线出现断层或者重叠。解决办法是在恢复训练时,手动设置SummaryCollector的初始 step,或者干脆开一个新的 summary 子目录,在目录名里标注resume。我一般选后者,因为这样历史曲线不会被污染,对比起来更清晰。
6.5 远程查看的注意事项
训练通常在服务器上,而你在本地看面板。除了--host 0.0.0.0,还要确认服务器的防火墙是否放行了对应端口。如果服务器在云上,安全组规则也要检查。另外,TensorBoard 的面板数据是通过 HTTP 传输的,如果训练时间很长、数据点很多,首次加载可能会比较慢,耐心等一下,或者用--samples_per_plugin限制数据点数量。
我在实际使用中最大的体会是:监控配置这件事,前期花十分钟配好,后期能省下几个小时甚至几天的排查时间。尤其是做多组对比实验的时候,TensorBoard 的多 run 叠加功能简直是救命稻草。你不需要记住每个实验的最终 loss 是多少,打开面板,所有曲线一目了然。
最后再分享一个小技巧:如果你的实验比较多,可以在 TensorBoard 左侧的 run 列表里给每个 run 改颜色和显示名,把重要的实验标成醒目的颜色,把失败的实验隐藏掉。这样面板会清爽很多,汇报的时候也方便截图。