资讯详情

资讯详情

从零搭建一个 AI Infra 实验室⑲:模型服务怎么扩容——Autoscaling、Cold Start 与 Model Cache

上一篇我们通过 KServe成功将 Qwen 模型部署成了 Kubernetes 中的 InferenceService。到这里我们已经完成了几个阶段从最初让 GPU 能被容器访问到 NVIDIA Device Plugin、GPU Operator再到 vLLM 进入 Kubernetes最后通过 KServe 管理模型服务。现在模型已经能够正常运行。但如果要把它变成一个持续对外提供服务的系统还需要考虑一个非常现实的问题如果请求越来越多一个模型实例处理不过来怎么办对于普通 Web 服务我们可能首先想到 Kubernetes Deploymentreplicas: 1 ↓ replicas: 3增加几个 Pod再通过 Service 分配请求。但对于 LLM事情没那么简单。一个新的模型 Pod 不仅需要 CPU 和内存还需要 GPU。即使获得了 GPU也要准备模型文件、加载权重、初始化推理引擎最后才能真正接受请求。那么模型服务如何自动扩容HPA 能不能直接用于 LLM为什么增加了 Pod却不一定增加推理能力模型冷启动为什么会很慢Model Cache 能够解决哪些问题为什么 Scale-to-Zero 对 GPU 推理既有吸引力又很难实现本文继续使用前面搭建的 KServe 环境研究三个关键概念Autoscaling、Cold Start 与 Model Cache。一、背景问题模型扩容为什么比普通应用复杂1. 普通 Web 服务的扩容先看一个常见的无状态 Web 服务。假设我们部署一个 Python HTTP ServerClient | v Kubernetes Service | ---- Pod A | ---- Pod B | ---- Pod C每个 Pod 运行同样的程序能够独立处理请求。当 CPU 使用率提高时Kubernetes 可以通过 HPA 自动增加副本。新 Pod 启动后通过健康检查进入 Service 的可用后端集合。对于启动较快、资源需求不大的无状态应用这种模式通常比较直接。2. LLM 模型服务的不同之处但一个 LLM Pod 不是普通 HTTP Server。它通常包含Model Server | -- Runtime | -- Model Weights | -- GPU Memory | -- KV Cache | -- Inference Scheduler在真正处理请求之前还需要完成大量准备工作。比如我们使用的 vLLM首先需要找到模型文件。随后加载模型权重、初始化 CUDA 运行环境、建立 KV Cache并准备推理执行所需的数据结构与 Kernel。整个过程可能花费几十秒甚至几分钟。而且还有一个很直接的限制不是 Kubernetes 希望运行多少模型 Pod集群就一定有多少 GPU 可以使用。我们目前只有一张 RTX 3060。如果每个模型 Pod 申请nvidia.com/gpu: 1那么在默认的独占资源分配方式下这张 GPU 通常只能满足一个这样的 Pod。HPA 可以创建第二个 Pod却无法替它创造第二张 GPU。这就引出了模型扩容的第一个核心问题副本数量与实际推理能力不是一回事。二、原理Autoscaling、Cold Start 与 Model Cache1. Autoscaling自动扩缩容到底在扩什么Autoscaling顾名思义就是根据负载和资源使用情况动态调整服务规模。Kubernetes 中最常见的是 HPAHorizontal Pod Autoscaler。它管理的是工作负载的副本数量。例如低负载 | v HPA Desired Replicas 1 | v Deployment1 Pod 负载提高 | v HPA Desired Replicas 2 | v Deployment2 PodsHPA 本身不负责创建 Pod。它通过调整 Deployment 等工作负载的副本数量让对应的控制器完成实际扩缩容。因此HPA 主要完成的是一个决策现在应该有多少个副本至于新的 Pod 能否成功运行则交给 Kubernetes 调度器、容器运行时及其他基础设施组件处理。HPA 怎样判断需要扩容最常见的是 CPU 利用率。例如CPU Request1000m HPA Target50% 目标 CPU 使用量约 500m当 CPU 使用率持续高于目标时HPA 就可能增加副本。其基本计算关系为期望副本数 ≈ 当前副本数 × 当前指标值 / 目标指标值实际计算还会考虑容忍区间、指标缺失、Pod Ready 状态以及扩缩容稳定窗口等因素。不过这种方式对于 LLM 有一个问题CPU 使用率不一定能够准确反映 GPU 推理负载。一个模型可能正在 GPU 上执行大量计算但 CPU 使用率并不高。反过来模型初始化、编译或者数据准备也可能让 CPU 短时间繁忙。因此对于生产级 LLM Serving仅使用 CPU HPA 往往不够。2. GPU Autoscaling为什么不能只看 CPULLM 的性能瓶颈通常与 GPU 资源密切相关。但 GPU 利用率也不是唯一指标。我们以前学习 vLLM 时已经接触过几个概念Continuous BatchingKV CachePrefill 与 Decode请求并发TTFT 和 TPOT。这些都会影响模型的实际服务能力。例如有两个模型实例Replica A GPU Utilization70% 等待请求10 Replica B GPU Utilization90% 等待请求0究竟哪个更忙单纯看 GPU Utilization可能认为 B 更忙。但 A 的请求队列已经积累了十个请求也可能意味着用户正在承受更高的等待延迟。此外Prefill 与 Decode 的计算特征不同不同 Context Length 的请求也会产生不同的 KV Cache 压力。因此更合适的 LLM 扩容指标可能是指标关注的问题CPU UtilizationCPU 侧资源压力GPU UtilizationGPU 计算繁忙程度GPU Memory / KV Cache推理资源占用Waiting Requests请求积压程度Running Requests当前并发执行情况TTFT / Latency用户体验是否恶化这里要区分两个概念。HPA 是 Kubernetes 的自动扩缩容机制。而 GPU 指标、请求队列或者延迟只是我们希望提供给 Autoscaler 的不同观测信号。Kubernetes HPA 可以通过 Custom Metrics 等机制扩展但 Metrics Server 本身主要提供 CPU 和内存资源指标。如果需要使用 vLLM 特有的队列、KV Cache 等指标就需要额外的监控和指标适配链路。本文暂时使用 CPU HPA 验证扩缩容机制不构建复杂的 GPU 指标系统。3. 扩容决策不等于 GPU 调度这是 LLM Serving 中一个特别重要的边界。假设 HPA 判断需要两个模型副本HPA | | Desired Replicas 2 v Deployment | -- Pod AGPU 1 | -- Pod B需要 GPU 1在默认情况下Kubernetes 的 NVIDIA Device Plugin 将 GPU 作为可分配的扩展资源进行管理。对于一张 GPUAllocatable: nvidia.com/gpu: 1如果 Pod A 已经获得这一份资源Pod B 就可能因为 GPU 不足而一直 Pending。我们在第⑯篇研究过 GPU Time-Slicing。通过 Time-Slicing确实可以让多个 Pod 共享同一张 GPU。但需要注意GPU 共享并不等于 GPU 计算能力真的增加了。多个实例仍然需要竞争同一张物理 GPU 的计算和显存资源。而且 RTX 3060 并不支持 NVIDIA MIG无法像部分数据中心 GPU 那样进行硬件级实例隔离。所以真正意义上的横向扩容最终还是要考虑 GPU 资源供给。单节点实验室可以验证控制机制但不能简单等同于多节点生产集群。4. Cold Start新模型实例为什么不能立即服务HPA 决定扩容后即使 Kubernetes 有足够的 GPU也不意味着新副本能够立即提供服务。一个典型模型启动流程如下创建 Pod | v 调度到 GPU Node | v 准备模型文件 | v 启动 Model Runtime | v 加载模型权重 | v 初始化 CUDA / KV Cache | v 编译与预热 | v Readiness Probe | v 开始提供推理服务这里的整个启动过程涉及多个阶段。其中任何一个阶段耗时较长都可能影响新副本真正投入服务的时间。例如HPA 在 12:00:00 判断需要扩容。但新的模型 Pod 在 12:04:00 才 Ready。那么这四分钟内新的副本并不能承担实际推理负载。对于传统 Web 服务这种启动延迟可能相对较小。但对于 LLM尤其是几十 GB 甚至上百 GB 的大模型Cold Start 可能成为扩容的重要瓶颈。这意味着LLM Autoscaling 不只是决定何时扩容还需要考虑扩容需要多久才能真正生效。5. Model Cache能不能避免重复下载我们之前已经在 Kubernetes 中使用过 PVC 保存模型文件。这是一个很自然的优化方向。如果新 Pod 每次启动都要重新从 Hugging Face 下载模型New Pod | v Hugging Face | v Download Model | v Load to GPU那么网络速度、远程仓库可用性和模型大小都会影响冷启动。而如果模型文件已经准备好了New Pod | v Local / Shared Model Storage | v Load to GPU就可以减少远程获取模型的时间。常见的存储方式包括存储方式特点emptyDirPod 生命周期内使用通常不会跨 Pod 保留PVC可以持久化模型文件节点本地缓存可利用本地 SSD / NVMe共享模型存储多个节点或实例可以访问对于我们的实验室最容易理解的是 PVC。但要特别注意Model Cache 不等于 KV Cache。Model Cache 保存的是模型权重等模型文件。KV Cache 保存的是推理过程中产生的 Attention Key/Value 状态。二者用途完全不同Model Cache → 减少模型获取成本 KV Cache → 减少推理过程中的重复计算此外模型文件缓存也不等于模型已经加载到 GPU。即使模型文件保存在本地新的 vLLM 实例仍然需要加载权重、初始化运行环境和完成预热。所以 Model Cache 可以降低部分 Cold Start 成本却不能彻底消除冷启动。KServe 还提供可选的 Local Model Cache 能力用于更系统地管理节点级模型缓存。但它需要相应组件和存储配置不是安装基本 KServe Controller 后就自动启用。对于本篇单节点实验没有必要额外安装一整套缓存控制器。6. Scale-to-Zero把 GPU 完全释放出来可行吗Autoscaling 还有一个很吸引人的目标Scale-to-Zero。其思想是没有请求 | v 模型副本缩容到 0 | v 释放 GPU 资源 | v 新的请求到达 | v 重新启动模型对于昂贵的 GPU 资源这种机制显然有价值。特别是在多个模型共享 GPU 集群、但并非所有模型都持续有请求的场景。然而它也带来新问题没有 Pod 时新请求由谁接收又由谁判断应该启动哪个模型请求是否需要等待如果模型冷启动需要几分钟用户能否接受这种延迟KServe 的 Knative 部署模式可以结合 Serverless 机制实现相关能力。而我们目前使用的是 KServe v0.20 Standard 模式通过 Deployment 和 HPA 管理模型服务。Standard 模式下的普通 CPU HPA 并不原生提供完整的按请求 Scale-to-Zero 能力。因此本篇只讨论 Scale-to-Zero 的设计意义不在当前环境里强行实验。三、架构图KServe、HPA 与 GPU 怎样协作现在可以把前面的几个概念组合起来。整个架构可以理解为三个层次。第一层模型服务管理。InferenceService | v KServe Controller | -- Deployment | -- Service | -- HPA第二层自动扩缩容。Metrics Server | v Resource Metrics | v HPA | v Desired Replicas | v Deployment第三层实际模型运行。Kubernetes Scheduler | v GPU Resource Allocation | v Predictor Pod | -- Storage Initializer | | | v | Model Files | -- KServe Container | v vLLM | v GPU这三个层次有各自的职责。KServe 负责管理模型服务的期望状态。HPA 根据指标决定副本数量。Kubernetes 负责实际调度与运行 Pod。vLLM 负责模型推理。而 Storage Initializer 和 Model Cache 则影响模型准备阶段的效率。这也说明AI Infra 的模型弹性扩容不是某个单独组件可以完全解决的问题而是多个基础设施组件之间的协作。下面进入实验。四、实验准备整理环境并安装 Metrics Server1. 当前实验环境继续沿用之前的环境不重新安装 Kubernetes、GPU Operator、vLLM 或 KServe。项目配置主机Lenovo REN7000K-28IMBCPUIntel i5-10400FGPUNVIDIA RTX 3060 12GBOSUbuntu 24.04Kubernetesv1.37.1Container RuntimecontainerdGPU 管理NVIDIA GPU OperatorKServev0.20.0Standard 模式Runtime 后端vLLM模型Qwen2.5-0.5B-InstructNamespaceai-infra这里继续使用上一期已经创建的InferenceService: qwen-kserve不重新创建第二个模型服务。这样可以把实验重点放在 Autoscaling 和 Cold Start而不是重复部署 KServe。2. 清理上一篇遗留的资源这是一个比较容易忽略的步骤。前面几篇实验陆续创建过 Deployment、Job、PVC 和 InferenceService。如果全部保留后面观察 Pod 和 GPU 资源时会比较混乱。先检查kubectl get deploy,job,pvc,isvc -n ai-infra kubectl get pods -n ai-infra上一期在部署 KServe 时我们已经把旧的 vLLM Deployment 缩容到 0。但既然现在改用 KServe 的 Predictor就没有必要继续保留这份旧 Deployment。确认它仍然是第⑰篇创建的vllm后可以删除kubectl delete deployment vllm \ -n ai-infra --ignore-not-found之前用于下载模型的 Job 也已经完成kubectl delete job vllm-model-download \ -n ai-infra --ignore-not-found这里没有删除 GPU Operator、KServe也没有删除正在使用的qwen-kserve。对于之前创建的 PVC需要先确认是否还保存着有价值的模型文件。kubectl get pvc -n ai-infra如果暂时还希望复用第⑰篇的模型缓存可以继续保留。不应仅仅因为当前没有 Pod 挂载就直接删除 PVC它可能还包含需要保留的数据。另外如果旧 ReplicaSet 留下了Unknown状态的历史 Pod可以在确认当前 Predictor 已经 Ready 后按具体 Pod 名称删除这些失效对象。最终我们希望实验环境中只有一个真正运行的 Qwen Predictor。3. 安装 Metrics Server前面虽然已经搭建 Kubernetes但并不意味着 Metrics Server 一定已经安装。HPA 使用 CPU 和内存资源指标时需要 Metrics Server 提供 Metrics API。首先检查kubectl top nodes如果 Metrics API 尚不可用就需要安装。本实验 Kubernetes 为 v1.37.1使用 Metrics Server v0.9.0kubectl apply -f \ https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml对于实验室中的 Kubelet 证书环境我们采用下面的参数kubectl patch deployment metrics-server \ -n kube-system --typejson \ -p[{op:add,path:/spec/template/spec/containers/0/args/-,value:--kubelet-insecure-tls}]该参数用于跳过 Kubelet 服务证书校验适合本次实验环境正式生产环境应正确配置证书信任。如果 Metrics Server 已能正常采集则无需添加。等待 Deployment Readykubectl rollout status deployment/metrics-server \ -n kube-system最后检查kubectl top nodes kubectl top pods -n ai-infra能够正常显示 CPU 和内存用量就说明基础指标链路已经可用。这里不需要安装 Prometheus、Grafana 或额外的 Metrics Adapter。因为本次实验只使用 CPU HPA。4. Storage Initializer模型下载、Xet 与内存配置上一篇我们通过 KServe 从 Hugging Face 下载了 Qwen 模型。这里还有一个值得补充的问题KServe 是怎样获取模型文件的对于storageUri: hf://Qwen/Qwen2.5-0.5B-InstructKServe 会通过 Storage Initializer在模型推理容器启动之前准备模型文件。其中Hugging Face Hub 可能使用 Xet 存储技术。4.1 Hugging Face Xet 是什么Xet 是 Hugging Face 用于管理和传输大型模型、数据集的一种存储技术。它使用内容寻址和数据块去重等机制减少重复数据的存储与传输开销。从huggingface_hub 0.32.0开始Python 客户端通常会自动安装并使用hf-xet。但需要区分Xet StorageHugging Face 仓库端的存储机制。hf-xetPython 客户端使用的 Xet 下载组件。即使模型文件采用 Xet 存储也可以禁用hf-xet客户端改用普通 HTTP 下载路径。在我们的实验中由于使用网络代理之前的模型下载存在长时间没有完成的情况。为了减少实验环境中的不确定性我们选择禁用hf-xet。这不是所有 KServe 环境都需要采用的配置。如果 Xet 下载正常没有必要禁用它。4.2 检查 Storage Initializer 配置首先查看 KServe 的默认 StorageContainerkubectl get clusterstoragecontainer kubectl get clusterstoragecontainer default -o yaml找到其中的spec: container: name: storage-initializer image: kserve/storage-initializer:v0.20.0注意查看两个地方env: # HTTP_PROXY、HTTPS_PROXY 等 resources: limits: cpu: 1 memory: 1Gi其中env负责配置代理及 Hugging Face 下载客户端环境变量resources用于控制模型下载容器的资源使用。在本次环境中我们已经配置了 HTTP/HTTPS 代理但 Storage Initializer 原来的内存限制只有1Gi。为避免下载大文件时出现内存不足后面将其调整为3Gi。4.3 禁用 hf-xet 并调整内存限制先备份当前配置kubectl get clusterstoragecontainer default \ -o yaml storagecontainer-backup.yaml编辑kubectl edit clusterstoragecontainer default在原有spec.container.env列表中增加- name: HF_HUB_DISABLE_XET value: 1保留现有的HTTP_PROXY、HTTPS_PROXY、NO_PROXY等环境变量不要覆盖整个列表。如果已经存在这个变量只需检查其值不必重复添加。然后将同一个spec.container.resources下的内存限制调整为resources: requests: cpu: 100m memory: 100Mi limits: cpu: 1 memory: 3Gi这里的3Gi是根据本次实验实际遇到的内存不足问题选择的配置并不是 KServe 的统一推荐值。保存后检查kubectl get clusterstoragecontainer default \ -o jsonpath{.spec.container.env[?(.nameHF_HUB_DISABLE_XET)].value}{\n} kubectl get clusterstoragecontainer default \ -o jsonpath{.spec.container.resources.limits.memory}{\n}预期分别输出1 3Gi需要说明修改 ClusterStorageContainer 不会改变已经创建的 Init Container。新配置会在后续 Predictor Pod 创建时生效。如果目前模型服务已经 Ready没有必要为了验证环境变量而专门重启 Pod。4.4 验证 Xet 是否真正禁用如果后面创建了新的 Predictor Pod并且它正处于Init:0/1阶段可以通过下面的命令检查下载客户端kubectl exec -i -n ai-infra \ Predictor-Pod名称 \ -c storage-initializer -- python - PY import os from huggingface_hub.file_download import is_xet_available print(HF_HUB_DISABLE_XET:, os.getenv(HF_HUB_DISABLE_XET)) print(Xet available:, is_xet_available()) PY预期结果HF_HUB_DISABLE_XET: 1 Xet available: False注意这个命令必须在 Storage Initializer 正在运行时执行。初始化结束后Init Container 已退出无法再通过kubectl exec进入。还可以检查 Pod 实际获得的内存限制kubectl get pod Predictor-Pod名称 \ -n ai-infra \ -o jsonpath{.spec.initContainers[0].resources.limits.memory}{\n}对于采用新配置创建的 Pod预期输出3Gi。最后通过日志查看模型下载结果kubectl logs -n ai-infra \ Predictor-Pod名称 \ -c storage-initializer成功后可以看到类似Successfully copied hf://Qwen/Qwen2.5-0.5B-Instruct to /mnt/models我们在这套配置下已经成功完成模型下载。需要强调之前的下载问题并不能确定完全由 Xet 导致因为实验过程中也发现 Storage Initializer 存在内存限制不足的问题。5. 检查 KServe 当前状态kubectl get isvc qwen-kserve -n ai-infra正常情况下应该是READY True再检查kubectl get deploy,pods -n ai-infra kubectl get hpa -n ai-infra当前 Predictor 只需要一个副本。由于整个实验室只有一张 GPU模型部署更新继续使用此前配置的Recreate策略避免滚动更新时新旧 Predictor 同时争夺 GPU。需要注意Recreate控制的是 Deployment 更新策略并不能阻止 HPA 主动增加副本。至此实验环境准备完成。五、实验一HPA 能否自动扩容 GPU 模型这是本篇最重要的实验。我们只需要验证两个问题第一HPA 能否根据 CPU 指标扩容第二只有一张 GPU 时扩容会发生什么1. 配置 HPA首先为 InferenceService 指定 HPAkubectl annotate isvc qwen-kserve \ -n ai-infra \ serving.kserve.io/autoscalerClasshpa \ --overwrite然后设置kubectl patch isvc qwen-kserve \ -n ai-infra --typemerge \ -p { spec: { predictor: { minReplicas: 1, maxReplicas: 2, scaleMetric: cpu, scaleTarget: 50 } } }参数含义最小副本1 最大副本2 扩容指标CPU 目标利用率50%原有 Predictor 的 CPU Request 为requests: cpu: 1所以 HPA 的 CPU 目标对应约 500m。执行kubectl get hpa -n ai-infra应该能看到 KServe 管理的 Predictor HPA。如果修改 InferenceService 引发新的 Pod 版本部署需要等模型恢复 Ready 后再观察自动扩缩容。2. 主动制造 CPU 负载观察扩容前面已经配置好 HPACPU Target: 50% Min Replicas: 1 Max Replicas: 2现在我们主动制造 CPU 压力观察 HPA 是否扩容。Terminal 1持续观察 HPA 和 Podwatch -n 2 \ kubectl get hpa,deploy,pods -n ai-infraTerminal 2制造 CPU 负载先确认 Predictor 已经1/1 Runningkubectl get pods -n ai-infra在实验准备完成、命名空间只有一个 Predictor Pod 的情况下获取其名称POD$(kubectl get pods -n ai-infra \ -o jsonpath{.items[0].metadata.name}) echo $POD然后在kserve-container内运行一段持续 180 秒的 Python CPU 计算kubectl exec -i -n ai-infra $POD \ -c kserve-container -- python - PY import time end time.monotonic() 180 while time.monotonic() end: pass print(CPU load finished) PY这段程序会持续消耗约一个 CPU 核心180 秒后自动结束。它不需要额外创建 Deployment也不会修改模型文件。回到 Terminal 1观察 HPA。在本次实验环境中CPU Request 为 1000mHPA 目标为 50%因此持续的 CPU 负载应该能够触发扩容。我们之前实际观察到HPA CPU: 100% / 50% HPA Replicas: 2 Deployment: 1/2 Ready第二个 Pod 处于0/1 Pending这是因为整个集群只有一张 RTX 3060而每个 Predictor 都要求独占一张 GPU。如果需要确认可以使用kubectl describe pod Pending-Pod名称 \ -n ai-infra查看是否出现Insufficient nvidia.com/gpu。需要说明本次是模拟 CPU 负载以验证 CPU HPA 的扩容机制不代表真实 LLM 请求吞吐量测试。3. 停止负载观察自动缩容上面的 Python 程序运行 180 秒后会自动结束CPU load finished此时不需要执行额外的停止命令CPU 负载会自然下降。继续在 Terminal 1 观察watch -n 2 \ kubectl get hpa,deploy,pods -n ai-infraCPU 使用率下降后HPA 会考虑将副本数从 2 缩减到 1。需要注意HPA 默认具有约 5 分钟的缩容稳定窗口因此不会在 CPU 下降后立刻删除第二个 Pod。最后可以查看 HPA 事件kubectl describe hpa qwen-kserve-predictor \ -n ai-infra我们此前实际观察到New size: 2 reason: cpu resource utilization above target New size: 1 reason: All metrics below target到这里就验证了完整的CPU 负载增加 → HPA 扩容 → GPU 资源不足 → CPU 负载下降 → HPA 缩容。六、实验二观察一次模型 Cold Start实验一验证了 HPA 能够增加副本数量但即使 Kubernetes 有足够的 GPU 资源新增 Pod 也不能立刻提供推理服务。接下来我们主动重建一次 Predictor Pod观察模型冷启动过程。1. 恢复单副本状态首先确保实验一已经结束CPU 压力程序已停止HPA 最大副本数恢复为 1。如果尚未恢复可以执行kubectl patch isvc qwen-kserve \ -n ai-infra --typemerge \ -p { spec: { predictor: { minReplicas: 1, maxReplicas: 1 } } }然后检查kubectl get isvc,hpa,deploy,pods -n ai-infra确认HPA 最小和最大副本数均为 1。Deployment 为1/1 Ready。当前只有一个正在运行的 Predictor Pod。InferenceService 为ReadyTrue。注意如果恢复 HPA 配置时已经触发了 Predictor 重建可以直接把这次重建作为 Cold Start 实验不必再删除一次 Pod。2. 删除 Predictor触发重新创建先找到当前正在运行的 Predictorkubectl get pods -n ai-infra记录它的名称例如qwen-kserve-predictor-xxxxxTerminal 1观察状态变化watch -n 2 \ kubectl get isvc,deploy,pods -n ai-infraTerminal 2记录开始时间并删除 PodT0$(date %s) kubectl delete pod \ 当前Predictor-Pod名称 \ -n ai-infra这里仅删除 Pod不删除 Deployment 或 InferenceService。因为 Deployment 的期望副本数仍然为 1Kubernetes 会自动创建一个新的 Predictor Pod。需要注意模型服务在重建期间会暂时中断因此这个操作只适合当前实验环境。3. 观察 Cold Start 各个阶段回到 Terminal 1观察 Pod 状态变化Pod Created | v Pending | v Init:0/1 | v PodInitializing | v Running 0/1 | v Running 1/1这里最值得关注两个阶段。第一阶段Storage Initializer负责从 Hugging Face 准备模型文件。找到新创建的 Pod查看日志kubectl logs -n ai-infra \ 新Predictor-Pod名称 \ -c storage-initializer -f下载完成后可以看到类似Successfully copied hf://Qwen/Qwen2.5-0.5B-Instruct to /mnt/models Model downloaded in 238.298 seconds.这里的238.3 秒是这次实验得到的实际数据。第二阶段vLLM 初始化Storage Initializer 完成后KServe 启动主容器。执行kubectl logs -n ai-infra \ 新Predictor-Pod名称 \ -c kserve-container -f重点观察Model loading took ... torch.compile took ...这两个阶段分别反映模型权重加载以及相关编译工作耗费的时间。需要注意它们并不代表 vLLM 初始化的全部耗时。4. 记录 Pod Ready 总时间回到 Terminal 2等待新的 Predictor Pod Readykubectl wait pod/新Predictor-Pod名称 \ -n ai-infra \ --forconditionReady \ --timeout1800s T1$(date %s) echo Cold Start: $((T1-T0)) seconds这里的T0是删除旧 Pod 前记录的时间。因此这个数字包含旧 Pod 删除、重新调度、模型准备以及新 Pod 启动到 Ready 的过程适合作为本实验的端到端恢复时间而不是纯粹的 vLLM 引擎初始化时间。最后确认kubectl get isvc,deploy,pods -n ai-infra确保 InferenceService 也已经恢复ReadyTrue。5. 记录实验结果本次只需要记录三个主要数据观察项目实验结果Pod 重建至 Ready 总时间393 sStorage Initializer 模型下载238.3 svLLM 模型权重加载103.92 s如果希望更细致一些可以补充torch.compile时间但无需进行大量重复测试。我们之前的结果中0.5B 模型权重加载仅耗费约 1.38 秒而模型下载约为 235 秒。虽然不同模型和运行环境的耗时可能存在很大差异但本次实验已经说明即使 GPU 足够新的模型副本仍然可能花费数分钟才能真正投入服务。这也是为什么自动扩容不能只考虑副本数量还需要考虑模型准备和冷启动成本。七、实验三检查 Model Cache为什么不必每次下载最后一个实验不再重复下载模型。我们只检查当前 KServe 模型是怎样存储的并与前面第⑰篇的 PVC 方案进行对比。1. 检查模型目录从前面的日志可以知道Model Source: hf://Qwen/Qwen2.5-0.5B-Instruct Model Destination: /mnt/models在 Pod 启动时Storage Initializer 负责准备模型。随后主容器从/mnt/models加载权重。这两个容器共享 Pod 内的模型目录。如果当前目录由emptyDir提供那么它通常会伴随 Pod 的删除而消失。我们可以查看 Pod 配置kubectl get pod Predictor-Pod名称 \ -n ai-infra -o yaml重点检查spec.volumes spec.initContainers spec.containers volumeMounts确认/mnt/models的数据究竟来自emptyDir、PVC还是其他存储。如果使用emptyDir删除并重建 Pod 后就不能依赖这份数据继续存在。这也是为什么我们多次重建 KServe Predictor 时需要重新准备模型。2. 回顾上一篇的 PVC 模型缓存实际上在第⑰篇把 vLLM 部署到 Kubernetes 时我们已经使用过 PVC 和 Hugging Face Cache 保存模型数据。那篇使用的是Qwen2.5-7B-Instruct-AWQ本篇 KServe 使用的是Qwen2.5-0.5B-Instruct所以不能直接把第⑰篇保存的 7B AWQ 模型当作这次 0.5B 模型的缓存。但两者使用的原理是相同的。如果提前把目标模型下载到持久化存储并让新的 Predictor 复用它就有机会省去每次启动时的远程下载阶段。例如方案 A远程模型下载 Pod Created | v Download from Hugging Face | v Load Model | v Ready 方案 B已有模型文件 Pod Created | v Read Model from PVC | v Load Model | v Ready方案 B 不需要重复从远程仓库获取相同的模型文件。但它仍然需要完成模型加载和推理引擎初始化。3. 为什么这次不再增加缓存性能实验原因很简单我们在前面的实验中已经验证过 PVC 保存模型文件的机制。如果为了这一篇再建立一套 PVC、下载 Job、Storage 初始化流程只是把之前做过的工作重复一遍。而且还需要分别记录下载时间、加载时间、缓存命中情况等数据。对于理解 Model Cache 的基本原理并没有必要。这次实验重点只是确认当前 KServe 的模型文件存在哪里以及 Pod 重建后这份文件还能不能复用。至于更复杂的 Local Model Cache、跨节点缓存、缓存预热和淘汰策略可以等以后真正搭建多 GPU、多节点推理服务时再研究。八、观察结果三个实验说明了什么经过这三个实验我们可以将结果浓缩为一张表。实验观察结果核心结论HPA 自动扩容CPU 达到 108%副本数变为 2但只有 1 个 Ready扩容决策不等于 GPU 资源充足Cold Start模型下载约 238 秒Pod 创建后不一定能立即服务Model Cache检查 Pod 模型目录与持久化方式提前准备模型可以减少重复下载到这里Autoscaling、Cold Start 与 Model Cache 的关系也就清楚了负载提高 | v Autoscaling 决定增加模型副本 | v GPU Scheduling 必须有足够计算资源 | v Cold Start 新副本还要完成初始化 | v Model Cache 减少其中的模型获取开销 | v Model Ready 真正增加推理能力它们不是三个孤立的技术。而是模型服务弹性扩容过程中相互关联的三个问题。九、实验结束清理资源并恢复稳定状态按照之前几篇实验的习惯实验完成后需要整理资源。但这里也不能简单地把全部东西删除。因为下一篇我们还要继续使用 KServe 模型服务。1. 恢复单副本由于只有一张 RTX 3060不需要长期允许 HPA 创建两个独占 GPU 的模型副本。将最小和最大副本数都设置为 1kubectl patch isvc qwen-kserve \ -n ai-infra --typemerge \ -p { spec: { predictor: { minReplicas: 1, maxReplicas: 1 } } }HPA 仍然可以保留但它不会再请求创建第二个 GPU Pod。等待副本恢复后检查kubectl get isvc,hpa,deploy,pods -n ai-infra目标状态InferenceService: ReadyTrue HPA Replicas: 1 Deployment: 1/1 Ready Predictor Pod: 1/1 Running如果还有失效的历史 Pod确认它们不是当前正常运行的实例后再按名称清理。2. 哪些资源可以清理对于这一阶段的实验资源我们可以这样处理资源处理方式原因旧 vLLM Deployment删除已由 KServe Predictor 替代已完成的模型下载 Job删除不需要继续运行旧的 Unknown Pod确认后清理避免污染后续观察不再使用的实验 PVC确认数据无保留价值后删除避免占用磁盘这里需要注意PVC 删除可能导致持久化数据丢失不能将它和删除一个普通 Pod 等同对待。3. 哪些资源需要保留以下资源不必清理资源保留原因Kubernetes / containerd基础集群NVIDIA GPU OperatorGPU 基础设施KServe Controller / CRD模型服务管理Metrics Server后续资源指标与监控qwen-kserve InferenceService下一篇继续使用qwen-kserve Predictor Service下一篇继续使用已验证可用的模型存储配置避免不必要的重复部署下一篇将研究 AI Gateway需要一个正常运行的 LLM 后端服务。因此我们保留目前单副本的 Qwen KServe Predictor作为后面网关实验的目标服务。这样整个实验室既不会残留太多无用资源也不需要每篇文章都重新搭建模型。十、总结模型扩容不是简单地增加 Pod在前面的 Kubernetes 实验中我们已经知道 Deployment 可以管理副本Service 可以转发请求。到了这一篇我们进一步研究模型服务的弹性扩容。HPA 能够根据指标调整副本数量。但它不负责 GPU 资源供给。Kubernetes Scheduler 可以将 Pod 调度到具有足够资源的节点。但调度成功也不意味着模型已经能够提供推理。KServe 能够组织模型准备和服务生命周期。但如果模型需要从远程仓库下载或者推理引擎初始化时间很长Cold Start 仍然存在。Model Cache 则可以减少重复获取模型文件的开销。但它也无法代替模型权重加载、GPU 初始化和推理预热。所以一个真正具有弹性的 LLM Serving 系统至少要考虑扩容决策、GPU 资源、模型启动以及模型缓存。不过到这里我们还只是解决了“模型实例如何运行以及如何增加实例”的问题。假设以后实验室不只有一张 RTX 3060而是拥有多张 GPU可以同时运行多个模型实例。那么又会产生新的问题普通 Kubernetes Service 主要根据网络连接和 Endpoint 转发流量。但 LLM 请求具有非常不同的特征。有的请求 Prompt 很长有的很短有的正在 Prefill有的正在 Decode有的需要持续 Streaming另一些则可能很快结束。如果只使用普通负载均衡并不知道后端哪个 vLLM 实例更忙、哪个实例更适合处理当前请求也未必能够利用已有的 Prefix Cache。同时模型服务还可能需要统一认证、限流、Token 配额、请求路由和模型选择。这些问题已经不只是 Autoscaling而是LLM 流量管理。下一篇我们将继续研究看看为什么在模型服务前面增加一层理解 LLM 请求的 Gateway可能比单纯增加更多 Pod 更有意义。参考资料KubernetesHorizontal Pod AutoscalingMetrics ServerGitHubKServeHPA AutoscalingKServeModel Storage OverviewKServeLocal Model Cache
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →