​在多元算力时代,大模型的性能不仅取决于计算密集型的算子,更取决于那些随处可见的 Element-wise(逐元素)与 Reduction(规约)算子。它们虽然逻辑简单,却往往决定了整个模型的吞吐上限。

在 FlagGems 的日常开发中,我们反复验证一个问题:大部分的性能瓶颈,不出现在计算密度上,而出现在访存组织里。FlagGems 是基于 Triton 的高性能通用算子库,作为 FlagOS 开源生态的核心组件,FlagGems 目前支持超过 400 个算子,基本覆盖大模型需求,82% 以上的算子性能可与 CUDA 原生算子平齐甚至实现超越,成为全球最大的 Triton 单一算子库。更重要的是,它支持多款硬件后端,完成了对 28 种主流 AI 芯片的适配支持,做到“多芯片运行、处处高性能”。

本文将从工程实践角度,拆解 FlagGems 在 Element-wise 和 Reduction 两类基础算子上的优化方法。所有结论均来自真实 Profile 数据和实际的测试结果。

Element-wise 算子的性能上限在哪里?

Element-wise 算子(如 add、mul、gelu)是典型的访存密集型(Memory-bound)任务,特点是:计算量小,访存密度高。因此,其性能瓶颈通常不在计算,而在内存带宽。

计算公式:理论带宽上限 = 内存带宽 × (有效负载字节数 / 总传输字节数)。

在实际开发中,开发者常常发现即使逻辑写对了,带宽利用率(DRAM Utilization)也往往只有 30% 左右。那如何判断一个算子是否“足够快”呢?在 FlagGems 的工程实践中,我们通过两个硬指标来衡量:

  • L2 Hit Rate > 92%:说明 Cache Line 的复用合理。
  • DRAM Utilization > 75%:说明访存通路已接近硬件物理极限。 例如,经过 FlagGems 优化后的算子,在特定国产 GPU(HBM2e,1.6 TB/s)上,实测吞吐利用率可从原生的 28% 跃升至 79%。

如果低于任一值,则存在优化空间——不是算力不够,而是数据没送到位。

高效 Element-wise 的关键要素

  1. 内存访问连续性:对齐 vs 非对齐,差的不只是一点

GPU 的性能核心在于访存合并(Coalesced Access),以 Cache Line(通常是 128 字节)为单位,一次访问会读取整个 Cache Line 的数据。如果你的内存访问是对齐的,一次就能读满数据;但如果是非对齐的,一次访问就会跨两个 Cache Line,需要两次内存事务,数据搬运开销直接翻倍。

在 Triton 中最优的访存模式是用 tl.arange(0, BLOCK_SIZE) 生成连续偏移,这样相邻线程访问相邻地址,GPU 会自动合并访存;

但如果张量本身是非连续的,例如转置后的张量,stride[1] = H,访问就会变成“步长为 H 的跳跃式访问”。这种情况下,GPU 的访存合并机制完全失效,每次加载可能只拿到 1 个元素,这是硬件架构决定的,没有通用的软件优化方法。

FlagGems 的解法不是“优化非连续访问”,而是让 kernel 能够正确支持非连续输入,避免因错误计算地址而出错。在 kernel 中传入每个维度的 stride 参数,计算偏移时使用 stride 而非假设连续。

优化建议:对于非对齐访问的场景,可以用 padding 的方式,把输入数据补到对齐的大小,再去掉 mask,减少额外的内存事务;对于跳跃式访问,可以使用 FlagOS 新语言Triton-TLE 中的 tle.gpu.alloc 分配 SMEM,把数据连续加载到 SMEM 中,再从 SMEM 跳跃式访问,从而提高 GMEM 的访问效率。

  1. 向量化加载的 mask 策略 

Triton 里的 tl.load 支持 mask 参数,用来处理边界外的访问(比如输入大小不是 BLOCK_SIZE 的整数倍),mask 策略的选择,直接影响着算子的性能。

常见的 mask 策略有两种,一是边界检查 mask:直接用 offsets < n 作为 mask,这是最直观的写法,但会导致 Warp 内出现分支,产生 Warp Divergence,同时还会增加额外的指令开销;二是Padding 后无 mask:先把输入数据补到 BLOCK_SIZE 的整数倍,加载的时候不用 mask,计算完再截断。这种方式虽然多了一点数据拷贝的开销,但去掉了分支,整体性能反而更高。

mask = pid * BLOCK_SIZE + offsets < n 是常见写法。但当 n % BLOCK_SIZE ≠ 0 时,末尾 warp 会因 mask 分支导致指令发射效率下降。FlagGems 则改用 padded load + conditional store:


x = tl.load(ptr, mask=True)  # padding to BLOCK_SIZE multiple
y = compute(x)
tl.store(out_ptr, y, mask=offsets < n)  # store mask 更轻量

Nsight 显示:warp instruction issue slot utilization 从 63% → 89%。在 FlagGems 的测试中,去掉 mask 后的算子,性能大大提升,尤其是在输入大小不固定的场景下,提升非常明显。

  1. Mixed Precision 处理技巧  

现在很多大模型训练会用混合精度(FP16/BF16 做计算,FP32 做累加),很多人在实现算子时,会频繁使用 tl.cast 做类型转换,但处理不好,这部分操作会成为新的性能瓶颈。

  • 错误做法:x_f32 = x_f16.to(tl.float32) → 触发 scalar conversion,吞吐暴跌;
  • 正确做法:用 tl.math.fma 或 tl.math.exp 等 native FP16-aware 指令,或批量 pack/unpack:
x_u32 = tl.load(ptr_u32)  # load two FP16 as one u32
x_f16_a, x_f16_b = tl.math.ubf16_to_f32(x_u32)  # hardware-accelerated unpack

Reduction 的效率陷阱与优化

  1. 两阶段 vs 单阶段:不是越“规整”越好  

Reduction 算子(如 sum、mean、max)将一组数据通过规约运算合并为一个或多个结果。在大模型中,Reduction 出现在 LayerNorm、Softmax、Loss 计算等多个关键路径上。

许多开发者在写Reduction 算子时,会用单阶段实现:让所有线程块同时对全局数据做 Reduction,最后只有一个线程块输出结果。这种写法的问题非常明显,只有最后一个线程块在工作,前面的线程块都在闲置,GPU 的并行度根本没利用起来;多线程块访问全局内存时,会出现写冲突,需要原子操作,性能开销比较大。

FlagGems 默认采用 two-phase + grid-stride loop:

  • Phase 1:每个 warp 内部归约(warp-level reduce_add),零 divergent branch
  • Phase 2:block 内归约(shared memory + atomic add),控制 occupancy ≤ 50%

实测结果显示:(2^20,) shape 下,两阶段比单阶段快 1.8×,且 L2 traffic ↓ 44%。

  1. Block Size 与 Occupancy 的隐式契约

Reduction 算子的 BLOCK_SIZE 选择,直接决定了每个 SM 的 Occupancy(活跃线程块数),而 Occupancy 太低,就会导致 GPU 的算力闲置。

BLOCK_SIZE=1024不一定比512 快。主要原因在于:天数 GPU warp scheduler 最多并发 32 warp / SM;若 kernel register usage 高,1024 可能压到 16 warp/SM,occupancy 仅 50%  。

最佳实践参考:

  • 线程块数量应达到 SM 数量的数倍以上;
  • 线程块大小建议 128-1024;
  • FlagGems 建立 BLOCK_SIZE 查表:根据 register pressure 自动 fallback(FP32 reduction 选 512,FP16 选 1024)。
  1. Warp Divergence:最隐蔽的减速器  

Reduction 算子的实现中,很容易出现 Warp Divergence,尤其是在 Block 内 Reduction 的循环里,如果分支处理不当,同一个 Warp 里的线程会执行不同的路径,导致整个 Warp 的执行效率大幅下降。

优化技巧在于:

  • 用 Warp 级指令替代分支:比如用 tl.reduce_sum、tl.reduce_max 这类 Triton 内置的 Warp 级 Reduction 指令,它们是硬件原生支持的,没有分支开销。
  • 消除不必要的分支:把条件判断移到循环外面,或者用位运算、掩码操作替代 if-else 分支。
  • 对齐 Reduction 的步长:让 Reduction 的步长是 Warp 大小(32)的整数倍,这样同一个 Warp 里的线程都执行相同的操作,避免 Divergence。

FlagGems 中的工程实践案例

讲了诸多理论,不如看一个 FlagGems 的案例。

问题:原始 GELU kernel Profile 显示 sms__inst_executed_op_fadd 占比异常低,而 sms__inst_executed_op_fmul 高达 68% ,推断存在冗余乘法链。

原因:PyTorch 风格 0.5 x (1 + tanh(...)) 引入 3 次标量乘,未利用 tl.math.fma 合并。

优化动作:

  • 替换为 tl.math.fma(x,0.5,0.5 x tl.math.tanh(...))(等价但单指令);
  • 将 tanh 输入预缩放,避免内部 overflow guard 分支 ;
  • 对输入做 ubf16 → fp32 显式转换(tl.extra.cuda.libdevice.ubf16_to_f32,已验证存在于 Triton 3.0.0+)。

结果:
4f9790cd-d36e-4e40-8194-a7a187e846c7.png

结语:性能优化不是玄学,是有章可循的工程实践

很多人觉得算子优化是“碰运气的玄学”,但 FlagGems 的实践告诉我们:所有高性能算子的背后,都是对硬件原理的深刻理解,和对每一个细节的打磨。

在编写或优化算子时,可以对照以下清单参考:

  • [ ] tl.load 地址是否 16-byte 对齐?(FP16)或 32-byte(FP32);
  • [ ] mask 是否仅用于 tl.store?tl.load 尽量 pad + unconditional;
  • [ ] mixed precision 路径是否绕过 to () ?优先用 tl.math.* 和 pack/unpack;
  • [ ] reduction 是否分两阶段?warp-level 先归约,再 block-level 合并
  • [ ] BLOCK_SIZE 是否匹配 register pressure?查 occupancy 表而非直觉;
  • [ ] 所有 if 是否可被 tl.minimum/tl.maximum 或 predicated store 替代?

从判断性能上限,到优化内存访问,再到解决 Reduction 的并行度问题,每一步都有清晰的方法论和可落地的技巧。目前,FlagGems 已完成对英伟达、华为、摩尔线程、海光、天数等 28 种主流 AI 芯片的适配支持,在 40 个主流 AI 模型上的推理任务算子覆盖度达到 90%~100%,为开发者提供了极致的开发体验。

如果你想深入了解 FlagGems 里的更多算子实现,可以前往https://github.com/flagos-ai/FlagGems查看源码,也欢迎大家一起参加“FlagOS 开放计算全球挑战赛”算子开发和优化赛道(https://flagos.io/RaceDetail?id=296flq8k⟨=cn),使用 Triton 语言开发大模型常用算子,深入探索芯片体系结构,挑战算子极致性能!

关于众智FlagOS社区

为解决不同 AI 芯片大规模落地应用,北京智源研究院联合众多科研机构、芯片企业、系统厂商、算法和软件相关单位等国内外机构共同发起并创立了众智 FlagOS 社区。成员单位包括北京智源研究院、中科院计算所、中科加禾、安谋科技、北京大学、北京师范大学、百度飞桨、硅基流动、寒武纪、海光信息、华为、基流科技、摩尔线程、沐曦科技、澎峰科技、清微智能、天数智芯、先进编译实验室、移动研究院、中国矿业大学(北京)等多家在 FlagOS 软件栈研发中做出卓越贡献的单位。

FlagOS 是一款专为异构 AI 芯片打造的开源、统一系统软件栈,支持 AI 模型一次开发即可无缝移植至各类硬件平台,大幅降低迁移与适配成本。它包括大型算子库、统一AI编译器、并行训推框架、统一通信库等核心开源项目,致力于构建「模型-系统-芯片」三层贯通的开放技术生态,通过“一次开发跨芯迁移”释放硬件计算潜力,打破不同芯片软件栈之间生态隔离。

官网:https://flagos.io

GitHub 项目地址:https://github.com/flagos-ai

GitCode 项目地址:https://gitcode.com/flagos-ai

SkillHub:https://skillhub.flagos.io

标签: none

添加新评论