1. 为什么要在K230上折腾AI部署
第一次拿到K230开发板的时候,我脑子里想的其实很简单:这玩意儿带NPU,算力标称6TOPS,功耗又低,能不能把我自己训练的一个小模型塞进去跑起来?结果从模型训练到真正在板子上跑通,中间踩的坑比我想象的多得多。这篇文章就是把我整个流程复盘一遍,从数据集准备、模型训练、量化转换,到最终在K230上部署推理,每一步都尽量写清楚为什么这么做、当时遇到了什么问题、怎么解决的。
K230是嘉楠科技推出的一款边缘计算芯片,核心卖点就是内置了NPU(神经网络处理单元),专门用来加速神经网络推理。它跟纯CPU跑推理完全不是一个概念——CPU跑一个ResNet34可能一帧要几百毫秒甚至更久,而NPU可以把同样的模型压到几十毫秒甚至更低。这个差距在边缘设备上非常关键,因为边缘设备通常功耗受限、散热受限,不可能靠堆CPU核心来换性能。
那什么人适合看这篇内容?如果你手头有K230开发板,想把自己训练的模型部署上去;或者你在做端侧AI硬件部署的选型,想了解从训练到部署的完整链路;又或者你只是好奇NPU到底怎么用起来,那这篇应该都能给你一些参考。我默认你有基本的Python和深度学习基础,但不需要你之前接触过K230或者任何NPU相关的开发。
整个流程我分成四大块:模型训练、模型转换与量化、K230环境搭建与部署、以及实际运行中的问题排查。每一块我都会把关键决策点的理由讲清楚,因为很多教程只告诉你“这么做”,但不告诉你“为什么这么做”,导致换个模型就不知道怎么改了。
2. 模型训练:从数据集到可用的ResNet34
2.1 为什么选ResNet34而不是更大的模型
在边缘设备上部署模型,第一原则是“模型够用就好,不要贪大”。我一开始想的是用ResNet50甚至更大的模型,但后来算了一下K230的NPU算力和内存限制,果断退回到ResNet34。ResNet34的参数量大约是21M,浮点模型大小在80MB左右,经过量化后可以压到20MB以内,这对K230的内存来说比较友好。
另外,ResNet34的结构比较规整,没有太多特殊算子,这对NPU的算子支持比较友好。如果你用一个包含大量自定义算子或者动态shape的模型,NPU很可能不支持,最后只能回退到CPU跑,那就失去意义了。所以选模型的时候,除了看精度,还要看它的算子是否“常规”。
我这次的任务是一个图像分类任务,类别数不多,大概10类左右。数据集是我自己采集的,每类大概500张图片,总共5000张左右。这个数据量不算大,所以训练的时候用了比较强的数据增强,包括随机裁剪、翻转、颜色抖动等。
2.2 训练环境的搭建与关键参数
训练我是在一台带GPU的机器上做的,用的是PyTorch。这里有个坑要注意:K230的NPU工具链对PyTorch版本和算子版本有一定要求,不是随便什么版本都能顺利转换。我建议用PyTorch 1.12或者1.13,太新的版本可能在转换时遇到不支持的算子。
训练脚本本身没什么特别的,标准的ResNet34训练流程。但有几个参数我调了比较久:
- 学习率:用了余弦退火,初始学习率0.01,因为数据集不大,太大会震荡。
- Batch size:32,再大显存不够,再小训练不稳定。
- Epoch:大概跑了80个epoch,因为数据量小,容易过拟合,所以加了早停。
- 权重衰减:1e-4,防止过拟合。
训练完之后,验证集精度大概在92%左右。这个精度对于实际应用来说够用了,但我知道量化之后精度会掉一些,所以留了一些余量。
注意:训练的时候最好把模型保存成ONNX格式,因为K230的工具链通常是从ONNX开始转换的。PyTorch直接转ONNX的时候要注意opset版本,我用的opset 11,比较稳。
2.3 导出ONNX时的常见坑
导出ONNX这一步看起来简单,但实际上很容易出问题。我遇到的主要有这几个:
第一个是动态shape的问题。训练的时候如果用了动态输入尺寸,导出ONNX时可能会带上动态维度,而K230的NPU通常要求固定输入尺寸。所以导出的时候要指定固定的输入尺寸,比如1x3x224x224。
第二个是算子不支持。有些PyTorch算子转ONNX之后会变成自定义算子,NPU不认。我建议导出之后用ONNX Runtime跑一下,确认推理结果和PyTorch一致,然后再看ONNX的算子列表,确认没有奇怪的算子。
第三个是预处理和后处理的处理。很多人会把预处理(比如归一化)也放进模型里,但这样会增加NPU的负担,而且有些预处理算子NPU不一定支持。我的做法是把预处理放在CPU上做,模型只负责纯推理部分。
导出ONNX的代码大概长这样:
import torch import torch.onnx model = ResNet34(num_classes=10) model.load_state_dict(torch.load('best.pth')) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, 'resnet34.onnx', opset_version=11, input_names=['input'], output_names=['output'], dynamic_axes=None )导出之后,我建议用Netron打开ONNX文件看一眼,确认输入输出和算子都正常。
3. 模型转换与量化:让模型适配NPU
3.1 K230的工具链长什么样
K230的模型转换工具链主要是基于嘉楠自己的NNCase编译器。NNCase负责把ONNX模型转换成K230 NPU能执行的kmodel格式。这个过程包括算子映射、量化、内存分配等步骤。
NNCase的安装我就不细说了,官方文档有。但要注意版本匹配:NNCase的版本要和K230的固件版本对应,不然可能出现转换成功但板子上跑不了的情况。我用的NNCase版本是1.x,具体版本号建议去官方社区确认。
转换的基本流程是:ONNX -> NNCase IR -> 量化 -> kmodel。其中量化是最关键的一步,直接决定精度和性能。
3.2 量化到底在做什么
量化简单说就是把浮点权重和激活值用低比特整数表示,通常是int8。这样做的好处是模型体积缩小4倍,推理速度提升,功耗降低。但代价是精度会掉。
K230的NPU支持int8量化,也支持混合量化(部分层int8,部分层int16)。我这次用的是全int8量化,因为ResNet34对这种量化比较鲁棒。
量化需要一个校准数据集,用来统计激活值的分布,确定量化参数(scale和zero point)。校准数据集不需要标签,只需要输入图片,通常从训练集里随机抽几百张就够了。我用了大概200张。
注意:校准数据集的质量很重要。如果校准集和实际推理时的数据分布差异大,量化后的精度会掉得很厉害。所以校准集最好能覆盖实际场景的各种情况。
3.3 转换脚本与参数说明
NNCase的转换脚本大概长这样:
import nncase # 加载ONNX模型 with open('resnet34.onnx', 'rb') as f: model_content = f.read() # 编译配置 compile_options = nncase.CompileOptions() compile_options.target = 'k230' compile_options.input_shape = [1, 3, 224, 224] compile_options.input_type = 'uint8' compile_options.input_range = [0, 255] compile_options.mean = [0.485, 0.456, 0.406] compile_options.std = [0.229, 0.224, 0.225] compile_options.quant_type = 'uint8' # 量化配置 ptq_options = nncase.PTQTensorOptions() ptq_options.samples_count = 200 ptq_options.set_tensor_data(calib_data) # 编译 compiler = nncase.Compiler(compile_options) compiler.import_onnx(model_content) compiler.use_ptq(ptq_options) compiler.compile() kmodel = compiler.gencode_tobytes() with open('resnet34.kmodel', 'wb') as f: f.write(kmodel)这里有几个参数需要解释:
- input_type和input_range:指定输入数据的类型和范围。如果输入是uint8的图片,范围就是0-255。
- mean和std:归一化参数。这里有个坑:如果你在训练时用了归一化,这里也要对应设置,但要注意NNCase的归一化是在NPU内部做的,所以你的输入应该是原始图片数据,不需要在CPU上再做归一化。
- quant_type:量化类型,uint8或者int8。K230通常用uint8。
转换完成后,你会得到一个kmodel文件,这个就是最终要放到板子上跑的文件。
3.4 量化精度掉了怎么办
我第一次转换完之后,在PC上模拟跑了一下,发现精度从92%掉到了85%左右。这个掉得有点多,所以我做了一些调整:
第一个是增加校准样本数量,从200增加到500,精度回升到88%左右。
第二个是调整量化算法,NNCase支持不同的量化校准方法,比如KLD、MSE等。我试了MSE,比默认的好一些。
第三个是对某些敏感层使用混合量化。ResNet34的第一层和最后一层对量化比较敏感,我把这两层设成int16,其他层保持int8,精度回到了90%左右。
如果这些都不行,那就只能考虑量化感知训练(QAT),在训练时就模拟量化过程,让模型适应量化误差。但QAT比较麻烦,我这次没用到。
4. K230环境搭建与模型部署
4.1 固件烧录与系统启动
K230开发板拿到手之后,第一件事是烧录固件。官方提供了固件包和烧录工具,通常是通过USB线连接板子和PC,然后用烧录工具把固件写到板子的存储里。
烧录的时候要注意几点:
- 固件版本要和NNCase版本匹配,不然kmodel可能跑不了。
- 烧录前要确保板子进入烧录模式,通常是按住某个按键再上电。
- 烧录完成后要重新上电,让系统正常启动。
系统启动后,你可以通过串口或者USB网络连接到板子。我习惯用串口,因为最稳定。串口通信的波特率通常是115200,用minicom或者PuTTY都可以。
4.2 把kmodel传到板子上
板子上的系统通常是一个精简的Linux,有基本的文件系统。你可以通过scp或者U盘把kmodel文件传上去。我一般用scp,因为方便:
scp resnet34.kmodel root@192.168.1.100:/root/传上去之后,你需要一个推理程序来加载kmodel并执行推理。K230的SDK里通常有示例代码,你可以基于示例改。
4.3 推理程序的编写
推理程序的核心是调用K230的运行时API,加载kmodel,输入数据,获取输出。大概流程是:
- 初始化运行时
- 加载kmodel
- 设置输入数据
- 执行推理
- 获取输出
这里有个关键点:输入数据的预处理。前面说了,归一化是在NPU内部做的,所以你在CPU上只需要把图片resize到模型输入尺寸,然后转换成uint8数组就行。
推理程序的代码大概长这样(基于官方示例改的):
from k230_runtime import Runtime rt = Runtime() rt.load_model('resnet34.kmodel') # 读取图片并resize img = preprocess('test.jpg', (224, 224)) # 设置输入 rt.set_input(0, img) # 执行推理 rt.run() # 获取输出 output = rt.get_output(0) pred = output.argmax() print('Predicted class:', pred)实际代码会比这个复杂一些,因为要处理内存分配、多线程等,但核心逻辑就是这样。
4.4 性能实测与对比
部署完成之后,我测了一下推理速度。单帧推理时间大概在15ms左右,也就是大约60fps。这个速度对于很多实时应用来说已经够了。
对比一下,如果我用CPU跑同样的模型,单帧大概要200ms以上,差距非常明显。而且NPU的功耗比CPU低很多,这对边缘设备来说很重要。
不过要注意,这个15ms是纯推理时间,不包括预处理和后处理。如果预处理比较重,整体延迟会增加。所以实际应用中要优化整个pipeline,不能只看推理时间。
5. 常见问题与排查技巧实录
5.1 模型转换失败怎么办
转换失败是最常见的问题,原因通常有几个:
- ONNX算子不支持:用Netron看一下ONNX的算子列表,如果有一些奇怪的算子,尝试用ONNX Simplifier简化一下,或者替换成支持的算子。
- 输入shape不匹配:确认ONNX的输入shape和转换配置里的input_shape一致。
- NNCase版本不匹配:确认NNCase版本和固件版本对应。
我遇到过一次转换失败,最后发现是ONNX里有一个Resize算子,NNCase不支持。解决办法是把Resize操作从模型里拿出来,放到CPU上做。
5.2 板子上跑推理报错
板子上跑推理报错,常见的有:
- kmodel加载失败:通常是kmodel文件损坏或者版本不匹配。重新转换一次,确认版本对应。
- 内存不足:K230的内存有限,如果模型太大或者输入尺寸太大,可能内存不够。尝试减小输入尺寸或者用更小的模型。
- 输入数据格式不对:确认输入数据的类型和范围与转换配置一致。
5.3 精度不达预期
精度问题通常出在量化上。排查思路是:
- 先在PC上用NNCase的模拟器跑一下量化后的模型,看精度掉了多少。
- 如果掉得不多(1-2%),可能是正常的量化误差。
- 如果掉得很多,检查校准数据集是否覆盖了实际场景。
- 尝试增加校准样本、调整量化算法、或者对敏感层用混合量化。
5.4 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 转换失败 | 算子不支持 | 简化ONNX或替换算子 |
| 转换失败 | shape不匹配 | 检查input_shape配置 |
| 加载kmodel失败 | 版本不匹配 | 确认NNCase和固件版本 |
| 推理报错 | 内存不足 | 减小模型或输入尺寸 |
| 精度掉太多 | 量化误差大 | 增加校准样本或混合量化 |
| 推理速度慢 | 回退到CPU | 检查算子是否被NPU支持 |
5.5 一些实操心得
最后分享几个我在实际操作中总结的小技巧:
第一个是先用小模型跑通流程。不要一上来就用大模型,先用一个简单的模型(比如MobileNet)把整个流程跑通,确认环境没问题,再换大模型。这样可以快速定位问题是出在流程上还是模型上。
第二个是保留浮点模型作为对照。在PC上跑浮点模型的结果要保存下来,部署到板子上之后,用同样的输入对比输出,确认精度差异。
第三个是注意温度。K230在长时间高负载运行时会发热,温度过高可能会降频。如果做长时间推理,最好加个散热片。
第四个是串口通信的稳定性。如果用串口调试,注意波特率和流控设置,不然可能丢数据。我一般用115200波特率,不开流控。
整个流程走下来,从模型训练到K230上跑通,大概花了我一周多的时间,其中大部分时间花在量化调优和问题排查上。但跑通之后,看到模型在板子上实时推理的效果,还是很有成就感的。如果你也在做类似的事情,希望这篇内容能帮你少踩一些坑。