Skip to content

Latest commit

 

History

History
176 lines (141 loc) · 10.6 KB

File metadata and controls

176 lines (141 loc) · 10.6 KB

ReLU 初学者教程

本目录围绕 relu.mlu 展示了两个学习阶段的程序、一个运行脚本和如何快速上手的说明。所有示例都在 Experiments/02_relu 下,可以直接参考对应文件。

1. 最小实现(relu_minimal.mlu

  • 使用单个任务,通过 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
  • API 理解 - __bang_relu 的特点

    • __bang_relu 是 BangC 提供的专用激活函数 API
    • 实验:尝试不使用 __bang_relu,而是用其他 API 实现 ReLU:
      • 方案1:使用 __bang_maxmax(0, x)
      • 方案2:使用 __bang_active_relu(如果存在)
      • 方案3:使用条件判断和逐元素操作
    • 观察:不同实现的性能差异?__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_srcnram_dst
    • 实验:尝试修改代码,使用 in-place 操作(输入和输出使用同一个 buffer):
      • __bang_relu(nram_dst, nram_src, ...) 改为 __bang_relu(nram_src, nram_src, ...)
      • 观察:是否能正常工作?性能是否有变化?
    • 思考:In-place 操作的优势是什么?什么情况下可以使用 in-place 操作?为什么有些算子不能使用 in-place?
  • NRAM 使用优化

    • 代码中使用了两个 NRAM buffer:nram_srcnram_dst
    • 实验:尝试只使用一个 buffer(in-place 操作),观察:
      • NRAM 使用量如何变化?
      • 性能如何变化?
      • 是否所有激活函数都支持 in-place?
    • 思考:如何优化 NRAM 使用?单 buffer 和双 buffer 的权衡是什么?

2. Tiling 实现(relu.mlu

  • 按照 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 = 16384TASK_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 哪种更合适?为什么?
  • 任务数量优化 - 逐元素操作的并行性

    • 当前 num_tasks 根据 TASK_E_SLICE 自动计算
    • 实验:尝试不同的任务分配策略:
      • 策略1:增加任务数量(减小 TASK_E_SLICE),每个任务处理更少的数据
      • 策略2:减少任务数量(增大 TASK_E_SLICE),每个任务处理更多的数据
    • 观察:不同策略下的性能变化
    • 对比:与 VecAdd 的任务分配策略对比,理解逐元素操作的并行性特点
    • 思考:逐元素操作是否更容易并行化?为什么?任务数量与硬件核心数的关系如何?
  • 性能优化技巧 - In-place vs Out-of-place

    • 实验:对比两种实现方式:
      • Out-of-place:__bang_relu(nram_dst, nram_src, ...)(当前实现)
      • In-place:__bang_relu(nram_src, nram_src, ...)(优化版本)
    • 观察:
      • 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 需要更复杂的优化技巧?
  • 综合调优:结合以上参数,尝试找到在保证正确性的前提下性能最优的配置组合。建议:

    1. 先理解逐元素操作的特点(无数据依赖,易于并行)
    2. 尝试 in-place 优化,减少 NRAM 使用和内存访问
    3. 根据数据规模选择合适的 tile 大小和任务数量
    4. 对比 Block 和 Union 任务类型,选择最适合的
    5. 记录不同配置下的运行时间,找到最优组合

3. 运行脚本(build_eval.sh

运行脚本提供了完整的编译和执行环境,包含必要的环境变量设置和编译命令。

3.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 运行时冲突

3.2 使用方法

脚本接受一个参数:.mlu 源文件的文件名。

./build_eval.sh relu.mlu
./build_eval.sh relu_minimal.mlu

脚本会:

  1. 自动切换到脚本所在目录(Experiments/02_relu
  2. 使用 cncc 编译器编译 .mlu 文件,生成可执行文件
  3. 执行生成的可执行文件并输出结果

3.3 编译参数说明

脚本使用的编译命令:

cncc "${MLU_SOURCE}" -o "${TARGET}" --bang-mlu-arch=mtp_592 -O3 -lm
  • --bang-mlu-arch=mtp_592: 指定目标 MLU 架构为 mtp_592
  • -O3: 最高级别的优化
  • -lm: 链接数学库

4. 建议的学习流程

  1. 先用最小实现确认 kernel、host、编译链的基本流程,通过 build_eval.sh relu_minimal.mlu 编译运行并验证正确性;
  2. 理解激活函数的概念,对比 ReLU 与 VecAdd 的区别(单输入 vs 双输入,激活函数 vs 简单运算);
  3. relu.mlu 中跟踪 tiling 逻辑,重点关注逐元素操作的特点和 in-place 优化的可能性,通过 build_eval.sh relu.mlu 编译运行并验证正确性;
  4. 进行最小实现的探索任务(激活函数理解、API 使用、与 VecAdd 对比、in-place 操作);
  5. 进行 tiling 实现的参数调优,重点关注逐元素操作的优化技巧(in-place、tiling 策略、任务分配),逐步调整参数,观察性能变化并记录耗时。