1. 从单模型到推理平台:部署这件事到底在解决什么问题
模型部署这个词,听起来像是运维的活儿,但真正做过的人都知道,它其实是算法、工程、硬件三者的交叉地带。你训练出一个模型,准确率再高,如果推不出去、跑不起来、扛不住并发,那它就只能躺在实验记录里。我见过太多团队在实验室里把指标刷到SOTA,一到正式环境就各种翻车——延迟飙高、显存溢出、吞吐量上不去,最后不得不降级回退到规则方案。
这篇文章想聊的,是我自己在正式环境部署模型时踩过的坑和总结出来的框架思路。从最简单的单模型服务,到多模型编排,再到如今大语言模型(LLM)的推理平台,这条演进路线背后有一套清晰的逻辑。不管你是刚接触部署的算法工程师,还是正在搭建推理平台的后端开发,或者是需要把模型落地到业务里的技术负责人,这些内容应该都能给你一些参考。
先说清楚一个基本判断:模型部署不是“把模型跑起来”这么简单,它是一套围绕延迟、吞吐、成本、稳定性四个维度做权衡的系统工程。单模型服务的时候,你只需要关心一个模型的输入输出;到了LLM推理平台,你要处理的是动态批处理、KV Cache管理、多模型路由、GPU资源调度、弹性扩缩容这一整套问题。复杂度不是线性增长的,是指数级的。
我自己的经历是从一个图像分类模型的服务化开始的。那时候用Flask包了个接口,单进程单GPU,QPS个位数,觉得部署也就那么回事。后来业务量上来,要同时跑检测、分类、分割三个模型,还要支持A/B测试和灰度发布,才发现之前那套东西根本不够用。再后来接触LLM,第一次看到vLLM的PagedAttention论文时,才意识到推理优化已经成了一门独立的学问。
所以这篇文章的结构是这样安排的:先拆解部署框架的整体设计思路,讲清楚不同阶段的选型逻辑;然后深入核心细节,把Triton、vLLM这些工具的关键机制和实操要点讲透;接着给出一套完整的实操流程,从环境准备到服务上线;最后整理常见问题和排查技巧。每一部分都会尽量给出“为什么这么做”的解释,而不只是“怎么做”。
提示:本文涉及的部署方案均基于公开技术文档和社区实践,具体参数需要根据你的硬件配置和业务场景做调整,不要直接照搬。
2. 部署框架的整体设计与选型逻辑
2.1 单模型服务阶段的典型架构与局限
最早期的模型服务,架构简单到可以用一张图说清楚:客户端发请求,服务端加载模型,推理,返回结果。这个阶段最常见的技术栈是Flask或FastAPI加PyTorch,模型直接加载在进程内存里,每次请求走一遍前向传播。
这种架构的问题在并发上来之后会集中爆发。首先是Python的GIL限制,多线程并不能真正并行执行推理;其次是每次请求都要走一遍完整的预处理、推理、后处理流程,没有批处理的概念,GPU利用率极低;再者是模型和代码耦合在一起,换模型要改代码重新部署,没有版本管理的概念。
我当时的做法是用Gunicorn起多个Worker进程,每个进程加载一份模型,试图用多进程绕过GIL。但这样做的问题是显存占用翻倍,一张卡上跑不了几个Worker,而且请求分发不均匀,有的Worker忙死有的闲死。后来换成Triton Inference Server,才算真正解决了这些问题。
Triton的核心价值在于它把模型服务和业务逻辑解耦了。你只需要把模型按照规定的目录结构放好,写一个配置文件描述输入输出,Triton就能自动处理批处理、并发执行、动态加载这些事情。它支持TensorFlow、PyTorch、ONNX、TensorRT等多种后端,甚至可以用Python写自定义的推理逻辑。
2.2 多模型编排与推理平台的演进
当业务需要同时服务多个模型时,单模型服务的架构就不够用了。你需要考虑模型路由、版本管理、资源隔离、监控告警这些问题。这时候就需要一个推理平台来统一管理。
推理平台的核心组件包括:模型仓库(存储模型文件和版本)、推理引擎(执行实际计算)、路由层(分发请求)、调度器(管理GPU资源)、监控系统(采集指标)。Triton在这个阶段依然可以用,但它更偏向于单机多模型的场景。如果要跨节点调度,就需要Kubernetes这样的容器编排系统来配合。
我自己的平台选型经历了几次变化。最开始用Triton加Docker Compose,管理几个模型还够用。后来模型数量增加到十几个,GPU节点也有好几台,就换成了Kubernetes加Triton的方案。Kubernetes负责Pod调度和弹性扩缩容,Triton负责单节点内的模型管理和推理执行。这个组合的优点是生态成熟、社区活跃,缺点是学习曲线陡峭,运维复杂度高。
到了LLM时代,推理平台的需求又变了。LLM的推理和传统模型有本质区别:它是自回归生成,每次输出一个token,需要反复调用模型;它的显存占用极大,KV Cache可能比模型本身还大;它的请求长度差异悬殊,短的几十个token,长的几万个token。这些特点决定了传统的批处理策略在LLM场景下效果很差。
vLLM就是为解决这些问题而生的。它的核心创新是PagedAttention,把KV Cache分成固定大小的块来管理,就像操作系统的虚拟内存分页一样。这样做的好处是显存利用率大幅提升,因为不再需要为每个请求预留最大长度的连续显存空间。配合连续批处理(Continuous Batching),vLLM可以在一个批次里同时处理不同阶段的请求,吞吐量比朴素实现高出数倍。
2.3 选型决策的关键维度与对比
面对Triton、vLLM、Ollama、SGLang这些工具,怎么选?我一般从以下几个维度来评估:
| 维度 | Triton | vLLM | Ollama | SGLang |
|---|---|---|---|---|
| 主要场景 | 传统模型多模型服务 | LLM高吞吐推理 | 本地LLM快速体验 | LLM结构化生成 |
| 批处理 | 动态批处理 | 连续批处理+PagedAttention | 有限支持 | RadixAttention |
| 多模型 | 原生支持 | 单模型为主 | 多模型切换 | 单模型为主 |
| 部署复杂度 | 中等 | 中等 | 低 | 中等 |
| 硬件要求 | GPU为主 | GPU为主 | CPU/GPU均可 | GPU为主 |
| 生态成熟度 | 高 | 高 | 高 | 中 |
选型的核心原则是:匹配你的主要矛盾。如果你要服务的是BERT、ResNet这类传统模型,Triton是最稳妥的选择;如果你要部署LLM并且追求高吞吐,vLLM是当前社区的主流方案;如果你只是想在本地快速跑一个模型做demo,Ollama的体验最好;如果你需要做结构化输出或者复杂的生成控制,SGLang的RadixAttention和DSL可能更合适。
还有一个容易被忽略的点是模型格式。传统模型常用ONNX、TensorRT、TorchScript这些格式,LLM则多用HuggingFace Transformers格式或GGUF格式。GGUF是llama.cpp推出的量化格式,适合在CPU或低显存GPU上运行,但吞吐量不如vLLM。ONNX Runtime在传统模型上性能很好,但对LLM的支持还在完善中。
3. 核心细节解析与实操要点
3.1 Triton Inference Server的模型仓库与配置
Triton的模型仓库结构是有严格规定的。每个模型一个目录,目录名就是模型名,里面放版本号命名的子目录,每个版本目录里放模型文件和配置文件。
model_repository/ ├── text_classification/ │ ├── 1/ │ │ ├── model.py │ │ └── config.pbtxt │ └── config.pbtxt └── image_encoder/ ├── 1/ │ └── model.onnx └── config.pbtxtconfig.pbtxt是Triton的核心配置文件,它定义了模型的输入输出、批处理策略、实例数量等关键参数。我见过很多人在这里踩坑,最常见的问题是输入输出的维度写错,导致Triton无法正确分配显存。
name: "text_classification" platform: "python" max_batch_size: 32 input [ { name: "INPUT_TEXT" data_type: TYPE_STRING dims: [ -1 ] } ] output [ { name: "OUTPUT_SCORE" data_type: TYPE_FP32 dims: [ 2 ] } ] instance_group [ { count: 2 kind: KIND_GPU gpus: [ 0 ] } ] dynamic_batching { preferred_batch_size: [ 8, 16, 32 ] max_queue_delay_microseconds: 100000 }max_batch_size决定了Triton能合并的最大请求数。这个值不是越大越好,因为批处理会增加单次推理的延迟。我的经验是,对于延迟敏感的场景,max_batch_size设在8到16之间;对于吞吐优先的场景,可以设到32甚至64。max_queue_delay_microseconds是等待批处理凑齐的最长时间,设得太大会增加延迟,设得太小会导致批处理效率低。100毫秒是一个比较平衡的值。
instance_group里的count决定了每个GPU上启动几个模型实例。多实例可以提高并发能力,但会占用更多显存。如果模型本身不大,显存充足,可以设2到4个实例;如果模型很大,显存紧张,就设1个。
注意:Triton的Python后端虽然灵活,但性能不如C++后端。如果模型可以导出为ONNX或TensorRT,优先用这些后端。Python后端适合做预处理后处理逻辑复杂的场景。
3.2 vLLM的PagedAttention与连续批处理机制
vLLM的性能优势主要来自两个机制:PagedAttention和连续批处理。理解这两个机制,对于调优和排查问题非常重要。
PagedAttention的核心思想是把KV Cache分成固定大小的块(Block),每个块存储固定数量token的Key和Value向量。不同请求的块可以存储在非连续的显存空间里,通过块表(Block Table)来映射逻辑位置和物理位置。这样做的好处是消除了显存碎片,因为不需要为每个请求预留最大长度的连续空间。
举个例子:假设一个请求的最大生成长度是2048个token,但实际只生成了100个token。朴素实现会为这个请求预留2048个token的KV Cache空间,浪费了95%的显存。PagedAttention只分配实际需要的块,100个token可能只占2到3个块,显存利用率大幅提升。
连续批处理解决的是另一个问题。传统批处理是静态的:一个批次里的所有请求必须同时开始、同时结束。如果批次里有一个请求生成了1000个token,另一个只生成了10个token,那短请求必须等长请求完成才能释放资源。连续批处理允许在一个批次里动态加入新请求、移除已完成请求,GPU始终处于满负荷状态。
vLLM的部署命令很简洁:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 256 \ --port 8000tensor-parallel-size是张量并行的GPU数量,需要根据模型大小和GPU数量来设置。7B模型用2张卡做张量并行比较合适,70B模型可能需要4到8张卡。max-model-len是最大序列长度,这个值直接影响KV Cache的显存占用,设得越大显存需求越高。gpu-memory-utilization是显存利用率上限,0.9表示使用90%的显存,留10%给系统和其他进程。max-num-seqs是最大并发序列数,这个值决定了连续批处理的批次大小上限。
我实测下来,Qwen2.5-7B-Instruct在2张A100 80G上,max-model-len设为8192,gpu-memory-utilization设为0.9,可以支持大约200个并发请求,吞吐量在3000 tokens/s左右。如果max-model-len降到4096,并发数可以翻倍,吞吐量也能提升30%左右。
3.3 模型量化与显存优化的实操细节
显存是LLM部署中最稀缺的资源。除了PagedAttention,量化是另一个重要的优化手段。常见的量化方案有GPTQ、AWQ、GGUF、FP8等。
GPTQ和AWQ是训练后量化(PTQ)方法,把模型权重从FP16压缩到INT4,显存占用降到原来的四分之一左右。GPTQ的量化粒度更细,精度损失更小;AWQ对激活值做保护,在某些任务上表现更好。我一般优先用AWQ,因为它在推理时的速度略快于GPTQ。
GGUF是llama.cpp的格式,支持CPU和GPU混合推理。它的优势是可以在显存不足时把部分层放到CPU上,缺点是推理速度慢。如果GPU显存足够,不建议用GGUF。
FP8是H100等新卡支持的原生格式,精度损失比INT4小,速度比FP16快。但FP8需要硬件支持,老卡用不了。
量化的实操命令以AWQ为例:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct-AWQ \ --quantization awq \ --dtype float16 \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85注意--quantization awq这个参数必须加上,否则vLLM会按FP16加载,量化就白做了。--dtype float16指定计算时的数据类型,量化模型的计算通常还是用FP16。
提示:量化会带来一定的精度损失,部署前一定要在你的业务数据上做评估。我见过量化后准确率掉5个百分点的案例,这种就不能接受。一般来说,INT4量化在7B以上的模型上精度损失在1到2个百分点以内,是可以接受的。
4. 完整实操流程:从环境准备到服务上线
4.1 环境准备与依赖安装
正式环境部署的第一步是环境准备。我习惯用Docker来管理环境,因为这样可以保证开发、测试、生产环境的一致性。
以vLLM为例,官方提供了预构建的镜像:
docker pull vllm/vllm-openai:latest如果需要特定版本,可以指定tag,比如vllm/vllm-openai:v0.6.3。我建议在生产环境使用固定版本,不要用latest,因为latest可能随时更新,引入不可预期的变化。
启动容器的命令:
docker run --runtime nvidia --gpus all \ -v ~/.cache/huggingface:/root/.cache/huggingface \ -p 8000:8000 \ --ipc=host \ vllm/vllm-openai:v0.6.3 \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 8192--runtime nvidia和--gpus all是让容器能访问GPU。--ipc=host是共享主机内存,vLLM在多进程通信时需要这个。-v把HuggingFace的缓存目录挂载到容器里,这样模型下载一次就够了,不用每次启动都重新下载。
如果不用Docker,直接pip安装也可以:
pip install vllm==0.6.3但要注意CUDA版本和PyTorch版本的兼容性。vLLM 0.6.3需要PyTorch 2.4以上,CUDA 12.1以上。版本不匹配会导致各种奇怪的错误,比如kernel编译失败、显存分配异常等。
4.2 模型下载与格式转换
模型下载有两种方式:直接从HuggingFace Hub拉取,或者手动下载后放到本地目录。直接拉取最简单:
from huggingface_hub import snapshot_download snapshot_download( repo_id="Qwen/Qwen2.5-7B-Instruct", local_dir="./models/Qwen2.5-7B-Instruct", local_dir_use_symlinks=False )如果网络不稳定,可以用hf_transfer加速:
pip install hf_transfer export HF_HUB_ENABLE_HF_TRANSFER=1格式转换主要针对量化模型。如果你有一个FP16的模型,想转成AWQ格式,可以用AutoAWQ:
from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path = "Qwen/Qwen2.5-7B-Instruct" quant_path = "Qwen2.5-7B-Instruct-AWQ" model = AutoAWQForCausalLM.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"} model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_path) tokenizer.save_pretrained(quant_path)q_group_size是量化分组大小,128是常用值。w_bit是量化位数,4表示INT4。version是量化算法版本,GEMM是通用矩阵乘法版本,兼容性最好。
4.3 服务启动与接口测试
服务启动后,vLLM会暴露一个兼容OpenAI API的接口。测试命令:
curl http://localhost:8000/v1/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen/Qwen2.5-7B-Instruct", "prompt": "请用一句话解释什么是模型部署", "max_tokens": 100, "temperature": 0.7 }'如果返回正常,说明服务已经跑起来了。接下来要做压力测试,看看在实际负载下的表现。我一般用locust或wrk来做压测。
wrk -t4 -c100 -d30s --latency -s post.lua http://localhost:8000/v1/completionspost.lua是自定义的压测脚本,构造请求体。-t4是4个线程,-c100是100个并发连接,-d30s是持续30秒。
压测时要关注几个指标:首token延迟(TTFT)、每token延迟(TPOT)、吞吐量(tokens/s)、GPU利用率、显存占用。TTFT反映的是预填充阶段的性能,TPOT反映的是解码阶段的性能。如果TTFT高,说明预填充计算量大,可以考虑增加张量并行度;如果TPOT高,说明解码效率低,可以检查KV Cache的块大小是否合理。
4.4 监控告警与日志采集
正式环境必须有监控。vLLM暴露了Prometheus格式的指标,可以直接接入Prometheus加Grafana的监控体系。
curl http://localhost:8000/metrics关键指标包括:vllm:num_requests_running(正在处理的请求数)、vllm:num_requests_waiting(等待队列长度)、vllm:gpu_cache_usage_perc(KV Cache使用率)、vllm:avg_prompt_throughput(预填充吞吐)、vllm:avg_generation_throughput(解码吞吐)。
我一般会设置几个告警规则:等待队列长度超过50持续1分钟,说明容量不足,需要扩容;KV Cache使用率超过95%持续5分钟,说明显存快满了,可能需要降低max-model-len或增加GPU;TTFT的P99超过2秒,说明预填充阶段有瓶颈。
日志采集用ELK或Loki都可以。vLLM的日志默认输出到stdout,Docker环境下可以用docker logs查看,Kubernetes环境下用kubectl logs。我习惯把日志收集到Loki,然后在Grafana里和指标一起看,排查问题时可以对照时间线。
5. 常见问题与排查技巧实录
5.1 显存溢出与OOM的排查路径
显存溢出是LLM部署中最常见的问题。报错信息通常是CUDA out of memory,但原因可能有很多种。
第一步是确认显存被什么占用了。用nvidia-smi看整体显存占用,用torch.cuda.memory_summary()看PyTorch的显存分配情况。如果模型加载后就OOM,说明模型本身太大,需要量化或增加GPU。如果运行一段时间后OOM,说明KV Cache增长超出了预期,需要降低max-model-len或max-num-seqs。
我遇到过一个案例:模型加载正常,但一有请求就OOM。排查后发现是gpu-memory-utilization设成了0.95,留给KV Cache的空间太小,第一个请求的预填充就撑爆了。改成0.85后问题解决。
还有一个隐蔽的坑是显存碎片。即使总显存够用,如果碎片太多,也可能分配失败。vLLM的PagedAttention本身就是为了解决碎片问题,但如果用了其他推理框架,可能需要手动调用torch.cuda.empty_cache()来整理碎片。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 加载模型时OOM | 模型太大 | 检查模型参数量和精度 | 量化或增加GPU |
| 首个请求OOM | KV Cache预留不足 | 检查gpu-memory-utilization | 降低该值到0.8-0.85 |
| 运行中OOM | 并发过高 | 检查num_requests_running | 降低max-num-seqs |
| 随机OOM | 显存碎片 | 检查显存分配日志 | 重启服务或换PagedAttention方案 |
5.2 推理延迟波动的归因分析
延迟波动是另一个让人头疼的问题。同样的请求,有时候100毫秒返回,有时候要2秒。这种波动通常来自几个方面。
首先是批处理的影响。连续批处理虽然提高了吞吐,但也会让单个请求的延迟变得不确定。如果一个请求被分到了一个很大的批次里,它的解码速度就会变慢。这是吞吐和延迟的固有权衡,没有完美的解决方案。如果业务对延迟极其敏感,可以考虑关闭连续批处理,或者设置更小的max-num-seqs。
其次是KV Cache的换入换出。当显存不足时,vLLM会把部分KV Cache换到CPU内存,需要时再换回来。这个换入换出的过程会带来毫秒级的延迟。如果观察到周期性的延迟尖峰,可以检查gpu_cache_usage_perc是否接近100%。
还有一个容易被忽略的因素是GPU频率。有些云厂商的GPU实例默认是节能模式,频率会根据负载动态调整。在高负载时频率上不去,导致推理变慢。可以用nvidia-smi -q -d CLOCK查看当前频率,用nvidia-smi -lgc锁定频率。
提示:延迟优化没有银弹,必须结合业务场景做权衡。我的经验是,先保证P99延迟可接受,再优化吞吐。如果反过来,很容易陷入“吞吐上去了但用户体验崩了”的困境。
5.3 模型加载失败的典型原因
模型加载失败的原因五花八门,我整理了几个最常见的:
配置文件缺失或格式错误。HuggingFace模型目录下必须有config.json、tokenizer.json、tokenizer_config.json这些文件。如果是从其他地方拷贝的模型,很容易漏掉某个文件。用snapshot_download下载可以避免这个问题。
权重文件损坏。下载过程中断可能导致.safetensors文件不完整。可以用safetensors库的校验功能检查:
from safetensors import safe_open with safe_open("model.safetensors", framework="pt") as f: for key in f.keys(): tensor = f.get_tensor(key) print(key, tensor.shape)版本不兼容。vLLM的版本和模型架构的版本要匹配。比如Qwen2.5需要vLLM 0.6.0以上,Qwen2需要vLLM 0.5.0以上。用太老的vLLM加载新模型会报KeyError或AttributeError。
自定义模型代码缺失。有些模型需要trust_remote_code=True,因为它们的架构定义在模型仓库的Python文件里,不在Transformers库里。如果忘了加这个参数,会报Model type not recognized。
5.4 并发性能调优的实操经验
并发性能调优是一个反复迭代的过程。我的做法是先用默认参数跑一遍基准测试,然后逐个调整参数,观察指标变化。
第一步是确定max-num-seqs。这个值决定了同时处理的最大请求数。设得太小,GPU利用率上不去;设得太大,延迟会飙升。我一般从128开始,逐步增加到256、512,观察吞吐量和延迟的变化。当吞吐量不再增长而延迟明显上升时,就是拐点。
第二步是调整max-model-len。这个值直接影响KV Cache的显存占用。如果业务请求的平均长度是1000个token,P99长度是4000个token,那max-model-len设4096就够了,没必要设8192。每降低一半,KV Cache的显存占用就降低一半,可以支持更多的并发。
第三步是考虑张量并行。张量并行可以把模型切分到多张GPU上,降低单卡的显存压力,但会增加通信开销。2卡张量并行的通信开销大约在10%到20%之间,4卡会更高。如果单卡显存够用,不建议用张量并行。
第四步是考虑流水线并行。流水线并行把模型的不同层放到不同GPU上,通信开销比张量并行小,但会有流水线气泡。对于LLM推理,流水线并行的效果通常不如张量并行。
我实测的一组数据:Qwen2.5-7B-Instruct,2张A100 80G,max-model-len=4096,gpu-memory-utilization=0.9,max-num-seqs从128增加到512时,吞吐量从2200 tokens/s提升到3800 tokens/s,但P99 TTFT从800毫秒增加到2.3秒。最终我选择了256作为平衡点,吞吐量3200 tokens/s,P99 TTFT 1.2秒。
6. 从单模型到LLM推理平台的架构演进思考
6.1 传统模型与LLM部署的架构差异
传统模型部署和LLM部署在架构上有本质区别。传统模型是“一次推理一次输出”,输入一张图或一段文本,输出一个结果,请求之间相互独立。LLM是“自回归生成”,输入一个prompt,输出一个token序列,每个token的生成都依赖于之前的token。
这个差异导致了两者在批处理策略上的根本不同。传统模型的批处理是静态的,一个批次里的请求同时开始同时结束。LLM的批处理必须是动态的,因为不同请求的生成长度差异很大。vLLM的连续批处理就是为此设计的。
另一个差异是显存管理。传统模型的显存占用是固定的,模型加载后显存就确定了。LLM的显存占用是动态的,KV Cache随着生成长度增长。PagedAttention通过分页管理解决了这个问题。
还有一个差异是服务接口。传统模型通常用gRPC或HTTP的简单接口,输入输出是固定维度的张量。LLM用流式接口,输出是逐个token返回的。这对服务端的并发处理能力提出了更高要求。
6.2 多模型路由与版本管理的实现
当平台上有多个模型时,路由和版本管理就变得重要了。路由层需要根据请求中的模型标识,把请求分发到对应的推理服务。版本管理需要支持灰度发布和回滚。
我自己的做法是用一个轻量级的网关来做路由。网关维护一个模型到服务地址的映射表,请求进来后查表转发。映射表可以动态更新,支持热加载。
class ModelRouter: def __init__(self): self.routes = {} def register(self, model_name, version, endpoint): key = f"{model_name}:{version}" self.routes[key] = endpoint def route(self, model_name, version=None): if version: key = f"{model_name}:{version}" return self.routes.get(key) else: candidates = [k for k in self.routes if k.startswith(f"{model_name}:")] if candidates: latest = sorted(candidates)[-1] return self.routes[latest] return None灰度发布可以通过权重路由来实现。比如新版本上线时,先给10%的流量,观察指标正常后再逐步增加。
6.3 弹性扩缩容与成本控制
弹性扩缩容是推理平台的重要能力。业务量有高峰有低谷,如果一直保持最大容量,成本会很高。Kubernetes的HPA可以根据CPU或自定义指标来扩缩容。
对于LLM推理,CPU利用率不是一个好的扩缩容指标,因为瓶颈在GPU。更好的指标是等待队列长度或KV Cache使用率。可以用Prometheus Adapter把这些指标暴露给HPA。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-deployment minReplicas: 1 maxReplicas: 4 metrics: - type: Pods pods: metric: name: vllm_num_requests_waiting target: type: AverageValue averageValue: "10"这个配置的意思是,当等待队列的平均长度超过10时,自动扩容,最多扩到4个副本。缩容的阈值可以设得低一些,比如等待队列为0持续5分钟就缩容。
成本控制还有一个手段是混合部署。把延迟不敏感的离线任务和在线服务混部在同一批GPU上,离线任务在在线服务低谷时占用资源,高峰时让出资源。这需要更复杂的调度策略,但可以显著提高GPU利用率。
6.4 未来演进方向与个人判断
从单模型服务到LLM推理平台,这条路我走了大概三年。回头看,最大的感受是:部署框架的演进方向是越来越“重”。早期的Flask加PyTorch,几百行代码就能跑起来;现在的推理平台,涉及容器编排、服务网格、监控告警、自动扩缩容,复杂度高了不止一个量级。
但这个“重”是有价值的。当业务规模上来之后,没有这套基础设施,根本撑不住。我见过太多团队在业务量增长后被迫重构部署架构,代价很大。如果一开始就按照平台的思路来设计,虽然前期投入大一些,但后期扩展会顺畅很多。
对于未来,我个人比较关注几个方向:一是推理和训练的融合,比如用同一个集群既做训练又做推理,提高资源利用率;二是异构硬件的支持,除了GPU,还有NPU、TPU等各种加速器,如何统一调度是个问题;三是自动调优,根据业务负载自动调整批处理大小、并发数这些参数,减少人工调参的成本。
这些方向目前都还在演进中,没有成熟的方案。但有一点是确定的:模型部署会越来越像一个独立的工程领域,需要专门的知识和技能。如果你正在这个领域,建议多动手实践,多踩坑,多总结。文档和论文能给你方向,但真正的经验只能从实操中来。
最后分享一个我自己的小习惯:每次部署新模型时,我都会记录一份“部署日志”,包括硬件配置、软件版本、关键参数、压测数据、遇到的问题和解决方案。这份日志在后续排查问题和优化性能时非常有用,也方便团队其他成员参考。踩过的坑不白踩,记录下来就是经验。