1. 从一场 MeetUp 看训推融通的真实痛点
PyTorch 生态在国内的落地,这几年明显在加速。从最早大家还在纠结“装 PyTorch 到底用 conda 还是 pip”“CUDA 版本对不上怎么办”,到现在越来越多团队开始关心一个更实际的问题:训练和推理能不能用同一套框架、同一套代码、同一套硬件跑通。这个转变背后,是大量模型从实验室走向生产环境时暴露出来的割裂感——训练用一套栈,推理又要换一套,中间还要做格式转换、精度对齐、算子适配,每一步都是坑。
龙蜥社区和壁仞科技联合办的这场 PyTorch 生态共建 MeetUp,主题落在“训推融通新路径”上,其实就是在回应这个痛点。所谓训推融通,说白了就是让训练侧和推理侧共享同一套底层能力,减少重复适配的成本。而 TorchSUPA 这个名字,是这场活动里绕不开的核心——它是壁仞基于 PyTorch 生态做的一套扩展方案,目标就是让 PyTorch 的模型能在国产加速卡上顺畅地跑起来,不管是训练还是推理。
这篇文章不是活动通稿的复述,而是想从一个一线开发者的视角,把这场 MeetUp 背后涉及的技术脉络拆开讲清楚。PyTorch 基础框架怎么和国产硬件适配、训推融通到底难在哪、TorchSUPA 这类方案解决了什么问题、普通开发者怎么上手,这些才是真正有价值的部分。如果你正在做 PyTorch 模型部署,或者正在评估国产算力方案,下面的内容应该能帮你少走一些弯路。
2. PyTorch 生态适配的核心难点拆解
2.1 为什么 PyTorch 适配国产硬件不是“装个驱动”那么简单
很多人对 PyTorch 适配的理解停留在“装好驱动、装好 CUDA、跑起来就行”。这个认知在 NVIDIA 生态里基本成立,因为 CUDA 和 cuDNN 已经把底层算子封装得很完整,PyTorch 官方对 CUDA 的支持也是一等公民。但换到非 CUDA 的加速卡上,事情就完全不一样了。
PyTorch 的算子体系是分层的。最上层是 Python API,比如torch.nn.Conv2d、torch.matmul;中间是 ATen 算子库,这是 PyTorch 的核心计算层;最下面是具体的后端实现,CUDA 后端就是 cuDNN、cuBLAS 这些库。当你要把 PyTorch 跑到另一套硬件上时,最上层的 Python API 不用动,但 ATen 层往下就需要重新对接。这意味着每一个算子都要有对应的硬件实现,而且精度、性能、内存管理都要对齐。
这就是为什么 PyTorch 适配一个新硬件平台,工作量远比想象中大。不是写一个 backend 就完事,而是要覆盖几百个算子的实现、验证、调优。更麻烦的是,PyTorch 版本迭代很快,每次大版本升级都可能引入新的算子或者改变算子行为,适配层要持续跟进。
2.2 训练和推理的割裂到底割在哪
训练和推理看起来都是“跑模型”,但实际对底层的要求差别很大。训练侧关注的是梯度计算、反向传播、混合精度、分布式通信,对算子的要求是“全”——你得支持所有前向和反向算子。推理侧关注的是延迟、吞吐、量化、算子融合,对算子的要求是“精”——常用算子要极致优化,不常用的可以慢慢补。
这种差异导致了一个现实:很多硬件厂商在适配 PyTorch 时,会优先做推理侧的适配,因为推理场景更容易落地,客户需求也更明确。训练侧的适配往往滞后,因为工作量大、验证周期长。结果就是用户拿到卡之后发现,推理能跑,训练跑不了,或者训练能跑但性能不理想。
训推融通要解决的就是这个问题。理想状态下,同一套 PyTorch 代码,训练和推理都能跑在同一张卡上,不需要换框架、不需要转格式、不需要重新调优。这要求底层适配层同时覆盖训练和推理的算子需求,并且在两个场景下都有可接受的性能表现。
2.3 TorchSUPA 的定位和它想解决的问题
TorchSUPA 是壁仞针对 PyTorch 生态做的扩展层。从命名来看,SUPA 应该是壁仞的软件栈名称,TorchSUPA 就是 PyTorch 对接这套软件栈的桥梁。它的核心目标,是让 PyTorch 的模型能够以较小的改动成本迁移到壁仞的硬件上运行。
这类方案通常的做法是:在 PyTorch 的 dispatch 机制里注册一个新的 backend,把 ATen 算子映射到自家硬件的实现上。同时提供设备管理、内存管理、流管理等基础设施,让 PyTorch 的运行时能够识别并调度这些设备。对于用户来说,最直观的体验就是torch.device("supa")或者类似的设备标识,然后把模型.to(device)就能跑。
但真正决定这套方案好不好用的,是算子覆盖率和性能。算子覆盖率决定了你能跑多少模型,性能决定了你跑得划不划算。这两点都需要长期的投入和优化,不是一次 MeetUp 就能解决的。不过从生态共建的角度看,龙蜥社区和壁仞的合作,至少说明这件事有人在认真做,而且愿意把进展拿出来和开发者交流。
3. 训推融通的技术路径与实操要点
3.1 从 PyTorch 到国产硬件的适配链路
要把 PyTorch 模型跑到国产加速卡上,完整的链路大致是这样的:模型定义在 Python 层,经过 TorchScript 或者 eager 模式进入 ATen 算子层,ATen 算子通过 dispatch 机制路由到具体的 backend,backend 调用硬件厂商提供的算子库,算子库再调用驱动和硬件。
这条链路里,最容易出问题的环节是算子路由和算子实现。PyTorch 的 dispatch 机制会根据 tensor 的 device type 来决定用哪个 backend。如果某个算子在目标 backend 上没有注册实现,就会报错或者 fallback 到 CPU。fallback 到 CPU 虽然能跑通,但性能会断崖式下跌,而且可能引入 device 不一致的问题。
所以适配工作的核心,就是尽可能多地注册算子实现,并且保证这些实现的数值精度和 CUDA 版本对齐。精度对齐这件事听起来简单,做起来很磨人。同一个算子,不同硬件上的浮点累加顺序可能不同,导致结果有微小差异。对于推理来说,这点差异通常可以接受;但对于训练来说,梯度累积的误差可能会被放大,导致模型收敛行为不一致。
3.2 环境搭建:从 conda 到 PyTorch 的完整流程
不管用什么硬件,PyTorch 环境搭建都是第一步。这一步踩坑最多,我见过太多人卡在conda : 无法将"conda"项识别这种问题上。这里把完整流程捋一遍,以 Linux 环境为例。
首先是 Python 环境管理。Miniconda 或者 Anaconda 都行,安装完之后要确保 conda 在 PATH 里。Windows 上如果遇到 conda 命令不识别,通常是安装时没勾选“Add to PATH”,或者 PowerShell 的执行策略限制。Linux 上一般是没 source 对应的 shell 配置文件。
# 下载并安装 Miniconda wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh source ~/.bashrc # 创建独立环境 conda create -n pytorch_env python=3.10 conda activate pytorch_env创建独立环境这一步很重要。PyTorch 不同版本对 Python 和 CUDA 的要求不一样,混在一个环境里迟早出问题。我习惯给每个项目单独建环境,虽然占点磁盘空间,但省心。
接下来是 PyTorch 安装。如果目标硬件有官方提供的 PyTorch 包,优先用官方的,因为里面已经包含了适配好的 backend。如果没有,就用 PyTorch 官方源装 CPU 版本或者 CUDA 版本,再单独装硬件厂商的适配包。
# 以 CUDA 12.1 为例,从官方源安装 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 验证安装 python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"验证这一步别省。torch.cuda.is_available()返回 False 的情况太常见了,原因可能是驱动版本不匹配、CUDA 版本不匹配、或者 PyTorch 装成了 CPU 版本。早发现早解决,别等到跑模型的时候才报错。
3.3 模型迁移中的算子兼容性检查
环境搭好之后,下一步是把模型跑起来。这时候最容易遇到的就是算子不支持的问题。PyTorch 的报错信息通常会告诉你哪个算子没有实现,比如NotImplementedError: The operator 'aten::xxx' is not currently implemented for the supa device。
遇到这种情况,有几个处理思路。第一是看有没有替代算子,比如某些自定义算子可以用基础算子组合实现。第二是看能不能把不支持的算子放到 CPU 上跑,虽然性能有损失,但至少能跑通。第三是向硬件厂商反馈,推动他们补充算子实现。
我一般会写一个简单的脚本,把模型里用到的所有算子列出来,然后逐个检查在目标设备上是否支持。这样可以在正式跑训练之前就发现问题,而不是跑了一半才崩。
import torch # 列出模型用到的所有算子 def list_operators(model, input_shape): device = torch.device("cpu") model = model.to(device) x = torch.randn(input_shape).to(device) # 使用 torch.jit.trace 获取计算图 traced = torch.jit.trace(model, x) graph = traced.graph operators = set() for node in graph.nodes(): if node.kind() == "aten::": operators.add(node.schema()) return operators # 检查算子是否在目标设备上支持 def check_operator_support(operators, device_type): unsupported = [] for op in operators: try: # 尝试在目标设备上执行 pass except NotImplementedError: unsupported.append(op) return unsupported这个脚本只是个框架,实际用的时候要根据具体模型调整。核心思路就是提前暴露问题,别等到训练脚本跑起来才发现算子不支持。
3.4 训练侧的关键配置:混合精度与分布式
模型能跑通之后,接下来要关心的是训练效率和稳定性。混合精度训练(AMP)是现在的基本操作,能显著降低显存占用、提升训练速度。但混合精度对硬件的要求也更高,需要硬件支持 FP16 或者 BF16 的计算。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for data, target in dataloader: optimizer.zero_grad() with autocast(): output = model(data) loss = criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这段代码在 CUDA 上是标准写法,但换到其他硬件上,autocast和GradScaler的行为可能不一样。有些硬件对 FP16 的支持不完整,或者梯度缩放策略不同,需要根据厂商文档调整。我建议先在单卡上验证混合精度训练的收敛性,确认没问题再上分布式。
分布式训练是另一个大坑。PyTorch 的 DDP(DistributedDataParallel)依赖 NCCL 做通信,换到非 CUDA 硬件上,通信库也要换。壁仞这类厂商通常会提供自己的通信库,接口上兼容 NCCL,但底层实现不同。配置的时候要注意环境变量和初始化方式。
import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP # 初始化进程组 dist.init_process_group(backend="nccl") # 非 CUDA 硬件可能需要换成厂商提供的 backend # 包装模型 model = DDP(model, device_ids=[local_rank])分布式训练最怕的是通信 hang 住,表现是程序卡在某一步不动,也不报错。排查这种问题,首先要确认所有进程都正常启动,然后检查网络配置和防火墙。如果用的是厂商提供的通信库,还要确认版本和硬件匹配。
4. 推理侧优化与训推一致性保障
4.1 推理性能优化的几个抓手
推理侧和训练侧的优化思路完全不同。训练侧追求的是吞吐和收敛速度,推理侧追求的是延迟和吞吐的平衡。常见的推理优化手段包括算子融合、量化、动态 shape 支持、KV Cache 优化等。
算子融合是最基础也最有效的手段。比如 Conv + BN + ReLU 这三个算子,在推理时可以融合成一个算子,减少内存访问和 kernel launch 开销。PyTorch 本身有torch.jit.freeze和torch.jit.optimize_for_inference可以做一部分融合,但更激进的融合需要硬件厂商的编译器支持。
量化是另一个大杀器。FP16 量化通常能带来 2 倍左右的加速,INT8 量化能到 4 倍。但量化会引入精度损失,需要做校准(calibration)来最小化误差。PyTorch 提供了torch.quantization模块,支持动态量化和静态量化。静态量化需要校准数据集,流程稍微复杂一点,但效果更好。
import torch.quantization as quantization # 静态量化示例 model.eval() model.qconfig = quantization.get_default_qconfig('fbgemm') model_prepared = quantization.prepare(model) # 用校准数据跑一遍 with torch.no_grad(): for data in calibration_loader: model_prepared(data) model_quantized = quantization.convert(model_prepared)这段代码在 CPU 上跑没问题,但换到加速卡上,量化方案可能不同。有些硬件有自己的量化工具链,需要把 PyTorch 模型导出成 ONNX 或者厂商自定义格式,再用他们的工具做量化。这就涉及到格式转换的问题。
4.2 PyTorch 转 ONNX 的实操与坑点
ONNX 是模型交换的事实标准,PyTorch 转 ONNX 也是常见操作。但转换过程并不总是顺利,尤其是模型里有自定义算子或者动态控制流的时候。
import torch.onnx # 导出 ONNX dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "model.onnx", export_params=True, opset_version=13, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} )opset_version的选择很关键。版本太低可能不支持某些算子,版本太高可能目标推理引擎不支持。一般建议选 11 到 13 之间的版本,兼容性比较好。dynamic_axes用来支持动态 batch size,如果推理时 batch size 会变,这个必须加。
转换之后一定要验证。用 ONNX Runtime 跑一遍,和 PyTorch 的输出对比,确认数值误差在可接受范围内。我见过太多转完之后精度掉点的情况,原因可能是算子实现差异、常量折叠出错、或者数据类型转换问题。
import onnxruntime as ort import numpy as np # 验证 ONNX 模型 ort_session = ort.InferenceSession("model.onnx") ort_inputs = {ort_session.get_inputs()[0].name: dummy_input.numpy()} ort_outputs = ort_session.run(None, ort_inputs) # 对比 PyTorch 输出 with torch.no_grad(): torch_output = model(dummy_input) # 计算误差 diff = np.abs(ort_outputs[0] - torch_output.numpy()).max() print(f"Max diff: {diff}")如果误差太大,就要逐步排查。先检查输入数据是否一致,再检查算子版本,最后看是不是某些算子转换有问题。ONNX 的调试工具onnx.checker和onnx.shape_inference可以帮忙定位问题。
4.3 训推一致性怎么保证
训推一致性是训推融通的核心目标之一。理想情况下,同一个模型在训练和推理时的数值行为应该一致,至少要在可接受的误差范围内。但实际中,训练和推理的数值差异来源很多:算子实现不同、精度不同、并行策略不同、甚至内存布局不同都可能导致结果差异。
保证训推一致性,首先要统一算子实现。训练和推理用同一套算子库,这是最根本的。如果训练用一套、推理用另一套,一致性就很难保证。TorchSUPA 这类方案的价值就在这里——它提供统一的算子实现,训练和推理都走同一套底层。
其次要统一精度策略。训练时用 FP32 或者 AMP,推理时用 FP16 或者 INT8,精度差异必然导致结果差异。如果业务对一致性要求高,推理时也要用 FP32,或者做精度对齐的校准。
最后要做端到端的验证。拿一个训练好的模型,分别在训练环境和推理环境下跑同一批数据,对比输出。如果差异在阈值内,说明一致性达标;如果差异大,就要逐层排查,找到差异最大的层,分析原因。
5. 常见问题排查与避坑经验
5.1 环境类问题速查
环境问题是最高频的,我整理了一个速查表,覆盖大部分常见情况。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| conda 命令不识别 | PATH 未配置 | echo $PATH | 重新 source 配置文件或手动添加 PATH |
| torch.cuda.is_available() 返回 False | 驱动/CUDA 版本不匹配 | nvidia-smi查看驱动版本 | 安装匹配的 PyTorch 版本 |
| 导入 torch 报错 | 依赖库缺失 | 查看报错信息 | 安装缺失的依赖,如 libgomp |
| 显存不足 | batch size 太大 | torch.cuda.memory_summary() | 减小 batch size 或使用梯度累积 |
| 训练速度慢 | 数据加载瓶颈 | 检查 DataLoader 的 num_workers | 增加 num_workers 或使用更快的存储 |
这张表里的问题,我几乎每个都遇到过。特别是 conda 命令不识别,Windows 上尤其常见。最稳妥的办法是安装时勾选“Add to PATH”,如果忘了勾,就手动把 conda 的路径加到系统环境变量里。
5.2 算子不支持时的应急处理
算子不支持是迁移过程中最头疼的问题。除了前面说的替代方案,还有一个应急处理办法:用torch.jit.script把不支持的算子替换成自定义实现。PyTorch 允许注册自定义算子,你可以用基础算子组合出一个功能等价的实现。
import torch # 注册自定义算子 @torch.jit.script def custom_op(x, y): # 用基础算子实现 return x * y + x # 在模型中使用 class MyModel(torch.nn.Module): def forward(self, x, y): return custom_op(x, y)这个办法的缺点是性能可能不如原生实现,但至少能让模型跑起来。等厂商补充了算子实现,再切回去就行。
5.3 性能调优的实操心得
性能调优是个细活,没有银弹。我的经验是,先定位瓶颈,再针对性优化。用 PyTorch Profiler 可以比较方便地找到热点。
from torch.profiler import profile, ProfilerActivity with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: model(input_data) print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))这个输出会告诉你哪些算子耗时最多。如果是某个算子特别慢,可能是实现不够优化,可以尝试用厂商提供的优化版本替换。如果是数据加载慢,就优化 DataLoader。如果是通信慢,就检查网络和通信库配置。
还有一个容易被忽略的点是内存分配。PyTorch 的缓存分配器在 CUDA 上表现很好,但换到其他硬件上可能不一样。如果发现显存碎片化严重,可以尝试设置PYTORCH_CUDA_ALLOC_CONF环境变量,或者手动管理内存。
6. 生态共建对开发者的实际意义
6.1 从“能用”到“好用”还有多远
国产硬件适配 PyTorch,目前阶段大部分还处在“能用”的水平。基本算子能跑,常见模型能训,但距离“好用”还有差距。好用的标准是什么?我觉得至少包括:算子覆盖率达到 95% 以上、性能达到同级别硬件的 80% 以上、文档和工具链完善、社区响应及时。
龙蜥社区和壁仞的这次合作,方向是对的。生态共建不是一家公司能搞定的事,需要硬件厂商、框架社区、应用开发者一起参与。硬件厂商提供底层能力和算子实现,框架社区提供集成和测试,应用开发者提供真实场景和反馈。这个循环转起来,适配进度才会快。
对普通开发者来说,参与生态共建的门槛其实不高。遇到算子不支持,提个 issue 或者 PR;发现文档有误,反馈一下;有好的实践,写篇博客分享。这些看似微小的贡献,积累起来就是生态的进步。
6.2 开发者现在可以做什么
如果你手头有壁仞的卡,或者想尝试国产算力方案,现在就可以做几件事。第一,搭一套 PyTorch 环境,跑通一个经典模型,比如 ResNet 或者 BERT,记录遇到的问题。第二,试试 TorchSUPA 的接口,看看迁移成本有多大。第三,如果遇到问题,去社区反馈,推动解决。
如果你还没有硬件,也可以关注龙蜥社区和壁仞的后续活动。这类 MeetUp 通常会释放一些测试资源或者合作机会,对开发者来说是低成本接触新硬件的好途径。
我在实际使用中的体会是,国产算力的进步速度比想象中快。两年前很多模型还跑不起来,现在主流模型基本都能跑了。训推融通这个方向,虽然还有很多硬骨头要啃,但至少路径已经清晰了。对于做 AI 基础设施的团队来说,现在开始积累国产硬件上的经验,未来会是一个不小的优势。