1. 从“同桌”这个词说起:一个信号背后的技术分量
“寒武纪拿下PyTorch最高席位,与英伟达同桌”——这个标题我第一次看到的时候,正在调一个模型训练脚本,手边跑着的是一台装了消费级显卡的机器。说实话,第一反应不是兴奋,而是“终于”。因为做深度学习框架适配这行的人都知道,PyTorch的技术治理结构里,能进入核心决策层的企业,从来不只是“贡献了几行代码”那么简单。
PyTorch基金会(PyTorch Foundation)的治理架构,是2022年从Meta手里剥离出来、交给Linux基金会托管之后逐步成型的。它的理事会(Governing Board)和技术咨询委员会(Technical Advisory Council)席位,基本代表了全球深度学习框架生态里最有话语权的一批玩家。英伟达、AMD、Meta、Google、微软、亚马逊这些名字长期占据核心位置,原因很直接:它们要么是框架的主要贡献者,要么是硬件后端的主要实现方,要么是最大规模的使用方。
寒武纪能拿到这个席位,意味着它在PyTorch生态里的角色,从“下游适配者”变成了“上游共建者”。这个转变的技术含量,比很多人想象的要高得多。我见过太多人把“支持PyTorch”理解成“能跑起来就行”,但真正做过框架后端适配的工程师都清楚,从“能跑”到“进治理层”,中间隔着的是对框架核心抽象的理解深度、对算子语义的精确实现、以及对整个编译栈的持续投入。
这篇文章我想聊的不是新闻本身,而是这个信号背后,一个AI芯片公司的软件栈到底要具备什么样的能力,才能走到这个位置。同时,我也会把PyTorch环境搭建、芯片适配、算子实现这些实操层面的东西拆开讲清楚,让不管是刚入门的新手,还是正在做国产硬件适配的同行,都能从中拿到能直接用的东西。
2. PyTorch生态的“席位”到底意味着什么
2.1 基金会治理结构里的技术话语权
PyTorch基金会的理事会成员,通常来自几个类别:创始成员(Meta、AMD、AWS、Google、Microsoft、NVIDIA等)、一般成员、以及关联成员。每个席位背后对应的不只是资金赞助,更重要的是技术方向的投票权和标准制定参与权。
技术咨询委员会(TAC)的职责更偏工程侧,负责审批新的后端接入方案、评审核心API的变更提案、协调跨厂商的算子语义一致性。一个硬件厂商如果只是“能跑PyTorch”,它只需要维护一个torch.compile的后端或者一个PrivateUse1的设备扩展就行。但进入TAC或理事会,意味着你要参与决定“下一个版本的PyTorch,设备抽象层应该怎么改”“新的算子注册机制要不要兼容旧后端”这类问题。
我举个具体的例子。PyTorch 2.x引入的torch.compile和TorchInductor,对后端硬件提出了全新的要求。以前你只要实现一套ATen算子就能跑Eager模式,现在还要考虑Dynamo的图捕获、Inductor的代码生成、以及不同后端之间的调度策略。如果一个芯片厂商在TAC里有席位,它就能在Inductor的后端接口设计阶段就提出意见,而不是等接口冻结了再去逆向适配。
2.2 从“适配”到“共建”的技术门槛
我接触过不少做国产芯片软件栈的团队,大家普遍有一个误区:觉得把PyTorch的算子库对着文档实现一遍,跑通ResNet和BERT,就算“支持PyTorch”了。这个标准在2019年可能还说得过去,放到今天远远不够。
现在的PyTorch生态,一个合格的硬件后端至少要覆盖这几层:
- ATen算子层:这是最基础的,几百个算子的语义要跟CUDA后端对齐,包括各种边界条件、数据类型提升规则、广播语义。
- 调度与内存管理层:PyTorch的CUDACachingAllocator那套内存池机制,在非CUDA设备上怎么复现,直接影响到训练时的显存利用率和碎片化程度。
- 图编译层:Dynamo捕获的FX图,要能被你的后端正确消费。Inductor生成Triton代码的那套流程,如果你的硬件不支持Triton,就得自己实现一套等价的代码生成路径。
- 分布式训练层:NCCL在CUDA生态里的地位不用多说,非CUDA设备要接入PyTorch的分布式接口,要么实现一套兼容NCCL API的通信库,要么走Gloo的路线但性能会打折扣。
- 量化与推理优化层:INT8、FP8这些低精度格式的支持,以及和
torch.ao量化工具链的对接。
寒武纪能进理事会,说明它在这些层面至少拿出了让社区认可的方案。具体是哪些层面,公开信息里没有完整披露,但从它之前开源的torch_mlu(Cambricon PyTorch扩展)和catch(寒武纪的PyTorch后端)来看,ATen算子覆盖和调度层是下了功夫的。
2.3 对普通开发者的实际影响
你可能会问:一个芯片公司进理事会,跟我一个天天写nn.Module的人有什么关系?
关系很直接。最明显的一点是,以后你在PyTorch里用torch.mlu或者类似的设备抽象时,它的行为会更接近torch.cuda。以前国产芯片的PyTorch适配经常出现“这个算子不支持”“那个dtype报错”的情况,很大程度上是因为适配方没有参与上游设计,只能被动跟着CUDA后端的实现走。有了席位之后,设备抽象层的接口设计会更考虑多后端的通用性,而不是默认以CUDA为唯一参考。
另一个影响是文档和教程的覆盖。PyTorch官方教程里如果开始出现非CUDA设备的示例,对新手来说学习成本会低很多。我见过太多人第一次接触国产AI芯片时,卡在“怎么把.cuda()换成对应的设备调用”这一步上。
3. 芯片适配PyTorch的完整技术路径拆解
3.1 设备抽象层:PrivateUse1机制怎么用
PyTorch从1.13开始正式引入了PrivateUse1这个设备类型,专门给非CUDA、非ROCm的第三方硬件用。它的设计初衷就是让芯片厂商不用改PyTorch核心代码,通过扩展机制就能注册自己的设备。
具体来说,你需要做这几件事:
# 注册设备名称 torch.utils.rename_privateuse1_backend("mlu") # 注册设备模块 torch._register_device_module("mlu", MLUModule) # 生成对应的设备类型 torch.utils.generate_methods_for_privateuse1_backend()这三行代码执行完之后,你就可以像用torch.cuda一样用torch.mlu了。tensor.mlu()、torch.mlu.current_device()、torch.mlu.synchronize()这些方法都会自动生成。
但这里有个坑:rename_privateuse1_backend必须在任何张量创建之前调用,而且一个进程里只能调用一次。我见过有人在Jupyter Notebook里反复执行注册代码,结果第二次就报错。正确的做法是把它放在包的__init__.py里,或者用一个单独的初始化模块来管理。
3.2 算子注册:从ATen到你的硬件
PyTorch的算子注册机制,核心是TORCH_LIBRARY和TORCH_LIBRARY_IMPL这两个宏。对于第三方后端,你需要为每个算子实现对应的kernel,然后注册到你的设备类型上。
// 以add算子为例 TORCH_LIBRARY_IMPL(aten, PrivateUse1, m) { m.impl("add.Tensor", TORCH_FN(mlu_add_tensor)); m.impl("add.Scalar", TORCH_FN(mlu_add_scalar)); m.impl("add.out", TORCH_FN(mlu_add_out)); }这里的关键是算子变体。PyTorch里一个add操作可能有十几个变体:add.Tensor、add.Scalar、add.out、add.Scalar_out、add_.Tensor(原地操作)等等。你如果只实现了add.Tensor,那用户写torch.add(a, b, out=c)的时候就会报“未实现”的错误。
我的经验是,先把PyTorch的native_functions.yaml里所有标记为CompositeExplicitAutograd的算子过一遍,这些是可以通过组合其他算子实现的,优先级可以放低。真正要优先实现的是CompositeImplicitAutograd和那些直接对应硬件指令的算子。
3.3 内存管理:别小看CachingAllocator
CUDA生态里,CUDACachingAllocator是PyTorch显存管理的核心。它通过缓存已分配的内存块,避免频繁调用cudaMalloc和cudaFree带来的性能开销。在非CUDA设备上,如果你直接用malloc和free,训练速度可能会掉30%以上。
寒武纪的torch_mlu里实现了一套MLUCachingAllocator,基本思路和CUDA版本一致:维护一个按大小分桶的空闲块列表,分配时优先从缓存里找合适大小的块,找不到再向驱动申请。释放时不立即归还给驱动,而是放回缓存。
这里有个细节值得注意:内存池的大小和碎片化策略。CUDA的allocator默认会保留所有释放的块,直到进程结束。如果你的设备显存比较小(比如推理卡只有16GB),可能需要设置一个上限,超过之后主动释放一些块。PyTorch提供了torch.cuda.memory._set_allocator_settings这样的接口,第三方后端也可以实现类似的配置项。
3.4 图编译与Inductor后端对接
PyTorch 2.x之后,torch.compile成了性能优化的主要入口。它的工作流程是:Dynamo捕获Python字节码生成FX图,AOTAutograd做前向和反向的图分解,Inductor把FX图 lowering 成Triton代码或者C++代码。
对于非CUDA设备,你有两个选择:
- 实现一个Inductor后端:继承
torch._inductor.codegen.common.CodeGen,实现自己的调度和代码生成逻辑。这条路工作量大,但性能上限高。 - 走Triton兼容路线:如果你的硬件能跑Triton生成的代码(或者你能把Triton IR翻译成自己的指令),那就可以复用Inductor的大部分流程。
寒武纪走的是哪条路,公开资料里没有明确说。但从它之前发布的torch_mlu更新日志来看,torch.compile的支持是逐步推进的,早期版本需要设置torch._dynamo.config.suppress_errors = True来跳过不支持的图。
4. 实操:从零搭建PyTorch环境并验证芯片适配
4.1 环境准备:Anaconda与Python版本选择
不管你用的是CUDA设备还是国产芯片,Anaconda都是管理Python环境最省心的方式。我个人的习惯是每个项目一个独立环境,避免依赖冲突。
# 创建环境,Python版本建议3.9或3.10 conda create -n pytorch_mlu python=3.10 conda activate pytorch_mlu # 安装PyTorch基础包 # 注意:如果你的芯片厂商提供了定制版PyTorch,要用他们的源 pip install torch torchvision torchaudio这里有个关键点:PyTorch版本和芯片驱动版本的匹配。CUDA生态里,PyTorch 2.0需要CUDA 11.7或11.8,PyTorch 2.1开始支持CUDA 12.1。国产芯片也有类似的版本对应关系,装之前一定要看厂商的release note。
我踩过的一个坑是:用conda装PyTorch时,conda会自动装一个它认为兼容的CUDA runtime,但这个runtime可能和你系统里的驱动版本不匹配。后来我改成用pip装,并且明确指定--index-url指向厂商的包源,问题就少了。
4.2 验证设备可用性与基本算子
环境装好之后,第一件事是验证设备能不能被PyTorch识别:
import torch # 检查设备是否可用 print(torch.mlu.is_available()) # 如果是寒武纪 print(torch.mlu.device_count()) print(torch.mlu.get_device_name(0)) # 创建一个张量并移动到设备上 x = torch.randn(3, 3) x_mlu = x.mlu() print(x_mlu.device) # 跑一个简单的矩阵乘法 a = torch.randn(1024, 1024).mlu() b = torch.randn(1024, 1024).mlu() c = torch.mm(a, b) print(c.sum())如果这几步都能跑通,说明基础的算子注册和内存管理没问题。接下来要测的是算子覆盖度。我的做法是拿一个真实的模型,比如ResNet-50,跑一遍前向和反向,看哪些算子会报“未实现”。
import torchvision.models as models model = models.resnet50().mlu() x = torch.randn(32, 3, 224, 224).mlu() y = model(x) loss = y.sum() loss.backward() print("ResNet-50 forward/backward OK")如果这一步报错,错误信息通常会告诉你缺哪个算子。比如aten::adaptive_avg_pool2d没实现,你就需要去补这个算子的kernel。
4.3 性能对比:别只看“能跑”
“能跑”和“跑得快”是两回事。我见过一些适配方案,功能测试全过,但训练速度只有CUDA版本的十分之一。问题通常出在几个地方:
- 算子实现没有用上硬件的向量化指令:比如矩阵乘法,如果只是用for循环在CPU上算完再拷贝回设备,那速度肯定不行。
- 内存拷贝太频繁:每次算子调用都做一次host-device同步,会把流水线打断。
- 没有做算子融合:PyTorch Eager模式下,
conv + bn + relu是三个独立的kernel调用。CUDA生态里有cuDNN做融合,非CUDA设备如果没做类似的优化,性能差距会很大。
我一般会用torch.profiler来看每个算子的耗时:
with torch.profiler.profile( activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.MLU], schedule=torch.profiler.schedule(wait=1, warmup=1, active=3), on_trace_ready=torch.profiler.tensorboard_trace_handler('./log') ) as prof: for step, data in enumerate(dataloader): if step >= 5: break train_step(model, data) prof.step()看trace的时候重点关注两件事:一是设备上的kernel执行时间占比,二是host和device之间的同步点。如果同步点太多,说明你的后端在算子调度上还有优化空间。
4.4 分布式训练接入的注意事项
单卡跑通之后,下一步通常是多卡分布式。PyTorch的分布式接口主要有两种:DistributedDataParallel(DDP)和FullyShardedDataParallel(FSDP)。它们底层都依赖通信库来做梯度同步。
CUDA生态里用NCCL,非CUDA设备要么实现一套兼容NCCL API的通信库,要么用Gloo。Gloo的问题是它主要针对CPU优化,在设备间通信时性能损失比较大。我实测过一个4卡训练的任务,用Gloo的吞吐量只有NCCL的60%左右。
如果你的芯片厂商提供了自己的通信库,接入方式通常是实现torch.distributed.ProcessGroup的子类,然后通过init_process_group的backend参数指定。这里要注意的是通信和计算的重叠。DDP默认会在反向传播的同时做梯度allreduce,如果你的通信库不支持异步操作,这个重叠就做不起来,训练速度会明显下降。
5. 常见问题与排查技巧实录
5.1 算子未实现报错怎么定位
最常见的报错长这样:
RuntimeError: Could not run 'aten::xxx' with arguments from the 'MLU' backend.排查步骤:
- 确认这个算子在CUDA后端有没有实现。如果CUDA也没有,那可能是PyTorch版本的问题。
- 检查你的
TORCH_LIBRARY_IMPL注册代码,看算子名和变体是否写对了。PyTorch的算子名是大小写敏感的,add.Tensor和add.tensor不一样。 - 如果算子是通过组合实现的(Composite),检查依赖的子算子是否都已实现。
我整理了一个速查表:
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
Could not run 'aten::xxx' | 算子未注册 | 实现并注册对应kernel |
Expected all tensors to be on the same device | 张量设备不一致 | 检查.to(device)调用 |
MLU out of memory | 显存不足 | 减小batch size或优化内存池 |
NCCL error/Gloo error | 通信库配置问题 | 检查环境变量和网络配置 |
dtype not supported | 数据类型不支持 | 转换到支持的dtype或实现该dtype的kernel |
5.2 环境配置的坑:驱动、CUDA、PyTorch三者关系
虽然这里聊的是国产芯片,但很多人是在CUDA环境里做开发,然后迁移到国产芯片上。CUDA环境本身就有不少坑,我顺带说一下。
驱动版本和CUDA版本的关系:NVIDIA驱动是向下兼容CUDA的,但有一个最低版本要求。比如CUDA 12.1需要驱动版本>=530。你可以用nvidia-smi看驱动版本,用nvcc --version看CUDA版本。如果nvcc显示的版本和PyTorch编译时用的CUDA版本不一致,可能会出现运行时错误。
PyTorch和CUDA的对应关系:PyTorch官网的安装命令里会明确写cu118、cu121这样的后缀。如果你用conda install pytorch而不指定,conda可能会装一个CPU版本,或者装一个和你驱动不匹配的CUDA版本。
Anaconda环境隔离:我强烈建议用conda创建独立环境,不要在base环境里装PyTorch。因为不同项目可能依赖不同版本的PyTorch,混在一起迟早出问题。
5.3 性能调优的独家经验
做了几年框架适配,我总结了几条性能调优的经验,有些是文档里不会写的:
第一条:先看数据加载,再看模型计算。很多人一上来就优化算子,结果发现瓶颈在DataLoader上。用torch.utils.data.DataLoader的时候,num_workers设成CPU核心数的一半到三分之二比较合适,pin_memory=True在CUDA环境下能加速host到device的拷贝,但在非CUDA设备上不一定有效,要实测。
第二条:batch size不是越大越好。大batch能提高硬件利用率,但也会增加显存压力和通信开销。我一般会做一个batch size的扫描,从16开始翻倍,看吞吐量的变化曲线,找到拐点。
第三条:混合精度训练要谨慎。AMP(自动混合精度)在CUDA上很成熟,但在非CUDA设备上,FP16的算子覆盖度可能不够。如果发现loss变成NaN,先检查是不是某个算子在FP16下溢出了。
第四条:算子融合是最大的性能杠杆。如果你们的芯片支持自定义算子融合,一定要把conv+bn+relu、linear+gelu这些常见pattern做进去。我见过一个案例,光是融合了这几个pattern,训练速度就提升了40%。
5.4 从CUDA迁移到国产芯片的代码改动清单
如果你有一个现成的CUDA项目要迁移到国产芯片,需要改的地方其实不多,但每一处都要仔细:
# 1. 设备指定 # 原来 device = torch.device("cuda:0") # 改成 device = torch.device("mlu:0") # 或厂商指定的设备名 # 2. 张量移动 # 原来 x = x.cuda() # 改成 x = x.mlu() # 3. 分布式后端 # 原来 dist.init_process_group(backend="nccl") # 改成 dist.init_process_group(backend="cncl") # 或厂商提供的后端名 # 4. 随机种子 # 原来 torch.cuda.manual_seed(42) # 改成 torch.mlu.manual_seed(42) # 5. 性能分析 # 原来 with torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.CUDA]): # 改成 with torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.MLU]):看起来简单,但实际迁移时最容易出问题的是第三方库的依赖。比如apex、deepspeed、flash-attention这些库,它们内部有大量CUDA-specific的代码。如果厂商没有提供对应的移植版本,你可能需要自己改。
6. 这件事对行业意味着什么
6.1 多后端生态的必然趋势
PyTorch基金会接纳寒武纪,本质上反映了一个趋势:深度学习框架正在从“CUDA中心化”走向“多后端并行”。这个趋势不是PyTorch一家的事,TensorFlow有tf.device的插件机制,JAX有PJRT(Portable JAX Runtime),大家都在做类似的事情。
对开发者来说,这意味着以后写代码时,设备相关的部分会越来越抽象。你可能不需要写x.cuda(),而是写x.to(device),然后通过配置来决定用哪个后端。这对代码的可移植性是好事,但也要求你对不同后端的特性有基本了解,不然性能调优会无从下手。
6.2 国产芯片软件栈的短板与机会
说实话,国产AI芯片在硬件参数上追得很快,但在软件栈上普遍落后。这个落后不是“能不能跑”的问题,而是“好不好用”的问题。具体表现在:
- 文档质量参差不齐:很多厂商的文档只告诉你“怎么装”,不告诉你“为什么这么装”,出了问题只能提工单。
- 社区支持薄弱:CUDA生态里有Stack Overflow、有GitHub上成千上万的issue,国产芯片的社区还在建设中。
- 工具链不完整:性能分析工具、调试工具、可视化工具,这些CUDA生态里习以为常的东西,在国产芯片上往往缺失。
寒武纪进PyTorch理事会,至少说明它在软件栈上的投入得到了社区认可。这对整个国产芯片行业是一个正向信号:软件生态的建设,开始被放到和硬件同等重要的位置。
6.3 给开发者的建议:现在该做什么
如果你是一个深度学习开发者,不管你现在用的是CUDA还是国产芯片,我有几个建议:
第一,不要把设备相关的代码写死。用device = torch.device(...)这样的方式,而不是到处写.cuda()。这样以后迁移的时候,改一个地方就行。
第二,关注PyTorch的RFC(Request for Comments)。PyTorch的重大变更都会先发RFC,比如PrivateUse1机制、torch.compile的后端接口,都是在RFC阶段就公开讨论的。提前了解这些,能让你在适配时少走弯路。
第三,动手试。如果你手边有国产芯片的开发板或者云上的实例,花一个下午把PyTorch环境搭起来,跑一个简单的模型。很多问题只有亲手做了才会遇到,看文档是看不出来的。
第四,参与社区。PyTorch的GitHub issue和论坛里,关于非CUDA后端的讨论越来越多。你遇到的问题,很可能别人也遇到过。把你的解决方案分享出来,既帮了别人,也让自己对问题的理解更深一层。
7. 一个具体的算子适配案例:从报错到跑通
7.1 问题现场:adaptive_avg_pool2d未实现
我之前帮一个团队做模型迁移,模型里用了nn.AdaptiveAvgPool2d((1, 1)),在CUDA上跑得好好的,换到某国产芯片上就报错:
RuntimeError: Could not run 'aten::adaptive_avg_pool2d' with arguments from the 'XXX' backend.查了一下,这个算子在PyTorch里的实现是CompositeExplicitAutograd,也就是说它本身不直接对应硬件指令,而是通过组合其他算子实现的。理论上,如果基础算子都实现了,这个算子应该能自动工作。但报错说明,要么是组合路径上的某个基础算子没实现,要么是自动微分部分出了问题。
7.2 排查过程:逐层分解
我的排查思路是这样的:
第一步,确认adaptive_avg_pool2d在CUDA后端的实现方式。翻PyTorch源码,发现它最终调用的是adaptive_avg_pool2d_out_cuda,里面用了at::native::adaptive_avg_pool2d这个函数。
第二步,检查这个函数依赖哪些基础算子。主要是mean、view、unsqueeze这几个。写一个最小复现脚本:
import torch x = torch.randn(1, 64, 7, 7).mlu() # 手动模拟adaptive_avg_pool2d y = x.mean(dim=[2, 3], keepdim=True) print(y.shape) # 应该是 (1, 64, 1, 1)如果这一步报错,说明mean算子有问题。如果这一步能过,那问题出在自动微分或者算子注册上。
第三步,检查自动微分。adaptive_avg_pool2d的反向传播需要adaptive_avg_pool2d_backward,这个算子在CUDA后端是单独实现的。如果国产芯片的后端没有实现这个反向算子,那前向能跑,反向就会报错。
7.3 解决方案:注册复合算子
确认问题之后,解决方案有两种:
方案一:实现缺失的基础算子。如果mean没实现,那就补mean的kernel。这是最彻底的做法,但工作量大。
方案二:注册复合算子。在TORCH_LIBRARY_IMPL里,把adaptive_avg_pool2d注册为一个CompositeImplicitAutograd算子,让PyTorch自动用基础算子组合出前向和反向。
TORCH_LIBRARY_IMPL(aten, PrivateUse1, m) { m.impl("adaptive_avg_pool2d", TORCH_FN(at::native::adaptive_avg_pool2d)); m.impl("adaptive_avg_pool2d_backward", TORCH_FN(at::native::adaptive_avg_pool2d_backward)); }这里的关键是,at::native::adaptive_avg_pool2d这个函数本身是设备无关的,它内部会调用mean等基础算子。只要基础算子在你的设备上实现了,这个复合算子就能工作。
7.4 验证与性能测试
改完之后,重新跑模型:
model = MyModel().mlu() x = torch.randn(16, 3, 224, 224).mlu() y = model(x) loss = y.sum() loss.backward() print("Forward and backward OK")跑通之后,用profiler看一下这个算子的耗时。如果发现adaptive_avg_pool2d的耗时占比很高,那可能需要进一步优化,比如针对(1, 1)这种输出尺寸做特化实现。
这个案例的通用经验是:遇到算子未实现,先查PyTorch源码看它是怎么实现的,再决定是补基础算子还是注册复合算子。不要一上来就写kernel,很多时候组合现有算子就能解决问题。
8. 关于PyTorch版本选择的一些个人建议
8.1 稳定版还是Nightly版
PyTorch的发布节奏是每季度一个稳定版,中间有nightly版。对于生产环境,我强烈建议用稳定版。nightly版虽然能提前用到新特性,但API变动频繁,而且可能有未修复的bug。
对于芯片适配来说,稳定版还有一个好处:厂商的适配通常是跟着稳定版走的。你用nightly版,可能遇到厂商还没适配的API变更。
8.2 从哪个版本开始支持PrivateUse1
PrivateUse1机制是PyTorch 1.13正式引入的。如果你用的芯片厂商的适配是基于更早的版本(比如1.12),那它可能用的是更老的扩展机制,比如torch.utils.cpp_extension或者直接改PyTorch源码。后者的维护成本很高,每次PyTorch升级都要重新打patch。
所以,如果你在选择芯片方案,可以问一下厂商:你们的PyTorch适配是基于哪个版本?用的是PrivateUse1还是改源码?这个问题的答案,很大程度上反映了厂商软件栈的成熟度。
8.3 长期支持版本的考量
PyTorch基金会从2.0开始,对每个大版本提供一定的长期支持。但说实话,PyTorch的LTS策略不如Ubuntu那么明确。我的建议是:如果你的项目周期比较长,选一个社区活跃、厂商适配跟得紧的版本,然后锁定这个版本,不要频繁升级。
我自己的项目里,PyTorch版本是写在requirements.txt里的,精确到小版本号。升级之前,一定会在测试环境里跑一遍完整的回归测试。
9. 写在最后:一些零散但有用的经验
做框架适配这几年,我最大的体会是:软件栈的成熟度,比硬件参数更能决定一个芯片好不好用。一个算力很强的芯片,如果PyTorch适配做得稀烂,开发者用起来会非常痛苦。反过来,一个算力中等的芯片,如果软件栈做得好,能覆盖大部分常用模型,那它的实际可用性反而更高。
寒武纪进PyTorch理事会,是一个积极的信号,但也是一个起点。进了理事会不等于所有问题都解决了,后面还有大量的工程工作要做。对开发者来说,保持关注、动手尝试、反馈问题,是对这个生态最好的支持。
最后分享一个小技巧:如果你在适配过程中遇到了PyTorch的bug,或者觉得某个API设计不合理,可以在PyTorch的GitHub上提issue。提issue的时候,附上一个最小复现脚本,说明你的设备类型和PyTorch版本。我提过几个关于PrivateUse1的issue,社区的响应速度比我想象的要快。参与开源社区,其实没有想象中那么遥不可及。