本目录围绕 relu.mlu 展示了两个学习阶段的程序、一个运行脚本和如何快速上手的说明。所有示例都在 Experiments/02_relu 下,可以直接参考对应文件。
- 使用单个任务,通过 BangC API(
__memcpy和__bang_relu)完成 ReLU 激活函数dst[i] = max(0, src[i]); - 不做 tiling:假设数据量较小(1024 个元素),一次性将所有数据加载到 NRAM 中处理,然后写回 GDRAM;
- 激活函数特性:ReLU 是逐元素操作,单输入单输出,比 VecAdd 更简单(不需要两个输入),但引入了激活函数的概念(有条件判断);
- 主机端准备随机输入、分配设备内存、拷贝数据、执行 Kernel,然后拷贝回主机并与 CPU 参考结果比对;
- 适合初学者理解激活函数的概念、逐元素操作的实现,以及从 VecAdd 到更复杂算子的过渡。
探索任务:
参考文档:Cambricon BANG C/C++ 编程指南 - 硬件实现
-
激活函数理解 - ReLU 的特性:
- ReLU 函数:
f(x) = max(0, x),当 x < 0 时输出 0,当 x >= 0 时输出 x - 实验:观察输入数据中正数和负数的分布,理解 ReLU 如何"激活"正数、"抑制"负数
- 对比实验:尝试修改 CPU 参考实现,使用 if-else 实现 ReLU,对比与
max(0, x)实现的区别 - 思考:为什么 ReLU 被称为"激活函数"?它在神经网络中的作用是什么?为什么使用
max(0, x)而不是if (x > 0) x else 0?
- ReLU 函数:
-
API 理解 -
__bang_relu的特点:__bang_relu是 BangC 提供的专用激活函数 API- 实验:尝试不使用
__bang_relu,而是用其他 API 实现 ReLU:- 方案1:使用
__bang_max(max(0, x)) - 方案2:使用
__bang_active_relu(如果存在) - 方案3:使用条件判断和逐元素操作
- 方案1:使用
- 观察:不同实现的性能差异?
__bang_relu的优势是什么? - 思考:为什么 BangC 提供专门的激活函数 API?这些 API 是否针对硬件做了优化?
-
与 VecAdd 的对比:
- 输入数量:VecAdd 需要两个输入(a 和 b),ReLU 只需要一个输入(src)
- 操作复杂度:VecAdd 是简单的加法,ReLU 需要条件判断(max 操作)
- 实验:对比两个算子的代码结构:
- NRAM buffer 数量:VecAdd 需要 3 个(a, b, c),ReLU 只需要 2 个(src, dst)
- API 调用:VecAdd 使用
__bang_add,ReLU 使用__bang_relu - 数据流动:两者都是 GDRAM → NRAM → 计算 → NRAM → GDRAM
- 思考:单输入算子和双输入算子在实现上有什么区别?哪种更容易优化?
-
In-place 操作探索:
- 当前实现:输入和输出使用不同的 buffer(
nram_src和nram_dst) - 实验:尝试修改代码,使用 in-place 操作(输入和输出使用同一个 buffer):
- 将
__bang_relu(nram_dst, nram_src, ...)改为__bang_relu(nram_src, nram_src, ...) - 观察:是否能正常工作?性能是否有变化?
- 将
- 思考:In-place 操作的优势是什么?什么情况下可以使用 in-place 操作?为什么有些算子不能使用 in-place?
- 当前实现:输入和输出使用不同的 buffer(
-
NRAM 使用优化:
- 代码中使用了两个 NRAM buffer:
nram_src和nram_dst - 实验:尝试只使用一个 buffer(in-place 操作),观察:
- NRAM 使用量如何变化?
- 性能如何变化?
- 是否所有激活函数都支持 in-place?
- 思考:如何优化 NRAM 使用?单 buffer 和双 buffer 的权衡是什么?
- 代码中使用了两个 NRAM buffer:
- 按照 16K 的
TILE_E和 256K 的TASK_E_SLICE将数据切成多个 tile,通过 NRAM 中的nram_src/dst缓冲来避免频繁访问 GDRAM; - Kernel 中根据
taskId分配连续 chunk,逐 tile 加载、执行__bang_relu,再写回 GDRAM; - Host 端设置
num_tasks、使用 BLOCK 模式,并通过 Notifier 记录耗时,同时调用verify_rm_rm验证结果。
探索任务 - 参数调优:
参考文档:Cambricon BANG C/C++ 编程指南 - 任务映射
-
Tiling 策略理解 - 逐元素操作的 tiling:
- ReLU 是逐元素操作,每个元素独立计算,没有数据依赖
- 对比实验:与 VecAdd 的 tiling 策略对比:
- VecAdd:需要同时加载两个输入 tile(a 和 b),计算后写回一个输出 tile(c)
- ReLU:只需要加载一个输入 tile(src),计算后写回一个输出 tile(dst)
- 实验:尝试修改代码,使用 in-place tiling(输入和输出使用同一个 tile):
- 将
__bang_relu(nram_dst, nram_src, ...)改为__bang_relu(nram_src, nram_src, ...) - 观察:是否能正常工作?性能是否有提升?
- 将
- 思考:逐元素操作的优势是什么?为什么 ReLU 比 VecAdd 更容易优化?
-
Tiling 大小调优 - 单输入算子的特点:
- 当前
TILE_E = 16384,TASK_E_SLICE = TILE_E * 16 = 262144 - 实验:尝试修改
TILE_E(如 8192、32768、65536),观察:- NRAM 使用量如何变化?(单输入算子只需要 2 个 buffer,比 VecAdd 的 3 个 buffer 更省)
- 内存访问次数如何变化?
- 性能如何变化?
- 对比:与 VecAdd 的 tiling 大小对比,为什么 ReLU 可以使用更大的 tile?
- 思考:单输入算子和双输入算子在 tiling 策略上有什么区别?如何根据算子特性选择 tile 大小?
- 当前
-
任务类型(ktype)调优:
- Block 任务(
cnrtFuncTypeBlock):当前使用的任务类型 - Union 任务(
cnrtFuncTypeUnion1/Union2/Union4等):需要满足对齐约束 - 实验:尝试将
cnrtFuncTypeBlock改为cnrtFuncTypeUnion1,观察:- 是否能正常编译和运行?
- 如果出现
CN_ERROR_INVALID_VALUE错误,检查taskDimX是否满足对齐要求 - 性能是否有提升或下降?
- 思考:对于 ReLU 这种简单的逐元素操作,Block 和 Union 哪种更合适?为什么?
- Block 任务(
-
任务数量优化 - 逐元素操作的并行性:
- 当前
num_tasks根据TASK_E_SLICE自动计算 - 实验:尝试不同的任务分配策略:
- 策略1:增加任务数量(减小
TASK_E_SLICE),每个任务处理更少的数据 - 策略2:减少任务数量(增大
TASK_E_SLICE),每个任务处理更多的数据
- 策略1:增加任务数量(减小
- 观察:不同策略下的性能变化
- 对比:与 VecAdd 的任务分配策略对比,理解逐元素操作的并行性特点
- 思考:逐元素操作是否更容易并行化?为什么?任务数量与硬件核心数的关系如何?
- 当前
-
性能优化技巧 - In-place vs Out-of-place:
- 实验:对比两种实现方式:
- Out-of-place:
__bang_relu(nram_dst, nram_src, ...)(当前实现) - In-place:
__bang_relu(nram_src, nram_src, ...)(优化版本)
- Out-of-place:
- 观察:
- NRAM 使用量:In-place 只需要 1 个 buffer,Out-of-place 需要 2 个 buffer
- 内存访问:In-place 减少了一次写回和一次加载
- 性能:In-place 通常更快
- 思考:什么情况下可以使用 in-place 操作?为什么 VecAdd 不能使用 in-place?ReLU 为什么可以?
- 实验:对比两种实现方式:
-
与 VecAdd 和 Softmax 的对比:
- 复杂度对比:
- VecAdd:双输入,简单加法,单遍扫描
- ReLU:单输入,条件判断,单遍扫描
- Softmax:单输入,多遍扫描,数值稳定性
- 优化空间对比:
- VecAdd:tiling 优化,任务并行
- ReLU:tiling 优化,任务并行,in-place 优化
- Softmax:tiling 优化,任务并行,融合指令优化
- 思考:为什么 ReLU 比 VecAdd 有更多优化空间?为什么 Softmax 需要更复杂的优化技巧?
- 复杂度对比:
-
综合调优:结合以上参数,尝试找到在保证正确性的前提下性能最优的配置组合。建议:
- 先理解逐元素操作的特点(无数据依赖,易于并行)
- 尝试 in-place 优化,减少 NRAM 使用和内存访问
- 根据数据规模选择合适的 tile 大小和任务数量
- 对比 Block 和 Union 任务类型,选择最适合的
- 记录不同配置下的运行时间,找到最优组合
运行脚本提供了完整的编译和执行环境,包含必要的环境变量设置和编译命令。
脚本开头设置了以下环境变量,这些是运行 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 relu.mlu
./build_eval.sh relu_minimal.mlu脚本会:
- 自动切换到脚本所在目录(
Experiments/02_relu) - 使用
cncc编译器编译.mlu文件,生成可执行文件 - 执行生成的可执行文件并输出结果
脚本使用的编译命令:
cncc "${MLU_SOURCE}" -o "${TARGET}" --bang-mlu-arch=mtp_592 -O3 -lm--bang-mlu-arch=mtp_592: 指定目标 MLU 架构为 mtp_592-O3: 最高级别的优化-lm: 链接数学库
- 先用最小实现确认 kernel、host、编译链的基本流程,通过
build_eval.sh relu_minimal.mlu编译运行并验证正确性; - 理解激活函数的概念,对比 ReLU 与 VecAdd 的区别(单输入 vs 双输入,激活函数 vs 简单运算);
- 在
relu.mlu中跟踪 tiling 逻辑,重点关注逐元素操作的特点和 in-place 优化的可能性,通过build_eval.sh relu.mlu编译运行并验证正确性; - 进行最小实现的探索任务(激活函数理解、API 使用、与 VecAdd 对比、in-place 操作);
- 进行 tiling 实现的参数调优,重点关注逐元素操作的优化技巧(in-place、tiling 策略、任务分配),逐步调整参数,观察性能变化并记录耗时。