CANN Ascend 950 FP32 转 FP8 E4M3FN 实战:DINTLV 互补 Cast 与 B16 pack Store 的 4:1 窄化优化路径
2026/9/18 4:37:36 网站建设 项目流程

CANN Ascend 950 FP32 转 FP8 E4M3FN 实战:DINTLV 互补 Cast 与 B16 pack Store 的 4:1 窄化优化路径

【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples

FP32 转 FP8 E4M3FN 是低精度推理、通信压缩与量化算子边界最常见的 4:1 窄化转换。本教程取自 CANN 高性能实战演进样例库的 VF 数据变换专题,面向 Ascend 950(dav-3510)寄存器计算(RegBase)SIMD VF 编程模型,通过四个可独立构建、运行和验证的阶段,演示如何用DIST_DINTLV_B32载入、ZERO/TWO 互补CastOr合并与DIST_PACK_B16Store 写出,把 VF 热循环的显式寄存器整理降到最低,最终将 CANNsim VF cycles 从 1250 优化到 379(等效提速 3.30 倍)。读完本文,你将掌握一套可复用的 4:1 窄化数据通路模板,以及"先画 lane 数据流、再删显式整理指令"的 VF 优化方法论。

1. 从 4:1 窄化到寄存器 lane 设计

FP32 占 4 字节,FP8 只占 1 字节,位宽比 4:1。Ascend 950 的 Vector 寄存器宽 256 B,一个寄存器可容纳 64 个 FP32 或 256 个 B8(byte)。因此每轮转换以 128 个元素为一组最自然:输入占两个 B32 寄存器(2 × 64 个 FP32),转换后的输出只占 128 B,即半个 B8 寄存器。

问题的核心不在Cast指令本身,而在于窄化前后寄存器 lane 的排布:64 个 FP32 转换后只产生 64 个有效 FP8 byte,它们落在 B8 视图的哪些 lane 上,决定后续需要多少PackInterleave和中间寄存器才能恢复连续输出顺序。这正是本教程四个阶段反复调整的对象。

本教程的目标数据通路(见教程 README):

FP32 ND -> LoadAlign(DIST_DINTLV_B32) -> Cast(ZERO) + Cast(TWO) -> Or -> StoreAlign(DIST_PACK_B16) -> FP8 E4M3FN raw bytes

四个阶段使用相同的多核切分、20480 元素 tile 和双缓冲流水,只调整 VF 热循环中的数据通路,从而保证性能差异完全来自寄存器数据排布技巧本身。

2. 支持范围与运行前提

依据教程 README 的"支持范围",本样例适用前提如下:

维度约束
硬件Ascend 950,编译架构dav-3510
软件CANN 9.2.0,Release 构建,ASC 使用-O3
输入/输出连续二维 FP32 ND 输入,FP8 E4M3FN raw byte 输出
转换语义CAST_RINT(舍入)、NO_SAT(不饱和)、ZEROING(mask 合并清零)
测试用例fulltailperftrace
尾块处理kernel 将有效元素补齐到 128 元素组后进入 VF,写回 GM 时只复制有效范围

修改舍入或饱和规则后,必须重新生成 golden,否则逐 bit 校验会失败。

当运行环境有 64 个 AIV 时,四个用例的 shape 分别为(该 shape 由 host 端MakeCases按核数动态计算,见各阶段.asc中同名函数):

CaseShape(64 AIV 时)用途
full(1280, 4096)完整 tile 的多核正确性验证
tail(1281, 4099)非整 tile、非整寄存器组与 guard 验证
perf(2560, 4096)多 tile 端到端性能采集
trace(320, 4096)每核一个 tile 的 CANNsim VF 流水分析

trace用例中每个 AIV 处理一个 20480 元素 tile,共 160 个 128 元素组。最终阶段的主指令规模为:160 次双输出 Load、320 次 Cast、160 次 Or 和 160 次 Store。需要说明的是,这个规模用于判断显式 Pack、Interleave 和重复写回是否已消除,并不等同于脱离指令时延和流水约束的 cycle 理论值。

3. 目录结构与构建方式

教程位于vf_data_transform_story/vf_data_transform_tutorials/fp32_to_fp8/,四个阶段目录各自包含完整的.asc源码与CMakeLists.txt。源码完整包含该阶段的 VF、kernel、数据搬运和 host 调用代码,不引用其他阶段的实现;公共目录(vf_data_transform_tutorials/include/)只提供 ACL 运行时初始化、设备内存与二进制 I/O 等辅助能力(见 tutorials 总 README)。

阶段主要变化构建目标
0_dintlv_b32_loadDINTLV B32 双输出载入基线vf_data_transform_fp32_to_fp8_0_dintlv_b32_load
1_complementary_castZERO/TWO 互补 Cast 与 Orvf_data_transform_fp32_to_fp8_1_complementary_cast
2_pack_b16_storeB16 pack Store 连续写出vf_data_transform_fp32_to_fp8_2_pack_b16_store
3_shared_b8_maskOr 和 pack Store 共享 B8 maskvf_data_transform_fp32_to_fp8

聚合构建目标vf_data_transform_fp32_to_fp8_tutorial由 fp32_to_fp8/CMakeLists.txt 统一挂接四个阶段目标。

在仓库根目录构建全部阶段:

source /usr/local/Ascend/ascend-toolkit/set_env.sh cmake -S . -B build -DNPU_ARCH=dav-3510 -DCMAKE_BUILD_TYPE=Release cmake --build build --target vf_data_transform_fp32_to_fp8_tutorial -j4

运行前先安装 Python 数据生成依赖(见专题总览):

python3 -m pip install -r requirements.txt

4. 基准实现:0_dintlv_b32_load

源码:0_dintlv_b32_load/dintlv_b32_load.asc。

基线首先用DIST_DINTLV_B32(distributed interleave)Load,一次把连续 128 个 FP32 拆成两个 64 元素通道,分别放入两个 B32 寄存器:

LoadAlign<float, LoadDist::DIST_DINTLV_B32>( evenInputReg, oddInputReg, input + groupOffset);

本阶段尚未使用互补 Cast,两路都采用RegLayout::ZERO。两路转换结果中的有效 byte 排布相同,需要各执行两级 Pack(B32→B16→B8),再通过Interleave恢复原始元素顺序:

Cast<fp8_e4m3fn_t, float, kFp32ToFp8CastTrait>(evenOutputReg, evenInputReg, fullB32Mask); Cast<fp8_e4m3fn_t, float, kFp32ToFp8CastTrait>(oddOutputReg, oddInputReg, fullB32Mask); Pack<uint16_t, uint32_t, HighLowPart::LOWEST>(evenPackedB16, evenOutputB32); Pack<uint16_t, uint32_t, HighLowPart::LOWEST>(oddPackedB16, oddOutputB32); Pack<uint8_t, uint16_t, HighLowPart::LOWEST>(evenPackedB8, evenPackedB16); Pack<uint8_t, uint16_t, HighLowPart::LOWEST>(oddPackedB8, oddPackedB16); Interleave(interleavedOutputReg0, interleavedOutputReg1, evenPackedB8, oddPackedB8);

这里kFp32ToFp8CastTrait定义为{RegLayout::ZERO, SatMode::NO_SAT, MaskMergeMode::ZEROING, RoundMode::CAST_RINT},即 ZERO 布局、不饱和、mask 合并清零、就近舍入。

每个 tile(160 组)因而包含640 次 Pack 和 160 次 Interleave。CANNsim 测得 1250 cycles,显式寄存器整理是首要瓶颈。

源码中的kElementsPerRepeat = AscendC::VECTOR_REG_WIDTH / sizeof(float)static_assert(kElementsPerRepeat == 64)固定为 64,即 Ascend 950 的 B32 向量宽度;kElementsPerGroup = 2 × 64 = 128kElementsPerTile = 20480。这些常量同时约束了 tile 必须包含完整的双通道组、UB 槽位需保持 64 B 对齐,以及双缓冲 footprint 不超过运行时 UB 容量(kAscend950RuntimeUbBytes = 216 KB)。

5. 优化阶段一:ZERO/TWO 互补 Cast 与 Or 合并

源码:1_complementary_cast/complementary_cast.asc。

第二路 Cast 改用RegLayout::TWO,与第一路的RegLayout::ZERO形成互补的 B8 lane。两路均保持NO_SATZEROINGCAST_RINT

constexpr CastTrait kFp32ToFp8CastTrait = { RegLayout::ZERO, SatMode::NO_SAT, MaskMergeMode::ZEROING, RoundMode::CAST_RINT}; constexpr CastTrait kFp32ToFp8CastTraitTwo = { RegLayout::TWO, SatMode::NO_SAT, MaskMergeMode::ZEROING, RoundMode::CAST_RINT};

ZEROING保证每个有效 B8 lane 只由一路 Cast 提供数据(另一路对应位置为 0),因此可以直接用Or合并两路,无需关心 lane 重叠。由于本阶段尚未引入 pack Store,合并结果仍通过一次显式 B16→B8 Pack 和普通 B8 Store 写出:

Or(mergedOutputReg, evenOutputB8, oddOutputB8, fullB8Mask); Pack<uint8_t, uint16_t, HighLowPart::LOWEST>(packedOutputReg, mergedOutputB16); StoreAlign<uint8_t, StoreDist::DIST_NORM_B8>( output + groupOffset, packedOutputReg, halfB8Mask);

注意这里的halfB8Mask使用MaskPattern::VL128,对应 128 B 的有效输出长度;fullB8Mask使用MaskPattern::ALL,覆盖完整的 B8 寄存器 lane。

CANNsim 测得379 cycles,较基线减少69.7%,等效提速3.30 倍。ZERO/TWO layout 与 Or 共同消除了两级双路 Pack 和 Interleave,热循环只剩最后一级显式压缩(每 tile 160 次 Pack)。

6. 优化阶段二:B16 pack Store 消除显式 Pack

源码:2_pack_b16_store/pack_b16_store.asc。

Or之后的有效 byte 位于相邻 B16 的低 8 bit(即每个 B16 的低字节)。DIST_PACK_B16Store 可以在写回 UB 时完成紧排,不再需要独立的 Pack 寄存器:

StoreAlign<uint8_t, StoreDist::DIST_PACK_B16>( output + groupOffset, mergedOutputReg, storeB8Mask);

该阶段把每组的显式 Pack 全部删除,主数据通路已经收敛为:双输出 Load → 两次 Cast → 一次 Or → 一次 Store。CANNsim 仍为 379 cycles——说明在当前 tile 规模下,显式 Pack 与 pack Store 的差异没有改变关键路径;但中间寄存器和显式整理指令已经消除,这段代码可以直接作为 B16 pack 写出的模板复用。

7. 优化阶段三:共享 B8 mask

源码:3_shared_b8_mask/shared_b8_mask.asc。

Or和 B16 pack Store 都消费合并前的完整 B8 lane,因此可以共享同一个B8 ALLmask。B32 Cast mask 与 B8 mask 都是循环不变量,应在 VF 循环前创建一次:

MaskReg fullB32Mask = CreateMask<float, MaskPattern::ALL>(); MaskReg fullB8Mask = CreateMask<uint8_t, MaskPattern::ALL>(); Or(mergedOutputReg, evenOutputB8, oddOutputB8, fullB8Mask); StoreAlign<uint8_t, StoreDist::DIST_PACK_B16>( output + groupOffset, mergedOutputReg, fullB8Mask);

CANNsim 仍为 379 cycles。当前-O3已对前一阶段的固定 predicate 做出等价处理,因此这一改动没有额外 cycle 收益;但显式共享能准确表达 predicate 粒度——pack Store 消费的是合并前的完整 B8 源 mask(256 lane),而不是 128 B 的输出长度,复用时不要误把 128 B 输出长度当成 pack Store 的源 mask 范围。

8. 统一的多核框架与尾块处理

四个阶段的 kernel(Fp32ToFp8Kernel)与 host 代码完全一致,只有 VF 热循环不同,这正是"只改变待分析数据通路"这一设计原则的体现(参见 专题总览的 G8 统一口径验证)。框架要点:

  • 多核切分:按GetBlockIdx()/GetBlockNum()将 tile 轮转分配给各 AIV,coreTileCount = (totalTiles - coreIdx + coreNum - 1) / coreNum;每个 AIV 串行处理分配给自己的多个 tile。
  • Ping-pong 双缓冲kBufferCount = 2,每个 UB 槽位大小为kInputTileBytes + kOutputTileBytes(20480 FP32 + 20480 B8),通过SetFlag/WaitFlagV_MTE2MTE2_VV_MTE3MTE3_V四类事件之间同步 GM→UB 拷贝、VF 计算和 UB→GM 写回。
  • VF 调用Fp32ToFp8Tile通过asc_vf_call<Fp32ToFp8TutorialVf>(inputAddr, outputAddr, groupTimes)进入__simd_vf__热循环;循环变量与组数使用uint16_t,满足 SIMD VF 参数约束。
  • 尾块补齐:对最后一个 tile,validElements取实际剩余元素,alignedElements向上取整到 128 元素组;VF 按补齐后的完整组执行,CopyOutputTile写回 GM 时使用validTileLayout只拷贝有效范围,补齐区不进入最终输出。
  • 资源检查与用例生成:host 通过aclrtGetDeviceInfo(ACL_DEV_ATTR_VECTOR_CORE_NUM / ACL_DEV_ATTR_UBUF_PER_VECTOR_CORE)PlatformAscendCManager交叉校验 AIV 核数与 UB 容量,不满足 footprint 即拒绝运行。

9. 性能结果与分析

性能数据使用 CANN 9.2.0 的 Ascend950PR_9589 CAMODEL V100 采集。构建类型为 Release,执行trace用例,每个 AIV 处理 20480 个 FP32。VF cycles取业务 kernel core 0 的avg_cycles;有效 B/cycle 只按本 tile 的有效输入、输出字节数计算(口径详见专题总览"性能采集"一节)。

阶段VF cycles相对上一阶段输入 B/cycle输出 B/cycle每 tile 的主要冗余
0_dintlv_b32_load1250基线65.5416.38640 Pack + 160 Interleave
1_complementary_cast3793.30x216.1554.04160 个显式 Pack
2_pack_b16_store3791.00x216.1554.04mask 在循环内分别创建
3_shared_b8_mask3791.00x216.1554.04目标数据通路

从 DINTLV 基线到最终版本,VF cycles 从 1250 降到 379,减少 69.7%,等效提速 3.30 倍。阶段二和阶段三的代码变化没有在当前 CAMODEL 上转化为额外 cycle 收益,但分别消除了显式 Pack、明确了 pack Store 的 predicate 视图,最终阶段达到本教程的主指令规模目标。当前没有可直接对照且转换语义完全相同的加速库接口,因此教程不列不等价的库性能数据。

性能采集统一使用脚本对trace用例运行 CANNsim(新版本工具包中优先调用npusim,并兼容仍使用cannsim命令的版本;若需渲染报告请先python3 -m pip install plotly):

bash Samples/2_Performance/vf_data_transform_story/vf_data_transform_tutorials/scripts/profile_tutorial.sh \ cannsim fp32_to_fp8 build /tmp/vf_data_transform_cannsim/fp32_to_fp8 trace

10. 运行与验证

四个阶段均支持fulltailperftraceallall顺序运行fulltailperf,也是命令行缺省值)。以最终阶段为例:

./build/Samples/2_Performance/vf_data_transform_story/vf_data_transform_tutorials/fp32_to_fp8/3_shared_b8_mask/vf_data_transform_fp32_to_fp8 --case all

程序会生成固定模式 FP32 输入和 E4M3FN golden,对有效输出逐 bit 比较,并检查带 guard 的完整输出区(防止越界写)。tail用例同时覆盖非整 tile 和不足 128 元素的最后一组。验证成功时输出包含:

[HOST][tail] status=PASS ... bin_match=true

数据默认保存在可执行文件旁的artifacts/fp32_to_fp8/<case>/目录,可通过环境变量VF_DATA_TRANSFORM_ARTIFACT_DIR指定其他位置。host 输出中还包含runtime_aiv_coresplatform_ub_bytesub_footprint_bytes等资源信息,可用于确认运行环境满足双缓冲 UB 需求。

11. 总结与复用建议

FP32 到 FP8 的四步路径围绕 4:1 窄化排布展开,每一步解决一类具体问题:

  1. DINTLV B32 Load把连续输入拆成两路 64 元素通道,天然适配两个 B32 寄存器;
  2. ZERO/TWO Cast把两路结果放入互补的 B8 lane,使 Or 可以直接合并;
  3. B16 pack Store在写回时完成最后一级紧排,删除独立的 Pack 指令;
  4. 共享 B8 mask让 Or 与 Store 复用同一个循环不变量 predicate,准确表达源 lane 粒度。

复用代码时应把DINTLV_B32、ZERO/TWO layout、OrPACK_B16Store作为整体核对,不能只替换其中某一步。目标 FP8 格式、舍入模式或饱和规则改变后,应同时更新CastTrait和 golden;若下游能够直接消费转换后的寄存器,融合计算并减少 UB 或 GM 往返,通常比继续调整单条 Cast 更有价值——这也是 VF 数据变换专题总览 中"优先融合计算、布局转换和 scale"(G7)的通用原则。专题内其他 tutorial(如 HALF → INT8、INT8 → HALF)展示了扩展方向与跨步 Store 场景,可与本教程对照阅读。

【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询