教程在 Amazon EKS 上将一个大模型部署到两个 GPU 节点,分别承担预填充和解码,并统一提供兼容 OpenAI 的接口。预构建的 Ray LLM 容器包含完整推理栈,减少手动组装工作。
本文介绍如何使用支持多节点部署的 Ray LLM Deep Learning Containers,在 Amazon Elastic Kubernetes Service(Amazon EKS)的两个 GPU 节点上部署一个大语言模型(LLM)。该方案将 prefill 和 decode 分别放在独立节点上,并通过同一个 OpenAI 兼容端点提供服务。这类多节点配置常用于张量并行、流水线并行、数据并行,以及 prefill/decode 分离的推理工作负载。在之前的一篇文章中,我们介绍了 Ray Serve DLC,并用它在单个 GPU 节点上部署了一个通过 HTTP 端点提供服务的小模型。你可以直接获得可用的推理端点,而容器会保证 GPU 软件栈中各组件的兼容性,包括 CUDA 库,因此不需要编写样板代码。
Ray 是一个分布式计算框架,因此 Ray Serve 部署可以跨越多个节点。客户使用多节点 Ray Serve 的原因有很多:有些希望运行单个 GPU 容纳不下的基础模型或开放权重模型;有些需要超过单个副本所能提供的吞吐量;还有一些希望通过分离推理的两个阶段来提升性能。但无论是哪种情况,客户都必须自行搭建依赖链:为 GPU 软件栈选择兼容的库,并在某个组件与其他组件不再兼容时排查问题。这些缺乏差异化价值的工作会消耗时间和精力,让团队无法专注于模型交付。
AWS Deep Learning Containers(DLC)长期以来一直在为训练和推理工作负载解决这类问题。DLC 是预先构建、经过性能优化的 Docker 镜像,将框架、依赖项和 GPU 软件栈打包成经过测试并修补的组件组合,拉取后即可使用。随着支持多节点部署的 Ray LLM DLC 推出,这种方式也扩展到了多节点推理。你可以获得一个专为通过 HTTP 端点提供模型服务而构建的容器,由 AWS 维护和测试,并且已经组装好完整的推理软件栈。
本文将逐步介绍如何使用多节点 Ray LLM DLC 实现多节点分离式推理:在 Amazon EKS 上,将一个大语言模型部署到两个配备 GPU 的节点,让它们通过同一个端点协同提供服务。其中一个 GPU 节点处理 prefill 阶段,另一个 GPU 节点处理 decode 阶段。将这两个阶段分开,可以避免计算密集的长 prompt 阻塞正在处理的请求的 token 生成。这种方式非常适合 Retrieval Augmented Generation(RAG)、文档摘要和 Agent 应用等工作负载。我们使用的多节点 Ray LLM DLC 内置了 Ray Serve LLM、NVIDIA Inference Transfer Library(NIXL)和 Elastic Fabric Adapter(EFA)软件栈,无需编写应用代码即可提供 OpenAI 兼容 API。完整代码可在配套仓库中获取。
LLM 推理包含两个不同的阶段。Prefill 并行处理整个输入 prompt,并构建键值(KV)缓存。随后,decode 每次生成一个 token,同时读取并扩展这个缓存。Prefill 受计算能力限制,而 decode 受内存带宽限制。当两个阶段共用一个 GPU 时,它们会争抢同一组资源,突发且计算密集的 prefill 工作会干扰持续进行、对延迟敏感的 decode 工作。将 prefill 与 decode 分开后,你可以为每个阶段分别选择硬件和服务配置,从而提升大规模部署时的整体性能。
PD 分离将两个阶段放到不同节点上,使它们互不干扰,并且可以独立扩缩容。代价是 prefill 阶段构建的 KV 缓存必须交给 decode 节点,而且传输速度必须足够快,否则就会抵消分离带来的收益。这也正是节点间互连如此重要的原因。
AWS DLC 是预先构建、经过性能优化的 Docker 镜像,将框架、依赖项和 GPU 软件栈打包成经过测试并修补的组件组合,拉取后即可使用。每次发布前,所有组件都会一起接受验证,因此 CUDA runtime、框架和服务层之间不会出现版本不匹配的问题。安全补丁则在构建时应用。
Ray LLM DLC 将这一方式扩展到了分布式 LLM 服务。它内置 vLLM 作为推理引擎、Ray Serve 的 HTTP 服务层、用于节点间 KV 缓存传输的 NIXL,以及用于节点间高带宽通信的 EFA 软件栈(libfabric 和 aws-ofi-nccl)。由于 Ray Serve LLM 库内置了应用构建器,整个服务应用只需要一段 YAML 配置。不需要 handler 类,不需要模型打包工具,也不需要构建自定义镜像。只需一个辅助函数 build_pd_openai_app,接收 prefill 配置和 decode 配置,就能连接两者之间的 KV 缓存传输,并通过同一个 OpenAI 兼容端点提供服务。
下图展示了该部署在 Amazon EKS 上的布局。用户请求首先到达 CPU 系统节点上的 Ray head,两个 GPU worker 节点则分别运行模型的 prefill 和 decode 阶段。
图 1:Amazon EKS 上的多节点推理架构,Ray head 位于 CPU 系统节点,prefill 和 decode 阶段分别运行在两个 GPU worker 节点上
该部署是一个位于虚拟私有云(VPC)中的 EKS 集群。节点运行在私有子网中,通过 NAT Gateway 访问外部网络。一个 CPU 系统节点(m7i.xlarge)承载系统工作负载、KubeRay operator,以及仅使用 CPU 的 Ray head pod。两个 GPU worker 节点(g6.8xlarge)运行 Ray worker:一个负责 prefill 阶段,另一个负责 decode 阶段。两个节点都从 Amazon Elastic Container Registry(Amazon ECR)拉取 Ray LLM DLC。
KubeRay 监听你应用的 RayService,并自动完成 worker 加入 head 的连接配置。Ray Serve 将 prefill 和 decode 部署调度到两个 GPU worker 上。KV 缓存通过 NIXL 从 prefill worker 传输到 decode worker,并通过 NIXL 的 libfabric backend 使用 EFA。由于 EFA 无法跨可用区(AZ)通信,GPU 节点组被限定在同一个 AZ 内。
要跟着本文操作,你需要:
使用以下命令确认 AWS 凭证有效:
aws sts get-caller-identity
应用定义在 manifest/rayservice.yaml 的 serveConfigV2 配置块中。它使用 build_pd_openai_app,该函数接收 prefill_config 和 decode_config,并连接两者之间的 KV 缓存传输:
applications:
- name: qwen
import_path: ray.serve.llm:build_pd_openai_app # OpenAI-compatible PD app, built in
route_prefix: /
args:
prefill_config:
model_loading_config: { model_id: qwen3.5-9b, model_source: Qwen/Qwen3.5-9B }
engine_kwargs:
max_model_len: 4096
dtype: bfloat16
gpu_memory_utilization: 0.90
enforce_eager: true # Skip CUDA graph capture to reduce peak GPU memory
kv_transfer_config:
kv_connector: NixlConnector
kv_role: kv_both
engine_id: prefill
kv_connector_extra_config: { backends: ["LIBFABRIC"] }
deployment_config: { num_replicas: 1 }
decode_config:
model_loading_config: { model_id: qwen3.5-9b, model_source: Qwen/Qwen3.5-9B }
engine_kwargs:
max_model_len: 4096
dtype: bfloat16
gpu_memory_utilization: 0.90
enforce_eager: true
kv_transfer_config:
kv_connector: NixlConnector
kv_role: kv_both
engine_id: decode
kv_connector_extra_config: { backends: ["LIBFABRIC"] }
deployment_config: { num_replicas: 1 }
其中几个设置起着关键作用。max_model_len: 4096 根据 GPU 上实测的 KV 缓存容量设定上下文长度。dtype: bfloat16 和 gpu_memory_utilization: 0.90 分别设置计算精度,以及 vLLM 可以使用的显存比例。enforce_eager: true 跳过 CUDA graph capture,以降低峰值 GPU 显存占用,具体效果取决于模型和输入。kv_transfer_config 是实现分离的关键:两个阶段都使用 NixlConnector,设置 kv_role: kv_both,并使用不同的 engine_id,一个为 prefill,另一个为 decode。最关键的设置是 kv_connector_extra_config: { backends: ["LIBFABRIC"] }。如果没有这一行,NIXL 默认使用 UCX,即使有 EFA,也只能达到 TCP 级别的传输速度。
manifest 的其余部分位于 rayClusterConfig 中,用于描述 pod。Head pod 仅使用 CPU(num-gpus: "0"),并被限定在系统节点组上。每个 worker pod 请求一个 GPU 和一个 EFA 接口(nvidia.com/gpu: "1"、vpc.amazonaws.com/efa: "1"),并通过 nodeSelector: { role: gpu-worker } 调度到 GPU 节点。Worker 以 privileged: true 运行,因为 NIXL 需要向 EFA NIC 注册 CUDA 显存,仅有 IPC_LOCK capability 并不足够。Worker 设置 FI_PROVIDER=efa,从 pod IP 获取 VLLM_HOST_IP,并挂载共享内存卷。内存限制设得远高于约 18 GiB 的 checkpoint 大小,使操作系统的页缓存能够容纳它。对于 g6.8xlarge 上的 Qwen3.5-9B,在使用上述缓存配置时,重启后的模型权重加载时间会从约 145 秒降到约 2 秒。
所有脚本共用一个配置文件 scripts/env.sh。该脚本包含 CLUSTER_NAME、REGION、K8S_VERSION 和 NAMESPACE 的默认值。其他默认配置涉及系统节点组、GPU 节点组、KubeRay 版本和模型。在运行脚本之前,通过 export 导出变量即可覆盖任意默认值。
从项目根目录开始,先设置基准目录,再加载变量:
CURRENT_DIR=$(pwd)
cd $CURRENT_DIR/ray-serve-multi-node/scripts
source ./env.sh
./deploy_cluster.sh
这一步会创建 EKS 集群,包括 VPC、OIDC provider 和核心 add-on,以及一个 CPU 系统节点组,用于运行系统工作负载、KubeRay operator 和 Ray head。节点运行在私有子网中,通过 NAT Gateway 访问外部网络,集群子网则位于提供该 GPU 实例类型的 AZ 中。该脚本具有幂等性,首次运行需要 15–20 分钟。
./deploy_node_group.sh
这一步会创建 GPU 节点组:两台启用 EFA 的 g6.8xlarge 实例,限定在同一个 AZ 内,并打上 role=gpu-worker 标签,让 Ray worker 调度到这些节点上。启用 EFA 时还会安装 EFA device plugin,它会在每个节点上声明 vpc.amazonaws.com/efa 资源。这一步大约需要 5–20 分钟,首次运行通常更久。
./install_kuberay.sh
这一步通过 Helm 安装 KubeRay operator,并将其限定在系统节点组上,让 GPU 节点专门用于推理。KubeRay 会监听你在下一步应用的 RayService,并自动完成 worker 加入 head 的连接配置。这一步大约需要 1–2 分钟。
./deploy_ray_service.sh
这一步使用 envsubst,根据你的镜像、模型和命名变量渲染 manifest/rayservice.yaml,然后应用该配置。KubeRay 会创建一个 head pod 和两个 worker pod,Ray Serve LLM 则启动 prefill 和 decode 部署。脚本会等待 head 进入 Ready 状态、两个 worker 进入 Running 状态,最后等待 Serve 应用报告 RUNNING。由于每个节点都需要加载模型,这一步预计需要几分钟。
你可以随时查看状态,包括每个 pod 被调度到了哪个节点,以及可用的 GPU 容量:
./deploy_ray_service.sh status
Serve 应用健康后,KubeRay 会创建一个稳定的 <rayservice-name>-serve-svc Service。对它进行端口转发:
kubectl port-forward -n inference svc/ray-llm-serve-svc 8000:8000
由于 DLC 提供 OpenAI 兼容 API,你可以列出模型,并发送标准的 chat-completion 请求,无需自定义 schema:
curl --silent http://127.0.0.1:8000/v1/models
curl --fail --silent --show-error \
--request POST "http://127.0.0.1:8000/v1/chat/completions" \
--header "Content-Type: application/json" \
--data '{
"model": "qwen3.5-9b",
"messages": [
{"role": "user", "content": "Explain the benefits of prefill/decode disaggregation."}
],
"max_tokens": 200,
"temperature": 0.7
}'
如果 NIXL 无法使用 libfabric,它会回退到 TCP 级别速度的传输方式,而且不会报错,因此有必要确认它实际选择了哪个 backend。在 worker 日志中执行 grep:
for POD in $(kubectl get pods -n inference -l ray.io/node-type=worker \
-o jsonpath='{.items[*].metadata.name}'); do
kubectl exec -n inference "$POD" -c ray-worker -- \
grep -rhoE "Backend (LIBFABRIC|UCX) was instantiated" /tmp/ray/session_latest/logs/ 2>/dev/null
done | sort | uniq -c
deploy_node_group.sh 在创建节点组时,已经会输出每个节点的 vpc.amazonaws.com/efa 数量。如果需要测量跨节点带宽,DLC 内置了一个预编译的 NVIDIA Collective Communications Library(NCCL)基准测试程序,路径为 /usr/local/bin/all_reduce_perf。
为避免持续产生费用,请按相反顺序删除资源:
./delete_ray_service.sh # Remove the RayService
./install_kuberay.sh cleanup # Uninstall the KubeRay operator
./delete_node_group.sh # Remove the GPU node group
./delete_cluster.sh # Delete the EKS cluster
你已经通过 prefill/decode 分离,在两个 GPU 节点上运行了同一个模型,而整个应用只是一段 YAML 配置,无需编写 handler 代码、使用模型打包工具,也无需手动组装 GPU 和 EFA 软件栈。多节点 Ray LLM DLC 提供经过测试的推理软件栈,KubeRay 编排 Ray 集群,NIXL 则通过 EFA 在节点间传输 KV 缓存。你的工作是配置这些组件,无需从头构建它们。
运行多节点推理的常见模式包括张量并行和流水线并行,将单个实例容纳不下的模型拆分到多个实例;数据并行,通过复制模型提高吞吐量;以及 prefill/decode 分离,将推理的两个阶段放到不同节点上。
接下来,你可以增加 decode 副本来提高吞吐量,在更大的 GPU 实例上使用张量并行或流水线并行来运行更大的模型,或者在同一个经过测试的基础镜像上添加模型所需的额外库。完整代码、当前镜像 tag 列表和底层机制,可参阅配套仓库、Ray LLM DLC 可用镜像页面、Ray Serve LLM prefill/decode 文档,以及 EFA 文档。