CSK6 LNN系列教程
本系列教程主要介绍在CSK6板卡上进行AI模型部署。预计分为以下几个部分:
- LNN部署及示例工程仿真 (申请开发板必看)
- CSK6开发板实机运行模型推理程序
- CSK6开发板调用摄像头进行实时模型运行
- CSK6开发板进阶AI应用开发
本文为第一篇,介绍 LNN 环境的安装并以pytorch-cifar100数据集/resnet18模型为例进行训练打包出模型,最终使用仿真工具完成推理执行。
用户可以参照本教程掌握LNN工具链的使用,尝试将自己的模型进行转换导出,并使用thinker进行运行推理的仿真输出。 鉴于CSK6 NPU的模型限制与算子列表的限制,建议用户采用开源的小模型(如squeezenet、mobilenet、mobilenetv2、resnet18 等)
LNN 环境安装
LNN 是聆思科技AI算法适配工具合集,当前包含量化训练组件linger、推理引擎组件thinker。
0.LNN工作流程简述
LNN工具链主要功能包括:
1.规约性检查:检查当前模型结构是否和底层硬件适配,跑通2~4步无报错即证明通过检查,如有报错,则需要调整模型结构。为节约时间,第2步可进行少量训练能导出模型结构即可,无须等到模型收敛;
2.浮点-定点两阶段训练:先进行浮点网络的约束训练,再针对量化友好的浮点模型进行量化训练微调;
3.导图:将量化无损的pytorch模型导出为ONNX格式的计算图;
4.模型分析和打包:利用thinker的离线工具tpacker对生成的ONNX图进行打包,得到序列化后的文件;
5.仿真推理执行:使用示例工程test_thinker,指定输入数据、资源文件和输出文件路径,运行模拟代码,即可看到模型推理结果。
1.配置运行环境
本小结将讲解如何使用源码安装的方式进行环境的配置和安装。(其它安装方式可参考GitHub或聆思文档中心说明)。
请确保本机环境满足以下版本要求:
- gcc/5.4-os7 及以上
- cmake/3.17.3 及以上
- cuda/10.2-cudnn-7.6.5(不支持 Ampere 及更新架构的NVIDIA显卡)
(1)linger环境配置及安装
创建虚拟环境
# 使用conda创建虚拟环境,搭建linger运行环境,指定python版本3.7.0
conda create -n linger-env python==3.7.0
# 激活虚拟环境
conda activate linger-env
源码安装
# 安装linger的源码。安装完成之后进入linger目录下,执行安装脚本:
git clone https://github.com/LISTENAI/linger.git
cd linger && sh install.sh
根据requirements.txt文件安装所有依赖项
# 完成依赖包安装,若无法执行,则手动安装requirements中的各种依赖项
pip install -U pip
cat requirements.txt | xargs -n 1 pip install
安装验证
python
Python 3.7.0 (default, Jul 8 2020, 22:11:17)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>import linger
# 无报错提醒,则说明linger已成功被安装
(2)thinker环境配置及安装
创建虚拟环境
# 使用conda创建虚拟环境,搭建thinker的运行环境,指定python版本3.8.5
conda create -n thinker-env python==3.8.5
# 激活虚拟环境
conda activate thinker-env
源码安装
# 安装thinker的源码。安装完成之后进入thinker目录下,执行安装脚本:
git clone https://github.com/LISTENAI/thinker.git
进入thinker的目录下,对thinker进行编译。
在编译之前需要先修改script/x86_linux.sh
和test/auto_test.sh
脚本中的CMAKE的路径,修改为anaconda/bin
所在的目录。修改完CMAKE路径后,执行脚本进行编译:
cd thinker
sh ./scripts/x86_linux.sh
根据requirements.txt文件安装所有依赖项
# 完成依赖包安装,若无法执行,则手动安装requirements中的各种依赖项
pip install -U pip
cat requirements.txt | xargs -n 1 pip install
安装验证
python
Python 3.8.5 (default, Sep 4 2020, 07:30:14)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import thinker
# 无报错提醒,则说明thinker已被成功安装
训练与仿真
下面将基于pytorch-cifar100数据集和resnet18模型展示工作流程,用户可参照此流程,对其他模型进行训练与导出。
pytorch-cifar100 项目链接:https://github.com/weiaicunza...
你可以先使用git clone
指令将该项目clone至本地。
浮点训练与量化训练
(1)浮点训练
首先确保在当前环境下,浮点模型训练基于pytorch能够跑起来,在 pytorch-cifar100 项目目录下,执行以下指令:
# shell终端
conda activate linger-env
python train.py -net resnet18 -gpu
执行上述指令后,会出现缺少tensorboard
的报错,如下:
Traceback (most recent call last):
File "train.py", line 23, in <module>
……
import tensorboard
ModuleNotFoundError: No module named 'tensorboard'
由于tensorboard
和 linger 有冲突,即使这里安装了tensorboard
,后续进行浮点训练和量化训练时也要注释掉tensorboard
的相关代码,因此,我们先注释掉 train.py 文件中的tensorboard
相关代码(后面标注了“#tensorboard注释”的代码)
第一部分tensorboard注释(第23行):
#from torch.utils.tensorboard import SummaryWriter #tensorboard注释
第二部分tensorboard注释(第48~53行左右)
last_layer = list(net.children())[-1]
# for name, para in last_layer.named_parameters():#tensorboard注释
# if 'weight' in name: #tensorboard注释
# writer.add_scalar('LastLayerGradients/grad_norm2_weights', para.grad.norm(), n_iter) #tensorboard注释
# if 'bias' in name: #tensorboard注释
# writer.add_scalar('LastLayerGradients/grad_norm2_bias', para.grad.norm(), n_iter) #tensorboard注释
第三部分tensorboard注释(第62~72行左右)
#update training loss for each iteration
# writer.add_scalar('Train/loss', loss.item(), n_iter) #tensorboard注释
if epoch <= args.warm:
warmup_scheduler.step()
# for name, param in net.named_parameters():#tensorboard注释
# layer, attr = os.path.splitext(name) #tensorboard注释
# attr = attr[1:] #tensorboard注释
# writer.add_histogram("{}/{}".format(layer, attr), param, epoch) #tensorboard注释
第四部分tensorboard注释(第112~115行左右):
print()
#add informations to tensorboard
# if tb: #tensorboard注释
# writer.add_scalar('Test/Average loss', test_loss /len(cifar100_test_loader.dataset), epoch) #tensorboard注释
# writer.add_scalar('Test/Accuracy', correct.float() / len(cifar100_test_loader.dataset), epoch) #tensorboard注释
return correct.float() / len(cifar100_test_loader.dataset)))
第五部分tesnorboard注释(第170~177行左右):
# writer = SummaryWriter(log_dir=os.path.join(
# settings.LOG_DIR, args.net, settings.TIME_NOW)) #tensorboard注释
input_tensor = torch.Tensor(1, 3, 32, 32)
if args.gpu:
input_tensor = input_tensor.cuda()
# writer.add_graph(net, input_tensor) #tensorboard注释
第六部分tensorboard注释(最后一行):
# writer.close()#tensorboard注释
注释完成后,重复执行上述指令:
python train.py -net resnet18 -gpu
如能正常出现log信息,则证明能够正常训练,请先停止训练进程。
在train.py文件中的 loss_function = nn.CrossEntropyLoss()
这行代码之前添加少量代码,导入linger,增加约束条件:
# 导入linger
import linger
net=net.cuda()
dummy_input=torch.randn(8,3,32,32,requires_grad=True).cuda()#设置模型输入大小
train_mode ="clamp" #clamp:浮点训练阶段范围约束,quant:量化训练阶段
linger.trace_layers(net,net,dummy_input,fuse_bn=True)#net为初始模型结构,dummy_input为模型输入数据
"""linger.disable_normalize(net.fc)#设置不量化的层"""
normalize_modules=(nn.Conv2d,nn.Linear,nn.BatchNorm2d)#设置需要量化的层,可使用默认值
net=linger.normalize_layers(net,normalize_modules=normalize_modules,normalize_weight_value=8, normalize_bias_value=None,normalize_output_value=8)#模型量化参数设置
loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=args.lr, momentum=0.9, weight_decay=5e-4)
train_scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=settings.MILESTONES, gamma=0.2) #learning rate decay
iter_per_epoch = len(cifar100_training_loader)
warmup_scheduler = WarmUpLR(optimizer, iter_per_epoch * args.warm)
打开pytorch-cifar100/conf/global_settings.py
,将文件中最后一行代码的每十轮保存一个模型改为每一轮保存一个模型。
#save weights file per SAVE_EPOCH epoch
#SAVE_EPOCH = 10#原本是每十轮保存一个模型
SAVE_EPOCH = 1#为了加快进度,修改为每一轮保存一个模型
输入以下指令,检查训练能否正常进行:
python train.py -net resnet18 -gpu
运行几个epoch后,在pytorch-cifar100/checkpoint/resnet18
文件夹中生成了一个**.pth
文件,在规约性检查阶段,此时即可停止浮点训练,往下进行量化训练。
(2)量化训练
加载(torch.load(…)) (1)浮点训练 中的保存的浮点模型**.pth,
,按照如下方式修改约束代码,将浮点算子替换为量化算子。
# 导入linger
import linger
net=net.cuda()
dummy_input=torch.randn(8,3,32,32,requires_grad=True).cuda()#设置模型输入大小
train_mode ="quant" #clamp:浮点训练阶段范围约束,quant:量化训练阶段
linger.trace_layers(net,net,dummy_input,fuse_bn=True)#net为初始模型结构,dummy_input为模型输入数据
"""linger.disable_normalize(net.fc)#设置不量化的层"""
normalize_modules=(nn.Conv2d,nn.Linear,nn.BatchNorm2d)#设置需要量化的层,可使用默认值
replace_tulpe=(nn.Conv2d,nn.Linear, nn.BatchNorm2d, nn.AvgPool2d)#❤
net=linger.normalize_layers(net,normalize_modules=normalize_modules,normalize_weight_value=8, normalize_bias_value=None,normalize_output_value=8)#模型量化参数设置
net=linger.init(net, quant_modules=replace_tulpe,mode=linger.QuantMode.QValue)#❤
#加载浮点训练时生成的**.pth
net.load_state_dict(torch.load("/checkpoint/preactresnet18/Tuesday_25_April_2023_15h_54m_02s/resnet18-1-regular.pth"))#❤
执行同样的指令开启量化训练:
python train.py -net resnet18 -gpu
此阶段会对算子参数的合规性进行检查,如有不符合的设置,会出现报错,比如:
Files already downloaded and verified
……
line 334, in __init__
assert kernel_size_h >= stride_h and kernel_size_w >= stride_w, f"kernel_size_h >= stride_h and kernel_size_w >= stride_w, but you have {kernel_size_h} < {stride_h} or {kernel_size_w} < {stride_w}"
AssertionError: kernel_size_h >= stride_h and kernel_size_w >= stride_w, but you have 1 < 2 or 1 < 2
linger要求卷积核的尺寸大于相应方向的步长,这里出现了卷积核尺寸小于步长的情况,故而会报错。为了通过检查,需要对网络模型的结构进行修改,方式如下:
打开pytorch-cifar100/models/resnet.py
,在BasicBlock定义部分,修改不满足限制条件的算子(文件中的第35~41行左右)。
#shortcut
self.shortcut = nn.Sequential()
#the shortcut output dimension is not the same with residual function
#use 1*1 convolution to match the dimension
if stride != 1 or in_channels != BasicBlock.expansion * out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * BasicBlock.expansion, kernel_size=1, stride=stride, bias=False), #❤
nn.BatchNorm2d(out_channels * BasicBlock.expansion)
)
def forward(self, x):
return nn.ReLU(inplace=True)(self.residual_function(x) + self.shortcut(x))
修改上述代码中标注“#❤”的代码,将kernel_size改为3,并设置padding=1,如下:
nn.Conv2d(in_channels, out_channels * BasicBlock.expansion, stride=stride, kernel_size=3, padding=1, bias=False)
此外,此处也会检查linger不支持的算子,例如,目前linger不支持AdaptiveAvgpool2d算子,需要修改为当前支持的Avgpool2d算子:
#self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))#不支持的算子
self.avg_pool = nn.AvgPool2d(kernel_size=4)#修改为支持的算子
若不修改,则会在后续的打包时出现类似如下的报错:
……
line 66, in init_tensor
raise AttributeError(
AttributeError: not support Operator: "GlobalAveragePool" yet
其它规则限制,可参考模型限制与算子列表或者项目仓库中的相关说明:
https://github.com/LISTENAI/l...
https://github.com/LISTENAI/t...
修改完成之后,重新进行浮点训练、量化训练,直到无报错。训练几个epoch后,同样在pytorch-cifar100/checkpoint/resnet18
文件夹中生成了一个**.pth
文件。在规约性检查阶段,停止量化训练,使用工具导出计算图。
3.导出量化后模型文件
使用pt2onnx.py脚本,导出onnx计算图。
python pt2onnx.py
请在脚本的ch_path和onnx_path处填写相应的文件路径,然后执行上述转换onnx计算图的指令进行转换。
pt2onnx.py脚本源码:
#导入相关的包
import linger
import torch
import torch.nn as nn
from models.resnet import resnet18
# 定义函数main,传入检查点文件路径、onnx文件路径、模型网络
def main(checkpoint_path: str, onnx_path:str, net):
# 加载模型参数
net.load_state_dict(torch.load(checkpoint_path))
# 设置为评估模式
net.eval()
# 输入dummy数据,生成输出out
out = net(dummy_input)
# 使用torch.onnx.export将pytorch模型转化为onnx模型
with torch.no_grad():
torch.onnx.export(net,dummy_input,onnx_path,opset_version=11,
input_names=["input"],output_names=["output"])
if __name__=='__main__':
# 设置检查点文件路径、onnx文件路径
ch_path="/xxx/xxx.pth"
onnx_path ='/xxx/resnet18.onnx'
# 创建resnet18的实例
net = resnet18().cuda()
# 定义dummy输入数据
dummy_input=torch.randn(1,3,32,32,requires_grad=True).cuda()
# 定义replace_tuple,表示需要替换的模块类型,用于模型量化
replace_tuple =(nn.Conv2d,nn.Linear,nn.BatchNorm2d,nn.AvgPool2d)
# 使用linger.trace_layers获取模型的BN信息,并进行BN融合
linger.trace_layers(net,net,dummy_input,fuse_bn=True)
# 使用linger.init进行模型量化
net=linger.init(net,quant_modules=replace_tuple,mode=linger.QuantMode.QValue)
# 运行main函数,将模型从pytorch格式转化为onnx格式
main(ch_path,onnx_path,net)
4.模型分析及数据打包
切换到thinker-env环境:
conda deactivate
conda activate thinker-env
执行以下命令,使用thinker离线工具 tpacker 对步骤3中生成的 onnx 计算图打包,得到model.bin
文件。
tpacker -g net.onnx -d True -o model.bin
打包工具会对计算图进行图优化、模拟引擎执行以规划内存占用并将分析结果序列化到资源文件中。该部分会对算子的输入大小、整体内存的检查。
运行模型打包指令,我们会发现出现以下报错:
===================================================================================
****** load model:/data/user/hxzhang/mission3/pytorch-cifar100/resnet18.onnx ******
===================================================================================
****** graph optimizer ******
Traceback (most recent call last):
File "/data/user/hxzhang/anaconda3/envs/thinker-env/bin/tpacker", line 8, in <module>
sys.exit(main())
File "/data/user/hxzhang/anaconda3/envs/thinker-env/lib/python3.8/site-packages/thinker/tpacker.py", line 65, in main
graph = graph_optimizer(graph, args.strategy, args.dump)
File "/data/user/hxzhang/anaconda3/envs/thinker-env/lib/python3.8/site-packages/thinker/graph_optimizer/graph_optimizer.py", line 10, in graph_optimizer
graph.init_tensor()
File "/data/user/hxzhang/anaconda3/envs/thinker-env/lib/python3.8/site-packages/thinker/graph/Graph.py", line 66, in init_tensor
raise AttributeError(
AttributeError: not support Operator: "Shape" yet
这是因为该部分对 onnx 不支持的算子进行检查。例如,onnx 不支持动态形状的模型,因此需要对resnet.py
相关的代码(前向传播部分,第118~129行左右)进行修改:
#output = output.view(output.size(0), -1)
output = output.view(-1,int(output.numel()//output.size(0)))
此外,参考上述链接中的模型限制,PSRAM整体可用空间为8MB,内置FLASH可用空间为8MB,要求单个模型大小整体不超过8M,如果我们不对网络模型的结构进行修改,后续会报错提示模型的大小超过了8M的可用空间限制。
这里提供一种简单的修改方法:打开pytorch-cifar100/models/resnet.py
,在RseNet网络定义部分(第74~93行左右)进行修改:
class ResNet(nn.Module):
def __init__(self, block, num_block, num_classes=100):
super().__init__()
self.in_channels = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True))
#we use a different inputsize than the original paper
#so conv2_x's stride is 1
self.conv2_x = self._make_layer(block, 64, num_block[0], 1)
self.conv3_x = self._make_layer(block, 128, num_block[1], 2)#❤
self.conv4_x = self._make_layer(block, 256, num_block[2], 2)#❤
self.conv5_x = self._make_layer(block, 512, num_block[3], 2)#❤
#self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.avg_pool = nn.AvgPool2d(kernel_size=4)
self.fc = nn.Linear(512 * block.expansion, num_classes)#❤
为了减小模型结构,将卷积层卷积核的个数都改为64,相应的,线性层也要进行对应的修改。按照该思路对上述代码中标注“#❤”的代码进行修改,如下:
self.conv2_x = self._make_layer(block, 64, num_block[0], 1)
self.conv3_x = self._make_layer(block, 64, num_block[1], 2)#❤
self.conv4_x = self._make_layer(block, 64, num_block[2], 2)#❤
self.conv5_x = self._make_layer(block, 64, num_block[3], 2)#❤
#self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.avg_pool = nn.AvgPool2d(kernel_size=4)
self.fc = nn.Linear(64 * block.expansion, num_classes)#❤
修改完成后,重新回到步骤2中的浮点训练,重复执行打包指令报错之前的所有过程,再次执行模型打包指令,可看到以下日志输出:
****** graph serialize ******
pack param begin
pack param success
pack tensor begin
pack tensor success
pack operator begin
pack operator success
pack io begin
pack io success
MemType.PSRAM need capacity: 716960 Bytes
MemType.SHARE_MEM need capacity: 301536 Bytes
resource_total_size:743488
==================================================================================
****** pack success ******
===================================================================================
经过修改后的模型大小不超过8M,规约性检查通过,打包成功。
至此,即可进行正常浮点模型训练,及量化训练,直至模型收敛。将收敛后的模型导出打包生成model.bin文件。
5.引擎仿真执行结果
我们可以使用图像处理脚本将一张图片处理为input.bin
,作为我们算法引擎的输入。
图像处理脚本链接:
https://github.com/LISTENAI/t...
打开thinker文件夹,在thinker文件夹下进行引擎仿真:
cd thinker
使用示例工程test_thinker,指定输入数据、资源文件和输出文件路径,运行模拟代码,即可看到模型推理结果。
chmod +x ./bin/test_thinker
./bin/test_thinker input.bin model.bin output.bin 3 32 32 6
这里的demo是100分类,填写好各文件的路径之后,执行上述指令,在终端显示输出如下内容,其表示输入模型的分类结果,包括序号、标签和相应的概率值。该结果表示模型已计算完成,工程实例顺利跑通。
init model successful!
create executor successful!
forward successful!
Predicted category index: 82
Predicted label: sunflower
probability:127