单卡4090榨干vLLM:本地部署高并发代码大模型与流式代理踩坑记

2次阅读
没有评论

共计 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 秒以内,完全感觉不到有人在和你抢显卡。

总结与避坑心得

把模型从玩具搬进实际生产工具链,核心从来不是“能不能跑通”,而是“高压边界下的稳定性”。回顾整个折腾过程,有三条最实在的经验总结:

  1. 别对 24G 显存抱有幻觉: 跑 32B 模型,AWQ 是唯一可行的解法。不要尝试给它开 32k 甚至 64k 的上下文,在单卡下 16k 是平衡 KV Cache 并发数与 OOM 概率的黄金甜点位。
  2. 警惕反向代理的中间件缓冲: 任何流式推理架构,只要链路中经过 Nginx、Traefik 或 Cloudflare,第一件事就是去检查 proxy_buffering,这是 90% 流式响应卡顿元凶。
  3. 用 LiteLLM 统一收口鉴权: 不要让下游客户端直连推理引擎端口。通过代理层加个 Master Key 和 RPM/TPM 频次截流,即使内网有人写了死循环脚本刷 API,也不至于让整块 4090 当场暴毙。

这套方案目前已经在内网稳定服役近一个月,日常抗 3-5 位工程师的重度代码生成和 Agent 调用几乎毫无压力。极客折腾的意义大概就在这里:用一张消费级显卡加一套靠谱的软件架构,把算力压榨出企业级推理服务的体验。

正文完
 0
评论(没有评论)