为什么你的LLM服务慢到哭?vLLM三招让GPU利用率从24%飙到96%

0 阅读12分钟

如果你的LLM服务线上延迟高、吞吐低、GPU还闲得发慌,大概率不是模型太烂,是你的推理框架没选对。

今天聊一个硬核话题:vLLM凭什么成为LLM生产部署的事实标准。不是背概念,是从底层问题出发,拆解它到底解决了什么、怎么解决、线上怎么用。

我会按这个顺序来:

  1. 三个要命的瓶颈——vLLM之前的世界有多惨
  2. vLLM三板斧——PagedAttention、Continuous Batching、Tensor Parallelism
  3. 四层架构拆解——从API到GPU发生了什么
  4. 线上部署实操——Docker单机到K8s集群
  5. Ollama vs vLLM怎么选——别选错
  6. 六道高频面试题——能答上来才算真懂

这玩意儿不是玄学,是工程。

一、三个要命的瓶颈:vLLM之前的世界有多惨

先说结论:传统LLM推理框架的GPU显存利用率只有24%左右。你没看错,花了十几万买的A100,76%的显存在空转。

瓶颈1:KV Cache显存浪费

大模型推理时,每个请求都需要一块连续的显存空间存KV Cache(Key-Value缓存)。问题在于——传统框架按最大可能长度预分配显存。

打个比方:你去餐厅吃饭,服务员直接给你留一个100人的大包间,不管你实际就3个人。结果餐厅明明能接待50桌,实际只能接待5桌。

具体数字更扎心:一个13B模型在A100上,传统框架预分配KV Cache占掉约80GB显存,但实际平均利用率只有24%。剩下的76%全在"占着茅坑不拉屎"。

瓶颈2:请求排队等批

传统Static Batching的逻辑是:凑够一批请求→一起推理→等最长的那个生成完→才能处理下一批。

这就跟坐公交车一样:车上有50个座位,上来3个人,司机说"等坐满再开"。然后你就等着,等到50个人凑齐了才发车。最惨的是如果你那批里有个生成2000 token的长请求,整个batch都得等它写完。

瓶颈3:单GPU装不下大模型

一个70B模型FP16需要约140GB显存,单张A100只有80GB。传统框架基本就傻了——装不下就是装不下,你只能量化压缩或者换小模型。

这三个问题叠加的结果:GPU算力本身不是瓶颈,显存管理和调度才是。GPU在空转等内存,而不是在算东西。

二、vLLM三板斧:每一招都直击要害

第一斧:PagedAttention--虚拟内存分页管理

核心思想:把KV Cache的连续显存分配改成按需分页分配。

操作系统怎么管内存的?虚拟内存+分页。你申请1GB内存,OS不会真给你分配1GB物理内存,而是给你一堆虚拟页,用到哪页分配哪页。

vLLM把同样的思路搬到了GPU上:

  • 把KV Cache切成固定大小的Block(通常每Block存16个token的KV)
  • 物理显存按需分配,用到才给
  • 逻辑Block和物理Block通过Block Table映射
  • 一个请求的KV Cache不需要连续存储

效果:显存利用率从24%飙到96%。同一张GPU能同时服务的请求数翻了4倍。

还有个意外收获:Block级别的共享让Prefix Caching变得很自然。多个请求共享同一个system prompt,那段KV Cache只需要存一份,所有请求共享。这在实际场景中省的不是一点半点。

第二斧:Continuous Batching--请求动态进出

核心思想:不等一批凑齐,不等一批跑完。请求随时进随时出。

Static Batching的问题在于"木桶效应"--最长的请求决定整批的时间。如果一批50个请求,49个生成了20 token就完事,最后1个要生成2000 token,那49个请求的GPU资源在等待期间全浪费了。

Continuous Batching的做法:

  • 每次iteration只处理当前有活干的请求
  • 某个请求生成完了立刻退出batch
  • 新请求随时加入batch
  • GPU从不闲着

效果:吞吐量提升2-4倍。延迟反而更低了,因为短请求不用等长请求。

第三斧:Tensor Parallelism--多GPU切分

核心思想:一张卡装不下大模型,那就把模型切成块分到多张卡上。

不是按Layer切(那是Pipeline Parallelism),而是按Tensor切。具体来说,矩阵乘法 A×B 可以按列切分B:

  • GPU 1 算 A×B₁
  • GPU 2 算 A×B₂
  • 最后All-Reduce合并结果

这样每张卡只需要存模型的一部分,70B模型用2-4张A100就能跑了。虽然通信开销增加了一些延迟,但换来的是能跑大模型。

三者关系:PagedAttention省显存->更多请求能同时塞进去->Continuous Batching让这些请求高效调度->Tensor Parallelism让大模型也能跑。三板斧组合起来,GPU才真正忙起来。

三、四层架构拆解:从API到GPU发生了什么

vLLM的架构很清晰,四层各司其职:

┌─────────────────────────────────────┐
│  API Server (FastAPI)               │  ← OpenAI兼容接口
│  /v1/chat/completions, /v1/engines  │
├─────────────────────────────────────┤
│  Engine                             │
│  ├ Scheduler (调度器)                │  ← Continuous Batching调度
│  └ KV Cache Manager (分页管理)       │  ← PagedAttention
├─────────────────────────────────────┤
│  Worker                             │
│  ├ Model Runner (模型执行)           │
│  └ PagedAttention CUDA Kernel       │  ← 底层算子
├─────────────────────────────────────┤
│  Hardware                           │
│  GPU(s) / NVLink / 多卡              │  ← Tensor Parallelism
└─────────────────────────────────────┘

API Server层

FastAPI写的,对外暴露OpenAI兼容的REST接口。你之前用OpenAI SDK写的代码,把base_url从https://api.openai.com改成http://your-vllm-server:8000就能跑,业务代码零改动。

这跟Ollama的/v1/chat/completions一样的设计思路--降低迁移成本。

Engine层

这是vLLM的大脑,两个核心组件:

Scheduler:决定每次iteration跑哪些请求。它维护一个waiting队列和running队列,根据显存余量动态调度。新请求进waiting,有显存了拉到running,生成完了踢出去。这就是Continuous Batching的具体实现。

KV Cache Manager:管理Block的分配和回收。每个请求有一个Block Table记录它的逻辑Block映射到哪些物理Block。请求结束了,Block立刻回收给下一个请求用。这就是PagedAttention的实现。

Worker层

每个Worker对应一张GPU。Model Runner负责跑模型的前向传播,PagedAttention CUDA Kernel是专门写的底层算子,直接操作Block的KV Cache,不走PyTorch的注意力机制。

这层是性能的关键--自己写CUDA Kernel而不是用PyTorch原生的注意力,才能精确控制显存访问模式。

Hardware层

多GPU通过NVLink互联,Tensor Parallelism在这层生效。单卡就是普通的GPU推理。

四、线上部署实操:从Docker单机到K8s集群

Docker单机部署

线上最快的方式,几个关键参数:

docker run --gpus all \
  --shm-size 8g \
  -v /data/models:/models \
  -p 8000:8000 \
  vllm/vllm-openai:latest \
  --model /models/qwen2-7b \
  --tensor-parallel-size 2 \
  --max-model-len 4096

三个容易踩的坑:

  1. --gpus all:透传所有GPU。如果只想要部分GPU,用--gpus '"device=0,1"'指定。
  2. --shm-size 8g:默认64MB太小,PyTorch多进程共享内存会爆。给8GB基本够。
  3. 模型挂载用PVC:别把模型打包进镜像,一个13B模型镜像就几十GB,构建一次要命。挂载到容器里秒级启动。

K8s集群部署

生产环境真正用的,核心是三个资源对象:

nvidia-device-plugin:让K8s能调度GPU。没这个,K8s不知道GPU是什么资源。装了之后,Pod配置里写nvidia.com/gpu: 2就能申请2张GPU。

PVC共享模型:多个Pod共享同一个模型存储,避免每个节点都下载一份。用ReadWriteMany的存储类(比如NFS或CephFS)。

HPA自动扩缩容:这个最有意思。HPA不能只看CPU/内存利用率(GPU推理Pod的CPU使用率很低),要看请求队列长度。vLLM暴露了/metrics端点,里面有vllm:num_requests_waiting指标。基于这个指标扩缩容:队列长了加Pod,空闲了缩Pod。

另外要配启动探针,vLLM冷启动加载模型需要30秒到几分钟,默认探针太激进会杀Pod。initialDelaySeconds: 120给足加载时间。

Ollama vs vLLM:别选错

这两个不是竞争关系,是开发环境 vs 生产环境的区别:

维度OllamavLLM
定位本地开发调试生产环境高吞吐
显存管理简单预分配PagedAttention分页
批处理Static BatchingContinuous Batching
多GPU不支持Tensor Parallelism
启动速度快(秒级)慢(加载模型30s+)
API兼容OpenAI兼容OpenAI兼容
业务代码改动

关键点:两者API完全兼容。开发用Ollama,上线切vLLM,只需要改一个base_url。这才是好的架构设计--接口统一,实现可换。

我本地用Ollama跑Qwen3:8B和GLM4:9B做开发调试,线上用vLLM跑同款模型服务真实流量。业务代码用的是LangChain4j的ChatModel接口,底层切换完全透明。

五、线上真实场景:公司里到底怎么用的

场景1:高并发聊天服务

某互联网公司用Qwen2-72B做客服聊天,日均100万次对话。用vLLM + 4张A100 80GB:

  • Tensor Parallelism=4,72B模型FP16约144GB,4卡分摊每卡36GB
  • PagedAttention让同时服务200+并发会话(传统框架只能服务50个)
  • Continuous Batching让P99延迟从800ms降到200ms
  • 峰值QPS约500,GPU利用率保持在85%以上

场景2:批量推理任务

某数据公司每晚跑10万条数据抽取任务。之前用Transformers库串行跑,8小时。换vLLM后:

  • Continuous Batching自动批处理,不用手动凑batch
  • 2小时跑完,快了4倍
  • KV Cache复用:所有任务用同一个system prompt,Prefix Caching命中率高

场景3:多模型A/B测试

K8s上起多个vLLM Pod,每个Pod跑不同模型。流量通过Service按比例分发:

  • Pod 1: Qwen2-7B(80%流量,成本低)
  • Pod 2: Qwen2-72B(20%流量,高质量)
  • ModelRouter根据任务复杂度路由
  • 这跟我之前手敲的ModelSwitcher设计思路一模一样,只是从本地Ollama换成了vLLM集群

场景4:成本优化

vLLM最狠的不是性能提升,是同等硬件能服务更多用户。某创业公司从Transformers切换到vLLM:

  • 同样4张A100
  • 之前最大并发50,vLLM后最大并发200
  • 不用加机器,服务能力翻了4倍
  • 每月省下2张A100的云费用约¥3万

这就是vLLM的价值--不是让GPU算得更快,是让GPU不闲着。

六、六道高频面试题:能答上来才算真懂

Q1: PagedAttention为什么能提升显存利用率?

:传统框架按最大序列长度预分配连续显存,实际平均利用率24%。PagedAttention把KV Cache切成固定大小的Block,按需分配,用到哪页给哪页,逻辑Block通过Block Table映射到物理Block。利用率提升到96%,同一张GPU能服务的并发请求数翻了4倍。

加分项:提到Prefix Caching--共享system prompt的多个请求只存一份KV Cache。

Q2: Continuous Batching和Static Batching有什么区别?

:Static Batching凑满一批才跑,等最长的请求生成完才能处理下一批,存在木桶效应。Continuous Batching每次iteration只处理当前活跃请求,生成完的立刻退出,新请求随时加入,GPU不闲着。吞吐提升2-4倍,短请求延迟更低。

加分项:提到iteration级别的调度--每生成一个token就是一次iteration,Scheduler每次都重新决定哪些请求参与。

Q3: Tensor Parallelism和Pipeline Parallelism有什么区别?

  • Tensor Parallelism:按矩阵列切分权重矩阵,多张卡同时算同一层的不同部分,结果All-Reduce合并。通信开销在层内。
  • Pipeline Parallelism:按Layer切分模型,不同层放在不同卡上,数据像流水线一样经过各卡。通信开销在层间。
  • vLLM主要用Tensor Parallelism,因为它更适合低延迟场景。Pipeline Parallelism更常见于训练。

Q4: vLLM和Ollama有什么区别?线上怎么选?

:Ollama定位本地开发调试,简单预分配显存,Static Batching,不支持多GPU。vLLM定位生产环境,PagedAttention+Continuous Batching+Tensor Parallelism。两者API都兼容OpenAI格式,业务代码只需改base_url。开发用Ollama,上线用vLLM。

加分项:提到Ollama启动快(秒级),vLLM启动慢(加载模型30s+),所以K8s部署要配启动探针。

Q5: K8s部署vLLM,HPA基于什么指标扩缩容?

:不能只看CPU/内存,GPU推理Pod的CPU使用率很低。应该看vLLM暴露的/metrics端点中的vllm:num_requests_waiting(等待队列长度)。队列长了加Pod,空闲了缩Pod。另外要配置启动探针initialDelaySeconds: 120以上,因为vLLM冷启动加载模型需要时间。

Q6: 为什么说"vLLM解决的是显存瓶颈不是算力瓶颈"?

:GPU推理时,大部分时间GPU不是在算,是在等数据从显存搬过来。传统框架的显存管理太粗放,导致很多显存空转,能同时服务的请求数很少,GPU算力闲置。vLLM通过PagedAttention把显存利用率从24%拉到96%,更多请求能同时进来,Continuous Batching让这些请求高效调度,GPU才真正忙起来。所以瓶颈不在算力,在显存管理和调度效率。

总结:三句话记住vLLM

  1. PagedAttention解决显存浪费--分页管理,利用率24%→96%
  2. Continuous Batching解决调度低效--请求动态进出,吞吐×2-4
  3. Tensor Parallelism解决模型太大--多卡切分,70B也能跑

核心认知:vLLM不是让GPU算得更快,是让GPU不闲着。显存瓶颈非算力瓶颈,这个认知在面试时说出来,面试官会眼前一亮。

线上实操口诀:开发用Ollama,上线用vLLM,API兼容只改base_url,Docker单机够用,K8s看队列扩缩容。


下一篇我们会聊LLM可观测性--线上跑了vLLM之后怎么监控Token消耗、延迟分布、质量指标,别等用户投诉才发现问题。