一个周末的下午,我盯着本地那块 6GB 显存的显卡,开始了新项目的第一次训练。数据集近 20G,模型打算用 resnet 预训练模型做微调,结果刚开始跑,CUDA out of memory 的红色报错就打了我一个措手不及。我被逼着把目光放到了华为云 ModelArts 上,断断续续花了一周时间,从一个只会写 Python 训练脚本的新手,到完整走通“上传数据—云上训练—调参—部署在线服务”的全流程。
这篇笔记是在 ModelArts 上完成模型训练与部署之后沉淀下来的,记录了我所有踩过的坑、卡住的环节、以及最后总结出的可直接照抄的流程。如果你是第一次接触这个平台,或者想在云上训练模型、部署一个能调用的推理接口,这篇内容应该能帮你节省大量时间。
1. 项目概述:为什么把训练任务搬上 ModelArts
1.1 本地训练的场景瓶颈
开始之前,先聊清楚一件事:我为什么放着本地显卡不用,非要去搞云平台。做模型训练的人应该都有同感,本地训练的问题其实不是“能不能跑”,而是“跑得不爽”。
第一是显存上限。我的消费级显卡显存只有 6GB,跑 resnet 这类经典的图像分类模型,batch size 稍微设大一点就爆显存。batch size 一旦变小,模型收敛速度肉眼可见地变慢,一个 epoch 要跑十几分钟。第二是环境维护。一个项目用 PyTorch 1.8,另一个项目用 TensorFlow 2.4,还有项目需要老版本的 NumPy,本地用 conda 建了一堆环境,时间久了自己都分不清哪个环境对应哪个项目,更别提到处是 CUDA Toolkit 和 cuDNN 的版本兼容问题。第三个问题是资源占用。一次云上训练至少占掉我本地机器大半天,期间想做个 PPT 都卡,更不可能一边训练一边干别的。
如果你对这几件事很熟悉,那你应该能理解,换到平台化、资源隔离的训练环境几乎是不可避免的选择。
1.2 ModelArts 能解决什么
ModelArts 是华为云上的一站式 AI 开发平台,核心价值在于把整个模型生命周期串在一起,而不是只给一个裸 GPU。对我来说,它解决了三件事。
第一是弹性算力。我需要多大显存就用多大规格的实例,从几 GB 到几十 GB 都有,按需申请,用完释放,不用自己囤显卡。第二是环境预置。平台已经预装好了 MindSpore、PyTorch、TensorFlow 等常用框架的镜像,我不用花两天时间去搭 CUDA 和 Python 环境。第三是部署闭环。训练完的模型可以直接注册,然后一键部署成在线 API 服务,不用再单独写 Flask 接口、配置 GPU 服务器。
整体流程相当清晰,大致是:数据传到对象存储 OBS → 创建训练作业 → 查看日志与指标 → 注册模型 → 部署在线服务。我个人的经验是,一旦你理解了这条流水线,后面所有操作都只是在其中填内容,而不是每次从零开始。
2. 环境准备与数据管理:从零到一手训练数据
2.1 从华为云获取数据:OBS 桶与 moxing
跟大多数云平台类似,ModelArts 不直接管理训练数据,而是依赖对象存储服务 OBS。你需要先创建一个 OBS 桶,把数据集传进去,然后训练作业再从桶里拉数据。
我第一次用 OBS 的时候,其实是被“从华为云获取数据”这句话绕了一下。一开始我以为是登录网页直接上传,后来发现那样传大量小文件效率很低。正确姿势是用 Python SDK 或者命令行工具传,逻辑和传文件到网盘完全不一样。
把数据从 OBS 传到训练容器里,官方推荐用内置的 moxing,它在 ModelArts 的公共镜像里已经预装了。我习惯在训练脚本的开头写上:
import moxing as mox # 把 OBS 桶里的数据复制到训练容器的本地路径 mox.file.copy_parallel("/your_bucket/data/", "/tmp/dataset/") # 训练结束后,把输出模型复制回 OBS mox.file.copy_parallel("/tmp/output/", "/your_bucket/output/")如果你用 OBS 的原始 SDK,可能还需要处理 AK/SK 认证,而 moxing 在训练作业里已经帮你继承了权限,这点非常方便。不过有个细节我得提醒:copy_parallel适合大量小文件传输,如果是几个大文件,直接用mox.file.copy反而更稳。
2.2 挑选合适的预训练模型
模型选择是训练质量的决定性因素之一。如果你任务的数据量不够大,又从零开始训练一个完整模型,大概率会陷入欠拟合或者需要极长时间调参的尴尬。我这次用的是 resnet 预训练模型,官方在 ModelArts 的预置模型列表里直接给了下载入口,也支持去 ModelZoo 拉。
选预训练模型的逻辑其实很简单:预训练权重已经在大规模数据上学到了通用特征,你只需要在新的小数据集上微调,也就是把最后几层替换成你自己的分类头。举个例子,resnet 原本是解决 1000 类分类问题,你的任务只有 10 类,那就要删掉最后的全连接层,换一个新的输出层。
如果你做的是中文 NLP 任务,与 resnet 对应的是 roberta 中文预训练模型这一类的 transformer 模型。处理思路完全一致,冻结大部分层,只微调后几层和分类头。
微调时学习率是个关键点。预训练模型本身已经收敛到一个比较好的位置,如果学习率太大,会把原来的特征破坏掉。我自己的经验是,先用 1e-4 量级的较小学习率,再配合 Warmup,效果明显比直接用 1e-3 稳定。这个后面还会展开聊。
2.3 数据集的目录规范
训练最怕的不是模型复杂,而是数据一团乱。我第一次做的时候,把训练集和验证集混在一个文件夹里,结果训练作业起来半天后才发现评估指标异常,浪费了不少钱和时间。
后来我固定了一套目录规范,不搞花活:
dataset/ train/ class_a/ a_001.jpg a_002.jpg class_b/ b_001.jpg val/ class_a/ class_b/另外要留意样本不均衡的问题。如果某个类别样本特别少,训练出来的模型会对这个类别识别得极差。常规做法是给少数类增加采样权重,或者先做数据增强增加样本多样性。这个阶段多做一点检查,绝对比训练到一半发现训练集和验证集重合要有价值。
3. 模型训练:把脚本跑进云端作业
3.1 训练脚本改造要点
本地训练脚本搬到 ModelArts,最大的改动不是算法,而是数据路径和输出路径。我在本地训练时习惯用相对路径读数据,在云上必须改成绝对路径,并且通过命令行参数传入,这样同一份代码才能本地和云端都能跑。
官方文档会在训练作业里提供一组路径参数,比如data_url是训练数据的 OBS 路径,train_url是输出模型的路径。我用 PyTorch 写了个兼容脚本,核心长这样:
import argparse import moxing as mox parser = argparse.ArgumentParser() parser.add_argument("--data_url", type=str, default="") parser.add_argument("--train_url", type=str, default="") parser.add_argument("--batch_size", type=int, default=32) parser.add_argument("--epochs", type=int, default=50) parser.add_argument("--lr", type=float, default=1e-4) args = parser.parse_args() # 从 OBS 拉数据 mox.file.copy_parallel(args.data_url, "/tmp/data") # 训练完成后,把模型和日志写回 OBS mox.file.copy_parallel("/tmp/model_output", args.train_url)你可能会问,为什么非得 copy 到本地路径再训练?因为训练容器是临时创建的,直接读 OBS 虽然也行,但每次读文件都要走网络,小文件多的时候性能很差。拉到本地磁盘是最稳的做法。
还有一点,云上训练作业结束后容器就会被释放,所以模型权重、日志、tensorboard 输出都要在训练尾声统一写到train_url,否则就全丢了。这个教训我第一次就踩过,训练的模型还没保存,作业就结束了,血亏。
3.2 创建训练作业:关键配置细节
创建训练作业时,有几个配置项需要认真看,而不是直接点默认值。
AI 引擎:选 PyTorch 还是 MindSpore,取决于你的代码和预训练模型来源,建议跟本地代码保持一致。我用 PyTorch 写得顺手,就选了 PyTorch。
训练规格:这是计费的起点。如果你是想跑大规模训练,可以按 GPU 显存来选,比如我选的实例是 16GB 显存,batch size 设 64,跑 resnet-50 的微调不会爆。想省钱的思路是:先用最小规格跑通代码,比如很小的实例或者免费的 GPU 资源,确认代码没问题之后,再申请大资源做正式训练。这样做可以避免花大价钱发现脚本有个低级 bug。
数据路径和输出路径:把第 3.1 节的 OBS 桶路径填进去。这里最容易犯的错误是填了 OBS 桶根目录,而不是具体子目录,导致训练脚本读取到大量无关文件。
日志路径:默认会存到训练作业详情里,我建议直接开启日志同步到 OBS,方便后续排查。
3.3 训练过程监控与 nan 排查
训练期间,我习惯每隔几分钟刷一次作业详情页,看 loss 曲线是否下降。ModelArts 的日志视图可以实时看到 stdout 输出,比本地拿终端盯还要方便。
有一次训练,跑到第 3 个 epoch,loss 突然显示为nan,我盯着屏幕看了半天,第一反应是“数据里有脏值”。排查后发现问题根本不是数据,而是模型在训练过程中发生了梯度爆炸。这类“模型训练报 nan”的原因很典型,大概有几种:
- 学习率太大:loss 在前期直接冲上天,然后变成 nan。这种最常见,把学习率从 1e-3 降到 1e-4,问题就消失了。
- 数据没有做归一化:像素值直接当输入,数值范围过大,会放大梯度的波动。
- 损失函数计算溢出:比如某些经过 exp 操作的损失函数,数值范围过大时会出现 inf,最终变成 nan。
- 标签错位:数据集的标注里混入了 null 或极值。
我的排查套路是:先在本地用一个很小的 batch 复现,如果本地没复现,那大概率是数据读取或增强逻辑在云上表现不一致。如果本地立刻复现,就逐项检查学习率、归一化、损失函数。再配合 TensorBoard 看每一层梯度的变化,找到爆点的速度会快很多。
4. 模型评估与调优:进步往往发生在细节里
4.1 评估指标与模型注册
训练结束后,ModelArts 会给出最终的 loss 和 accuracy。这两个指标只能看个大概,想评估模型哪一类表现差,还是要靠混淆矩阵和各类别的 precision、recall。我第一次只盯着总 acc,后来发现某个类别几乎没被识别出来,这就是典型的样本不均衡导致的“虚假繁荣”。
常规做法是在训练脚本中定期记录验证集上的混淆矩阵日志,最后找一个你觉得符合业务需求的 checkpoint 注册。ModelArts 的模型注册会把 OBS 上的模型文件、推理代码、配置文件打包成一个可部署的模型版本,这一步是衔接训练和部署的关键。
注册的时候,版本号和模型描述一定要写清楚,尤其之后你可能会迭代很多版,没有版本管理,第二天你自己都分不清哪个模型对应哪个实验。
4.2 超参数调优心得
超参数调优这块,我和很多朋友交流过,大家公认最值得先调的就是学习率、batch size 和 epoch 数量。但注意,这三个参数之间是有耦合的:学习率需要根据 batch size 调整,batch size 变了,最优学习率也会跟着变。
一个比较踏实的手动调参顺序是:先用小 batch 比如 16,找到能稳定收敛的学习率范围,再逐步放大 batch size 到 32 或 64,同时按比例微调学习率。这个思路比较接近“从小处入手”的原则,比一次性把所有参数都抛给自动搜索要容易掌控得多。
在 ModelArts 上,也可以设置超参数和组合搜索,但搜索会影响成本,我对自己的理解是先用直觉缩小范围,再做一两次自动化搜索。
我自己的经验是:微调预训练模型时,加入学习率 Warmup,前几个 epoch 从很小的学习率慢慢升到预设值,能明显让 loss 下降更平滑。这背后是因为预训练权重已经处在一个不错的区域,突然用大学习率容易冲出去。
另外,如果你用多卡训练,还要留意一个细节,就是 BatchNorm 层的统计信息在多卡上会合并,不同卡之间数据分布差异大时,模型可能变得不稳定。很多新同学在 ModelArts 上把分布式的开关打开,结果训练变慢了反而踩了性能坑,我的建议是小规模实验先别开启分布式,等数据量和模型体积都上来了再考虑多卡。
5. 模型部署:把训练产物变成在线接口
5.1 部署方式选型
模型训练得再好,最终总是要给别人调用的。ModelArts 提供了几种部署方式,我根据项目需求选了在线推理服务,原因很简单:我要让下游系统实时调用模型接口,延迟必须低、并发要能扛。
在线推理服务:模型部署成一个 Web API,提供一个 URL,外部系统通过 HTTP 调用。适合业务系统实时请求,比如图片分类、OCR、内容审核等。
批量推理:一次性处理大批数据,不要求实时返回。这个适合离线任务,比如把所有历史报表扫描一遍,输出结果到 OBS。成本上通常会比在线服务便宜,但延迟高,不适合交互场景。
边缘部署:把模型部署到边缘设备,比如常见的 rk3588 这类开发板,部署例如 yolov8 这种目标检测模型,不依赖云上算力。如果你将来要走边云协同,ModelArts 的模型也可以导出到边缘节点,但整个链路会更复杂一些。
我这次选择在线服务,主要是因为它和业务的对接最简单。
5.2 模型转换与推理配置文件
部署模型的格式很有讲究。我训练时用的 PyTorch,导出的权重是.pth,但 ModelArts 在线推理服务默认对不同的模型框架有不同的配置要求,因此建议把它转成更通用的格式。
ModelArts 提供模型转换功能,可以把 TensorFlow、PyTorch 模型转换输出为 MindIR、ONNX、TensorRT 等格式。我选择把 PyTorch 模型导出为 ONNX,这样后面如果在本地或者边缘端做推理,也能复用同一份模型文件。
具体操作上,我在训练结束前加了一段:
dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["output"], opset_version=11 )导出成功后,再把.onnx文件和相关推理代码放到 OBS 的同一个模型目录下。接着要准备一个config.json,ModelArts 会用它来加载模型、执行推理。这是一个常见的配置文件:
{ "model_algorithm": "image_classification", "model_type": "ONNX", "runtime": "python3.6", "infer_framework": "ONNXRuntime", "dependencies": [ "numpy==1.19.5", "Pillow==8.2.0" ] }其中model_algorithm是你给模型定义的标识,dependencies是推理时需要用到的 Python 包及版本。配置文件如果写错,部署出来的服务大概率会一直报“模型加载失败”。我可以坦白地说,这个文件是我部署阶段卡最久的地方,因为官方文档的默认模板并不一定适配你自己的依赖。
5.3 调用在线服务与常见坑
部署成功以后,在线服务会分配一个调用地址和一份鉴权信息。我用 Python 脚本走了一遍调用流程,核心代码非常简单:
import requests import base64 # 读取测试图片并转成 base64 with open("test.jpg", "rb") as f: img_base64 = base64.b64encode(f.read()).decode("utf-8") response = requests.post( "https://your-service-url/v1/infer", headers={"Authorization": "Bearer your_token"}, json={"data": {"image": img_base64}} ) print(response.json())这里有几个容易踩的坑,我用“血泪经验”来形容都不为过:
第一就是请求超时。我第一次部署时,模型推理一次要 3 秒,而服务端默认的超时时间很短,直接导致调用方反复收到超时错误。解决办法是让请求带上X-TimeLimit之类的参数,或者直接调整服务的推理超时配置。
第二是图片格式问题。我把整张图片直接塞给模型,结果模型里要求的是经过预处理的张量,于是推理结果变得不可用。正确的做法通常是把图片解码、缩放、归一化后再送入模型,或者让推理代码内部处理原始输入。
第三是网络策略。新创建的在线服务如果绑定了安全组,初始状态可能禁止外部访问。用本地电脑直接调不通的时候,先检查是不是防火墙和安全组在作怪,而不是怀疑代码写错了。
第四,也是容易忽略的一点:如果你更新了模型版本,旧服务不会自动切换到新版本。我有一阵子改了模型,重新部署后接口返回的还是旧结果,原因就是新版本没有完成告警替换。务必在模型版本管理页面把新版本设为默认,或者重新部署一个服务。
6. 成本控制与后续扩展:让模型持续创造价值
6.1 如何省钱
云上训练虽然方便,但如果不加控制,账单可能会让你肉疼。我列一下自己总结的控费经验:
第一是“先小后大”。先用小规格实例把代码跑通,确认流程无误,再换大规格做完整训练。这一步可以节省许多浪费在调试上的大额算力费用。
第二是及时释放资源。在线服务如果一直开着,即使没有请求也会按固定周期计费。我习惯在每天结束后查看一下有没有闲置的实例,该停就停。
第三是选择合适的计费方式。ModelArts 通常有按需计费、包周期、以及资源套餐包,如果你只是偶尔用,按需即可;如果长期跑训练任务,包周期或套餐包更划算。
我大概比过一笔账:按需使用一个中等 GPU 实例跑完整训练,一次费用可能相当于包月费用的十几分之一。所以长期反复训练的项目,包周期绝对是更好的选择。
6.2 把模型带离云端:本地推理与其他扩展
训练部署都在云端搞定之后,最近我又遇到一个需求:想在没有公网的情况下,在本地跑轻量级推理。这时云端训练的好处就体现出来了——模型文件导出下来,用 ONNX Runtime 或 TensorRT 就能直接接到本地的业务里。
如果你想在本地部署自己的服务,思路和 ModelArts 在线服务类似,用 Flask 写一个接口,封装推理逻辑,暴露 HTTP 端口。唯一需要额外处理的是显存占用和并发策略,这也是很多本地部署场景的痛点。
我最近也在尝试做更大规模的模型,比如大语言模型的私有化部署,除了用主流的 ollama 这类工具外,训练阶段我还是更倾向用 ModelArts 这类托管平台。因为大模型微调对显存和环境的要求更苛刻,本地一张显卡很难跑起来。训练完模型,得到权重,再拿回本地或私有环境做推理部署,是一种相当务实的路径:云上做重活,本地做轻活。
这种模式的本质是:把需要资源密集的环境依赖交给平台去解决,让算法工程师把精力集中在数据、模型结构和业务效果上。很多人一上来就排斥云平台,觉得不如本地“可控”,但等你真正跑通一次全流程,就会理解托管平台在时间与成本上的优势有多明显。
最后再分享一点我个人的体会,写这篇学习笔记的过程,其实是把整个流程重新梳理了一遍,很多当时迷迷糊糊的细节,比如路径配置、推理超时、模型格式转换,在写下来之后反而变得异常清晰。我强烈建议你也给自己的每次训练做一份记录,不用长,但要把关键参数、出错信息、解决方式记下来。下次遇到相似问题,翻笔记比翻聊天记录和收藏夹靠谱得多。