摘要: vLLM 在 AMD MI300X 上释放了惊人的性能,针对 Llama 3.1 405B,其吞吐量比 Text Generation Inference (TGI) 高出 1.5 倍,首字延迟 (TTFT) 快 1.7 倍。针对 Llama 3.1 70B,其吞吐量比 TGI 高 1.8 倍,TTFT 快 5.1 倍。本指南探讨了最大化效率的 8 个关键 vLLM 设置,向您展示如何利用 AMD 上的开源 LLM 推理力量。如果您只想查看最佳参数,请直接跳至 快速入门指南

   
Llama 3.1 405B 在 8 x MI300X 上的 vLLM 与 TGI 性能对比(BF16, 32 QPS)。

   
Llama 3.1 70B 在 8 x MI300X 上的 vLLM 与 TGI 性能对比(BF16, 32 QPS)。

引言

Meta 最近宣布,他们 Llama 3.1 405B 模型的全部实时流量都运行在 AMD MI300X GPU 上,这展示了 AMD ROCm 平台在大语言模型 (LLM) 推理方面的强大实力和就绪状态。这一令人振奋的消息恰逢 ROCm 6.2 发布,该版本对 vLLM 支持带来了显著改进,使得利用 AMD GPU 进行 LLM 推理比以往任何时候都更加容易。

ROCm 是 AMD 对标 CUDA 的方案,对某些人来说可能不太熟悉,但它正迅速成熟为一个强大且高性能的替代方案。有了 vLLM,利用这种力量变得前所未有的简单。我们将向您展示如何操作。

vLLM 对比 TGI

vLLM 在 AMD MI300X 上释放了惊人的性能,针对 Llama 3.1 405B,其吞吐量比 TGI 高 1.5 倍,首字延迟 (TTFT) 快 1.7 倍。针对 Llama 3.1 70B,其吞吐量比 TGI 高 1.8 倍,TTFT 快 5.1 倍。

在 Llama 3.1 405B 上,在各种每秒查询数 (QPS) 场景下,vLLM 的首字延迟 (TTFT) 和吞吐量均显著优于 TGI。在 TTFT 方面,在优化配置下,vLLM 在 16 QPS 时的响应速度比 TGI 快约 3.8 倍。在吞吐量方面,vLLM 始终优于 TGI,在 1000 QPS 的 ShareGPT 数据集上,优化设置下的最高吞吐量为 5.76 请求/秒,而 TGI 为 3.55 请求/秒。

即使在默认配置下,vLLM 的表现也优于 TGI。例如,在 16 QPS 时,vLLM 的默认配置实现了 4.05 请求/秒的吞吐量,而 TGI 为 2.58 请求/秒。这种性能优势在不同的 QPS 水平上都得以保持,凸显了 vLLM 在处理大语言模型推理任务方面的高效性。



Llama 3.1 405B 在 8 x MI300X 上的 vLLM 与 TGI 性能对比(BF16, QPS 分别为 16, 32, 1000;命令见附录)。

如何以最佳性能运行 vLLM

关键设置与配置

我们对各种 vLLM 设置进行了广泛测试,以确定 MI300X 的最佳配置。以下是我们的发现:

  • 分块预填充 (Chunked Prefill):经验法则是在 MI300X 的大多数情况下暂时禁用它,以获得更好的性能。
  • 多步调度 (Multi-Step Scheduling):通过多步调度可以显著提高 GPU 利用率和整体性能。将 --num-scheduler-steps 设置为 10 到 15 之间的值,以优化 GPU 利用率。
  • 前缀缓存 (Prefix Caching):在特定场景下,将前缀缓存与分块预填充结合使用可以增强性能。但是,如果用户请求的前缀缓存命中率较低,建议同时禁用分块预填充和前缀缓存。
  • 图捕获 (Graph Capture):在处理支持长上下文长度的模型时,将 --max-seq-len-to-capture 设置为 16384。但请注意,增加此值并不总能保证性能提升,有时由于桶大小 (bucket sizes) 不理想,反而可能导致性能下降。
  • AMD 特定优化:禁用 NUMA 平衡并调整 NCCL_MIN_NCHANNELS 可以进一步提升性能。
  • KV 缓存数据类型:为了获得最佳性能,请使用默认的 KV 缓存数据类型,它会自动匹配模型的数据类型。
  • 张量并行 (Tensor Parallelism):为了优化吞吐量,请使用能容纳模型权重和上下文的最小张量并行 (TP) 度,并运行多个 vLLM 实例。为了优化延迟,请将 TP 设置为等于节点内的 GPU 数量。
  • 最大序列数:为了优化性能,根据 GPU 的内存和计算资源,将 --max-num-seqs 增加到 512 或更高。这可以显著提高资源利用率和吞吐量,特别是对于处理短输入和输出的模型。
  • 使用 CK Flash Attention:CK Flash Attention 的实现比 Triton 实现快得多。

详细分析与实验

案例 1:分块预填充 (Chunked Prefill)

分块预填充是 vLLM 中的一项实验性功能,它允许将大型预填充请求划分为较小的块,并与解码请求一起批处理。这通过使计算密集型的预填充请求与内存密集型的解码请求重叠,从而提高了系统效率。您可以通过在 LLM 构造函数中设置 --enable_chunked_prefill=True 或使用 --enable-chunked-prefill 命令行选项来启用它。

根据我们运行的实验,我们发现调整分块预填充值比禁用该功能有轻微改进。但是,如果您不确定是否启用分块预填充,只需从禁用它开始,通常可以预期获得比使用默认设置更好的性能。这是针对 MI300X GPU 的特性。




案例 2:调度器步数 (Number of scheduler steps)

vLLM v0.6.0 中引入了多步调度,承诺提高 GPU 利用率和更好的整体性能。正如这篇 博客文章 中详述的,这种性能提升背后的魔力在于它能够一次性执行调度和输入准备,并连续运行模型多个步骤而无需中断 GPU。通过巧妙地在这些步骤中分散 CPU 开销,它极大地减少了 GPU 空闲时间并大幅提升了性能。

要启用多步调度,请将 --num-scheduler-steps 参数设置为大于 1 的数字(默认值为 1)。值得一提的是,我们发现多步调度的值越高,收益递减越明显,因此我们坚持将上限设为 15。




案例 3:分块预填充和前缀缓存

分块预填充和前缀缓存是 vLLM 中的优化技术,分别通过将大型预填充分解为较小的块以实现高效批处理,以及重用跨查询的共享前缀的缓存 KV(键值)计算来提高性能。

默认情况下,如果模型的上下文长度超过 32k 标记,vLLM 将自动启用分块预填充功能。用于预填充的分块最大标记数默认设置为 512。

在深入研究图表之前,我们先解释实验中使用的术语。Fresh Run 指的是前缀缓存内存完全没有被填充的情况。2nd Run 指的是在 Fresh Run 之后再次运行基准测试脚本。通常,在 2nd Run 重新运行 ShareGPT 基准数据集时,我们会获得大约 50% 的前缀缓存命中率。

观察下面的图表,我们可以对这个实验得出三个结论:

  1. 基于柱状图 2(红色)与基准线(蓝色)的对比,性能有巨大提升。
  2. 基于柱状图 3(黄色)、柱状图 5(橙色)和柱状图 6(青色)与基准线的对比,分块预填充的性能取决于用户请求输入提示词长度的分布。
  3. 在我们的实验中,我们发现柱状图 3(黄色)和柱状图 4(绿色)的前缀缓存命中率分别在 0.9%50% 左右。基于柱状图 3(黄色)和柱状图 4(绿色)与基准线及柱状图 2(红色)的对比,这告诉我们:如果用户请求没有较高的前缀缓存命中率,那么同时禁用分块预填充和前缀缓存可能是一个很好的经验法则。




案例 4:要捕获的最大序列长度 (Max sequence length to capture)

vLLM 中的 --max-seq-len-to-capture 参数控制 CUDA/HIP 图可以处理的最大序列长度,图捕获通过捕获并重放 GPU 操作来优化性能。如果序列超过此长度,系统将退回到 Eager 模式逐个执行操作,这可能效率较低。这适用于常规模型和编码器-解码器模型。

我们的基准测试揭示了一个有趣的趋势:增加 --max-seq-len-to-capture 并不总是能提高性能,有时甚至会降低性能。这可能是由于 vLLM 为不同序列长度创建“桶”(buckets) 的方式造成的。

原因如下:

  • 分桶 (Bucketing):vLLM 使用桶来对相似长度的序列进行分组,从而为每个桶优化图捕获。
  • 最佳分桶:最初,桶的粒度很细(例如 [4, 8, 12,…, 2048, 4096]),允许对各种序列长度进行高效的图捕获。
  • 粗粒度分桶:增加 --max-seq-len-to-capture 可能导致桶变得更加粗糙(例如 [4, 8, 12, 2048, 8192])。
  • 性能影响:当输入序列落入这些更大、精度更低的桶中时,捕获的 CUDA/HIP 图可能不是最优的,从而可能导致性能下降。

因此,虽然使用 CUDA/HIP 图捕获更长的序列看起来有益,但考虑其对分桶和整体性能的潜在影响至关重要。找到最佳的 --max-seq-len-to-capture 值可能需要通过实验来平衡图捕获效率与特定工作负载的合适桶大小。




为了进一步优化 vLLM 在 AMD MI300X 上的性能,我们可以利用 AMD 特定的环境变量。

  • 禁用 NUMA 平衡:非统一内存访问 (NUMA) 平衡有时会阻碍 GPU 性能。正如 AMD MAD 仓库 中推荐的,禁用它可以防止潜在的 GPU 挂起并提高整体效率。可以使用以下命令实现:
      # disable automatic NUMA balancing
      sh -c 'echo 0 > /proc/sys/kernel/numa_balancing'
      # check if NUMA balancing is disabled (returns 0 if disabled)
      cat /proc/sys/kernel/numa_balancing
      0
    
  • 调整 NCCL 通信:NVIDIA 集体通信库 (NCCL)(在 AMD 上为相应兼容库)用于 GPU 间通信。对于 MI300X,AMD vLLM 分支性能文档 建议将 NCCL_MIN_NCHANNELS 环境变量设置为 112,以潜在地增强性能。

在我们的测试中,启用这两个配置带来了轻微的性能提升。这与 “NanoFlow: Towards Optimal Large Language Model Serving Throughput” 论文 的发现一致,该论文指出虽然优化网络通信是有益的,但由于 LLM 推理主要由计算密集型和内存密集型操作主导,因此影响可能有限。

尽管收益可能很小,但微调这些环境变量有助于从您的 AMD 系统中榨取最大性能。




案例 6:KV 缓存类型 Auto/FP8

默认情况下,vLLM 会自动分配与模型数据类型匹配的 KV 缓存类型。但是,vLLM 在 MI300X 上也支持原生 FP8,我们可以利用它来降低 KV 缓存的内存需求,从而增加模型可部署的上下文长度。

我们尝试使用 Auto KV 缓存类型和 FP8 KV 缓存类型,并将其与默认基准进行比较。从下图可以看出,使用 Auto KV 缓存类型(红色)比将 KV 缓存类型设置为 FP8(黄色)获得了更高的每秒请求率。理论上,这可能是由于 Llama-3.1-70B-Instruct (bfloat16) 模型中的量化开销造成的,但由于开销成本看起来很小,在某些情况下,为了大幅减少 KV 缓存需求,这仍然是一个不错的权衡。




案例 7:TP 4 与 TP 8 之间的性能差异

张量并行 (Tensor Parallelism) 是一种分配大型模型计算负载的技术。它通过将单个张量拆分到多个设备上来工作,从而允许对特定操作或层进行并行处理。这种方法减少了模型的内存占用,并实现了跨多个 GPU 的扩展。

虽然增加张量并行度可以通过提供更多计算资源来提高性能,但收益并不总是线性的。这是因为随着更多设备的参与,通信开销会增加,并且每个 GPU 上的工作负载会减少。鉴于 MI300X 强大的处理能力,每个 GPU 过小的工作负载实际上可能导致利用率不足,进一步阻碍性能扩展。

因此,在优化吞吐量时,我们建议启动多个 vLLM 实例,而不是激进地增加张量并行度。这种方法往往能产生更线性的性能提升。但是,如果首要任务是最小化延迟,那么增加张量并行度可能是更有效的策略。




案例 8:最大(并行)序列数的影响

--max-num-seqs 参数指定了每次迭代可以处理的最大序列数。此参数控制批处理中的并发请求数,影响内存使用和性能。在 ShareGPT 基准测试中,由于样本的输入和输出长度较短,托管在 MI300X 上的 Llama-3.1-70B-Instruct 每次迭代可以处理大量请求。在我们的实验中,即使 --max-num-seqs 设置为 1024,它仍然是一个限制因素。




快速入门指南

如果您不确定部署设置和用户请求的分布,可以:

  • 使用 CK Flash Attention*(虽然我们这里没有展示,但 CK Flash Attention 的实现比 Triton 对应实现快得多)
    • export VLLM_USE_TRITON_FLASH_ATTN=0
  • 禁用分块预填充 --enable-chunked-prefill=False
  • 禁用前缀缓存
  • 如果模型支持长上下文长度,将 --max-seq-len-to-capture 设置为 16384
  • --num-scheduler-steps 设置为 10 或 15。
  • 设置 AMD 环境
    • sh -c 'echo 0 > /proc/sys/kernel/numa_balancing'
    • export NCCL_MIN_NCHANNELS=112
  • 根据 GPU 的显存和计算资源,将 --max-num-seqs 增加到 512 及以上。
VLLM_USE_TRITON_FLASH_ATTN=0 vllm serve meta-llama/Llama-3.1-70B-Instruct --host 0.0.0.0 --port 8000 -tp 4 --max-num-seqs 1024 --max-seq-len-to-capture 16384 --served-model-name meta-llama/Llama-3.1-70B-Instruct --enable-chunked-prefill=False --num-scheduler-steps 15 --max-num-seqs 1024

为了快速设置,我们已将 vLLM 0.6.2(commit: cb3b2b9ba4a95c413a879e30e2b8674187519a93)的 Docker 镜像编译到了 Github Container Registry。获取镜像请:

# v0.6.2 post
docker pull ghcr.io/embeddedllm/vllm-rocm:cb3b2b9
# P.S. We also have compiled the image for v0.6.3.post1 at commit 717a5f8
docker pull ghcr.io/embeddedllm/vllm-rocm:v0.6.3.post1-717a5f8

运行镜像启动 Docker 容器:

sudo docker run -it \
   --network=host \
   --group-add=video \
   --ipc=host \
   --cap-add=SYS_PTRACE \
   --security-opt seccomp=unconfined \
   --device /dev/kfd \
   --device /dev/dri \
   -v /path/to/hfmodels:/app/model \ # if you have pre-downloaded the model weight, else ignore
   ghcr.io/embeddedllm/vllm-rocm:cb3b2b9 \
   bash

现在使用我们发现的参数启动 LLM 服务:

VLLM_USE_TRITON_FLASH_ATTN=0 vllm serve meta-llama/Llama-3.1-70B-Instruct --host 0.0.0.0 --port 8000 -tp 4 --max-num-seqs 1024 --max-seq-len-to-capture 16384 --served-model-name meta-llama/Llama-3.1-70B-Instruct --enable-chunked-prefill=False --num-scheduler-steps 15 --max-num-seqs 1024

结论

本指南探讨了 vLLM 在 AMD MI300X GPU 上部署大语言模型的强大能力。通过精细调整分块预填充、多步调度和 CUDA/HIP 图捕获等关键设置,我们展示了如何获得比标准配置和替代服务方案更大幅度的性能提升。vLLM 实现了显著提高的吞吐量和更快的响应时间,使其成为在 AMD 硬件上部署 LLM 的理想选择。

然而,需要承认的是,我们的探索主要集中在短输入和输出的通用聊天机器人用法上。需要进一步研究以针对特定用例(如摘要或长文本生成)优化 vLLM。此外,对 Triton 和 CK 注意力内核之间性能差异的更深入研究可能会产生进一步的见解。

我们还要感谢 Leonard Lin 撰写的 这篇精彩博客文章,关于如何为 MI300X 进一步优化 vLLM,包括 hipBLAS 与 hipBLASLt、CK Flash Attention 与 Triton Flash Attention、张量并行与流水线并行等。

致谢

本博文由 Embedded LLM 团队起草,感谢 Hot Aisle Inc. 为 vLLM 基准测试赞助 MI300X 资源。

附录

服务器规格

以下是出色的 Hot Aisle 服务器的配置:

  • CPU:2 x Intel Xeon Platinum 8470
  • GPU:8 x AMD Instinct MI300X 加速器。我们在基准测试中使用的模型和软件如下:
  • 模型:meta-llama/Llama-3.1-405B-Instruct 和 meta-llama/Llama-3.1-70B-Instruct
  • vLLM (v0.6.2):vllm-project/vllm:一个用于 LLM 的高吞吐量且内存高效的推理和服务引擎 (github.com) commit: cb3b2b9ba4a95c413a879e30e2b8674187519a93
  • 数据集:ShareGPT
  • 基准测试脚本:仓库中的 benchmarks/benchmark_serving.py

我们根据仓库中的 Dockerfile.rocm 构建了兼容 ROCm 的 vLLM Docker(我们已经推送了用于运行基准测试的 vLLM 版本的 Docker 镜像。通过 docker pull ghcr.io/embeddedllm/vllm-rocm:cb3b2b9 获取)。所有基准测试均在 Docker 容器实例中运行,并使用 4 块 MI300X GPU,使用 CK Flash Attention 且 VLLM_USE_TRITON_FLASH_ATTN=0

详细基准测试配置

配置 命令
vLLM 默认配置 VLLM_RPC_TIMEOUT=30000 VLLM_USE_TRITON_FLASH_ATTN=0 vllm serve Llama-3.1-405B-Instruct -tp 8 --max-num-seqs 1024 --max-num-batched-tokens 1024
TGI 默认配置 ROCM_USE_FLASH_ATTN_V2_TRITON=false TRUST_REMOTE_CODE=true text-generation-launcher --num-shard 8 --sharded true --max-concurrent-requests 1024 --model-id Llama-3.1-405B-Instruct
vLLM (本指南建议) VLLM_RPC_TIMEOUT=30000 VLLM_USE_TRITON_FLASH_ATTN=0 vllm serve Llama-3.1-405B-Instruct -tp 8 --max-seq-len-to-capture 16384 --enable-chunked-prefill=False --num-scheduler-steps 15 --max-num-seqs 1024
TGI (本指南建议) ROCM_USE_FLASH_ATTN_V2_TRITON=false TRUST_REMOTE_CODE=true text-generation-launcher --num-shard 8 --sharded true --max-concurrent-requests 1024 --max-total-tokens 131072 --max-input-tokens 131000 --model-id Llama-3.1-405B-Instruct