拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从单模型到LLM推理平台:vLLM、Triton与K8s部署实战指南

从单模型到LLM推理平台:vLLM、Triton与K8s部署实战指南

1. 从单模型到推理平台:部署这件事到底在解决什么问题

模型部署这个词,听起来像是运维的活儿,但真正做过的人都知道,它其实是算法、工程、基础设施三拨人坐在一起吵架的过程。算法同学说我的模型精度掉了0.5个点,工程同学说你的显存占用怎么又涨了,运维同学说这台机器的GPU利用率只有15%你让我怎么跟老板交代。我做了几年模型服务,从最早用Flask包一个PyTorch模型跑在单卡上,到后来管几十张卡的推理集群,踩过的坑基本能写一本小册子。

这篇文章想聊的,是把一个训练好的模型变成线上稳定服务,再进一步变成能支撑多模型、多租户、弹性伸缩的LLM推理平台,中间要跨过哪些坎。核心关键词就几个:模型部署、LLM、vLLM、Triton、K8s。适合谁看?如果你正在纠结“我训好的模型怎么给别人用”,或者“公司要上大模型但不知道选什么推理框架”,又或者“K8s集群跑起来了但GPU利用率上不去”,那这篇应该能给你一些直接能抄的作业。

先说清楚一个基本盘:模型部署不是把模型文件扔到服务器上跑起来就完事了。它要解决的是四个问题——延迟、吞吐、成本、稳定性。这四个词互相打架,你要低延迟就得牺牲批处理,要吞吐就得攒batch,要成本就得提高GPU利用率,要稳定性就得做冗余和隔离。所谓“部署框架全景”,本质上就是在这四个维度上找平衡点的工具箱。

单模型服务和LLM推理平台,看起来都是“把模型跑起来”,但复杂度差了一个数量级。单模型服务你只需要关心一个模型的输入输出、一个进程的显存占用、一台机器的资源。到了LLM推理平台,你要面对的是:多个模型版本共存、不同模型对显存和算力的需求差异巨大、请求的输入长度从几十token到几十万token不等、还要做多租户隔离和计费。这不是加几台机器就能解决的问题,架构上就得重新设计。

我见过太多团队在单模型阶段用Flask加gunicorn跑得好好的,一上LLM就崩了。原因很简单:传统Web服务的并发模型和LLM推理的并发模型完全不是一回事。一个HTTP请求进来,传统服务可能几十毫秒就返回了,LLM推理可能要几秒甚至几十秒,而且中间GPU一直在算。你用同步阻塞的方式去处理,线程池瞬间就被打满,后面的请求全部排队超时。所以从单模型到LLM平台,第一个要换的就是推理引擎和并发模型。

2. 推理引擎选型:vLLM、Triton、Ollama到底怎么选

2.1 先搞清楚每个工具的设计目标

选推理引擎这件事,最怕的就是“别人说好我就用”。我见过一个团队用Ollama做线上服务,QPS一上来直接跪了,然后问我为什么。我说Ollama的设计目标是让个人开发者在本地快速跑模型,它的默认配置压根没考虑高并发场景。这不是Ollama不好,是你用错了地方。

先把几个主流选项的设计目标理清楚:

工具设计目标适用场景不适用场景
Ollama本地快速体验和开发个人开发、原型验证、桌面端高并发线上服务
vLLM高吞吐LLM推理线上LLM服务、批量推理非LLM模型、超低延迟单请求
Triton通用模型服务框架多框架模型混合部署、企业级服务快速原型、个人项目
LM Studio桌面端模型体验本地对话、模型评测任何服务端场景
SGLang结构化LLM推理复杂prompt、多轮对话简单单轮推理

这个表不是绝对的,但能帮你快速排除明显不合适的选项。比如你要做的是线上API服务,Ollama和LM Studio直接划掉,它们就不是干这个的。

2.2 vLLM为什么成了LLM推理的默认答案

vLLM火起来核心就一个东西:PagedAttention。这个技术说白了就是把操作系统的虚拟内存分页思想搬到了KV Cache管理上。传统做法是给每个请求预分配一大块连续显存来存KV Cache,但你怎么知道这个请求会生成多长?预分配多了浪费,预分配少了不够用。PagedAttention把KV Cache切成固定大小的块,用多少分配多少,块之间不需要连续。这一下子把显存利用率从可能不到50%拉到了90%以上。

我实测过一个场景:同样一张A100 80G,用HuggingFace Transformers直接推理,并发到8左右就OOM了;换vLLM,同样模型并发能到30以上,吞吐量差了将近4倍。这不是vLLM有什么魔法,就是显存管理效率的差距。

vLLM的另一个杀手锏是Continuous Batching。传统batching是等一批请求凑齐了一起送进GPU,等最慢的那个生成完再处理下一批。Continuous Batching是每个iteration都重新组batch,已经生成完的请求退出,新来的请求补进来。GPU利用率直接从“脉冲式”变成了“持续满载”。

部署vLLM的典型命令长这样:

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --dtype auto \ --port 8000

几个参数值得展开说。--tensor-parallel-size 2表示用两张卡做张量并行,适合单卡放不下的模型。--gpu-memory-utilization 0.9是显存使用上限比例,留0.1给CUDA上下文和临时buffer,设太高容易OOM,设太低浪费显存。--max-model-len是最大序列长度,这个值直接影响KV Cache的块数量,设太大显存不够,设太小长请求会报错。

注意:--gpu-memory-utilization不是越高越好。我见过有人设0.98,结果跑了一段时间后CUDA OOM,因为碎片化和临时分配没留余量。0.85到0.92是比较稳的区间。

2.3 Triton的定位和vLLM完全不同

Triton不是推理引擎,它是模型服务框架。它自己不实现推理计算,而是把各种推理后端(PyTorch、TensorRT、ONNX Runtime、vLLM等)包装成统一的服务接口。你可以把它理解成一个“模型服务的操作系统”。

Triton的核心价值在于多模型多框架统一管理。一个Triton实例可以同时跑一个PyTorch的图像分类模型、一个TensorRT的检测模型、一个vLLM的LLM推理后端。每个模型有自己的版本管理、动态批处理配置、实例组配置。对于企业级场景,这种统一管理能力比单点性能更重要。

Triton的模型仓库结构是这样的:

model_repository/ ├── image_classifier/ │ ├── config.pbtxt │ └── 1/ │ └── model.pt ├── llm_model/ │ ├── config.pbtxt │ └── 1/ │ └── model.py └── detection/ ├── config.pbtxt └── 1/ └── model.plan

每个模型目录下的config.pbtxt定义了输入输出、批处理策略、实例数量等。这个配置文件是Triton的核心,写好了性能翻倍,写不好还不如直接跑。

2.4 选型决策树

我一般用这个决策树来选:

  1. 只做LLM推理,追求高吞吐 → vLLM或SGLang
  2. 需要同时服务多种类型模型(CV+NLP+LLM) → Triton + vLLM后端
  3. 本地开发快速验证 → Ollama或LM Studio
  4. 需要极致低延迟单请求 → TensorRT或ONNX Runtime
  5. 需要结构化输出和复杂prompt编排 → SGLang

这个决策树不是死的,但能帮你快速缩小范围。最怕的就是拿一个工具硬套所有场景,最后哪个场景都不满意。

3. 从单机到K8s:部署架构的演进路径

3.1 单机部署的天花板在哪里

单机部署模型,最直接的方式就是起一个进程,加载模型,暴露HTTP接口。简单场景下这完全够用,但很快就会碰到天花板。

第一个天花板是显存。一张A100 80G,跑一个7B模型用FP16大概占14G,加上KV Cache和中间激活,实际能用到20G左右。剩下60G看起来很多,但你要跑多个模型或者更大的模型就不够了。模型并行可以解决单模型放不下的问题,但多模型共存还是得靠多机。

第二个天花板是可用性。单机挂了服务就没了,没有冗余。你可能说那我起两个进程,但两个进程抢同一张卡,一个OOM另一个也受影响。真正的冗余需要多机。

第三个天花板是弹性。流量高峰来了想加机器,单机架构下你得手动部署新实例、配置负载均衡、同步模型文件。流量下去了想缩容,又得手动操作。K8s把这些变成了声明式的配置。

3.2 K8s部署GPU服务的核心配置

在K8s上跑GPU服务,和跑普通Web服务有几个关键区别。首先是GPU资源调度,你需要安装NVIDIA Device Plugin,让K8s能识别和分配GPU。然后Pod的resources里要声明nvidia.com/gpu: 1。

一个典型的vLLM Deployment配置:

apiVersion: apps/v1 kind: Deployment metadata: name: vllm-qwen spec: replicas: 2 selector: matchLabels: app: vllm-qwen template: metadata: labels: app: vllm-qwen spec: containers: - name: vllm image: vllm/vllm-openai:v0.27.1 args: - "--model" - "Qwen/Qwen2.5-7B-Instruct" - "--tensor-parallel-size" - "1" - "--gpu-memory-utilization" - "0.9" resources: limits: nvidia.com/gpu: 1 ports: - containerPort: 8000 readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 120 periodSeconds: 10

这里有几个坑要注意。initialDelaySeconds: 120是因为模型加载需要时间,7B模型大概要1到2分钟,70B模型可能要5分钟以上。如果readinessProbe设太短,Pod还没加载完就被判定为不健康,会被反复重启。

注意:vLLM的镜像版本要和CUDA版本匹配。vllm/vllm-openai:v0.27.1默认用的是CUDA 12.1,如果你的节点驱动版本较老,可能需要换镜像或者升级驱动。我遇到过节点驱动是CUDA 11.8,跑vLLM 0.27.1直接报错的情况。

3.3 多模型共存和资源隔离

LLM推理平台和单模型服务的核心区别之一,就是多模型共存。不同团队要部署不同的模型,有的用7B,有的用72B,有的要长上下文,有的要低延迟。怎么在一套K8s集群里管好这些?

方案一是每个模型一个Deployment,通过K8s的命名空间做隔离。优点是简单直接,每个模型独立伸缩。缺点是资源利用率低,每个Deployment都要预留GPU,即使没流量也占着卡。

方案二是共享推理后端,用Triton或者vLLM的多模型能力,在一个进程里加载多个模型。优点是资源利用率高,缺点是隔离性差,一个模型OOM可能影响其他模型。

方案三是混合方案,核心模型独立部署保证稳定性,长尾模型共享后端提高利用率。这也是我目前比较推荐的方案。

资源隔离方面,K8s的requests和limits对GPU来说比较特殊。GPU不像CPU可以超卖,requests和limits必须相等,否则调度器会报错。这意味着你没法像CPU那样设置“请求0.5张卡,限制1张卡”。要做GPU共享,得用MIG(Multi-Instance GPU)或者时间片轮转方案,但MIG只支持A100及以上,时间片轮转对LLM推理的延迟影响很大。

3.4 自动伸缩的坑

K8s的HPA(Horizontal Pod Autoscaler)对LLM服务来说,用起来没那么简单。普通Web服务可以用CPU利用率或者QPS来触发伸缩,但LLM服务的瓶颈在GPU,而GPU利用率这个指标本身就很微妙。

GPU利用率高不一定代表需要扩容。比如一个请求正在生成很长的输出,GPU利用率一直是100%,但QPS可能只有1。这时候扩容没用,因为瓶颈在单个请求的生成时间,不在并发处理能力。

我一般用排队请求数或者P99延迟作为伸缩指标。排队请求数超过阈值说明处理不过来,需要扩容。P99延迟超过SLA说明用户体验下降,也需要扩容。这两个指标比GPU利用率更能反映真实需求。

但LLM服务的冷启动时间是个大问题。一个新Pod从创建到能处理请求,7B模型要1到2分钟,70B模型要5分钟以上。这意味着扩容决策要提前做,不能等排队了再扩。我一般会设置一个预热池,保持一定数量的空闲Pod,随时能接管流量。

4. 实操:从零搭一个LLM推理服务

4.1 环境准备和依赖检查

假设你有一台带GPU的Linux机器,想从零搭一个vLLM服务。第一步不是装vLLM,是检查环境。

# 检查GPU驱动 nvidia-smi # 检查CUDA版本 nvcc --version # 检查Python版本 python --version

nvidia-smi的输出里,右上角的CUDA Version是驱动支持的最高CUDA版本,不是你当前安装的CUDA版本。这个值要大于等于你要用的CUDA版本。比如vLLM 0.27.1需要CUDA 12.1,你的驱动显示CUDA Version 12.2,那就没问题。

Python版本建议3.10到3.12。3.13有些依赖还没适配,3.9有些新特性用不了。

4.2 安装vLLM和模型下载

安装vLLM最省事的方式是用官方Docker镜像,但如果你想在宿主机上直接跑,用pip:

pip install vllm==0.27.1

这个命令会自动装PyTorch、CUDA runtime、transformers等依赖。但要注意,pip装的PyTorch可能和你系统的CUDA版本不匹配。如果报错,先去PyTorch官网找对应CUDA版本的安装命令。

模型下载有两种方式。一种是让vLLM自动从HuggingFace下载,第一次启动时会下载到~/.cache/huggingface。另一种是提前用huggingface-cli下载好,指定本地路径。

# 提前下载模型 huggingface-cli download Qwen/Qwen2.5-7B-Instruct --local-dir ./qwen2.5-7b

提前下载的好处是启动快,而且可以控制模型文件的位置。如果机器不能直连HuggingFace,可以用镜像站或者手动下载。

4.3 启动服务和参数调优

启动vLLM服务:

python -m vllm.entrypoints.openai.api_server \ --model ./qwen2.5-7b \ --served-model-name qwen2.5-7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 256 \ --dtype auto \ --port 8000

--max-num-seqs 256是最大并发序列数,这个值直接影响显存占用和吞吐。设太大KV Cache不够用,设太小并发上不去。7B模型在A100 80G上,256是个比较安全的起点。

--dtype auto让vLLM自动选择精度。如果模型是FP16的,就用FP16;如果是BF16的,就用BF16。BF16在A100及以上卡上性能更好,因为Tensor Core对BF16的支持更完整。

启动后测试:

curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-7b", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 100 }'

如果返回正常,说明服务跑起来了。如果报错,看日志里的CUDA error或者OOM,一般是显存不够或者版本不匹配。

4.4 压测和性能基线

服务跑起来只是第一步,你得知道它能扛多少并发。用vllm自带的benchmark工具:

python -m vllm.benchmarks.benchmark_serving \ --backend openai \ --base-url http://localhost:8000 \ --model qwen2.5-7b \ --dataset-name sharegpt \ --num-prompts 1000 \ --request-rate 10

这个命令会模拟每秒10个请求的负载,跑1000个prompt,输出吞吐量、延迟分布等指标。重点看两个数:TTFT(Time To First Token)和TPOT(Time Per Output Token)。TTFT反映首字延迟,TPOT反映生成速度。线上服务一般要求TTFT小于1秒,TPOT小于50毫秒。

我实测Qwen2.5-7B在A100上,FP16精度,并发32的情况下,TTFT大概200毫秒,TPOT大概20毫秒,吞吐量大概1500 tokens/s。这个数据可以作为基线,如果明显低于这个值,说明配置有问题。

5. 生产环境常见故障和排查思路

5.1 OOM:最常见也最头疼的问题

OOM是LLM推理服务最常见的故障,没有之一。表现是服务突然挂掉,日志里出现CUDA out of memory。原因可能有几种:

第一种是显存预估不足。模型权重占一部分,KV Cache占一部分,中间激活占一部分,CUDA上下文占一部分。很多人只算了模型权重,忘了KV Cache。KV Cache的大小和max-model-len、max-num-seqs、num_layers、hidden_size都有关系。粗略估算公式是:

KV Cache = 2 * num_layers * hidden_size * max_model_len * max_num_seqs * dtype_size

以Qwen2.5-7B为例,28层,hidden_size 3584,max_model_len 8192,max_num_seqs 256,FP16(2字节):

2 * 28 * 3584 * 8192 * 256 * 2 = 约 840GB

这显然不对,因为KV Cache是按需分配的,不是一次性全分配。但这个公式能帮你理解各个参数的影响。实际使用中,vLLM会根据gpu_memory_utilization动态管理KV Cache块。

第二种是内存碎片。长时间运行后,显存碎片化导致没有连续的大块显存可用。解决办法是定期重启服务,或者用vLLM的--enable-prefix-caching减少重复分配。

第三种是请求长度超限。一个请求的输入长度超过了max-model-len,vLLM会直接报错。如果没做好错误处理,可能导致服务崩溃。解决办法是在网关层做输入长度检查,超限的直接拒绝。

5.2 服务无响应:不一定是挂了

有时候服务没挂,但请求一直不返回。这种情况比OOM更难排查,因为日志里可能什么错误都没有。

常见原因是请求排队。vLLM的--max-num-seqs限制了同时处理的请求数,超出的请求会在队列里等待。如果队列太长,请求就会超时。解决办法是监控队列长度,超过阈值就扩容或者限流。

另一个原因是长请求阻塞。一个请求生成了很长的输出,占着GPU不放,后面的请求只能等。vLLM的Continuous Batching可以缓解这个问题,但如果长请求太多,还是会影响整体吞吐。解决办法是设置--max-tokens上限,或者用优先级队列。

还有一种情况是网络问题。K8s的Service或者Ingress配置有问题,请求根本没到Pod。排查方法是看Pod的日志有没有收到请求,如果没有,就是网络层的问题。

5.3 K8s相关的故障

在K8s上跑GPU服务,除了模型本身的问题,还有K8s层面的问题。

Pod一直Pending:最常见的原因是GPU资源不足。kubectl describe pod看Events,如果显示Insufficient nvidia.com/gpu,就是集群里没有空闲GPU了。解决办法是加节点或者减少副本数。

Pod反复重启:看kubectl logs --previous看上次崩溃的日志。如果是OOM,调大显存或者调小max-num-seqs。如果是readinessProbe失败,调大initialDelaySeconds。

服务发现失败:K8s的Service通过Label Selector找到Pod,如果Label不匹配,Service就没有Endpoints。kubectl get endpoints检查一下,如果是空的,就是Label问题。

节点GPU驱动异常:有时候节点上的GPU驱动会挂掉,nvidia-smi报错。这时候需要重启节点或者重新加载驱动。K8s的Device Plugin会检测到GPU不可用,把节点标记为不健康。

5.4 常见问题速查表

现象可能原因排查方法解决办法
CUDA OOM显存不足看日志确认OOM降低gpu-memory-utilization或max-num-seqs
请求超时队列积压看队列长度指标扩容或限流
Pod PendingGPU不足kubectl describe pod加节点或减副本
Pod重启探针失败kubectl logs --previous调大initialDelaySeconds
服务无响应网络问题看Pod是否收到请求检查Service和Ingress
吞吐量低批处理配置不当看GPU利用率调大max-num-seqs
首字延迟高模型加载慢看TTFT指标用更快的存储或预热

6. 几个容易踩的坑和实操心得

6.1 模型格式转换的坑

HuggingFace上的模型格式五花八门,有safetensors、bin、gguf等。vLLM主要支持safetensors和bin,gguf需要额外转换。如果你下载的是gguf格式,得先转成safetensors。

转换工具用llama.cpp的convert_hf_to_gguf.py反向转,或者用transformers重新保存:

from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("./model-gguf") tokenizer = AutoTokenizer.from_pretrained("./model-gguf") model.save_pretrained("./model-safetensors", safe_serialization=True) tokenizer.save_pretrained("./model-safetensors")

这个转换过程可能丢精度,特别是量化模型。如果原模型是4bit量化的,转成FP16再转回去,精度可能进一步下降。所以尽量直接用原始格式,别来回转。

6.2 多卡并行的通信开销

用--tensor-parallel-size做多卡并行时,卡之间的通信开销不可忽略。NVLink的带宽比PCIe高一个数量级,所以同一台机器内的多卡并行效果最好。跨机器的张量并行通信开销很大,一般用流水线并行代替。

我实测过2卡A100用NVLink做张量并行,7B模型的吞吐量大概是单卡的1.8倍,不是2倍,因为通信有开销。如果是PCIe连接,可能只有1.5倍。所以如果单卡能放下模型,优先用单卡。

6.3 长上下文的显存陷阱

现在很多模型支持128K甚至更长的上下文,但长上下文的显存消耗是指数级增长的。KV Cache的大小和序列长度成正比,128K上下文的KV Cache可能是8K的16倍。一张80G的卡,跑8K上下文可能能并发256个请求,跑128K可能只能并发16个。

解决办法是用滑动窗口注意力或者KV Cache量化。滑动窗口只保留最近N个token的KV,适合长文档摘要这类场景。KV Cache量化把FP16的KV压成INT8,显存减半,但精度可能下降。

6.4 监控指标的选择

LLM服务的监控和普通Web服务不一样。普通Web服务看QPS、延迟、错误率就够了,LLM服务还要看:

  • TTFT:首字延迟,影响用户体验
  • TPOT:每token生成时间,影响总响应时间
  • 队列长度:反映服务压力
  • GPU利用率:反映资源使用效率
  • KV Cache使用率:反映显存压力
  • 请求长度分布:反映负载特征

这些指标里,我最关注的是队列长度和KV Cache使用率。队列长度持续大于0说明处理不过来,KV Cache使用率接近100%说明显存快满了。这两个指标能提前预警,比等OOM了再处理要好。

6.5 版本升级的注意事项

vLLM的版本迭代很快,几乎每个月都有新版本。升级版本时要注意:

第一,模型兼容性。新版本可能不支持旧版本的模型格式,或者对某些模型的支持有变化。升级前先在测试环境验证。

第二,API兼容性。vLLM的OpenAI兼容API大部分是稳定的,但有些参数可能变了。比如--max-num-seqs的默认值在不同版本可能不同。

第三,CUDA版本。新版本可能要求更高的CUDA版本,如果你的驱动不支持,就得先升级驱动。

我一般会保留一个稳定版本,新版本先在测试环境跑一周,确认没问题再上生产。升级时用滚动更新,先更新一个副本,观察一段时间再更新其他副本。

6.6 成本优化的几个思路

LLM推理的成本主要在GPU上。一张A100按小时计费,一个月下来不少钱。优化成本有几个思路:

提高GPU利用率。用Continuous Batching和PagedAttention把GPU利用率从30%提到70%,相当于成本减半。

混合精度。FP16比FP32快一倍,BF16比FP16在A100上又快一些。如果精度允许,用INT8量化还能再快一倍。

模型蒸馏。用大模型蒸馏一个小模型,7B的模型能达到70B模型80%的效果,但推理成本只有十分之一。

请求调度。把长请求和短请求分开处理,短请求用低延迟配置,长请求用高吞吐配置。这样能避免长请求阻塞短请求。

自动伸缩。流量低谷时缩容,高峰时扩容。但要注意冷启动时间,别缩得太狠。

7. 从单模型到平台:架构演进的实际路径

7.1 第一阶段:单模型单机

这是最简单的阶段,一个模型跑在一台机器上,直接暴露HTTP接口。适合内部工具、Demo、小流量场景。技术栈就是vLLM或者Triton加一个反向代理。

这个阶段的关键是把服务跑稳。配置好显存参数,做好健康检查,加一个简单的监控。别想太多,先跑起来。

7.2 第二阶段:多模型多机

流量上来了,一个模型不够用了,或者要部署多个模型。这时候需要引入K8s做编排,每个模型一个Deployment,用Service做负载均衡。

这个阶段的关键是资源管理。GPU是稀缺资源,怎么分配给不同的模型是个问题。我一般用命名空间做隔离,每个团队一个命名空间,配额管理GPU资源。

7.3 第三阶段:推理平台

到了这个阶段,你需要的不只是跑模型,还要做模型管理、版本管理、灰度发布、A/B测试、计费、审计。这时候Triton或者自研的推理平台就派上用场了。

推理平台的核心能力包括:

  • 模型仓库:统一管理模型文件、版本、配置
  • 服务编排:自动部署、伸缩、故障恢复
  • 流量管理:灰度、A/B测试、限流、熔断
  • 可观测性:指标、日志、链路追踪
  • 多租户:隔离、配额、计费

这个阶段没有标准答案,每个公司的实现都不一样。但核心思路是一样的:把模型部署这件事从“手工操作”变成“声明式管理”。

7.4 我踩过的一个典型坑

说一个我实际踩过的坑。有一次我们上线一个新模型,配置里--max-model-len设了32768,因为模型支持32K上下文。结果上线后频繁OOM。排查发现,虽然大部分请求只有几百token,但KV Cache是按最大长度预分配的,32K的KV Cache把显存吃光了。

解决办法是设置--max-model-len为实际需要的长度,比如8192,然后在网关层限制输入长度。如果确实需要长上下文,用单独的实例跑,别和短请求混在一起。

这个坑的教训是:模型支持的能力不等于你都要用。配置参数要根据实际场景来,别直接抄模型卡上的最大值。

7.5 另一个坑:K8s的GPU调度

K8s默认的GPU调度是整卡调度,一个Pod要么占一张卡,要么不占。这意味着你没法把一张卡分给两个Pod。对于小模型来说,这很浪费。

解决办法有几种。一是用MIG,把A100切成多个小实例,每个实例独立调度。但MIG只支持A100及以上,而且配置比较麻烦。二是用时间片轮转,多个Pod共享一张卡,但延迟会受影响。三是用vGPU方案,比如阿里云的cGPU或者腾讯云的qGPU,但这些都是厂商绑定的。

我目前的建议是:如果模型小,就合并到一个Pod里用vLLM的多模型能力跑。如果模型大,就整卡调度,别折腾共享。

8. 一些零散但有用的经验

8.1 模型加载加速

模型加载慢是个痛点,特别是大模型。几个加速方法:

用本地SSD。模型文件放在NVMe SSD上,加载速度比网络存储快很多。70B模型从网络存储加载可能要10分钟,从本地SSD加载只要2分钟。

用内存文件系统。把模型文件放到/dev/shm,加载速度更快,但重启后丢失。

预热。服务启动后先跑几个请求,把KV Cache和CUDA kernel都预热好,再接入流量。

8.2 请求超时设置

LLM请求的超时设置很讲究。设太短,长请求会被截断;设太长,客户端会一直等。我一般设置分级超时:

  • 连接超时:5秒
  • 首字超时:30秒
  • 总超时:300秒

首字超时30秒是因为模型加载后第一个请求可能比较慢,之后就好了。总超时300秒是给长输出留余量,一般请求不会超过这个时间。

8.3 日志和追踪

LLM服务的日志要记录请求ID、输入长度、输出长度、TTFT、TPOT、总耗时。这些数据能帮你分析性能瓶颈和用户行为。

追踪方面,OpenTelemetry是个好选择。它能把一个请求的完整链路串起来,从网关到推理引擎到GPU,每个环节的耗时都看得到。

8.4 安全考虑

模型服务的安全容易被忽视。几个基本点:

输入过滤:防止prompt注入和恶意输入。特别是对外服务,一定要做输入长度和内容检查。

输出过滤:防止模型输出敏感信息。可以用关键词过滤或者另一个模型做审核。

访问控制:API Key或者OAuth,别裸奔。

限流:防止单个用户打满服务。按用户或者按IP限流。

8.5 最后分享一个小技巧

如果你在K8s上跑vLLM,遇到Pod启动慢的问题,可以试试Init Container预加载模型。用一个Init Container把模型从网络存储拷贝到本地SSD,主容器直接从本地加载。这样Pod启动时间能缩短一半以上。

initContainers: - name: model-loader image: busybox command: ["cp", "-r", "/remote-model", "/local-model"] volumeMounts: - name: remote-storage mountPath: /remote-model - name: local-ssd mountPath: /local-model

这个技巧对70B以上的大模型特别有用,因为模型文件几十上百G,从网络存储加载真的很慢。

模型部署这件事,说到底是在资源、性能、成本、稳定性之间找平衡。没有银弹,只有适合当前场景的方案。从单模型到LLM推理平台,每一步演进都是被实际问题推着走的。先把单模型跑稳,再考虑多模型,最后才是平台化。别一上来就搞大架构,容易把自己绕进去。

返回列表