共计 3498 个字符,预计需要花费 9 分钟才能阅读完成。
上个月,我把工作室局域网里挂给几台开发机跑 Continue 和 Cline 的本地代码补全后端彻底重构了一遍。起初偷懒直接拉了个 Ollama,单人写代码体感还行,但只要两个人同时触发多文件上下文补全,或者开着 Agent 进行跨项目重构,显卡直接陷入假死:首字延迟(TTFT)飙到 8 秒以上,并发请求排队卡到怀疑人生。
Ollama 适合个人本地轻度把玩,但其底层的 llama.cpp 在高并发批处理(Continuous Batching)和 PagedAttention 显存碎片管理上,面对生产级 Agent 场景确实有些力不从心。这篇记录把单卡 RTX 4090(24GB 显存)榨干到极致的实战全流程:基于 vLLM 引擎运行 Qwen2.5-Coder-32B-Instruct-AWQ,外挂 LiteLLM 做聚合流式网关,并彻底解决 SSE 流传输缓冲与显存 OOM 隐患。
一、算力显存账本与选型:为什么是 32B-AWQ?
在 24GB 显存的物理限制下,跑模型必须锱铢必较。我们的核心目标是: 代码生成能力必须达到 GPT-4o 级别可用度、上下文至少支持 16k tokens、单卡能稳定扛住 4~6 路并发 。
- 14B 全精度 /FP8: 显存绰绰有余,但面对深度重构和复杂 AST 理解,逻辑推理深度明显逊于 30B+ 梯队。
- 32B 纯 INT4/AWQ:Qwen2.5-Coder-32B-Instruct 的 AWQ 量化版模型权重实测占用约 18.5 GB。
- 显存盈余:24 GB – 18.5 GB ≈ 5.5 GB。这 5.5GB 显存就是我们调度 KV Cache 的全部底牌。
在 vLLM 中,如果不做精细化限制,加载 16k context 很容易在突发并发下直接抛出 CUDA Out of Memory。因此,精控 KV Cache 分配和开启分块预填充(Chunked Prefill)是这套架构跑通的核心技术点。
二、vLLM 生产级启动调优配置
环境基于 Ubuntu 22.04 LTS,Nvidia Driver 550.120,CUDA 12.4。我们直接使用官方 Docker 镜像,摒弃宿主机环境污染问题。
# 拉取官方优化镜像
docker pull vllm/vllm-openai:v0.7.2
# 运行 vLLM 服务容器
docker run -d --name vllm-qwen32b \
--gpus '"device=0"' \
--ipc=host \
--restart unless-stopped \
-p 8000:8000 \
-v /data/models:/models \
vllm/vllm-openai:v0.7.2 \
--model /models/Qwen2.5-Coder-32B-Instruct-AWQ \
--quantization awq \
--dtype float16 \
--max-model-len 16384 \
--gpu-memory-utilization 0.93 \
--enable-chunked-prefill \
--max-num-batched-tokens 2048 \
--max-num-seqs 16 \
--enforce-eager \
--disable-log-requests
核心参数排坑解析:
--gpu-memory-utilization 0.93:留出约 1.6GB 物理显存给 PyTorch 上下文及 CUDA Runtime,防止瞬间显存抖动直接把进程打爆挂掉。--enable-chunked-prefill: 极客必开项 。大模型在处理大段 Prompt 注入(如 Agent 扔进去上百行报错日志)时会霸占整个 GPU 算力,导致其他人的 Token 输出完全停滞。开启分块预填充后,vLLM 会把长 Prompt 拆解为多个 batch 与其他请求的 decode 过程混合执行,显著降低多并发下的 P99 延迟。--enforce-eager:在 24GB 显存极限边缘跑 32B,关闭 CUDA Graph 捕获(Eager 模式)虽然会牺牲约 5% 的纯 Decode 吞吐,但能挤出约 1.2GB 本来用于预热静态图的宝贵显存,换取更充足的 KV Cache 块。
三、构建高并发网关:LiteLLM + Nginx 流式代理
为了让 Cursor、Continue、Cline 以及内网脚本能够无缝接入,我并没有直接把 8000 端口裸奔暴露,而是中间架设了一层 LiteLLM Proxy 作为安全网关与负载中继。
1. LiteLLM 配置 (config.yaml)
model_list:
- model_name: qwen-coder
litellm_params:
model: openai/Qwen2.5-Coder-32B-Instruct-AWQ
api_base: http://127.0.0.1:8000/v1
api_key: "none"
stream_timeout: 60
general_settings:
master_key: sk-nassky-token-auth-2025
alerting: ["slack"]
router_settings:
timeout: 120
num_retries: 2
2. 踩坑重点:Nginx 反向代理 SSE 流卡死排查
配置好 LiteLLM 并通过 Nginx 对外透出域名解析时,测试遇到了非常诡异的现象:Continue 插件点击补全后,代码不是一个字一个字顺滑喷出来,而是等待十几秒后突然“啪”地一下吐出整整一大坨,流式响应彻底失效。
问题出在 Nginx 默认开启的 proxy_buffering。当输出是 Server-Sent Events (SSE) 时,Nginx 会尝试凑满一个 buffer chunk 才向下游刷新,导致用户端流式体感完全破产。修正后的核心代理配置如下:
location /v1/ {
proxy_pass http://127.0.0.1:4000/v1/; # LiteLLM 端口
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# 关键防卡顿配置:彻底关闭反代缓冲
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 延长超时,避免 Agent 思考较久时被 Nginx 掐断
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
四、实测压测数据对比:vLLM vs Ollama
在局域网内,我使用 locust 模拟了不同并发场景下的代码补全请求(Prompt 长度约 2048 tokens,生成 256 tokens),测试结果非常残酷:
| 框架 / 引擎 | 并发线程数 (Users) | 首字延迟 (TTFT Mean) | 整体系统吞吐 (Token/s) | GPU 显存利用率 |
|---|---|---|---|---|
| Ollama (Default) | 1 | 480 ms | 38.2 tk/s | ~82% (动态分配) |
| Ollama (Default) | 4 | 3850 ms | 44.5 tk/s (排队阻塞) | ~91% |
| vLLM (Chunked-Prefill) | 1 | 460 ms | 39.1 tk/s | 93% (静态池化) |
| vLLM (Chunked-Prefill) | 4 | 620 ms | 142.8 tk/s | 93% |
实测数据表明,单请求下二者差异不大,甚至由于 vLLM 关闭了 CUDA Graph,单请求还略微慢了极小的一截;但一旦并发推到 4 路,vLLM 的 Continuous Batching 机制展现了压倒性优势:吞吐直接从 44.5 tk/s 飙升至 142.8 tk/s,而平均首字时间依然死死压在 1 秒以内,完全感觉不到有人在和你抢显卡。
总结与避坑心得
把模型从玩具搬进实际生产工具链,核心从来不是“能不能跑通”,而是“高压边界下的稳定性”。回顾整个折腾过程,有三条最实在的经验总结:
- 别对 24G 显存抱有幻觉: 跑 32B 模型,AWQ 是唯一可行的解法。不要尝试给它开 32k 甚至 64k 的上下文,在单卡下 16k 是平衡 KV Cache 并发数与 OOM 概率的黄金甜点位。
- 警惕反向代理的中间件缓冲: 任何流式推理架构,只要链路中经过 Nginx、Traefik 或 Cloudflare,第一件事就是去检查
proxy_buffering,这是 90% 流式响应卡顿元凶。 - 用 LiteLLM 统一收口鉴权: 不要让下游客户端直连推理引擎端口。通过代理层加个 Master Key 和 RPM/TPM 频次截流,即使内网有人写了死循环脚本刷 API,也不至于让整块 4090 当场暴毙。
这套方案目前已经在内网稳定服役近一个月,日常抗 3-5 位工程师的重度代码生成和 Agent 调用几乎毫无压力。极客折腾的意义大概就在这里:用一张消费级显卡加一套靠谱的软件架构,把算力压榨出企业级推理服务的体验。