ORCA论文阅读
现阶段有什么问题:
之前的推理引擎在处理基于transform模型decode的时候存在局限
由于语言模型的自回归特性,每次模型的输出作为模型的输入再次执行模型,导致输出多少个token,就需要运行多少次模型,并且这个运行多少次模型是不确定的
之前的推理引擎只需要运行一次就可以了
这导致了以下的问题
- 之前的推理引擎是以请求为粒度调度的,很多请求过来,我通过调度方法来确定那些请求,走一遍模型,然后所有引擎一起返回
- 但是对于有自回归特性的语言模型,每个请求不知道的自己输出多少token,运行多少次,这就导致了一个问题,如果调度器选择了一批请求,那么返回这一批的请求的时间取决于耗费时间最初的请求,之前的已经完成的请求无法直接返回
之前的推理引擎这么做是有原因的:
- 之前的模型确实一次的就可以运行完成
- 批处理可以高效地利用GPU资源,多个请求的输入可以聚合成一个大张量,来和模型参数进行矩阵运算,提高效率。另一点就是这一批次在计算过程中可以重复使用模型参数,由于内存墙的存在,可以提高效率
一般推理服务系统包含两部分,一个是调度器,另一个是执行引擎,下图是一个简单的示例:
调度器的调度算法多种多样,为了满足各种需求也就是有各种算法,比如有的目标是吞吐,有的目标是延迟,
现有的推理服务问题是,如果我一次调度了两个请求,一个是"I love",另一个是“I think”,那么这两个请求会一直等到第二个请求结束第一个才能返回。
所以改进方法也是容易想到的,不在请求调度,使用迭代调度,运行模型一次我们就返回到调度器,看看有没有结束的,然后还可以在把请求池里面请求在塞进去。可以解决我们上面提出的问题,

上面这个是原文的图示,请求的有阴影的块就是prompt,也就是请求的输入,$x_{ij}$代表的是第x个请求的第j个token。其中$x_1$这个请求已经进行了两次迭代,生成了$x_{13}x_{14} $。经过一次迭代之后,执行引擎返回了下一个的token。
这样做有一个问题,就是运行效率的问题。在上文中,讨论了之前推理引擎设计的核心就是批处理,效率高,迭代级调度效率低,这里做简单解释:
- 可能有人会疑问为什么会下降,不是传入的批次大小是一样的吗?
- 实际上批处理有个苛刻的要求就是说每个请求的形状要是一样的,如果不一样,那么就没有办法进行批处理。形状不一致意味着底层要做不同大小的循环/访存,无法在一次 kernel 调用里并行完成;但 kernel 调用本身开销很大,效率会急剧下降。
- 其余人可能认为,我们在一个kernel中使用一个循环来处理,这就是一个kernel,但是问题时这样的话就不能共享权重参数,计算每个请求都需要重新加载权重,由于内存墙的存在,效率不够高
比如$x_1$和$x_2$,他们都处于decode阶段,但是他们的长度不同,那么在计算注意力的时候,k的长度就是不同的,所以不能批处理。
对于$x_3$和$x_4$,他俩长度不同,无法进行拼接。
对于$x_1$和$x_3$,他俩处于不同的阶段,对于使用了kv cache的推理引擎,$x_3$处于profill阶段,需要将这个2个token都放进去,然后根据最后一个token的输出隐藏维度计算下一个token,但是对于$x_1$,他只需要将一个token放进去,然后使用之前的kv cache进行计算,他俩的计算都不一样,所以更不能批处理了。
为了解决这个问题,提出了选择批处理计算,这里的选择指的是将推理引擎中的多个算子选择出来进行批处理。
token被嵌入在加上位置信息,这个是token级别的操作。
qkv线性转换也是token级别的操作,
但是attention操作是请求级别的操作,
ffn中的线性变化也是token级别的操作
层归一化是也是token级别的操作,他是在token隐藏向量维度上进行归一化
gelu则是对单个数值进行操作,对每个标量进行操作
所以根据上述,可以把这个token级别操作的算子进行批处理,对于注意力机制进行单独处理,这个就是选择批处理的含义。
可以看到,在CPU端,我们现将几个请求合并成token级别,进入里面进行操作,这个时候一起使用模型权重,进行批处理。在计算