AWS博客详解在HyperPod上通过Curgine实现分层KV缓存,将GPU显存扩展至分布式NVMe池,降低推理成本。
在大规模运行大语言模型(LLM)推理时,通常需要在 KV 缓存上做出权衡:要么为能够容纳不断增长的 KV 缓存的过大 GPU 实例付费,要么接受首 token 时间(TTFT)变慢——因为相同的提示词在每次请求时都会被重新计算。对于跨业务线端点、检索增强生成(RAG)管道或多轮对话应用部署广泛的开源基础模型(如 Qwen、Llama、DeepSeek 等)的团队而言,这种权衡直接转化为更高的基础设施成本和更差用户体验。
根本原因很简单。在生成过程中,vLLM 将已处理的每个 token 的注意力键值(keys and values)存储在 KV 缓存中,这样就不必在每个步骤重新计算它们。前缀缓存通过复用包含相同前导 token(如通用系统提示词)的跨请求缓存来扩展这一机制。在像 ml.g6e.4xlarge(每个 GPU 48 GB)这样性价比高的实例上,一旦模型权重和运行时分配被计入,为前缀缓存留下的内存就很有限——随着模型变大或并发量提高,情况会进一步收紧。长提示词的缓存命中率下降,相同的系统提示词在每次请求时都会被重新预填充,水平扩展的 vLLM 副本各自维护着相互独立的缓存。路由到不同的副本在功能上相当于冷启动。
本文中,我们在 Amazon SageMaker HyperPod 上构建了一种分层 KV 缓存架构,将缓存层次从 GPU 和 CPU 内存扩展到共享的、分布式的 NVMe 池。它基于 HyperPod 的两个能力——托管分层 KV 缓存(Managed Tiered KV Cache)和智能路由(Intelligent Routing),并添加了 Curvine 作为共享的 L2 层(GPU → CPU → 共享 NVMe)。通过此设置,你可以在接近本地磁盘的速度下跨副本复用 KV 缓存。
我们完整走一遍从启用 HyperPod 分层存储,到在节点本地 NVMe 上部署 Curvine worker,再到为文件系统支持的 L2 打补丁 Inference Operator 的端到端实现流程。在一次测试部署中,这实现了最高 100% 的跨 Pod 缓存命中率、最高 2.7 倍的 TTFT 提升,以及约 1,900 token 提示词约 56 ms 的跨节点 L2 读取延迟。完整的基准测试方法和结果见基准测试(Benchmarking)部分。通过这种架构,以前需要 P5 实例的工作负载可以在成本更低的 G6e 实例上运行,从而降低每个端点的成本。实际节省取决于模型大小和流量特征。
核心思想是将 KV 缓存扩展到单个 Pod 可容纳的范围之外。不再接受每个 vLLM 副本孤立存在——各自独占 GPU 块、各自的 CPU 溢出区域、不共享——我们构建了一个三层体系结构:L0(GPU HBM)、L1(本地 CPU/主机内存)和 L2(Curvine,跨节点共享缓存),并在其上叠加缓存感知的请求路由。
L0 – GPU 前缀缓存。 这是 vLLM 原生的分页注意力层,以最低的访问延迟保存最热的 KV 块,但其容量仅为模型权重占用后剩余的 GPU 内存。在 48 GB GPU 上,一个 7B 模型以 bf16 格式运行时权重约需 14 GB,剩余超过 30 GB 用于 KV 块,绰绰有余,所以 L0 压力很小。一个 32B 模型的权重占用约 64 GB,甚至无法容纳在一块 48 GB GPU 上。即使分片后,用于 KV 的内存也所剩无几,缓存在并发下迅速填满并发生驱逐。随着模型规模和流量增长,这个不断缩小的余量空间正是将缓存扩展到 GPU 之外变得必要的根本原因。
L1 – CPU 内存卸载。 当 GPU 块被驱逐时,LMCache 在主机 DRAM 中捕获它们,防止数据丢失。这运行在每个推理 Pod 内部,并在 InferenceEndpointConfig CRD 中设置 enableL1Cache: true 时由 SageMaker HyperPod Inference Operator 自动管理。可以将其视为安全网。它很快,是 Pod 本地的,并通过 InstanceMemoryAllocationPercentage 设定大小(建议从 20% 开始)。
L2 – 共享分布式 NVMe 池。 这是跨副本复用发生的地方。Curvine 是一个轻量级分布式缓存文件系统,将 G6e/P5 实例自带的本地 NVMe 驱动器聚合成一个单一命名空间,FUSE 客户端(一种用户态驱动程序,将存储池呈现为普通挂载目录)将其作为 ReadWriteMany PVC(PersistentVolumeClaim)挂载到每个推理 Pod 中。LMCache 通过其 fs:// 连接器进行读写,因此这个分布式池看起来像一个本地目录。由于每个 Pod 都挂载了相同的命名空间,一个副本写入的 KV 块可以立即被其他副本读取。
Curvine 本身运维简单:主节点(Primary Node,在 Curvine 文档中称为 "Master")处理元数据和日志持久化,元数据保存在 Amazon Elastic Block Store(Amazon EBS)上以保证 durability,而 Worker 组件运行在每个 GPU 节点上并将数据存储在节点的 NVMe 上(通常挂载在 /opt/dlami/nvme/curvine-data)。如果某个 Worker 宕机,它所持有的缓存会被重新计算,不存在数据丢失风险,因为这些是可重现的 KV 块。
智能路由 – 将请求送到正确的副本。 三层缓存只有在请求落到已经持有相关 KV 块的副本上时才能发挥全部优势。HyperPod Inference Operator 内置了一个路由器,支持三种策略:
路由器维护一个前缀树(前缀感知,prefix-aware)或查询每个 worker 的缓存状态(kv-aware),以选择最有可能产生缓存命中的副本。这个过程是透明的,无需客户端修改。
各组件如何协同工作。Inference Operator 作为 Amazon Elastic Kubernetes Service(Amazon EKS) addon 安装,管理完整生命周期。它启动带有 LMCache sidecar 的 vLLM Pod,配置 L1 和 L2 后端,部署路由器,并暴露一个负载均衡的端点。你在 InferenceEndpointConfig CRD 中声明所需的缓存拓扑结构(enableL1Cache、enableL2Cache、l2CacheBackend、routingStrategy),Operator 会渲染出正确的环境变量、卷挂载和路由规则。目前需要注意的一点:CRD 的 l2CacheBackend 字段原生只接受 redis 或 tieredstorage。要将 L2 指向 Curvine FUSE 挂载点,我们需要在 vLLM 容器规格中修补 LMCACHE_REMOTE_URL 环境变量,设置为 fs://localhost:0/mnt/curvine/l2cache/。我们会在实现第四阶段(Stage 4)中详细讲解这个补丁。
最终效果是:请求到达路由器,被分发到前缀匹配最好的副本,该副本依次检查 GPU 块(L0)、CPU(L1),然后是共享 NVMe 池(L2)。只有在完全未命中时才会从头重新预填充。对于具有中高度提示词重叠的工作负载(例如约 40% 以上的前导 token 相同,如通用系统提示词或共享 RAG 上下文),跳过该预填充步骤会显著降低 TTFT。
图 1 展示了完整的数据路径。每个 vLLM Pod 堆叠了一个 L0 GPU 前缀缓存和一个 L1 CPU 卸载层。在它们下方,所有 Pod 通过 Curvine 分布式文件系统共享 L2 层——该文件系统由节点本地 NVMe 聚合而成,通过 FUSE 以 ReadWriteMany 方式挂载,而 Curvine 元数据节点将数据持久化到 Amazon EBS。HyperPod 智能路由器位于前端,将每个请求引导到最可能已经持有相关缓存的副本。
图 1:分层 KV 缓存架构
Curvine 是一个高性能分布式缓存文件系统,位于应用程序和 Amazon Simple Storage Service(Amazon S3)、HDFS 或 NAS 等底层存储之间。客户端通过 CLI、SDK、FUSE 或 CSI 访问它。主节点处理元数据,Worker 通过本地磁盘缓存提供数据以实现低延迟 I/O。图 2 展示了 Curvine 架构及其关键组件。
图 2:Curvine 架构
Curvine 工作原理(集群视图):
客户端向 Master 发送元数据 RPC,向 Worker 发送数据 I/O。
Master 通过心跳协调 Worker 并为负载均衡和高可用(HA)放置数据块。
Worker 读写本地层并根据热度提升/降级数据。
在未命中或策略驱动的持久化时,Curvine 从底层文件系统(UFS)加载/转储数据,因此 durability 留在底层存储,而 Curvine 加速访问。
Amazon SageMaker HyperPod 分层存储(SageMaker HyperPod Tiered Storage)是一项集群级能力,为推理工作负载提供节点本地缓存层。分层存储激活后,SageMaker HyperPod 在每个 GPU 节点上部署 ai-toolkit DaemonSet,为 L1 CPU 卸载保留可配置的主机内存份额(InstanceMemoryAllocationPercentage),并将本地 NVMe 实例存储暴露在 /opt/dlami/nvme 下,以便 Curvine Worker 可以将其聚合到共享的 L2 命名空间。当 InferenceEndpointConfig CRD 上设置 enableL1Cache 和 enableL2Cache 时,Inference Operator 自动消费这些层级。
本演练假设 SageMaker HyperPod 集群由 Amazon EKS 编排。如需创建,请按照 SageMaker 文档中的使用 Amazon EKS 编排 SageMaker HyperPod 集群,或使用 AWSome Distributed AI 代码库中的参考模板通过 AWS CloudFormation 创建。至少配置两个 GPU 节点。单节点无法演示跨节点复用。本文全程以集群名 hyperpod-cluster-eks 和 US West(俄勒冈)AWS 区域(us-west-2)为例,请替换为您自己的集群名和区域以在您的账户中复现此方案。
验证以下前提条件已就绪:
具备本地 NVMe 的 GPU 容量:一个 SageMaker HyperPod EKS 集群,至少包含一个 GPU 实例组。建议使用 G6e 或 P5,因其具备本地 NVMe,Curvine 将其聚合成 L2。
CLI 工具:在您的工作站上:AWS Command Line Interface(AWS CLI)v2(具备 sagemaker:UpdateCluster 和 eks:CreateAddon 权限),kubectl 已通过 aws eks update-kubeconfig 配置为指向该集群,以及 Helm v3。
用于 EBS 挂载的 IAM:授予 EBS CSI 驱动角色 sagemaker:AttachClusterNodeVolume、sagemaker:DetachClusterNodeVolume 和 eks:Describe*,以便 Curvine 元数据节点可以挂载其 EBS 卷。保持 Amazon Virtual Private Cloud(Amazon VPC)CNI 和 EBS CSI 插件为最新版本。
模型权重:本文从 HuggingFace 拉取 Qwen2-7B,因此无需 S3 桶。如需自行准备权重,请使用具备 SageMaker HyperPod 执行角色读取权限的 Amazon S3 桶。TLS 证书自动生成。
分层存储已在第 1 阶段启用。推理 Operator、Amazon S3 和 Amazon FSx CSI 驱动、Metrics Server 和 Cert Manager 在第 2 阶段安装(或通过控制台快速安装),EBS CSI 驱动和 Curvine 在第 3 阶段安装。
以下过程以此实现为示例,划分为五个阶段。显示的集群名和区域为占位符,请替换为您自己的。
分层存储是集群级开关。一旦分层存储激活,HyperPod 会自动将 ai-toolkit DaemonSet 部署到每个节点。
# 通过 update-cluster 在现有集群上启用(推荐)
aws sagemaker update-cluster \
--cluster-name hyperpod-cluster-eks \
--tiered-storage-config Mode=Enable,InstanceMemoryAllocationPercentage=20 \
--node-recovery Automatic
API 注意事项。单独使用 --tiered-storage-config 调用 update-cluster 会返回 ValidationException。必须同时提供 --node-recovery 或 --instance-groups 中的至少一个。方法是先运行 describe-cluster 读取当前的 NodeRecovery 值,然后原样传回。这对集群配置没有副作用。
InstanceMemoryAllocationPercentage 接受 20–100 的值。从 20 开始,根据观察到的吞吐量和命中率按需增加。用以下命令验证:
aws sagemaker describe-cluster --cluster-name hyperpod-cluster-eks \
--query 'TieredStorageConfig'
# 预期输出:{"Mode": "Enable", "InstanceMemoryAllocationPercentage": 20}
kubectl get ds -n aws-hyperpod ai-toolkit
# 预期输出:
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
# ai-toolkit 2 2 2 2 2 <none> 45s
最便捷的方式是 SageMaker 控制台中的快速安装,它会一并配置 IAM 角色并安装 S3 CSI、FSx CSI、Metrics Server、Cert Manager 和推理 Operator。CLI 方式如下:
EKS_CLUSTER_NAME=$(aws sagemaker describe-cluster --cluster-name hyperpod-cluster-eks \
--query 'Orchestrator.Eks.ClusterArn' --output text | cut -d'/' -f2)
for addon in aws-mountpoint-s3-csi-driver aws-fsx-csi-driver metrics-server cert-manager; do
aws eks create-addon --cluster-name $EKS_CLUSTER_NAME --addon-name $addon --region us-west-2
done
aws eks create-addon \
--cluster-name $EKS_CLUSTER_NAME \
--addon-name amazon-sagemaker-hyperpod-inference \
--configuration-values file://addon-config.json \
--region us-west-2
部署 Curvine 之前必须具备若干先决条件。将 VPC CNI 插件和 EBS CSI 驱动升级到当前版本,优先使用 IRSA 而非 Pod Identity,以避免节点上额外消耗 IP。授予 aws-ebs-csi-dri-role 前提条件中列出的 EBS 挂载权限(没有这些权限,在 SageMaker HyperPod 节点上挂载 EBS 会返回 ValidationException)。最后,验证集群中存在一个 EBS StorageClass(例如 ebs-sc)。用 kubectl get sc 检查实际名称。在 SageMaker HyperPod EKS 集群上,默认的 EBS StorageClass 通常是 gp3。将该名称用于下面 Helm 安装中的 master.storage.meta.storageClass 和 master.storage.journal.storageClass,或者先创建一个 ebs-sc StorageClass:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
helm repo add curvine https://curvineio.github.io/helm-charts
helm repo update
helm install curvine-csi curvine/curvine-csi \
-n curvine --create-namespace \
--version 0.3.2-alpha \
--set controller.sidecars.provisioner.image=registry.k8s.io/sig-storage/csi-provisioner:v3.6.0 \
--set node.sidecars.nodeDriverRegistrar.image=registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.10.0 \
--set controller.container.securityContext.privileged=true \
--set node.container.securityContext.privileged=true
kubectl get csidrivers | grep curvine # 确认驱动已注册
CSI 安装不会自动创建 StorageClass。必须手动创建:
kubectl apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: curvine-sc
provisioner: curvine
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
parameters:
master-addrs: "curvine-master-0.curvine-master.curvine.svc.cluster.local:8995"
fs-path: "/l2cache"
path-type: "DirectoryOrCreate"
EOF
kubectl get sc curvine-sc # 确认 curvine-sc 已创建
Curvine CSI 0.3.x 及更高版本需要三个 StorageClass 参数:master-addrs(Curvine 主节点 RPC 端点,必须与 Helm 安装创建的服务 DNS 匹配,如果主组件有副本则用逗号分隔多个地址)、fs-path(Curvine 文件系统内的挂载路径前缀)和 path-type(DirectoryOrCreate 让 CSI 自动创建目录)。缺少这些参数会导致 PVC 制备失败并返回 Parameter 'master-addrs' is required。
对于 KV 缓存工作负载,节点本地 NVMe 配合 hostPath 是推荐的 Worker 数据后端:G6e/P5 实例搭载 NVMe(约 3 GB/s),性能远高于 EBS gp3,不产生额外成本,且对于缓存数据可恢复的场景是可接受的。
# 步骤 1:首次安装(引导)。格式化标志初始化元数据/日志/数据目录。在 0.3.x 上必需,
# 否则主 Pod 会因"RocksDB directories not found"而失败。
helm install curvine curvine/curvine -n curvine --create-namespace \
--version 0.3.2-alpha \
--set image.pullPolicy=Always \
--set cluster.formatMaster=true \
--set cluster.formatWorker=true \
--set cluster.formatJournal=true \
--set master.replicas=1 \
--set worker.replicas=2 \
--set "master.nodeSelector.sagemaker\.amazonaws\.com/compute-type=hyperpod" \
--set "worker.nodeSelector.sagemaker\.amazonaws\.com/compute-type=hyperpod" \
--set master.storage.meta.storageClass=ebs-sc \
--set master.storage.journal.storageClass=ebs-sc \
--set "worker.storage.dataDirs[0].name=data1" \
--set "worker.storage.dataDirs[0].type=SSD" \
--set "worker.storage.dataDirs[0].enabled=true" \
--set "worker.storage.dataDirs[0].size=100Gi" \
--set "worker.storage.dataDirs[0].storageClass=" \
--set "worker.storage.dataDirs[0].hostPath=/opt/dlami/nvme/curvine-data" \
--set "worker.storage.dataDirs[0].mountPath=/data/data1"
# 步骤 2:所有 Pod 进入 Running 后,立即禁用格式化标志,
# 以避免未来 Pod 重启时重新格式化并清除现有缓存/元数据:
helm upgrade curvine curvine/curvine -n curvine \
--version 0.3.2-alpha --reuse-values \
--set cluster.formatMaster=false \
--set cluster.formatWorker=false \
--set cluster.formatJournal=false
kubectl get pods -n curvine # 确认 Pod 保持 Running / 正常重启
约束条件。主节点元数据和日志必须使用持久性 EBS 存储。节点重建时元数据或 WAL 丢失是不可接受的。当 Worker.dataDirs[0].storageClass 为空且设置了 hostPath 时,Helm chart 会启用 hostPath 模式。两者互斥,必须且仅选择其中一种。
启用分层存储后,安装了操作符,Curvine 也已运行,最后一步是部署推理端点并将其 L2 缓存指向 Curvine。这涉及三个步骤:使用 InferenceEndpointConfig CRD 声明端点,打补丁将渲染后的 Deployment 挂载 Curvine PVC,以及覆盖操作符注入的缓存 URL,使 L2 的读写操作指向 Curvine。
应用下面的 InferenceEndpointConfig CRD(deploy-qwen-kvcache.yaml)。SageMaker HyperPod Inference Operator 会将其调和为一个 Deployment、vLLM Pod(每个 Pod 带有一个 LMCache sidecar)、智能路由器和一个负载均衡端点。与标准部署有三个不同之处:
模型来源是 huggingface:操作符内置的 init 容器在 Pod 启动时将权重下载到 /opt/ml/model,因此不需要 S3 暂存。(如果偏好自行暂存权重,可设置 modelSourceType: s3 并配合 s3Storage 块。)
LMCACHE_REMOTE_URL 故意未在 CRD 中列出。当 enableL2Cache: true 时,操作符会注入自己的值。在此也声明它会产生重复的 env 条目,然后还需要通过索引找到并移除。保留不写,然后在下一节的补丁中覆盖注入的值。
tlsConfig 被省略了:它不是必填字段,操作符会自动将端点证书生成到其默认输出桶中。
# deploy-qwen-kvcache.yaml
apiVersion: inference.sagemaker.aws.amazon.com/v1
kind: InferenceEndpointConfig
metadata:
name: qwen2-7b-instruct-kvcache
namespace: default
spec:
modelName: qwen2-7b-instruct
instanceType: ml.g6e.4xlarge
invocationEndpoint: v1/chat/completions
replicas: 2
modelSourceConfig:
modelSourceType: huggingface # Operator downloads weights to /opt/ml/model
prefetchEnabled: true
huggingFaceModel:
modelId: Qwen/Qwen2-7B-Instruct # public model; add tokenSecretRef for gated models
kvCacheSpec:
enableL1Cache: true
enableL2Cache: true
l2CacheSpec:
l2CacheBackend: "tieredstorage" # placeholder to pass CRD validation; overridden by the Stage 4 patch
intelligentRoutingSpec:
enabled: true
routingStrategy: prefixaware
# tlsConfig is intentionally omitted: the Operator auto-generates the
# endpoint certificate into its default output bucket.
metrics:
enabled: true
modelMetrics:
port: 8000
loadBalancer:
healthCheckPath: /health
worker:
image: public.ecr.aws/deep-learning-containers/vllm:0.11.1-gpu-py312-cu129-ubuntu22.04-ec2-v1.0
args:
- "--model"
- "/opt/ml/model"
- "--max-model-len"
- "16384"
- "--tensor-parallel-size"
- "1"
resources:
limits:
nvidia.com/gpu: "1"
requests:
cpu: "8"
memory: 32Gi
nvidia.com/gpu: "1"
modelInvocationPort:
containerPort: 8000
name: http
modelVolumeMount:
name: model-weights
mountPath: /opt/ml/model
environmentVariables:
# Do NOT add LMCACHE_REMOTE_URL here: when enableL2Cache is true the
# Operator injects its own value; declaring it here creates a duplicate
# env entry. It is replaced with the Curvine fs:// URL by the Stage 4
# patch after the Deployment is rendered.
- name: LMCACHE_REMOTE_SERDE
value: "naive" # cachegen serde has a zip bug under the fs connector
- name: PYTHONHASHSEED
value: "0" # required: identical cache keys across Pods
其中两个环境变量需要解释,因为它们看起来都很随意,但实际上都不是:
LMCACHE_REMOTE_SERDE=naive —— cachegen 序列化器在 LMCache 的文件系统连接器下有一个 zip 序列化 bug。naive 序列化器是稳定选择。
PYTHONHASHSEED=0 —— LMCache 从 Python 哈希值派生缓存键。没有固定的种子,每个 Pod 对相同 prompt 计算出的键不同,跨 Pod 共享会在静默中无法命中。
应用它并等待两个 Pod 都变为 Ready(每个 Pod 运行三个容器:vLLM、反向代理、otel-collector):
kubectl apply -f deploy-qwen-kvcache.yaml
kubectl get pods -l app=qwen2-7b-instruct-kvcache -w # WAIT: 2 Pods, 3/3 Running
Operator 渲染的 Deployment 需要两个 CRD 无法表达的调整:将 Curvine PVC 挂载到 vLLM 容器,以及将 L2 重新指向 FUSE 挂载点。当 enableL2Cache: true 时,Operator 注入 LMCACHE_REMOTE_URL=sagemaker-hyperpod://$(NODE_IP):9200(其节点本地后端),而由于 l2CacheBackend 仅接受 redis 或 tieredstorage,没有 CRD 字段可以将 L2 指向 Curvine 路径。因此我们直接打补丁到渲染后的 Deployment。
有一个复杂情况:Operator 运行一个调和循环,对运行中的 Deployment 打的补丁会在下一次重新渲染时被覆盖。因此可靠的顺序是:暂停 Operator,将 Deployment 缩容到零,在单个命令中应用所有补丁,再扩容回来,只在 Pod 都 Ready 后恢复 Operator。缩容到零也回避了滚动更新死锁:当副本数等于可用 GPU 数(两个副本,两个单 GPU 节点)时,默认的 maxSurge 会尝试在释放旧 Pod 之前启动一个新 Pod,而新 Pod 会永远卡在 Pending 状态,等待一个永远不会释放的 GPU。
首先暂停 Operator 并排空 Deployment:
DEPLOY_NAME="qwen2-7b-instruct-kvcache"
# 1. Pause the Operator so its reconcile loop cannot overwrite the patch
kubectl scale deployment hyperpod-inference-controller-manager \
-n hyperpod-inference-system --replicas=0
# 2. Scale the model Deployment to 0 (frees the GPUs; avoids the maxSurge deadlock)
kubectl scale deployment $DEPLOY_NAME --replicas=0
接下来,找到 Operator 在容器 env 数组中放置 LMCACHE_REMOTE_URL 的位置。不要硬编码索引,它会变化