05 Kernel优化技术
学习目标
- 理解Kernel的概念以及Kernel优化在推理引擎中的作用
- 掌握Im2Col算法的原理、实现和优化方法
- 理解Winograd算法的原理以及如何减少卷积计算量
- 掌握QNNPACK的核心技术和间接卷积算法
- 了解内存访问优化的关键技术和方法
5.1 Kernel优化概述
5.1.1 什么是Kernel
在深度学习推理引擎中,Kernel是算子在特定硬件上的具体实现。如果说算子(Operator)是对数学运算的高层抽象,那么Kernel就是这些运算在底层硬件上的实际执行代码。
以卷积算子为例: - 算子层:定义了卷积的数学运算(输入与卷积核的滑动窗口乘加) - Kernel层:针对不同硬件(CPU、GPU、NPU)有不同的实现,如使用cuDNN的Kernel、使用OpenCL的Kernel、使用NEON的Kernel等
Kernel优化是推理引擎性能优化的最后一环,也是最关键的环节。即使上层的图优化、算子融合做得再好,如果底层Kernel实现低效,推理性能也无法达到预期。
5.1.2 Kernel优化的重要性
在CNN网络中,卷积操作占据了大约60%-90%的计算时间。因此,卷积Kernel的优化程度直接决定了整个推理系统的性能上限。
Kernel优化的核心目标是:在给定硬件上,以最少的时钟周期完成指定的张量运算。这涉及到:
- 计算效率:充分利用硬件的并行计算能力
- 内存访问效率:减少内存访问延迟和带宽消耗
- 指令级并行:利用SIMD/Vector指令加速计算
- 线程级并行:利用多核并行加速计算
5.1.3 Kernel优化策略
算法层面的优化
通过改进卷积的计算算法来减少计算量,如: - Im2Col:将卷积转换为矩阵乘法,利用成熟的BLAS库 - Winograd:通过多项式优化减少乘法次数 - 稀疏化:跳过零值的计算
内存访问层面的优化
通过优化数据排布和访问模式来提高缓存命中率,如: - 数据分块(Blocking/Tiling):提高缓存局部性 - 数据预取(Prefetching):隐藏内存访问延迟 - 数据复用:在寄存器级别复用数据
硬件层面的优化
充分利用硬件特性,如: - SIMD向量指令:单指令多数据 - 共享内存:GPU线程间共享数据 - Tensor Core:专门用于矩阵运算的硬件单元
5.2 Im2Col算法详解
5.2.1 Im2Col算法原理
为什么需要Im2Col?
在传统的卷积计算中,卷积核在输入特征图上滑动,每次滑动计算一个输出点。这种方式的缺点是:
- 内存访问不连续:不同行的数据在内存中不连续
- 计算效率低:每次只计算一个输出点
- 难以利用硬件优化:无法使用高效的矩阵乘法库
Im2Col(Image to Column)算法将卷积操作转换为矩阵乘法(GEMM,General Matrix Multiply),从而可以利用成熟的矩阵乘法库来加速计算。
Im2Col的核心思想
Im2Col将输入特征图按照卷积窗口展开成列,卷积核展开成行,从而将卷积转化为矩阵乘法。
设输入特征图为\(X\),形状为\((N, C_{in}, H_{in}, W_{in})\),卷积核为\(W\),形状为\((C_{out}, C_{in}, K_H, K_W)\),输出为\(Y\),形状为\((N, C_{out}, H_{out}, W_{out})\)。
卷积的计算公式为:
Im2Col将输入转换为矩阵\(A\),形状为\((H_{out}W_{out}, K_HK_WC_{in})\),将卷积核转换为矩阵\(B\),形状为\((C_{out}, K_HK_WC_{in})\),则输出为矩阵\(C = AB\),形状为\((H_{out}W_{out}, C_{out})\)。
5.2.2 Im2Col实现过程
输入数据重排
将输入特征图按照卷积窗口展开成矩阵。假设输入为单通道图像,卷积核为3x3,步长为1:
输入特征图:
[[a, b, c, d],
[e, f, g, h],
[i, j, k, l],
[m, n, o, p]]
卷积窗口滑动的位置:
位置(0,0): [a, b, c, e, f, g, i, j, k]
位置(0,1): [b, c, d, f, g, h, j, k, l]
位置(1,0): [e, f, g, i, j, k, m, n, o]
位置(1,1): [f, g, h, j, k, l, n, o, p]
Im2Col转换后的矩阵A(每行是一个卷积窗口的展开):
[[a, b, c, e, f, g, i, j, k], # 位置(0,0)
[b, c, d, f, g, h, j, k, l], # 位置(0,1)
[e, f, g, i, j, k, m, n, o], # 位置(1,0)
[f, g, h, j, k, l, n, o, p]] # 位置(1,1)
卷积核重排
将卷积核展开成矩阵。假设有\(C_{out}\)个卷积核,每个卷积核的形状为\((C_{in}, K_H, K_W)\):
卷积核权重:
形状为(C_out, C_in*K_H*K_W)
每个卷积核展开为一行:
[[w_00, w_01, ..., w_0n], # 卷积核0
[w_10, w_11, ..., w_1n], # 卷积核1
...
[w_m0, w_m1, ..., w_mn]] # 卷积核m
矩阵乘法
将Im2Col转换后的矩阵\(A\)和卷积核矩阵\(B^T\)相乘:
C = A @ B^T
C的形状为(H_out*W_out, C_out)
每个输出点对应C的一行
5.2.3 Im2Col的优化
空间组合优化
空间组合(Spatial Pack)优化是一种基于分治法的Im2Col优化策略。它将输入特征图在空间维度上划分为多个小区域,分别处理。
假设将输出划分为\(2\times2\)的小块:
原始输出划分:
[[小块0, 小块1],
[小块2, 小块3]]
每个小块使用独立的Im2Col计算
空间组合优化的优势: 1. 提高缓存局部性:每个小块的计算 Working Set更小 2. 支持并行化:不同小块可以并行计算 3. 减少内存占用:中间结果更小
内存拷贝优化
Im2Col的一个主要开销是数据重排时的内存拷贝。优化策略包括:
- 原地重排:如果内存足够,利用原地置换算法减少拷贝
- 增量更新:只更新发生变化的数据,而非全量重排
- 延迟执行:将Im2Col融入计算过程中,避免显式重排
5.2.4 Im2Col的代码实现
import numpy as np
def im2col(input_data, kernel_size, stride=1, padding=0):
"""
将输入特征图转换为列矩阵
参数:
input_data: (C, H, W) 或 (N, C, H, W)
kernel_size: 卷积核尺寸
stride: 步长
padding: 填充
返回:
col: (out_h*out_w, C*kernel_size*kernel_size)
"""
if input_data.ndim == 3:
input_data = input_data[np.newaxis, ...] # 添加batch维度
N, C, H, W = input_data.shape
K = kernel_size
out_h = (H + 2*padding - K) // stride + 1
out_w = (W + 2*padding - K) // stride + 1
# 应用padding
if padding > 0:
padded = np.pad(input_data,
((0, 0), (0, 0), (padding, padding), (padding, padding)),
mode='constant')
else:
padded = input_data
# 分配输出矩阵
col = np.zeros((N, out_h, out_w, C, K, K))
# 填充col矩阵
for y in range(out_h):
for x in range(out_w):
y_start = y * stride
x_start = x * stride
col[:, y, x, :, :, :] = padded[:, :, y_start:y_start+K, x_start:x_start+K]
# 变换形状: (N, out_h, out_w, C, K, K) -> (N*out_h*out_w, C*K*K)
col = col.transpose(0, 1, 2, 3, 4, 5).reshape(N * out_h * out_w, -1)
return col, (out_h, out_w)
def conv_im2col(input_data, weight, bias=None, stride=1, padding=0):
"""
使用Im2Col实现卷积
"""
N, C, H, W = input_data.shape
K, _, C_out, _ = weight.shape # K=kernel_size, C_out=out_channels
# Im2Col转换
col, (out_h, out_w) = im2col(input_data, K, stride, padding)
# weight形状变换: (C_out, C_in, K, K) -> (C_out, C*K*K)
weight_flat = weight.reshape(C_out, -1)
# 矩阵乘法
out_flat = col @ weight_flat.T
# Reshape输出: (N*out_h*out_w, C_out) -> (N, C_out, out_h, out_w)
out = out_flat.reshape(N, out_h, out_w, C_out).transpose(0, 3, 1, 2)
# 添加偏置
if bias is not None:
out = out + bias.reshape(1, -1, 1, 1)
return out
5.3 Winograd算法详解
5.3.1 Winograd算法原理
Winograd算法是另一种加速卷积计算的算法,由Shmuel Winograd在1980年提出,2015年被应用于深度学习卷积加速。
核心思想
Winograd算法的核心思想是:用更多的加法替代乘法,从而减少计算量。因为在硬件中,乘法比加法消耗更多的时钟周期和面积。
一维Winograd
设输入信号为\(d = [d_0, d_1, d_2, d_3]^T\),卷积核为\(g = [g_0, g_1, g_2]^T\),则F(2,3)表示2个输出点需要3个卷积核元素。
普通卷积计算: $\(y_0 = d_0 g_0 + d_1 g_1 + d_2 g_2\)$ $\(y_1 = d_1 g_0 + d_2 g_1 + d_3 g_2\)$
需要6次乘法和4次加法。
Winograd优化后: $\(m_1 = (d_0 - d_2) g_0\)$ $\(m_2 = (d_1 + d_2) \cdot \frac{g_0 + g_1 + g_2}{2}\)$ $\(m_3 = (d_2 - d_1) \cdot \frac{g_0 - g_1 + g_2}{2}\)$ $\(m_4 = (d_1 - d_3) g_2\)$
只需要4次乘法和8次加法。乘法减少2次!
Winograd的矩阵形式
Winograd算法可以用矩阵形式表示:
其中: - \(G\):卷积核变换矩阵 - \(B^T\):输入变换矩阵 - \(A^T\):输出变换矩阵 - \(\odot\):逐元素乘法(Hadamard积)
5.3.2 二维Winograd
将一维Winograd扩展到二维卷积。对于\(F(2\times2, 3\times3)\)的卷积:
二维Winograd的实现步骤:
- 输入变换:将输入分成\(4\times4\)的小块,通过\(B^T\)和\(B\)进行变换
- 卷积核变换:通过\(G\)和\(G^T\)对每个\(3\times3\)卷积核进行变换
- 元素级乘法:对应位置相乘
- 输出变换:通过\(A^T\)和\(A\)恢复输出
5.3.3 Winograd的变换矩阵
对于F(2,3),常用的变换矩阵为:
G = [[1, 0, 0],
[1/2, 1/2, 1/2],
[1/2, -1/2, 1/2],
[0, 0, 1]]
B^T = [[1, 0, -1, 0],
[0, 1, 1, 0],
[0, -1, 1, 0],
[0, 1, 0, -1]]
A^T = [[1, 1, 1, 0],
[0, 1, -1, -1]]
对于F(2x2, 3x3),变换后的矩阵尺寸: - 输入:\(4\times4\) - 卷积核:\(4\times4\) - 输出:\(2\times2\)
5.3.4 Winograd的优缺点
优点
- 减少乘法次数:对于\(3\times3\)卷积核,理论上可以减少约2.25倍的乘法量
- 适用于小卷积核:\(3\times3\)卷积核是CNN中最常用的,Winograd优化效果显著
- 硬件友好:加法比乘法更快速、更节能
缺点
- 需要额外的变换开销:输入和权重的变换需要计算
- 精度损失:变换过程中的浮点运算可能引入误差
- 仅适用于固定尺寸的卷积:不同尺寸需要不同的变换矩阵
- 内存占用增加:需要存储中间变换结果
适用场景
Winograd算法最适合以下场景: - 卷积核尺寸为\(3\times3\)或更小 - 输入输出通道数较大(充分利用并行性) - 批量大小较大(分摊变换开销)
5.4 QNNPACK算法详解
5.4.1 QNNPACK概述
QNNPACK(Quantized Neural Networks PACKage)是Facebook于2018年开源的量化神经网络加速库,专门针对移动端ARM处理器优化。
QNNPACK的核心创新是间接卷积算法(Indirect Convolution Algorithm),它解决了传统Im2Col算法的两个主要问题:
- 内存占用大:Im2Col需要额外的内存来存储重排后的矩阵
- 缓存效率低:大矩阵乘法难以充分利用L1缓存
5.4.2 间接卷积算法原理
间接卷积算法的核心思想是:不直接复制数据,而是通过间接寻址的方式访问输入数据。
传统Im2Col: - 需要将输入复制到一个更大的连续内存区域 - 然后使用标准的矩阵乘法计算
间接卷积: - 预先建立一个"间接缓冲区"(Indirection Buffer) - 间接缓冲区存储指向实际输入数据的指针 - 通过指针间接访问数据进行计算
5.4.3 间接缓冲区
间接缓冲区是间接卷积算法的核心数据结构。假设:
- 输入形状:\((C_{in}, H, W)\)
- 卷积核形状:\((C_{out}, C_{in}, K_H, K_W)\)
- 输出形状:\((C_{out}, H_{out}, W_{out})\)
间接缓冲区是一个形状为\((H_{out} \times W_{out}, K_H, K_W)\)的指针数组。对于输出位置\((h_{out}, w_{out})\),其对应的间接缓冲区条目指向输入中位置\((h_{out} \times s + k_h, w_{out} \times s + k_w)\)的元素,其中\(s\)是步长。
间接缓冲区访问示意:
输出位置(0,0)的计算:
使用间接缓冲区[0, 0, 0] -> 输入[0, 0, 0]
使用间接缓冲区[0, 0, 1] -> 输入[0, 0, 1]
使用间接缓冲区[0, 0, 2] -> 输入[0, 0, 2]
...
输出位置(0,1)的计算:
使用间接缓冲区[0, 1, 0] -> 输入[0, stride, 0]
...
5.4.4 QNNPACK的优化技术
1. 消除Im2Col数据拷贝
传统Im2Col需要将输入拷贝到连续内存,QNNPACK通过间接寻址避免了这个拷贝开销。
2. 提高缓存效率
QNNPACK将计算划分为小的tiles,使得每个tile的计算完全在L1缓存中进行。对于ARM Cortex-A75,每个tile的大小通常为\(4\times4\)或\(8\times8\)。
3. 充分利用INT8 SIMD
QNNPACK专为INT8量化设计,利用ARM NEON指令集的SIMD能力并行处理多个数据:
# 伪代码:INT8矩阵乘法的一块计算
# 假设 a 和 b 是 4x4 的 INT8 块
# result 是 4x4 的 INT32 累加器
mov w0, #0
dup v0.4s, w0 # 初始化累加器为0
dup v1.4s, w0
dup v2.4s, w0
dup v3.4s, w0
ld1 {v4.16b}, [x0], #16 # 加载输入a的4行
ld1 {v5.16b}, [x1], #16 # 加载权重b的4行
sdot v0.4s, v4.4b, v5.4b[0] # 点积
sdot v1.4s, v4.4b, v5.4b[1]
sdot v2.4s, v4.4b, v5.4b[2]
sdot v3.4s, v4.4b, v5.4b[3]
4. 内存复用
QNNPACK在计算过程中复用中间结果的内存,避免不必要的分配和释放。
5.4.5 QNNPACK vs Im2Col
| 特性 | Im2Col | QNNPACK |
|---|---|---|
| 内存拷贝 | 需要O(HWKKC)额外内存 | 无额外拷贝 |
| 缓存效率 | 取决于GEMM实现 | 优化到L1缓存级别 |
| 适用场景 | 通用卷积 | 量化神经网络 |
| 移动端性能 | 一般 | 优秀 |
| 实现复杂度 | 较低 | 较高 |
5.5 内存访问优化
5.5.1 内存层级与访问成本
在现代处理器中,内存访问存在明显的层级结构,不同层级的访问延迟差异巨大:
| 层级 | 典型延迟 | 典型大小 |
|---|---|---|
| 寄存器 | 1 cycle | < 1KB |
| L1 Cache | 1-3 cycles | 32-64KB |
| L2 Cache | 10-15 cycles | 256KB-1MB |
| L3 Cache | 40-60 cycles | 几MB-几十MB |
| 主内存 | 100-300 cycles | 几GB |
Kernel优化的核心原则是:尽量使用更高级别的内存,减少对低级内存的访问。
5.5.2 数据分块(Blocking/Tiling)
数据分块是一种提高缓存命中率的优化技术。其核心思想是:将大数据结构划分为小块,使得每个小块可以完全放入缓存。
一维分块
def matrix_vector_mul_blocked(A, x, block_size=64):
"""分块优化的矩阵向量乘法"""
n = len(x)
y = np.zeros(n)
for i in range(0, n, block_size):
# 外层循环加载一个数据块到缓存
end_i = min(i + block_size, n)
yi = 0
for k in range(n):
# 内层循环累加
# A[i:end_i, k]应该在缓存中
yi += np.dot(A[i:end_i, k], x[k])
y[i:end_i] = yi
return y
二维分块(用于矩阵乘法)
def gemm_blocked(A, B, C, block_size=64):
"""分块优化的矩阵乘法"""
n = A.shape[0]
m = B.shape[1]
k = A.shape[1]
for i in range(0, n, block_size):
for j in range(0, m, block_size):
for kk in range(0, k, block_size):
# C[i:i+bs, j:j+bs] += A[i:i+bs, kk:kk+bs] @ B[kk:kk+bs, j:j+bs]
A_block = A[i:min(i+block_size, n), kk:min(kk+block_size, k)]
B_block = B[kk:min(kk+block_size, k), j:min(j+block_size, m)]
C[i:min(i+block_size, n), j:min(j+block_size, m)] += A_block @ B_block
return C
5.5.3 数据预取(Prefetching)
数据预取是一种隐藏内存访问延迟的技术。其核心思想是:在使用数据之前,预先将数据从内存加载到缓存。
软件预取
// 软件预取示例
for (int i = 0; i < N; i++) {
// 预取下一个block的数据
__builtin_prefetch(&A[(i + BLOCK_SIZE) * N]);
__builtin_prefetch(&B[i + BLOCK_SIZE]);
// 当前block的计算
for (int j = 0; j < BLOCK_SIZE; j++) {
C[i * BLOCK_SIZE + j] += A[i * N + j] * B[j];
}
}
硬件预取
现代CPU具有硬件预取器,可以根据程序的内存访问模式自动预测并预取数据。优化策略包括:
- 顺序访问:利用顺序预取器
- 跨步访问:利用跨步预取器
- 避免随机访问:随机访问模式会让预取器失效
5.5.4 数据复用
数据复用是在不同计算之间重复使用同一份数据,减少内存访问。
寄存器复用
在计算过程中,将常用数据保持在寄存器中:
// 寄存器复用示例
float sum = 0;
for (int i = 0; i < N; i++) {
float ai = A[i]; // 将A[i]保持在寄存器中
for (int j = 0; j < M; j++) {
sum += ai * B[i * M + j]; // 直接使用寄存器中的值
}
}
共享内存复用(GPU)
在GPU中,使用共享内存让同一线程块的线程共享数据:
__global__ void conv_kernel(float* input, float* output, float* weight) {
__shared__ float shared_input[TILE_SIZE][TILE_SIZE];
// 线程块内线程协作加载数据到共享内存
int tx = threadIdx.x;
int ty = threadIdx.y;
// 协作加载
shared_input[ty][tx] = input[ty * N + tx];
__syncthreads();
// 使用共享内存中的数据
float sum = 0;
for (int k = 0; k < K; k++) {
sum += shared_input[ty][k] * weight[k * K + tx];
}
output[ty * N + tx] = sum;
}
5.5.5 内存对齐与向量化
内存对齐
内存对齐可以确保数据从对齐的地址开始访问,CPU可以一次性读取完整的数据字:
// 对齐的数据结构
struct alignas(16) Vector4 {
float data[4]; // 16字节对齐
};
// 访问对齐的数据可以使用高效的SIMD指令
向量化
向量化使用SIMD(Single Instruction Multiple Data)指令一次处理多个数据:
// 标量版本
void add_arrays(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
// 向量化版本(使用AVX指令)
void add_arrays_vectorized(float* a, float* b, float* c, int n) {
int i = 0;
for (; i + 7 < n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(&c[i], vc);
}
// 处理剩余元素
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}
5.6 Kernel优化实践
5.6.1 卷积Kernel优化示例
以下是一个针对CPU的卷积Kernel优化示例:
void conv_optimized(float* input, float* output, float* weight,
int N, int C, int H, int W,
int K, int out_C, int out_H, int out_W,
int stride, int padding) {
// 预处理:计算指针偏移
int H_pad = H + 2 * padding;
int W_pad = W + 2 * padding;
#pragma omp parallel for collapse(2)
for (int n = 0; n < N; n++) {
for (int oc = 0; oc < out_C; oc++) {
for (int oh = 0; oh < out_H; oh++) {
for (int ow = 0; ow < out_W; ow++) {
float sum = 0.0f;
// Kh循环展开
for (int kh = 0; kh < K; kh++) {
int ih = oh * stride + kh - padding;
// Kw循环展开
for (int kw = 0; kw < K; kw++) {
int iw = ow * stride + kw - padding;
if (ih >= 0 && ih < H_pad && iw >= 0 && iw < W_pad) {
// 内层通道循环向量化
float* input_ptr = input + n * C * H_pad * W_pad
+ ih * W_pad + iw;
float* weight_ptr = weight + oc * C * K * K
+ kh * K + kw;
// SIMD向量化累加
__m256 vec_sum = _mm256_setzero_ps();
for (int c = 0; c < C; c += 8) {
__m256 vec_in = _mm256_loadu_ps(input_ptr + c);
__m256 vec_w = _mm256_loadu_ps(weight_ptr + c * K * K);
vec_sum = _mm256_fmadd_ps(vec_in, vec_w, vec_sum);
}
sum += _mm256_reduce_add_ps(vec_sum);
}
}
}
output[n * out_C * out_H * out_W + oc * out_H * out_W + oh * out_W + ow] = sum;
}
}
}
}
}
5.6.2 优化检查清单
在实现和优化Kernel时,建议检查以下方面:
计算优化 - [ ] 是否使用了算法优化(Im2Col、Winograd等) - [ ] 是否消除了不必要的计算 - [ ] 是否利用了硬件的并行计算能力 - [ ] 是否启用了编译器优化(-O2/-O3)
内存访问优化 - [ ] 数据是否对齐到缓存行边界 - [ ] 是否使用了分块/tiling提高缓存命中率 - [ ] 是否使用了预取隐藏内存延迟 - [ ] 是否最小化了内存带宽消耗
向量化优化 - [ ] 是否使用了SIMD指令 - [ ] 循环是否对齐到向量化长度 - [ ] 是否避免了向量化不友好的代码模式
并行化优化 - [ ] 是否利用了多核并行(OpenMP/MPI) - [ ] 是否避免了伪共享(False Sharing) - [ ] 线程数是否与物理核心数匹配
5.6.3 性能分析方法
Profiling工具
使用profiling工具分析Kernel性能:
# Linux perf
perf stat -e cycles,instructions,cache-references,cache-misses ./inference
# NVIDIA Nsight Compute
ncu --metrics sm__throughput.avg.pct_of_peak_sustained_elapsed ./inference
# Intel VTune
vtune -collect gpu-hotspots ./inference
roofline模型
Roofline模型用于分析Kernel的理论性能上限:
│
FLOPS/s │ *
│ * │ 内存_bound
│ * │──────────
│ * │
│ * │
│* │
│ │
└──────────────────
计算强度 (FLOPs/Byte)
- 如果Kernel在内存_bound区域,说明优化内存访问更重要
- 如果Kernel在计算_bound区域,说明优化计算更重要
本章小结
-
Kernel概念:Kernel是算子在特定硬件上的具体实现,是推理引擎性能优化的最后一环。卷积Kernel占据了CNN推理的主要计算时间,其优化程度直接决定推理性能。
-
Im2Col算法:通过将卷积转换为矩阵乘法(GEMM)来加速计算。核心思想是将输入按卷积窗口展开成矩阵,然后利用成熟的BLAS库进行计算。空间组合优化可以进一步提高缓存效率。
-
Winograd算法:用更多的加法替代乘法来减少计算量。适用于\(3\times3\)等小卷积核,可以显著减少乘法次数。但需要额外的变换开销,且仅适用于固定尺寸的卷积。
-
QNNPACK算法:Facebook开源的移动端量化加速库,核心是间接卷积算法。通过建立间接缓冲区避免Im2Col的数据拷贝开销,优化到L1缓存级别,充分利用INT8 SIMD指令。
-
内存访问优化:核心是提高缓存命中率,包括数据分块(Blocking/Tiling)、数据预取(Prefetching)、数据复用和内存对齐与向量化等技术。
思考与练习
-
概念理解:解释Im2Col算法如何将卷积操作转换为矩阵乘法。这种转换带来的主要优势和潜在开销是什么?
-
原理分析:Winograd算法用加法替代乘法的核心思想是什么?为什么在硬件中这种替换可以提升性能?
-
对比分析:对比Im2Col和间接卷积算法(QNNPACK)的优缺点。在什么场景下你会选择使用哪种算法?
-
设计思考:假设你需要为一个新的AI加速器设计卷积Kernel,你会采用哪些优化策略?请从算法、内存访问、并行化三个层面进行分析。
-
实践应用:使用Roofline模型分析一个给定Kernel的性能瓶颈。如果一个Kernel的实测性能远低于Roofline上限,可能的原因有哪些?
-
扩展思考:随着Transformer架构的流行,传统的CNN优化技术(如Im2Col)是否仍然适用?请分析Transformer中矩阵乘法(MatMul)的优化策略。