尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从单模型服务到LLM推理平台:模型部署、vLLM与K8s实战

从单模型服务到LLM推理平台:模型部署、vLLM与K8s实战 1. 从跑通一个模型到撑起一个平台的认知分水岭很多人第一次接触模型部署都是从一行docker run或者一个python app.py开始的。本地把权重加载进去接口能返回结果截图发个朋友圈这事就算成了。但真正到了正式环境你会发现事情完全不是这个量级模型不止一个请求量忽高忽低GPU 卡就那么几张业务方还要求灰度发布、A/B 测试、监控告警、故障回滚。这时候你面对的已经不是怎么把模型跑起来而是怎么让一堆模型在有限资源上稳定地对外服务。这篇内容想聊的就是这条演进路径从最朴素的单模型服务到多模型统一调度再到面向 LLM 的推理平台。关键词里出现的模型部署、LLM、vLLM、Triton、K8s基本勾勒出了这条路径上的核心组件。我会把每个阶段为什么这么选坑在哪里参数怎么定讲清楚而不是丢一堆名词让你自己去拼。适合已经能把模型跑起来、但一上生产就手忙脚乱的工程师也适合正在做推理平台选型的技术负责人。先说一个反直觉的结论单模型服务和推理平台之间最大的差距不是技术栈而是资源抽象这件事。单模型服务里模型和进程是一一绑定的你脑子里想的是这个进程占几张卡而平台化之后模型变成了一个可以被调度、被复用、被动态加载的资源对象你脑子里想的应该是这批请求该路由到哪个已加载的模型实例上。想不通这一层后面堆再多组件都是白搭。2. 单模型服务阶段那些看起来能用、上量就崩的做法2.1 最朴素的 Flask/FastAPI 包一层为什么撑不住最常见的起步方式写个 FastAPI启动时把模型 load 进显存暴露一个/predict接口前面挂个 Nginx。小流量下确实没问题我早期做图像分类服务就是这么干的。但问题会在几个地方集中爆发。第一是并发模型不匹配。Python 的 GIL 决定了你用多线程跑推理基本没意义真正吃满 GPU 要靠批处理batching。而朴素写法是一个请求进来就推理一次GPU 利用率可能只有 10%~20%因为大部分时间在等数据搬运和 kernel 启动。第二是显存常驻。模型一直占着卡哪怕半夜没请求也不释放多模型场景下这就是灾难。第三是没有健康检查和优雅退出进程崩了没人拉起来正在处理的请求直接丢。我见过一个团队用这种方式部署 BERT 类模型单卡 QPS 卡在 30 上不去后来换成带动态批处理的方案同样的卡跑到 200。差距不在模型在服务框架。2.2 动态批处理单模型服务第一个必须补的课动态批处理dynamic batching的核心思想是在极短的时间窗口内比如几毫秒到几十毫秒把多个请求攒成一个 batch一起送进 GPU。GPU 最擅长的就是并行矩阵运算batch size 从 1 提到 8延迟可能只增加 20%但吞吐直接翻好几倍。这里有个关键参数叫max_batch_size和batch_timeout。设太小攒不起来等于没批设太大延迟飙升用户体验崩。经验值是在线交互类服务batch_timeout 控制在 5~10msmax_batch_size 根据显存和模型大小定通常 8~32。离线批处理任务可以放宽到几百毫秒甚至秒级。提示动态批处理不是万能的。如果请求的输入长度差异极大比如 LLM 场景有的问一句话有的贴一整篇文档简单 padding 到统一长度会浪费大量算力。这时候需要的是连续批处理continuous batching这是后面讲 vLLM 的重点。2.3 单模型服务的边界在哪里判断你该不该继续用单模型服务看三个信号模型数量超过 3 个、GPU 卡需要跨模型共享、业务要求不停机更新模型。只要中了一条就该考虑往平台化走了。继续硬扛的代价是运维复杂度指数上升——每个模型一套部署脚本、一套监控、一套扩缩容逻辑改一处要动十处。3. 多模型服务框架Triton 到底解决了什么问题3.1 Triton 的核心抽象模型仓库 调度器NVIDIA Triton Inference Server 是这一层最典型的代表。它把模型抽象成一个**模型仓库model repository**里的目录每个模型有自己的配置文件config.pbtxt声明输入输出、使用的后端backend、批处理策略、实例数量。服务启动时扫描仓库按配置加载。这个抽象带来的直接好处是同一个进程可以同时服务多个模型共享 GPU 资源统一暴露 HTTP/gRPC 接口。你不用再为每个模型写一套服务代码只需要写配置。支持的 backend 覆盖 TensorRT、ONNX Runtime、PyTorch、TensorFlow甚至 Python 自定义 backend。一个典型的config.pbtxt长这样name: resnet50 platform: onnxruntime_onnx max_batch_size: 16 input [ { name: input data_type: TYPE_FP32 dims: [3, 224, 224] } ] output [ { name: output data_type: TYPE_FP32 dims: [1000] } ] dynamic_batching { preferred_batch_size: [8, 16] max_queue_delay_microseconds: 5000 } instance_group [ { count: 2 kind: KIND_GPU gpus: [0] } ]instance_group里的count: 2表示在 GPU 0 上起两个模型实例配合动态批处理能进一步提升吞吐。preferred_batch_size告诉调度器优先凑成 8 或 16 的批。3.2 模型版本管理与热更新Triton 的模型仓库支持版本号目录比如resnet50/1/、resnet50/2/。默认策略是加载最新版本但你可以通过接口指定用哪个版本实现灰度。更实用的是模型控制 API不重启服务的情况下可以 unload 旧版本、load 新版本。这对生产环境太重要了——重启一次服务所有模型都要重新加载冷启动可能几分钟期间服务不可用。我踩过的一个坑Triton 的模型加载是懒加载还是预加载取决于配置。默认启动时会尝试加载所有模型如果某个模型配置写错整个服务可能起不来。生产环境建议开启--strict-model-configfalse让它自动推断配置同时用--model-control-modeexplicit手动控制加载时机避免一个坏模型拖垮全局。3.3 Triton 在 LLM 场景下的局限Triton 很强但它是为传统深度学习模型设计的。到了 LLM 时代它的短板暴露出来LLM 的推理是自回归的一个请求要跑几十上百步每步生成一个 token。传统批处理是一个 batch 一起进一起出而 LLM 里不同请求的生成长度不同短请求早就结束了长请求还在跑如果按传统批处理短请求要等长请求GPU 利用率极低。这就是为什么 LLM 推理需要专门的框架。Triton 也能通过 TensorRT-LLM backend 跑 LLM但配置复杂且对连续批处理的支持不如 vLLM 这类原生方案来得自然。4. LLM 推理框架选型vLLM 凭什么成为主流4.1 PagedAttention把显存碎片问题摁下去LLM 推理的显存消耗分两块模型权重和 KV Cache。KV Cache 是注意力机制里缓存的历史 key/value随着生成长度线性增长。传统做法是给每个请求预分配一块连续显存按最大可能长度分配结果就是大量显存被浪费在预留但没用上的空间里碎片严重能同时服务的请求数上不去。vLLM 的核心创新PagedAttention借鉴了操作系统的虚拟内存分页思想把 KV Cache 切成固定大小的 block按需分配逻辑上连续、物理上可以不连续。这样一来显存利用率能从传统的 20%~40% 提升到 90% 以上同样的卡能同时服务的请求数翻好几倍。这不是玄学是实打实的吞吐提升。4.2 连续批处理让 GPU 一刻不停连续批处理continuous batching也叫 iteration-level scheduling解决的是前面提到的长短请求互相拖累问题。它的做法是以每一步生成一个 token为调度单位某一步结束后完成的请求立刻退出 batch新请求立刻补进来。GPU 永远在处理一个满员的 batch利用率拉满。实测下来vLLM 相比朴素的 HuggingFacegenerate循环吞吐提升通常在 10 倍以上具体倍数取决于请求长度分布和并发量。这也是为什么现在一提 LLM 部署vLLM 几乎是默认答案。4.3 vLLM 部署的实操参数与坑启动一个 vLLM 的 OpenAI 兼容服务典型命令python -m vllm.entrypoints.openai.api_server \ --model /models/Qwen2.5-7B-Instruct \ --served-model-name qwen2.5-7b \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 256 \ --port 8000几个参数值得展开说--tensor-parallel-size张量并行度等于用几张卡跑一个模型。7B 模型单卡 24G 基本够70B 就得上 4 卡或 8 卡。注意这个值必须是 2 的幂次或能被注意力头数整除否则起不来。--gpu-memory-utilizationvLLM 会按这个比例预占显存用于 KV Cache。设 0.9 意味着留 10% 给其他开销。设太高容易 OOM设太低浪费显存。建议从 0.85 起步观察稳定后再调。--max-model-len最大上下文长度。这个值直接决定 KV Cache 的单请求占用设太大显存吃紧设太小长文本请求会被拒。要和模型本身支持的长度匹配。--max-num-seqs同时处理的最大请求数。太小并发上不去太大显存扛不住。注意vLLM 启动时会做一次显存 profiling如果gpu-memory-utilization设得过高可能在 profiling 阶段就报 OOM。遇到这种情况先降这个值而不是怀疑模型有问题。另一个常见坑是模型格式。vLLM 对 HuggingFace 格式支持最好GGUF 格式虽然也能跑但性能打折ONNX 需要额外转换。如果你手头是 GGUF 量化模型想要极致性能建议用 llama.cpp 系列想要高并发服务还是转成 HF 格式喂给 vLLM。4.4 vLLM、SGLang、TGI、Ollama 的定位差异这几个经常被放在一起比其实定位完全不同框架定位适合场景不适合场景vLLM高吞吐生产推理在线服务、高并发单机个人玩票SGLang高吞吐 复杂编排需要结构化生成、多轮简单问答TGIHuggingFace 官方HF 生态深度用户非 HF 模型Ollama本地一键运行个人开发、桌面端生产高并发llama.cpp极致轻量边缘设备、CPU 推理大规模并发Ollama 底层其实也用了 llama.cpp它的价值在于把模型下载、量化、运行打包成一条命令对个人开发者极其友好。但它的调度能力、并发能力、显存管理都远不如 vLLM别拿它上生产。我见过有人用 Ollama 扛线上流量QPS 一过 5 就开始排队这就是定位错配。5. K8s 上的推理平台把模型当成一等公民来调度5.1 为什么推理平台最终都会落到 K8s当模型数量、GPU 节点数量、团队人数都上来了裸机部署的运维成本会压垮你。K8s 提供的价值在于统一的资源调度、声明式的部署管理、自动扩缩容、服务发现、滚动更新。这些能力对推理平台来说是刚需。但 K8s 原生不认识 GPU。你需要NVIDIA Device Plugin把 GPU 暴露成可调度资源然后在 Pod 里通过resources.limits申请resources: limits: nvidia.com/gpu: 2这样调度器就知道这个 Pod 要占 2 张卡会把它调度到有足够 GPU 的节点上。5.2 推理服务的 K8s 部署形态Deployment 还是 StatefulSet大多数推理服务用Deployment就够了因为模型实例是无状态的模型权重从镜像或 PVC 加载KV Cache 是请求级的。但有几个细节要注意模型权重怎么进容器小模型可以打进镜像大模型几十上百 G必须用 PVC 挂载或者启动时从对象存储拉。打进镜像会导致镜像巨大、拉取慢、更新困难。就绪探针readinessProbe要配好模型加载可能要几分钟这期间 Pod 不能接流量。探针要指向一个模型已加载完成的健康接口而不是简单的端口探测。优雅退出preStop terminationGracePeriodSecondsLLM 请求可能跑很久直接 kill 会丢请求。要给足退出时间让正在处理的请求跑完。5.3 自动扩缩容HPA 在推理场景下的尴尬K8s 的 HPA 默认按 CPU/内存扩缩容但推理服务的瓶颈是GPU 和请求队列长度CPU 可能一直很低。这时候需要基于自定义指标扩缩容比如用 Prometheus Adapter 把 vLLM 暴露的num_requests_waiting指标接进来队列长了就扩容。但 GPU 扩容有个现实问题扩容慢。一个新 Pod 从调度到模型加载完成可能要好几分钟。等它起来流量高峰可能已经过去了。所以生产上更常见的策略是预留 buffer平时保持一定冗余实例配合较激进的扩容阈值和较保守的缩容阈值宁可多占一点资源也别让用户等。5.4 多模型共享 GPUMIG 与时间片一张卡怎么给多个模型用两条路MIGMulti-Instance GPU把 A100/H100 这类卡硬件切分成多个独立实例每个实例有独立的显存和算力隔离性好。缺点是切分粒度固定且不是所有卡都支持。时间片共享多个进程轮流用 GPU靠 CUDA 的上下文切换。隔离性差一个进程跑满会影响其他进程但灵活。生产上如果模型对延迟敏感优先 MIG如果是离线批处理混部时间片也能接受。Triton 的instance_group其实就是在做进程级的共享配合显存控制能实现一定程度的隔离。6. 平台化之后的新问题网关、可观测性与成本6.1 LLM 网关别让业务直接怼推理服务推理平台前面通常要加一层网关职责包括鉴权、限流、路由、多模型统一接口、token 计费统计。关键词里提到的 llm 网关 就是这个东西。它的价值在于把哪个模型部署在哪、用什么协议这些细节对业务屏蔽掉业务只管调一个统一的 OpenAI 兼容接口。路由策略是网关的核心。常见的有按模型名路由、按成本路由便宜的模型优先、按负载路由哪个实例队列短去哪个、按用户等级路由VIP 走专用实例。这些策略组合起来能在不增加硬件的情况下显著提升资源利用率。6.2 可观测性LLM 服务的指标和传统服务不一样传统服务看 QPS、延迟、错误率就够了。LLM 服务还要看首 token 延迟TTFT、每 token 输出延迟TPOT、输入输出 token 数、KV Cache 使用率、排队请求数。TTFT 决定用户感觉快不快TPOT 决定吐字顺不顺这两个指标比总延迟更能反映体验。vLLM 原生暴露 Prometheus 指标接上 Grafana 就能看。重点盯vllm:num_requests_waiting排队数和vllm:gpu_cache_usage_percKV Cache 使用率前者持续大于 0 说明该扩容了后者接近 1 说明显存快满了。6.3 成本控制推理平台最容易被忽视的一环GPU 是烧钱的。平台化之后如果没有成本意识很容易出现卡都占着但利用率很低的情况。几个实用手段按需加载/卸载模型冷门模型不常驻请求来了再加载用完一段时间后卸载。代价是首次请求慢。量化INT8/INT4 量化能把显存占用降一半甚至更多吞吐提升明显精度损失在可接受范围内。AWQ、GPTQ 是常见的 LLM 量化方案。混部把延迟不敏感的离线任务和在线服务混在同一批卡上用优先级调度错峰使用。7. 我在实际落地中踩过的几个真实坑第一个坑是显存碎片导致的看起来够用其实不够。有次部署一个 13B 模型卡是 24G模型权重占 26G 的一半多理论上够。但实际跑起来频繁 OOM原因是 KV Cache 分配时找不到连续的大块显存。后来把gpu-memory-utilization从 0.95 降到 0.85问题消失。显存不是看总量是看最大连续可用块。第二个坑是K8s 的 GPU 调度和实际显存不匹配。K8s 只认几张卡不认卡上还剩多少显存。如果两个 Pod 各申请 1 张卡但每个模型都要占满整卡显存第二个 Pod 起来就会 OOM。解决办法是用 MIG 切分或者用支持显存感知调度的方案再或者干脆一个模型独占一张卡。第三个坑是模型加载时间被严重低估。一个 70B 模型从磁盘加载到显存冷启动可能要 5~10 分钟。如果 K8s 的initialDelaySeconds设得太短Pod 还没加载完就被判定为不健康反复重启永远起不来。探针的初始延迟一定要按最坏情况设宁可多等别让它误杀。第四个坑是网关超时和推理超时没对齐。网关默认 30 秒超时但 LLM 生成一篇长文可能要 60 秒结果网关先断了用户看到报错其实后端还在正常跑。网关超时必须大于推理服务的最大预期耗时并且要支持流式返回让用户边生成边看到内容。8. 从当前架构往下一步走的几个方向如果你现在的平台已经能稳定跑起来接下来可以关注这几个方向。一是推理加速的持续优化比如投机解码speculative decoding、前缀缓存prefix caching前者用小模型草稿大模型验证来提速后者对多轮对话场景能省掉重复的 prompt 计算。二是多模态模型的统一服务图像、语音、文本模型用同一套调度框架管理这对平台抽象能力要求更高。三是弹性与混部把在线推理和离线训练/批处理放到同一个资源池用优先级和抢占机制提升整体利用率。这些方向没有标准答案取决于你的业务形态和成本结构。但有一条是确定的平台的价值不在于支持多少种模型而在于把部署一个模型这件事的成本降到足够低。当业务方能在几分钟内自助上线一个新模型且不用关心底层是几张卡、跑在哪个节点上时这个平台才算真正立住了。
返回列表