1. 背景介绍
针对我们今天要讨论的话题,从第一性原则出发,要回答的第一个问题就是,为什么要计算大模型占用的显存资源?一句话概括:显存太小,模型无法运行;显存太大,浪费金钱。所以从成本的角度来看,很有必要分析计算大模型的资源占用【1】。
当你手头想要部署某个开源大模型,你的老板可能会问你,需要多大的资源?这时候需要你来确定使用哪种GPU来运行模型。
本文是一篇扫盲帖,主要介绍什么是显存,显存对大模型的意义,以及如何为大模型选择合适的GPU。
2. 显存含义
显存,也称为视频随机存取存储器(Video Random Access Memory,VRAM),是一种用于GPU的特殊内存类型。是用于存储图形处理器(GPU)处理数据的专用存储器。显存的主要作用是为GPU提供快速访问数据的能力,包括图像、纹理、帧缓存等。显存的大小和速度直接影响图形处理能力,特别是在3D渲染、高分辨率视频处理、深度学习和其他需要大量数据处理的任务中。GPU都会配备一定的VRAM【2】,如下图所示。
与标准系统RAM相比,VRAM提供了更高的带宽和较低的延迟。GPU和其内存之间的数据传输速度更快,允许GPU快速读写数据,从而加快图形渲染和数据计算的速度。它与GPU直接集成在一起,通常具有更高的传输速度和专门优化的架构。
与系统RAM不同,系统RAM与CPU和其他组件共享,而VRAM则专用于GPU。确保GPU可以不间断地访问内存,从而提高了性能的稳定性和可预测性。GPU通过允许应用程序中的重复计算并行运行来补充CPU架构,而主程序则继续在CPU上运行。CPU可以被认为是整个系统的任务管理者,负责协调各种通用计算任务,而GPU则执行范围更窄但更专业化的任务(通常是数学计算)。利用并行计算的强大能力,GPU可以在相同的时间内完成比CPU更多的工作。CPU和GPU各有优势,CPU擅长处理复杂的逻辑和串行任务,而GPU擅长处理并行计算任务。显存则是GPU发挥其计算能力的重要支撑,足够大的显存容量和带宽可以防止因为数据传输瓶颈而导致的性能下降【3】。
3.张量核心(Tensor Cores)以及与显存的关系
在这里,顺便提一下,从刚才章节2中的GPU配置表,我们还可以发现,GPU携带了两种关键组成:Cuda Cores(CUDA 核心)和Tensor Cores(张量核心),这个配置信息对于GPU的选择也是尤为重要【4】。CUDA 核心和 Tensor Cores 并不相同。各自有不同的用途,并针对不同类型的计算任务设计。CUDA(Compute Unified Device Architecture)核心是 NVIDIA 于 2007 年开发的,是 GPU 的基本处理单元,能够处理广泛的并行计算任务。CUDA 核心在顺序处理方面表现出色,是传统图形渲染、物理仿真和通用 GPU 计算的主力。另一方面,Tensor Cores 是 NVIDIA 在 2017 年推出的 Volta 架构中引入的专用处理单元,并在后续几代产品中得到进一步改进。这些核心专门设计用于加速深度学习,特别是神经网络计算中常见的矩阵乘法和卷积操作。下表列出了一些差异点:
对比项 | CUDA Cores | Tensor Cores |
用途 | 通用并行计算 | 专为深度学习加速 |
精度 | 主要为FP32和FP64 | 混合精度(FP16、FP32、INT8、INT4) |
工作类型 | 各种GPU计算任务 | 优化用于矩阵乘法和卷积操作 |
编程类型 | CUDA编程模型 | 通过高级AI框架和CUDA访问 |
架构 | 基于单指令、多线程模型 | 围绕矩阵乘法累加引擎设计 |
适合使用场景 | 传统图形渲染 | 深度学习训练和推理 |
性能 | 通用GPU计算中的高性能 | 在矩阵操作中提供高吞吐量 |
从对比项目来看,对于深度学习、大模型任务来说,Tensor Cores显得更为关键。Tensor Cores 支持混合精度计算(FP16、INT8、INT4 等),在不显著降低计算精度的情况下,减少计算所需的内存和带宽。这使得 Tensor Cores 在处理深度学习工作负载时,比只支持单一精度(如 FP32、FP64)的 CUDA Cores 更高效【11, 12】。
大模型通常有数十亿个参数,使用混合精度计算可以显著减少显存占用,同时加速计算。Tensor Cores 的设计目的就是为了提供大规模并行计算的更高吞吐量。在处理特定类型的任务(如矩阵运算)时,它们能比 CUDA Cores 提供更高的计算能力。
Tensor Cores 与 CUDA Cores共享 VRAM(显存),在 NVIDIA GPU 架构中,显存(VRAM)是一个公共资源,供所有类型的计算单元(包括 CUDA Cores 和 Tensor Cores)使用【5】。如下图所示:
4. 显存对于大模型的重要性
4.1 影响大模型显存占用的关键项
显存对于大模型很重要,在训练和推理过程中涉及大量计算。特别是使用Transformer架构的大模型(如GPT系列、Llama系列等),高度依赖并行计算。模型由多层组成,每层包含的参数(权重)需要加载到显存中,以支持快速高效地访问。Transformer中的注意力机制涉及词元之间的上下文和关系,需要大量的显存。
大模型利用显存高效处理推理过程中的大量数据和计算,相应的使用方式主要包含如下:
- 模型参数:常见的大模型目前涉及数十亿甚至含数万亿的参数,在推理过程中这些参数存储在显存中。这是当前大模型显存使用的主要部分。
- 激活值:大模型的每一层计算,都可能会生成大量的激活数据,这些中间结果数据会暂时存储在显存中,以便后续计算。
- 批处理大小:大模型通过批量处理输入来提高效率。较大的批处理size需要更多显存来同时容纳多个输入。
- 精度:大模型使用的数值精度会明显影响显存使用。精度指的是用于推理的不同类型的浮点数。较低的精度(如FP16)能够减少显存需求,使得在同样的显存大小内可以容纳更大的模型或批处理,且对准确性的影响较小。
4.2 精度
这里我们对精度展开讲一下,在大模型的量化或者优化层面,经常会看到对精度的选择。模型训练和推理的速度随着大模型的推广使用变得越来越重要,减小计算过程中数据的长度从而降低存储和带宽,该方案能够快速提升大模型的计算效率。
浮点精度是一种通过二进制表示数字的方式【6,7】。最常见的浮点精度格式有半精度(FP16)、单精度(FP32)和双精度(FP64),每种格式在特定应用中都有各自的优点、缺点和适用性。浮点精度的结构设计可以定义广泛的数值范围。第一个二进制位表示数的正负号;接下来的二进制位表示以2为底的指数,用于表示整数部分;最后一组二进制位表示有效位(也称为尾数),用于表示小数点后的数值。
FP32 或单精度 32 位浮点精度使用 32 位二进制来表示数值。这种格式是使用最广泛的浮点精度格式,它在一定程度上牺牲了部分精度,以较少的数字表示更轻量的数值。较少的数字占用更少的内存,从而提高计算速度。
FP32 单精度使用:
- 1 位表示正负号;
- 8 位表示以 2 为底的指数;
- 23 位表示小数部分(也称为有效位或尾数)。
FP64 或双精度 64 位浮点精度使用 64 位二进制来表示系统计算中的数值。该格式在主流选项中提供最高的精度,非常适合对精度和准确性有严格要求的应用。
FP64 双精度使用:
- 1 位表示正负号;
- 11 位表示以 2 为底的指数;
- 52 位表示小数部分(也称为有效位或尾数)。
进一步,对FP16、BF16、T32做介绍:
FP16 或半精度 16 位浮点精度仅使用 16 位二进制。这种格式在深度学习工作负载中正呈上升趋势,逐渐取代传统的 FP32。由于神经网络中的较低精度权重似乎并不会对模型的性能产生重大影响,FP32 的额外精度可以被用来换取更高的计算速度。 FP16 主要用于深度学习模型的训练和推理,以实现快速计算,在单位时间内执行更多的计算。
FP16 半精度使用:
- 1 位表示正负号;
- 5 位表示以 2 为底的指数;
- 10 位表示小数部分(也称为有效位或尾数)。
BF16 或 BFloat16 是由谷歌开发的一种格式,称为“Brain Floating Point Format”(大脑浮点格式)。谷歌发现 FP16 并未考虑深度学习的应用,因为其范围过于有限。虽然仍然使用 16 位二进制,他们重新分配了位数,使其范围更接近 FP32。由于 BF16 和 FP32 使用相同数量的位表示指数,将 FP32 转换为 BF16 更加快速且简单,只需忽略部分小数位。 BF16 正在成为运行和训练原本使用 FP32 的深度神经网络时替代 FP16 的标准。BF16 也是在 FP32 和 BF16 之间进行混合精度计算的理想选择。由于 BF16 在指数方面与 FP32 相似,它可以作为一种直接替代,提供较少小数位的精度,同时使用的位数减半。存储 BF16 所需的内存较低,加上转换的简便性,在原生 FP32 计算的工作负载中使用 BF16 可以显著提高速度。
BF16 使用:
- 1 位表示正负号;
- 8 位表示以 2 为底的指数;
- 7 位表示小数部分(即有效位或尾数)。
TF32 或 Tensor-Float32 是由 NVIDIA 制作的一种数学模式,将 FP32 的 32 位表示值压缩为 19 位。类似于 BF16 的功能,TF32 使用相同的 8 位来定义指数,以保持与 FP32 相同的范围和易于转换的特性,同时使用来自 FP16 的 10 位尾数/小数部分。由于使用了与 FP32 相同的 8 位指数,TF32 是 NVIDIA GPU 执行 FP32 计算的一种方式,它将 FP32 的 23 位尾数缩短并舍入为 TF32 的 10 位。TF32 与 FP32 浮点数的比较: 从技术上讲,TF32 是一种 19 位二进制格式,是在表示值的小数部分上精度较低的 FP32。NVIDIA Ampere 及以后的 Tensor Cores 在进行矩阵乘法时,使用将 FP32 输入转换为 TF32 的方式来进行操作,并将矩阵乘积的结果输出为 FP32 以便进一步计算。其工作过程如下:在进行矩阵运算时,FP32 首先通过将 23 位尾数/小数部分四舍五入为 10 位或第 10 位小数,转换为 TF32。接下来执行 FP32 矩阵计算,然后将输出的 TF32 值重新定义为 FP32 值【】。后续的非矩阵乘法操作将使用 FP32。
TF32 或 Tensor-Float32 使用:
- 1 位表示正负号;
- 8 位表示以 2 为底的指数;
- 10 位表示小数部分(即有效位或尾数)。
至于整型数的表示,则更加简单,只需要去除尾数部分即可。【8】以表格形式展示以上的数据类型精度,可以更好地帮助理解,可以参考下。这里注意一下,这里面的数值精度列是指保留的十进制小数部分的位数。
对于不同的精度,GPU架构和版本的不同,支持的力度存在差异【9】。因此对于模型的训练、推理涉及的数值精度,需要按照实际情况选择GPU型号。
4.3 实际案例
有了上述知识背景,我们来看一个具体的案例【13】:
假设你的任务是需要运行一个具有七十亿参数的大模型,比如Llama 2或Qwen2。在生产环境中运行和提供大模型服务需要使用GPU,应该选择什么样的GPU呢?
如果给你一个选择是A10型号,以下是A10的关键规格,需要你判断是否可行?
A10 关键规格
- FP32(单精度浮点运算): 31.2 TFLOPS
- TF32 Tensor Core(张量核心TF32精度): 62.5 TFLOPS | 125 TFLOPS(稀疏优化)
- BFLOAT16 Tensor Core(张量核心BFLOAT16精度): 125 TFLOPS | 250 TFLOPS(稀疏优化)
- FP16 Tensor Core(张量核心FP16精度): 125 TFLOPS | 250 TFLOPS(稀疏优化)
- INT8 Tensor Core(张量核心INT8精度): 250 TOPS | 500 TOPS(稀疏优化)
- INT4 Tensor Core(张量核心INT4精度): 500 TOPS | 1000 TOPS(稀疏优化)
- GPU 内存: 24 GB GDDR6
- GPU 内存带宽: 600 GB/s
- 最大功耗(TDP): 150W
在推理过程中,我们主要关注三个关键指标:
- FP16 Tensor Core(张量核心FP16精度):在半精度(FP16)下有125 TFLOPS(每秒万亿次浮点运算)的计算能力。半精度是一种二进制数字格式,每个数字占用16位,使用半精度是因为它在不丧失精度的情况下需要更少的内存。
- GPU 内存:通过将参数数量(以十亿为单位)乘以2来快速估算模型的大小(以GB为单位)。这种方法基于一个简单的公式:每个参数在半精度下使用16位(即2字节)内存,因此内存使用量大约是参数数量的两倍。例如,一个七十亿参数的模型大约占用14GB的内存。A10具有24GB的显存(VRAM),因此可以轻松运行一个七十亿参数的模型,并且仍然有大约10GB的剩余内存作为缓冲。这个备用内存在模型执行中起着重要作用。
- GPU 内存带宽:可以以600 GB/s的速度从GPU内存(也称为HBM或高带宽内存)向片上处理单元(也称为SRAM或共享内存)移动数据。
显然,从精度支持、内存支持维度,A10显然是可以满足需求。接下来我们再来看看GPU内存带宽这个维度是否能满足?通过上述数据,可以计算出硬件的ops比率。ops比率,在计算机硬件性能评估中,是指每字节数据的运算次数,即操作数与字节数的比率(operations per byte),用于衡量硬件在处理数据时的计算效率,是判断计算是受计算能力限制还是受内存带宽限制的一个重要指标。根据规格表中的数据,我们计算出 A10 的 ops比率:
ops_to_byte_A10 = compute_bw / memory_bw = 125 TF / 600 GB/S = 208.3 ops / byte
为了充分利用计算资源,必须在每次内存访问时完成 208.3 次浮点操作,一旦发现每字节只能完成少于 208.3次浮点操作,那么说明系统性能受到了内存带宽的限制。本质上意味着系统的速度和效率受限于数据传输速度或输入输出操作的处理能力。如果要在每字节完成超过 208.3 次浮点操作,那么说明系统反而是计算受限。在这种状态下,效能和性能受到限制的不是内存带宽,而是芯片所拥有的计算单元的数量。
接下来需要计算模型算术强度。为了确定我们是受内存带宽限制还是受计算限制。需要计算 70 亿参数的大模型的算术强度,然后将其与刚刚计算出的 GPU 的 ops比率进行比较。算术强度是算法所需的计算操作数除以其所需的字节访问数的比率,是一种与硬件无关的衡量标准。70 亿参数的 大模型在推理过程中计算量最大的部分是注意力层,因此我们在此处计算模型的算术强度。
首先梳理注意力层在底层是如何工作的,该过程分为两个阶段:
-
预填充(Prefill):在第一阶段,模型并行地接收提示 token,填充键-值(KV)缓存。KV 缓存可以被认为是模型的状态,嵌套在注意力操作中。在预填充阶段,没有生成 token。
-
自回归采样(Autoregressive sampling):在该阶段,利用当前状态(存储在 KV 缓存中)来采样和解码下一个 token。关于KV 缓存可以看之前的文章《Transformer KV Cache原理深入浅出》。
回顾一下attention的计算公式:
在标准计算过程【14】中,其过程如下:
- 从HBM(高带宽存储器)中以块的形式加载Q和K,计算S = QKᵀ,并将S写入HBM。
- 从HBM中读取S,计算P = softmax(S),并将P写入HBM。
- 从HBM中以块的形式加载P和V,计算O = PV,并将O写入HBM。
- 返回O。
再来看下7B模型的参数配置:
N 是大模型的序列长度,设置了上下文窗口。
- 对于 Llama 2 7B,N = 4096。
d 是单个注意力头的维度, 有32个头。
- 对于 Llama 2 7B,d = 128。
Q, K, V 都是用于计算注意力的矩阵。
- 它们的维度是 N x d,在本例子中是 4096 x 128。
S 和 P 是在方程中计算出的两个矩阵。
- 它们的维度是 N x N,在本例子中是 4096 x 4096。
O 是注意力计算结果的输出矩阵。
- 它是一个 N x d 的矩阵,在本例子中是 4096 x 128。
那么基于上述的参数,有以下的内存项分布:
首先计算第一列和第三列相加(从内存加载的数据和存储到内存的数据)来计算总内存移动量。
总内存移动量(bytes)= (2 * 2 * (N * d)) + (2 * (N * N)) + (2 * ((N*N) + (N * d))) + (2 * (N * N)) + (2 * (N * N)) + (2 * (N * d))= 8N^2 + 8Nd bytes
接着处理第二列相加(对加载数据的计算)来计算总计算量。
总计算量(floating_point_ops)= ((2 * d) * (N * N)) + (3 * (N * N)) + ((2 * N) * (N * d))= 4(N^2)d + 3N^2 ops
那么算术强度为:
llama算术强度 ~= total compute / total memory movement= 4d(N^2) + 3N^2 ops / 8N^2 + 8Nd bytes= 62 ops/byte
对于Llama 2 7B模型,我们计算出来其算术强度为每字节62次操作,这远低于刚才A10的操作/字节比率208.3。所以说,在自回归阶段,模型受制于内存带宽,也就是在将一个字节从内存移动到计算单元所需的时间内,可以完成远远超过一个字节的计算。这反映出,我们没有充分发挥出GPU的计算能力。
一种解决方案是利用额外的片上内存以批处理的方式通过模型运行前向传递。也就是,通常我们可以采用近实时的处理模式,等待几百毫秒后积累一批请求,然后一次性运行所有请求,而不是实时地逐个处理到达的请求。这样能够重用已经加载到GPU的SRAM中的模型部分。批处理通过在相同的内存加载和存储数量下执行更多计算,增加了模型的算术强度,从而减少了模型受内存带宽限制的程度。
那么问题又来了,该设置多大的批次数?
刚才我们计算出来7B llama的参数大概能占用14GB,而A10提供了24GB的显存,所以还有10GB的预留。接下来也就是需要计算出剩余GPU内存能够容纳的序列数量。
回到KV缓存模块,在注意力层的预填充步骤中,我们根据提示(或输入序列)填充KV缓存。KV缓存包含我们在注意力计算期间使用的矩阵K和V。这里需要用到一些之前的值以及一些新的值来计算KV缓存的大小:
d
(记作d_head
)是单个注意力头的维度。对于Llama 2 7B,d = 128
。n_heads
是注意力头的数量。对于Llama 2 7B,n_heads = 32
。n_layers
是注意力块出现的次数。对于Llama 2 7B,n_layers = 32
。d_model
是模型的维度。d_model = d_head * n_heads
。对于Llama 2 7B,d_model = 4096
。
在半精度(FP16)下,每个浮点数占用2字节存储空间。有两个矩阵(k, v),要计算KV缓存的大小,我们将两者都乘以n_layers
和d_model
,得出以下公式:
kv_cache_size= (2 * 2 * n_layers * d_model) bytes/token= (4 * 32 * 4096) bytes/token= 524288 bytes/token~ 0.00052 GB/token
kv_cache_tokens= 10 GB / 0.00052 GB/token= 19,230 tokens
从结果来看,10GB可以容纳19,230个token。因此,对于Llama 2标准的4096个token序列长度,系统可以同时处理4个序列的批次。在推理期间每次批处理4个请求来填满KV缓存。可以大幅增加吞吐量。
以上我们讨论了如果内存带宽成为限制的情况下,如何提高吞吐量。接下来我们再来讨论下如果要考虑GPU价格的基础上,如何在推理场景做速度和成本之间的平衡。 例如某些场景下批处理可能不合适。比如延迟敏感的聊天机器人,显然不能等个几百毫秒。因此我们不能充分利用GPU的片上内存并缩小规模。这个时候就可以考虑选择16 GB VRAM的T4 GPU。该7B参数模型仍然能够支持运行,但剩余的内存容量更少,只有2 GB。相对来说更便宜。
GPU | 推理时间 | 价格 |
T4 | 6.98 seconds | $0.01052/min |
A10 | 3.49 seconds | $0.02012/min |
4.4 一种GPU内存占用估算公式
我们刚讨论了影响大模型显存占用大小的影响因素以及给出了一个具体的案例讲解。这里再介绍一下看到的内存占用估算方法, 仅供参考。【1】给出了如下计算公式,也就是在参数量乘对应精度byte的基础上,再乘1.2的系数。
另外汇总了一个表格,对常见大模型在固定序列长度、批处理大小和单个GPU的情况下所需的显存大小进行展示。
5. 显存需求计算在线工具
此外,除了上述估算公式外,还有一些估算显存需求的在线工具:
GPU-poor【15】可以基于你给出的配置,直接估算出模型内存占用量。如下图所示:
huggingface也给出了一个模型内存计算器【16】,可以通过输入模型的访问url直接计算。
6. 结论
总之,显存是GPU的专用内存,负责处理大模型所需的大量计算和数据,并提供高带宽来支持数亿到数十亿参数的计算。为了避免因显存不足导致大模型运行失败,或因显存过多而浪费成本,准确估算所需的最低显存量非常重要。影响显存需求的主要因素包括模型参数、激活值、批处理大小和计算精度。由于模型参数占用显存最多,可以使用上述的估算公式或在线计算器等工具来确定所需的资源量。
7. 参考材料
【1】Understanding VRAM and how Much Your LLM Needs
【2】The Best NVIDIA GPUs for LLM Inference: A Comprehensive Guide
【3】Understanding Nvidia CUDA Cores: A Comprehensive Guide
【4】Tensor Cores vs CUDA Cores: The Powerhouses of GPU Computing from Nvidia
【5】FlashAttention: Understanding GPU Architecture
【6】Defining Floating Point Precision - What is FP64, FP32, FP16?
【7】Understanding Mathematics behind floating-point precisions
【8】深度学习中的数据类型介绍:FP32, FP16, TF32, BF16, Int16, Int8
【9】Understanding NVIDIA’s Tensor Core Technology
【10】NVIDIA A100 Tensor Core GPU Architecture
【11】Understanding Tensor Cores
【12】Using Tensor Cores for Mixed-Precision Scientific Computing
【13】A guide to LLM inference and performance
【14】FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness
【15】Are you GPU poor?
【16】Model Memory Calculator