[论文笔记][GPU Modeling] The Pipeline Performance Model: a generic executable performance model for GPUs

本文是2019年发布的新论文, 由比利时大学的两位学者攥写. 提出了一个通用GPU模型, 主要用来加强GPU code 性能方面的理解.

动机

文章一开始提到GPU因为CUDA和OPENCL变得越来越流行, 但是在GPU上编程的人却很少理解他们写的code到底带来什么样的performance, 因此无从诊断这些code的bottleneck在哪, 也不知道这些code的能不能跑的更快.

前人的研究

  • Graphical Model
    • Roofline: an insightful visual performance model for multicore architectures, 2009
    • GPURoofline: a model for guiding performance optimizations on GPUs, 2012
    • The boat hull model: enabling performance prediction for parallel computing prior to code development, 2012
    • A practical performance model for compute and memory bound GPU kernels, 2015

** 这些Paper考虑了inefficiency, 但是没有对non-ideal latency hiding建模, 因为他们认为GPU计算和memory subsystem是独立的. **

  • Analytical and quan- titative methods
    • An analytical model for a GPU architecture with memory-level and thread-level parallelism awareness, 2009.
    • An adaptive performance modeling tool for GPU architectures, 2010.
    • A performance analysis framework for identifying potential benefits in GPGPU applications, 2012
    • A quantitative performance analysis model for GPU architectures, 2011

** 这几篇paper考虑了computation和memory subsystem的交互, 但是在分析时混入了低效的操作, 这使得出来的结果不好解释 **
而且上述paper有好几篇都没法重现, 因为参数实在太多而且还需要专门的工具. 比如这篇GPUMech: GPU performance modeling technique based on interval analysis,2014, 就用了simulator验证准确性.
还有这篇: GATSim: abstract timing simulation of GPUs, 2017. 建了个抽象的timing model 来加速code在GPU上的simualtion.
这两篇的手段和本文有些类似, 但他们重点关注NVIDIA GPU, 使用PTX code 和 GPGPUSim. 而本文提出的pipeline performance model, 其目的是通配各种GPU. 而且这个model为了能轻易找到由inefficiency pattern 引起的bottleneck, 以及能迅速评估优化结果, 做的和Source code很相似. 这个model基于这么一件事: 在GPU上运行的code的performance是针对最关键部分的, 最关键部分主要是由以下两点决定, 需要(等待)执行的指令的latencies, 以及能够避免这些latencies的并行数量.
Model使用了一种叫做instruction dependence graph的表现形式, 然后每条指令都被赋予issue latency和completion latency, 以此来把hardware考虑在内. Simulator 使用了这个graph, GPU的简易特征, 以及运行的配置, 来模拟code运行时的time behavior. 这个model的优势在于把source code model 和GPU model 分开, 这样执行新的simulation就很快, instruction latencies通常和code无关, instruction dependence graph也和GPU具体型号无关.

Background: GPU Arch, Code Execution

这一节主要聊一聊背景知识, 大部分定义来自OpenCL的definition.
GPU里有很多Compute Unit(计算单元), 这些单元是由专门的处理逻辑, 很多register, 和L1 cache 组成. Memory access subsystem 把GPU 和RAM 连起来(通常是个L2 cache). Threads 以SIMT的形式运行: 多个thread共享一个instruction unit. 对NV GPU来说, 32 thread 组成一个warps, 对AMD GPU 来说, 64 threads 叫做一个wave fronts. 本文只使用warp的定义. 想达到最高速度, 一定要讨论SIMT, 因为它影响着memory access 也会造成code的序列化(serialization, parallelism的反面)
kernel function里会指出哪些code由一条thread处理, host program会决定多少条thread来处理一个kernel. 一个work group 里的thread可以通过barrier synchronization, 共享L1 cache 来互相协力. 再由compute unit处理这些work group. 能同时被conpute unit 运行的work group数量有限, 所以很多得等到其他完成之后再运行. 需要处理的work group的size和数量还有最大并行数量被称为kernel的execution configuration(运行配置).
kernel 的占用率就是指同时运行的thread数量除以最大数量. 传统建议就是提升这个占用率来获得更好的性能, 但也有一些其他因素比如ILP, MLP, instruction latencies, 也很关键.

Model

Pipline的性能主要由两个latency决定: issue latency 和 completion latency. issue latency是指issue下一条独立指令距离上一条所需最短时间 (叫做λ). conpletion latency是指指令issue到结果available之间的时间.(叫做Λ). 一个简单pipeline, λ是1个cycle, Λ是pipline stage的数目.
处理一堆指令所花时间是由指令之间充分独立的程度决定的. 比如 处理W threads of N dependent instructions 需要的时间是:
在这里插入图片描述
如果N很大, Λ/λ 也很大 (pipeline很长), 在有dependency情况下, run time 主要被Λ(conpletion latency) 决定, 这种情况下pipeline越长, 等的时间也越长. 在sufficient时(没有dependency)情况, pipeline满速运行, 每个cycle(或每个λ) 都能处理一条指令.
Compute Unit 的建模用到了以上概念, instruction dependence graph表示一个Warp, 里面的node表示 每条指令, 每条边表示互相的dependence. 这些node都被标注了instruction latency 和completion latency, 为了能够模拟code在GPU上的timing behavior.

Model里的Latencies和实际hardware里的latencies没必要完全一致, 因为:

  1. 既然是SIMT, warp instruction就是一个基础scheduling unit, 因此为了获得最大instruction rate, 从λ算出来的instruction rate 要乘以warp size. 举例来说, NVIDIA GPU的λ = 1, 每个cycle 执行32个instruction(per compute unit), AMD GPU 每个cycle 执行64个instuction.
  2. 为保证这个model具有普遍性, 使用了CUDA/OpenCL里的指令, 这些指令通常被翻译成多个hardware指令.
  3. 同一个指令对于不同的kernel可能有不同的latencies. 比如memory instruction 的latencies取决于memory access pattern, caching, 和warp间的contention

这些latencies是从一些列micro-benchmarks里决定出来的. 在各种占用率的情况下, 执行了一个包含了很多依赖关系的instruction的kernel. 论文作者猜测, 整个runtime的分布类似一个boat-hull(两头尖中间大), completion 和issue latency可以分别从maximal run time和 minimal run time 推导出来.

Instruction Dependence Graph (IDG) 表示了kernal里instruction的data dependences, 这个图是从program dependence graph推导出来的(通过把所有控制依赖都解决). 循环指令是这么做的: 重复这个node N遍(N就是循环次数). 这就是说需要考虑loop unrolling, 这可能会把某些循环指令给删了, 然后导致ILP(instruction level parallelism), MLP(memory level parallelism) 提升. 条件语句是通过串接branches来实现. 没有thread去过的branch会被删掉. 注意这对于不同的warp会产生不同的graph. 由此引起的概率问题本篇文章不涉及.

在这里插入图片描述
上图左边是一个简单的kernel, 右边就是对应的IDG. 给定latency之后, 就可以通过IDG算出执行这个kernal的最小cycle数(对于一个warp来说)

cycles = Λindex(一条寻址指令所需cycle数)
+Λmem(一条内存访问指令需要的cycle数)
+λmem (下一条内存访问指令需要间隔的cycle数)
+Λfmadd (一个加法所需cycle数)
+λmem (西一条内存写回指令需要间隔的cycle数)

这里memory相关的lantency都认为是相等的(不会出现有时多有时少的情况), 当最后一个memory request发出时, 认为warp完成. GPU kernel run time 就是遵从这样的原则被model出来. 但是使用公式来计算不太现实, 需要造一个simulator来模拟执行时timing behavior.

这个simulator吃了compute unit个数P, processor时钟频率f, work group的总数量Ntotal, 可被Compute Unit并行处理的work group的数量M, 根据kernel和GPU latencies做的instruction dependence graph. 假设所有work group都同时完成, 这样才好估计一个compute unit需要处理的最大work group数量 N = [ N t o t a l P ] N = \left [\frac{Ntotal}{P}\right ] N=[PNtotal].

首先给了M个work group, 然后按照之前描述schedule所有的指令. 这个simulator一直记录cycle数(已经考虑instruction之间dependences, issue latency 和 单cycle能issue的最大指令数量.) 如果有一个group结束运行, 那么会有一个新的group(如果有的话)取而代之. 这样的方式一直持续, 直到所有group都结束. 然后把cycle数除以f就得到时间(s).

验证

本文simulator使用了以下算法来评估model, 见图. 这个simulator把每个compute unit当做pipeline的拼接, 分别对应
ALU,
SFU,
local memory,
global memory subsystem,
processes issue, completion event
这样的实现方式其实并没有考虑并行状况(单cycle能issue的最大指令数量), 这个问题本文不讨论, 但放着不管会造成某些指令模拟时过度乐观, 所以作者设计simulator时也考虑了这种局限.
在这里插入图片描述
这个simulator在2个NVIDIA GPU上测试了一些kernel, TeslaC2050, GeForce GTX1060 6GB. 其配置如下表, 之后文章就用Fermi, Pascal来指代这两种GPU
在这里插入图片描述
测试model时, 为了保证完备性, 每个kernel都以不同的group size, group count 组合来run, 以充分考虑占有率和group size的影响. 但是为了节省空间, group size只能是大于等于32 的2的x次方(32, 64, 128, …). Fermi GPU 用了34种组合, 18个warp. Pascal GPU 用了78种组合, 43个warp.

下表是simulate 4个benchmark kernels的结果
floating point addition, fadd
hardware accelerated cosine, cos
loop overhead, loop
memory access, memory

最后两列是平均误差和标准差.
在这里插入图片描述
从上表可以看出simulation的结果非常精确, 尤其是硬件操作的相关指令. Pascal GPU没有Fermi那么精确, 有以下原因:

  1. Pascal 有个boost clock, 这可能造成kernel运行的frequency不确定.
  2. multiple warp scheduler在Pascal GPU的影响很大, 这并没有被simulation考虑在内.

这两种GPU对于memory benchmark都不是很准, 其原因见下图, 真实情况下, memory access并不是个完美的pipeline. 要想达到极限performance, 需要的occupancy比原先以为的更高. 因为memory contention(内存竞争). 不管什么原因, 可以通过让latency与warp count挂钩 这样的建模方式把这种情况模拟出来.

在这里插入图片描述
明显发现, model和真实GPU还是有区别的.

现在来研究一个OpenCL实现的矩阵乘, 使用到了local memory, 源代码是8x8 work groups. 见下图
在这里插入图片描述
我们使用了model模拟运行了这个kernel, 还有个类似的16x16 work groups. NVIDIA PTX code 显示这两个kernel的inner loop 都完全展开了, 但是outer loop仅在8x8展开, 16x16部分展开. 为了简化IDG的构建, 删去了outer loop的unrolling, 这对run time影响很小. inner loop 的展开引入了local memory 的并行处理, 这个乘加指令依赖于两个local memory read, 和之前的乘加指令(除了第一条). 这里需要注意IDG仅考虑了loop和global memory write的latency.
内存访问的latency取决于pattern, caching 和occupancy, 我们写了简单的benchmark来模拟矩阵里的数据访问, 我们创造了各种访问类型, 和occupancy范围, 得到相应指令的latency, 如下:
在这里插入图片描述
我们在两个GPU上都跑了1024x1024的矩阵乘, 使用了8x8, 和16x16两个kernel. 结果如下:
在这里插入图片描述
mmul08 和mmul16就是分别指8x8 和16x16 work group. 整体看来simulation结果和实际测量的结果很相近. Fermi的mmul16 simulation被高估了, 原因可能是global memory latency比我们想的要差.
最后, model能应对改变. 比如我们改变了代码里local memory indice, 这会导致local memory bank confilicts. 一个warp里的thread试图读取一个bank下的不同data就会发生confilicts, 导致local memory 性能下降, 因为memory要串行处理这些request. 我们在model里通过加大local memory 的latency来表现这种情况. 然后重新run simulation. 得到第三列数据, simulation结果也正确的计算了这个变化带来的影响, 实际上因为local memory变成bottleneck, simualtion也变得更加准确.

Conclusion

本文介绍了一种pipeline performance model, 使用了Instruction dependence graph 来表示指令的issue/completion latency. 还配合了各种配置以用来模拟 code在各种occupancy情况下在GPU内的timing behavior.
指令的latency是从micro-benchmark得来的, 可以用在不同的kernel里(同一个GPU). 类似的, IDG是从OpenCL/CUDA code里推导来的, 可以用在各个GPU的同一个kernel里.
model经过了micro-benchmark和矩阵乘的测试, 所有kernel都在各种occupancy range下run了 (通过改变group size). 尽管和真实情况有些误差, 但model能捕捉主要的performance contributor. 最大的难点在于memory instruction latency不够准确. 最后, model能够迅速确认某条指令是否是bottleneck. (通过修改local memory latency 来模拟conflict情况, 结果证明simulation能很好的预测run time)
未来会对model做的改进:

  1. 通过compiler technology 自动化IDG 的构建.
  2. 继续改进memory instruction 的model. 比如考虑内存竞争的影响(latency 和occupancy关联)
  3. 改进simulator, 使之考虑指令分发限制的影响, 还有测试更多混合指令下model的表现. 会引入performance model benchmark(大多model对于这个benchmark都表现的不够好)

观后评价

  1. 本文提出的建模方法确实比较新颖, 使用instruction dependence graph来描述code在GPU里的行为, 货真价实的instruction based performance modeling. 值得参考, 也许意义重大. 作为2019年新发布的paper, 值得关注一下, 拭目以待.
  2. 现阶段问题是IDG的构建是人工做的, 而且有瑕疵, 并不能完全还原GPU内部处理指令的样子, 其次对于memory latency的误差较高, 有很多architecture的问题并没有考虑, 个人认为这个model似乎不够复杂, 很多行为不好描述. 想要精确建模还需要设计各种micro benchmark来做correlation, 显得理论支撑有些不太够.
  3. 如作者提到的, 对model的测试仅限于micro-benchmark(可能是简单指令重复) 和矩阵乘法, 其实更加复杂的benchmark还没用上, GPU也只用了两种. 测试case的多样性明显不够
  4. 其目的主要还是帮助软件编程人员理解他们在GPU上run的code(CUDA/OpenCL) 其瓶颈到底在哪, 因为正常情况GPU是不会反馈太多内部信号的, 所以需要借助model去挖掘哪条instruction因为什么原因导致run time不理想. 其初衷并不是改变GPU架构, 不过也可以往这方面发展.
  5. 比利时大神写的paper还真蛮难啃的, 有很多地方表达委婉, 对GPU内部行为解释不够深入. 比如为什么inner loop 在16x16的时候没完全展开等等
  6. 文章多次提到一篇phd毕业论文: 这篇文章是2016年 伯克利phd 写的Understanding Latency Hiding on GPUs, 介绍了GPU的架构, 怎样model latency, latency hiding是什么, 怎样提取硬件参数, 和评估前人model. 多亏了本篇paper, 我才知道还有这么一篇modeling thesis, 以后有机会仔细啃它.
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值