Skip to content

Latest commit

 

History

History
62 lines (36 loc) · 7.15 KB

gpu-dynamic-workload.md

File metadata and controls

62 lines (36 loc) · 7.15 KB

gpu上的动态workload计算

workload的动态性

计算可以具有动态workload的特征,具体是指:固定size input数据本身的内容,可以导致执行结果的output的size不同。比如将某个数据展开成多项数据,且展开的项数无法静态的确定。静态的workload相反的,则是一种比较规整的运算,比如已知size的矩阵乘法。

动态workload本身也不只仅限于output的size的动态的,在执行某个计算过程,即便input output都是静态的size,但是计算所需的中间数据尺寸,如果取决于数据,而不能静态确定,那么也是属于动态的workload。可以认为对于静态workload,计算所需的中间资源也必须静态决定。

要支持动态性质的workload,或者说要支持动态性质的计算。意味着runtime必须能够支持动态的资源分配。这样的资源一般指的是内存(也有可能是任何资源)。即runtime需要有动态内存分配机制。支持动态性质的workload,核心就是支持动态内存分配机制。

计算的out of core性质

某些中间资源动态workload的计算,可能是不必要的,而可以转化为静态的workload。

比如我们要计算1+2+3+..n, 一个糟糕的实现可能是创建出一个n大小的vec,然后再逐项目求和,这种做法就会导致不必要的动态内存需求。一个好的实现只需要记录当前的n和求和的结果,这样完成计算只需要2个u32的内存需求。一个更好的实现就是直接利用求和公式得到结果,这属于数学层面的改进。

假设某个逻辑需要spawn出多个子计算逻辑,那么有可能这样的spawn是不必要的,可以改写为iterator, 迭代的去计算,采用流式的算法进行计算。但是显然不是所有的计算都能具备这样的性质。这样的性质就是out of core算法的性质。传统而言,我们认为out of core指的是计算的输入无法完全存储于runtime内存内。如果认为计算是个multi stage的形态,假设计算能够产生动态的out of bound的中间内存需求,那么其实也必须要求计算实现具备out of core的能力。

gpu workload的动态性和工程限制

一般而言,我们习惯于使用gpu执行大批量的静态workload,编程起来方便易于理解。但是动态workload也是非常重要的。

大部分gpu上的计算,其问题本身并不具有动态性,是outofcore的。但是我们往往会以动态的算法进行编程,比如上面的例子,spawn出一个n大小的vec。这么做的目的是充分利用gpu的执行宽度以优化性能。这是一个典型的pattern:一个计算需求,分裂为多个独立的计算需求,这些独立的计算需求可以被并行执行。gpu上的dynamic workload其实际目的是dynamic parallelism

动态内存分配的实现和限制

gpu上的动态workload有个严重限制:图形api的限制 gpu buffer只能预先分配,不能动态扩容。不能动态扩容并意味着不能实现真正的动态workload,但也不意味着完全不能实现动态性,因为动态内存分配可以基于预分配的空间进行。

基于这样的预分配空间,gpu上的动态内存分配,一般有两种实现:

  • 通过通过独立的dispatch prefix scan动态内存需求,得到前序和作为分配的地址,然后再执行实际计算。这种方式能够保证原始的计算顺序
  • 通过bump allocation来分配内存,不能保证原始的计算顺序

只能预先分配这个问题会在实际工程上造成非常严重的影响, 比如

  • 总是需要尽可能的设置保守估计的内存消耗
    • 如果存在多个相互依赖的dyanmic workload,那么每一个stage都需要这样的保守内存设置,导致整个系统的内存消耗非常巨大
  • 需要实现分配失败的降级逻辑
    • host端异步检查,执行扩容,重新计算
      • 保证正确性:blocking的帧内同步检查,或者延迟到后续帧提交。
        • 实现复杂,性能差,或者高延迟
        • 同步检查在web上是无法实现的
      • 放弃正确性:逻辑上形成降级的表现
        • 可能需求上无法接受

对于内存问题,有缓解方式可以将不同stage之间的临时内存消耗alias在一起,但是并不能彻底解决内存高消耗问题。

如何更好的表达 gpu动态workload?

通用来说,对于一个实际上静态workload / out of core gpu计算,假设其计算过程可以中间可以引入动态的parallelism来加速计算。那么:

一个简单的做法是将这样的parallelism按照问题本身的完整规模进行展开,但是这么做会导致动态的资源需求完全由计算数据决定,即完全丧失问题的outofcore性质,对于实际上无法真正动态调整内存的平台,即导致上述的问题。

我认为一个可行的做法是,是在需要展开子计算时,其展开的宽度,由当前执行环境的剩余可用宽度决定,而不是问题本身。不同的invocation之间可以通过bumpallocation来抢占可用的执行宽度。(当然我们也许需要通过某种机制保障invocation总是能成功获取至少一个执行宽度,来避免starvation)

采用上述的做法,我们可以保证这样的gpu计算,不会因为执行器的内存不足而导致计算失败。当执行内存高于问题本身所需的最大宽度,那么具备最大的并行能力,最好的执行性能。减少执行内存,只会影响并行度,只会导致性能变化。而用户在host端可以异步的轮询内存使用率,来根据应用的实际负载来渐进的响应的调整执行器内存消耗,以正确平衡内存消耗和执行性能。

然而实际上这个方案有另一个严重问题,这种按需展开,直到不能展开的polling逻辑,需要实现在shader内,需要由shader来触发,而不能通过commandbuffer内提前录制indirect dispatch。因为polling的次数是问题本身动态决定的,所以你无法在host端提前得知需要的indirect dispatch次数。所以你只能尽可能多的,保守的录制indirect dispatch,导致性能问题,而同样的,你需要处理dispatch不足导致的计算失败问题。这就陷入了内存问题一样的进退两难的境地。

我认为这样的限制,来自于图形api另一个重大能力缺陷:如果认为indirect dispatch/draw 表达了gpu上work的能够控制执行宽度的依赖关系,目前你没有任何途径表达带环(以及环的退出条件)的依赖关系。最新一代的api,比如workgraph也并没有解决这样的问题:只能允许单个节点循环自己,并且有递归次数限制,并不支持通用的有环的计算。

如果我们将这样的polling实现为shader内的loop,虽然能解决问题,但是性能很可能不是最优的。因为单个的dispatch只能设置单个workgroup size,而workgroup size应该为不同的并行逻辑单元单独调整优化。另外这么做意味着要将整个gpu driven的计算图完全编译在一个shader内,如果有多个dyanmic的子计算spawn结构需要级联在一起,以及计算逻辑就非常复杂的情况,其本身可行性就是一个问题。