在 AMD MI300X 上服务 LLM 的最佳实践

15 分钟阅读
由 Embedded LLM 和 Hot Aisle Inc. 联合投稿

总结: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 最近宣布,他们目前 100% 的 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 模型上,无论是在首字延迟(TTFT)还是在不同查询每秒(QPS)场景下的吞吐量方面,vLLM 的表现都明显优于 TGI。就 TTFT 而言,在优化配置下,vLLM 在 16 QPS 时获得的平均响应时间比 TGI 快约 3.8 倍。在吞吐量方面,vLLM 持续优于 TGI,在优化后的设置下,使用 ShareGPT 数据集在 1000 QPS 时达到了每秒 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)大小次优,反而可能导致性能下降。
  • 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:分块预填充

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

基于我们的实验,我们发现调整分块预填充值比禁用该功能有轻微的提升。但是,如果您不确定是否要启用分块预填充,只需从禁用它开始,通常就能获得比使用默认设置更好的性能。这对于 MI300X GPU 而言是特定的。




案例 2:调度步数

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

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




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

分块预填充和前缀缓存是 vLLM 中的优化技术,它们分别通过将大型预填充拆分为更小的块以进行高效批量处理,以及复用跨查询共享前缀的 KV(键值)计算结果,来提高性能。

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

在深入图表分析之前,我们先解释实验中使用的术语。新鲜运行 (Fresh Run) 指的是前缀缓存内存未被填充的情况。第二次运行 (2nd Run) 指的是在新鲜运行之后再次运行基准测试脚本。通常情况下,在第二次运行中重跑 ShareGPT 基准数据集时,我们可以获得约 50% 的前缀缓存命中率。

观察下图,我们可以得出关于此实验的三个结论:

  1. 基于 Bar 2(红色)与基准线(蓝色)的对比,性能有巨大的提升。
  2. 基于 Bar 3(黄色)、Bar 5(橙色)和 Bar 6(青色)与基准线的对比,分块预填充的性能取决于用户请求输入提示的长度分布。
  3. 在我们的实验中,我们发现 Bar 3(黄色)和 Bar 4(绿色)的前缀缓存命中率分别约为 0.9%50%。根据 Bar 3 和 Bar 4 与基准线和 Bar 2 的对比,我们可以得出结论:如果用户请求没有很高的前缀缓存命中率,将分块预填充和前缀缓存同时禁用被认为是一个好的经验法则。



案例 4:要捕获的最大序列长度

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

我们的基准测试揭示了一个有趣的趋势:增加 --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 Collective Communications Library (NCCL) 用于 GPU 间通信。对于 MI300X,AMD vLLM 分支性能文档建议将 NCCL_MIN_NCHANNELS 环境变量设置为 112,以潜在地增强性能。

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

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




案例 6:KVCache 类型 Auto/FP8

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

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




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

张量并行是一种分发大模型计算负载的技术。它通过将单个张量拆分到多个设备上,从而允许并行处理特定的操作或层。这种方法减少了模型的内存占用,并实现了跨多个 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
  • --max-num-seqs 增加到 512 及以上,具体取决于 GPU 的内存和计算资源。
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 的 Docker 镜像(提交:cb3b2b9ba4a95c413a879e30e2b8674187519a93)编译至 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

总结

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

然而,必须承认我们的探索主要集中在具有短输入和输出的通用聊天机器人使用场景。需要进一步调查以优化 vLLM 在摘要或长文本生成等特定用例中的表现。此外,深入研究 Triton 和 CK 注意力核之间的性能差异可能会产生更多见解。

我们还要感谢 Leonard Lin 的精彩博客文章,文中介绍了如何进一步优化 vLLM 以适配 MI300X,包括 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) 提交: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,通过设置 VLLM_USE_TRITON_FLASH_ATTN=0 使用 CK Flash Attention。

详细基准测试配置

配置命令
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