惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
Recorded Future
Recorded Future
Microsoft Azure Blog
Microsoft Azure Blog
M
MIT News - Artificial intelligence
酷 壳 – CoolShell
酷 壳 – CoolShell
I
InfoQ
博客园 - 司徒正美
Stack Overflow Blog
Stack Overflow Blog
Simon Willison's Weblog
Simon Willison's Weblog
爱范儿
爱范儿
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V
Visual Studio Blog
H
Help Net Security
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
B
Blog RSS Feed
美团技术团队
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
腾讯CDC
G
Google Developers Blog
Cyberwarzone
Cyberwarzone
T
Tenable Blog
A
About on SuperTechFans
WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
L
Lohrmann on Cybersecurity
C
Cisco Blogs
K
Kaspersky official blog
Recent Announcements
Recent Announcements
Vercel News
Vercel News
C
Cybersecurity and Infrastructure Security Agency CISA
有赞技术团队
有赞技术团队
P
Privacy & Cybersecurity Law Blog
Hugging Face - Blog
Hugging Face - Blog
S
Schneier on Security
Apple Machine Learning Research
Apple Machine Learning Research
Cloudbric
Cloudbric
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
D
Docker
U
Unit 42
S
Security @ Cisco Blogs

Пусть этот камень будет более крепким, чем человек

【琐记】烟火与尘埃 【Triton】Triton实现矩阵乘 【LLM推理加速】FlashAttention 【LLM推理加速】PagedAttention 【LLM推理加速】Online Softmax LLM基础知识【1】 Transformer模型 【AI编译】LayerGroup Tiling Tile的疑惑和思考 【AI编译】深度优先的Tile调度,万事大吉? 【AI编译】多级流水线Tile调度策略 【CUDA C++】GPU内存使用【3】 【AI编译】Cache缓存地址映射 【CUDA C++】GPU存储【2】 【CUDA C++】GPU基本介绍【1】 【00】0序章-不受欢迎的来客 【转载】我来了——持续低熵 【Halide】调度优化【2】 【感想】写作进度报告5 【Halide】调度优化【1】 【转载】北大中文男足战报2 【BYOC】TVM切分子图 【转载】北大中文男足战报1 【AI编译】张量生命周期管理 SystemC 用寄存器同步建模方法 【脉动阵列】脉动阵列类型 【感想】写作进度报告4 【BYOC】TVM添加自定义编译器 ccompiler 【感想】写作进度报告3 【Tengine】推理流程脑图【2】 【Tengine】推理流程脑图【1】 【NCNN】学习ncnn模型转换 【编译器】使用llvm编译自定义语言【3】编译 object 【编译器】使用llvm编译自定义语言【2】转llvm IR 【编译器】使用llvm编译自定义语言【1】构建AST 【AI编译】如何进行内存分配 【感想】写作进度报告2 【AI编译】layer-group之后如何tiling 【AI编译】如何进行layer-group 【量化】连续卷积层首尾量化的可行性 【Gemm】内存对齐 【gemm】Gemm计算加速 【TVM】通过代码学习编译流程【5】FuseOps 【TVM】通过代码学习编译流程【6】CodeGen 【TVM】通过代码学习类【3.5】Pass 【TVM】通过代码学习编译流程【4】BuildRelay 【AI编译】Tiling操作能优化什么时间 【TVM】通过代码学习编译流程【3】模型编译 【TVM】通过代码学习编译流程【2】模型转换 【TVM】通过代码学习编译流程【1】必要知识 【感想】写作进度报告1 【Winograd】卷积加速算法原理及实现 SystemC 等待异步事件解决方案 【TVM】Python脚本实现模型编译和保存 【推理引擎】常见AI推理框架 【3D建模】T110E3卡迪夫蓝调皮肤模型 【TVM】C++部署运行TVM 【推理引擎】NCNN和Tengine量化推理逻辑对比 【3D建模】IS-7攻城锤流纹岩皮肤展示 【TVM】根据例子走通代码库 博客汇总目录 【Im2Col】卷积加速算法【2】NHWC 【Im2Col】卷积加速算法【1】 NCHW openBlas库的安装与简单使用 C语言工程调用Cpp库解决方案 foo Hello World
【im2col】AScend conv accelerate
Post author: XianMu@Пусть этот камень будет более крепким, чем ч · 2025-08-10 · via Пусть этот камень будет более крепким, чем человек

# 前置信息

(1)本文讲解使用的例子

以如下的卷积为例,进行昇腾 Im2Col 卷积过程:

  • Input 输入维度为 NHWC :【2,25,25,17】
  • 外圈蓝色代表 pad
  • Kernal 维度为 CCHkWk :【34,17,3,3】
  • 操作为 3*3 卷积 pad=1, Group=1, Stride=1, 2D 卷积
  • 得到输出的维度 为 NHWC : 【22,25,25,18】

从图上可以轻易看出相关信息。

例子

现在想起来,光是遇到你这个家伙,就感觉自己赚到了。
------ 大家好啊 我是 暮冬 Z 羡慕

(2)矩阵乘运算单元

昇腾达芬奇架构设计了 16*16 的矩阵乘运算单元,能够提供强大的并行乘加计算能力,可以以一条指令实现两个 16*16 的矩阵相乘的运算。所以昇腾 Im2Col 卷积的目的就是让卷积能够高效地利用 “矩阵乘运算单元” 进行计算。

davincii

感兴趣的可以阅读昇腾架构介绍书籍。

矩阵计算单元可以⽤⼀条指令完成两个 16×16 矩阵的相乘运算(标记为 163,也是 Cube 这⼀名称的来历),等同于在极短时间内进⾏了 163=4096 个乘加运算,并且可以实现 FP16 的运算精度。如图 3-7 所⽰,矩阵计算单元在完成 C=A×B 的矩阵运算时,会事先将矩阵 A 按⾏存放在输⼊缓冲区中,同时将矩阵 B 按列存放在输⼊缓冲区中,通过矩阵计算单元计算后得到的结果矩阵 C 按⾏存放在输出缓冲区中。在矩阵相乘运算中,矩阵 C 的第⼀元素由矩阵 A 的第⼀⾏的 16 个元素和矩阵 B 的第⼀列的 16 个元素由矩阵计算单元⼦电路进⾏ 16 次乘法和 15 次加法运算得出。矩阵计算单元中共有 256 个矩阵计算⼦电路,可以由⼀条指令并⾏完成矩阵 C 的 256 个元素计算。 摘自《昇腾 AI 处理器架构与编程》

# 权重排布

昇腾 Im2Col 五维卷积加速算法 基本流程:

输入为 nhwc 输出为 nhwc

权重维度变化: 权重的维度变化离线进行,不消耗神经网络推理时间。(神经网络推理大致分为 模型转换 量化 推理三个步骤,权重的维度转换可以在模型转换时进行,不占用推理的时间)。下面是权重变换的分步流程,代码实现可以一步完成,也可以分多步完成(因为不影响推理时间。)

weight change

上方的变换如果比较抽象的话,可以结合后面的流程来理解。

# 权重 从 kernel 4D 变换到 kernel 2D

weight change

weight change2

上图是 Kernel 2D 的数据排布方式,维度为【2*3*3*16,34】,为了简便,跳过昇腾 5D 结构,直接从 4D 转到 2D。下面介绍 4D 数据和 2D 数据的一一对应关系。

  • D 图 ① 覆盖区域表示 一个卷积核【17,3,3】展开成 2D 中的一列。对应于 A 图中一整个卷积核。34 个卷积核将展开为 34 列。因此每列代表一个卷积核。
  • B 图,卷积核通道数为 17,需要补零为 16 的倍数 32,并拆分成 2 块(分别是紫色、黄色)。E 图:每一列(每一个卷积核)的紫色部分②是卷积核通道方向拆分的第一块(B 图中的紫色),黄色部分③是拆分的第二块(B 图中的黄色)。
  • 拆分的每一块(比如紫色部分)又分成 3*3(kernel 行 * 列),F 图: ④覆盖的是 kernel 第一行 (对应于 C 图中的④的部分),⑤覆盖的是 kernel 第二行(对应于 C 图中⑤的部分),相似的⑥覆盖的是 kernel 第三行(对应于 C 图中⑥的部分)。3*3 卷积核一共就三行
  • 每一个紫色的小方格代表通道方向的 16 个数。
  • 至此,kernel 4D 和 kernel 2D 所有的数据都一一对应了。例如 F 图中:⑦代表第 6 个卷积核、通道拆分的第二块、第一行、第二列、通道方向的 16 个数。

通过上述对应关系,我们不难得到维度为【2*3*3*16,34】的卷积核 2D 形式。由于昇腾卷积算法的 AI 计算核心是 16*16 的矩阵乘运算单元,同时为了取数方便,还需要将卷积核 2D 转换为大 Z 小 N 排布方式。

# 权重 从 kernel2D 变换到大 Z 小 N

2d

第一步,将 2D【2*3*3*16,34】中 34 补零为 16 的倍数,即 48,得到【2*3*3*16,48】。

第二步,将其按照 16*16 的方格进行划分,得到【2*3*3,3】个【16,16】的小块。(图中画成了 4 个小块,实际应该是 3 个,示意图,见谅)

第三步,将这些小块按照大 Z 小 N 的顺序进行排布。大 Z 指的是外部按照行优先,将按照 Cube1 到 Cube8 这种 “Z” 字形排布;小 N 指的是内部按照列优先,即每个 16*16 的 Cube,先排第一列,然后是第二列... 详见最右边的彩色表示。

多说一句,之所以专门按照 “小 N” 排布,是因为在矩阵运算中,权重作为矩阵乘的第二个参数,数据是按列取的。这就意味着在实际内存中要跳着取数(内存中都是按照行优先排序),自然效率低。提前将其按照列优先的方式进行排布,那么在矩阵乘运算中可以连续取数。至此,我们得到了 【2*3*3,3,16*16】的权重大 Z 小 N 排布形式,这种形式使得能够一次性取出 256 个数参与计算,效率很高。

下面的代码一次性完成了 权重 4D nhwc 到权重大 Z 小 N 排布,仅供参考。还是那句话,权重的变换离线进行,不占用宝贵的推理时间,所以无须关心转换的效率。

void WeightTrans_A(const float* filters, const TensorDim weight_dim, Ascend5Dim we_5D_dim, float* we_tran5D, 
            AscendTransform5Dim we_tran5D_dim, int CUBE_row, int CUBE_col){
    int lastdim4 = we_tran5D_dim.move * we_tran5D_dim.channel * we_tran5D_dim.LW * we_tran5D_dim.cube;
    int lastdim3 = we_tran5D_dim.channel * we_tran5D_dim.LW * we_tran5D_dim.cube;
    int lastdim2 = we_tran5D_dim.LW * we_tran5D_dim.cube;
    int single_filter_num = weight_dim.c * weight_dim.h * weight_dim.w;
    int single_filter_channel = weight_dim.h * weight_dim.w;
    for(int ch_cube=0; ch_cube<we_tran5D_dim.batch; ch_cube++){  
        int index_1 = ch_cube * lastdim4;
        for(int hk=0; hk<we_tran5D_dim.move; hk++){  
            int index_2 = index_1 + hk * lastdim3;
            for(int wk=0; wk<we_tran5D_dim.channel; wk++){  
                int index_3 = index_2 + wk * lastdim2;
                for(int cout_cube=0; cout_cube<we_tran5D_dim.LW; cout_cube++){ 
                    int index_4 = index_3 + cout_cube*we_tran5D_dim.cube;
                    for(int cube_row=0; cube_row<CUBE_row; cube_row++){
                        for(int cube_col=0; cube_col<CUBE_col; cube_col++){
                            int index = index_4 + cube_row*CUBE_col + cube_col;                       
                            if((cout_cube*CUBE_col+cube_row)>=weight_dim.n  || (ch_cube*CUBE_col+cube_col)>=weight_dim.c){
                                we_tran5D[index] = 0;
                            }else{
                                
                                int index_from = (cout_cube*CUBE_col+cube_row)*single_filter_num + (ch_cube*CUBE_col+cube_col)*single_filter_channel + hk*weight_dim.w+ wk;                                
                                we_tran5D[index] = filters[index_from];
                            }  
                        }
                    }
                }
            }
        }
    }
}

# 输入排布

输入 tensor 的内存排布为 nhwc 输出为 nhwc

昇腾算法的维度详细变换如图下图所示。这里展示了输入 input 从 4D 维度转换到 昇腾 5D 结构,然后再转换到 2D 结构,最后转换到大 Z 小 Z 维度。写这么详细只是为了方便读者理解,而在实际操作中,由于 Input 的变换是在线进行,消耗宝贵的推理时间,所以如华为昇腾书中所说:input 先是从 4D 维度 通过软件算法转换为 昇腾 5D 维度(在模型推理过程中这一步可能不需要,因为中间层的 tensor 已经处于昇腾 5D 维度了),之后从昇腾 5D 维度通过 硬件直接转换到大 Z 小 Z 排布(模型推理过程肯定是边转换变计算,所以不会将整个 tensor 转换为大 Z 小 Z 之后,才进行矩阵运算阶段的。本博客为方便,将整个 tensor 完全转换到大 Z 小 Z,再进行后面计算。)

说完这些,就可以介绍一下昇腾算法极致高效的输入的排布转换过程了!

input

# 输入 从 Input 4D 到 Input 5D

还是再强调一下,昇腾可以做到整个模型的中间层的 tensor 均保持昇腾 5D 的维度,所以思考一下,可能只有最初输入到模型的 tensor 需要 从 Input 4D 转 到 Input 5D,或者再数据预处理的时候就将数据处理为 5D 排布。

trans6

  • G 图是最原始的 Input4D 结构,当然,batch 维度 N=2 没有画,只画了一个。它的维度是【25,25,17】
  • H 图为昇腾 5D 结构图,首先要将通道方向的 17 补齐为 16 的倍数 32,同时每 16 个进行一次拆分,拆成两组。
  • 最后注意一下数据的排布顺序就好了:注意 5D 结构中,K_cube 位于最内层,这些数据是连续的,所以先把 高 h=1, 宽 w=1 位置的 16 个数据排在一起。
  • 紧接着将宽度方向 25 个 K_cube 排在一起,变成 25*16
  • 然后再遍历高的方向。变成 25*25*16
  • 最后是遍历两组,得到昇腾的 5D 结构【2,25,25,16】

此处数据搬运较为简单。

# 输入 从 Input 5D 直接搬到 大 Z 小 Z

昇腾通过专门设计的硬件,将 input 从 5D 格式直接搬到 大 Z 小 Z 排布。想要知道怎么搬以及为什么这么搬,还真不得不把其 2D 排布讲明白。 《昇腾 AI 处理器架构与编程》这本书中直接跳过了 2D 排布,导致晦涩难懂。

# Input 5D 到 Input 2D

所以我们直接看 Input2D 与 Weight 2D 的对应情况,如下图所示。

trans5

  • J 图为 input2D 【25*25,2*3*3*16】 K 图为 Weight2D 【2*3*3*16,34】。再回忆一下 Weight2D 数据每一行和每一列的数据的意义,它的一列数据 2*3*3*16 代表什么呢? 2*3*3*16 代表一整个卷积核,2 代表该卷积核通道方向拆成两块,那么 3*3*16 就是每一块的 高 * 宽 * K_cube。
  • 好巧!Input2D 的一行也是 2*3*3*16!(废话,不一样就没法算了)。既然 weight2D 一列数据的意义一清二楚,那么对应的 Input2D 数据一行的意义也就呼之欲出啦! Input2D 的一行 就是卷积核在某个滑动窗口位置对应的 input 数据。例如,Input2D 的第一行,就对应于 I 图 3*3 的彩色窗口数据(没有 Pad 的情况下)。
  • 也就可以推知,Input2D 的每一行绿色部分,就是 I 图通道方向拆分的第一块(拆分的绿色部分);每一行的的蓝色部分,就是 I 图通道防线拆分的第二块(中间深蓝宽度 1,和补齐的浅蓝 15)
  • 那么,为什么 Input2D 有足足 625 行呢?因为滑动窗口纵向滑动 25 次,每次纵向滑动,都包含横向的 25 次,总共 625 次。

假如直接计算 Input2D 矩阵乘 Weight2D,卷积计算就得到最终结果啦!这就是普通的 Im2Col 算法,不清楚的小伙伴们还可以去读一下 Im2Col 算法 NCHWIm2Col 算法 NHWC

从 2D 的角度来看,算法是不是很简单啊。

不要高兴的太早,还没完呢。

# Input 2D 到 大 Z 小 Z

trans4

接下来是将 Input2D 转换到大 Z 小 Z 排布

第一步,将 Input2D【25*25,2*3*3*16】中 25*25 补零为 16 的倍数,即 640,得到【640,2*3*3*16】 ,如图 L。

第二步,将其按照 16*16 的方格进行划分,即得到【40,18】个【16,16】的小块,如图 M。

第三步,将这些小块按照大 Z 小 Z 的顺序进行排布。大 Z 指的是外部按照行优先,将按照 Cube1 到 Cube720 这些块按照 “Z” 字形排布;像 N 图上方排成一行;小 Z 指的是内部也按照行优先,即每个 16*16 的 Cube,先排第一行,然后是第二行... 详见 N 图中的颜色表示。

trans3

上图来自《昇腾 AI 处理器架构与编程》,矩阵 A 的排布为大 Z 小 Z,矩阵 B 的排布为大 Z 小 N,大家可以再理解一下。

至此,Input 的大 Z 小 Z 排布已经实现,接下来就是 16*16 的矩阵乘了。

trans2

  • Input 现在是【40,18】个【16,16】小块,如左图,当然,它现在处于大 Z 小 Z 的一维排布。
  • Weight 现在是 【18,3】个【16,16】小块,如中间图,当然,它现在处于大 Z 小 N 的一维排布。
  • 不知道分块矩阵乘的小伙伴可以再搜索下 《线性代数》中的分块矩阵乘运算。
  • 内部,进行两个 16*16 块的矩阵乘运算,由于 weight 已经按照列优先进行排布,所以矩阵乘的顺序如上图最右边所示。
  • 外部,对【40,18】和【18,3】做矩阵乘运算。
  • 至此,我们得到了【640,18】的矩阵。
  • 然后将上图两图灰色部分对应的多余数据裁掉,就得到了卷积结果【25,25,34】 ,当然,还得遍历一下 batch,得到【2,25,25,34】

# Input5D 搬到大 Z 小 Z

前两小节介绍了 Input5D 变换到 Input 2D,再变换到 大 Z 小 Z 的过程。而在昇腾芯片中,从 Input5D 到 Input2D 由硬件一步实现。

如果前面两小节已经看明白了的话,那么搬运的秘密就呼之欲出了。

trans1

  • 看上图,左图是 Input 的 5D 维度排布【2,25,25,16】,右边是 Input 2D 排布【25*25,2*3*3*16】。中间是个滑动窗口示意图,3*3,因为本文中用的例子就是 3*3 卷积。
  • 回忆一下右边 2D 排布的数据的意义,每一个小格子是通道方向的 16 个数,每一行是滑动窗口每一个位置对应的 2*3*3*16 个数。滑动窗口纵向滑动 25 次,每次要横向滑动 25 次,所以有 625 行数据,再加上补齐的 15 行,才达到了 640 行数据。
  • 那么右图红色 1 的位置是滑动窗口 a 在第一个位置所对应的 16 个数字;红色 2 的位置是滑动窗口 a 横向滑动一次对应的 16 个数字;红色 3 的位置是滑动窗口 a 横向滑动第三次对应的 16 个数字;依次类推,红色 16 的位置是滑动窗口横向滑动第 16 次对应的 16 个数字。这 16 次滑动,滑动窗口的 a 在左图从 1 滑倒 16!
  • 也就是说,右图红色框的 1-16 与左图 1-16 一一对应!
  • 再来回忆一下,左图中 1-16 这 16*16 的数据是连续的吗?是!(不清楚的再回去看 Input 的维度变换)
  • 那么右图中的 1-16 这 16*16 个数据是连续的吗?它是! 根据大 Z 小 Z 排布,这红色框中 16*16 的数据刚好被分到一个小 Cube 中!
  • 昇腾能够从 Input5D 中一次性拷贝 256 个数据到大 Z 小 Z 排布!

当然,我猜测昇腾应该是设计了 16 个 DMA 组成的 DAM 队列,来实现一次 256 个数据的搬运。真的是相当高效了!

文章好长啊!画了好多图!

# 后记

本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,我看到一定及时回复!