单卡搞定多模型混部:vLLM + LiteLLM 构建高并发企业级本地 LLM 网关实战

9次阅读
没有评论

共计 4193 个字符,预计需要花费 11 分钟才能阅读完成。

最近团队的私有化 AI 节点频频告警:业务端既要跑 Qwen2.5-7B 搞内部代码补全,又要跑一个针对客服场景微调的 Llama-3-8B,偶尔还需要调用嵌入模型(BGE-M3)做 RAG 检索。起初为了图省事,大家直接在单台配备 24GB 显存(RTX 4090)的 Linux 宿主机上拉了 Ollama,结果稍微上点并发,平均延迟(TTFT)直接飙升到 8 秒以上,流式断流甚至 OOM 崩溃频发。

排查下来原因很直接:Ollama 适合单人桌面端折腾,但面对多任务、多请求并发场景,其底层模型轮转机制极其粗暴——频繁交换显存中的权重,不仅吞吐低,还无法精细化控制每个任务的显存常驻比例。上周我花了两天时间,将整套本地推理栈重构为 vLLM(推理后端)+ LiteLLM(统一智能网关) 的架构,在单卡 24GB 上完美跑起一主一辅两个 7B/8B 模型的并发推理,并发吞吐提升了 4.2 倍,客户端接入代码一行没改。今天把整套落地配置、踩坑细节和压测数据完整梳理出来。

一、为什么放弃 Ollama,转向 vLLM + LiteLLM?

在企业级服务或多人共享的极客工作流中,本地大模型推理核心要解决三个问题: 显存复用率、KV Cache 调度效率以及统一路由认证 。

  • KV Cache 碎片化与吞吐差距 :vLLM 的核心优势在于 PagedAttention,它将显存中的 KV 缓存切成固定大小的物理页,消除了长文本生成时的显存碎片。实测在 10 并发下,同样是 Qwen-7B,vLLM 的 Token 输出吞吐能达到原生 HuggingFace 或 Ollama 的 3~4 倍。
  • 细粒度显存切分(显存常驻):通过 --gpu-memory-utilization 参数,我们可以精确为每个模型实例划定显存上限。例如为代码模型划定 0.55(约 13.2GB),为小尺寸微调对话模型划定 0.35(约 8.4GB),彻底杜绝由于模型切换引发的冷启动与卡顿。
  • 统一接入与鉴权(LiteLLM 担当反向代理):对外暴露标准的 /v1/chat/completions 接口,并提供 API Key 鉴权、速率限制(Rate Limiting)、失败重试和自动模型映射,前端应用完全无感。

二、架构设计与显存配额规划

我们以单张 24GB 显存设备为例,规划以下实例分布:

  • Port 8001 (vLLM 实例 1):Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4,负责通用代码与复杂推理,分配 50% 显存(约 12GB)。
  • Port 8002 (vLLM 实例 2):meta-llama/Llama-3.2-3B-Instruct(FP16)或 8B 4-bit 量化版本,负责轻量分类与提取,分配 35% 显存(约 8.4GB)。
  • Port 4000 (LiteLLM Proxy):轻量容器,负责负载分发、协议聚合与流量监控,仅占用不到 200MB 宿主机内存。

三、实战部署:Docker Compose 一键启动多模型集群

为了保证环境隔离,不污染宿主机 CUDA 环境,我们全部采用 Docker 容器化运行。以下是经过生产检验的 docker-compose.yml 核心配置文件:

version: '3.8'

services:
  # 实例 1:主力代码大模型
  vllm-qwen:
    image: vllm/vllm-openai:v0.6.3
    container_name: vllm-qwen
    runtime: nvidia
    restart: unless-stopped
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
    volumes:
      - /data/models/huggingface:/root/.cache/huggingface
    ports:
      - "8001:8000"
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4
      --served-model-name qwen-coder
      --gpu-memory-utilization 0.52
      --max-model-len 8192
      --trust-remote-code
      --disable-log-requests

  # 实例 2:轻量级对话 / 提取模型
  vllm-llama:
    image: vllm/vllm-openai:v0.6.3
    container_name: vllm-llama
    runtime: nvidia
    restart: unless-stopped
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
    volumes:
      - /data/models/huggingface:/root/.cache/huggingface
    ports:
      - "8002:8000"
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      --model meta-llama/Llama-3.2-3B-Instruct
      --served-model-name llama-fast
      --gpu-memory-utilization 0.38
      --max-model-len 4096
      --trust-remote-code
      --disable-log-requests

  # 统一智能网关
  litellm-proxy:
    image: ghcr.io/berriai/litellm:main-v1.49.0
    container_name: litellm-proxy
    restart: unless-stopped
    ports:
      - "4000:4000"
    volumes:
      - ./litellm-config.yaml:/app/config.yaml
    command: ["--config", "/app/config.yaml", "--port", "4000"]
    depends_on:
      - vllm-qwen
      - vllm-llama

紧接着,在同目录下编写 LiteLLM 的路由配置文件 litellm-config.yaml:

model_list:
  - model_name: gpt-3.5-turbo  # 兼容旧客户端的默认别名映射
    litellm_params:
      model: openai/qwen-coder
      api_base: http://vllm-qwen:8000/v1
      api_key: "none"

  - model_name: qwen-coder
    litellm_params:
      model: openai/qwen-coder
      api_base: http://vllm-qwen:8000/v1
      api_key: "none"

  - model_name: llama-fast
    litellm_params:
      model: openai/llama-fast
      api_base: http://vllm-llama:8000/v1
      api_key: "none"

general_settings:
  master_key: "sk-nassky-local-prod-key" # 网关统一访问密钥

litellm_settings:
  drop_params: true     # 过滤掉本地模型不支持的前端奇葩参数
  request_timeout: 120  # 避免长提示词生成超时

四、关键踩坑记录与性能调优

在实际跑通这套方案时,我在压测阶段遇到了三个典型的“深坑”,如果不加注意,系统极易崩盘:

1. 两个 vLLM 实例争抢显存导致的 CUDA OOM

很多同学直接把两个实例的 --gpu-memory-utilization 设成 0.5 + 0.5 = 1.0,启动时瞬间报错退出。 切记:CUDA Context 本身以及 PyTorch 激活值需要占用固定的静态显存(约 1GB~1.5GB)。单卡 24GB 部署双实例时,两者设定的比例之和建议严格控制在 0.88 ~ 0.90 之间,留出 2GB 左右的缓冲池给底层上下文调度。

2. ipc: host 绝对不能漏掉

在多进程高负载下,PyTorch 共享内存如果受限,容器会莫名其妙出现 RuntimeError: DataLoader worker (pid xxx) is killed by signal: Bus error 或僵死。在 Docker Compose 中必须加上 ipc: host,让容器直接使用宿主机的共享内存。

3. Context Length(上下文长度)与 KV Cache 的置换权衡

如果不需要处理几十万字的超级文档, 务必使用 --max-model-len 压制上下文最大长度 (例如代码模型限制到 8192,提取模型限制到 4096)。如果不加限制,默认模型会尝试申请 32K 甚至 128K 的 KV 预分配空间,这会迅速吃满预留显存,导致并发处理槽位(Concurrency Slots)暴跌至个位数。

五、性能验证与压测结果

服务启动后,使用 Python 编写并发压测脚本,对网关 http://localhost:4000/v1 进行 20 并发、每请求 500 Token 生成的压力测试,实测对比数据如下:

部署方案 20 并发平均吞吐 (Token/s) 首字延迟 TTFT (ms) 显存占用状态 双模型并存稳定性
原生 Ollama 调度 34.2 3,240 动态抢占(剧烈抖动) 频繁卸载模型,响应超时
vLLM + LiteLLM 混部 148.6 410 恒定 90%(稳如磐石) 双模型常驻,零冷启动

可以看到,吞吐量不仅暴涨超过 4 倍,最关键的是首字生成延迟(TTFT)从令人难以忍受的 3 秒缩减到 400 毫秒左右,代码补全与流式对话的体验有了质的飞跃。

总结与避坑心得

对于个人极客或中小研发团队,不要盲目去采购昂贵的集群多卡机器。利用单张消费级顶级显卡(3090/4090),通过 vLLM 精细显存切分 + LiteLLM 统一协议路由 的思路,完全能用工程化手段榨干硬件潜能。这种架构既保留了如同调用 OpenAI API 般的开发便利性,又在底层实现了毫秒级的响应与稳定的吞吐。下一阶段如果显存吃紧,还可以进一步引入 AWQ 4-bit 量化甚至投机采样(Speculative Decoding),进一步将推理速度推向极限。

===EOF===

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