本目录围绕 MaxPool2d 算子展示了两个学习阶段的实现、一个运行脚本和如何快速上手的说明。所有示例都在 Experiments/04_maxpooling 下,可以直接参考对应文件。
- 使用
__bang_maxpoolAPI 直接实现 MaxPool2d,这是最简单的方式; - API 优势:
__bang_maxpool是 BangC 提供的内置函数,可以直接对整个 NCHW 格式的张量进行池化操作,无需手动处理窗口和循环; - 数据规模:使用较小的数据规模(2 batch × 4 channels × 64×64 特征图),便于理解 API 的基本用法;
- 无手动窗口处理:不需要手动收集窗口元素,
__bang_maxpool会自动处理所有池化操作; - 适合初学者快速理解
__bang_maxpoolAPI 的使用方式和参数含义。
API 参考文档:Cambricon BANG C - __bang_maxpool
探索任务:
-
API 理解 -
__bang_maxpool的参数:__bang_maxpool(dst, src, channel, height, width, kernel_height, kernel_width)- 实验:尝试修改各个参数(batch、channels、in_h、in_w、kernel_h、kernel_w),观察输出尺寸如何变化
- 实验:理解 7 参数版本和 13 参数版本的区别:
- 7 参数版本:
__bang_maxpool(dst, src, channel, height, width, kernel_height, kernel_width),默认 stride = kernel_size - 13 参数版本:
__bang_maxpool(dst, src, channel, height, width, kernel_height, kernel_width, padding_height, padding_width, stride_height, stride_width)
- 7 参数版本:
- 思考:为什么 7 参数版本更简单?什么场景下需要使用 13 参数版本?
-
数据布局理解 - NCHW 格式:
__bang_maxpool要求输入和输出都是 NCHW 格式(batch, channel, height, width)- 实验:观察代码中如何计算
TOTAL_IN和TOTAL_OUT,理解 NCHW 布局的内存排列 - 实验:尝试修改
BATCH或CHANNELS,观察内存使用如何变化 - 思考:为什么使用 NCHW 格式而不是 NHWC?不同布局对性能有什么影响?
-
输出尺寸计算:
- 输出尺寸公式(7 参数版本,stride = kernel_size):
out_h = floor((in_h - kernel_h) / kernel_h) + 1 - 实验:尝试不同的
kernel_h、kernel_w组合,验证输出尺寸公式 - 实验:观察当
stride = kernel时,输出尺寸如何变化(这是最常见的配置) - 思考:为什么输出尺寸公式是这样的?padding 如何影响输出尺寸?
- 输出尺寸公式(7 参数版本,stride = kernel_size):
-
NRAM 使用分析:
- 代码中使用了
nram_src[TOTAL_IN]和nram_dst[TOTAL_OUT]存储整个张量 - 实验:尝试逐步增大输入尺寸(64×64 → 128×128 → 256×256...),观察何时会 NRAM 溢出
- 计算 NRAM 使用量:
TOTAL_IN * 4B + TOTAL_OUT * 4B - 思考:为什么需要将整个张量加载到 NRAM?能否直接在 GDRAM 上进行池化?
- 代码中使用了
-
使用 Dilation 的高级实现:
__bang_maxpoolAPI 不直接支持 dilation(膨胀)参数,如果需要实现 dilation,需要使用__bang_maxpool_value_indexAPI- 推荐学习路径:在掌握
__bang_maxpool的基础上,学习如何使用__bang_maxpool_value_index实现支持 dilation 的 MaxPool2d - API 参考文档:Cambricon BANG C - __bang_maxpool_value_index
- 实验:阅读
__bang_maxpool_value_index的文档,理解它返回最大值和索引的特性 - 思考:为什么需要
__bang_maxpool_value_index?它在反向传播中有什么作用?
- 按照 tiling 策略,将 (batch, channel) 对分配给多个任务,每个任务处理多个 (batch, channel) 对;
- Kernel 中根据
taskId确定当前任务处理哪些 (batch, channel) 对,逐个加载到 NRAM 中进行池化,再写回 GDRAM; - NRAM 优化:每次只加载单个 (batch, channel) 对的数据,避免将整个张量加载到 NRAM,适合更大规模的数据;
- 任务并行:使用多个任务并行处理不同的 (batch, channel) 对,提高整体性能;
- Host 端设置
num_tasks、使用 BLOCK 模式,并通过 Notifier 记录耗时,同时调用verify_rm_rm验证结果。
探索任务 - 参数调优:
参考文档:Cambricon BANG C/C++ 编程指南 - 任务映射
-
任务分配策略理解 - 为什么按 (batch, channel) 分配任务?:
- 当前实现:每个任务处理
BC_PER_TASK个 (batch, channel) 对,num_tasks = ceil(BATCH * CHANNELS / BC_PER_TASK) - 实验:尝试修改
BC_PER_TASK(1、2、4、8...),观察:- 任务数量如何变化?
- NRAM 使用量如何变化?
- 性能如何变化?
- 观察:不同
BC_PER_TASK下的负载均衡情况 - 思考:为什么按 (batch, channel) 分配是合理的?这种分配方式如何保证数据独立性?
- 当前实现:每个任务处理
-
Tiling 大小的影响:
- 当前实现:
NRAM_BUFFER_SIZE_IN = 64 * 64,NRAM_BUFFER_SIZE_OUT = 32 * 32 - 实验:尝试修改
IN_H和IN_W(64 → 128 → 256...),观察:- NRAM 是否溢出?
- 是否需要调整
NRAM_BUFFER_SIZE_IN和NRAM_BUFFER_SIZE_OUT? - 性能如何变化?
- 思考:如何根据硬件 NRAM 大小调整 tiling 参数?有没有自动计算 tiling 大小的方法?
- 当前实现:
-
任务数量(dim)调优:
- 当前
num_tasks根据BC_PER_TASK自动计算,设置dim = {num_tasks, 1, 1} - 实验:尝试不同的
BC_PER_TASK,观察:- 任务数量如何变化?
- 性能如何变化?
- 是否存在最优的任务数量?
- 思考:任务数量与硬件核心数的关系?是否存在最优的任务数量?
- 当前
-
任务类型(ktype)调优:
- Block 任务(
cnrtFuncTypeBlock):当前使用的任务类型 - Union 任务(
cnrtFuncTypeUnion1/Union2/Union4等):需要满足对齐约束 - 实验:尝试将
cnrtFuncTypeBlock改为cnrtFuncTypeUnion1,观察:- 是否能正常编译和运行?
- 如果出现
CN_ERROR_INVALID_VALUE错误,检查num_tasks是否满足对齐要求 - 性能是否有提升或下降?
- 思考:对于 MaxPool2d 这种按 (batch, channel) 分配的任务,Union 类型是否合适?为什么?
- Block 任务(
-
与最小实现的对比:
- 复杂度对比:
- Simple 版本:单个任务,整个张量加载到 NRAM,代码简单
- Tiling 版本:多个任务,逐 (batch, channel) 加载,代码稍复杂
- NRAM 使用对比:
- Simple 版本:
TOTAL_IN * 4B + TOTAL_OUT * 4B(可能溢出) - Tiling 版本:
NRAM_BUFFER_SIZE_IN * 4B + NRAM_BUFFER_SIZE_OUT * 4B(更可控)
- Simple 版本:
- 性能对比:
- 运行两个版本,对比执行时间
- 适用场景对比:
- Simple 版本:小规模数据,学习 API 使用
- Tiling 版本:大规模数据,实际生产使用
- 思考:什么时候应该使用 Simple 版本?什么时候应该使用 Tiling 版本?
- 复杂度对比:
-
综合调优:结合以上参数,尝试找到在保证正确性的前提下性能最优的配置组合。建议:
- 先理解 MaxPool2d 算法的基本原理(空间池化、滑动窗口)
- 理解
__bang_maxpoolAPI 的参数含义和限制 - 理解任务分配策略(按 (batch, channel) 分配的优势)
- 根据数据规模选择合适的 tiling 参数
- 调整
BC_PER_TASK、任务类型等参数,观察性能变化 - 记录不同配置下的运行时间,找到最优组合
__bang_maxpool API 不直接支持 dilation(膨胀)参数。如果需要实现 dilation,需要使用 __bang_maxpool_value_index API。
Dilation(膨胀)的作用:
- dilation > 1 时,池化窗口中的采样点之间有间隔,可以增大感受野而不增加参数数量
- dilation = 1 时,等同于普通池化(无膨胀)
- dilation 是卷积神经网络中重要的技术,尤其在语义分割等任务中应用广泛
__bang_maxpool_value_index API:
- API 参考文档:Cambricon BANG C - __bang_maxpool_value_index
- 特点:返回池化后的值和索引(value + index)
- 应用场景:
- 实现 dilation 支持
- 支持反向传播(需要最大值的位置索引)
- 更灵活的池化窗口处理
学习建议:
- 先掌握
__bang_maxpoolAPI 的使用(maxpooling_simple.mlu和maxpooling_simple_tiling.mlu) - 阅读
__bang_maxpool_value_index的官方文档,理解其参数含义 - 参考相关资料或示例代码,学习如何使用
__bang_maxpool_value_index实现支持 dilation 的 MaxPool2d - 理解 dilation 的工作原理(通过
ih = h_start + kh * dilation控制采样间隔) - 尝试修改代码,实现支持 dilation 的 MaxPool2d
实验建议:
- 尝试不同的 dilation 值(1、2、3...),观察:
- 采样点如何变化(dilation=2 时,采样点之间有 1 个间隔)
- 输出尺寸如何变化(使用输出尺寸公式计算)
- 感受野如何增大
- 对比 dilation=1(普通池化)和 dilation>1(膨胀池化)的效果
运行脚本提供了完整的编译和执行环境,包含必要的环境变量设置和编译命令。
脚本开头设置了以下环境变量,这些是运行 BangC 程序所必需的:
NEUWARE_HOME=/usr/local/neuware: 指定 Neuware SDK 的安装路径,Neuware 是寒武纪 MLU 的开发工具包LD_LIBRARY_PATH: 添加 Neuware 的库文件路径($NEUWARE_HOME/lib64),确保运行时能找到 MLU 相关的动态链接库PATH: 添加 Neuware 的二进制工具路径($NEUWARE_HOME/bin),使cncc编译器可以直接调用MLU_VISIBLE_DEVICES=0: 指定使用第 0 号 MLU 设备(在多卡环境下可以选择其他设备)TORCH_DEVICE_BACKEND_AUTOLOAD=0: 禁用 PyTorch 的设备后端自动加载,避免与 BangC 运行时冲突
脚本接受一个参数:.mlu 源文件的文件名。
./build_eval.sh maxpooling_simple.mlu
./build_eval.sh maxpooling_simple_tiling.mlu脚本会:
- 自动切换到脚本所在目录(
Experiments/04_maxpooling) - 使用
cncc编译器编译.mlu文件,生成可执行文件 - 执行生成的可执行文件并输出结果
脚本使用的编译命令:
cncc "${MLU_SOURCE}" -o "${TARGET}" --bang-mlu-arch=mtp_592 -O3 -lm--bang-mlu-arch=mtp_592: 指定目标 MLU 架构为 mtp_592-O3: 最高级别的优化-lm: 链接数学库
- 先用最小实现理解
__bang_maxpoolAPI 的基本用法,通过build_eval.sh maxpooling_simple.mlu编译运行并验证正确性; - 理解
__bang_maxpoolAPI 的参数含义和输出尺寸计算; - 进行最小实现的探索任务(API 理解、数据布局、输出尺寸计算、NRAM 使用分析);
- 用 tiling 实现理解任务并行和 NRAM 优化,通过
build_eval.sh maxpooling_simple_tiling.mlu编译运行并验证正确性; - 理解 tiling 策略(按 (batch, channel) 分配任务、NRAM 缓冲优化);
- 对比最小实现和 tiling 实现的性能差异,理解各自的适用场景;
- 进行 tiling 实现的参数调优(任务分配策略、tiling 大小、任务数量、任务类型),逐步调整参数,观察性能变化并记录耗时;
- 阅读
__bang_maxpool_value_index的官方文档,理解 dilation 的实现原理; - 尝试实现支持 dilation 的 MaxPool2d(作为进阶练习)。
- VecAdd:一维向量,逐元素操作,按元素切分任务
- ReLU:一维向量,逐元素激活,按元素切分任务,支持 in-place 优化
- Softmax:二维矩阵,多遍扫描,按行分配任务
- MaxPool2d:四维张量,空间池化,按 (batch, channel) 分配任务,使用
__bang_maxpoolAPI
思考:为什么 MaxPool2d 适合按 (batch, channel) 分配任务?这种分配方式如何保证数据独立性?与其他算子的任务分配策略有何不同?