恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于vLLM的高并发推理服务搭建:原理、调参与压测实录
首页
资讯中心
/
基于vLLM的高并发推理服务搭建:原理、调参与压测实录
基于vLLM的高并发推理服务搭建:原理、调参与压测实录
发布时间:2026/10/16 7:52:43
做本地大模型服务最难受的往往不是“能不能跑”而是“人一多就卡”。单人调试的时候模型响应顺畅QPS看着也还行一旦接上团队应用或者多个客户端并发一起来GPU显存瞬间被KV Cache吃满请求排队、超时、OOM接连上演。这个场景我踩了无数次坑之后最终稳定在一个开源方案上vLLM。这篇博客记录的就是我基于vLLM高吞吐推理引擎搭建本地高并发推理服务的完整过程包括原理拆解、部署参数、压测数据和排障实录适合那些已经能跑通单机推理、想把模型正式服务化的开发者参考。先说结论vLLM解决的核心问题不是“把单条推理变快”而是“在有限的显存里塞进更多并发请求同时保证吞吐”。它做的事本质上和数据库的缓冲池管理、操作系统的虚拟内存分页很像。下面我会把这些机制掰开讲清楚再给出可以直接抄走的启动配置和调优方法。1. 项目缘起本地推理服务为什么会被并发打垮1.1 单请求推理与高并发推理的差距很多人初次部署大模型时用的是最直接的流程加载模型写一个循环对每个输入调用模型的生成方法。单条请求时模型全部资源都服务这一个请求首token延迟可能只有几百毫秒生成速度也快。但这套思路一旦暴露给多个客户端就立刻崩盘。原因在于大模型推理是“算力密集显存密集”双密集任务。每个请求在生成过程中都要维护一套KV Cache——就是模型在注意力计算中缓存的Key和Value矩阵。显存是硬上限一个请求占用的KV Cache和上下文长度成正比。假设某7B模型在FP16精度下权重占了约14GB显存显卡是24GB那剩余可用的也就8~10GB。单条长对话可能直接吃掉几GB并发一多余量瞬间见底。更麻烦的是模型在生成时是顺序逐token进行解码的。一个请求生成长度为512个token时它经历了512次前向计算每次只输出一个token。如果这时候另一个请求进来传统的串行处理只能排队等待。如果做成静态批量又要等当前批次所有请求全部生成完毕才能处理新请求任何一个长尾请求都会拖慢整批速度。这就是“本地推理服务被并发打垮”的根本原因。1.2 为什么最终选了vLLM市面上解决大模型推理并发问题的方案有好几个方向但vLLM是我实际对比后认为最适合本地场景的。它的核心优势有三个。第一KV Cache显存浪费少。vLLM提出了PagedAttention机制把KV Cache拆成分页块按需分配实测下相同显存能支撑的并发数比其他实现高出不少。第二调度效率高。vLLM实现了Continuous Batching连续批处理在每一步解码时动态决定哪些请求继续占用资源、哪些请求已经结束可以释放新请求可以随时插入空位不需要等待整个批次结束。第三部署简单、生态成熟。它提供了和OpenAI兼容的API Server直接拉起HTTP服务客户端可以用现成的SDK接入。同时它是用高性能算子库做底层加速不需要我手动写CUDA优化代码。我最初也考虑过自己实现一套简易的批量推理框架但很快放弃了推理框架最难的并非“把多个请求拼在一起”而是显存分配、调度策略、算子优化、并发安全这些细节短时间内很难做到可靠。vLLM相当于把这些已经解决得比较好的问题封装好我只需要关注业务参数。2. 核心机制拆解vLLM凭什么能撑起高并发2.1 PagedAttention给KV Cache做分页管理要理解PagedAttention先要知道传统实现是怎么浪费显存的。在常规推理框架里每个请求的KV Cache通常会按照最大可能长度一次性预分配。比如模型支持最大2048个token的上下文那么即使某个请求只生成了20个token系统也为它预留了2048长度对应的KV Cache空间。这个预留空间既不能给其他请求用也没法动态缩小导致显存利用率很低。vLLM的做法和操作系统管理内存的思路完全一致把KV Cache切分成固定大小的物理块每个请求不再持有连续的一段显存而是持有一张“块表”分别指向多个分散的物理块。当请求的KV Cache增加时才新分配一个块当块用不到了立刻释放回全局块池其他请求可以马上使用。这个机制带来的直接收益是显存碎片化大幅减少。更关键的是不同请求的相同前缀比如相同的system prompt可以共享物理块。比如一个多轮对话场景里100个用户的前半部分上下文完全一样传统方案要重复存100份PagedAttention只需存一份引用计数加一即可。这一下就把显存占用压下去了。我实际测试时感受最明显的是在使用同样的--gpu-memory-utilization参数下vLLM能支撑的并发序列数几乎是我手写方案的数倍。显存不再以“请求数”为单位被预占而是以实际生成的token数为单位动态分配这才是高并发的底层底气。2.2 Continuous Batching边推理边插队的调度策略传统的批量推理是静态批处理一批请求打包进入模型等所有请求全部生成完毕再释放资源接收下一批。这个模式的问题叫“长尾阻塞”——第一批里只要有一个请求生成了很长的文本其他早就生成完的请求也必须陪着等GPU空转新请求也挤不进来。Continuous Batching把这个逻辑改成了“迭代级调度”。在每个解码迭代结束后引擎都会检查当前批量内的请求状态有请求生成完了吗有请求因为触发了停止条件要退出吗如果产生了空位是否还有排队中的新请求可以立即加入这个加入动作发生在下一个迭代开始前而不是下一个批次开始前。这样带来的效果就是GPU始终在干“当前最该干的活”。生成短的请求早退早走生成长的请求继续占着资源新请求的等待时间被压缩到极短。对于高并发场景这个动态调度是QPS能上去的第二关键因素。不过也要提醒一点Continuous Batching的“动态性”意味着单个请求的生成速度可能会波动。因为每个迭代里模型实际处理的token数量是变化的某个请求可能会因为同时并行处理的请求较多而暂时变慢。这在多数服务场景下可以接受但如果业务对单请求延迟极敏感就需要通过限制并发数来换取稳定性。2.3 前缀缓存与量化继续压榨显存的两张牌PagedAttention解决的是“KV Cache怎么存”而前缀缓存解决的是“相同的KV Cache不用重复算”。在多轮对话、RAG检索问答这类场景里所有请求通常共享一大段不变的system prompt或文档上下文。如果没有前缀缓存每个新请求都要把这段公共内容重新计算一遍Key和Value白白消耗算力和显存。vLLM在开启--enable-prefix-caching之后会为已计算过的前缀块建立哈希索引。新请求进入时如果它的前缀和缓存中某个块完全一致引擎直接复用对应的KV Cache不需要重新前向计算。这种优化在小并发下可能不明显但并发请求量一大公共前缀越长收益越夸张。我的实测中固定system prompt达512token时开启前缀缓存后整体生成耗时降低了约30%。另一张牌是量化。vLLM原生支持多种量化格式常见的GPTQ、AWQ和FP8都可以直接加载。4bit量化能把7B模型的权重从约14GB压缩到约4GB省下来的显存几乎全都能留给KV Cache。可以说量化是“低成本显卡跑高并发”最有效的手段。但量化不是无代价的。4bit精度在部分任务上会有可感知的质量下降尤其对数学推理、代码生成这类对token分布敏感的任务。我的建议是优先用FP8或更高精度的量化尝试如果质量达标再考虑4bit也可以在同一模型下分别跑一遍评测集用数据决定是否启用量化。3. 部署实操从零拉起一个本地高并发推理服务3.1 硬件、软件与模型格式准备先说要跑起来需要什么。硬件方面一张NVIDIA架构的GPU是底线显存建议不低于24GB。这个容量大概能支撑7B模型FP16权重加一定并发的KV Cache。如果只有16GB显存可以配合4bit量化模型也能获得不错的并发能力。CPU和内存不能太差因为vLLM启动时要加载模型权重模型文件会先进入内存再搬运到显存内存至少要是模型文件大小的两倍。软件方面操作系统推荐Linux发行版CUDA版本要匹配所使用的推理框架要求。Python建议用3.10及以上版本并通过虚拟环境隔离依赖。如果机器上已经装了深度学习框架要注意版本兼容性我遇到过因为框架版本冲突导致vLLM编译出问题的情况后面在问题排查章节细说。模型格式方面vLLM支持直接从开源模型仓库加载也支持加载本地目录。我建议先使用transformers的通用格式因为兼容性最好。如果要上线高并发服务再考虑使用量化格式。无论哪种格式部署前都要确认模型目录结构完整包含配置文件、分词器文件和权重文件缺一个都会导致加载失败。3.2 安装vLLM与最小可用命令安装vLLM最省事的方式是使用预编译的轮子包。创建虚拟环境后直接执行安装命令即可。安装过程会自动拉取底层依赖的若干运行时库整个过程可能持续几分钟。安装完成后用命令查看版本号确认安装成功。python -m venv venv source venv/bin/activate pip install vllm python -c import vllm; print(vllm.__version__)接下来是启动服务的完整命令。这个命令会拉起一个兼容OpenAI接口的服务端监听8000端口python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --served-model-name my-chat-model \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 32 \ --enable-prefix-caching \ --port 8000启动成功后可以看到引擎初始化日志、模型加载进度以及KV Cache的分配信息。此时用curl发送一个最简单的聊天请求验证curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: my-chat-model, messages: [{role: user, content: 你好请简单介绍一下自己}], max_tokens: 128 }返回结果里会包含id、choices、usage等字段和常见的模型API格式一致。走到这一步说明服务已经通透了。3.3 核心启动参数逐一拆解上面那串启动命令里每个参数都值得仔细理解因为直接关系到并发能力和稳定性。--gpu-memory-utilization是显存利用率目标默认值是0.9。它表示vLLM会尽量使用90%的显存用于缓存和运行时。这个值不是越大越好设太高容易和图形界面或其他进程抢显存导致启动失败设太低又会压缩KV Cache的可用空间降低并发上限。我单卡跑服务时习惯设0.90卡上还有其他进程时降到0.80。--max-model-len是允许的最大上下文长度。它决定了单个请求最多能占用的KV Cache上限。这个值设得越大单个请求的内存天花板越高可容纳的并发数越低。如果业务都是短对话没必要设8192设2048或4096可以为并发腾出更多空间。反之如果业务需要处理长文档这个值就不能太低否则长请求会被直接拒绝。--max-num-seqs是单个迭代内最大同时处理的序列数。这是控制并发上限最关键的一个参数。它太高会打爆显存太低则浪费GPU算力。建议从16起步观察显存占用和延迟变化再逐步上调。32是我在24GB显存上常用的平衡值。--tensor-parallel-size是多卡并行度。当单卡显存放不下模型权重时设为2或4可以跨卡切分模型。要注意多卡并行会引入卡间通信开销小模型没必要开。我一般只在模型推理所需的显存明显超过单卡容量时才启用。--enable-prefix-caching是上面提到的前缀缓存开关多轮对话场景建议打开。--served-model-name是客户端请求时用的模型名可以自定义不必和本地目录路径完全一致。还有一个比较实用的是--host 0.0.0.0默认服务只监听本机回环地址外部机器访问不了要对外提供推理服务时需要显式绑定所有网卡接口。4. 高并发压测与调优实录4.1 最小可用的并发压测脚本服务跑起来之后要验证“高并发”不能靠肉眼看得用数据说话。我写了一个很轻量的压测脚本用并发线程池同时发送多个请求统计总耗时、生成的总token数从而算出吞吐量。import json import time from concurrent.futures import ThreadPoolExecutor import requests API_URL http://localhost:8000/v1/chat/completions MODEL_NAME my-chat-model PROMPT 请用一段话解释什么是注意力机制尽量控制在200字以内。 def send_request(_): payload { model: MODEL_NAME, messages: [{role: user, content: PROMPT}], max_tokens: 256, temperature: 0.7, } resp requests.post(API_URL, jsonpayload, timeout120) data resp.json() content data[choices][0][message][content] return len(content) concurrency 16 total_requests 64 start time.time() with ThreadPoolExecutor(max_workersconcurrency) as executor: results list(executor.map(send_request, range(total_requests))) elapsed time.time() - start total_tokens sum(results) print(f总请求数: {total_requests}, 并发: {concurrency}, 耗时: {elapsed:.2f}s) print(f生成token总数: {total_tokens}, 吞吐: {total_tokens / elapsed:.2f} tokens/s)这个脚本用requests发HTTP请求每收到一个响应就统计返回内容的字符数近似当作token数。实际token数需要更精确的统计但压测对比时这个粒度够用。我的一组典型测试数据是单张24GB显存GPU部署某开源7B对话模型FP16精度max-num-seqs32max-model-len8192max_tokens256。并发数为8时整体吞吐约1800 tokens/s并发增至16时吞吐提升到约2600 tokens/s并发增至32时吞吐约2900 tokens/s但单请求平均响应时间明显变长。这符合预期并发增加让GPU更饱和但每个请求分到的资源变少响应变慢。4.2 吞吐与延迟的跷跷板效应压测数据告诉我的第一件事是本地高并发推理追求的是“总量最大化”而不是“单个请求最快”。如果把并发从1调到32总吞吐能翻几倍但单请求延迟也会从几百毫秒涨到数秒。做服务化时必须根据业务场景在两者之间确定平衡点。有一类场景适合高吞吐配置离线批量生成、批量内容审核、批量摘要对延迟不敏感只关心单位时间内处理多少条。对这类场景可以把max-num-seqs尽量调高把gpu-memory-utilization拉到0.95让引擎尽可能多地并发处理。另一类场景则相反在线对话、实时助手用户等不了太久。此时要压低并发上限或者控制每个请求的max_tokens避免长输出占据资源太久。我的经验是在线场景下优先保证首token延迟在1秒内这就得把并发控制在显存和算力都比较舒适的区域。4.3 哪些参数对QPS影响最大按影响程度排序我实际调参的优先级是这样的。第一是显存总量和max-model-len。显存是硬约束max-model-len直接改变单请求占用上限。一个请求从128token的生成长度增加到2048tokenKV Cache占用会成倍增长能并发处理的请求数迅速下降。第二是max-num-seqs。它相当于控制“管道里同时跑多少任务”太小则显存有富余但算力利用不足太大则显存先被占满。这个值需要在显存监控下反复试找到拐点。第三是量化。从FP16切到4bit后权重显存减少了约10GB这些空间全部转化为KV Cache池直接拉高并发上限。第四是--tensor-parallel-size它只在你确实需要跨卡时才有意义。小模型强行开多卡反而因为通信开销让吞吐下降。4.4 监控显存和吞吐的手段调优不能瞎猜。我每次压测时都会另开一个终端实时盯显存占用。用nvidia-smi就能看到进程的显存使用量和利用率。vLLM在启动日志里也会打印KV Cache的预留大小比如“GPU KV cache size: 10.00GB”这个数字基本等于并发增长空间的潜力量度。更精确的监控可以打开Prometheus指标接口vLLM默认支持/metrics端点可以导出当前正在处理的请求数、平均token生成延迟、前缀缓存命中率等指标。配合开源监控面板能把服务运行状态看得清清楚楚。不过对大多数场景盯住nvidia-smi和启动日志里那两个数字已经够用。5. 常见问题与排查实录5.1 启动时报显存不足或加载失败这是我被问得最多的一类问题。报错通常发生在启动阶段提示CUDA显存不足。最常见原因是--gpu-memory-utilization设得偏高或者机器上其他进程占用了显存。排查时先看nvidia-smi确认空闲显存到底有多少再调低gpu-memory-utilization比如从0.90改成0.80或0.70。还有一个容易被忽略的点是max-model-len太大它会逼迫vLLM一次性预留大量KV Cache空间导致启动阶段就失败。另一个启动失败原因是模型格式和vLLM版本不兼容。不同vLLM版本对模型配置的解析逻辑有差异遇到解析报错时可以查版本对应关系。我的习惯是先确认安装的vLLM版本再确认模型所用框架的版本如果两者存在已知冲突升级其中一方通常能解决问题。5.2 首token延迟异常高服务能启动但客户端发出请求后要等很久才看到第一个token这很影响体验。排查顺序我建议如下。先区分是“排队等待”还是“生成缓慢”。如果请求进入服务后排在它前面的请求还有很多首token延迟自然会高。这时候调低max-num-seqs或加大显存空间才能缓解。如果不是排队问题就要检查前缀缓存是否命中。关闭前缀缓存时每个请求的公共上下文都得重新计算开启后复用缓存首token时间能大幅下降。还要检查请求里的system prompt是否完全一致只要有一丁点差异前缀就无法命中。最后检查是不是首次请求触发了模型编译和预热。服务刚启动后的第一批请求通常会明显偏慢这是正常现象。可以让服务先空跑一个短请求完成预热再进入正式流量。5.3 并发一高请求就被拒绝或断连压测时把并发调到一定值后客户端突然开始报连接错误或超时。这通常是显存或者连接数触顶了。看服务端日志会发现“请求被丢弃”或“批次已满”之类的提示。max-num-seqs是有上限的当同时处理的请求数超过它新请求只能排队。如果排队队列也被占满服务端会直接拒绝。遇到这个情况要么调大max-num-seqs前提是显存允许要么在客户端做重试和限流避免雪崩。有时候问题不在vLLM而在HTTP服务器的连接处理能力。并发太高时会触发文件描述符或端口连接数上限可以适当调整系统的文件描述符限制。压测客户端也不要每次都新建连接尽量复用连接池能显著减轻服务端压力。5.4 前缀缓存开了但感觉没生效有段时间我怀疑前缀缓存是“玄学”开了之后性能没有明显提升。后来才发现命中前缀缓存有两个前提一是请求的公共前缀必须完全一致二是缓存空间没有被其他内容挤掉。第一个前提最容易出问题。很多客户端SDK会自动给请求加上时间戳或随机ID的meta字段如果这些字段被拼进了messages数组请求前缀就永远不一致。我的做法是把system prompt放在messages数组的第一个元素并确保每次请求的system prompt字符串逐字节一致。第二个前提和显存规模有关。如果并发大且上下文长缓存块会被频繁替换命中率自然低。通过指标接口可以看到前缀缓存命中率如果低于50%就需要考虑增大KV Cache池或缩短输入长度。5.5 版本升级后性能反而下降这一点是我个人经验教训。有一次我升级了vLLM版本同一份配置下吞吐反而掉了一截。回退之后又恢复对比日志才发现新版本调整了默认调度策略并且改变了某些参数的默认值。所以现在我的流程是任何版本升级都先在压测环境跑一遍同样的基准测试对比QPS、首token延迟、显存占用三个指标确认没有回退再上生产。升级后第一件事是查看官方文档的变更日志尤其是默认参数和模型兼容性部分。6. 个人经验与收尾建议用vLLM跑本地高并发推理服务这段时间我的整体体会是这个引擎把“并发调度”和“显存管理”这些最难的事情处理得相当扎实但最终效果是否满意很大程度还是取决于业务侧的参数选择和场景定位。想给刚开始搭本地推理服务的人三条实操建议。第一先定场景再定参数。你的业务是偏实时交互还是偏批量吞吐直接决定了max-num-seqs、max-model-len和max_tokens这三者的取值。没有一种配置能同时让延迟和吞吐都最优必须根据场景做取舍。第二一定做量化评估再决定精度。4bit量化能显著提升并发但如果模型能力下降导致下游任务质量受损省下的显存没有意义。我习惯准备一份固定的评测集在FP16和不同量化级别下各跑一遍用数据说话。第三版本和依赖要锁死。vLLM迭代速度很快新版本可能带来更好的性能也可能带来默认行为的改变。生产环境尽量固定版本号并保留上一版本的部署方式方便回退。最后分享一个小技巧压测时除了看吞吐一定记录一下“目标并发下的首token延迟”。一次压测如果平均响应时间已经超过业务可接受范围那么再高的吞吐数字也只是自欺欺人。把“业务可容忍延迟”作为硬约束在约束内最大化吞吐这才是做高并发服务的正道。