Skip to content

Latest commit

 

History

History
187 lines (152 loc) · 11.5 KB

File metadata and controls

187 lines (152 loc) · 11.5 KB

Softmax 初学者教程

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

1. 最小实现(softmax_minimal.mlu

  • 使用单个任务,通过 BangC API(__memcpy__bang_argmax__bang_fusion__bang_active_exp__bang_sum)完成 Softmax 归一化;
  • 不做 tiling:假设数据量较小(1 个样本,1024 个特征),一次性将所有数据加载到 NRAM 中处理,然后写回 GDRAM;
  • Softmax 算法采用三遍扫描:
    • Pass 1: 找到最大值(用于数值稳定性)
    • Pass 2: 计算 exp(x - max) 并累加求和
    • Pass 3: 归一化(除以总和)
  • 主机端准备随机输入、分配设备内存、拷贝数据、执行 Kernel,然后拷贝回主机并与 CPU 参考结果比对;
  • 适合初学者理解 BangC 编译器的基本编译/执行流程和 Softmax 算法的实现。

探索任务

参考文档:Cambricon BANG C/C++ 编程指南 - 硬件实现

  • 算法理解 - 为什么需要三遍扫描?

    • 观察代码中的三遍扫描:Pass 1 找最大值,Pass 2 计算 exp,Pass 3 归一化
    • 实验:尝试修改代码,去掉 Pass 1(不找最大值),直接计算 exp(x),观察结果是否正确
    • 你会发现:当输入值较大时(如 > 100),exp(x) 会溢出,导致结果错误或 NaN
    • 数值稳定性原理:Softmax 的数学性质:softmax(x) = softmax(x - c),其中 c 是任意常数。通过减去最大值,确保所有指数值都在合理范围内
    • 思考:为什么选择减去最大值而不是其他值?这如何保证数值稳定性?
  • API 理解 - 融合指令的作用

    • 代码中使用了 __bang_fusion(FUSION_FSA, ...)__bang_fusion(FUSION_FMA, ...)
    • 参考文档Cambricon BANG C 内置函数 - 向量融合函数
    • 融合指令类型
      • FUSION_FMA: dst = src0 * src1 + src2(乘加融合)
      • FUSION_FSA: dst = src0 - src1 + src2(减加融合)
      • FUSION_FMS: dst = src0 * src1 - src2(乘减融合)
      • FUSION_FAA: dst = src0 + src1 + src2(加加融合)
      • 等等
    • 实验:尝试将融合指令替换为普通指令:
      • FSA: dst = src0 - src1 + src2 → 改为先 __bang_sub__bang_add
      • FMA: dst = src0 * src1 + src2 → 改为先 __bang_mul__bang_add
    • 观察性能变化:融合指令通常能减少内存访问次数,提高性能
    • 思考:融合指令的优势是什么?在什么场景下使用 FSA 和 FMA?为什么 Softmax 中 Pass 2 使用 FSA,Pass 3 使用 FMA?
  • API 理解 - 特殊函数的使用

    • __bang_argmax:找到最大值及其索引
    • __bang_active_exp:计算指数函数
    • __bang_sum:向量求和(归约操作)
    • 实验:尝试理解这些 API 的特点:
      • __bang_argmax 返回什么?为什么需要 2 个元素的数组?
      • __bang_sum 是归约操作,与逐元素操作有什么区别?
      • 这些 API 是否支持任意长度?是否有对齐要求?
  • NRAM 使用分析

    • 代码中使用了多个 NRAM buffer:nram_srcnram_dstnram_argmax_result
    • 实验:尝试逐步增大 DIM(1024 → 2048 → 4096...),观察何时会 NRAM 溢出
    • 计算 NRAM 使用量:nram_src[DIM] + nram_dst[DIM] + nram_argmax_result[2] = 2 * DIM * 4B + 8B
    • 思考:为什么需要两个 buffer(src 和 dst)?能否复用同一个 buffer?三遍扫描如何影响 NRAM 的使用?
  • 算法简化探索

    • 实验:尝试将三遍扫描合并为两遍:
      • 方案1:Pass 1 和 Pass 2 合并(边找最大值边计算 exp,但需要先找到最大值才能计算 exp)
      • 方案2:Pass 2 和 Pass 3 合并(边计算 exp 边归一化,但需要先知道总和才能归一化)
    • 你会发现:由于数据依赖关系,三遍扫描是必要的
    • 思考:能否用其他算法减少扫描次数?代价是什么?

2. Tiling 实现(softmax.mlu

  • 按照 16K 的 FEAT_TILE 将特征维度切成多个 tile,通过 NRAM 中的 nram_tile 缓冲来避免频繁访问 GDRAM;
  • Kernel 中根据 taskId 分配行(每个任务处理一行),每行采用三遍扫描:
    • Pass 1: 逐 tile 找到行最大值
    • Pass 2: 逐 tile 计算 exp(x - max),累加求和,存储中间结果
    • Pass 3: 逐 tile 归一化
  • Host 端设置 num_tasks = BATCH(每个任务处理一行),使用 BLOCK 模式,并通过 Notifier 记录耗时,同时调用 verify_rm_rm 验证结果。

探索任务 - 参数调优

参考文档:Cambricon BANG C/C++ 编程指南 - 任务映射

  • Tiling 策略理解 - 为什么按行分配任务?

    • 当前实现:每个任务处理一行(ROW_SLICE = 1),num_tasks = BATCH
    • 实验:尝试修改 ROW_SLICE,让每个任务处理多行(如 2、4、8 行),观察:
      • 任务数量如何变化?
      • 性能如何变化?
      • NRAM 使用量如何变化?
    • 对比实验:尝试按列切分(每个任务处理所有行的部分列),观察:
      • 这种切分方式是否可行?为什么?
      • 三遍扫描的实现会如何变化?
    • 思考:为什么 Softmax 适合按行切分?这与算法的数据依赖关系有什么关系?
  • Tiling 大小调优 - 三遍扫描的影响

    • 当前 FEAT_TILE = 16384,每行需要处理多个 tile
    • 实验:尝试修改 FEAT_TILE(如 8192、32768、65536),观察:
      • Pass 1:需要遍历的 tile 数量如何变化?
      • Pass 2:需要遍历的 tile 数量如何变化?
      • Pass 3:需要遍历的 tile 数量如何变化?
      • 总的内存访问次数如何变化?
      • 性能如何变化?
    • 分析:三遍扫描意味着每行需要访问 GDRAM 三次(Pass 1、Pass 2、Pass 3),tile 大小如何影响总的内存访问量?
    • 思考:是否存在最优的 tile 大小?如何平衡 NRAM 使用和内存访问次数?
  • 融合指令性能分析

    • 代码中 Pass 2 使用 __bang_fusion(FUSION_FSA, ...) 计算 x - row_max
    • 代码中 Pass 3 使用 __bang_fusion(FUSION_FMA, ...) 计算 exp_val * inv_sum
    • 参考文档Cambricon BANG C 内置函数 - 向量融合函数
    • 实验:尝试将融合指令替换为普通指令组合,对比性能:
      • FSA: dst = src0 - src1 + src2__bang_sub + __bang_add
      • FMA: dst = src0 * src1 + src2__bang_mul + __bang_add
    • 观察:融合指令是否能减少内存访问?性能提升有多少?
    • 思考:融合指令在什么场景下最有效?为什么?为什么 Pass 2 使用 FSA(dst = nram_tile - 0.0f + (-row_max)),Pass 3 使用 FMA(dst = nram_tile * inv_sum + 0.0f)?
  • 任务类型(ktype)调优

    • Block 任务cnrtFuncTypeBlock):当前使用的任务类型
    • Union 任务cnrtFuncTypeUnion1/Union2/Union4 等):需要满足对齐约束
    • 实验:尝试将 cnrtFuncTypeBlock 改为 cnrtFuncTypeUnion1,观察:
      • 是否能正常编译和运行?
      • 如果出现 CN_ERROR_INVALID_VALUE 错误,检查 taskDimX(即 BATCH)是否满足对齐要求
      • 性能是否有提升或下降?
    • 思考:对于 Softmax 这种按行分配的任务,Union 类型是否合适?为什么?
  • 任务数量优化 - 行级并行 vs 其他策略

    • 当前策略:num_tasks = BATCH,每个任务处理一行
    • 实验:尝试不同的任务分配策略:
      • 策略1:num_tasks = BATCH / 2,每个任务处理 2 行(修改 ROW_SLICE = 2
      • 策略2:num_tasks = BATCH / 4,每个任务处理 4 行(修改 ROW_SLICE = 4
      • 策略3:固定任务数量(如 128),动态分配行
    • 观察:不同策略下的性能变化
    • 思考:任务数量与硬件核心数的关系?是否存在最优的任务数量?为什么按行分配是合理的?
  • 多遍扫描的性能影响

    • Softmax 需要三遍扫描,每遍都需要访问 GDRAM
    • 实验:记录每遍扫描的时间(可以添加 Notifier),分析:
      • Pass 1(找最大值)的时间占比
      • Pass 2(计算 exp 和求和)的时间占比
      • Pass 3(归一化)的时间占比
      • 哪一遍是性能瓶颈?
    • 优化思考:能否通过算法优化减少扫描次数?能否通过硬件特性(如 Cache)减少内存访问?
  • 综合调优:结合以上参数,尝试找到在保证正确性的前提下性能最优的配置组合。建议:

    1. 先理解 Softmax 算法的特殊性(三遍扫描、数值稳定性)
    2. 理解融合指令的优势和使用场景
    3. 根据数据规模选择合适的 tile 大小和任务分配策略
    4. 记录不同配置下的运行时间,找到最优组合
    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 softmax.mlu
./build_eval.sh softmax_minimal.mlu

脚本会:

  1. 自动切换到脚本所在目录(Experiments/03_softmax
  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: 链接数学库(Softmax 需要 exp 函数)

4. 建议的学习流程

  1. 先用最小实现确认 kernel、host、编译链的基本流程,通过 build_eval.sh softmax_minimal.mlu 编译运行并验证正确性;
  2. softmax.mlu 中跟踪 tiling 逻辑,重点关注三遍扫描的实现和融合指令的使用,通过 build_eval.sh softmax.mlu 编译运行并验证正确性;
  3. 进行最小实现的探索任务(上限和下限探索),理解 NRAM 容量限制和对齐要求;
  4. 进行 tiling 实现的参数调优,逐步调整 FEAT_TILE、任务类型、任务数量等参数,观察性能变化并记录耗时。