第二章:Details 重点数据细读
本章正式进入 Details 页,这可是全部指标的大合集,被切成 14 个 section,从上往下排开,信息量比 Summary 大一个量级,本章着重讲透其中最值得细读的三个重点 section,剩下的 section 留到下一章按页面顺序补齐。
每个 section 里面是若干表格和图表,section 的末尾会挂着一些带 OPT 或 INF 标记的条目,OPT 是优化建议,INF 是信息提示,都是 ncu 的规则引擎拿采到的数据自动分析出来的。它们其实就是 Summary 页底部那三条 Prioritized Rules 的出处,规则引擎从全页的警告里挑出最要紧的几条排了序。所以读 Details 的基本顺序是,先看表格里的数据,再看 section 挂出的警告。点击 section 名称左边的小三角形,还可以展开更详细的图表信息,部分 section 名称栏的最右侧还有下拉选项框可以筛选展示更详细图表信息中的特定图表,作者也会一并讲解,再右侧的对话气泡图标按钮就是针对这个 section 写 comments。所有 comments 内容都在 comments 页面中,本合集不会使用。
把鼠标长时间悬停到某一个数据上,ncu 会弹出一个更详细的信息介绍表,可以看到这个值对应的 Raw 值叫什么名字、单位是什么、最大值平均值是多少这类更细的信息。所有 Raw 值都在 Raw 页面中,当前章节跳过。
GPU Speed Of Light Throughput
GPU Speed Of Light Throughput,是 Details 页的第一个 section,这个名称是一种化用。我们都知道光速是宇宙里的速度上限,GPU 里也有类似的机制,硬件每个单元都有各自的峰值吞吐,比如 SM 的计算能力、显存的带宽,谁也超不过自己的峰值。这个 section 里每个百分比的分母就是对应单元的理论峰值,分子是这个 kernel 实际跑到的水平,跑到 100% 就意味着已经触及到这块硬件的理论极限了,当然,实际上因为功耗约束,散热情况,环境湿度等等各种原因,我们可以认为跑到 100%是不可能的。
基本数据表
ncu 说明为:对 GPU 计算与内存资源吞吐的高层级总览,每个单元的吞吐量报告的都是它相对理论峰值的实际利用率百分比。
紧接着是 10 个数据,如下表所示:
| 项 | 值 | 说明 |
|---|---|---|
| Compute (SM) Throughput | 15.98 % | SM 是真正干计算的单元,里面分了好几条并行的管线,各管一摊:整数逻辑、浮点乘加、访存、特殊函数等等,每条管线都有各自的理论峰值,这一行把各条管线的利用率分别统计出来再取最高者,量的是计算侧最忙的那条管线 第一章中我们就见过这个项了,现在这个值属于低水平,说明计算远没有成为瓶颈 |
| Memory Throughput | 80.44 % | 内存是一整条链路,L1TEX、L2、DRAM 一级一级往外走,这一行把链路上各级的利用率取最高者,量的是整个内存子系统里最忙一级的水平 第一章中我们就见过这个项了,现在这个值属于高水平,计算侧又是低水平,说明内存侧应当成为优化的重点,我们注意到这个数值和 L2 Cache Throughput 一模一样,直接暴露出瓶颈就出在 L2 |
| L1/TEX Cache Throughput | 92.45 % | L1TEX 在每个 SM 内部,是数据进出 SM 的第一道门,全局内存的读写和共享内存的读写都要经过它,这一行把单元内各条存取通路取最高者,分母用的是活跃周期口径,92.45% 是单元整体有多忙 现在看来这个值非常高,但是它把病态二 bank conflict 反复重放的共享内存流量全算进去了,所以还需进一步分析 |
| L2 Cache Throughput | 80.44 % | L2 是整块 GPU 共享的第二级缓存,所有 SM 的数据出门都要经过它,这一行把 L2 各条通路取最高者 现在这个值非常高,它也被取作 Memory Throughput,说明内存侧它承压最重 |
| DRAM Throughput | 23.23 % | DRAM 就是真正的显存,数据最后一级,这一行从显存控制器侧统计,量的是真正走显存带宽的那部分流量 现在这个值较低,远没吃满,显存本身不是瓶颈,再和 L2 Cache Throughput 一比对,出了显存的流量不大,L2 流量很大,那么内存侧压力瓶颈就可以锁定在 GPU 内部 L2 这一级了 |
| Duration | 318.43 us | kernel 在 GPU 上实际花掉的时间,GPU 自己的计时器量出来的,不是 CPU 侧掐的表,CPU 发射 kernel、调度上都有开销,量到的只是 GPU 侧从开跑到收尾这一段 第一章中我们就见过这个项了,是相当重要的一个指标 |
| Elapsed Cycles | 443,927 | GPU 是同步数字电路,一切工作按节拍推进,时钟每走过一拍就算一拍,这个数就是这次执行里 GPU 时钟走过的总节拍数,用节拍数表述好处是不受到频率高低的影响 这一项其实就是第一章中我们见过的 cycles |
| SM Active Cycles | 437,992.54 | 每个 SM 真正有活干的时间,按节拍数统计,再对全部 SM 取平均,所以带着小数,代表 SM 群体平均在岗了多少拍 我们用这个数除以 Elapsed Cycles 得 98.7%,说明 SM 几乎全程没歇着,反过来,如果这个比例很低,多半是两种情形之一,一种是 kernel 太小,总共没几个 block,装不满第一个 wave 驻留,SM 从头就有一大半在空转,另一种是尾巴太长,最后剩下的一小部分 block 凑不齐一个完整 wave,跑着跑着大部分 SM 先干完闲了,只剩少数还在收尾,两种情形都说明并行度没有铺满 GPU,wave 这个概念指同时驻留在 SM 上的那批 block,第三章 Occupancy 那里会正式见到 |
| SM Frequency | 1.39 GHz | SM 实际跑的平均时钟频率,它不是单独的频率传感器,是拿 Elapsed Cycles 除以 Duration 得到的,节拍数除以实际花掉的时间,出来的就是这段时间里时钟平均跑多快 注意到值比 3090 标称的 boost 频率低了一截,原因可能有功耗温度管理会压频率,软件侧的指令要求锁频率等,本 kernel 计算负载不重,GPU 也没有必要把频率拉满 |
| DRAM Frequency | 9.74 GHz | 显存颗粒实际跑的平均时钟频率,它同样不是单独的频率传感器,是拿 DRAM 侧数出来的节拍数除以时间 GDDR6X 是双倍速率,拿 9.74 GHz 乘 2 约 19.5 Gbps,正是 3090 显存的标称数据速率,再乘 384 bit 位宽、除以 8,就是序章里 936 GB/s 的峰值带宽了 |
读过这十个值,计算侧 15.98% 对内存侧 80.44%,一闲一忙,瓶颈在内存侧不在计算侧,SM 五分之四的时间在等内存把数据送来,而不是在算,所以具体再看内存的链路。L1TEX 在 SM 内部,L2 全 GPU 共享,DRAM 才是真正的显存,数据越往外走越慢,瓶颈不在显存带宽上,而是卡在 GPU 内部的 L2 这一级。正常流程下一步应该分析 L2 如此繁忙的原因具体是落在哪一行代码上,可由于我们是先射箭再画靶,所以直接知道答案就是序章故意留下的病态一,非合并读把 sector 流量放大了 8 倍,多搬的流量压的正是 L1TEX 到 L2 这段 GPU 内部通路。
表格底下还挂着两条 INF 附注。第一条说:本 workload 对设备算力或内存的利用率已经超过 80.0%,想再提升性能,大概率得把活从最忙的单元挪到别的单元去,建议先到 Memory Workload Analysis section 里分析 L2。这条附注就是 ncu 在给建议,让我们去看另一个 section,正是后文会关注的。第二条说:这台设备 fp32 峰值性能与 fp64 峰值性能之比是 64:1,本 workload 达到的 fp32 峰值性能接近 0%,fp64 峰值性能为 0%。这条留到本节后部分 Roofline 图再解读。
接下来几小节,我们来关注这一 section 展开后的各图表。
GPU Throughput 横条图
这个图就是简单的把 Compute (SM) Throughput 和 Memory Throughput 两个百分比画成了柱状图:
刻度的单位就叫 Speed Of Light (SOL),意思是相对各自的光速走了多远。这张图的全部意义,就是用最直观的方式把结论展示出来,两根柱子一短一长,长柱子在哪边,瓶颈就在哪边。
GPU Throughput Breakdown 数据表
GPU Throughput 横条图下面跟着两张 Breakdown 表,就是把两个总百分比拆成了明细,两张表都按利用率从高到低排,每一行的分母是这条通路自己的理论峰值,分子是这次执行里的实际用量。
读这张表之前,需先了解一些有关的概念。
- SM 里的执行部分不是一整块万能的计算器,而是拆成了好几条专用的硬件管线(pipeline),每条管线只会干一类活,有的管整数运算,有的管浮点乘加,有的管 sin、cos 这类超越函数,还有的专门管访存指令的发包收包。一个 SM 内部还等分成 4 个 SMSP(SM sub-partition,SM 的子分区),每个 SMSP 里都有自己的执行管线和一个调度器(warp scheduler),驻留在 SM 上的 warp 也分摊给 4 个 SMSP,调度器的职责就是发射指令,每个周期以自己手头的某一个 warp 为目标,向其发射出一条指令,每周期最多发一条,即有一个发射槽。一条指令发射出去之后,按类型派到对应的管线上去执行,整数指令走整数管线,访存指令走访存管线,各进各的门。发射了却不等于马上执行完,访存指令可能因为共享内存 bank conflict 这类原因要拆成几拍重放,每重放一次就再占掉一个发射槽,只有做完的那一次才记成执行,所以发射和执行是两个角度。派活的路也不止一条,调度器除了把指令发给本分区的各条管线,还会把一部分指令交给 MIO(memory input/output),MIO 是内存输入输出单元,负责排队把指令转发给 LSU、TEX、IDC、CBU 这些整个 SM 共享的单元,MIO 上存放寄存器操作数的队列就叫 MIO PQ。命名上还要分清管线和单元,管线(pipe)是给执行单元送指令的通道,单元(unit)才是真正干活的硬件,指令由管线送进单元去执行,以 XU 为例,XU 管线负责把超越函数和类型转换这类指令送进 XU 单元,官方给 XU 单元的全名是 Transcendental and Data Type Conversion Unit,超越函数与类型转换单元,LSU、ADU、CBU 这些官方也都叫 unit,只有 FMA 官方直接叫管线,还有 MIO、IDC 这类名字里直接就是单元的。还有一点要注意,这些管线是并行干活的,同一个周期里整数管线和访存管线可以各自都在忙,所以各条管线之间不是分蛋糕的关系,加起来也不必等于 100%。
- 内存侧也有一套要先认识的部件。先看 L2,L2 由多个 slice 组成,slice 是 L2 的分区,整块缓存被拆成若干片,各管一部分地址的数据。每个 slice 内部分 Tag、Miss、Data 三级,一条请求进来先到 Tag 级查 tag,tag 是缓存行的唯一钥匙,缓存行是缓存登记数据的基本单位,L1 和 L2 的缓存行都是 128 字节,tag 对上了说明数据就在这块缓存里,叫命中,对不上叫缺失,交给 Miss 级处理,Miss 级负责把缺失的请求转发给下一级,存储是分层次的,从 L1TEX、L2 到显存一级一级往外垫,越往外容量越大、速度越慢,对 L2 来说,下一级就是显存 DRAM,转发之后 Miss 级就等显存把数据送回来补进缓存,Data 级则是数据真正进出的地方,命中就直接从这里取数,没命中的话,等下一级把数据搬回来,先写进 Data 级存下,再由 Data 级移交给请求方,这一步就叫回填。slice 和 L1TEX、显存之间互发数据包,都要经过 XBAR(交叉开关),XBAR 是一张交换网络,负责把数据包从源单元搬到指定的目标单元。再看 L1TEX 这头,L1TEX 里的数据存储和共享内存共用同一片 SRAM,按 bank 这套并行通路组织,把存储拆成 32 条各自能独立服务 thread 的小通路,一个字落在哪条 bank 上是固定的,序章讲 bank conflict 时细说过。流量按 sector 计数,一个 sector 是缓存行或显存里对齐的 32 字节小块,一条 128 字节的缓存行正好装 4 个 sector,序章讲全局内存以 32 字节为最小搬运单位时讲的就是这个概念,判定命中要求 tag 在、数据也在,tag 在而数据不在,同样算缺失。最后是 wavefront(工作包),一条请求在管线里处理到末端,也就是即将动手搬数据的那一步,硬件会把请求打包成至少一个 wavefront,包里的活一个周期并行做完,不同的包之间排队串行、各占一个周期,请求要搬的数据越宽越散,拆出来的包往往越多。
先看左边的 Compute Throughput Breakdown,把计算侧拆到各条管线和单元:
| 项 | 值 | 说明 |
|---|---|---|
| SM: Inst Executed Pipe Lsu | 15.98 % | LSU 是 Load Store Unit,访存指令的专用单元,管线是给单元送指令的通道,本行统计的就是把访存指令送进 LSU 单元的这条管线,load、store、原子、归约指令都由 LSU 发给 L1TEX,全局、本地、共享三种空间都管,顺带还发 S2R 这类特殊寄存器读取、shuffle 和 CTA 级 barrier 指令,值是拿 LSU 管线执行的指令总数除以总周期数,得到平均每周期执行几条,再除以这条管线每周期最多能执行的条数,得到百分比 计算侧 LSU 是最忙的一条管线,主表 Compute (SM) Throughput 的 15.98% 取的就是这一行 |
| SM: Issue Active | 14.13 % | 这一行统计的是发射槽被真正用掉的周期占比,算法是发射槽被占用的周期数除以总周期数,量的是"发指令"这件事有多忙,和下一行"执行指令"是两个角度 14.13% 说明调度器只有百分之十几的节拍真发出了指令,其余节拍的窗口都空着,这是因为调度器手底下的 warp 都在等东西,等显存返回的数据,等共享内存 bank conflict 的重放,等 barrier 处的队友,具体各种原因各占多少,第三章 Warp State Statistics 里细算 |
| SM: Inst Executed | 14.11 % | 真正执行完成的指令速率,和上面的 Issue Active 是两个角度,一个管发射一个管执行,算法是拿执行完的指令总数除以总周期数,得到平均每周期执行几条,再除以每个 SMSP 每周期 1 条的上限,得到百分比,发射与执行的区别、重放要再占发射槽,这些前面概念里已经说过 和 Issue Active 只差 0.02,说明发出去的指令基本一次就做完了,差出来的全是重放开销 |
| SM: Pipe Alu Cycles Active | 11.84 % | ALU 管线,管大部分位操作和逻辑指令,也执行 IMAD、IMUL 以外的整数指令,在 Ampere 上还负责 FP32 与 FP16 之间的快速转换,算法是管线处于活跃状态的周期数除以总周期数 访存地址计算这类整数活都记在 ALU 管线头上,11.84% 就是说每个 SMSP 的 ALU 管线有 11.84% 的节拍在转,本 kernel 里就是算下标、算地址这些活 |
| SM: Pipe Fmaheavy Cycles Active | 8.82 % | FMA Heavy 管线,干 FP32 的 FADD、FMUL、FMAD,FP16 的 HADD2、HMUL2、HFMA2,还有整数乘法 IMUL、IMAD 和整数点积,GA10x 把这类活拆给 FMA 和 FMAHeavy 两条管线分着干,算法是管线活跃周期除以总周期数 本 kernel 没有浮点运算,这 8.82% 干的都是整数乘加的活,下标计算这类 IMAD 都归 FMA Heavy 管线 |
| SM: Inst Executed Pipe Xu | 8.67 % | XU 单元的官方全名是 Transcendental and Data Type Conversion Unit,超越函数与类型转换单元,sin、cos、rsqrt 这类特殊函数和整型与浮点之间的类型转换都在这个单元里执行,XU 管线负责把这类指令送进单元,值是拿 XU 管线执行的指令总数除以总周期数,得到平均每周期执行几条,再除以这条管线每周期最多能执行的条数,得到百分比 本 kernel 里 sin、cos 这类运算一个都没有,这些活全是病态三带来的,树形归并里要对变量求余,编译器没法把对变量求余优化成位运算,只能按整数除法展开成 I2F(整数转浮点)、MUFU.RCP(浮点求倒数)、F2I(浮点转回整数)再配整数乘法回代,XU 单元的活就是这么来的,8 轮归并里每轮每个 thread 都要做一遍 |
| SM: Pipe Fma Cycles Active | 5.11 % | FMA 管线,和 FMAHeavy 管线干的是一类活,在 GA10x 上 FMA 是一条逻辑管线,用来表示峰值算力的口径,算法是管线活跃周期除以总周期数 5.11% 和 FMAHeavy 的 8.82% 合起来就是整数乘加的全部工作量,两条管线分摊 |
| SM: Inst Executed Pipe Adu | 4.33 % | ADU 是地址分歧单元,负责分支和跳转的地址分歧处理,还捎带常量加载和 block 级 barrier 指令的支持,值是拿 ADU 管线执行的指令总数除以总周期数,得到平均每周期执行几条,再除以每周期最多能执行的条数,得到百分比 本 kernel 的分支没有分歧,这 4.33% 基本都是 block 内 thread 同步的 barrier 攒出来的,树形归并每轮归并一次就要 barrier 一次 |
| SM: Mio Pq Read Cycles Active | 2.71 % | MIO 的结构和 MIO PQ 的来历都在前面概念里介绍过了,这一行统计的是 PQ 把操作数送往管线的活跃周期,是队列的出口,算法是队列活跃的周期数除以总周期数 2.71% 说明这条队列没堵 |
| SM: Mio Pq Write Cycles Active | 2.71 % | 本行和上面的 MIO PQ Read Cycles Active 统计同一条 MIO PQ 的两端,Read 那一行是队列的出口,本行是队列的入口,统计的是寄存器堆把操作数写进 PQ 的活跃周期,算法都是各自的活跃周期数除以总周期数 两个数一模一样,进和出完全平衡 |
| SM: Mio2rf Writeback Active | 2.70 % | 访存结果从 MIO 写回寄存器文件的通路,nvprof 时代算 SM 利用率用的就是这条通路,可见是个汇总性质的通道,算法也是活跃周期数除以总周期数 2.70% 和上面 MIO PQ 的两行几乎相等,三个数在说同一段通路 |
| SM: Inst Executed Pipe Cbu Pred On Any | 0.20 % | CBU 是 Convergence Barrier Unit,汇聚屏障单元,负责 warp 级的汇聚、barrier 和分支指令,Pred On Any 是任意线程谓词生效的执行口径,算法和 ADU 管线那行一样:指令总数除以总周期数,再除以每周期最多能执行的条数 block 内 thread 同步的活也归到这条名下,0.20% 很低,barrier 指令本身不多 |
| SM: Inst Executed Pipe Uniform | 0.09 % | Uniform Data Path,标量数据通路,执行那些所有线程用同一份输入、产生同一份输出的指令,算法和 ADU 管线那行一样:指令总数除以总周期数,再除以每周期最多能执行的条数 0.09%,本 kernel 这类指令很少 |
| SM: Pipe Tensor Cycles Active | 0 % | Tensor 管线,执行各种 MMA 矩阵乘加指令,注意 Tensor 管线和 Tensor Core 管线是两个东西 本 kernel 没有矩阵运算 |
| SM: Pipe fp64 Cycles Active | 0 % | 双精度浮点管线,专门执行 double 类型的浮点运算,在 GA10x 上双精度峰值只有单精度的 1/64 本 kernel 没有 double 运算 |
| SM: Memory Throughput Internal Activity | 0 % | SM 侧内存活动的内部口径,量的是 SM 内部内存处理活动有多忙,值是内部活动的速率除以相应的理论峰值 本 kernel 这一项值为 0,说明 SM 内部没有独立于 L1TEX 之外的内存活动 |
| IDC: Request Cycles Active | 0 % | IDC 是 InDexed Constant Cache,索引式常量缓存,SM 里用寄存器做索引去查常量的那个缓存,算法是请求活跃的周期数除以总周期数 本 kernel 没有用寄存器做索引的常量访问,用不上 IDC,值为 0 |
| SM: Instruction Throughput Internal Activity | 0 % | 指令吞吐的内部活动口径,量的是 SM 内部指令处理的繁忙程度,值是内部活动的速率除以相应的理论峰值 本 kernel 的指令都分散在上面各条管线里,没有额外的内部活动,值为 0 |
| SM: Inst Executed Pipe Tex | 0 % | 纹理管线,把纹理和 surface 指令转发给 L1TEX 的 TEXIN 入口,算法是执行的指令总数除以总周期数,再除以每周期最多能执行的条数 本 kernel 不碰纹理,值为 0 |
| SM: Inst Executed Pipe Ipa | 0 % | IPA 这条管线,作者查遍了手头的资料,NVIDIA 论坛贴出的官方指标参考表里只写了一句"# of warp instructions executed by ipa pipe",IPA 管线执行的 warp 指令数,缩写代表什么只字未提,论坛里有人专门发帖问各条管线的含义,也没有得到官方回复,作者就不懂装懂了 本 kernel 没有指令派到这条管线上,值为 0,它具体管什么并不影响这份报告的解读 |
右边的 Memory Throughput Breakdown,把内存链路拆到了 L1TEX、L2、DRAM 各级:
| 项 | 值 | 说明 |
|---|---|---|
| L2: T Sectors | 80.44 % | L2 的每个 slice 内部分 Tag、Miss、Data 三级,T 指的是 Tag 级,所有从 L1TEX 过来的请求都要先在这里查 tag,tag 是缓存行的钥匙,查到了才知道数据在不在 非合并读把 sector 流量放大了 8 倍,压力全兑现在这一级,主表 Memory Throughput 的 80.44% 取的就是这一行,真正的瓶颈 |
| GPU: Compute Memory Access Throughput Internal Activity | 80.44 % | 整条内存链路的汇总行,把各级取最高 数值和主表的 Memory Throughput 相等,把它放进 Breakdown 就是给大家看总数字的来历 |
| L2: T Tag Requests | 80.43 % | Tag 级按查找次数统计,值是拿 tag 查找的总次数除以总周期数,得到平均每周期查多少次,再除以这条通路每周期最多能处理的查找次数,得到百分比 和上一行只差 0.01,两项统计都挤在 Tag 级这一关 |
| L2: Lts2Xbar Cycles Active | 80.41 % | L2 slice 往 XBAR 方向的活跃周期,XBAR 是交叉开关,负责把数据包从源单元搬到目标单元,L2 和外界的一切往来都要经过它 80.41% 说明 L2 往外的路口几乎没闲过 |
| L2: Xbar2lts Cycles Active | 78.17 % | XBAR 往 L2 slice 方向的活跃周期 78.17%,L2 内外的通路都接近打满 |
| L1: Data Pipe Lsu Wavefronts | 56.14 % | L1TEX 的 Data 级上 LSU 方向的 wavefronts,wavefront 是请求处理到末段生成的"工作包",一个包里的活并行做,不同包之间排着队串行做 全局和共享内存的数据都从这条管道过,56.14% 是全局数据管道的真实水平 |
| L1: M Ltex2Xbar Req Cycles Active | 46.23 % | L1TEX 的 Miss 级,标签里的 M 指的就是 Miss 级,请求在 L1 没命中,就得出门去 L2 这一行统计 L1TEX 往 XBAR 发请求方向的活跃周期,46.23% 说明出门的路也不轻快 |
| L1: M Xbar2ltex Read Sectors | 46.18 % | Miss 之后从 L2 读数据回填 L1TEX,按 sector 统计 和上一行一起正是一去一回 |
| DRAM: Cycles Active | 23.23 % | 显存侧的活跃周期,L2 和 DRAM 颗粒之间坐着一排显存控制器,数据在显存和 L2 之间往返都要经过它们 23.23% 是它们有多忙,数值和主表的 DRAM Throughput 一致 |
| L2: D Sectors | 22.69 % | L2 的 Data 级,D 指的就是 Data 级,对着显存方向进出 L2 的 sectors 量的是 L2 真正往显存搬了多少货,22.69% 和 Tag 级的 80.44% 一比,大头都堵在 Tag 级,出了 Data 级的流量不大 |
| L1: Lsu Writeback Active | 21.27 % | LSU 方向的数据写回通路,从全局和共享内存读回来的数据都要经这条通路交还给寄存器 21.27%,回程的路也没空着 |
| DRAM: Dram Sectors | 20.72 % | 显存颗粒侧按 sector 统计 和 DRAM Cycles Active 一个量级,两行互相印证 |
| L1: Lsuin Requests | 15.98 % | Lsuin 是 Load/Store input,L1TEX 收 LSU 请求的入口,按请求数统计 数值和计算侧 LSU 管线那一行一模一样,一头发的指令,一头收的请求,正好对上 |
| L2: D Sectors Fill Device | 10.06 % | 从显存往 L2 回填数据的 sectors,fill 指的是回填缓存行,device 指显存 10.06%,回填的量不大,和 L2 命中率 87.53% 呼应,大部分请求不用回填 |
| L1: Data Bank Writes | 6.68 % | L1 数据存储 bank 的写活动 6.68% 并不高 |
| L1: Data Bank Reads | 1.62 % | L1 数据存储 bank 的读活动 1.62% 并不高,和 L1: Data Bank Writes 一起看,认为 L1 的压力主要不在 bank 上 |
| L1: Texin Sm2tex Req Cycles Active | 0.25 % | TEXIN 是 L1TEX 收纹理请求的入口,SM 往纹理单元发请求走这里 本 kernel 不碰纹理,只剩零头 |
| L2: D Sectors Fill System | 0.00 % | 从系统内存往 L2 回填的 sectors,system 指主机内存 这一项是 0,说明这次执行没有任何数据跨越 PCIe 去主机内存兜圈子,数据全在显存里 |
| L1: Data Pipe Tex Wavefronts | 0 % | L1TEX 数据级上纹理方向的 wavefronts 0,不碰纹理就是 0 |
| L1: T Wavefronts | 0 % | L1TEX Tag 级上纹理路径的 wavefronts 同样是 0 |
| L1: Tex Writeback Active | 0 % | 纹理方向的数据写回通路,也为 0 这三行连成一片的 0 在说同一件事:纹理这条支路整个是闲的 |
| L2: D Atomic Input Cycles Active | 0 % | 原子操作从显存方向进入 L2 的通路 0,本 kernel 没用任何原子操作 |
| GPU: Compute Memory Request Throughput Internal Activity | 0 % | 汇总行的另一个口径,按请求统计 本报告里它是 0,活动都统计在 sector 口径的那条汇总行上 |
从这张表读出 Compute (SM) Throughput 和 Memory Throughput,计算侧取 LSU 管线的 15.98%,内存侧取 L2 T Sectors 的 80.44%。DRAM 瓶颈的样例里总数字等于 DRAM 通路的值,L2 瓶颈的样例里就等于 L2 的值。
需注意主表里 L1/TEX Cache Throughput 是 92.45%,Breakdown 里 L1 的 Data Pipe Lsu Wavefronts 却只有 56.14%。这是因为 UI 标签把这块单元写成带斜杠的 L1/TEX,规则原文和指标名里写作 L1TEX,是同一个东西,正文统一用 L1TEX。两个数的口径不一样,92.45% 度量的是 L1TEX 单元整体有多忙,把共享内存的访问也算在内,本报告病态二的 bank conflict 反复重放,共享内存的流量不小,都记在 L1TEX 头上;56.14% 只是全局数据那一条管道的利用率。找内存瓶颈的时候,以 Memory Throughput 的 80.44% 为准。
GPU Speed Of Light Rooflines
Breakdown 表再往下是一系列 Roofline 图,算是 GPU Speed Of Light Throughput 的延伸。这一排一共五张图加一张表,在开始之前,我们需要认识一下什么是 Roofline 图。这类图在 UI 里统称 Rooflines,图的主体叫 Roofline(屋顶线),斜线与横线的交接点叫 Ridge Point(屋脊点),kernel 的实测落点叫 Achieved Value。
Roofline 图长得像一间屋子的屋顶剖面:左边一段斜线往右上爬,爬到 Ridge Point 后接一段横线打平。Roofline 图的横轴是算术强度,单位 FLOP/byte,算法是拿 kernel 实际完成的浮点运算次数除以 kernel 实际搬运的字节数,即平均每搬运一个字节的货摊上几次浮点运算,这个比值衡量一个 kernel 是偏算还是偏搬,比值越小说明搬得多算得少,比值越大说明算得多搬得少。Roofline 图的纵轴是性能,单位 FLOP/s,就是 kernel 每秒实际完成的浮点运算次数。坐标轴都是对数坐标。左边斜线部分瓶颈是搬运:这个区间里计算单元算得飞快,数据搬过来立马就算掉了,算力是闲的,性能完全跟着搬运走,随着搬运量提升计算次数也跟着提升,性能和算术强度成正比,画出来就是一条斜线,斜率就是显存带宽,显存每秒能搬多少字节,性能每秒就能涨多少 FLOP;右边横线部分瓶颈是算力:数据管够,性能顶到了计算单元的峰值算力,到了上限线就平了,数据再多每个周期也只能算这么多;两段的交接点即 Ridge Point,Ridge Point 的横坐标就是刚好把显存带宽喂满所需要的最低算术强度。
第一张 Floating Point Operations Roofline 是总图,把 fp32 和 fp64 两个屋顶画在了一起。先看两条屋顶线,水平线较高的是单精度 fp32 的屋顶,横线的高度是单精度峰值算力,由 FFMA 指令的每周期吞吐量乘 2 再乘 SM 频率得到,一条 FFMA 一次就算出 a×b+c 里的两个浮点运算,所以要乘 2;下面那个是双精度 fp64 的屋顶,横线高度是双精度峰值算力,3090 上双精度算力只有单精度的六十四分之一,所以整个屋顶被压低很多。屋顶线上标着蓝色小方块,是重要点的标记,打在 Ridge Point 和斜线最左端的起点。图中有一个显眼的青色圆点,是本 kernel 自己的 Achieved Value,圆点在图上的位置就是本 kernel 在这台机器上的处境,横坐标是实测算术强度,拿本 kernel 实际完成的浮点运算次数除以实际从显存搬运的字节数,纵坐标是实测浮点性能。鼠标悬浮在图上的某个位置,会弹出一个细节窗口,具体显示鼠标所在位置的坐标数值。摁住鼠标左键在图上拖出一个框选范围,可以将框选范围针对性放大,或者通过图右上角的按钮进行缩放和视图重置操作。本 kernel 几乎不做浮点运算、只顾搬数据,圆点离 Ridge Point 差着数量级,性能也远低于天花板。
第二张 Double Precision,这张图其实就是总图里只关注 fp64 的图,但它把双精度拆得更细了,变成了按照内存层级来拆,一个精度拆出了三条屋顶线,分别拿 L1TEX、L2、DRAM 三级的理论字节带宽当分母,斜线三条互相平行,L1TEX 的最左、L2 居中、DRAM 最右,判断依据是 Ridge Point 的横坐标等于峰值算力除以带宽,L1TEX 带宽最大所以 Ridge Point 最靠左,DRAM 带宽最小 Ridge Point 最靠右;三条屋顶线的峰值完全相同,都是 DFMA 的峰值算力,故横线出现了重叠。DFMA 是双精度的乘加指令,fused multiply-add 的 double 版本,一条 DFMA 一次算出 a×b+c 里的两个双精度运算,前面 fp32 屋顶那条横线的高度就是由对应的单精度指令 FFMA 乘 2 得到的,双精度屋顶同理。本 kernel 里没有一条 double 运算,所以说这张图上没有任何一个 Achieved Value 点。
第三张 Half Precision,这张图其实就是总图里只关注 fp16 的图,按内存层级拆的,L1TEX 最左、L2 居中、DRAM 最右,屋顶的高度换成了 fp16 的峰值算力,斜率换成了 L1TEX、L2、DRAM 三级各自的带宽,和前一张同理。本 kernel 里也没有一条 fp16 运算,所以说这张图上同样没有任何一个 Achieved Value 点。
第四张 Single Precision,这张图其实就是总图里只关注 fp32 的图,按内存层级拆,L1TEX 最左、L2 居中、DRAM 最右。本 kernel 里用的浮点全部是 fp32,这张图上也出现 Achieved Value 点了,而且是三个彩色的点,这三个点是同一个 kernel 的实测性能,分别拿内存链路三级各自的实测流量当分母算出来的:拿 L1TEX 的流量算,分母最大,算术强度最低,点落在最左边;拿 L2 的流量算,落在中间;拿 DRAM 的流量算,分母最小,算术强度最高,落在最右边,位置正好和总图里那个点重合。浮点性能相同,流量却一级比一级少,三个点从左到右把 L1TEX、L2、DRAM 的差距直接画在了图上,最左和最右两点的水平距离就是流量放大了多少倍。
第五张 Tensor Core Operations Roofline,这张图是针对 Tensor Core 的,和前面浮点图的读法一模一样,只是单位从 FLOP 换成了 OP,意为 operation,矩阵乘加算一次记两个运算。这张图里其实定义了 42 条屋顶线,14 条 Tensor 运算路径每条拆出 L1TEX、L2、DRAM 三级,但各路径的 Tensor 运算量不为零才画出来。本 kernel 一条 Tensor 运算都没做,42 条屋顶线一条都不画,所以图就是空的了。
Tensor Core Operations Roofline 的下面还附着一张数据表,交待 Tensor Core 图里 Rooflines 的来历,每一行是一条 Tensor Core 的运算路径,Src 是送进 Tensor Core 的数据格式,Dst 是算完吐出来的格式,Sparsity 是 Ampere 引入的结构稀疏开关,压缩格式是 2:4,要求每 4 个元素至少 2 个是零,Tensor Core 只算非零值,开了它峰值直接翻倍。
本 kernel 没用过 Tensor Core,记执行量的那几列(执行次数、折合每周期、每秒)从上到下全是 0,Peak % 一列也是 0,表里就剩峰值两条列有数:
| 项 | Peak Operations/Cycle | Peak Operations/s | 说明 |
|---|---|---|---|
| Src:bf16 Dst:fp32 Sparsity:off | 41,984 | 58,365.64 | bf16 是深度学习常用的半精度格式,1 符号位、8 指数位、7 尾数位,指数位比 fp16 多,动态范围大,算完以 fp32 输出,这一行是不开稀疏的基准档 |
| Src:bf16 Dst:fp32 Sparsity:on | 83,968 | 116,731.27 | 同一条路径开了稀疏,峰值正好翻倍,后面每对 off/on 两行都是这个关系 |
| Src:fp16 Dst:fp16 Sparsity:off | 83,968 | 116,731.27 | 输出也从 fp32 换成 fp16,写回位宽省一半,峰值比 bf16→fp32 高一档 |
| Src:fp16 Dst:fp16 Sparsity:on | 167,936 | 233,462.55 | 开稀疏再翻倍,浮点路径里的最高峰 |
| Src:fp16 Dst:fp32 Sparsity:off | 41,984 | 58,365.64 | 和 bf16→fp32 同吞吐,输入换成 fp16 不吃亏 |
| Src:fp16 Dst:fp32 Sparsity:on | 83,968 | 116,731.27 | 稀疏翻倍 |
| Src:fp64 | 289.54 | 402.52 | 全表最低,Tensor Core 对双精度的支持非常有限,fp64 的活有专门的 fp64 管线 |
| Src:int1 | 1,343,488 | 1,867,700.37 | 全表最高,1 bit 的二值网络路径,位宽越小 Tensor Core 越能吞 |
| Src:int4 Sparsity:off | 335,872 | 466,925.09 | 4 bit 整数路径 |
| Src:int4 Sparsity:on | 671,744 | 933,850.18 | 稀疏翻倍 |
| Src:int8 Sparsity:off | 167,936 | 233,462.55 | 8 bit 整数路径,量化推理的常用档 |
| Src:int8 Sparsity:on | 335,872 | 466,925.09 | 稀疏翻倍 |
| Src:tf32 Dst:fp32 Sparsity:off | 20,992 | 29,182.82 | tf32 是 Ampere 新加的格式,1 符号、8 指数、10 尾数共 19 位,输入时从 fp32 截断而来,算完仍以 IEEE fp32 输出,训练里常见的折中档,全表最低的浮点路径 |
| Src:tf32 Dst:fp32 Sparsity:on | 41,984 | 58,365.64 | 稀疏翻倍 |
小结一下这张表,表里值的量级差了四个数量级,从 fp64 的 402 到 int1 的一百八十多万,一条路径的位宽越窄,Tensor Core 一个周期能吞的运算就越多,这些天花板线的高低全由位宽决定。
Memory Workload Analysis
这个 section 回答内存到底忙在哪、闲在哪,是整个 Details 页的第四个 section。
基本数据表
ncu 说明为:对 GPU 内存资源的详细分析,当所涉及的硬件单元被完全占满(对应 Mem Busy)、单元之间的通信带宽被耗尽(对应 Max Bandwidth)、或者发射访存指令的吞吐被拉到上限(对应 Mem Pipes Busy)时,内存就可能成为整个 kernel 性能的瓶颈。
紧接着是 9 个数据,如下表所示:
| 项 | 值 | 说明 |
|---|---|---|
| Memory Throughput | 217.22 Gbyte/s | Memory Throughput 是显存颗粒进出的字节速率,读和写两个方向都算在内,这张表最核心的数就是它,算法是拿 GPU 的显存控制器每秒实际传输的字节数统计出来,单位直接就是 Gbyte/s 217.22 GB/s 就是本 kernel 每秒从显存搬了这么多货,拿它除以序章列过的峰值带宽 936 GB/s 得 23.2%,和 GPU Speed Of Light 里 DRAM Throughput 的 23.23% 正好对上,一个说比例一个说流量,两张表互相咬合 |
| Mem Busy | 80.44 % | Mem Busy 是内存链路上的硬件单元被占满的程度,算法是按访问量统计,把链路各级的利用率取最高 80.44% 说明压力最重的那一级已经占满八成,数值和 GPU Speed Of Light 的 Breakdown 里那行 GPU: Compute Memory Access Throughput 的 80.44% 一字不差,同一个数在两张表里露面,压力集中在 L2 |
| Max Bandwidth | 80.41 % | Max Bandwidth 是单元之间通信带宽被耗尽的程度,算法是按请求量统计,同样把链路各级取最高 80.41% 说明最挤的那条通路也到了八成,数值和 GPU Speed Of Light 的 Breakdown 里 L2 往 XBAR 那条的 80.41% 一致,访问和请求两个口径都指向同一处 |
| L1/TEX Hit Rate | 0.08 % | L1/TEX Hit Rate 是 L1TEX 缓存里命中的 sector 占比,命中率就是"要的数据已经在缓存里"的比例,算法是命中的 sector 数除以全部请求的 sector 数 0.08% 贴地,本 kernel 每个数只被读一次、写一次,没有任何重访,命中率高不起来是访问模式决定的,正常现象不是病 |
| L2 Hit Rate | 87.53 % | L2 Hit Rate 是 L2 缓存里命中的 sector 占比,算法和 L1/TEX Hit Rate 同理,命中的 sector 数除以全部请求的 sector 数,这张表里很需要关注的数 87.53% 这么高,非合并读多搬的 sector 把隔壁 block 要用的数顺手带进了 L2,等隔壁 block 来读时直接命中,不用再去显存里搬 |
| Mem Pipes Busy | 15.98 % | Mem Pipes Busy 是 SM 里发射访存指令的那条管线有多忙,算法是把各条访存管线的利用率取最高 15.98% 说明访存指令的发射远没到饱和,数值和 GPU Speed Of Light 里 Compute (SM) Throughput 的 15.98% 一致,SM 这边最忙的就是访存指令那条 LSU 管线 |
| L2 Compression Input Sectors | 0 | L2 Compression Input Sectors 是进 L2 压缩单元的 sector 数,压缩是拿硬件把重复数据压小以省带宽的功能,算法就是直接统计进压缩单元的 sector 数量 0,GeForce 卡砍掉了这个功能,想压也没有这个硬件 |
| L2 Compression Success Rate | 0 % | L2 Compression Success Rate 是压缩尝试成功的比例,算法是压缩成功的次数除以压缩尝试的总次数 0%,没有压缩尝试就没有成功,和上一行同为 0 |
| L2 Compression Ratio | 0.00 | L2 Compression Ratio 是压缩达到的比率,算法是压缩前的数据量除以压缩后的数据量 0.00,作者的显卡就没有压缩这个功能 |
9 个数据里我们主要关注三个重点数据。第一个是 Memory Throughput 的 217.22 Gbyte/s,拿它除以序章列过的峰值带宽 936 GB/s 得 23.2%,和 GPU Speed Of Light 里 DRAM Throughput 的 23.23% 正好对上,一个说流量一个说比例,两个 section 互相印证,显存这条路只跑了两成多。第二个是 Mem Busy 和 Max Bandwidth 的 80.44% 和 80.41%,访问和请求两个口径把压力集中在同一处------L2 这一级已经占满八成,显存却只跑了两成多。第三个是 L2 Hit Rate 的 87.53%,和 L1/TEX Hit Rate 的 0.08% 摆在一起是个反常的组合,这个反差当场就能解释:每个数只被读一次、写一次,没有任何重访,数据在 L1 这一级没有第二次机会,命中率自然贴地;而非合并读每个线程只用到 sector 里的 4 字节,硬件却把 32 字节整个 sector 都搬了回来,多搬的部分里正好有隔壁 block 要用的数,等隔壁 block 来读自己的列时数据已经在 L2 里了,87.53% 的高值就是这么来的。三个数合起来指向同一个判断:瓶颈不在显存,在 L2 和它前后的通路上。这个判断可以往下算一步来验证。217.22 GB/s 乘上执行时间 318.43 us,全程从显存搬出的数据约 69 MB,而 kernel 要读的数组一共 2^24 个 float,即 64 MiB 约 67 MB,两个数字在同一量级,读流量明明在 sector 层面被放大了 8 倍,出了 GPU 却只比数组本身大一点,说明多搬的字节被 L2 消化掉了,没有落到显存上;再拿 GPU Speed Of Light 的 Breakdown 里 sector 总数验算,16,785,408 个 sector 请求里,真正第一次把数据从显存调进 L2 的只有约 210 万个,占 12.5%,剩下 87.5% 全部命中,和表里的 87.53% 正好吻合。至此,我们可以得出一个重要结论:病态一的 8 倍读放大是真实存在的,压在 L1TEX 和 L2 之间的 GPU 内部通路上,L2 把多搬的字节消化得很干净,显存本身倒没有那么大的负荷。
这个 section 下面还挂着三条 OPT 警告:
第一条说:从 L1TEX 看全局加载的访存模式可能不是最优,平均每个 sector 传输 32 字节,每个线程只利用了其中 4.0 字节,可能由线程间的地址间隔造成,可以到 Source Counters section 里查未合并的全局加载。这条第一章已经细讲过,它就是病态一在 L1TEX 视角的再现。
第二条说:全局存储的访存模式可能不是最优,平均每个 sector 32 字节里同样只利用了 4.0 字节,可能由线程间的地址间隔造成,可以到 Source Counters section 里查未合并的全局存储。第一章说它和上一条是一个意思,这里更明确的说法是:加载侧的 4.0 对应病态一,存储侧的 4.0 对应的其实是序章的病态四稀疏写回,每个 block 只有 thread 0 往全局内存写 1 个 float,warp 里 32 个线程只有 1 个在写,硬件照样占一整个 sector,4/32 正是它的写照。两条警告病根同型,都是非合并访问,但代码位置不同,一个在读循环,一个在写回。
第三条说:共享内存加载的访存模式可能不是最优,在全部 909,312 次共享加载请求上平均造成 2.0 路的 bank conflict,产生 524,288 次 bank conflict,占共享加载总 wavefronts 1,831,674 的 28.62%,可以到 Source Counters section 里查未合并的共享加载。这条就是序章的病态二。序章算的 8 路 bank conflict 是单次请求的最坏情形,而这里的 2.0 路是全 kernel 所有共享加载请求摊平后的平均值,1,831,674 除以 909,312 约等于 2.0,病态二那批 8 路 bank conflict 的读,被树形归并等环节大量没有 bank conflict 的读摊薄了。预计加速 26.46% 的算法和第一章 80.90% 是同一套公式,拿 bank conflict 占比 28.62% 乘上 L1TEX 的吞吐 92.45%,正好 26.46%。
Memory Chart
section 最底下还有一张 Memory Chart,把整个内存系统的流量画在一张图上:最左边是 Kernel,往右依次是 Global、Local、Texture、Surface、Shared 这几类空间,中间是 L1TEX 和 L2 两个大框,最右边是 Device Memory 和 System Memory,每个框里标着各自的命中率,箭头方向表示数据往哪流,箭头上的数字是数据总量或流量,颜色按利用率染色,图右侧那条渐变色条对应 %Peak 的 0 到 100%,越亮越接近满负荷。L2 框下方还挂着个 L2 Compression 框,因为硬件不支持 L2 压缩所以恒为 0。图右上角有两个下拉选项,Values 在 Transfer Size(以数据量显示,默认)和 Throughput(以数据吞吐速率显示)之间切换;Inactivity 控制显示效果,默认 Greyed Out 把闲置的框和箭头涂成灰色,选成 Hidden 就把它们整个藏起来,第三个选项 Ignored 作者建议不要使用。
图里的框要分两种来认,左列是逻辑视图,右列是物理存储,不能把左边的 Global 和右边的 Device Memory 看成两份存储。左列的框从 kernel 的视角出发,把 kernel 发出的访存请求按地址空间分类:Global 是全局内存空间,cudaMalloc 出来的大数组就是从这里访问的;Local 是局部内存空间,寄存器装不下的数据溢到这里;Texture 和 Surface 是纹理内存的读写两个口;Load Global Store Shared 框,对应不经过寄存器中转、直接从全局内存搬进共享内存的那类指令,Ampere 架构加入的 cp.async 异步拷贝就是这类指令;Shared 是共享内存空间,每个 block 私有。要注意这些空间只是地址的分类,不是一块块独立的存储,Global 和 Local 的数据物理上就放在右列的 Device Memory 里。中间的 L1TEX 是每个 SM 内部的第一级缓存,左边这些空间进出 SM 的数据都要经过 L1TEX,L1TEX 正下方的 Shared Memory 框和 L1TEX 共用同一份物理硬件,L2 Cache 则是整块 GPU 共享的第二级缓存。右列的框才是真正的存储硬件:Device Memory 是 GPU 自己的显存,Global、Local、Texture 的数据都放在显存里,System Memory 是 CPU 侧的系统内存,和 GPU 之间走 PCIe 通道。数据的实际流向就是从 Device Memory 经 L2、L1TEX 一路搬进 SM 交给 kernel,图上的箭头把这条链画在了框和框之间。L2 框下方挂着的 L2 Compression 是 L2 的压缩单元,前面说过 GeForce 卡上没有这个硬件。L1TEX 和 L2 框里标的命中率前面都读过了,0.08% 和 87.53%,图和数据表互相印证。
然后读 Global 支路上的数字。Kernel 框出发的箭头标着 532.48 K Inst,意思是 kernel 一共发出了 532,480 条全局访存指令:读这边,八条展开的 LDG(LDG 是 SASS 汇编里全局加载指令的名字,Load Global)乘上 65,536 个 warp,正好 524,288 条;写这边,8,192 个 block 各由 thread 0 写回 1 个 float,正好 8,192 条,加起来就是 532.48 K。Global 和 L1TEX 之间两条 Req 箭头上的 524.29 K 和 8.19 K,分别就是这批读请求和写请求,一条指令换一条请求,可知读请求数是写请求的 64 倍,本 kernel 的全局访存几乎全落在读上。再往下,L1TEX 到 L2、L2 到 Device Memory 两段各把读和写拆成两条箭头:读的方向,L1TEX 到 L2 是 536.37 MB,L2 到 Device Memory 只有 67.12 MB,8 倍的放大直接画在了图上;写的方向,L1TEX 到 L2 是 262.14 KB,正好是 8,192 条写指令各占一个 32 B sector 搬下去的量,L2 到 Device Memory 是 2.05 MB,这是最终落到显存颗粒上的量,平摊到 8,192 条写指令每条约 250 字节,写这边同样有放大,只是总量小,重点仍在那 8 倍的读放大上。Local、Texture、Surface 三条支路全是 0.00,System Memory 那边也是 0.00 B,本 kernel 只碰全局内存。Shared 支路上也有数字,本 kernel 用了共享内存,树形归并那一层要把各线程的部分和搬进共享内存再归并:Kernel 到 Shared 的箭头标着 1.88 M Inst,全部共享访存指令共 1,884,160 条,正好等于 Shared 到 L1TEX 两条 Req 箭头上 909.31 K 读请求加 974.85 K 写请求,读的这 909.31 K 正是前面 bank conflict 警告统计的那批共享加载。
把 Values 切到 Throughput,箭头上的数字从字节数换成了速率:L1TEX 到 L2 是 1.68 TB/s,L2 到 Device Memory 拆成读 210.78 GB/s、写 6.45 GB/s 两条,合计 217.23 GB/s,和表里 Memory Throughput 的 217.22 正好对上。两种口径一个看总量、一个看快慢,配合着读,整个内存系统哪条路有货、哪条路挤,全部都清楚了。
Source Counters
前面几节在链路的层级上查找问题,现在我们来看 Source Counters,是 Details 页的最后一个 section,在代码的层级上定位问题。
读这张表之前,需先了解一些有关的概念。Warp 调度前面讲 SM 执行模型的时候已经介绍过了,每个 SM 里有四个调度器,每个调度器手头管着一批 warp,每个周期从中挑一个发射指令;warp 能被发射,前提是它的下一条指令已经具备执行条件,如果这条指令要的数据还没从内存回来,或者要等的同步还没到齐,调度器想发也发不出去,这个 warp 就处在停顿状态,停顿的原因有很多种,等显存的数据回来是一种,等共享内存腾出手是一种,等 block 内的线程到齐又是一种,ncu 给每种原因都起了名字;Warp 采样是这个 section 收集停顿信息的手段,kernel 运行期间每隔一小段时间看一眼各 SM 上的 warp,哪个正停着就记一个样本,记下它当时停在哪条指令上、因为什么停住,kernel 跑完,这些样本就按指令汇总成图上的统计。
ncu 的说明为:这一 section 给的是源码级的指标,包括分支效率和采样的 warp 停顿原因,Warp Stall Sampling 指标在 kernel 运行期间周期性采样得来,记录 warp 什么时候被卡住、无法被调度,全部停顿原因的说明见官方文档,只有当调度器不能做到每个周期都发射指令时才值得去关注这些停顿。
最后这句在本报告正好命中,前面的 GPU Speed Of Light 里已经看到 Compute (SM) Throughput 只有 15.98%,八成以上的周期调度器都发不出指令,warp 停顿值得认真看。说明文字下面紧接着是 4 个数据,如下表所示:
| 项 | 值 | 说明 |
|---|---|---|
| Branch Instructions | 663,552 | 分支类指令一共执行了多少条,warp 级统计,树形归并每轮循环有跳转判断,每轮归并还有 barrier,65,536 个 warp 平均每条摊上 10 轮多一点的分支和屏障,正好是这个量 放在第一行是因为 Source Counters 讲的就是代码行为,分支是代码里最直观的控制流,先看它占总指令的比例是多少 |
| Branch Instructions Ratio | 0.03 % | 分支指令条数占总指令条数的比例,0.03% 看着小得可以忽略 这里有个要留神的地方:原值是分支指令 663,552 除以全部执行指令 20,488,192 等于 0.0324,按百分比说就是 3.24%,UI 把小数 0.03 直接贴上了 % 标签,读成百分之零点零三就差了一百倍,这个数真实的意思是分支占指令总数的百分之三左右 |
| Branch Efficiency | 100 % | 分支跳转目标一致的比例,SASS 口径,算法是拿跳转目标一致的分支次数除以全部分支次数,100% 意味着每一次跳转,warp 里所有在岗线程都走同一条路 本 kernel 的分支全部整齐划一。树形归并每轮让一半线程陪跑,但"谁干活"的 if 判断编译器没有做成跳转指令,而是做成了谓词执行:if 体里的每条指令都带一个执行开关(谓词),该干的线程把它打开,不该干的空过这条指令,warp 全程一起往前走;真正发出的跳转只有循环回跳那几处,判断条件对整个 warp 是同一个值,所以硬件层面没有任何一次分歧跳转,这个排除项很重要,病态三的浪费不发生在分支环节 |
| Avg. Divergent Branches | 0 | 平均每次分支跳转产生了几个分叉目标,只有当 warp 里两个及以上线程跳向不同地址时它才计数 0,和上一行互相印证,没有分歧分支 |
分支这几行先读。Branch Efficiency 100%,平均分歧分支 0,说明这个 kernel 的分支指令本身没有造成浪费。树形归并每轮都让一半线程陪跑,按理说分歧很严重才对,但报告告诉我们,病态三的浪费不发生在分支这个环节,线程陪跑、warp 在屏障前干等,这些浪费要到下一章的 Scheduler Statistics 和 Warp State Statistics 里去算。
表格底下还挂着两条 OPT 优化建议。
第一条 OPT 因其 Est. Speedup: 86.10% 在所有 OPT 里最大,所以直接展示在了 Summary 页,我们在第一章已经读过了:本 kernel 存在未合并的全局访问,一共多产生了 14,680,064 个 sector,占全部 16,785,408 个的 87%,主要的源码位置到 L2 Theoretical Sectors Global Excessive 表里找,CUDA Programming Guide 有减少未合并访问的进一步说明。它提到的那张定位表本体就在本 section,展开在这条建议的下面。表按指令一行一行列出来,一行对应一条 SASS 指令;Location 列写这条指令来自哪一行 CUDA 源码,括号里带着 SASS 指令的地址;Value 列是这条指令多产生的 sector 数;Value (%) 是它在全部多余 sector 里占的比例。图上的五行从 Location 列到数值全都一模一样,原因是编译器把 8 轮搬数循环展开成了八条相同的 LDG.E 指令,它们全都来自 reduce.cu:90 这同一行源码,每条干的活也一样多:每条发出 2,097,152 个 sector,其中 1,835,008 个是多余的,占了 13%,八条合计正好 14,680,064。reduce.cu:90 就是病态一从全局内存搬数的那一行。
第二条 OPT 的 Est. Speedup 是 18.52%,排不进 Summary 页,其实和前一条一个模子:本 kernel 存在未合并的共享访问,一共多产生了 524,288 个 wavefront,占全部 2,801,664 个的 19%,主要的源码位置到 L1 Wavefronts Shared Excessive 表里找,CUDA Best Practices Guide 有优化共享内存访问的例子。这就是病态二在 Source Counters 侧的再现,和 Memory Workload Analysis 里那条 bank conflict 警告互为表里,一个数 bank conflict,一个数多余的 wavefront。关键指标表里 Metric Name 是 derived__memory_l1_wavefronts_shared_excessive,Value 524288,Guidance 一栏的建议是减少 L1TEX 里多余的 wavefront。再往下是定位表,重点关注前两行,两条 LDS.128,Location 都指向 reduce.cu:99,即病态二循环求和的那一行,每条 262,144、各占 50%,524,288 个多余 wavefront 全部出在这两条指令上------病态二的那个读循环被编译器合成了两条 128 位共享读,一条 128 位共享读本来 4 拍就能完事,现在因为 bank conflict 拖成了 8 拍。表里剩下的几行 Value 都是 0,分别指向 reduce.cu:119 和 reduce.cu:111,同样是碰共享内存的指令,但没有产生多余的 wavefront。
section 展开后,最底下还会展示两张表:
左边这张 Warp Stall Sampling (All Samples),是以 Warp 采样的角度来体现结果的。ncu 在 kernel 运行期间,每隔一小段时间就把各 SM 上的 warp 都看一眼,哪个 warp 正停着不动,也就是前面说的停顿,就记一笔样本,写明停在哪条指令、因为什么停;kernel 跑完,把全部样本按指令归堆,就归成了这张表。表格这么读:一行对应一条 SASS 指令,Location 列标着这条指令来自哪一行 CUDA 源码,括号里带着 SASS 指令的地址;Value 列是这条指令名下攒到的样本笔数,一笔样本就是逮到一次 warp 停着不动的瞬间,它不是上面定位表那种"多余的量",这张表里没有多余的东西;Value (%) 是占全部样本的比例,全部样本共 17,547 笔;横条按停顿原因分段染色,逮到时 warp 因为什么停,就染成那种原因的颜色;行按数值从大到小排,最上面一行横条最长,先读它。最上面一行是树形归并循环里的分支跳转指令,紧跟在 block 内 thread 同步指令的后面,4,105 笔、占 23%,染的颜色几乎全是 barrier,说明停在这条指令上的 warp 等的是 block 内的线程在同步点到齐,病态三的信号在这里冒头,答案在调度和等待里,下一章接着算。
右边这张 Most Instructions Executed 不来采样这一套,它数的是另一回事:每条指令实打实被执行了多少次。它不做看一眼记一笔的动作,而是精确计数,每条指令到 kernel 结束为止一共执行了多少次,一次不漏全数出来,归成这张表。表格这么读:和左边一样,一行对应一条 SASS 指令,行按数值从大到小排;Value 列是执行次数,Value (%) 是占全部执行次数的比例。读最上面一行:不止一条指令,好几条横条并排一样长,每条都执行了 524,288 次,全来自树形归并的循环,8 轮归并乘上 65,536 个 warp 正好是这个数,整个 kernel 里最忙的循环就被这排横条点了出来,病态三就压在这个循环里;病态一那八条 LDG.E 每条只有 65,536 次,搬数循环已经展开,每条指令一个 warp 只执行一次。
本节的几张表的 Location 列都是可以点击的,本身也写得很清楚,点一下就会直接跳到 Source 页上对应的指令位置。表里对应的是 SASS 指令,指令能对应回 CUDA-C 的源码行,靠的是编译时加的 -lineinfo 参数。
这一章到这里就结束了。Details 页从上往下走了一遍:GPU Speed Of Light 把问题指到了内存侧,计算侧大量空转;Memory Workload Analysis 和 Memory Chart 把实际带宽和 8 倍的读放大摆在了眼前,病态一、二、四都有警告点名;Source Counters 再下到代码这一层,排除了分支浪费,把病态一、二、三都指到了 reduce.cu 的具体行上。剩下还没讲透的只有病态三,答案在调度和等待里,下一章我们会把剩下的 section 依次读完。