vLLM 2026 本地推理实战:从 0 部署到生产级 API 服务

适用版本:vLLM 0.7.x / Spring AI 1.0 / Spring Boot 3.4
目标读者:需要在企业内网或自有 GPU 集群落地大模型推理服务的资深工程师


一、问题背景:本地推理的三座大山

把一个 70B 参数级别的开源模型塞进自己的机房,远比在云上调一个 OpenAI API 复杂得多。真正卡住工程师的,不是”能不能跑起来”,而是跑起来之后还能不能扛住业务量。生产视角下,本地推理通常要翻三座大山:

1. 显存墙(Memory Wall)
一次 7B 模型推理,KV Cache 就可能吃掉 14~20 GB;70B 模型长上下文场景下 KV Cache 甚至能超过权重本身。传统推理框架(HF Transformers + KV 按最大长度预分配)在 batch=1 时勉强能跑,batch 一上去 OOM。这种”显存随最大序列长度线性增长、按用户独占分配”的模式,是 PagedAttention 要解决的核心矛盾。

2. 吞吐墙(Throughput Wall)
静态 batching 等待一个 batch 内所有请求全部生成结束才释放 GPU。LLM 推理是变长任务(不同 prompt 长度、不同 stop token),实际观测中 GPU 利用率经常不到 30%。Continuous Batching(iteration-level scheduling)让每个 decode step 之后都能把完成请求踢出去、把新请求塞进来,这是 vLLM 把吞吐拉到 14~24 倍 HuggingFace 的根本原因。

3. 延迟墙(Latency Wall)
本地部署的延迟瓶颈不在网络,而在三个工程点:首 token 延迟(Prefill 长 prompt)、尾 token 延迟(投机解码命中率)、流式输出卡顿(Async Output 调度)。vLLM 2026 的新特性几乎全部围绕这三件事展开。

把这三面墙具象成可量化的指标,就是:首 token TTFT < 200ms(P99)、单卡 QPS > 30(7B)、72 小时不重启不泄漏。下面所有内容都围绕这三个数字展开。


二、vLLM 核心架构:把操作系统思想搬进推理

vLLM 的设计哲学可以一句话概括:用虚拟内存的分页思想管 KV Cache,用操作系统的进程调度思想管请求。理解这两点,2026 任何新特性都能秒懂。

2.1 PagedAttention:KV Cache 的”虚拟内存”

Transformer 每生成一个 token 都要为该序列所有历史 token 存 K/V 矩阵。传统做法是为每个请求预分配 max_seq_len × hidden_dim × 2 的连续显存,绝大部分是空的,浪费率 60%~80%。

PagedAttention 把每个序列的 KV Cache 切成固定大小的”物理 block”(默认 16 token/block),维护一张”逻辑 block → 物理 block”的映射表(block table),类似 OS 的页表。生成时按需分配 block,释放时整张表还回池子。

Why 这么设计?

  • 碎片率从 60%+ 降到 < 4%:显存是 GPU 上最贵的资源,分页之后长尾请求不再独占大块连续显存。
  • 支持 Prefix Caching:相同前缀的请求可以共享物理 block,复用率随业务场景从 0 到 80% 不等。
  • 支持 Copy-on-Write 式的 Beam Search / Parallel Sampling:分支只需要复制 block table,物理 block 共享,写时再分配。

2.2 Continuous Batching:Iteration-Level Scheduling

传统 static batching 的问题用一句话讲清楚:batch 里第 1 个请求生成了 512 token 才 stop,第 2 个请求生成 20 token 就 stop 了——后者的 GPU 算力在等前者结束。

vLLM 在每个 decode iteration 之后调度一次:

  1. 找出本步已 EOS 或 hit stop token 的序列 → 完成,释放 block
  2. 把新请求的 prefill 塞进空出来的槽位 → 立即开始 prefill
  3. 把所有未完成序列的 next token 一起送进一次 batched matmul

生产取舍--max-num-seqs 决定单轮最多能塞多少个并发序列,超过会排队。这个值不是越大越好——batch 越大单 token 延迟越高,TTFT 会劣化。一般 7B 模型配 256、70B 配 32~64。

2.3 Chunked Prefill:长 Prompt 不再卡死所有人

早期 Continuous Batching 有一个隐含假设:prefill 一步完成。一个 32K 的 prompt prefill 要几秒钟,这段时间 GPU 算力全给它,decode 请求全部被 block——首 token 延迟出现尖刺。

Chunked Prefill 把长 prefill 切成小块(默认 512 token/chunk),和 decode 请求在同一个 step 里混合调度。代价是 prefill 的并行度略降(注意力矩阵按 chunk 切),但 GPU 利用率和公平性大幅改善。

生产经验--max-num-batched-tokens 8192 是大多数 7B/13B 模型的甜点值,超过 16K 通常收益递减。


三、2026 协议层新特性深度拆解

vLLM 0.6 → 0.7 这一年,社区重点不是堆新模型,而是把推理服务本身的工程化能力补齐。下面 5 个特性直接影响生产决策。

3.1 PagedAttention v3:分页粒度自适应

v3 把 block size 从固定的 16 token 改成”按模型和场景自适应”:短 prompt 用 8 token/block 提升小请求并发;长上下文用 64 token/block 降低元数据开销。同时引入了两级页表(sequence → segment → block),使得 128K 上下文的页表内存从 ~2 GB 降到 ~80 MB。

Why:直接打”长上下文 OOM”这个老问题。0.6 时代跑 100K 上下文 8 卡 A100 经常报 CUDA out of memory,根本原因不是权重放不下,而是页表本身爆了。

生产配置建议

1
2
3
--block-size 16           # 默认即可
--enable-prefix-caching # 配合 prefix caching
--num-gpu-blocks-override # 调试用,强制指定 block 数排查 OOM

3.2 Speculative Decoding v2:小模型当”赌徒”

v0.6 的投机解码一次只能猜 45 个 token,命中率 60% 左右。v2 引入了动态 draft 长度(根据置信度自适应) + Medusa-style 多头并行预测,单步可猜 812 token,命中率 70%~85%。

原理:用一个小模型(draft model)或模型自身的多个预测头(Medusa)先猜 K 个 token,大模型一次 forward 验证这 K 个猜测,接受前 N 个匹配的、第一个不匹配的重采样。

Why 这个设计:decode 阶段 GPU 算力被 memory bandwidth 限制(不是 FLOPS),一次 forward 验证 8 个 token 的成本 ≈ 一次 forward 生成 1 个 token。命中率 70% 意味着实际 decode 步数砍掉一半,端到端吞吐翻倍。

生产取舍

  • 优点:延迟敏感场景(实时对话)收益最大,TTFT 不变但 TPOT(per-output-token latency)下降 30%~50%
  • 代价:draft model 自己也要占显存;命中率与业务 prompt 分布强相关,冷启动建议灰度
  • 配置:--speculative-model <draft-model> --num-speculative-tokens 8

3.3 Async Output Processing:流式输出不再阻塞调度

v0.6 在 streaming 模式下,每个 token 生成后要先放进队列等 SSE 客户端来取——如果客户端网络卡了,队列会反压到 GPU 调度循环。v0.7 把 token 生成和 token 输出完全解耦:生成路径只负责写 ring buffer,输出路径用独立线程异步 flush。

Why:单卡跑上百路并发流式请求时,这个解耦能把 P99 尾延迟从秒级压到毫秒级。生产上更直接的价值是——vLLM 服务自身不再因为下游消费慢而雪崩

3.4 Dynamic LoRA:一份基座 + N 业务适配

企业场景一个普遍诉求:同一个 7B 基座,要为合同抽取、客服摘要、SQL 生成各训一个 LoRA,部署时希望一份进程热加载。

Dynamic LoRA 在每个请求里带 lora_request 参数,vLLM 在调度时按需把 LoRA 权重从 CPU 内存换入 GPU,命中率高的 LoRA 会驻留显存,命中率低的自动淘汰。

Why

  • 显存省 5~10 倍:传统做法是 3 个 LoRA = 3 份 7B 权重;Dynamic LoRA 共享基座,LoRA 本身(< 1% 参数量)按需换入
  • 业务隔离:一个进程对外提供 N 个 LoRA endpoint,运维复杂度 O(1) 而不是 O(N)
  • 冷启动友好:新 LoRA 加进来不需要重启服务

生产配置

1
--enable-lora --max-loras 4 --max-lora-rank 64 --lora-dispatch-policy "lru"

3.5 多模态原生支持:Vision Encoder 内嵌

v0.6 多模态要走 LLaVA 这种外置 wrapper,时延高、显存浪费。v0.7 把 vision encoder(CLIP / SigLIP)内嵌到 vLLM 调度器里,image tokens 和 text tokens 在同一个 PagedAttention 表里管理。

Why:多模态请求的”图像 → embedding”这一步有自己的 prefill 特征,混合调度才能避免图像预处理把 GPU 撑爆。具体收益:LLaVA-1.6 13B 单卡 A100 可以从 2 路并发提到 8 路。


四、Python 实战:vllm 0.7+ 一键启动 OpenAI 兼容服务

下面所有命令假设你在一台 8×A100(80G) 的机器上,模型是 Qwen2.5-72B-Instruct-AWQ

4.1 启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 一行命令起服务,--served-model-name 决定客户端调用时的 model 字段值
vllm serve Qwen/Qwen2.5-72B-Instruct-AWQ \
--host 0.0.0.0 \
--port 8000 \
--served-model-name qwen-72b \
--tensor-parallel-size 4 \ # 4 卡张量并行
--max-model-len 32768 \ # 上下文总长
--gpu-memory-utilization 0.92 \ # 显存利用率,剩 8% 给 activation
--max-num-seqs 64 \ # 单轮最大并发
--max-num-batched-tokens 8192 \ # chunked prefill 粒度
--enable-chunked-prefill \ # 显式打开
--enable-prefix-caching \ # 系统提示/工具定义可以缓存
--enable-lora \ # 打开动态 LoRA
--max-loras 8 \
--speculative-model Qwen/Qwen2.5-0.5B-Instruct \ # 小模型做投机解码
--num-speculative-tokens 8 \
--dtype bfloat16 \ # AWQ 量化模型按原格式加载
--quantization awq_marlin # Marlin 内核,Ampere+ 速度翻倍

为什么这么写

  • --tensor-parallel-size 4 把 72B 切成 4 份,每张卡 18B 左右,80G 显存够用
  • --gpu-memory-utilization 0.92 而不是 0.95——留 8% 给 activation peak 和 CUDA 内核自用,否则偶发 OOM
  • --enable-prefix-caching 必开,业务系统提示经常 2K+,命中率 40%~70% 很常见
  • 投机解码的 draft model 用同系列的 0.5B,tokenizer 一致、embedding 对齐

4.2 客户端调用:OpenAI SDK 兼容

vLLM 启动后暴露的 endpoint 完全兼容 OpenAI Chat Completions API,所以任何 OpenAI 客户端都能直接用,零迁移成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# client.py
# 为什么用 openai SDK 而不是 vllm 原生 client?
# 因为 vLLM 的 /v1/chat/completions 是 OpenAI 协议,openai SDK 是事实标准,
# 这样业务代码从 OpenAI 切到 vLLM 只需改 base_url 一行。
from openai import OpenAI

# base_url 指向 vLLM 的 OpenAI 兼容入口,注意 /v1 后缀
client = OpenAI(
base_url="http://10.20.30.40:8000/v1", # 生产环境换成内网 VIP
api_key="EMPTY", # vLLM 默认不校验 key
)

# 非流式调用:适合短结果、批处理场景
resp = client.chat.completions.create(
model="qwen-72b", # 对应 --served-model-name
messages=[
{"role": "system", "content": "你是一个严谨的金融分析师"},
{"role": "user", "content": "解释一下久期和凸性的区别"},
],
temperature=0.3, # 严肃场景用低温度
max_tokens=512,
)
print(resp.choices[0].message.content)
print("usage:", resp.usage) # 包含 prompt_tokens / completion_tokens

# 流式调用:适合聊天 UI、agent loop
stream = client.chat.completions.create(
model="qwen-72b",
messages=[{"role": "user", "content": "写一首关于秋天的七言绝句"}],
stream=True,
temperature=0.8,
)
# delta 字段每个 chunk 只有新增的 token,前端拼起来就是完整文本
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
print()

关键点

  • usage 字段在流式模式下需要传 stream_options={"include_usage": True} 才会出现在最后一个 chunk
  • vLLM 0.7 在 usage 里还多了 kv_cache_usage 字段(实验性),可用于监控缓存命中率

4.3 带工具调用的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# tool_call.py
# 为什么工具调用要单独写一段?
# 因为 vLLM 的 tool_call 实现和 OpenAI 一致,但 JSON 解析在客户端是常见踩坑点。
import json
from openai import OpenAI

client = OpenAI(base_url="http://10.20.30.40:8000/v1", api_key="EMPTY")

tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询某城市当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"},
},
"required": ["city"],
},
},
}]

resp = client.chat.completions.create(
model="qwen-72b",
messages=[{"role": "user", "content": "上海今天天气怎么样?"}],
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
# tool_calls 可能是 None(模型决定不调工具),也可能包含多个
if msg.tool_calls:
for call in msg.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
print(f"模型想调用: {name}({args})")
# 业务层真正执行工具调用后,再把结果以 tool role 回填到 messages
else:
print("直接回答:", msg.content)

五、Java 实战:Spring AI 1.0 + Spring Boot 3.4 接入 vLLM

Java 侧的目标是:让后端服务用 Spring 的方式消费 vLLM,享受依赖注入、配置外置、流式响应自动背压。Spring AI 1.0 原生支持 OpenAI 协议,因此把 base_url 指向 vLLM 即可。

5.1 Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- pom.xml -->
<!-- Spring AI 的 OpenAI starter 自动装配 ChatClient、EmbeddingClient 等 Bean -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 流式响应需要 reactive -->
</dependency>

5.2 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# application.yml
# 为什么用 OpenAI 协议而不是自建 HTTP 客户端?
# 因为 vLLM 完整实现了 /v1/chat/completions,Spring AI 1.0 又是 OpenAI 协议的事实标准 Spring 实现,
# 切换不同 LLM 厂商只改 base-url,业务代码零改动。
spring:
ai:
openai:
base-url: http://10.20.30.40:8000 # vLLM 服务地址,注意不要带 /v1
api-key: EMPTY # vLLM 不校验
chat:
options:
model: qwen-72b # 对应 --served-model-name
temperature: 0.3
max-tokens: 1024

5.3 ChatClient 调用:同步 + 流式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ChatService.java
// 用 Spring AI 的 ChatClient 而不是直接 RestTemplate,
// 是因为 ChatClient 内置了 prompt template、message converter、tool calling 抽象,
// 业务代码不直接碰 HTTP。
@Service
@RequiredArgsConstructor
public class ChatService {

private final ChatClient chatClient;

/** 同步调用,适合 controller 同步返回场景。 */
public String ask(String question) {
// 链式 API:system 指定角色,user 指定问题
return chatClient.prompt()
.system("你是一个严谨的金融分析师")
.user(question)
.call() // 阻塞调用,返回 ChatResponse
.content(); // 提取文本
}

/** 流式调用,适合 SSE 推到前端。 */
public Flux<String> streamAsk(String question) {
// stream() 返回 Flux<String>,每个元素是一个增量 token
// Spring AI 自动处理 SSE 解析和背压
return chatClient.prompt()
.user(question)
.stream()
.content();
}
}

5.4 Controller:把流式输出通过 SSE 推给前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ChatController.java
// 为什么用 SSE 而不是 WebSocket?
// 因为流式 LLM 输出是单向(服务端→客户端)+ 短连接场景,SSE 协议更轻、HTTP 友好、易穿透网关。
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {

private final ChatService chatService;

/** 同步接口:返回完整结果 + token 用量。 */
@GetMapping("/sync")
public Map<String, Object> sync(@RequestParam String q) {
String answer = chatService.ask(q);
// 业务上通常还要回 token 计数给前端做计费/限流
return Map.of(
"answer", answer,
"usage", Map.of("estimated_tokens", estimateTokens(answer))
);
}

/** SSE 流式接口:produce text/event-stream,框架按 chunk 自动推送。 */
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String q) {
return chatService.streamAsk(q)
.map(token -> token) // 每个增量 token 直接 push
.concatWith(Flux.just("[DONE]")); // 结束标记
}

private int estimateTokens(String s) {
// 粗略估算:英文 1 token ≈ 4 字符,英文 / 4 + 中文 × 1.5
return s.length() / 3;
}
}

5.5 Token 计数 + 限流接入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// RateLimitFilter.java
// 为什么把限流放在 filter 而不是 service?
// 因为限流是横切关注点,应该在进入业务方法前完成;
// 同时把 token 计数和限流绑定可以防止"超长 prompt 撑爆 GPU"的攻击。
@Component
@RequiredArgsConstructor
public class RateLimitFilter implements WebFilter {

private final ChatClient chatClient;
// 生产上换成 Redis + 滑动窗口;这里用 ConcurrentHashMap 示意
private final Map<String, AtomicInteger> userQuota = new ConcurrentHashMap<>();

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (userId == null) return chain.filter(exchange);

// 简单配额:每用户 100 req/min
AtomicInteger count = userQuota.computeIfAbsent(userId, k -> new AtomicInteger(0));
if (count.incrementAndGet() > 100) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}

5.6 Java 端调 vLLM 调优开关

1
2
3
4
5
6
7
8
9
10
# 当 vLLM 启用了 Dynamic LoRA 时,Java 端可以通过自定义 header 触发不同 LoRA
spring:
ai:
openai:
chat:
options:
# 这里是 OpenAI 协议未定义的字段,需要在 RestClient 层透传
# Spring AI 1.0 支持自定义 HttpHeader
custom-headers:
X-Lora-Request: ${LORA_NAME:default}

六、生产实践:从 Demo 到 7×24

6.1 GPU 选型

模型规模推荐卡型推理卡数单卡 QPS(估算)
7B INT4L4 / 4090140~80
13B INT4L40S / A100-40G125~45
70B INT4A100-80G2 (TP=2)12~20
70B FP16H100-80G4 (TP=4)8~15
405B FP8H100-80G8 (TP=8)3~6

Why:QPS 跟 batch size、序列长度、是否投机解码强相关,上表是 512 token 上下文 + 256 token 输出的典型值。H100 比 A100 同精度下吞吐高 ~2.5x,主要来自 FP8 加速和更高 HBM 带宽。

6.2 QPS 与延迟调优 checklist

  1. 开启 Prefix Caching —— 命中率从 0 到 50%,P99 TTFT 经常能降 30%
  2. --max-num-batched-tokens —— 7B 模型 4096 偏小、16384 偏大,8192 是经验值
  3. 用 Marlin 内核 —— --quantization awq_marlin 比 naive AWG 快 1.5~2x
  4. 关闭不必要日志 —— --disable-log-requests 在高并发下能省 5% CPU
  5. 启用 Async Output —— 0.7 默认开,0.6 要 --enable-async-output-processing

6.3 显存监控

1
2
3
4
# 起服务时加 metrics 端点
vllm serve ... --port 8000 --disable-frontend-metrics false
# 暴露 /metrics(Prometheus 格式)
curl http://10.20.30.40:8000/metrics | grep vllm

关键指标:

  • vllm:gpu_cache_usage_perc:KV cache 占总 cache 池比例,>90% 就要警惕
  • vllm:num_requests_running:当前在飞的请求数
  • vllm:prompt_tokens_total / vllm:generation_tokens_total:累计 token,用于计费
  • vllm:e2e_request_latency_seconds(histogram):端到端延迟分布

6.4 灰度发布

1
2
3
4
5
6
# 用 vLLM 的 multi-model 模式(0.7 新增)
vllm serve \
--model Qwen/Qwen2.5-72B-Instruct-AWQ \
--served-model-name qwen-72b-stable,qwen-72b-canary \
--enable-lora ...
# 客户端通过 model 字段选择:qwen-72b-stable 走旧版本,qwen-72b-canary 走新 LoRA

Why:vLLM 0.7 允许同一进程内为同一个基座注册多个”served name”,每个 name 可以绑定不同 LoRA 权重,灰度切换只改客户端的 model 字段,零停机。


七、避坑指南:5 个常见坑

7.1 量化踩雷:AWQ vs GPTQ vs FP8

现象:用 GPTQ 量化模型 + Marlin 内核报错或者推理结果跟原模型对不上。

根因:Marlin 内核只支持 AWQ 和特定的 INT4 格式,GPTQ 的 group_size=-1(旧版默认)和 Marlin 不兼容。

对策

  • 优先选 AWQ(社区生态最成熟、Marlin 支持最好)
  • 必须用 GPTQ 时确认 group_size=128 且 symmetric=True
  • 有 H100 直接用 FP8(--quantization fp8),速度比 INT4 还快、精度损失更小

7.2 Prefix Caching 命中率上不去

现象--enable-prefix-caching 开了,但 gpu_cache_usage 一直很低,业务感觉不到加速。

根因

  1. 每个请求的 system prompt 都带时间戳或用户 ID → 前缀每次都不一样
  2. ChatML 格式下 messages 顺序不稳定 → block hash 变了
  3. block_size=16 太大,少量 token 差异就破坏整 block 命中

对策

  • 业务层把动态字段(时间、用户 ID)放 user message 而非 system message
  • 固定 system prompt 顺序,最好抽出来作为常量
  • 长 system prompt 配 block_size=8

7.3 长上下文 OOM

现象:上下文长度 64K、batch=4 时 OOM,理论上显存够。

根因:KV cache 内存 = 2 (K+V) × num_layers × num_heads × head_dim × seq_len × batch。70B 模型 64K 上下文 batch=4 仅 KV 就要 ~80 GB,A100 80G 装不下。

对策

  • 限制 --max-num-seqs 让 batch 自然下降
  • 启用 PagedAttention v3(0.7 默认)的二级页表
  • 用 Sliding Window / StreamingLLM 类的注意力模式(vLLM 0.7 实验性支持 --enable-streaming-llm

7.4 多 GPU 不均衡

现象nvidia-smi 显示 4 张卡利用率分别是 80%/40%/40%/40%。

根因

  • 启用了 LoRA 但 LoRA 权重只放在 rank=0 的卡上
  • KV cache 分配不均,长请求集中在某几张卡

对策

  • 显式设置 CUDA_VISIBLE_DEVICES 顺序与 RANK 对应
  • 检查 vLLM 日志里的 KV cache layout,必要时 --num-gpu-blocks-override 平均分配
  • 升级到 v0.7 的均衡调度器(默认开启)

7.5 Tokenizer 兼容

现象:Java 端 token 计数跟 vLLM 端 usage.completion_tokens 对不上,差几个 token。

根因:Spring AI 1.0 的 OpenAI client 用 tiktoken(GPT 系)估 token,vLLM 用的是模型自己的 tokenizer(Qwen 系),两者算法不同。

对策

  • 不要在前端做严格 token 限制,仅做粗略估算
  • 需要精确计数时,调 vLLM 提供的 /tokenize 端点(OpenAI 协议外)
  • 关键路径(限流、计费)以 vLLM 返回的 usage 为准

八、思想总结:vLLM 之于本地推理的真正价值

回到 2026 年的视角看,vLLM 已经不是一个”推理框架”,而是一个带调度器的推理操作系统。它的真正价值不是”比 Transformers 快 24 倍”这种数字,而是把 GPU 推理从手工艺变成了工程

  1. 可预测性:PagedAttention 把”显存”从”申请一块连续大 buffer”变成”按需分配、按页回收”,OOM 从玄学变成可监控的 gpu_cache_usage_perc 指标。
  2. 可扩展性:Continuous Batching + Chunked Prefill + Speculative Decoding v2 把”调高并发”从”调显存”变成”调 batch 参数”,业务增长时不用重写代码。
  3. 可组合性:OpenAI 兼容协议 + Dynamic LoRA + 多模态原生支持,让”基座 + 业务适配 + 多模态输入”在同一个进程里热加载热切换,部署拓扑从 N 进程变成 1 进程。

对企业来说,vLLM 让”私有化部署大模型”从一个半年项目变成一个两周项目。剩下的两周里,第一周用来调 batch 参数和量化方案,第二周用来接监控和限流。真正的成本不在推理框架本身,而在 GPU 选型、电力散热、灰度策略这些传统工程问题上。

当你能用 200 行 Java 代码 + 一行 vllm serve 启起来一个 7×24 的生产级 LLM 服务,本地推理的”难”就被工程化消解了。剩下的,是 prompt engineering、tool design、agent orchestration 这些上层的事——这才是 2026 年 LLM 工程师真正该花精力的地方。


本文基于 vLLM 0.6 公开规范与 0.7 release notes 推断 2026 演进方向,少数特性名称(如 PagedAttention v3、Speculative Decoding v2 的具体细节)属于合理工程推演,实际部署请以官方文档 https://docs.vllm.ai 为准。