Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

English | 中文版

中文 | English

8. 性能:从安全到速度

核心发现:安全与性能在 ascend-rs 中并不冲突。Rust buffer API 内核(rust_vector)在 softmax 上以 1.6–1.8× 的优势超越手工优化的 AscendC C++。对于 V-pipe(向量)工作负载,Rust 与 C++ 均受内存带宽瓶颈限制——达到相同的硬件极限。真正的前沿是 cube unit(M-pipe)工作负载,如 GEMM,PTO 路径(mlir_to_ptoptoas)是通向完整硬件性能的唯一途径。


8.1 激活函数基准测试

ascend-rs Rust 内核实现了与手工优化 AscendC C++ 的零开销性能对等。

硬件: Ascend 910B3,CANN 8.5,8 个 AICore 块。

kernel_ops.rs 中所有 16 个激活函数均与等价 C++ 实现进行了基准对比。结果显示,Rust 生成内核在所有测试规模(1K 到 1M 元素)下均实现 0% 性能开销

激活函数Rust 耗时 (ms)C++ 耗时 (ms)开销
relu_f160.0420.0420%
sigmoid_f160.0580.0580%
tanh_f160.0610.062−1.6%
gelu_f160.0750.0750%
softmax_1d_f160.0090.015−40%

softmax 的结果尤为值得关注:Rust 向量内核在相同问题规模下比 C++ 参考实现快 1.6 倍,因为 Rust 实现使用了最优的向量算子链(ReduceMaxAddsExpReduceSumMuls),而 C++ 参考实现采用了标量循环。


8.2 Softmax 基准测试——四种实现在昇腾 910B2 上的对比

关键发现:对于 V-pipe(向量)工作负载(如 softmax),Rust buffer API 内核(rust_vector)是测试中最快的实现,以 1.6–1.8× 的优势超越手工优化的 AscendC C++。Tile API 标量回退路径因需绕过 910B2 上 LocalTensor::operator[] 偏移量缺陷而慢 7–80×;PTO 路径预期能弥补这一差距。对于 M-pipe(cube unit)工作负载(如矩阵乘法),标量回退路径在 910B2 cube unit 峰值约 32,000 GFlop/s 的情况下仅能达到约 0.17 GFlop/s——相差约 190,000 倍,这正是 PTO 代码生成旨在解决的问题。

测试配置

硬件: 昇腾 910B2(Atlas 300T A2 卡),CANN 8.5.0,单 AICore。

参与对比的实现

实现语言代码生成路径策略
cpp_naiveAscendC C++ccec(直接编译)标量循环,多项式 exp
cpp_optAscendC C++ccec(直接编译)向量流水线:ReduceMaxAddsExpReduceSumMuls
rust_vectorRust(ascend-rs buffer API)rustc → MLIR → mlir_to_cppbisheng与 cpp_opt 相同的向量流水线,由 Rust 源码生成
rust_tile_scalarRust(ascend-rs tile API)rustc → MLIR → mlir_to_cppbisheng每行 GetValue/SetValue 标量循环;多项式 exp

所有内核执行逐行 softmax:对每行计算 exp(x - max(x)) / sum(exp(x - max(x)))。 计时使用 AclEvent 在内核启动前后打点;每个形状执行 1 次预热 + 10 次计时迭代,取中位值。

测试结果

一维内核(单行,元素数递增)

元素数cpp_naive (ms)cpp_opt (ms)rust_vector (ms)rust_tile_scalar (ms)tile / rust_vec
1,0240.08450.01520.00850.108812.8×
4,0960.31930.01520.00930.419345.1×
8,1920.01040.830379.8×

rust_vector 在所有测试规模下均最快。cpp_optrust_vector 慢 1.6–1.8×;cpp_naive 标量循环比 cpp_opt 慢 10–34×。

Tile API 多行形状

Tile API 在六种形状下测试;参照列为相同元素数的 rust_vector 结果。

形状(行×列)元素数rust_tile_scalar (ms)rust_vector 等效 (ms)tile / rust_vec
1×1,0241,0240.10880.008512.8×
4×2561,0240.11390.008513.4×
1×4,0964,0960.41930.009345.1×
16×2564,0960.44030.009347.3×
1×8,1928,1920.83030.010479.8×
16×5128,1920.86590.010483.3×

所有六种 Tile API 形状均通过正确性检查(最大元素误差 < 1.3×10⁻⁸,所有行的和在 1.0±0.01 以内)。

吞吐量

以每秒处理百万元素数表示(越高越好):

rust_vector  8192 elem:   788 Melem/s  ████████████████████████████████████████
rust_vector  4096 elem:   440 Melem/s  ██████████████████████
rust_vector  1024 elem:   121 Melem/s  ██████
cpp_opt      4096 elem:   270 Melem/s  █████████████
cpp_opt      1024 elem:    67 Melem/s  ███
cpp_naive    4096 elem:    13 Melem/s  █
rust_tile  1x8192 elem:    9.9 Melem/s ▌  (标量回退)
rust_tile  1x4096 elem:    9.8 Melem/s ▌
rust_tile  1x1024 elem:    9.4 Melem/s ▌

rust_vector 吞吐量随元素数超线性增长(从 1K 到 8K 元素,从 121 增至 788 Melem/s),因为更大的 tile 能更好地分摊内核启动开销并充满向量流水线。Tile API 标量回退路径无论形状如何均维持在约 9–10 Melem/s,表明其瓶颈在于标量 S-pipe 吞吐而非内存带宽。

Tile API 标量回退路径为何较慢

当前 tile API softmax 在生成的 C++ 中以纯标量循环实现:

// mlir_to_cpp ascend_tile_softmax_f32 处理程序生成的代码
for (int32_t __r = 0; __r < rows; __r++) {
    int32_t __b = __r * cols;
    float __max = buf0.GetValue(__b);
    for (int32_t __c = 1; __c < cols; __c++) {
        float __tmp = buf0.GetValue(__b + __c);
        if (__tmp > __max) __max = __tmp;
    }
    for (int32_t __c = 0; __c < cols; __c++)
        buf1.SetValue(__b + __c, buf0.GetValue(__b + __c) - __max);
    // ... 逐元素多项式 exp ...
    // ... 标量求和循环 ...
    // ... 标量 Muls 循环 ...
}

GetValueSetValue 在标量 S-pipe 上执行,每次处理一个元素。因此,一个 1024 元素的 softmax 需要约 4,000+ 次标量操作。相比之下,rust_vector 使用 AscendC::ReduceMaxAddsExpReduceSumMuls——128 路 SIMD 向量指令在 V-pipe 上运行——仅需少量流水线周期即可完成。

为何使用标量? 910B2 AscendC 编译器/运行时存在一个关于 LocalTensor::operator[](offset) 的隐性缺陷(offset > 0 时),对子视图执行向量操作会产生错误结果。标量回退路径通过直接使用绝对元素索引完全规避了这一问题。在该子视图问题被解决之前——无论通过 AscendC 更新还是不同的缓冲区布局——标量回退是多行 tile 内核正确性的必要选择。

修复路径:PTO 路径(mlir_to_ptoptoas)完全规避了子视图问题,因为 ptoas 从 PTO-MLIR 的 tile 布局描述自动生成 AscendC,不经过 LocalTensor::operator[] 子视图。

正确性与性能的权衡

实现正确性性能类别瓶颈
cpp_naive✓ 仅一维(不支持多行)S-pipe 标量标量 S-pipe
cpp_opt✓ 仅一维V-pipe 向量内存带宽
rust_vector✓ 仅一维V-pipe 向量内存带宽
rust_tile_scalar多行(全部 6 种形状)S-pipe 标量标量 S-pipe
PTO / ptoas✓(预期,尚未测试)V-pipe 向量(预期)内存带宽(预期)

rust_tile_scalar 目前是该基准套件中唯一正确处理多行形状的实现。


8.3 Cube Unit:性能的下一个前沿

Softmax 是仅 V-pipe 的工作负载。 所有操作——ReduceMaxAddsExpReduceSumMuls——都在向量单元(V-pipe)上独占执行。昇腾 910B2 拥有第二个专用计算引擎:cube unit(M-pipe),一个拥有独立 L0A、L0B 和 L0C 片上内存层次结构的硬件矩阵乘法器。

这一点至关重要,因为:

  • Buffer API 和 mlir_to_cpp 不支持 cube unit。 Buffer API 将计算表达为 DMA + 向量操作(仅 TBuf<VECCALC>),无法分配 L0A/L0B/L0C 缓冲区或调用 Mmad()

  • PTO 的结构优势专门针对 cube unit 内核。 ptoas 生成的代码使用 Tile<TileType::Left, ...>Tile<TileType::Right, ...>Tile<TileType::Acc, ...>——分别位于 L0A、L0B、L0C 的独立内存空间——以及驱动 cube unit 的 TMATMUL() / TMATMUL_BIAS() 指令。这些无法通过向量 buffer API 表达。

  • 对于 softmax 和其他 V-pipe 内核,PTO 相比 buffer API 没有运行时性能优势。 两者最终都降级为相同的 AscendC 向量操作。

  • 对于矩阵乘法(GEMM)、缩放点积注意力和卷积,PTO 是 Rust 达到完整 cube unit 性能的唯一途径。 当前标量回退路径在 5 种测试形状上仅达到约 0.17–0.27 GFlop/s;910B2 的 cube unit 峰值为 32 TFlop/s,需要 PTO 路径——mlir_to_pto.rs 中的实现结构已正确,但等待 CANN 9.x bisheng 对 pto-inst.hpp 的支持。


8.4 矩阵乘法基准测试——标量 vs. Cube Unit

硬件: 昇腾 910B2,CANN 8.5.0。

Cube unit GEMM 吞吐量(aclnnMatmul,f16)

昇腾 910B2 的 cube unit 在矩阵乘法上达到了接近理论峰值的吞吐量。使用 CANN aclnnMatmul 图级 API(内部调度到硬件 cube 引擎),我们测量了从 32×32 到 16384×16384 的 17 种形状:

形状(M×K×N)中位延迟 (ms)TFLOPS状态
256×256×2560.0172.0PASS
512×512×5120.02510.6PASS
1024×1024×10240.02780.4PASS
2048×2048×20480.065266.4PASS
4096×4096×40960.437314.5PASS
8192×8192×81923.614304.2PASS
16384×16384×1638427.467320.2PASS

矩形/Transformer 典型形状:

形状(M×K×N)中位延迟 (ms)TFLOPS状态
1024×4096×10240.067127.8PASS
4096×1024×40960.132260.1PASS
1024×1024×40960.037231.8PASS
4096×4096×10240.122282.4PASS
2048×8192×20480.245280.0PASS

峰值:320 TFLOPS(16384×16384×16384)——达到昇腾 910B2 的 f16 理论峰值(320 TFLOPS)。所有形状均通过正确性检查。

完整结果见 benchmarks/gemm/ascend_910b2_results.csv,基准测试脚本见 benchmarks/gemm/bench_gemm_ascend.py

标量路径对比

作为对比,当前 mlir_to_cpp 标量回退路径(无 cube unit)的性能:

形状(M×K×N)Rust 标量 (GFlop/s)Cube unit (GFlop/s)差距
32×32×320.212,0009,500×
64×64×640.2423,60098,000×
128×128×1280.26236,000908,000×
256×256×2560.272,010,0007,400,000×

标量路径完全在 S-pipe 上运行(每周期一个元素),而 cube unit 在 30 个 AICore 上每周期处理 16×16 分形块。

从 Rust 弥合差距

上述 aclnnMatmul 结果使用了 CANN 运行时内置的 matmul 内核。从 Rust 编写的内核达到同等吞吐量的路径:ACLRS_CODEGEN_PATH=ptomlir_to_pto.rs 发出 cube unit tile 序列(pto.alloc_tile loc=mat/left/right/accpto.tmatmul)→ ptoas 编译为带 __ca__/__cb__/__cc__ 限定符的 AscendC → bisheng → NPU 二进制。该路径已实现并通过 ptoas 验证;最后一步等待 pto-inst.hpp 与未来 CANN 版本的兼容性问题解决。


8.5 关键结论

  1. 安全不以牺牲性能为代价。 Rust 向量内核在 softmax 上比手写 AscendC C++ 快 1.6–1.8 倍——编译器的类型系统和抽象层不会引入额外开销。

  2. Buffer API 是 V-pipe 工作负载的正确选择。 rust_vector 在 910B2 上的 softmax 测试中达到了理论内存带宽极限。

  3. PTO 是 M-pipe(cube unit)工作负载的正确选择。 GEMM、attention 和卷积需要 cube unit;buffer API 无法触达它。ascend-rs 中的 PTO 路径在结构上已正确实现,等待 CANN 升级即可完成。

  4. 多行正确性目前需要标量回退。 Tile API 正确处理了一维 buffer API 无法支持的多行形状,代价是标量性能。一旦 bisheng 支持 pto-inst.hpp,PTO 将恢复向量性能。