ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从本地到华为云ModelArts:模型训练与在线部署实战记录

从本地到华为云ModelArts:模型训练与在线部署实战记录

把模型从本地搬到华为云ModelArts上,最直观的感受就是:再也不用为了装驱动、配CUDA、找GPU这些事折腾到半夜了。这篇文章是我最近一段时间的完整实操记录,从数据集上传、Notebook调试、训练作业提交,再到模型部署成在线服务,整个过程走了一遍,把值得记下来的步骤、参数和踩过的坑都整理了出来。如果你正准备在云平台上跑通自己的模型训练和部署,或者已经在用ModelArts但总觉得流程有点绕,这篇笔记应该能帮你省下不少时间。

1. 整体思路:为什么选ModelArts而不是自建GPU环境

1.1 从“搭环境”到“开箱即用”的转变

说实话,最开始我没打算用云平台。实验室里有一台8卡GPU服务器,听起来很猛,实际管理起来全是泪:显卡驱动不能乱动,CUDA版本和PyTorch版本必须精确配对,一旦有人为了自己项目升级了某个库,整个环境就崩给你看。更别提训练的时候资源要跟同事排队,明明在跑验证实验,还得小心翼翼地避开别人的大任务。

后来我把一个二分类图像识别Demo放到ModelArts上跑,反倒觉得顺畅很多。ModelArts是华为云的一站式AI开发平台,它并没有把“AI”包装得多玄,而是把“写代码、跑训练、上线推理”这条链路整合在同一个控制台里。创建Notebook时直接选一个预置镜像,PyTorch也好、MindSpore也好,框架版本、CUDA环境全都给你配好了。不是说自己搭环境完全不行,而是当你只是想验证模型效果、快速迭代的时候,花几个小时装环境真的太亏了。

从成本角度看,自建服务器还有另一个问题:闲置资源浪费。你买了一台8卡机器,可能每周只有几个小时的满负荷使用,其他时间都在跑空转。ModelArts这边是典型的按需计费,开发环境用CPU实例,训练作业用GPU实例,跑完释放掉,不为闲置时间买单。这个“即开即用、用完即走”的模式,对个人学习和小团队项目特别友好。

1.2 训练部署全流程的关键决策点

用ModelArts之前,我先在纸上把流程理了一遍,确定整个链路的几个节点:数据放哪里、代码在哪里调试、正式训练怎么跑、模型怎么部署。下面是我当时的决策表:

决策点自建/传统方式ModelArts方式
数据存储本地磁盘/自建NAS对象存储服务OBS
代码调试本地IDE+远程GPUNotebook在线调试
正式训练手动nohup + 自己盯日志训练作业,平台统一管理
模型上线自己写服务接口导入模型,一键部署在线服务
监控运维自己写脚本看GPU/日志平台自带日志和告警

最终我的实际路径确定为:OBS作为数据和模型的统一存储 -> 创建Notebook做代码调试 -> 用训练作业做正式训练 -> 在模型管理里创建模型版本 -> 部署成在线服务并调用验证。这条路径也是ModelArts上最常见的标准用法。

为什么不用自动学习?因为自动学习虽然不需要写代码,但可定制性差,我的实验需要自定义网络结构和损失函数,所以最终选择训练作业。如果只是快速验证数据能不能训、效果大致如何,自动学习确实值得一试。更详细的对比我放到后面讲。

2. 环境准备与数据接入

2.1 创建Notebook实例时容易被忽略的参数

创建Notebook的过程本身不难:登录华为云ModelArts控制台,找到“开发环境 > Notebook”,点“创建”就行。但有几个参数很容易被忽略,后面会直接影响体验。

第一个是镜像选择。不要随手选一个最新版本就完事,先想清楚自己要用什么框架。我用的是PyTorch,我就选了带PyTorch 1.8的预置镜像,顺便还把常用的torchvision也一并带了。选好了镜像,等于省掉大半环境配置的工夫。之后如果要装其他第三方库,也尽量基于这个镜像来做,不要再用conda建一个新环境,ModelArts镜像里的默认环境就是给Notebook用的,再自己折腾一套环境反而容易出问题。

第二个是资源规格。调试阶段建议先选CPU或较小的GPU实例,没必要一上来就直接选8卡V100。我的习惯是:先在CPU实例上保证代码逻辑能跑通,再切到GPU实例做实际训练。因为大多数报错都是数据路径、维度这类问题,用CPU调试更快,成本也更低。

第三个是存储空间。默认存储往往偏小,我一开始没注意,结果DataSet往里一放就爆了。后面学乖了,创建实例时把存储调到至少50GB,给数据集和临时文件留足空间。注意,这里说的存储是Notebook实例的临时存储,不是最终的数据存储,数据大概率还是要放OBS的。

第四个是“自动停止时间”。这个非常关键,最好设置成30分钟或1小时。我第一次用的时候忘了设,Notebook挂了一晚上没有关,第二天看到账单才心疼。自动停止是为了防止你人走了但实例还在计费,调试中途临时离开,平台会自动帮你关停,相当于是个保险。

创建时还需要一个密钥对,选“创建新密钥对”就行,方便后面通过Terminal登录实例。如果没有密钥,后期想在Notebook里用SSH操作会麻烦不少。

2.2 把数据集放进OBS的三种方式与权限问题

OBS是华为云的对象存储服务,ModelArts的数据集、模型文件、训练输出都会放到这里。数据上传方式无非三种,按场景选就行。

第一种是控制台上传,适合小文件。在OBS管理界面直接点击“上传”,选几个小文件没问题,但几百个图片文件这样点会把人点疯。

第二种是用obsutil命令行工具上传,适合大文件和目录。先把obsutil下载到本地,配置好AK/SK和endpoint,然后一条命令就能同步整个目录:

obsutil config -i=AK -k=SK -e=endpoint obsutil cp /本地目录/图像集 obs://你的桶名/dataset -r -f

这里-r表示递归拷贝整个目录,-f表示强制覆盖。obsutil支持断点续传,网络抖一抖问题不大。

第三种是在Notebook里用moxing直接把OBS数据拉到实例本地,适合数据处理过程中需要来回搬运的情况。代码非常简单:

import moxing as mox mox.file.copy_parallel('obs://你的桶名/dataset', '/home/ma-user/work/dataset')

这个moxing是ModelArts自带的文件传输工具,封装了OBS的读写逻辑。我第一次用的时候直接报了403 AccessDenied,排查了半天才知道是Notebook实例没有授权访问OBS。解决办法是在ModelArts的“全局配置”里,给当前账号添加OBS的委托权限,比如OBS OperateAccess或直接OBS FullAccess。不做这一步,不管是moxing还是训练作业读OBS都会失败。

经验:大量小文件最好先打成tar包再传OBS,训练时再解压。OBS对“大量小文件”的读写性能并不友好,文件数量一多,拷贝速度和后续读取速度都会明显下降。我试过直接把几千张图片传到OBS,moxing拷贝时花了不少时间,后来改成先压缩再传输,整个流程快了不止一倍。

2.3 依赖安装与镜像选择的经验

预置镜像里虽然自带PyTorch/TensorFlow这些主流框架,但绝对不可能包含你所有的依赖。我的代码里用到了albumentations做数据增强,镜像里没有,就得自己装。

在Notebook里安装依赖很简单,直接开一个Terminal,然后在当前环境里执行pip install。但我建议把依赖统一写到一个requirements.txt里,以后每次重建实例都可以一条命令装完:

pip install -r requirements.txt

装完后有两点要注意。第一是重启内核。如果是在Notebook里用!pip install安装,安装完最好重启kernel再跑代码,否则可能因为Python进程没有读取到新安装的包而报ImportError。第二是“装到哪个环境里”。ModelArts有些镜像默认有多个Python环境,不要在系统Python里乱装,最好先用which python确认一下当前环境,再安装对应的依赖。

如果真的遇到某些包卡版本,也可以在创建训练作业时通过“运行环境依赖”指定安装,但说实话,能在镜像里解决的事就不要等到训练作业里再解决。训练作业阶段联网安装依赖并不总是可靠,提前在Notebook里把环境验证清楚会更稳。

3. 模型训练:从脚本调试到分布式训练

3.1 单卡调试:先让代码跑通再说

我见过不少同学一上来就直接创建训练作业,结果训练任务秒挂,然后对着日志页发呆。与其这样,不如先在Notebook里把代码完整调试一遍。

我的做法是:先在Notebook里用CPU或一张小GPU,把batch_size调小,epoch设为1,跑一个极小的训练循环,确认数据加载、模型前向、损失计算、反向传播、精度评估这几步都没问题,再考虑去做完整训练。

调试时可以把超参数都集中到一个参数解析器里,方便后面提交训练作业时覆盖。典型代码如下:

import argparse parser = argparse.ArgumentParser() parser.add_argument('--epochs', type=int, default=10) parser.add_argument('--batch_size', type=int, default=16) parser.add_argument('--learning_rate', type=float, default=1e-3) parser.add_argument('--data_url', type=str, required=True) parser.add_argument('--train_url', type=str, required=True) args = parser.parse_args() print(f'epochs: {args.epochs}, batch_size: {args.batch_size}')

这一步的价值在于,把“代码逻辑错误”和“平台配置问题”分开。代码逻辑错误在Notebook里就能暴露,而训练作业里真正要排查的,往往只剩下路径、权限、资源这些问题。

3.2 训练脚本中的关键配置

当我把Notebook里的代码整理成标准训练脚本后,下一步就是创建训练作业。训练作业和Notebook调试最大的不同是:训练作业的运行环境是一台临时分配的机器,训练结束之后,机器上的一切都会消失。所以脚本里有两件事必须处理干净:数据和模型的“落地路径”。

训练作业通过超参传入data_url和train_url,分别对应OBS上的输入数据路径和输出模型路径。在脚本开头,把OBS数据拉到本地临时目录,训练结束后再把模型文件拷回OBS:

import moxing as mox import os local_data_path = '/home/ma-user/work/data' local_model_path = '/home/ma-user/work/model' os.makedirs(local_model_path, exist_ok=True) mox.file.copy_parallel(args.data_url, local_data_path) # 这里开始正常训练代码 # ... 训练循环 ... mox.file.copy_parallel(local_model_path, args.train_url)

创建训练作业的时候,“训练输入”要指定为OBS的数据路径,“训练输出”要指定为一个OBS目录,这样平台才会把这两个地址作为超参传入脚本。不需要在页面里写死训练参数,直接在参数列表里填--epochs=50、--batch_size=32就行。

这里有一个很容易踩的坑:训练作业的运行环境不是固定的,如果你在脚本里把数据写到/home/ma-user/work/data这种本地路径,那没问题,但千万别把数据写到代码目录里,也不要试图在整个训练周期里一直保留本地临时文件。所有需要持久化的东西,一定要在训练结束前上传到OBS。

3.3 使用训练作业与自动学习的区别

ModelArts还提供了一个“自动学习”功能,不需要写代码,只要准备数据集、选择任务类型,平台就会自动完成模型训练和部署。它确实省心,但不一定适合所有人。

对比项自动学习训练作业
代码要求无需写代码需要准备好训练脚本
模型结构平台预置,不可定制完全自定义
数据处理简单上传自己控制增强、划分、清洗流程
适用场景快速验证、常规分类/检测论文复现、特殊任务、算法调优

我选择训练作业,是因为我需要调参,也需要记录每一次实验的超参组合。自动学习的黑盒特性虽然方便,但很难知道中间发生了什么,出了问题也不太好排查。

3.4 训练日志与监控怎么看

训练作业启动后,回到“训练作业”列表页面,点击详情就能看到实时日志。平台会把脚本里的stdout和stderr都打在日志页里,最简单的监控方式就是在训练循环里用print打印每个epoch的loss和accuracy。

for epoch in range(epochs): train_loss = 0.0 for batch in train_loader: # ... train_loss += loss.item() print(f'epoch: {epoch}, loss: {train_loss:.4f}')

有了这个输出,我远程盯训练进度就方便多了。如果开了Notebook同时挂着训练作业,也可以把日志下载下来,用本地工具做分析。日志数据量大的时候,建议自己在代码里加一个只输出关键信息的开关,别把调试用的海量调试信息全部print出来。

遇到OOM(CUDA out of memory)时,日志里一般会直接出现RuntimeError: CUDA out of memory。我的第一反应不是换更大的GPU,而是把batch_size降下来。很多模型根本不需要那么大的batch,梯度累积等技巧也能弥补batch_size减小带来的不稳定。还有一次日志里报No space left on device,那是因为OBS的数据量太大,本地临时磁盘装不下了。解决办法是减少同时拷贝的数据量,或者把数据预处理后筛选一部分再训练。

4. 模型部署与在线推理

4.1 创建模型版本时的输入输出约定

训练完成后,模型文件会自动上传到OBS的train_url路径下。接下来要做的是在ModelArts里“导入模型”,生成一个可部署的模型版本。

导入模型时,需要指定OBS上的模型文件路径,以及模型使用的AI引擎。但仅仅上传一个权重文件还不够,ModelArts部署在线服务时,需要一个推理脚本负责把HTTP请求中的原始数据转成模型输入,再把模型输出转成HTTP响应。常见目录结构是这样的:

model_dir/ ├── customize_service.py └── model.pth

customize_service.py里一般会实现预处理和后处理。比如图像分类任务,客户端传的还是base64编码的图片,那么预处理里就要解码、缩放、归一化,得到模型需要的Tensor;后处理里再把模型输出的概率向量转成可读的标签和置信度。

import base64 import io import torch from torchvision import transforms from PIL import Image class MyService(BaseService): def _preprocess(self, data): image_base64 = data['image_base64'] image_bytes = base64.b64decode(image_base64) image = Image.open(io.BytesIO(image_bytes)).convert('RGB') transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) return transform(image).unsqueeze(0) def _postprocess(self, data): prob = torch.softmax(data, dim=1)[0] category_id = int(torch.argmax(prob)) score = float(torch.max(prob)) return {'category_id': category_id, 'score': score}

这个类名和方法名在具体版本里可能略有差异,但整体思路是一致的。最怕的是推理代码里的预处理和训练代码不一致。比如训练时用Resize(224, 224),推理时用了Resize(256, 256),效果会明显下降。所以我的习惯是,把预处理代码单独抽成一个函数,训练脚本和推理脚本都引用同一个逻辑,保证两边完全一致。

4.2 部署为在线服务的资源配置选择

在“部署上线 > 在线服务”页面,选择刚才的模型版本,点击部署。这里有个关键的资源配置选择。

如果是实时推理,我建议优先选GPU实例,比如单张T4或相近规格,实例数设为1。很多CV模型用CPU推理一张图要几百毫秒甚至几秒,用GPU的话延迟能降到几十毫秒。刚开始实验阶段,实例数不要贪多,1个就够了,并发压力大的时候再通过弹性伸缩加实例。

在线服务跟Notebook不太一样,它一旦运行就会持续计费,不管你有没有调用它。这一点真的需要刻在心里。我的测试流程都是白天部署、测试完当天删掉,绝不过夜。千万不要觉得“服务放着又不占我本地资源”,云上闲置实例会持续产生费用。

部署之后,ModelArts会为这个服务生成一个API接口。在“调用指南”里能看到完整的地址和鉴权方式。这一步如果提示模型不可用,多半是模型文件和推理代码不匹配,或者模型路径不对,需要回到模型管理页面去检查。

4.3 调用API与验证结果

服务部署成功后,可以用简单的HTTP请求来验证。我用的鉴权方式是AppCode,在服务详情页可以找到。把图片转成base64之后,构造一个POST请求就行。

curl -X POST https://模型服务地址/v1/infers \ -H "Content-Type: application/json" \ -H "X-Api-Appcode: 你的AppCode" \ -d '{"image_base64": "图片base64字符串"}'

如果要用Python,可以直接写脚本:

import requests import base64 url = 'https://模型服务地址/v1/infers' headers = { 'Content-Type': 'application/json', 'X-Api-Appcode': '你的AppCode' } with open('test.jpg', 'rb') as f: image_base64 = base64.b64encode(f.read()).decode() resp = requests.post(url, json={'image_base64': image_base64}, headers=headers) print(resp.json())

返回结果里会出现我在_postprocess里写好的字段,比如category_id和score。我习惯在做完整流程调试时,先用一张训练集中的图片测试,确认推理结果和预期标签一致;再换一张完全没有见过的图片,看看模型的泛化表现。如果第一张都预测错,那大概率不是模型的问题,而是预处理/后处理写错了。

5. 常见问题与排查技巧实录

5.1 OBS数据读取慢/失败

这个问题在训练和部署阶段都容易出现。最常见的报错是403或找不到文件。403基本都是委托权限问题,回到ModelArts全局配置里给账号加OBS权限即可。找不到文件则往往是路径写错了,OBS路径的格式是obs://桶名/目录/文件名,不是/桶名/...。调试的时候可以先在Notebook里用moxing试着读一下这个路径,能读通再提交训练作业。

读取慢的另一个原因是数据文件太多。几千张小图放在OBS,训练作业每次启动都要全量拉下来,拉取时间可能比训练时间还长。我的解决办法是把所有小文件打包成tar后再上传,训练脚本里先解压再读取。如果数据集太大,也可以考虑不全部下载到本地,而是边训练边从OBS流式读取,但这需要程序本身有良好的数据队列机制,复杂度会高一些。

5.2 训练作业启动失败

训练作业启动失败最容易看到的是日志开头有一行ModuleNotFoundError。这表示运行环境里缺少依赖包。处理方法有两个:一个是把依赖提前写进镜像,让每个训练任务都从一个干净的、能跑通的环境中启动;另一个是在训练脚本开头加上pip install -r requirements.txt。但后者要求训练环境能访问外网,在受限环境里不一定可靠,所以最稳的还是前者。

还有一种常见失败是“启动文件路径错误”。创建训练作业时要分别指定“代码目录”和“启动文件”。这里要注意,启动文件是相对于代码目录的路径,不要把整个绝对路径贴进去。比如代码目录是obs://桶名/code/,启动文件应该填train.py,而不是obs://桶名/code/train.py。我因为这个细节白白排队等了好几分钟。

5.3 模型部署后预测结果异常

部署服务不报错,但返回的结果完全不对,这种问题比“服务创建失败”更让人头疼。我遇到过两种情况。

第一种是预处理不一致。训练时图片做了归一化,部署时忘了做,或者颜色通道顺序反了,RGB变成了BGR,模型输出自然一塌糊涂。第二种是模型加载逻辑有问题。有些权重文件包含了完整的模型结构,有些只是state_dict,加载方式完全不同。如果部署时直接用了随机初始化的权重去推理,输出结果就是随机的。排查方法是先把模型下载到本地,用同样的推理脚本跑一张图,看输出是不是也异常。

5.4 费用控制建议

ModelArts本身不便宜,如果不控制,账单能让人心凉。我总结了几条比较实用的省钱经验:

开销来源省钱做法
Notebook实例启用自动停止,调试完手动删除
训练作业先用CPU/小GPU调试,确认没问题再上大卡
在线服务测试完立即停止/删除,不过夜
OBS存储低频访问存储,及时清理中间产物
模型版本只保留有意义的版本,废弃版本删掉

我记得有一次忘了关在线服务,放了整整一个周末,回来一看费用比训练一周还贵。从那以后我给自己定了规矩:凡是部署完,验证完,当天必须把服务停掉。哪怕第二天还要继续调试,也重新部署一次,反正最多几分钟的事,比白白烧钱强多了。

最后再分享一个我自己觉得最值的小技巧:提交正式训练作业之前,一定在Notebook里用超小batch把全流程跑一遍。这不是浪费时间,而是在给后面的自己省时间。把代码逻辑和平台配置这两个层面的事分开处理,遇到问题的时候脑子会清醒很多。ModelArts这套流程一旦跑顺,之后再做新的模型实验,基本就是套模板的活了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进