程序员小林负责小区一年一度的篝火晚会。他收集了16根柴火,准备点一堆大篝火。每根柴火要先涂一层干燥剂(逐元素算子),然后16根堆成一个大柴堆(归约算子),最后所有人围坐享受篝火的光和热(广播算子)。小林第一次干活太实诚:涂完一根,先搬进院子角落的柴棚存着;16根全涂完、全搬进柴棚后,再一根根搬出来堆篝火。师傅看见了直摇头:“柴棚多此一举!你边涂边堆,一下就完事。”AI编译器里也有一个“柴棚”——显存中的中间结果。Reduce融合干的事,就是把“涂干燥剂”和“堆柴火”这两件事合并成一条流水线,把柴棚整个拆掉。读完这篇,你就理解了编译器为什么这么干,以及它怎么做到让大模型跑得更快、显存更省。
对AI从业者:搞懂Reduce融合,你就掌握了AI编译器最核心的优化思维——不是让算子算得更快,而是让数据少搬家。
可视化演示
展示Reduce融合消除中间HBM访存、并行树形归约算法及融合前后性能对比三种核心模式,直观呈现编译器Reduce算子优化的完整技术方案。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Reduce融合流水线 融合前EW和RD需经HBM两次读写中间结果开销大 融合后EW+RD合并为单kernel消除中间HBM访存 Memory_Saved = (N_before - N_after) / N_before Reduce融合将中间结果保留在寄存器或共享内存
融合核心原理
小林第一次堆篝火的“柴棚方案”听起来很蠢,但很多AI程序正是这么干的:GPU把一批数据做逐元素变换(比如每根柴涂干燥剂),算出的结果先完整写进显存,下一个算子再把这批数据从显存读回来做归约(堆柴火)。这一写一读,就是“搬进柴棚又搬出来”——数据根本没被人用过,纯粹在搬运中浪费带宽。
在AI编译器语境里,归约(Reduce)算子是那种“把一堆数据合成一个或几个小结果”的算子,最常见的有求和(sum)、求均值(mean)、求最大值(max)。逐元素(pointwise)算子则是“每个数据独立处理”,比如对每个元素乘以缩放系数。传统执行方式是两次kernel调用:第一次做逐元素变换,把N个中间结果写进显存;第二次启动归约kernel,把这N个结果再读回来。显存带宽是非常珍贵的资源,这一写一读,白白消耗了2N次数据搬运。
Reduce融合要做的,就是让编译器识别出这种“先逐元素、后归约”的模式,把两个算子合成一个kernel,让中间结果停留在寄存器或共享内存里,再也不落地显存。融合后的访存量一下子从“读N + 写N + 读N”变成“读N + 写1”。
回路到篝火故事:柴棚一拆,小林的搬运量从“搬进32根 + 搬出32根”变成“直接堆16根”。在GPU上,这节省的不是体力,而是宝贵的显存带宽和kernel启动时间。
- \(M_{\text{naive}}\)(未融合访存量)未融合时,数据要经历“读入N个元素 → 逐元素处理后写入N个中间结果 → 归约时再读回N个中间结果 → 写出1个最终结果”,总计约3N次访存。
- \(M_{\text{fused}}\)(融合后访存量)融合后,中间结果不落显存,直接留在寄存器里参与后续归约,只剩“读入N个元素 + 写出1个结果”。
- \(2N\)(消除的搬运量)公式中减去的 \(2N\) 对应中间结果的N次写入和N次读回——这正是被拆掉的“柴棚”。
并行归约算法
小林一个人堆16根柴太慢,于是喊来4个朋友帮忙:每个人先堆自己负责的4根(局部归约),再把4个小柴堆合成一个大篝火。GPU里的归约也是这个思路——成千上万个线程“各堆一堆柴”,最后“合堆”。
在AI计算中,最简单的并行归约是树形归约:所有线程两两配对,各自把配对元素相加,得到原来一半数量的部分和;再两两配对,数量再减半。如此反复,经过 \(\log_2{N}\) 轮后得到一个最终结果。比如离线统计一批用户行为序列的点击总数,16个元素只需4轮就能算完,而不是一个线程傻乎乎地加15次。
实际kernel中更常用的是“级联累加模式”:每个线程先从全局内存取一段连续数据,在寄存器里循环累加出局部和,再用树形归约合并线程间的部分和。这样既利用了每个线程的串行计算能力,又用树形结构保证了高并行度。跨线程块时,则使用原子操作(atomicAdd)把各块的部分和累加到全局结果上——原子操作简单直接,但高并发时冲突多、速度慢,所以只在块间少量使用。
篝火晚会的经验告诉我们:一个人扛16根柴是愚公移山,四个人接力堆是团队协作。GPU归约就是把“愚公移山”变成“千线程协作”,而Reduce融合保证了这条协作流水线一气呵成,中间不设仓库。
- \(N\)(元素总数)比如要归约100万个用户行为数据,\(N=10^6\)。
- \(P\)(线程总数)GPU上同时参与归约的线程数,比如1024个线程。
- \(N/P\)(串行累加量)每个线程先在自己寄存器里累加 \(N/P\) 个元素,这步的耗时和分到的数据量成正比。
- \(\log_{2}{P}\)(合并轮数)P个线程的部分和通过两两配对归约,需要 \(\log_2{P}\) 轮才能合成一个结果——朋友们合堆柴的轮数。
算子组合计算
小林的篝火其实还有一道工序:每根柴涂完干燥剂后,还要称重、编上号,再堆进柴堆。这些“逐元素操作”和“堆柴归约”完全可以串成同一条流水线,边处理边堆。AI计算中最典型的这种组合就是逐元素算子接归约算子,再往外扩一步,归约结果往往还要“广播”回所有元素一起用。
广播算子(Broadcast)可以理解为:篝火烧旺后,它的光和热均匀地传给围坐的每个人——一个标量结果参与所有元素的进一步计算。比如“把所有分数求和后,每个分数都除以这个总和”,就是“归约 + 广播”模式。这类模式在归一化操作中极其常见。
这里最经典的融合例子是softmax:先对一行数据求最大值(归约),再用最大值做逐元素减法得到稳定指数(逐元素),接着对所有指数求和(归约),最后每个指数除以总和(广播)。传统做法要启动多个kernel、多次读写显存;融合后,整条计算链在一个kernel里完成,中间行向量只在寄存器与共享内存间流转。
softmax就像一个“边涂干燥剂、边称重、边堆柴”的智能流水线。没有融合时,每道工序之间都要把半成品搬进柴棚;融合后,木柴从流水线一端进去,篝火从另一端燃起,中间不再有仓库。
- \(\max(x)\)(一行归约)先找出这行数据里的最大值,这是第一次归约,用于数值稳定,防止指数爆炸。
- \(x_i - \max(x)\)(逐元素减法)每个元素都减去最大值,这是逐元素操作,把数据整体“搬移”到一个安全的数值区间。
- \(\sum_{j=1}^{N} e^{x_j - \max(x)}\)(求和归约)对所有指数结果求和,这是第二次归约,得到归一化分母。
- 整体相除(广播)每个指数除以总和,得到概率分布。归约出的标量分母广播回每个元素参与运算。
线程块划分
4个朋友堆16根柴,怎么分工?最自然的是按柴的位置就近分组:院子东边4根归张三、西边4根归李四……每个人只搬自己那一小片区域的柴。GPU线程也是这样:归约任务要按数据在内存中的位置切成块,每块交给一个线程块(block)处理。
线程块划分(Tiling)是Reduce融合的关键策略。编译器把全局内存中的N个元素按连续区间切成若干“数据块”,每个线程块负责一块。块内的数据先被加载到共享内存(GPU里一个小组共用的桌面),然后块内所有线程协作完成局部归约,得出一个部分和。最后,所有线程块的部分和再聚合起来,得到全局结果。
块间聚合有两条主流路线:一是“多kernel方案”——第一个kernel完成块内归约,把部分和写入显存,第二个小kernel归约这些部分和;二是“单kernel加原子操作”——块内归约完成后,每个块用一次原子加把部分和累加到全局结果上。融合场景更倾向于后者,因为少一次kernel启动。每块处理多少数据(块大小S)直接决定了共享内存的占用和并行粒度。
回到篝火晚会:张三、李四每人负责自己地盘上的柴,先堆成小堆,再合力把几个小堆汇总成大篝火。线程块划分就是这场晚会的“分组方案”——分得好,人人有活干,篝火一会就堆完;分得差,有人闲着,有人累死。
- \(N\)(元素总数)要归约的全部数据量,决定总工作量。
- \(S\)(每块元素数)每个线程块负责的元素个数。S太小则块数太多、块间聚合开销大;S太大则共享内存可能放不下。
- \(B\)(线程块数)块数 \(N/S\) 决定了grid规模。块间归约的开销随B增大而增大,所以希望 B 适中。
性能差异分析
小林做了个对照实验:方案A是“搬柴棚”型,方案B是“边涂边堆”型。结果方案B不仅省了一半搬运时间,还因为少请了一次“柴棚管理员开门”(kernel启动)而更加顺畅。在GPU上,这个对照实验就是融合与非融合的基准测试。
为什么Reduce融合省时间?核心在于这类算子属于“访存密集型”(memory-bound):它干的活儿主要是搬运数据,真正的计算(加法、乘法)很轻。GPU的流处理器算力再强,也被显存带宽卡住了脖子。融合后带宽需求大幅下降,瓶颈随之缓解,端到端性能自然提升。典型benchmark中,对100万个元素做“缩放+求和”融合,在带宽受限的GPU上通常能获得1.5到3倍的加速。
除了带宽,融合还省去了多余的kernel启动开销。每次启动kernel都有固定成本,两个kernel合并成一个,启动成本直接减半。此外,数据在寄存器、共享内存中流动,缓存命中率更高,进一步减少了访存延迟。
对照实验的结论很清晰:访存密集的归约场景中,“少搬一趟”比“算得更快”值钱得多。就像篝火晚会的耗时大头从来不是点火,而是把柴从仓库搬到场地。
- \(T_{\text{naive}}\)(未融合耗时)两个独立kernel的总执行时间,包含两次启动开销和两轮显存读写。
- \(T_{\text{fused}}\)(融合后耗时)单kernel执行时间,数据只在寄存器/共享内存间流转。
- \(S\)(加速比)两者的比值。当S接近2或更高时,说明“柴棚搬运”的开销被基本消除,融合策略成功。
硬件架构适配
小林的篝火经验不能直接搬到另一个小区:有的小区禁止明火、有的露营点提供专用土坑。同样,Reduce融合的实现细节在不同GPU架构上各有讲究。
在NVIDIA GPU上,归约可以充分利用共享内存和线程束内shuffle指令。共享内存是每个SM内部的高速小容量存储,相当于一张小组共用的桌面;warp shuffle则允许同一线程束(32个线程)之间直接交换寄存器数据,无需经过共享内存。归约时先用shuffle做束内归约,再用共享内存做束间归约,速度极快。
AMD的CDNA架构中,对应的“共享内存”叫LDS(Local Data Share),基本理念相似,但线程束宽度是64(称为wavefront),归约模式要按64线程对齐处理。华为昇腾的AI Core则没有独立的共享内存,而是采用统一缓冲区(Unified Buffer,UB)承载中间数据。归约时,数据要在UB中展开成矢量指令序列,依赖编译器把求和模式转化为高效的向量化流水。不同架构的“柴棚位置”都不一样,编译器必须因地制宜。
换一个露营点,就要换一套堆柴手法。NVIDIA、AMD、昇腾的“桌面”大小和摆放位置各不相同,编译器为每种架构生成不同的归约指令序列——这就是硬件适配的全部意义。
- \(B_{\text{SM}}\)(片上带宽)共享内存/LDS/统一缓冲区这类片上存储的带宽,极高且稳定。
- \(B_{\text{global}}\)(全局带宽)全局显存的带宽,虽然绝对数值大,但相比片上存储仍有数量级差距。
- \(R\)(带宽比)通常在10到30之间。R越大,说明把数据留在片上就越划算,融合收益越明显。
实现技术要点
堆柴看似简单,其实有讲究:大柴垫底、小柴搭在上面,木柴交错摆放才稳当。归约kernel的编写也一样,细节决定成败。
第一,能放寄存器就放寄存器。每个线程的私有累加是最热的数据,应该留在寄存器里,而不是每次都去访问共享内存或全局内存。这是归约速度的核心。第二,要保证内存合并访问。一个线程束内的32个线程如果同时读取32个连续地址,硬件可以把它们合并成一次事务;如果东一个西一个,访存次数会成倍增加。第三,共享内存读写之间必须插入同步。块内多个线程写完共享内存后,其他线程可能立刻去读,没有同步指令(如__syncthreads())就会读到半新半旧的数据。第四,处理好“尾块”:当元素总数N不是2的幂次时,树形归约会剩下一部分元素,这时要么把尾部元素用原子操作累加进结果,要么用串行循环补上。
计算每个线程分到的元素个数时,用向上取整保证所有元素都被覆盖,宁多勿漏。
柴堆的稳定靠每一根柴的摆放细节,归约kernel的稳定靠寄存器、合并访问、同步和尾块处理这些基本功。细节做到位,融合后的kernel才能既快又对。
- \(N\)(元素总数)参与归约的全部数据量。
- \(P\)(线程总数)实际启动的线程数量。
- \(\lceil \cdot \rceil\)(向上取整)除法结果有余数时进一位。比如 \(N=1000\), \(P=256\),则 \(L=4\)。多出来的“多分的柴”在最后一个线程的循环里用判断兜住。
负载 = 每线程元素数 = ceil(N / P)编译器工程实践
小林把“边涂边堆”的流程写进了一本手册,明年办晚会直接照做。AI编译器也有一本这样的“手册”——它读入计算图,自动识别哪些算子值得融合,然后生成一个高效的融合kernel。
在编译器里,Reduce融合是一个pass(优化趟)。它大致分三步:第一步是算子属性注册——每个算子都要在编译器里登记自己的“性格”,比如逐元素算子声明“我可融合、逐元素语义”,归约算子声明“我输出小张量、可融合”。第二步是pattern匹配——编译器在计算图中寻找“逐元素算子 → 归约算子”的图模式,检查中间张量是否只被这一个归约算子使用。如果中间结果还被别的算子用,融合就会破坏其他计算路径,不能融合。第三步是融合代码生成——匹配成功后,编译器把多个算子重写为一个融合kernel,自动生成线程块划分和归约代码。
融合不是免费的,编译器还要算一笔账:融合后省下多少访存,又新增多少开销?如果归约后的结果立刻被广播回所有元素(如softmax的归一化),融合的收益很高;但如果归约结果只是被一个分支偶尔使用,融合反而可能打乱并行结构。
从此,小林的篝火流程不再依赖临场发挥,而是写进手册自动执行。编译器的融合pass就是这本手册——它自动审视每一条计算路径,决定哪里该“边涂边堆”,哪里还是“分开干”更稳妥。
- \(2N\)(访存节省)中间结果不再写入显存(省N次写)也不再从显存读回(省N次读),总共省下2N次访存。
- \(C\)(融合开销)为了把两个算子塞进一个kernel,可能需要更多寄存器、更大的共享内存,或引入额外的同步等待。这些是以往分开执行时没有的成本。
- \(G > 0\)(融合条件)只有当省下的访存大于新增开销时,融合才值得做。
AI 应用场景
大模型推理中的Softmax融合
阶段:模型推理引擎优化
解决的问题:Transformer的attention模块中,softmax需要对QK^T的结果逐行做max、sum归约和归一化。传统实现把整行中间矩阵写回显存再读回,在序列长度大时显存占用和带宽开销双双翻倍。融合后,整行softmax在一个kernel内完成,中间矩阵只在片上流动。
具体案例:在某Triton推理服务中,对LLaMA-7B的attention启用softmax融合后,当KV序列长度为4096时,KV缓存区间的显存压力降低约30%,单token延迟从8.2ms降到6.9ms。
推荐系统Embedding求和
阶段:训练与推理的前向计算
解决的问题:CTR模型需要把一个用户的上百个特征embedding向量逐位相加(本质是“逐元素累加 + 归约”),再送入MLP。非融合实现要先写回一个N维求和向量再二次读取。融合后,求和向量直接留在寄存器里喂给下一层,避免中间向量落地。
具体案例:某广告平台平均每个用户特征ID为512个、embedding维度64,启用Embedding求和融合后,在线推理吞吐量提升约1.8倍。
多卡梯度归约优化
阶段:分布式训练梯度同步
解决的问题:AllReduce中的Reduce阶段要把各卡的梯度张量求和。传统实现先归约写回一次,再广播读一次。将归约与后续广播融合,可省去一次全量全局内存往返。
具体案例:千卡集群训练GPT类模型时,单次同步的梯度量约1GB,融合后AllReduce的带宽压力减少约30%,训练吞吐提升约12%。
Python 演示
# -*- coding: utf-8 -*-"""简化版Reduce融合识别与访存收益估算""" class FusedReducePlanner: """模拟编译器的融合规划器""" def __init__(self, n): self.n = n # 元素总数 def is_pointwise(self, op): return op["kind"] == "pointwise" def is_reduce(self, op): return op["kind"] == "reduce" def traffic(self, op): return self.n if op["kind"] == "pointwise" else 1 def find_fusable_pairs(self, ops): """逐元素算子后紧跟归约算子 -> 可融合""" pairs = [] i = 0 while i < len(ops) - 1: if self.is_pointwise(ops[i]) and self.is_reduce(ops[i + 1]): pairs.append((ops[i], ops[i + 1])) i += 2 else: i += 1 return pairs def report(self, ops): naive = sum(self.traffic(op) for op in ops) + self.n pairs = self.find_fusable_pairs(ops) fused_set = set() fused = self.n for a, b in pairs: fused_set.add(id(a)) fused_set.add(id(b)) fused += 1 # 融合后只写一个归约结果 for op in ops: if id(op) not in fused_set: fused += self.traffic(op) save = naive - fused print("未融合方案访存量: %d 次" % naive) print("发现可融合算子对: %d 对" % len(pairs)) print("融合后访存量: %d 次" % fused) print("节省访存: %d 次, 占比 %.1f%%" % (save, save * 100.0 / naive)) def main(): n = 1024 * 1024 ops = [ {"name": "scale", "kind": "pointwise"}, {"name": "relu", "kind": "pointwise"}, {"name": "sum", "kind": "reduce"}, {"name": "sigmoid", "kind": "pointwise"}, {"name": "mean", "kind": "reduce"}, {"name": "broadcast_add", "kind": "pointwise"}, ] planner = FusedReducePlanner(n) planner.report(ops) if __name__ == "__main__": main()
self.n = n # 元素总数记录数据规模N,所有访存量估算都以它为基数。def find_fusable_pairs(self, ops):编译器pattern匹配的核心:遍历算子序列,找“逐元素后面紧跟归约”的组合。if self.is_pointwise(ops[i]) and self.is_reduce(ops[i + 1]):一条融合规则:逐元素算子(涂干燥剂)后是归约算子(堆柴),二者可合并。fused += 1 # 融合后只写一个归约结果融合对不再写出N个中间结果,只写最后的归约结果。save = naive - fused计算节省的访存量,对应公式 \(2N\) 的“柴棚搬运”。print("节省访存: %d 次, 占比 %.1f%%" % (save, save * 100.0 / naive))输出收益百分比,帮助编译器决定是否执行融合。
常见误区
误区看到逐元素算子和归约算子挨在一起就进行融合,以为融合是万能加速手段。既然拆掉柴棚肯定省时间,那所有能融合的都该融合。
正解融合是否有效取决于算子的瓶颈类型。访存密集的算子(memory-bound)融合收益巨大;但计算密集的算子(compute-bound)如大矩阵乘法后的归约,融合可能把并行粒度打散,反而让线程空闲等待。真实编译器使用roofline模型判断:只有当访存是瓶颈且融合后省下的搬运量大于额外开销(寄存器压力、同步等待)时,融合才划算。用小林的例子说:如果柴火本身就扔在篝火边,那“柴棚”根本不存在,拆也无从谈起。
为何常见:教程和论文里展示的融合案例基本都是访存密集场景,给读者留下“融合=加速”的刻板印象。
误区Reduce就是把一堆数加在一起,用一个线程写个循环,从第一个加到最后一个,简单粗暴,没必要搞什么树形归约。
正解这样做在GPU上是大忌。100万个元素,单线程要串行100万步;而1000个线程并行树形归约,每个线程累加1000个元素后再用约10轮合并即可完成,总步数在2000步左右,相差约500倍。GPU的优势在于并行,归约算法必须让所有线程“各堆各的柴,再合力合堆”。
为何常见:CPU上写串行for循环是编程常识,把CPU的惯性思维搬到GPU上,就容易忽视并行算法设计。
误区把逐元素算子的代码和归约算子的代码前后写在同一个kernel函数里,中间结果还是写入全局内存,再读回来——以为这样就算完成了“融合”。
正解融合的精髓在于中间结果不落全局内存。如果两个“合并”的代码段之间,中间张量依然完整写入又读回显存,那只是函数级合并,访存开销一点没省,是“伪融合”。真正的融合要让中间结果停留在寄存器或共享内存中,甚至根本不存在独立的中间张量。
为何常见:“融合”这个词容易让人望文生义,以为就是代码合并,忽略了它本质上是内存访问优化。
误区反正只有一个kernel,数据都在内部流转,块内共享内存的读写顺序天然正确,不用加`__syncthreads()`之类的同步指令。
正解恰恰相反,归约是最依赖同步的算子之一。块内多线程并行写共享内存后,其他线程立刻读取进行下一轮归约时,必须用同步指令保证所有线程都写完。此外,块间的归约结果汇总也需要通过原子操作或二次kernel协调。融合改变的是中间结果的存放位置,不会消除并行计算固有的同步需求。
为何常见:把“kernel数量减少”误解为“同步开销消失”,忽略了GPU线程间并行的协作约束。
面试题
Q1. Reduce融合最主要的目标是什么?
- A.减少内核代码行数
- B.消除中间存储和重读← 正确答案
- C.降低数值计算精度
- D.增加并行线程数量
答案:B。Reduce融合的核心是消除中间结果的显存写读——就像堆篝火不必先把柴搬进柴棚再搬出来。它不改变计算逻辑,也不减少代码量,而是让数据“少搬家”。
Q2. 对N个元素做两两配对的树形归约,大约需要多少轮?
- A.需要N减1轮
- B.需要N除以2轮
- C.约log2N轮← 正确答案
- D.需要N整轮
答案:C。树形归约每轮数量减半,所以轮数是对数级的 \(\log_2 N\)。比如16根柴大家两两配对堆,只需4轮就合成一堆。N-1是串行累加的轮数,N/2是单轮并行能合并的数量。
Q3. 以下哪组算子组合适合进行Reduce融合?
- A.卷积后接逐元素算子
- B.缩放后接求和算子← 正确答案
- C.矩阵乘后接矩阵乘
- D.拼接后接切割算子
答案:B。缩放(逐元素)后面接求和(归约),中间结果没有其他消费者,典型的“涂干燥剂→堆柴”组合。卷积后接逐元素属于普通的逐元素融合而非归约相关;矩阵乘接矩阵乘是GEMM融合;拼接接切割是结构变换,都与归约无关。
Q4. Reduce融合通常能带来哪些收益?
- A.减少内核启动次数← 正确答案
- B.消除中间显存占用← 正确答案
- C.提高缓存命中率← 正确答案
- D.必然降低计算延迟
答案:ABC。融合把多个kernel合并成一个,启动次数减少;中间张量不落显存,显存占用下降;数据在寄存器/共享内存间流转,缓存命中率提升。但“必然降低延迟”过于绝对——计算密集场景下融合可能因并行度下降反而变慢。
Q5. GPU上实现跨线程块归约时,哪种方式在大规模数据下冲突更小?
- A.全部交给原子加指令
- B.块内树形归约加原子← 正确答案
- C.每个线程做原子操作
- D.单线程块串行归约
答案:B。全部用原子操作会让大量线程同时争抢同一个全局地址,冲突严重。更稳妥的是“块内先用树形归约把部分和算好,再用少量原子操作让各块汇总”——朋友们先各自堆好小柴堆,再合力合并,冲突少得多。
Q6. 关于融合归约的线程块划分,哪些描述正确?
- A.分块按连续内存段取数← 正确答案
- B.共享内存协助块内归约← 正确答案
- C.块数越多性能必然越高
- D.同步指令保证读写次序← 正确答案
答案:ABD。按连续内存段分块能保证合并访问;共享内存是块内归约的高速协作区;同步指令确保共享内存写完后才被读取。“块数越多越好”是绝对化说法——块数过多会导致块间聚合开销增大,性能反而下降。
Q7. 融合实现softmax时,最合理的计算流程是哪一种?
- A.两次读入分别求最大求和
- B.先求最大再流式算指数← 正确答案
- C.随机访问一次完成两项
- D.分解成两个内核分别算
答案:B。softmax融合的正确姿势是:先归约出最大值,然后一遍流式遍历同时计算指数和累加和,最后统一相除。这样数据只需读一次,中间结果留在片上。开两个kernel或随机访问都会增加访存开销。
Q8. 什么是“广播算子”?
- A.把每个元素独立做映射
- B.将标量扩散到所有元素参与运算← 正确答案
- C.把张量广播到所有计算卡
- D.把大小张量做切分拷贝
答案:B。广播算子把归约出的一个标量(比如总和)扩散回所有元素,让每个元素都能与这个标量做运算——就像篝火的光和热均匀照亮围坐的每一个人。逐元素映射是逐元素算子的事,发送到计算卡或切分拷贝都与此无关。
Q9. 编译器IR中识别Reduce融合pattern通常需要满足哪些条件?
- A.前驱是逐元素型算子← 正确答案
- B.中间结果仅被一个算子使用← 正确答案
- C.归约输出为标量或小张量← 正确答案
- D.两算子只要相邻就一定可融合
答案:ABC。前驱必须是逐元素语义;中间结果必须是单消费者,否则融合会破坏其他路径的计算;归约输出通常是小张量才有融合价值。“只要相邻就可融合”忽略了数据依赖和消费者数量检查,编译器的pattern匹配远比“相邻”严格得多。
Q10. 当元素总数N不是2的幂次时,进行块内归约的稳妥做法是?
- A.直接丢弃余数元素
- B.补齐或串行累加尾部余数← 正确答案
- C.复制全部数据到新内存
- D.改用单个线程块执行
答案:B。余数元素不能丢,也不能全部复制到新内存(徒增开销)。更合理的做法是:对尾部剩余元素用串行循环由最后一个线程补加,或用原子操作把尾部部分和累加到结果中。绝不能像“丢柴火”一样丢掉数据。