文章标题:LADDER: 通过硬件感知的张量变换实现高效的低精度深度学习计算
作者/机构:Lei Wang†⋄∗, Lingxiao Ma⋄, Shijie Cao⋄, Quanlu Zhang⋄, Jilong Xue⋄, Yining Shi‡⋄∗, Ningxin Zheng⋄, Ziming Miao⋄, Fan Yang⋄, Ting Cao⋄, Yuqing Yang⋄, Mao Yang⋄ (中国科学院大学†, 北京大学‡, 微软研究院⋄)
核心问题:
随着深度学习模型对性能需求的不断增长,利用低精度计算已成为一种趋势。然而,现有的硬件和软件对新兴的自定义低精度数据类型的支持不足且效率低下。硬件(如GPU)只能集成有限的几种标准数据类型计算单元,难以跟上算法需求的快速演进。软件方面,优化细粒度的低比特数据访问以适应粗粒度的内存系统(如NVIDIA GPU的4字节共享内存库宽)非常复杂,容易导致带宽浪费,需要大量非平凡的优化工作。这种软硬件支持的不足和低效严重阻碍了模型和加速器的创新。
研究目标:
本文旨在弥合不断演进的自定义数据类型与当前硬件支持的固定精度格式之间的鸿沟。目标是在不修改现有硬件的条件下,通过一种新颖的编译器设计,系统性地支持通用低比特精度自定义数据类型,并在现代加速器上实现高效的深度学习计算。
创新点:
1. 分离存储与计算的通用方法:本文观察到,硬件的内存系统可以存储任意数据类型,而计算单元支持的标准数据类型可以无损地表示大多数自定义数据类型。基于此,LADDER提出了一种通用方法:以自定义低精度数据类型存储和传输张量,通过类型转换以硬件支持的标准数据类型进行计算。这能够有效利用低比特数据类型在节省内存流量和占用方面的优势。
2. tType通用类型系统和扩展张量表达式:为了方便地实现快速演进的自定义数据类型(如块状数据类型MXFP),LADDER引入了一个名为tType的通用类型系统。tType是一个分块(tile-wise)的数据类型,通过显式指定类型宽度、元素形状和类型转换函数来定义所有常见的自定义类型。基于此,LADDER扩展了现有的张量表达式,使其能够原生支持为每个张量标注tType,从而系统地将带有自定义数据类型的DNN计算转换为标准的计算流水线。
3. 张量调度原语:为了优化涉及自定义数据存储、访问和类型转换的计算流水线,LADDER引入了一套新的张量调度原语,包括slice(切片)、map(映射)、pad(填充)和convert(转换)。这些原语能够将默认的计算流水线转换为经过优化的、性能更佳的等价形式,以适应不同的内存布局和硬件特性。
4. 硬件感知的优化策略:为了在复杂的变换空间中找到最优解,LADDER采用了一种分层的、硬件感知的优化策略。该策略将DNN计算建模为分块级别的数据流图,然后使用一种粒度感知的调度策略进行优化:较低层内存提供首选的数据访问粒度作为提示(hint),上层根据此提示决定最优的计算粒度,并通过张量变换进行对齐。
5. 系统实现与开源:LADDER基于TVM、Roller和Welder实现,并且已经开源。其核心DNN操作编译功能已作为BitBLAS库发布,可集成到现有的DNN和LLM框架中,为深度学习生态系统提供高效的低精度计算能力。
ldmatrix.2x2.f16加载2x2块)不对齐时,会导致带宽利用率低下(如图2左侧所示)。此外,由于缺乏FP16×INT8的计算指令,即使数据成功加载到寄存器也无法计算。系统概述:针对第2节中的观察,LADDER被设计为一个将数据类型视为一等公民的DNN编译器,它引入了张量变换来支持自定义数据类型上的高效DNN计算。图3展示了其系统架构。
核心抽象 tTile:LADDER的核心是TypedTile(tTile)抽象,它在基于分块(tile)的张量抽象上增加了数据类型信息(即tType,见§3.1)。算法设计者可以使用常用数据类型(如FP16)或定义自定义数据类型(如MXFP8, NF4)作为tType,并在此数据类型上定义DNN计算。随后,LADDER将输入的DNN模型转换为一个基于tTile的数据流图(tTile-graph),其中算子被定义为基于tTile的计算任务(tTile-operator)(见§3.1)。
硬件抽象 tTile-device:此外,LADDER将硬件加速器抽象为一个多层层次结构,每一层的需求都由一个tTile(tTile-device,见§3.1)来表示。tTile-device显式地描述了每一层的要求,例如支持的数据类型、事务大小等。通过将tTile-graph中的tTile与tTile-device对齐,以tTile-graph表示的DNN计算便可以在硬件加速器上执行。
调度机制与策略:给定初始的tTile-graph和硬件规格,LADDER会将DNN模型编译成在加速器上的高效执行计划。为了在tTile-device上调度tTile-graph并满足硬件层次结构的要求,LADDER将其调度机制与策略分离。在机制方面,LADDER提出了四种tTile变换原语:slice、map、pad和convert,它们能够将一个tTile变换为另一个等价的tTile(见§3.2)。
优化空间与策略:调度器会将初始的tTile-graph调度为一个能够精细控制tTile配置、变换以及在硬件层次结构上布局的tTile-graph。tTile抽象扩大了DNN计算的调度空间,并在内存占用效率和延迟效率之间开启了新的权衡。在策略方面,LADDER基于观察采用启发式方法,并提供了一种硬件感知的、分层的策略来优化延迟效率(见§3.3)。
代码生成:最后,编译好的、由tTile-graph表示的计划将被生成为给定硬件加速器的可执行代码。
tType:根据第2节的观察,DNN计算中的数据类型通常在元素级或块级粒度上定义。为了表达这些数据类型,LADDER引入了tType的概念(图4(a))。具体来说,tType表示一种由一组同质元素构成的数据类型。这些元素的布局是一个n维数组shape。每个元素共享相同的类型,并使用nElemBits位来存储。这组元素还共享相同的元数据。如第2节所述,数据类型通常可以被某些更高位的数据类型无损表示。c_tTypes表示一个tType可以通过c_func函数无损地转换为另一个tType。无论是现有的常用数据类型还是新的自定义数据类型,都可以用tType表示。例如,FP16类型可以表示为shape=[1]、nElemBits=16的tType。元素级的NF4类型可以表示为shape=[1]、nElemBits=4且元数据中包含共享值映射的tType。NF4类型可以无损地表示为FP16,因此在c_tTypes中可能有一个<FP16, NF4_to_FP16_func>条目。块级的OCP-MXFP8类型可以表示为shape=[32]、nElemBits=8且元数据中包含共享缩放因子的tType。
tTile:基于代表数据类型的tType,LADDER提出了tTile来表示特定数据类型的张量在细粒度分块上的形式。具体来说,如图4(b)所示,一个tTile被定义为一组具有相同数据类型dtype和n维数组布局shape的同质元素。一个tTile中的元素共享一个元数据。此外,tTile中的元素按行主序存储。
tTile-Operator:一个DNN算子(例如MatMul)通常被实现为一组独立且同质的任务,每个任务处理输入张量的一个分块并输出输出张的一个分块。通过tTile抽象,一个特定数据类型的张量在细粒度的分块级别上被表示。因此,LADDER可以利用tTile来将自定义数据类型的DNN算子表示为一组独立且同质的细粒度任务,即tTile-operator。具体来说,如图4(c)所示,一个tTile-operator显式地表示了对形状为shape的元素进行的张量计算任务。get_input_tTiles()和get_output_tTiles()返回此计算任务的输入和输出tTile。compute()则为输入和输出tTile执行在张量表达式expr中定义的计算。
tType注解的张量表达式:tTile-operator的计算被定义为一个基于索引的lambda表达式expr【索引13,Tvm: end-to-end optimization stack for deep learning,2018,arXiv;索引40,Halide: A language and compiler for optimizing parallelism, locality, and recomputation in image processing pipelines,2013,PLDI;索引57,Roller: Fast and efficient tensor compilation for deep learning,2022,OSDI】。然而,现有张量编译器【索引13,Tvm: end-to-end optimization stack for deep learning,2018,arXiv;索引21,Tensorir: An abstraction for automatic tensorized program optimization,2023,ASPLOS;索引52,Ansor: Generating high-performance tensor programs for deep learning,2020,OSDI;索引56,Amos: enabling automatic mapping for tensor computations on spatial accelerators with hardware abstraction,2022,ISCA;索引57,Roller: Fast and efficient tensor compilation for deep learning,2022,OSDI】中的张量表达式主要关注描述索引和形状,不能灵活地指示计算过程中的数据类型。例如,它无法表达一个FP16张量乘以一个FP16张量,并以FP32作为累加类型。为了支持混合数据类型上的计算表达,需要在计算过程中表达数据类型。因此,LADDER在张量表达式中引入了tType注解,以明确指示计算过程中的数据类型,包括输入、输出和中间数据,从而表示混合数据类型上的计算。例如,一个FP16类型的张量A[M,K]乘以一个NF4类型的张量B[N,K],以FP32作为累加类型,并输出一个FP16类型的张量C[M,N],可以如图5(a)所示进行表达。通过带有tType注解的张量表达式,给定形状后,LADDER可以推断出相应的输入和输出tTile。
tTile-Graph与硬件抽象:通过基于tTile的细粒度表示,DNN算子可以将一个DNN模型表示为一个细粒度的tTile-graph,其中每个节点是一个tTile-operator,每条边代表两个tTile-operator之间的依赖关系。现代硬件加速器通常具有硬件层次结构,包括内存层(如DRAM、寄存器)和计算单元。硬件层次结构中的每一层都有其数据访问偏好。具体来说,内存层通常要求通过事务(transaction)进行访问,其中事务是某个粒度下的一段连续数据或一个特定形状的数据。例如,NVIDIA GPU的共享内存要求事务是32个4字节的bank。计算单元通常也要求处理特定粒度下特定形状的数据。例如,NVIDIA GPU中的hfma2指令处理的粒度是两个FP16值。这些要求可以被描述为tTile。
tTile-device:因此,LADDER将硬件加速器抽象为一个由多个层级构成的层次结构,每个层级都由tTile描述,即tTile-device。每个层级要么是内存层,要么是计算单元,其要求(表示为某个粒度上的形状)被描述为一个tTile,而粒度则被描述为tType。图5(b)展示了NVIDIA A100 GPU使用FP16张量核心的tTile-device。FP16张量核心的MMA指令要求分别处理[16,16]和[8,16]粒度的两个输入。这可以表示为dtype=FP16、shape=[16,16]的tTile。FP16张量核心的数据加载指令要求加载[16,2]的数据,粒度为half8(即8个FP16值),可以表示为dtype=16B、shape=[16,2]的tTile。此外,充分利用共享内存的要求可以表示为dtype=4B、shape=[32]的tTile。全局内存的32字节事务要求可以表示为dtype=1B、shape=[32]的tTile。
tTile显式地描述了细粒度的张量存储和硬件层次结构的要求。在tTile-graph中表示的DNN计算应与tTile-device对齐以实现高效执行。幸运的是,根据我们在§2中的观察,流水线中的张量存储和访问可以被变换为逻辑上等价的格式,而每种格式在硬件层次结构中都有不同的性能影响。因此,LADDER提出了tTile变换机制,以支持将一个tTile的布局或tType变换为等价的tTile。tTile-operator的计算流水线扩展为硬件层次结构上的三个阶段:Transform-Load(变换-加载)、Compute(计算)和Transform-Store(变换-存储)。Transform-Load将tTile从较低的内存层加载到较高的内存层,并进行tTile变换。Compute在计算单元上执行tTile-operator的计算任务。Transform-Store将tTile从较高的内存层存储到较低的内存层,并进行tTile变换。tTile变换为等价的tTile,如图6所示。slice原语从tTile_input的地址index处切片出一组形状为shape的元素,并以out_shape的新tTile形式返回。slice原语通常用于表示数据分块(tiling)。map原语修改tTile中元素的布局。给定map_func,map原语将每个元素的地址映射到期望的地址。例如,在图5(d)中,从L2内存层到L1内存层的TransformLoad_L1B利用map原语和map_func修改了元素的地址。pad原语使用pad_value在pad_shape给定的每个边界上对tTile_input进行填充。pad_shape的长度是tTile_input的shape长度的2倍,分别描述了每个维度的左边界和右边界。convert原语将tTile_input的tType转换为给定的new_tType。给定的new_tType应在tTile_input的tType的c_tTypes中。convert将对tTile_input中的每个元素调用给定new_tType对应的c_func,并返回期望的new_tType的tTile。例如,在图5(d)中,TransformLoad_L1B使用convert原语将tType从NF4转换为FP16,以满足核心的FP16 tType要求。tTile可以通过slice和pad改变形状,通过map修改元素布局,或通过convert转换tType,从而变换为另一个等价的tTile。这使得tTile-operator的tTile能够变换以与tTile-device对齐,从而在硬件层次结构中被高效处理。A[32,63]乘以一个NF4张量B[32,63],以FP32作为累加,输出一个FP16张量C[32,32](图5(a)),在一个四层tTile-device(从L2到核心,图5(b))上执行。具体来说,图5(c)展示了执行的伪代码。A和B的tTile被变换并以FP16类型从L2加载到L1。然后,tTile通过ldmatrix加载到L0,并由mma指令处理,该指令在L0中以FP32累加中间结果。最后,L0中的C的tTile被变换并以FP16存储到L2。图5(d)(e)展示了NF4张量B为与tTile-device对齐而进行的详细变换,张量A的变换与此类似。具体来说,mma和ldmatrix指令要求L1中的数据类型为FP16。每一层也有其事务要求,如图5(b)所示。因此,TransformLoad_L1B对[16,63]进行切片并填充到[16,64],这与L2的事务要求对齐。然后,TransformLoad_L1B将其转换为FP16并映射到另一个元素布局,以与L1和L0的事务要求对齐。我们得到了L1中的FP16 L1_B[16,64]。接着,TransformLoad_L0B利用ldmatrix对L1_B进行切片,并在L0上得到FP16 L0_B[16,16],这与L1、L0和mma核心的要求对齐。tTile-graph的DNN计算,要将其调度到tTile-device上,我们可以将每个tTile-operator的tTile计算流水线(即Transform-Load、Compute和Transform-Store)映射到tTile-device上。具体来说,我们可以将每个tTile-operator划分为多个tTile以适应每个内存层的容量,调度tTile变换以使tTile与硬件层的要求对齐,并协调算子间的tTile配置和变换以进行整体优化。最终,整个tTile-graph被调度为一个数据流水线,其中一个tTile-operator节点的tTile在硬件层次结构中上下移动,并通过边传递给后继的tTile-operator节点。tTile-graph的调度空间变得非常大,因为tTile在DNN计算调度中开启了另一个维度(即张量变换)。此外,tTile变换引入了内存占用效率和延迟效率之间的新权衡,这给调度带来了更多的复杂性和挑战。以在NVIDIA GPU上计算FP16张量和NF4张量的MatMul为例,由于硬件支持限制,需要将NF4类型转换为FP16。此转换必须在从L1到L0的Transform-Load之前完成,因此可以安排在L2或L1进行。当转换在L2上进行时,会占用更多L2和L1的内存,但在后续从L2到L1再到L0的tTile移动中不会占用计算单元。当转换在L1上进行时,会节省L2的内存和L2的内存带宽,但会占用计算单元进行类型转换。当算子受计算单元限制时,前一个选项可以实现更低的延迟但内存占用更高。当算子受内存IO限制时,后一个选项在延迟和内存占用上都能实现更好的性能。此外,由于转换只需在从L1到L0的Transform-Load之前完成,此转换可以融合到前一个算子中执行,以实现更好的端到端性能。tTile,而上层通过与这个tTile表示的粒度对齐来决定最优的计算粒度,并进行变换。为了减少巨大的调度空间并在合理的时间内调度出合适的计划,LADDER采用了基于我们观察的启发式方法。tTile-graph的DNN模型g和表示为tTile-device的硬件规格D,并返回调度后的tTile-graph gret。Data: g: tTile-graph; D: tTile-device
Result: gret : scheduled tTile-graph
1 Function GetDeviceHint(g, D):
2 D = SelectDeviceConfig(g, D);
3 HintShape = None, HintGranularity = None;
4 for layer ∈ D.layers do
5 HintGranularity = LCM(HintGranularity, layer.tTile.type);
6 for layer ∈ D.layers do
7 layer.tTile = convert(layer.tTile, HintGranularity);
8 HintShape = LCM(HintShape, layer.tTile.shape);
9 for layer ∈ D.layers do
10 layer.tTile.shape = HintShape;
11 return D;
12 Function ScheduleTransform(op,D,lid):
13 tTileh = op.tTile[lid -1];
14 tTilel = op.tTile[lid ];
15 ScheduleSlice(tTilel , tTileh);
16 if LCM(tTilel .shape, tTileh.shape) != tTilel .shape then
17 SchedulePad(tTilel , tTileh, D);
18 if tTilel .type != tTileh.type then
19 ScheduleConvert(tTilel , tTileh, D);
20 if nBits(tTileh.shape[-1]) != nBits(D.layers[lid ].shape[-1]) then
21 ScheduleMap(tTilel , tTileh, D);
22 return op.transform[lid -1];
23 Function ScheduleConnectedGraph(g, D):
24 D = GetDeviceHint(g, D);
25 for lid in length(D.layers) do
26 for op ∈ g[lid ] do
27 op.tTile[lid ] = ScheduleTiling(op,D,lid );
28 if lid > 0 then
29 op.transform[lid ] = ScheduleTransform(op,D,lid );
30 g = ProfileAndSelect(g);
31 return g;
32 Function Schedule(g,D):
33 g = ExtractConnectedGraph(g, D);
34 for gconn ∈ g do
35 gconn = ScheduleConnectedGraph(gconn, D);
36 return g;
最初,该策略将图调度为子图(第33行)。每个子图表示一个计算流水线,它将`tTile`从最低内存层加载到核心,然后将结果存储回最低内存层。一个子图可以是一个`tTile-operator`或一组可以融合的`tTile-operator`。`ExtractConnectedGraph`可以利用现有的DNN编译器工作【索引13,Tvm: end-to-end optimization stack for deep learning,2018,arXiv;索引43,Welder: Scheduling deep learning memory access via tile-graph,2023,OSDI】。对于一个子图,它首先从硬件中推断出提示。具体来说,它首先选择合适的硬件配置(例如,计算核心)(第2行),偏好硬件支持的比特位最接近的`tType`。因为位数更多的数值类型通常需要更多的晶体管来实现硬件指令,并且通常性能较低。例如,在NVIDIA A100 GPU中,NF4类型可以转换为FP16或FP32进行处理,LADDER将选择FP16核心(312 TFlops)而不是FP32(19.5 TFlops)。然后,它通过比特对齐为每个硬件层找到对齐的粒度和形状,并配置提示(第1-11行)。以NVIDIA A100为例(图5(b)),`HintGranularity`是`ldmatrix`要求的16B,`HintShape`是`[4,8]`,其中内维是128B,与全局内存的32B事务和共享内存的128B事务对齐。然后,策略从顶层(即核心)到底层(即DRAM)逐层调度此子图(第25-29行)。在每一层,策略首先通过`ScheduleTiling`调度`tTile-operator`的分块并带有提示(第27行),然后调度`tTile`变换(第29行)。如果`ScheduleTiling`(第27行)调度的算子分块是`[4,8]`和16B的倍数,后续的`ScheduleTransform`可以使此调度与`tTile-device`对齐。此外,`ScheduleTiling`可以利用现有的张量编译器【索引13,Tvm: end-to-end optimization stack for deep learning,2018,arXiv;索引52,Ansor: Generating high-performance tensor programs for deep learning,2020,OSDI;索引57,Roller: Fast and efficient tensor compilation for deep learning,2022,OSDI】。在`ScheduleTransform`中,策略将检查形状和类型与`tTile-device`的对齐情况,并调度相应的变换来对齐`tTile`(第12-22行)。调度后可能会有一些候选方案,将对其进行性能评测并返回最佳方案(第30行)。
map变换中的map_func是非平凡的。LADDER提出了一种推断map_func的方法,即将tTile中的元素按行主序映射到所需的事务大小。图5(e)展示了一个例子:在16B的粒度下,为了将L0中的shape[16,2]映射到L1中所需的shape[8],元素按行主序被展平,得到shape[4,8]。map也可以支持其他的map_func。这种调度策略不保证最优。然而,如§5所示,这种调度策略已经可以超越最先进的技术,并能够在GPU上实现高效的低精度DNN计算。我们也希望,所提出的调度机制带来的这个优化空间可以被未来关于更先进调度策略的研究进一步探索。代码与依赖:LADDER的实现包含约5000行代码,包括Python和C++,基于开源DNN编译器:TVM【索引13,Tvm: end-to-end optimization stack for deep learning,2018,arXiv】、Welder【索引43,Welder: Scheduling deep learning memory access via tile-graph,2023,OSDI】和Roller【索引57,Roller: Fast and efficient tensor compilation for deep learning,2022,OSDI】。LADDER修改了TVM以实现核函数调度和生成核函数代码,同时利用Roller来推断高效的tTile配置。Welder是目前最先进的能够全面优化DNN模型的DNN编译器,被用于端到端的图优化。
工作流程:LADDER的输入是一个PyTorch程序。对于PyTorch内置的数据类型,LADDER不需要对DNN模型程序做任何修改。此外,对于PyTorch不支持的新数据类型,LADDER通过自定义算子扩展了PyTorch,以表达用户定义数据类型上的张量表达式。给定PyTorch程序,LADDER将其导出为ONNX图。LADDER也扩展了ONNX以表示新数据类型上的计算,其中tType注解的张量表达式保存在ONNX图节点的属性中。有了导出的ONNX图和目标硬件加速器的基于tTile的规范文件,LADD-ER会自动将ONNX图转换为tTile-graph并执行调度。然后,LADDER为目标硬件加速器生成设备代码。
硬件支持:我们为NVIDIA GPU和AMD GPU实现了LADDER,因为它们是DNN最流行的加速器。在本节的其余部分,我们将详细描述在NVIDIA GPU上的LADDER实现,并简要描述在AMD GPU上的实现。此外,如果新的硬件指令(例如最新Hopper GPU中的FP8张量核心)和其他硬件加速器(例如Graphcore IPU)符合基于tTile的硬件抽象并提供在硬件层次结构上加载和存储数据的编程接口,LADDER也可以移植到它们上面。
tType,例如FP32、FP16、INT8、FP8、MXFP、INT4、NF4、INT1。tTile中的元素和每个元数据。图7展示了在NVIDIA GPU上一个形状为[32,32]的MXFP8 tTile的存储方式。元素存储在一个数组中,而共享的缩放因子存储在另一个数组中。为了访问一个tTile,连续的线程处理连续的元素,从而实现合并访问。nElemBits不是2的n次方,例如3比特【索引22,Gptq: Accurate post-training quantization for generative pre-trained transformers,2022,arXiv】。为了支持这些数据类型,LADDER以4B的粒度进行存储,这是由于GPU的规格所致,例如10个3比特的值可以存储在一个4B(32位)的粒度中。cp.async指令来支持Ampere GPU上的新异步内存复制功能【索引3,NVIDIA A100 Tensor Core GPU Architecture】。此外,我们观察到将低比特整数(如INT4)转换为浮点数(如FP16)可能会引入显著的开销。LADDER使用LOP3指令【索引8,PTX ISA,https://docs.nvidia.com/cuda/parallel-thread-execution/index.html】实现了低于4比特的整数转换。我们修改了TVM中的代码生成模块以实现这些优化 。tTile配置的四层tTile-device。硬件配置:
软件配置:
模型与数据集:
数据类型配置:
WtypeAtype),均来自前沿研究文献,确保了模型质量。评测场景:
端到端推理延迟 (图8, 9, 10)
内存使用 (图11)
编译时间 (表2)
算子基准测试 (图12, 13)
优化分解 (图14)
比特宽度扩展性 (图15)
低精度LLM的效率与准确性 (图16)
本文介绍了LADDER,这是首个旨在优化通用低精度计算在GPU等加速器上的深度学习编译器。LADDER通过暴露一个通用的类型系统(tType)和扩展的张量表达式,使用户能够轻松实现和表达深度学习中的新数据类型。它引入了一套新的张量调度原语,以促进计算流水线中张量存储、访问和类型转换等方面的优化。LADDER的分层、硬件感知的优化策略能够驾驭复杂的变换空间,展示了其系统性支持各种低比特精度自定义数据类型的能力。这在无需硬件修改的情况下,提升了现代加速器上DNN计算的性能。这项创新不仅赋能模型设计者探索数据类型优化,也为硬件供应商提供了一个灵活的解决方案来扩展对多样化精度格式的支持。