从去年开始系统性接触华为云 ModelArts,前前后后跑通了从数据准备、模型训练到在线部署的全流程。这篇笔记不是官方文档的复述,而是我实际操作中留下的记录,包括那些文档里不会写的坑和抉择过程。如果你正打算用 ModelArts 跑自己的模型,或者已经在路上但被各种概念绕得晕头转向,这篇内容应该能帮你把整条链路捋顺。
先把结论放在前面:ModelArts 本质是一个把“训练”和“部署”两件事打包好的云平台。训练部分提供 GPU 资源、预置框架和开发环境,部署部分帮你把模型包装成 API 服务。你要做的核心工作集中在三件事——把数据弄上去、把训练脚本写好、把模型配置好,剩下很多运维琐事平台接走了。但正因为平台替你做了很多事,你更要搞清楚每一层背后发生了什么,否则出了问题排查会很痛苦。
1. 动手之前:先把 ModelArts 这套体系想明白
很多人第一次打开 ModelArts 控制台就懵了,满屏的“训练作业”“开发环境”“模型管理”“在线服务”,感觉像进了个工具超市。其实用一个生活类比就清楚了:ModelArts 相当于一个带 GPU 的云端工作室,你可以在这个工作室里写代码、跑训练,然后把你做好的“产品”挂到柜台上对外提供调用。
这个平台上最核心的几块是:数据管理帮你存数据和做标注;开发环境(Notebook)是写代码和调参的地方;训练作业是真正甩开膀子跑大规模训练的入口;模型管理把你训练出来的模型归档登记;在线服务则把模型变成一个可以实时调用的 API。这个链路是完整闭环的,每一步的产出都会流到下一步。
账号准备上,我建议先把你手头的账号确认好:实名认证要做,这个不用多说。更关键的是资源配额,ModelArts 很多区域默认的 GPU 配额并不高,如果你要用 V100 甚至 A800 级别的卡,大概率要先提工单申请配额提升。我第一次跑训练时就被“配额不足”卡了一次,后来学乖了:先把配额申请下来,再动手写代码。这一步没什么技术含量,但没做的话会在最关键的时候卡住你。
费用方面,ModelArts 的计费模式主要有按需和套餐包两种。按需适合偶尔跑跑实验,套餐包适合有计划地长期训练。我个人的习惯是:调试阶段用低规格 CPU 实例跑通代码逻辑,确认没有低级错误后再切 GPU 按需跑正式训练,这样能省下不少冤枉钱。另外,Notebook 实例只要不关闭,就会持续计费,哪怕你人已经睡觉去了。这个细节特别容易忽略,我身边至少两个同事因此多交了不少费用。
还有一个容易被忽视的点:ModelArts 不同区域的能力不一样,有些新功能只在部分节点上线。选区域时不要贪图距离近,先看一眼你要用的功能在不在那个区域。我一般会去 ModelArts 的“全局配置”里把授权先做了——如果不做授权,后面连 OBS 都要手动去捣鼓密钥,虽说也能跑通,但体验会差很多。
2. 数据准备与 OBS 管理:训练的命根子在这里
模型训练再怎么花哨,数据进不去全是白搭。ModelArts 训练任务运行时,容器是临时拉起来的,里面不可能长期存你的数据,所以数据必须统一放到对象存储服务(OBS)里,训练作业再通过路径去读取。这个设计初看有点绕,用熟悉的话说就是:你的数据住在“仓库”里,每次训练就是派一个“临时工”去仓库取货,取完货干完活就走人。这个临时工是不会把货存在自己家里的。
具体操作先走一遍控制台创建桶的流程。在 OBS 控制台创建桶时要选对区域——一定选和 ModelArts 同一个区域,否则后面访问会跨区域产生额外流量费用,甚至直接读不到。存储类别我一般选“标准存储”,如果你的数据本身长期不变,可以选“低频访问存储”省钱,但训练频繁读取时低频访问的读取费用和延迟并不划算,所以多数场景标准存储是稳妥选择。
桶建好之后,不要一上来就用网页控制台传大文件。网页直传小文件(几 MB 那种)还能忍受,但动辄几 GB 的数据集用网页传会等到怀疑人生。我建议用 obsutil 这个命令行工具,或者 OBS Browser+ 这个桌面客户端,两者都能断点续传、并行上传。传大量小文件的时候,尽量先打成 zip 或 tar 包再传,到训练脚本里再解压,因为 OBS 对“文件数量”很敏感——一万个 1KB 的小文件和 一个 10MB 的大文件,读写效率完全是两个量级。
如果你是团队协作,OBS 的权限管理也要稍微设计一下。ModelArts 的“委托”机制可以把 OBS 的访问权限自动授予训练作业和 Notbook,省去在代码里硬编码 AK/SK 的麻烦。硬编码密钥的问题是泄露风险高,而且密钥轮换时所有引用处都要跟着改。用委托之后,平台会通过临时凭证去访问 OBS,安全性和便利性都好很多。具体做法是在 ModelArts 的“全局配置”里添加“委托”,然后把桶的权限授权给这个委托。
数据处理这一步,ModelArts 自带的数据管理可以帮你做标注。图像分类、物体检测、文本分类这些常见场景,平台的标注工具基本够用。不过我做过的项目里,标注主力还是小组自己的工具,用 ModelArts 的标注主要是为了后续版本管理方便——它支持给数据集打版本号,每次训练前锁定一个版本,方便回溯到底是哪份数据训出的模型。这个版本管理的习惯值得刻意养成,因为没人希望实验出了问题后,发现连复现都复现不了。
还有一点,如果你数据本身就在 ModelArts 的“数据集”对象里,训练时可以直接引用数据集版本,它会自动处理好数据路径。但如果你的数据来自外部导入或者需要复杂清洗,我更建议自己在脚本里写 DataLoader,从 OBS 读取原始文件,灵活度大很多。ModelArts 里也支持从 OBS 读取数据,这里贴一个常见的 PyTorch 数据加载配置:
import torch from torch.utils.data import Dataset, DataLoader import boto3 from io import BytesIO # 用 boto3 接口从 OBS 拉取数据 session = boto3.session.Session() obs_client = session.client( 'obs', endpoint_url='https://obs.cn-north-4.myhuaweicloud.com', # 替换成你所在的 region aws_access_key_id='你的AK', # 正式环境请用委托/临时密钥 aws_secret_access_key='你的SK' ) class OBSImageDataset(Dataset): def __init__(self, bucket, prefix, transform=None): self.bucket = bucket self.prefix = prefix self.transform = transform self.keys = self._list_keys() def _list_keys(self): # 实际代码需要处理分页和数据清洗,这里省略具体实现 resp = obs_client.list_objects_v2(Bucket=self.bucket, Prefix=self.prefix) return [obj['Key'] for obj in resp.get('Contents', [])] def __getitem__(self, idx): obj = obs_client.get_object(Bucket=self.bucket, Key=self.keys[idx]) data = obj['Body'].read() img = Image.open(BytesIO(data)).convert('RGB') if self.transform: img = self.transform(img) return img, torch.tensor(0) # 标签按实际业务调整这段代码的意图不是让你直接抄,而是展示一个思路:训练脚本通过 SDK 与 OBS 交互,按需读取数据。但它有个硬伤——每次 get_object 都是网络 IO,训练数据读取会变成瓶颈。更现实的方案是先把数据集整体同步到本地磁盘再训练,或者在脚本启动后先并发下载数据、解压到本地工作目录,Epoch 内的读取就走本地文件系统。我后面写训练作业时就是这么干的,不要太迷信“流式读取”,那是分布式大数据场景才值得考虑的事。
3. 训练环节实操:从 Notebook 调参到正式训练作业
ModelArts 里训练有几种打开方式:自动学习、Notebook、训练作业。自动学习是最“傻瓜”的,你只要上传标注好的数据,平台自动选模型、自动调参,适合快速验证一个想法或者非算法背景的人用。但我这篇笔记主要针对自己写代码跑训练的路子,也就是 Notebook 加训练作业的组合。
先把 Notebook 环境说透。打开 ModelArts 的“开发环境”,创建 Notebook 实例时就会碰到一个选择:镜像选什么,规格选什么。镜像一般用平台预置的 PyTorch、TensorFlow 或 MindSpore 镜像,版本号一定要对照你的代码。我踩过的坑是:代码在本地跑得好好的,到云端镜像里版本不匹配,import 直接报错。所以创建实例前先确定你的依赖清单,尽量用官方预置镜像,不要自己从头搭,省一半时间。规格这里,调试阶段 CPU 实例完全够,比如用 8 核 32GB 的 CPU 规格,跑小批量数据验证模型可以跑通就够了。确认无误后再创建 GPU 实例做真正训练。
Notebook 里做的事情就是标准的调参试错流程。我以 ResNet18 在自定义图像分类数据上训练为例,给你看一眼我常用的最小化脚本骨架:
import torch import torch.nn as nn import torch.optim as optim from torchvision import models, transforms, datasets def create_model(num_classes): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层分类头 model.fc = nn.Linear(model.fc.in_features, num_classes) return model def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss = 0.0 correct = 0 total = 0 for inputs, targets in loader: inputs, targets = inputs.to(device), targets.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step() total_loss += loss.item() * targets.size(0) _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() return total_loss / total, 100.0 * correct / total if __name__ == '__main__': device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # dataset 换成你自己的 ImageFolder 结构 dataset = datasets.ImageFolder(root='data/train', transform=transform) loader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True, num_workers=4) model = create_model(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) for epoch in range(30): loss, acc = train_one_epoch(model, loader, criterion, optimizer, device) scheduler.step() print(f'Epoch {epoch:03d} | Loss: {loss:.4f} | Acc: {acc:.2f}%') # 每个 epoch 后保存 checkpoint,后续转训练作业时同理 torch.save(model.state_dict(), f'checkpoint_epoch_{epoch:03d}.pth')这里用到了预训练模型。需要说明的是,weights=ResNet18_Weights.IMAGENET1K_V1 是从 PyTorch 官方地址下载的预训练权重。如果你在 ModelArts 上运行时网络受限,提前把权重文件下到 OBS,再用代码加载本地权重。这个坑真的会卡人——看起来是代码问题,实际是网络策略问题。文本方向的读者如果用的是 RoBERTa 或者 ERNIE 这类中文预训练模型,思路完全一样:先下载预训练权重到本地,再通过 from_pretrained 指定本地路径,不要让它现去外网下载。
围绕训练参数的选择,很多初学者喜欢直接抄默认配置,但几个参数之间是联动的,不能孤立看待。学习率要结合 batch size 来看:batch size 翻倍,学习率通常也应该适当调大,否则收敛速度变慢。但学习率也不是越大越好,太大会导致 loss 变成 NaN 或者震dan。我这里用的 0.01 配合 batch 64 只是合理起点,实际项目里建议用余弦退火或者 ReduceLROnPlateau 这类动态调整策略。
关于混合精度,如果你的 GPU 是 Volta 以上架构并且显存吃紧,强烈建议开自动混合精度(AMP)。AMP 的原理是大部分计算用 FP16 加速,少量敏感操作保留 FP32,这样既能提速又能减少显存占用。我实际体验是相同 batch size 下显存占用下降约一半,训练速度也有可观提升。但 AMP 也会带来精度溢出的新问题,后面排查章节会说。
Notebook 里调试到位后,就要把脚本搬到训练作业了。为什么要多这一步?因为训练作业才是 ModelArts 正式干重活的地方。Notebook 适合交互式调式,但长时间跑训练占用着 Notebook 实例既浪费钱,也容易断连。训练作业是提交一个任务就让平台托管执行,跑完自动释放资源,还支持故障重启、异步日志查看,管理起来更正规。你用训练作业跑长任务,一边关掉 Notebook 控制成本,一边还能随时打开日志看进度,体验会好很多。
创建训练作业时,要指定代码目录、启动文件、训练输入(也就是 OBS 上的数据路径)、训练输出(checkpoint 和模型文件存放路径),以及计算规格。训练输出的路径尤其建议写到专门的 OBS 目录,比如 oss://your-bucket/model_output/resnet18_exp001/,这样每次实验的输出都隔离好,不会互相覆盖。
训练作业的运行机制上有个关键点:平台会把你的代码目录挂载到容器里,但代码目录之外的文件不会自动带过来。所以如果你的代码里依赖了额外的配置文件或预训练权重,一定要放在代码目录里,或者从 OBS 单独下载到工作路径。我早期写训练作业时,预训练权重放在 OBS 另一个目录,脚本里直接读 OBS 路径,结果每次跑都慢吞吞的——后来改成启动脚本先把它下载到 /cache 本地,速度才算正常。
ModelArts 的日志查看也是一门学问。训练作业页面能看到日志流,但日志太多时直接在页面翻拉非常痛苦。我的做法是:代码里用 logging 模块统一输出格式,并且把关键指标打印得简洁有规律,比如每个 epoch 一行。这样在日志流里 grep 关键词就能快速定位每个阶段的状态,不会在满屏的 INFO 里迷失方向。
还有一点关于分布式训练,如果你要跑多卡或者多机训练,ModelArts 的“训练作业”里可以直接指定节点数量和规格,平台帮你拉起分布式调度。但你至少要理解基本的分布式概念,比如数据并行时每个进程负责一部分 batch,梯度同步由后端完成。第一次跑分布式时建议先用单机多卡,脚本里加一句 torch.distributed.init_process_group 配置,然后逐步上多机。多机的话,数据集要放在能被所有节点访问的地方,文件的绝对路径一致性也要注意。
4. 部署上线:把模型变成能用的服务
训练完的模型不是把 .pth 文件往 OBS 一扔就完事的,你得让它“跑起来”对外提供服务。ModelArts 里的部署链路大致是:模型管理 → AI 应用 → 在线服务。模型管理是登记原始模型文件的入口;AI 应用是给模型配上运行环境、推理代码和资源规格的“部署模板”;在线服务就是真正对外提供 API 的实例。
先讲模型管理。在 ModelArts 控制台的“模型管理”里,你可以创建“模型”,填写 OBS 路径指向你的模型文件,选择元模型来源。如果你用的是 PyTorch,模型文件建议导出为 torchscript 格式或者 ONNX 格式,因为原生 .pth 依赖训练时的代码结构,一旦代码变了加载就会出问题。TorchScript 把模型结构和权重打包进一个二进制文件,部署时平台不需要知道你训练时的代码细节,推理框架直接加载即可,这正是部署环节里“标准化”的意义。
导出 TorchScript 的做法很直接:
import torch from my_model import create_model model = create_model(num_classes=10) model.load_state_dict(torch.load('checkpoint_epoch_029.pth')) model.eval() example_input = torch.randn(1, 3, 224, 224) traced_model = torch.jit.trace(model, example_input) traced_model.save('model.pt')导出后把 model.pt 上传到 OBS,在 ModelArts 里创建模型时,推理镜像选 PyTorch 官方镜像,再填写推理代码路径。如果你是 MindSpore 训练出来的模型,平台有模型转换工具,可以把模型转成通用的 OM 格式用于昇腾芯片推理,这类场景主要面向边缘设备。对我来说,云端服务优先走 PyTorch/ONNX 路线,边缘部署才考虑模型压缩与转换的问题。
创建好模型,下一步是“部署”。在线服务的三种形态值得分清楚:
| 服务类型 | 适用场景 | 特点 |
|---|---|---|
| 在线服务 | 实时 API 调用,低延迟 | 实例常驻,支持弹性伸缩,计费高 |
| 批量服务 | 离线大批量推理 | 跑完即释放,可控成本 |
| 边缘服务 | 边缘设备离线推理 | 模型下发到设备端,不依赖云侧 |
如果你只是验证效果,批量服务是最省钱的方式。但多数产品场景需要实时反馈,就得用在线服务。创建在线服务时,一个容易被忽略的配置是“实例数”和“计算规格”。推理实例跟训练实例不是一回事,推理不需要特别大的显存,大部分场景一个 T4 甚至 CPU 实例都够。但模型加载要占内存,实例规格过小会出现“冷启动失败”或“OOM”错误,调大规格大多能解。
在线服务的配置里还要填“健康检查”路径。平台会用这个路径定时探测服务是否存活。你写的推理脚本如果不实现对应的健康检查接口,服务会被判定不健康,反复重启。很多人在这一步卡很久,其实关键就是要让服务启动后能响应一个 HTTP 请求。推理代码我一般用 Flask 包一层,类似这样:
from flask import Flask, request, jsonify import torch import torchvision.transforms as transforms from PIL import Image import io, numpy as np app = Flask(__name__) model = None # ModelArts 在线服务要求实现 / 路径用于健康检查,/predictions 用于推理 @app.route('/', methods=['GET']) def health_check(): return jsonify({'status': 'ok'}) @app.route('/predictions', methods=['POST']) def predict(): file = request.files.get('file') if file is None: return jsonify({'error': 'no file'}), 400 image = Image.open(io.BytesIO(file.read())).convert('RGB') tensor = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) pred = outputs.argmax(dim=1).item() return jsonify({'prediction': pred}) if __name__ == '__main__': model = torch.jit.load('model.pt', map_location='cpu') model.eval() device = torch.device('cpu') app.run(host='0.0.0.0', port=8080)部署完成后,页面会给你一个 API 地址和鉴权信息。在线服务默认有 token 鉴权,调用时要把 token 放在请求头里。为了保证服务可用,你还应该设置好“服务流量限制”和告警规则,防止突发流量把实例打挂。ModelArts 的监控面板能看到调用量、时延、错误率,这些指标上线第一天就要盯起来,别等用户投诉才后悔没提前看。
调用方式很简单,用 requests 就够了:
import requests url = 'https://your-endpoint.cn-north-4.modelarts.aihub.cloud/' token = '你的认证token' with open('test.jpg', 'rb') as f: r = requests.post(url + 'predictions', files={'file': f}, headers={'X-Auth-Token': token}) print(r.status_code, r.text)在线服务部署后还有一个常被忽略的计费问题:实例只要处于“运行中”状态就在计费,哪怕一个请求都没有。所以不用的服务要及时下线。我见过有人忘了关在线服务,一个月下来账单吓死人。如果你对延迟敏感度要求不是极高,可以尝试“无服务器模式”或者自动缩容配置,模型空闲时自动释放实例,下次调用时冷启动。当然冷启动会有几秒延迟,不适合强实时场景。
5. 踩坑记录与排查技巧:这几类问题我基本都遇到过
真正让 ModelArts 体验下降的,不是大功能用不明白,而是细节问题一个接一个。我把这段时间踩过的高频问题整理成一份排查清单,每个问题都说清楚现象、原因和解决思路。
先说最让人崩溃的“训练 Loss 变成 NaN”。这个问题的原因有好几类,而且往往是多种因素叠加的结果。最经典的原因之一是学习率过大,梯度更新幅度超过参数能承受的范围,数值就崩了。解决思路是把学习率调低一到两个数量级,比如从 0.01 降到 0.001 再试。第二个常见原因是数据里有缺失值或异常值,尤其你用自己采集的数据时,NaN 常驻在数据里,一进入 loss 计算就传染到全部参数。我处理这类问题时先在训练脚本里加一个数据检查,打印出数据的分布,看看有没有 NaN 或无穷大。第三类原因是 AMP 混合精度下的 FP16 溢出:FP16 能表示的最小正数有限,梯度值太小直接变成 0,太大又会溢出变 inf,最终 loss 变成 NaN。解决办法是在 AMP 配置里为 loss 缩放设置较大的初始 scale,或者关闭 AMP 跑一次对照实验,看问题是否消失。
OBS 数据读取慢的问题也极常见。训练时 GPU 空转等数据,每一步都要等 IO,整体训练效率惨不忍睹。出现这个现象的根本原因是训练脚本从 OBS 逐个文件读取,文件越多,网络往返越多,吞吐越差。正解是启动脚本先把 OBS 上的数据集打包下载到本地磁盘,再在 DataLoader 里指向本地路径。如果数据集实在太大,就分片下载,配合 num_workers 增加并行读取。ModelArts 容器里 /cache 路径就是一个临时大盘,用完会自动清理,正适合放这些一次性数据。
第三个高频场景是训练作业“无故中断”。遇到这种问题,先别急着改代码,第一件事是去训练日志里找结束前的最后一批日志。拉日志时要注意,ModelArts 有个“日志保存”的开关,如果没保存,作业跑丢了日志也就丢了。我现在的习惯是每次创建训练作业前,确认日志保存路径配置好,同时在启动脚本里加一句“首次进入容器写一个 marker 文件”,这样能区分是容器还没拉起来就失败,还是运行时崩溃。
还有一类问题集中在“部署之后推理特别慢”。在线服务首次调用可能会等很久,不一定是你的模型慢,而是冷启动。冷启动时间来自镜像拉取、依赖初始化、模型加载三部分。如果你用了复杂的 Python 依赖,镜像冷启动一分钟以上都正常。解决办法是:把依赖固化进自定义镜像,不要每次现场装包;模型加载用绝对路径,不要每次初始化去 OBS 拉文件;如果慢得离谱,看模型文件是否真的在本地。这些都是优化动作,按顺序排查基本能覆盖。
关于镜像和框架版本,我再多说一嘴。ModelArts 预置镜像虽然方便,但如果你有特殊依赖,尤其是需要编译的库,最好还是自建自定义镜像。自建镜像时,基础镜像选官方 PyTorch 镜像,再 pip 安装额外依赖,推送到华为云 SWR(容器镜像服务),然后在创建“AI 应用”时指向这个镜像。版本对齐是这里面最磨人的事:CUDA 版本、PyTorch 版本、Python 版本三者不匹配,代码就可能在容器里起不来。我的建议是先在本地 Docker 里完整模拟跑一遍,确认无误后再推到云端。
最后附加一个配额相关的提示:无论训练还是部署,创建资源时遇到“资源不足”的提示,通常不是平台故障,而是你账号的配额或者区域资源池不够了。配额问题去提工单申请;区域资源池不够可以在其它区域建一个资源池再试,或者改用规格较低的实例。这两个操作虽然听着像“绕路”,但在高峰期确实管用。
最后的个人体会
把这一整套流程走完,我对 ModelArts 最大的感受是:它把 AI 工程化的“脏活累活”承接了一大半,但你不能因此放弃对底层细节的理解。训练作业为什么会失败、服务为什么冷启动慢、模型转换为什么报错——这些问题的答案最终都指向你对框架、容器、网络和存储的基本功。平台只是把你的精力从运维中解放出来,让你更聚焦算法和实验本身,但这个“被解放”的前提是你知道它在替你做什么。
如果让我给后来者一个建议,那就是养成“最小化验证”的习惯:数据只取一小批、模型只跑两三个 epoch、部署先用最低规格验证链路通不通,确认每一步都 OK 再放大规模。这种先小后大的节奏,帮我省下的时间远比想象中多。下一步我打算把训练好的模型导出成 ONNX,尝试在不同的推理引擎上跑,再把整套流程沉淀成自动化脚本,减少手工点击控制台的次数。这条路没有太多捷径,但一次跑通之后的顺畅感,是值得的。