近两个月,我把手头一个图像分类的小型网络任务,从本机GPU搬上了某云AI开发平台,完整走通了准备数据、提交训练作业、部署在线服务、通过API调用这条链路。写这份学习笔记,是因为这类平台的入门教程大多只讲到“点击创建任务”这一步,真正执行时,却会在路径、镜像、启动命令、模型落盘这些细节上反复卡壳。我自己第一版训练作业提交了三次才成功,部署模型又额外折腾了一个下午。要是你也正准备用云AI开发平台完成模型训练与部署,这篇笔记恰好能把前置知识、操作顺序和常见坑位串一遍,帮你省掉几个来回摸索的夜晚。
1. 为什么把训练和部署迁到云AI平台:我的本机困境
1.1 8GB显存让我连“稍大一点的模型”都跑不动
我手头这个图像分类任务,单卡训练时,骨干网络加上分类头,batch size 32在8GB显存的GPU上刚刚够用;一旦把输入分辨率提到512,或者换用更深的网络,立刻OOM。以前遇到这种情况,我的选择只有牺牲batch size强行训练,但收敛效果明显变差,还得反复试不同参数,时间全耗在来回调节上。
更麻烦的是机器共用问题。办公室里那台带GPU的台式机,几个同事都在用。有人在上面装了新版驱动,我这边某个依赖就起不来了。环境被改掉的情况,至少发生过三轮。每次重新装环境,花费的时间比训练本身还长。这种状态下,想验证一个想法,等待成本实在太高。
1.2 训练和部署分离,才能让模型真正被用起来
训练只是前半程。模型训练完,生成了一个权重文件,接下来怎么交给别人用?以前的做法,是把脚本和权重一起发给下游同学,对方自己配环境、自己跑推理。运气好,半天能跑通;运气不好,版本对不上,又得来回对答案。
云AI平台的思路是把训练和部署串成一条流水线:训练作业结束以后,产物落到模型仓库,再启动一个在线推理服务,下游拿到接口地址和鉴权方式就能直接调用。交付的形态从“一个压缩包”变成了“一个HTTP接口”,这对团队协作来说差别非常大。我这次迁移的核心诉求,其实就是这一条。
1.3 用租车类比来理解平台的全套工作方式
平台体系内的三个核心角色,可以各用一个日常物件来类比。
对象存储,像是一个共享网盘,训练数据和模型产物都放在这里,按桶和目录组织。训练作业,像是一个自动化的脚本执行器,你告诉它“用哪个镜像、跑哪条命令、输入从哪里读、输出写到哪里”,它开一台机器执行完就结束。部署服务,像是一个常驻的HTTP服务,你指定模型文件和推理代码,它把服务拉起来,对外提供接口。
三者之间靠“路径”和“格式”打交道。弄明白数据路径怎么传、模型文件放哪个位置、推理代码如何封装,后面所有卡壳点基本都能自行排查。
2. 开工前准备的几件事:数据、代码、运行环境
2.1 数据上传到对象存储,目录按类别组织最省事
我这次的数据集大约两万张图片,分成十几个类别。刚开始图省事,我把整个数据集打成一个大压缩包上传,训练作业启动后再解压。结果发现,解压本身要耗费额外的时间和临时空间,小文件一多,解压速度尤其慢。后来换成了“免解压目录”的方式,在对象存储里建好这样的结构:
dataset/ train/ class_01/ xxx.jpg yyy.jpg class_02/ val/ class_01/ class_02/训练时平台把整个桶的目录挂载到容器本地,PyTorch的ImageFolder可以直接读取这个路径,数据加载代码几乎不用改。如果你的数据量很大、小文件特别多,那么分片打包成类似TFRecord的格式更合理;如果单张文件几百KB,目录方式就是最简单可靠的选择。
有一点要注意:大文件上传建议用平台提供的客户端工具做断点续传,而不是用浏览器直接传。我用浏览器传一个5GB的压缩包,传到一半断掉就前功尽弃了。客户端的并发上传速度快很多,而且能校验完整性。
2.2 训练代码不要散传,整理成工程目录再上传
平台对训练作业的输入,一般是一个代码目录。把若干.py文件零散传上去,后续定位问题会非常痛苦。我建议至少按这样的结构准备:
train_project/ main.py model.py utils/ __init__.py metrics.py data/ dataset.py scripts/ train.sh requirements.txt平台会把整个工程目录放进训练容器的一个固定工作目录里,然后执行填写的启动命令。我的启动命令大致是:
bash scripts/train.sh --data ${DATA_PATH} --output ${OUTPUT_PATH}这里的DATA_PATH和OUTPUT_PATH,由平台通过环境变量注入到容器中,不同平台的具体变量名不一样。关键原则是:脚本里不要写死任何本地路径,所有路径都从命令行参数或环境变量读取,这样同一个工程才能在不同平台和不同机器间复用。
2.3 镜像版本和依赖列表,决定你能不能一次跑起来
训练作业运行在预置镜像里,镜像里已经包含CUDA、cuDNN和常见的深度学习框架。我本地用的是PyTorch,所以选择了对应版本的预置镜像,版本尽量与本地一致。
同时,我把依赖写进requirements.txt,并尽量锁定版本号,而不是写大于等于。为什么会强调这个?因为本地能跑的代码,到了云端镜像里,很可能因为某个传递依赖版本偏高,出现兼容性问题。我遇到过几次No module named "torch._C"这种看起来很诡异的报错,最后都是因为torchvision和pytorch版本不匹配。
如果你本地环境很复杂,先把关键包用pip freeze导出来,再手工筛选训练所需的依赖,不要整份文件原样复制。
提示:基础镜像里已经包含常见的深度学习框架,requirements.txt只写额外依赖即可。写得太多太长,反而增加因版本冲突导致作业失败的概率。
3. 创建训练作业:第一版模型怎么跑起来
3.1 建训练作业前,先确认四个参数
创建训练作业时,控制台一般要求填四项内容:数据输入路径、训练输出路径、计算规格、日志输出路径。我第一次建作业时,填写路径非常随意,结果第一个作业跑了几十分钟,突然报空间不足,原因是我把输出路径填成了数据路径下的一个子目录,模型每个epoch保存一次,就在数据目录里重复上传大文件,白白浪费时间和存储配额。
这四个参数的含义,要对应到平台替你做的四件事:
| 参数 | 平台动作 | 常见错误 |
|---|---|---|
| 数据输入路径 | 把对象存储路径挂载进容器 | 填错导致代码找不到文件 |
| 训练输出路径 | 把容器内输出目录同步回对象存储 | 填成数据目录,反复上传 |
| 计算规格 | 决定CPU、GPU卡数和内存 | 选得太低训练极慢 |
| 日志输出路径 | 收集stdout和容器日志 | 不填导致排查问题无日志 |
3.2 用一行命令验证挂载和路径
正式开始训练之前,强烈建议先用一个最简单的命令,验证路径是否真的通了:
ls -l /标注的输入路径或者用Python更好地递归确认:
python -c "import os; print(os.listdir('/标注的输入路径/'))"这个验证作业大概十几秒就能跑完。如果能看到数据文件列表,说明输入路径挂载正确;再随便写一个文件到输出路径,确认输出路径可写。这一步通过之后,后续跑正式训练,路径相关的错误概率会大大下降。
我见过不少同学急着提交真正的训练作业,结果日志里报FileNotFoundError,又回头花半小时找原因,其实用一分钟先验证路径就能解决。路径验证这一步,性价比极高。
3.3 日志和训练指标,比想象中更需要认真对待
容器里的标准输出,平台会抓取到日志服务里,训练详情页可以实时查看。但是有一个细节:直接使用print()输出,在容器里往往有缓冲,日志刷新会很滞后。我改用Python原生的logging模块,同时设置好级别和格式,日志出现的时机更可控,排查问题时也更容易按级别过滤。
训练指标方面,我习惯把每个epoch的loss和accuracy保存成TensorBoard的event文件,放到训练输出目录的tb_log/子目录下。训练结束后,在平台的Notebook里加载同一个输出路径,就能画损失曲线和准确率曲线。这样比在日志里翻数字直观得多。
断点续训也是需要考虑的环节。每次保存checkpoint时,不要只保留最新的,建议保留最近两到三个版本,并且把文件名带上epoch编号。这样万一某次实验效果不好,还能回到之前的状态重新调整。
4. 从“训练完”到“可调用”:在线推理部署的完整操作
4.1 模型落盘的固定格式,越早规划越好
不少人在训练作业里直接把模型保存为./model.pth,等到部署时才意识到,部署服务根本不知道模型文件在哪里。平台部署时通常需要三个信息:模型文件的位置、推理代码、运行环境。
我建议在训练脚本里,把输出目录结构规划成下面这样:
output/ checkpoint/ model_final.pth tb_log/ events_out.tfevents.* meta/ label_to_id.json eval_results.json代码里固定使用os.path.join(args.output, "checkpoint", "model_final.pth")来保存模型,并在训练结束时打印出模型文件的绝对路径。这个习惯形成以后,每次训练完,我都知道去哪里找产物,部署时也能快速指定路径。把label_to_id.json这类元信息一并保存下来,部署阶段做后处理会方便很多。
4.2 推理代码的封装,不只是把模型forward一遍
在线推理服务收到一个请求,真正要执行的步骤比很多人想象的多:反序列化请求、预处理、模型推理、后处理、序列化响应。我写推理代码时,会单独做一个inference.py,里面暴露一个核心入口函数,结构大致是:
def predict(request_body: dict) -> dict: image = decode_image(request_body["image_base64"]) tensor = preprocess(image) with torch.no_grad(): logits = model(tensor) result = postprocess(logits, request_body.get("topk", 5)) return resultpreprocess和postprocess拆开写,便于单测和调试。预处理通常包括图像解码、缩放、归一化;后处理包括softmax、topK筛选、标签映射。如果推理阶段和训练阶段的数据预处理不一致,模型效果会凭空掉一大截,这一点务必保持一致。
框架的选择也很重要。如果部署环境提供的推理框架与训练框架不同,那还得先做模型格式转换。我在部署前会确认平台支持哪种模型格式,比如PyTorch的权重文件、ONNX或者TensorFlow的SavedModel。最稳的方案,是直接用平台预置的推理镜像,加载与训练同框架的模型格式。
4.3 服务资源配置,别一上来就选最高规格
部署在线服务,需要选择CPU还是GPU实例、实例数量、弹性伸缩策略。早期实验阶段,我建议先用CPU实例跑通接口,因为在线推理如果请求量不大,CPU实例完全扛得住;确认业务逻辑正确后,再根据实际延迟决定是否需要GPU。
弹性扩缩容的阈值设置,我习惯把最小实例数设为1,最大实例数设为4,以CPU使用率在60%~80%之间作为扩容触发条件。这个设置很像“水位线”,高了就扩容,低了就缩容,避免请求突增时服务被压垮。不过,要留意一点:实例数变多之后,负载均衡和缓存命中的问题也会冒出来,数据需要存放在共享存储里,不能只存在某一台实例的本地磁盘上。
4.4 API调用与鉴权,掌握请求和返回结构就够了
部署成功后,平台会给出一个调用地址和鉴权信息,常见的是token或AK/SK方式。一个最简单的HTTP POST请求是这样的:
{ "image_base64": "...", "topk": 5 }返回体:
{ "class_ids": [10, 7, 12, 4, 22], "scores": [0.87, 0.05, 0.03, 0.02, 0.01] }这一步看起来简单,但还是有几个细节需要注意:一是鉴权头信息不要泄露,写进代码仓库等于公开了自己的调用额度;二是调用要设置超时时间,尤其是GPU实例冷启动时,第一个请求可能会慢一点;三是建议在本地写一个简单的压力脚本,验证一下服务在持续请求下的表现,再交给下游使用。
5. 踩坑记录:六个让我白费大半天时间的问题
5.1 训练秒挂,日志里只有一个FileNotFoundError
第一次正式提交训练作业,点下“创建”没几分钟就失败了。点开日志,底部躺着一行FileNotFoundError: [Errno 2] No such file or directory: 'data/train'。
我排查了很久才想清楚,控制台上填写的输入路径是对象存储路径,比如obs://my-bucket/dataset,但容器内实际挂载的路径是另一个本地路径,比如/obs/my-bucket/dataset。启动命令却用了相对路径data/train去读,容器里根本没这个目录。
从那以后,我的启动命令一律改成从平台注入的环境变量读取路径。不同的平台注入方式不同,但思路一致:用环境变量或占位符指定数据路径和输出路径,而不是手动复制控制台里的路径字符串。
5.2 本地能跑,云端却提示No module named "xxx"
有一次,训练作业里报缺少一个数据增强库。我第一反应是“我在本地已经装过了呀”。但训练作业跑在一个全新的容器里,本地Notebook里安装过的包不会自动跟随。
这类问题,根源在于本地环境与云端镜像的环境不一致。解决办法是维护好requirements.txt,并且锁定版本。我还专门试过一次,不锁版本,只写包名,结果云端装到了新版库,个别API的参数名变了,代码直接报TypeError。锁版本虽然看起来保守,却能最大程度保证可复现性。
5.3 模型保存位置不固定,部署时找不到文件
早期我把模型直接保存到./model.pth,相当于当前工作目录的根,没有放到明确的子目录里。训练作业跑完,输出目录里就一个孤零零的文件,部署时要到日志里找确切位置,反复人工确认。更麻烦的是,如果平台对输出目录有同步策略,直接丢在根目录的文件可能被各种临时文件干扰。
后来我固定了前述的目录结构,把模型放到checkpoint/子目录,并在训练代码中显式打印保存路径。平台有模型版本管理功能的,也尽量把产物登记进去,而不是只依赖文件系统。
5.4 推理服务一直报模型加载失败
部署完在线服务,我调用接口,报错提示模型加载失败。一开始我怀疑是模型文件损坏,重新导出了一次,还是一样。后来仔细翻推理服务的日志,发现它报缺少utils/模块——推理代码里引用了一个工具函数,但部署时我只上传了inference.py,没有把整个工程目录一起打包。
推理服务的代码包,和训练代码一样,要按工程目录完整上传。尤其要注意相对导入和动态依赖,凡是import到的模块,都必须包含在代码包里。这一点看似简单,却很容易遗漏。
5.5 镜像tag覆盖,导致训练环境“没有变化”
有次我修改了镜像里的依赖,重新构建后用同一个tag推送上去,再创建新的训练作业,结果作业跑起来还是旧环境,新依赖根本没生效。原因是对底层节点上的镜像缓存,相同tag不会重新拉取。
为了避免这种问题,我现在每次构建镜像,都会打一个新的日期时间tag,比如pytorch-1.13-custom-20240115-1830。新作业明确指定新tag,就不会因为缓存覆盖而拿到旧环境。这个习惯在多人协作时尤其重要,否则很容易出现“我这边改了,跑起来还是老版本”的尴尬。
5.6 晚上提交的训练,第二天早上才发现已经挂掉
有次晚上提交了一个预计训练十多个小时的作业,第二天上午一看,凌晨三点就失败了,等于白等一整晚。后来我做了两件事:第一,给训练作业设置失败告警,无论邮件还是短信,至少能立刻知道状态;第二,在训练代码里增加定期心跳日志,每隔一段时间打印一次进度,确保日志流能反映训练确实在推进。
长任务跑到一半出错,其实是平台使用中最浪费的一类问题。先花十几分钟把告警配好,再启动任务,是成本最低但收益最明显的举措。
6. 跑完整个流程之后,我对平台化工作方式的几点体会
先说工作流。我现在的习惯是:先在Notebook里加载少量数据,快速验证模型代码能跑通,再提交全量训练作业。这一步看起来多花了一些时间,实际上把“代码与环境问题”隔离在小规模阶段,比直接打全量训练再回来改代码要高效得多。同时,我会把整个训练工程纳入版本管理,训练脚本、requirements.txt、启动命令配置都跟着代码库一起走。需要复现某个实验时,拉取对应提交记录就能重建完整环境。
再说成本意识。训练结束后,一定记得及时释放资源,尤其是GPU规格,按小时计费的情况下,挂一个空闲GPU实例就是在不断烧钱。我习惯在训练作业里设置好最大运行时长,并保证训练结束前一定释放临时资源。部署服务在没有业务流量时,也可以缩容到0或直接停止。
最后分享一点个人感受。云AI开发平台真正有价值的地方,不只是免去自己维护GPU服务器,而是把模型交付的路径理顺了:数据有确定的位置,训练有标准的产出,模型有版本记录,推理有固定的接口。每一步都有明确的产物和交接位置,这比在本地“跑出一个模型”更接近工业界的交付方式。
把“数据路径、代码包、镜像、启动命令、产物定义”这套配置思维建立起来,就算以后换到其他云厂商的工具,需要改的也只是路径前缀和参数名。核心方法是一致的,迁移成本比我最初预想的小很多。