一、排了两个月的队我决定自己动手2024年底我给团队搭了一套推理服务基于 Transformers HuggingFace 的 naive 实现。QPS 大概在 0.8 左右——跑 LLaMA-13BA100 单卡。用户一多请求开始排队。最长的一次一个用户等了 47 秒才看到第一个 token。排队的根因不是模型慢。模型本身的前向计算差不多 120ms/token瓶颈在显存。传统推理框架里每个请求来了先分配一块连续的 KV Cache 空间。假设一个请求生成长度 2048 的序列KV Cache 要占用约 2.8GB 显存FP1613B 模型。问题是你不知道最终生多长于是只能按最大长度预分配。用户只说了 200 个字你给它留了 2048 个 token 的位置。碎片率和浪费率惨不忍睹。直到我看到 vLLM 那篇 PagedAttention 论文。它解决的就是这个问题——把 KV Cache 切成固定大小的页像操作系统的虚拟内存一样管理。我当时的第一反应是这不就是数据库里早就玩烂了的分页吗但认真看完实现发现把操作系统的内存管理思想搬到 GPU 显存上工程落地的细节比想象中多得多。本文从工程实现的角度拆解 PagedAttention 的设计思路、核心数据结构和我在接入过程中踩过的坑。二、KV Cache 为什么是瓶颈先算一笔账。一个 Transformer decoder layer 的 self-attention 计算中对于每个 token我们要算Q x * W_Q, K x * W_K, V x * W_V attn softmax(Q * K^T / sqrt(d)) * V对于自回归生成生成 token i 时需要用到之前所有 token 的 K 和 V。如果每次重新算复杂度是 O(n²) 的——生成到第 2048 个 token 时前面的都要重算一遍这是不可接受的。于是有了 KV Cache把每个 layer 的 K 和 V 矩阵存下来每次追加新 token 的 K 和 V。显存计算KV Cache per token 2 (K 和 V) × num_layers × d_model × dtype_bytes 以 LLaMA-13B 为例 num_layers 40 d_model 5120 dtype FP16 2 bytes 每个 token 的 KV Cache 2 × 40 × 5120 × 2 819,200 bytes ≈ 0.8MB 生成 2048 个 tokens → 2048 × 0.8MB ≈ 1.6GB 加上 batch 维度batch_size 4 时 → 6.4GBA100 80GB 显存模型权重占约 26GBFP16剩下的 54GB 用来做 KV Cache 和中间激活。你猜怎么着大部分推理框架在 batch_size8 时就把显存吃光了不是因为模型算力不够而是KV Cache 的分配策略太浪费。传统方案的问题预分配最大长度每个请求按 max_seq_len 预留空间实际用的可能只有 10%外部碎片请求长度不一先来的释放了空间但留下的空洞不连续没法给新请求用内部碎片预留了 2048 slot 但只用了 300那 1700 个 slot 就浪费了三、PagedAttention 的核心思想PagedAttention 的核心就一句话把 KV Cache 切成固定大小的物理块Block通过逻辑到物理的映射表来管理按需分配用完即还。像极了操作系统的分页内存管理。但 GPU 上没有 MMU所以 vLLM 自己做了一套 Block Manager。3.1 Block Table每个请求vLLM 里叫 Sequence维护一个逻辑 Block Table逻辑 Block ID | 物理 Block ID | 已占用的 slot 数 0 | 47 | 16 1 | 23 | 16 2 | 89 | 8物理 block 大小为 16 个 token 的 KV 数据。Block 满了16/16就分配下一个。最后一个 block 可能不满如上图 block 2 只用了 8 个 slot。这种设计带来的好处无外部碎片任何大小的释放都能被复用因为 block 是等长的按需分配只分配实际使用的 block不预分配Copy-on-Write同一个 block 可以被多个请求共享在 beam search 场景下特别有用3.2 Block Manager 的核心流程# 伪代码表达核心逻辑classBlockManager:def__init__(self,num_gpu_blocks,block_size16):self.free_blockslist(range(num_gpu_blocks))self.allocated{}# seq_id - [physical_block_ids]self.block_sizeblock_sizedefallocate(self,seq_id,num_tokens):为 seq 分配容纳 num_tokens 所需的物理块needed_blocksceil(num_tokens/self.block_size)already_usedlen(self.allocated.get(seq_id,[]))*self.block_sizeifalready_usednum_tokens:returnnew_blocks_neededceil((num_tokens-already_used)/self.block_size)iflen(self.free_blocks)new_blocks_needed:raiseOOM(显存不足需要执行 swap 或 preemption)for_inrange(new_blocks_needed):blockself.free_blocks.pop(0)self.allocated.setdefault(seq_id,[]).append(block)deffree(self,seq_id):forblock_idinself.allocated.get(seq_id,[]):self.free_blocks.append(block_id)delself.allocated[seq_id]四、工程实现细节4.1 注意力计算的修改PagedAttention 最 tricky 的部分在 CUDA kernel 层面。标准 multi-head attention 假设 K 和 V 是连续的——[num_tokens, num_heads, head_dim]。但有了分页之后物理上 K 和 V 的存储是离散的# 标准 attentionK 是连续 tensor [total_tokens, num_heads, head_dim]# PagedAttentionK 是 [num_blocks, block_size, num_heads, head_dim]# 其中 block 在物理上不连续所以 vLLM 自己写了两个 CUDA kernelpaged_attention_v1每个 block 单独触发一个 block-level GEMM然后累加。适合 block 数量少的情况。paged_attention_v2先 partial accumulate再 merge。通过减少 kernel launch 次数来降低 overhead。实际线上用的是 v2。从 A100 的 nsys profile 结果来看v2 相比 v1 减少了约 30% 的 kernel launch 时间。4.2 Prefix Caching自动前缀缓存vLLM 0.4.0 之后引入了 automatic prefix caching。同一个 block 的 KV 如果和之前某个请求的前缀相同可以直接复用。请求1: 介绍一下强化学习的基本原理 请求2: 介绍一下强化学习的应用场景 ^ 前缀 token 的 block 是相同的开启方式exportVLLM_ENABLE_PREFIX_CACHING1实测数据在 multi-turn conversation 场景下共享 system promptprefix cache hit rate 能达到 60-80%prefill 阶段的延迟降低约 40%。4.3 Block 大小的选择Block size 是 vLLM 的关键超参数。vLLM 默认 16但这个值的影响很微妙block_size 越大Block Table 越小内存开销低但内部碎片更多浪费率更高block_size 越小碎片率低但 Block Table 变大管理开销增加在 A100 上做过几组 A/B 测试结论是block_size平均显存利用率QPS (batch8)TFLOPS886.2%1.4138.2%1684.7%1.4839.1%3278.3%1.4437.8%6469.5%1.3535.1%block_size16 是 sweet spot——QPS 最高且显存利用率足够好。五、接入实战从 Transformers 迁移到 vLLM5.1 最小接入代码fromvllmimportLLM,SamplingParams llmLLM(modelmeta-llama/Llama-2-13b-chat-hf,tensor_parallel_size2,gpu_memory_utilization0.90,max_num_seqs256,enable_prefix_cachingTrue,)sampling_paramsSamplingParams(temperature0.7,top_p0.9,max_tokens2048,stop[/s],)outputsllm.generate(prompts,sampling_params)5.2 性能压测在 2×A100-80GB, LLaMA-2-13B 环境下指标TransformersvLLM (block16)提升倍数单请求延迟 (50 token 输出)1.2s0.9s1.33xBatch8 吞吐 (token/s)1283843.0xBatch32 吞吐 (token/s)2241,0244.57x最大支持 batch size1225621.3xKV Cache 利用率~45%~85%1.89x5.3 Serving 部署python-mvllm.entrypoints.openai.api_server\--modelmeta-llama/Llama-2-13b-chat-hf\--tensor-parallel-size2\--gpu-memory-utilization0.90\--max-num-seqs128\--port8000fromopenaiimportOpenAI clientOpenAI(base_urlhttp://localhost:8000/v1,api_keysk-xxx,)responseclient.chat.completions.create(modelmeta-llama/Llama-2-13b-chat-hf,messages[{role:user,content:解释一下 PagedAttention}],max_tokens1024,)六、生产环境中踩过的坑坑 1gpu_memory_utilization 调大不一定好我把gpu_memory_utilization设到 0.95结果跑了一周频繁出现 CUDA OOM。排查后发现这个参数只控制了 KV Cache 分配的显存上限但模型跑起来之后中间激活activation memory也是动态的。如果某个请求有很长的 prompt比如 8K中间激活 tensor 会撑爆剩下的那点空间。安全值是 0.85-0.90留出 10-15% 给中间激活和 CUDA context。坑 2max_num_seqs 不是设得越大越好把 max_num_seqs 设到 256结果 QPS 反而下降了。原因在于 vLLM 的调度策略是iterate-batch-level scheduling——每个 decode step 都把 batch 里所有 sequence 拿出来一起算。256 个 sequence 虽然不 OOM但算力分摊开之后每个 sequence 的延迟从 50ms 涨到了 300ms。从实测来看LLaMA-13B 在 A100 的 sweet spot 是 batch_size64~128 之间。坑 3量化的坑vLLM 支持 AWQ 和 GPTQ 量化模型。我用 AWQ 4bit 量化 LLaMA-13B模型文件从 26GB 降到了 7.2GB。但精度下降在某些任务上很明显——GSM8K 准确率从 82% 降到 71%HumanEval pass1 从 34% 降到 27%。vLLM 的 AWQ kernel 对 group size 有要求必须能被 128 整除且 group size 不能超过 256。如果量化时用了 group_size32vLLM 直接报错加载不了。坑 4Prefix Caching 的内存开销开启 prefix caching 后hash table 本身也吃显存。如果 prompt 几乎都不一样cache hit 率不到 5%hash table 反而浪费了空间。这个功能只有在共享前缀比例高的时候才有价值。七、性能调优实践7.1 调度器参数llmLLM(...,max_num_batched_tokens4096,max_num_seqs256,scheduler_delay_factor0.1,)max_num_batched_tokens控制 prefill 阶段的 batch 大小。经验值4096-8192。scheduler_delay_factor控制调度器等一等的意愿。0.1 表示等待时间占 decode iteration 时间的 10%。7.2 实测调优流程python-mvllm.entrypoints.openai.run_batch\--modelmeta-llama/Llama-2-13b-chat-hf\--input-file requests.jsonl\--tensor-parallel-size2\--gpu-memory-utilization0.90\--max-num-seqs128requests.jsonl 格式{prompt: Hello, how are you?, max_tokens: 256, temperature: 0.7} {prompt: Write a poem about AI, max_tokens: 512, temperature: 0.8}7.3 最终部署配置模型Llama-2-13b-chat-hf 硬件2×A100-80GB (NVLink) TP2 gpu_memory_utilization0.88 max_num_seqs128 enable_prefix_cachingtrue block_size16 max_num_batched_tokens6144 实测 - P50 TTFT380ms - P95 TTFT1.2s - TPOT52ms per token - QPS约 3.3 - 单卡显存峰值74.2GB (92.75%)八、与其它推理框架的对比特性vLLMTensorRT-LLMTGIPagedAttention✅ 原生❌❌量化支持AWQ/GPTQ/FP8AWQ/FP8/INT4AWQ/GPTQ调度策略基于分页调度静态 batch动态 batchOOM 恢复Preemption无无Prefix Caching✅❌有限易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐TensorRT-LLM 的优势在于推理速度——kernel 手工优化得更彻底同样的模型和硬件通过能达到 vLLM 的 1.1-1.2x。但上手成本高没有 preemption 机制显存不够直接崩。vLLM 胜在工程友好一键启动、自动调度、自动 prefix cache、graceful OOM 处理。九、总结与建议部署 LLM 推理服务的建议顺序先用 vLLM 上线——15 分钟跑起来稳定性够用再加 Prefix Caching——prompt 有共享前缀时提效最高再考虑量化——延迟不是瓶颈就不要量化选 AWQ 4bit最后再考虑 TensorRT-LLM——只有需要极致吞吐、愿意花两周调优时才有价值PagedAttention 给我的最大启发是AI 系统的瓶颈往往不在算法本身而在资源管理的粒度上。把 KV Cache 从连续大块切到小页管理不改变任何数学计算就带来了几十倍的吞吐提升。这种计算不变存储重构的思路在 AI 工程化中值得反复使用。最后留一条建议不要在生产环境用最新版 vLLM。vLLM 迭代极快每个 release 都可能引入 regression。我们的做法是锁定一个大版本比如 0.6.x小版本只打 patch 不追新等社区跑稳了再跳版本。
LLM推理优化:vLLM PagedAttention深度解析与工程实践
一、排了两个月的队我决定自己动手2024年底我给团队搭了一套推理服务基于 Transformers HuggingFace 的 naive 实现。QPS 大概在 0.8 左右——跑 LLaMA-13BA100 单卡。用户一多请求开始排队。最长的一次一个用户等了 47 秒才看到第一个 token。排队的根因不是模型慢。模型本身的前向计算差不多 120ms/token瓶颈在显存。传统推理框架里每个请求来了先分配一块连续的 KV Cache 空间。假设一个请求生成长度 2048 的序列KV Cache 要占用约 2.8GB 显存FP1613B 模型。问题是你不知道最终生多长于是只能按最大长度预分配。用户只说了 200 个字你给它留了 2048 个 token 的位置。碎片率和浪费率惨不忍睹。直到我看到 vLLM 那篇 PagedAttention 论文。它解决的就是这个问题——把 KV Cache 切成固定大小的页像操作系统的虚拟内存一样管理。我当时的第一反应是这不就是数据库里早就玩烂了的分页吗但认真看完实现发现把操作系统的内存管理思想搬到 GPU 显存上工程落地的细节比想象中多得多。本文从工程实现的角度拆解 PagedAttention 的设计思路、核心数据结构和我在接入过程中踩过的坑。二、KV Cache 为什么是瓶颈先算一笔账。一个 Transformer decoder layer 的 self-attention 计算中对于每个 token我们要算Q x * W_Q, K x * W_K, V x * W_V attn softmax(Q * K^T / sqrt(d)) * V对于自回归生成生成 token i 时需要用到之前所有 token 的 K 和 V。如果每次重新算复杂度是 O(n²) 的——生成到第 2048 个 token 时前面的都要重算一遍这是不可接受的。于是有了 KV Cache把每个 layer 的 K 和 V 矩阵存下来每次追加新 token 的 K 和 V。显存计算KV Cache per token 2 (K 和 V) × num_layers × d_model × dtype_bytes 以 LLaMA-13B 为例 num_layers 40 d_model 5120 dtype FP16 2 bytes 每个 token 的 KV Cache 2 × 40 × 5120 × 2 819,200 bytes ≈ 0.8MB 生成 2048 个 tokens → 2048 × 0.8MB ≈ 1.6GB 加上 batch 维度batch_size 4 时 → 6.4GBA100 80GB 显存模型权重占约 26GBFP16剩下的 54GB 用来做 KV Cache 和中间激活。你猜怎么着大部分推理框架在 batch_size8 时就把显存吃光了不是因为模型算力不够而是KV Cache 的分配策略太浪费。传统方案的问题预分配最大长度每个请求按 max_seq_len 预留空间实际用的可能只有 10%外部碎片请求长度不一先来的释放了空间但留下的空洞不连续没法给新请求用内部碎片预留了 2048 slot 但只用了 300那 1700 个 slot 就浪费了三、PagedAttention 的核心思想PagedAttention 的核心就一句话把 KV Cache 切成固定大小的物理块Block通过逻辑到物理的映射表来管理按需分配用完即还。像极了操作系统的分页内存管理。但 GPU 上没有 MMU所以 vLLM 自己做了一套 Block Manager。3.1 Block Table每个请求vLLM 里叫 Sequence维护一个逻辑 Block Table逻辑 Block ID | 物理 Block ID | 已占用的 slot 数 0 | 47 | 16 1 | 23 | 16 2 | 89 | 8物理 block 大小为 16 个 token 的 KV 数据。Block 满了16/16就分配下一个。最后一个 block 可能不满如上图 block 2 只用了 8 个 slot。这种设计带来的好处无外部碎片任何大小的释放都能被复用因为 block 是等长的按需分配只分配实际使用的 block不预分配Copy-on-Write同一个 block 可以被多个请求共享在 beam search 场景下特别有用3.2 Block Manager 的核心流程# 伪代码表达核心逻辑classBlockManager:def__init__(self,num_gpu_blocks,block_size16):self.free_blockslist(range(num_gpu_blocks))self.allocated{}# seq_id - [physical_block_ids]self.block_sizeblock_sizedefallocate(self,seq_id,num_tokens):为 seq 分配容纳 num_tokens 所需的物理块needed_blocksceil(num_tokens/self.block_size)already_usedlen(self.allocated.get(seq_id,[]))*self.block_sizeifalready_usednum_tokens:returnnew_blocks_neededceil((num_tokens-already_used)/self.block_size)iflen(self.free_blocks)new_blocks_needed:raiseOOM(显存不足需要执行 swap 或 preemption)for_inrange(new_blocks_needed):blockself.free_blocks.pop(0)self.allocated.setdefault(seq_id,[]).append(block)deffree(self,seq_id):forblock_idinself.allocated.get(seq_id,[]):self.free_blocks.append(block_id)delself.allocated[seq_id]四、工程实现细节4.1 注意力计算的修改PagedAttention 最 tricky 的部分在 CUDA kernel 层面。标准 multi-head attention 假设 K 和 V 是连续的——[num_tokens, num_heads, head_dim]。但有了分页之后物理上 K 和 V 的存储是离散的# 标准 attentionK 是连续 tensor [total_tokens, num_heads, head_dim]# PagedAttentionK 是 [num_blocks, block_size, num_heads, head_dim]# 其中 block 在物理上不连续所以 vLLM 自己写了两个 CUDA kernelpaged_attention_v1每个 block 单独触发一个 block-level GEMM然后累加。适合 block 数量少的情况。paged_attention_v2先 partial accumulate再 merge。通过减少 kernel launch 次数来降低 overhead。实际线上用的是 v2。从 A100 的 nsys profile 结果来看v2 相比 v1 减少了约 30% 的 kernel launch 时间。4.2 Prefix Caching自动前缀缓存vLLM 0.4.0 之后引入了 automatic prefix caching。同一个 block 的 KV 如果和之前某个请求的前缀相同可以直接复用。请求1: 介绍一下强化学习的基本原理 请求2: 介绍一下强化学习的应用场景 ^ 前缀 token 的 block 是相同的开启方式exportVLLM_ENABLE_PREFIX_CACHING1实测数据在 multi-turn conversation 场景下共享 system promptprefix cache hit rate 能达到 60-80%prefill 阶段的延迟降低约 40%。4.3 Block 大小的选择Block size 是 vLLM 的关键超参数。vLLM 默认 16但这个值的影响很微妙block_size 越大Block Table 越小内存开销低但内部碎片更多浪费率更高block_size 越小碎片率低但 Block Table 变大管理开销增加在 A100 上做过几组 A/B 测试结论是block_size平均显存利用率QPS (batch8)TFLOPS886.2%1.4138.2%1684.7%1.4839.1%3278.3%1.4437.8%6469.5%1.3535.1%block_size16 是 sweet spot——QPS 最高且显存利用率足够好。五、接入实战从 Transformers 迁移到 vLLM5.1 最小接入代码fromvllmimportLLM,SamplingParams llmLLM(modelmeta-llama/Llama-2-13b-chat-hf,tensor_parallel_size2,gpu_memory_utilization0.90,max_num_seqs256,enable_prefix_cachingTrue,)sampling_paramsSamplingParams(temperature0.7,top_p0.9,max_tokens2048,stop[/s],)outputsllm.generate(prompts,sampling_params)5.2 性能压测在 2×A100-80GB, LLaMA-2-13B 环境下指标TransformersvLLM (block16)提升倍数单请求延迟 (50 token 输出)1.2s0.9s1.33xBatch8 吞吐 (token/s)1283843.0xBatch32 吞吐 (token/s)2241,0244.57x最大支持 batch size1225621.3xKV Cache 利用率~45%~85%1.89x5.3 Serving 部署python-mvllm.entrypoints.openai.api_server\--modelmeta-llama/Llama-2-13b-chat-hf\--tensor-parallel-size2\--gpu-memory-utilization0.90\--max-num-seqs128\--port8000fromopenaiimportOpenAI clientOpenAI(base_urlhttp://localhost:8000/v1,api_keysk-xxx,)responseclient.chat.completions.create(modelmeta-llama/Llama-2-13b-chat-hf,messages[{role:user,content:解释一下 PagedAttention}],max_tokens1024,)六、生产环境中踩过的坑坑 1gpu_memory_utilization 调大不一定好我把gpu_memory_utilization设到 0.95结果跑了一周频繁出现 CUDA OOM。排查后发现这个参数只控制了 KV Cache 分配的显存上限但模型跑起来之后中间激活activation memory也是动态的。如果某个请求有很长的 prompt比如 8K中间激活 tensor 会撑爆剩下的那点空间。安全值是 0.85-0.90留出 10-15% 给中间激活和 CUDA context。坑 2max_num_seqs 不是设得越大越好把 max_num_seqs 设到 256结果 QPS 反而下降了。原因在于 vLLM 的调度策略是iterate-batch-level scheduling——每个 decode step 都把 batch 里所有 sequence 拿出来一起算。256 个 sequence 虽然不 OOM但算力分摊开之后每个 sequence 的延迟从 50ms 涨到了 300ms。从实测来看LLaMA-13B 在 A100 的 sweet spot 是 batch_size64~128 之间。坑 3量化的坑vLLM 支持 AWQ 和 GPTQ 量化模型。我用 AWQ 4bit 量化 LLaMA-13B模型文件从 26GB 降到了 7.2GB。但精度下降在某些任务上很明显——GSM8K 准确率从 82% 降到 71%HumanEval pass1 从 34% 降到 27%。vLLM 的 AWQ kernel 对 group size 有要求必须能被 128 整除且 group size 不能超过 256。如果量化时用了 group_size32vLLM 直接报错加载不了。坑 4Prefix Caching 的内存开销开启 prefix caching 后hash table 本身也吃显存。如果 prompt 几乎都不一样cache hit 率不到 5%hash table 反而浪费了空间。这个功能只有在共享前缀比例高的时候才有价值。七、性能调优实践7.1 调度器参数llmLLM(...,max_num_batched_tokens4096,max_num_seqs256,scheduler_delay_factor0.1,)max_num_batched_tokens控制 prefill 阶段的 batch 大小。经验值4096-8192。scheduler_delay_factor控制调度器等一等的意愿。0.1 表示等待时间占 decode iteration 时间的 10%。7.2 实测调优流程python-mvllm.entrypoints.openai.run_batch\--modelmeta-llama/Llama-2-13b-chat-hf\--input-file requests.jsonl\--tensor-parallel-size2\--gpu-memory-utilization0.90\--max-num-seqs128requests.jsonl 格式{prompt: Hello, how are you?, max_tokens: 256, temperature: 0.7} {prompt: Write a poem about AI, max_tokens: 512, temperature: 0.8}7.3 最终部署配置模型Llama-2-13b-chat-hf 硬件2×A100-80GB (NVLink) TP2 gpu_memory_utilization0.88 max_num_seqs128 enable_prefix_cachingtrue block_size16 max_num_batched_tokens6144 实测 - P50 TTFT380ms - P95 TTFT1.2s - TPOT52ms per token - QPS约 3.3 - 单卡显存峰值74.2GB (92.75%)八、与其它推理框架的对比特性vLLMTensorRT-LLMTGIPagedAttention✅ 原生❌❌量化支持AWQ/GPTQ/FP8AWQ/FP8/INT4AWQ/GPTQ调度策略基于分页调度静态 batch动态 batchOOM 恢复Preemption无无Prefix Caching✅❌有限易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐TensorRT-LLM 的优势在于推理速度——kernel 手工优化得更彻底同样的模型和硬件通过能达到 vLLM 的 1.1-1.2x。但上手成本高没有 preemption 机制显存不够直接崩。vLLM 胜在工程友好一键启动、自动调度、自动 prefix cache、graceful OOM 处理。九、总结与建议部署 LLM 推理服务的建议顺序先用 vLLM 上线——15 分钟跑起来稳定性够用再加 Prefix Caching——prompt 有共享前缀时提效最高再考虑量化——延迟不是瓶颈就不要量化选 AWQ 4bit最后再考虑 TensorRT-LLM——只有需要极致吞吐、愿意花两周调优时才有价值PagedAttention 给我的最大启发是AI 系统的瓶颈往往不在算法本身而在资源管理的粒度上。把 KV Cache 从连续大块切到小页管理不改变任何数学计算就带来了几十倍的吞吐提升。这种计算不变存储重构的思路在 AI 工程化中值得反复使用。最后留一条建议不要在生产环境用最新版 vLLM。vLLM 迭代极快每个 release 都可能引入 regression。我们的做法是锁定一个大版本比如 0.6.x小版本只打 patch 不追新等社区跑稳了再跳版本。