在本地 V100 服务器上部署 DeepSeek-R1-Distill-Qwen-32B-AWQ:从 vLLM 到 LMDeploy 的踩坑实录

摘要:记录在一台 8 卡 Tesla V100 (SM7.0) 的共享服务器上,仅使用 2张GPU32GB显卡,为 DeepSeek-R1-Distill-Qwen-32B-AWQ 模型寻找合适推理框架的全过程。经历了官方 vLLM 的硬件门槛、1Cat-vLLM 的 glibc/CUDA 版本地狱后,最终通过 LMDeploy 的 TurboMind 引擎成功部署。


文章目录

一、背景与目标

1.1 硬件环境

通过 SSH 登录后,首先对服务器进行「体检」:

# 系统概况
$ lsb_release -a
Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

$ nvidia-smi
Driver Version: 550.54.14
CUDA Version: 12.4

$ nvcc --version
Cuda compilation tools, release 12.4, V12.4.99

在这里插入图片描述

8 张 V100-SXM2-32GB 的显存与拓扑状态:

nvidia-smi topo -m

在这里插入图片描述

拓扑要点:

  • GPU0-3 组成 NUMA 0 域,内部通过 NV1/NV2 互联(带宽 ~100-150 GB/s)
  • GPU4-7 组成 NUMA 1 域,其中 GPU5↔GPU7 为 NV2 直连(最佳),GPU4↔GPU6 也为 NV1,但 GPU6↔GPU7 仅为 PIX(单 PCIe 桥,带宽较低)
  • 跨 NUMA 域通信(如 GPU3↔GPU4)走 SYS 链路,带宽仅 ~16-32 GB/s,应避免

8 张 V100-SXM2-32GB 的显存状态如下:

GPU显存占用状态说明
0971 MiB❌ 占用中已有其他用户的 Python 进程
1-40 MiB⚠️ 空闲但未选属于 NUMA 0,与目标卡跨 NUMA
50 MiB✅ 目标显卡与 GPU7 同属 NUMA 1,NVLink 直连
60 MiB⚠️ 空闲但未选属于 NUMA 1,但与 GPU7 仅 PIX 连接
70 MiB✅ 目标显卡与 GPU5 通过 NV2 高速互联

最终选择:GPU5 + GPU7,理由:

  1. 同属 NUMA 1 域,CPU 亲和性一致(12-23,36-47)
  2. NV2 直连,Tensor Parallel 通信延迟最低
  3. 两张卡完全空闲(0 MiB),且与被占用的 GPU0 物理隔离
  4. LMDeploy 的 TurboMind 引擎在此拓扑下性能最优

磁盘方面,/ 根分区仅剩约 26GB,而模型文件已存放在 NFS 挂载的 /home/yxn/data_share/models 目录下。
在这里插入图片描述

1.2 模型选择

为什么选 DeepSeek-R1-Distill-Qwen-32B?

DeepSeek-R1-Distill-Qwen-32B 是 DeepSeek 团队用 R1 满血版的推理能力蒸馏到 Qwen2.5-32B 底座上的产物 :

  • 基座:Qwen2.5-32B(中文能力极强,C-Eval 90+)
  • 蒸馏源:DeepSeek-R1 671B 的推理链
  • 核心优势:继承了 R1 的思维链推理能力,在数学(AIME 2024: 72.6)、代码(LiveCodeBench: 57.2)、逻辑推理上表现优异
  • 定位:32B 级最强的"推理专用"开源模型,适合复杂算法分析、数学证明、逻辑推导

💡 在双 V100 环境下,32B 是"单模型可承载的最大推理模型"——更大的 70B+ 需要 4 卡以上,更小的 14B 推理能力又不够纯粹。

为什么必须是 AWQ 4-bit 量化版?

DeepSeek-R1-Distill-Qwen-32B 在 ModelScope/HuggingFace 上有多种格式:

版本磁盘大小双 V100 32GB TP=2 可行性
FP16 原版~64GB❌ 权重刚好占满 64GB,KV Cache 空间几乎为零,上下文只能压到 1024 tokens
BF16 原版~64GB❌ 同上,且 V100 不支持 BF16 原生计算
AWQ 4-bit~18-20GB✅ 双卡 TP=2 每卡约 10GB,剩余 22GB 给 KV Cache,上下文可开 8K+
GPTQ 4-bit~18-20GB⚠️ 可用,但 LMDeploy TurboMind 对 AWQ 的 kernel 优化更成熟

选择 AWQ 的核心原因:

  1. 显存账本:FP16 的 32B 模型权重约 64GB,两张 V100 32GB 加起来刚好 64GB 卡边,几乎没有 KV Cache 空间,上下文只能压到 1024 tokens 以下,实用性极低
  2. AWQ vs GPTQ:在 V100 (SM7.0) 上,LMDeploy 的 TurboMind 引擎对 AWQ 的 kernel 支持更成熟且计算效率更高,且 AWQ 的量化精度通常优于 GPTQ
  3. 双卡 TP=2 后:AWQ 权重约 18-20GB,双卡均分后每张卡约 10GB,余量充足,上下文可开到 8K+

⚠️ 关键认知:V100 上跑 32B 模型,AWQ 4-bit 不是"将就",而是"唯一合理选择"。FP16 原版在双 V100 上根本跑不起来(KV Cache 为零)。

1.3 模型下载

途径 1:ModelScope(推荐,国内速度快) ⭐

ModelScope(魔搭社区)是国内最大的开源模型仓库,DeepSeek-R1-Distill-Qwen-32B-AWQ 有多个用户提供的高速下载镜像。
在这里插入图片描述
在这里插入图片描述

Step 1:安装 ModelScope SDK

# 建议使用独立 Conda 环境(参见 1.4 约束)
conda activate vllm  # 或你的环境名
pip install modelscope -U

我这里由于是在NFS的共享盘主机上下载就没有用conda环境了,并且之前已经下载过

在这里插入图片描述

Step 2:CLI 一键下载(推荐)

# 下载到指定目录
modelscope download \
  --model Valdemardi/DeepSeek-R1-Distill-Qwen-32B-AWQ \
  --local_dir /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ

📌 模型 ID 说明:ModelScope 上 DeepSeek-R1-Distill-Qwen-32B-AWQ 有几个不同用户提供的版本:

  • Valdemardi/DeepSeek-R1-Distill-Qwen-32B-AWQ:文件完整,4 个 safetensors 分片(model-00001-of-00004 到 00004),总计约 18-20GB
  • wmywmywww/DeepSeek-R1-Distill-Qwen-32B-AWQ:另一个常用镜像

建议下载前先在 https://modelscope.cn/models 搜索确认,选择 下载量高、文件完整(4 个 safetensors 分片 + config.json + tokenizer 等) 的版本。

正在下载:
在这里插入图片描述
下载完成:
在这里插入图片描述

Step 3:Python SDK 下载(适合需要自定义逻辑的场景)

# download_model.py
from modelscope import snapshot_download

model_dir = snapshot_download(
    'wmywmywww/DeepSeek-R1-Distill-Qwen-32B-awq',
    cache_dir='/home/yxn/data_share/models',
    local_files_only=False
)
print(f"模型已下载至: {model_dir}")

运行:

python download_model.py

Step 4:校验下载完整性

# 查看下载的文件
ls -lh /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ/

# 预期文件结构:
# ├── config.json              # 模型配置
# ├── tokenizer.json           # 分词器
# ├── tokenizer_config.json
# ├── generation_config.json
# ├── model-00001-of-00004.safetensors  # ~4.9GB
# ├── model-00002-of-00004.safetensors  # ~5.0GB
# ├── model-00003-of-00004.safetensors  # ~5.0GB
# ├── model-00004-of-00004.safetensors  # ~4.4GB
# └── model.safetensors.index.json

# 校验总大小(应约 18-20GB)
du -sh /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ/

在这里插入图片描述

✅ 完整性检查要点:

  • 4 个 .safetensors 分片齐全
  • config.json 中存在 "quantization_config" 字段,且 "quant_method": "awq"
  • 无 .incomplete 后缀的临时文件
途径 2:HuggingFace(备选)

如果 ModelScope 下载不稳定,可以从 HuggingFace 获取:

# 需配置 HF 镜像或代理
export HF_ENDPOINT=https://hf-mirror.com  # 国内镜像

pip install huggingface_hub
huggingface-cli download Valdemardi/DeepSeek-R1-Distill-Qwen-32B-AWQ \
  --local-dir /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ
途径 3:Git LFS 直接克隆
# 不推荐(速度慢、容易中断),仅作了解
git lfs install
git clone https://www.modelscope.cn/wmywmywww/DeepSeek-R1-Distill-Qwen-32B-awq.git \
  /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ
💡 小技巧:使用 tmux 或 nohup 保活下载会话

虽然 ModelScope 的 modelscope download 支持断点续传,但 SSH 连接可能因网络波动、客户端休眠或超时突然断开,导致下载进程被意外杀死。强烈建议在下载前启用保活机制,以下两种方式任选其一:

方式一:nohup(简单粗暴)

nohup 能让命令在退出终端后继续运行,适合一次性下载任务。

nohup modelscope download \
  --model wmywmywww/DeepSeek-R1-Distill-Qwen-32B-awq \
  --local_dir /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ \
  > /home/yxn/models/download.log 2>&1 &
  • 输出重定向到 download.log,随时 tail -f download.log 查看进度
  • 进程 PID 会打印在终端,如需终止:kill <PID>
方式二:tmux(更灵活,推荐)

tmux 是一个终端复用器,能创建持久化会话,即使 SSH 断开,会话中的进程仍在运行。下次登录后 tmux attach 即可恢复,配合断点续传,双重保险。

# 创建名为 download 的新会话
tmux new -s download

# 在会话中执行下载命令
modelscope download \
  --model wmywmywww/DeepSeek-R1-Distill-Qwen-32B-awq \
  --local_dir /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ

# 按 Ctrl+B 然后按 D 脱离会话(detach),进程继续运行
# 重新连接
tmux attach -t download

常用命令速查:

  • tmux ls – 列出所有会话
  • tmux kill-session -t download – 结束会话
  • Ctrl+B D – 脱离当前会话

💡 推荐组合:先 tmux new -s download 进入会话,再在会话中执行下载命令。这样即使网络中断,重连后 tmux attach -t download 即可看到实时进度,配合 ModelScope 的断点续传,几乎不会丢失任何进度。


1.4 约束条件

本次部署面临以下硬约束(这些约束直接决定了后续的选型决策):

  • GPU 隔离:只能使用 GPU5 和 GPU7(通过 CUDA_VISIBLE_DEVICES=5,7 隔离),绝对不能影响其他用户(GPU0 已被占用 971 MiB),也不能污染系统 Python 环境
  • 系统不可变:不能升级系统 glibc(当前 2.31),不能升级 NVIDIA 驱动(550.54.14),不能动系统 CUDA 12.4
  • 磁盘紧张:根分区 / 已用 86%(147G/181G),所有大文件操作必须落在 NFS(/home/yxn/data_share)上
  • 硬件限制:V100 (SM7.0) 不支持 BF16 原生计算,且官方 vLLM 的 AWQ 内核要求 SM75+

⚠️ 这些约束是整个部署方案的"边界条件"——后面的框架选型(LMDeploy)、量化格式(AWQ)、GPU 选择(GPU5+GPU7)都是在这些约束下的最优解。


二、方案选型:为什么 LMDeploy 是最终赢家

2.1 本地部署大模型的常见方式

在动手之前,有必要先理清当前本地大模型部署的几种主流路线。理解这些路线的定位,才能明白为什么在受限环境下某些方案走不通。

路线一:轻量级本地推理(Ollama / llama.cpp)

代表工具:Ollama、llama.cpp

特点:

  • 一键下载运行,对系统依赖极少
  • llama.cpp 通过 GGUF 格式 + 量化,能在 CPU/老 GPU 上跑起来
  • 提供简易的本地 OpenAI 兼容接口
  • 定位:个人开发者快速实验、本地知识库 Demo 原型

局限性:

  • 并发性能Ollama is suitable for single-user invocation; it may lag under concurrent requests from a team.发会卡顿**)
  • 生产环境服务能力有限

💡 适用场景:如果你只是想在树莓派或个人笔记本上跑个 7B 模型聊天,Ollama 是首选。但在双 V100 服务器上跑 32B 生产级服务,它不是最优解。

路线二:高性能服务框架(vLLM / SGLang / TensorRT-LLM)

代表工具:vLLM(社区生态最成熟)、SGLang(结构化输出强)、TensorRT-LLM(英伟达原厂,性能最强但最复杂)

特点:

  • 高吞吐、高并发,PagedAttention 等显存优化技术
  • 支持 AWQ/GPTQ/FP8 等多种量化格式
  • 提供 OpenAI 兼容 API,易于集成
  • 定位:中小团队对内/对外稳定 API 服务,普通 GPU 日常并发几十至几百 QPS

局限性:

  • 对硬件有门槛:vLLM 的 AWQ 内核需要 SM75+(Turing 架构及以上)
  • V100 (SM70) 在官方 vLLM 上直接不支持 AWQ 量化

⚠️ 关键认知:vLLM 是当前最热门的推理框架,但不是万能的。老卡 V100 在官方 vLLM 上跑 AWQ 会直接被拒之门——这是硬件架构代差决定的。

路线三:量化与部署一体化框架(LMDeploy)

代表工具:LMDeploy(上海人工智能实验室开发)

特点:

  • TurboMind 引擎原生支持 V100 (sm70) 的 AWQ/GPTQ INT4 推理
  • 对 CUDA 版本要求宽松(CUDA 11.3+,你的 12.4 完美兼容)
  • 对 glibc 要求低(2.31 即 Ubuntu 20.04 原生版本)
  • 一键量化导出 + 一键服务部署
  • 定位:硬件资源有限、大量使用量化、国产化服务器、多模态图文业务

优势:在 V100 这种老卡上,LMDeploy 是官方明确支持 SM70 的框架之一。

路线四:社区 Fork / 定制内核(1Cat-vLLM 等)

代表工具:1Cat-vLLM(vLLM 的 V100 专用 fork)

特点:

  • 通过集成 LMDeploy TurboMind 的 SM70 WMMA 内核,让 V100 也能跑 AWQ
  • 在 4×V100 等环境上验证了 Qwen3.5 27B/35B 的部署
  • 定位:高并发生产环境,且愿意折腾系统栈的用户

代价:

  • 预编译 wheel 要求 glibc 2.32+、CUDA 12.8+
  • 源码编译需要 torch 2.10.0+cu128(nightly 版本)
  • 运行时镜像锁定 Python 3.12 + glibc 2.41

⚠️ 关键认知:1Cat-vLLM 在"允许升级系统"的前提下是 V100 上跑 AWQ 的优秀方案,但代价是必须动系统栈。

2.2 四种路线的对比

路线代表工具V100 AWQ系统要求易用性生产并发
轻量级推理Ollama / llama.cpp✅ GGUF 可行极低⭐⭐⭐⭐⭐⭐⭐
高性能服务vLLM❌ SM75+ 门槛中⭐⭐⭐⭐⭐⭐⭐⭐⭐
量化部署一体LMDeploy✅ 原生 SM70低(glibc 2.31+)⭐⭐⭐⭐⭐⭐⭐⭐⭐
社区 Fork1Cat-vLLM✅ 已解锁 SM70高(glibc 2.32+/CUDA 12.8+)⭐⭐⭐⭐⭐⭐⭐⭐

2.3 回到我们的环境约束

本次部署面临三个硬约束(详见 1.4 节):

  • 系统不可变:glibc 2.31 不能升、CUDA 12.4 不能动、驱动 550 不能升级
  • GPU 隔离:只能用 GPU5 + GPU7
  • 磁盘紧张:根分区 86% 占用

把这些约束叠加到上面的路线选择上:

路线筛选过程:
│
├─ 路线一(Ollama/llama.cpp)
│   └─ ❌ 排除:llama.cpp 在 V100 上跑 GGUF 可行,
│        但我们选的是 AWQ 格式(HuggingFace 权重),不是 GGUF
│        且 Ollama 对 AWQ safetensors 的直接服务能力弱
│
├─ 路线二(官方 vLLM)
│   └─ ❌ 排除:AWQ 在 Volta 上 ❌(官方硬件兼容表明确)
│        启动即报 "AWQ is not supported on Volta"
│
├─ 路线四(1Cat-vLLM)
│   └─ ⚠️ 理论可行,但本环境走不通:
│       ├─ 预编译 wheel 要求 glibc 2.32+,我们是 2.31 ❌
│       ├─ 要求 CUDA 12.8+,我们是 12.4 ❌
│       ├─ 要源码编译需 torch 2.10.0+cu128(不存在稳定版)❌
│       └─ 在共享服务器上升级系统 glibc/CUDA 风险极高 ❌
│
└─ 路线三(LMDeploy)✅ 唯一可行
    ├─ TurboMind 原生支持 V100 SM70 的 AWQ 推理
    ├─ glibc 2.31 满足要求 ✅
    ├─ CUDA 12.4 满足要求(11.3+ 即可)✅
    ├─ 无需升级任何系统组件 ✅
    └─ pip install 一键安装,无编译 ❌→✅ 无编译=无痛苦

2.4 最终对比表

维度官方 vLLM1Cat-vLLM(社区 fork)LMDeploy
V100 AWQ 支持❌ SM75+ 门槛,直接拒绝 SM70✅ 已解锁 SM70✅ 原生 TurboMind 支持 SM70
对 CUDA 版本要求12.4+12.8+(硬门槛)11.3+(12.4 完美兼容)
对 glibc 要求2.31 即可2.32+(预编译 wheel 门槛)2.31(Ubuntu 20.04 原生)
额外编译无需源码编译 flash_attn_v100无需任何编译
torch 版本要求稳定版即可要求 torch 2.10.0+cu128(nightly)稳定版 PyTorch 即可
安装复杂度pip 安装需解决 ABI + CUDA + torch 三重版本地狱pip install 一键搞定
OpenAI API 兼容✅✅✅

2.5 结论

对于「Ubuntu 20.04 + Driver 550 + glibc 2.31 + CUDA 12.4」这套老环境,LMDeploy 是唯一不需要任何 workaround 即可跑通 AWQ 的方案。

需要说明的是,其他路线并非"不好",而是在"不能动系统环境"这个约束下走不通:

  • 官方 vLLM:如果你的卡是 T4/A100(SM75+),vLLM 是最热门的选择,生态成熟、社区资料丰富。
  • 1Cat-vLLM:如果你能升级系统到 Ubuntu 22.04+(glibc 2.35+)并安装 CUDA 12.8,它是 V100 上跑 AWQ 的高并发首选。但在共享服务器上,升级 glibc 等于"谋杀"所有用户的运行环境,绝不能动。
  • Ollama:如果你是个人开发者,只想快速跑个模型聊天,Ollama 一键拉起体验极佳。但我们要的是双 V100 上的生产级服务,Ollama 的并发能力偏弱。

LMDeploy 赢在"恰好匹配":

  1. ✅ 原生支持 V100 的 AWQ(其他主流框架要么不支持,要么要折腾)
  2. ✅ 对系统版本要求宽松(glibc 2.31 + CUDA 12.4 完全满足)
  3. ✅ 安装零编译(pip install 后直接能用)
  4. ✅ 性能优秀(TurboMind 引擎针对量化模型深度优化)
  5. ✅ OpenAI 兼容 API(客户端无需改造)

这就是为什么在"老系统 + 老显卡 + 不能动环境"这个特定场景下,LMDeploy 是唯一的低摩擦赢家。


三、部署环境准备

3.1 环境概览

在动手之前,先确认你的服务器环境是否与本教程一致。我的环境如下:

# 操作系统
$ cat /etc/os-release
Ubuntu 20.04.6 LTS (Focal Fossa)

# 内核版本
$ uname -r
5.4.0-216-generic

# glibc 版本(关键!)
$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.31-0ubuntu9.16) 2.31

# NVIDIA 驱动与 CUDA
$ nvidia-smi
NVIDIA-SMI 550.54.14    Driver Version: 550.54.14    CUDA Version: 12.4

# GPU 型号
$ nvidia-smi --query-gpu=name --format=csv,noheader | head -1
Tesla V100-SXM2-32GB

本教程所有操作均基于上述环境。如果你的系统版本、glibc、CUDA 或驱动版本与此不同,请自行调整部分步骤(例如 Conda 安装包的 Python 版本)。

3.2 检查磁盘空间

(⚠️ 我在这里差点翻车)

$ df -h /
Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv  181G  171G  778M 100% /

根分区只剩 778MB!如果你也遇到类似情况,务必先清理:

# 清理 pip 缓存(通常能释放数 GB)
pip cache purge

# 清理 conda 缓存
conda clean --all -y

# 删除之前下载的 CUDA 安装包和临时文件
rm -f ~/models/*.run
rm -rf ~/.local/share/Trash/*
rm -rf /tmp/pip-modern-metadata-*

💡 教训:我前期在安装 LMDeploy 前没有检查磁盘,导致 pip install 中途因空间不足而失败。后来才发现 / 只剩 778MB,而 pip 缓存就占了 4GB。任何安装操作前,务必先看磁盘。

3.3 规划目录结构

根据现在的内容和使用场景(V100 服务器、模型推理部署、共享数据),我们这样规划目录:

~/data_share/
├── models/                          # 只放模型权重
│   ├── DeepSeek-R1-Distill-Qwen-32B-AWQ/
│   └── Qwen3-30B-A3B-Instruct-2507/
│
├── tools/                           # 工具源码
│   └── llama.cpp/
│
├── pkgs/                            # 安装包 / 源码包
│   ├── flash-attention-v100/
│   └── cmake-3.28/
│
├── software/                        # 已安装软件
│   └── miniconda3/
│
├── scripts/                         # 只放脚本
│   ├── start_lmdeploy.sh
│   └── stop_lmdeploy.sh
│
├── cache/                           # 缓存
│   ├── conda/
│   └── pip/
│
├── logs/                            # 日志
│   └── lmdeploy_awq.log
│
├── nginx/                           # nginx 配置
│   ├── sites-available/
│   └── ssl/
│
├── envs/                            # conda 环境导出文件
│
└── README.md

创建目录

cd ~/data_share

# 1. 创建新目录
mkdir -p pkgs scripts envs software tools cache logs

# 2. 写个 README
cat > README.md << 'EOF'
# data_share 目录说明

| 目录 | 用途 |
|------|------|
| models/ | 模型权重(HuggingFace / AWQ / GGUF)|
| tools/ | 工具源码(llama.cpp 等)|
| pkgs/ | 安装包、whl、源码包 |
| software/ | 已安装软件(conda 等)|
| scripts/ | 启动/停止/管理脚本 |
| cache/ | conda/pip 缓存 |
| logs/ | 服务日志 |
| nginx/ | nginx 站点配置 |
| envs/ | conda 环境导出文件(*.yaml)|
EOF

为什么这样分

目录放什么不放什么
models/模型权重(safetensors、gguf 等)工具源码、缓存、日志
tools/llama.cpp 等工具源码模型文件
pkgs/whl、源码包、编译工具安装后的软件
software/已安装的软件(conda)安装包
scripts/启动/停止脚本安装包
cache/conda/pip 缓存日志
logs/服务日志模型
nginx/站点配置、SSL 证书其他配置

额外建议:如果你这台机器也是是多人在用,考虑在 models/ 下按模型名再细分,比如 models/Qwen2-72B/、models/DeepSeek-R1/,避免文件混在一起。

3.4 安装 Miniconda(指定目录)

由于根分区紧张,我们将 Miniconda 安装到共享盘的 software 目录下。

3.4.1 下载 Miniconda 安装脚本
cd /home/yxn/data_share/software
wget https://repo.anaconda.com/miniconda/Miniconda3-py39_23.5.2-0-Linux-x86_64.sh

如果你已经提前下载好了安装脚本(如 Miniconda3-py39_23.5.2-0-Linux-x86_64.sh),直接使用即可。

3.4.2 静默安装到指定目录
bash Miniconda3-py39_23.5.2-0-Linux-x86_64.sh -b -p /home/yxn/data_share/software/miniconda3

参数说明:

  • -b:静默模式,不交互,跳过 License 协议确认:不需要你手动翻页阅读并输入 yes,跳过所有交互式提问:全程自动,无需人工干预。
  • -p:指定安装路径

安装完成后,初始化 Conda(使其在终端中可用):

/home/yxn/data_share/software/miniconda3/bin/conda init bash
source ~/.bashrc

验证安装:

$ conda --version
# conda 23.5.2

在这里插入图片描述

✅ 至此,Conda 已安装在共享盘上,不占用根分区空间。

3.5 配置 Conda 和 pip 源(国内加速)

为了加快后续软件包下载速度,建议配置国内镜像源。这里以清华大学 TUNA 源为例(阿里云、中科大源同理)。

3.5.1 配置 Conda 源
# 添加清华源(按优先级排列)
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/

# 设置搜索时显示通道地址
conda config --set show_channel_urls yes

验证配置:

$ conda config --show channels

在这里插入图片描述

3.5.2 配置 pip 源
# 临时使用(仅当前命令生效)
pip install lmdeploy -i https://pypi.tuna.tsinghua.edu.cn/simple

# 永久配置(推荐)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn

验证配置:

$ pip config list

在这里插入图片描述

💡 为什么配置源? 默认的 Conda/PyPI 服务器在国外,国内下载速度可能只有几十 KB/s,配置镜像源后可提升到 MB/s 级别,尤其适合下载 20GB 的模型文件和大型 whl 包。

3.5 创建 Conda 环境(指定到共享盘)

我们将环境创建在共享盘的 envs 目录下,与模型文件同盘,方便管理。

# 创建环境(Python 3.12)
conda create -p /home/yxn/data_share/envs/lmd python=3.12 -y

# 激活环境
conda activate /home/yxn/data_share/envs/lmd

在这里插入图片描述

⚠️ 注意:如果 NFS 网络延迟较高,首次 pip install 可能会稍慢,但后续模型加载到 GPU 后,推理性能不受影响。你也可以选择放在本地 /opt/conda/envs/ 下,前提是磁盘空间充足。

3.6 安装 LMDeploy

在刚激活的环境中,直接安装 LMDeploy:

pip install lmdeploy --no-cache-dir

安装完成后验证:

python -c "import lmdeploy; print(lmdeploy.__version__)"

在这里插入图片描述

3.7 环境准备总结

组件路径说明
Miniconda/home/yxn/data_share/software/miniconda3安装在共享盘,不占根分区
Conda 环境/home/yxn/data_share/envs/lmdPython 3.12,与模型文件同盘
LMDeploy环境内 pip 安装版本 0.6.4,无额外依赖
模型文件/home/yxn/data_share/models/将在下一章下载

后续所有操作都在此 Conda 环境中进行,每次登录后记得:

conda activate /home/yxn/data_share/envs/lmd

🎉 至此,环境准备完成,没有任何 glibc 或 CUDA 版本冲突。 对比之前折腾 1Cat-vLLM 的经历,LMDeploy 的一键安装简直是天堂。


四、启动本地大模型服务

4.1 GPU 隔离:CUDA_VISIBLE_DEVICES 的正确用法

在共享服务器上,绝对不能让框架自己选 GPU。必须通过环境变量强制隔离:

export CUDA_VISIBLE_DEVICES=5,7
export PYTHONNOUSERSITE=1
  • CUDA_VISIBLE_DEVICES=5,7:只暴露 GPU 5 和 7,进程完全看不到其他卡。
  • PYTHONNOUSERSITE=1:防止 Python 导入用户主目录下的包,避免污染。

💡 为什么选 GPU 5 和 7? 因为前面根据 nvidia-smi topo -m 拓扑分析:GPU5 与 GPU7 同属 NUMA 1 域,且通过 NV2(NVLink 2 桥接) 直连,带宽约 150 GB/s,是双卡 Tensor Parallel 的最优组合。同时这两张卡完全空闲,与被占用的 GPU0 物理隔离,不会影响其他用户。

4.2 启动命令与参数详解

在激活的 Conda 环境中执行:

lmdeploy serve api_server \
    /home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ \
    --model-format awq \
    --tp 2 \
    --dtype float16 \
    --session-len 8192 \
    --cache-max-entry-count 0.8 \
    --model-name deepseek-r1-distill-qwen-32b-awq \
    --server-name 0.0.0.0 \
    --server-port 60080

参数详解:

参数值说明
--model-formatawq必须,声明模型为 AWQ 4-bit 量化格式。TurboMind 引擎对 AWQ 的支持在 V100 (sm70) 上原生可用。
--tp2Tensor Parallel=2,两张卡并行。必须与 CUDA_VISIBLE_DEVICES 暴露的卡数一致。
--dtypefloat16V100 不支持 BF16,必须显式指定 FP16。
--session-len8192最大上下文长度。AWQ 4-bit 下双卡显存充裕,8192 是安全起点,稳定后可尝试 16384。
--cache-max-entry-count0.8KV Cache 最多占用单卡显存的 80%,留 20% 余量防 OOM。
--model-name自定义OpenAI API 兼容的模型名,客户端调用时使用。
--server-port60080避开常用的 23333/8000 端口,防止与其他用户冲突。

⚠️ 关键点:DeepSeek-R1-Distill-Qwen-32B 作为 320 亿参数模型,FP16 权重约 64GB。在双 V100 32GB 上必须使用 AWQ 4-bit 量化 + --tp 2,否则单卡显存远远不够。

4.3 启动与验证

执行上述命令后,等待模型加载。首次启动时,模型权重从 NFS 读取到 GPU 可能需要 2-5 分钟,日志会停留在类似 Loading weights 的阶段,请耐心等待。

当看到以下输出,即表示服务启动成功:

如果要结束进程:
①还在原来的终端里 → 直接按 Ctrl+C
②已经离开终端 / 放到后台了:

pkill -f "lmdeploy serve api_server"
# 找到占用 60080 端口的进程 PID
lsof -i :60080

# 杀掉
kill -9 <PID>
验证一:显存占用

另开一个终端,执行:

nvidia-smi

关注 GPU 5 和 7 的显存占用:
在这里插入图片描述

结果:

  • GPU 5 和 7 各占用约 28.6 GB / 32 GB
  • 剩余约 3.4 GB 余量,足够稳定推理
  • 其他 GPU(0-4, 6)显存占用为 0,未受影响
验证二:API 测试(curl)
curl http://localhost:60080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-r1-distill-qwen-32b-awq",
    "messages": [{"role": "user", "content": "你好,请解释量子纠缠"}],
    "temperature": 0.7,
    "max_tokens": 512
  }'

返回结果示例:
在这里插入图片描述

💡 注意:DeepSeek-R1-Distill 模型会输出带 嗯、所以 等口语化标记的思考过程,这是 R1 蒸馏模型的典型特征,表明推理链正常工作。

验证三:API 测试(Python 脚本)

更贴近实际调用的 Python 测试脚本:

vim ~/data_share/scripts/test_lmdeploy.py
import requests
import time

url = "http://localhost:60080/v1/chat/completions"
headers = {"Content-Type": "application/json"}

data = {
    "model": "deepseek-r1-distill-qwen-32b-awq",
    "messages": [
        {"role": "user", "content": "用 Python 实现快速排序,并解释算法复杂度"}
    ],
    "temperature": 0.7,
    "max_tokens": 1024
}

print("发送请求中...")
start_time = time.time()

response = requests.post(url, headers=headers, json=data)
result = response.json()

elapsed = time.time() - start_time
print(f"首响延迟: {elapsed:.2f}s")
print(f"消耗 Token: {result['usage']['total_tokens']}")
print(f"\n模型回复:\n{result['choices'][0]['message']['content']}")

运行:

python ~/data_share/scripts/test_lmdeploy.py

输出结果示例:
在这里插入图片描述

验证四:OpenAI SDK 兼容测试

LMDeploy 的 API 与 OpenAI 完全兼容,可以用任何 OpenAI 客户端库调用:

vim ~/data_share/scripts/test_openai_sdk.py
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:60080/v1",
    api_key="none"  # LMDeploy 默认不需要 key,或随意填写
)

response = client.chat.completions.create(
    model="deepseek-r1-distill-qwen-32b-awq",
    messages=[
        {"role": "user", "content": "1+1 等于几?请一步步思考。"}
    ],
    temperature=0.7,
    max_tokens=512
)

print(response.choices[0].message.content)

运行:

pip install openai
python ~/data_share/scripts/test_openai_sdk.py

输出结果示例:
在这里插入图片描述

验证五:服务健康检查
# 查看模型列表
curl http://localhost:60080/v1/models

预期返回:{“object”:“list”,“data”:[{“id”:“deepseek-r1-distill-qwen-32b-awq”,“object”:“model”,“owned_by”:“lmdeploy”}]}
在这里插入图片描述

4.4 常见问题排查

现象可能原因解决方案
CUDA out of memoryKV Cache 占用过高降低 --cache-max-entry-count 到 0.7 或 0.6
gemm_config.in is not found 警告 + 崩溃未指定 --tp 或 tp 数不对确保 --tp 2 且与 CUDA_VISIBLE_DEVICES 卡数一致
启动后卡在 Loading weightsNFS 读取慢,正常现象耐心等待,双 V100 上 20GB 模型加载约 2-5 分钟
推理速度极慢(<5 tokens/s)GPU 拓扑不佳或 PCIe 瓶颈确认 GPU5+GPU7 是 NVLink 直连(NV2)
API 返回 404--model-name 与请求中的 model 不一致统一两端模型名
中文乱码或输出异常tokenizer 配置问题确认模型目录包含完整的 tokenizer.json 和 config.json

4.5 服务验证总结

当以下所有条件都满足时,说明服务部署成功:

  • ✅ nvidia-smi 显示 GPU5 和 GPU7 各占用约 28.6 GB
  • ✅ 其他 GPU 未被占用
  • ✅ curl 请求返回合法的 OpenAI 格式 JSON
  • ✅ 模型能输出带思考过程的连贯回答
  • ✅ 首响延迟在 8-15 秒之间(V100 双卡 AWQ 的正常水平)
  • ✅ 生成速度约 30-45 tokens/s

🎉 至此,DeepSeek-R1-Distill-Qwen-32B-AWQ 已在双 V100 上通过 LMDeploy 成功部署并验证!


生产化脚本配置

5.1 一键启动脚本

将以下内容保存为 start_lmdeploy.sh:

#!/bin/bash
# ============================================
# LMDeploy 启动脚本 - DeepSeek-R1-Distill-Qwen-32B-AWQ
# GPU: 5,7 (TP=2) | 路径基于 data_share 目录结构
# ============================================

set -e

# ---------- 配置区(按需修改)----------
CONDA_ENV_PATH="/home/yxn/data_share/envs/lmd"
MODEL_PATH="/home/yxn/data_share/models/DeepSeek-R1-Distill-Qwen-32B-AWQ"
PORT=60080
GPUS="5,7"
LOG_DIR="/home/yxn/data_share/logs"
LOG_FILE="$LOG_DIR/lmdeploy_awq.log"
PID_FILE="$LOG_DIR/lmdeploy_awq.pid"

# LMDeploy 参数
SESSION_LEN=65536          # 必须 > 8192,否则 warm-up 失败;V100 显存紧张时可降为 8448
CACHE_ENTRY_COUNT=0.8      # KV Cache 占用比例;OOM 时降到 0.5
MAX_PREFILL=8192
MAX_BATCH=4
API_KEY="sk-bcadaf67d3b046eabf82b8c4ec21f758"
# --------------------------------------

# 检查是否已有进程在运行
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
    echo "[ERROR] 服务已在运行 (PID $(cat $PID_FILE))"
    echo "[INFO] 如需重启,请先运行: bash $(dirname "$0")/stop_lmdeploy.sh"
    exit 1
fi

# 初始化 conda
eval "$(conda shell.bash hook 2>/dev/null || true)"
if ! command -v conda &> /dev/null; then
    echo "[ERROR] conda 未安装或不在 PATH 中"
    exit 1
fi

# 激活环境
echo "[INFO] 激活 conda 环境: $CONDA_ENV_PATH"
conda activate "$CONDA_ENV_PATH" 2>/dev/null || {
    echo "[WARN] conda activate 失败,尝试 source activate..."
    source activate "$CONDA_ENV_PATH" 2>/dev/null || {
        echo "[ERROR] 无法激活环境,请检查路径: $CONDA_ENV_PATH"
        exit 1
    }
}

# 设置环境变量
export CUDA_VISIBLE_DEVICES="$GPUS"
export PYTHONNOUSERSITE=1
export NCCL_WIN_ENABLE=0
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

echo "[INFO] CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES"
echo "[INFO] NCCL_WIN_ENABLE=$NCCL_WIN_ENABLE"

# 检查模型路径
if [ ! -d "$MODEL_PATH" ]; then
    echo "[ERROR] 模型路径不存在: $MODEL_PATH"
    exit 1
fi
if [ ! -f "$MODEL_PATH/config.json" ]; then
    echo "[ERROR] 模型目录缺少 config.json,请确认路径正确"
    exit 1
fi

# 创建日志目录
mkdir -p "$LOG_DIR"

# 如果日志文件已存在,备份旧日志
if [ -f "$LOG_FILE" ]; then
    BACKUP="$LOG_DIR/lmdeploy_awq_$(date +%Y%m%d_%H%M%S).log"
    mv "$LOG_FILE" "$BACKUP"
    echo "[INFO] 旧日志已备份: $BACKUP"
fi

echo "[INFO] 启动 LMDeploy 服务..."
echo "[INFO] 模型: $MODEL_PATH"
echo "[INFO] 日志: $LOG_FILE"
echo "[INFO] 端口: $PORT"

nohup lmdeploy serve api_server \
  "$MODEL_PATH" \
  --model-format awq \
  --tp 2 \
  --dtype float16 \
  --quant-policy 8 \
  --session-len $SESSION_LEN \
  --cache-max-entry-count $CACHE_ENTRY_COUNT \
  --max-prefill-token-num $MAX_PREFILL \
  --enable-prefix-caching \
  --reasoning-parser deepseek-r1 \
  --max-batch-size $MAX_BATCH \
  --model-name deepseek-r1-distill-qwen-32b-awq \
  --server-name 0.0.0.0 \
  --server-port "$PORT" \
  # --api-keys "$API_KEY" \
  > "$LOG_FILE" 2>&1 &

PID=$!
echo $PID > "$PID_FILE"

echo ""
echo "[INFO] ============================="
echo "[INFO] 服务已启动 (PID: $PID)"
echo "[INFO] API 地址: http://localhost:$PORT"
echo "[INFO] 日志文件: $LOG_FILE"
echo "[INFO] ============================="
echo ""
echo "查看日志: tail -f $LOG_FILE"
echo "停止服务: bash $(dirname "$0")/stop_lmdeploy.sh"
echo "测试服务: curl -H 'Authorization: Bearer $API_KEY' http://localhost:$PORT/v1/models"

使用方式:

# 赋予执行权限
chmod +x /home/yxn/data_share/scripts/start_lmdeploy.sh

# 运行
bash /home/yxn/data_share/scripts/start_lmdeploy.sh

💡 脚本说明:

  • 所有路径都基于你实际的共享盘结构(/home/yxn/data_share/)
  • 日志自动保存在 logs/ 目录下,按日期命名,方便回溯
  • 启动前会检查模型路径和 config.json 是否存在,避免启动后才发现路径错误
  • 使用完整的 conda.sh 路径激活环境,不依赖 ~/.bashrc,更健壮

在这里插入图片描述


5.2 停止脚本

将以下内容保存为 stop_lmdeploy.sh:

#!/bin/bash
# ============================================
# LMDeploy 停止脚本
# ============================================

LOG_DIR="/home/yxn/data_share/logs"
PID_FILE="$LOG_DIR/lmdeploy_awq.pid"
LOG_FILE="$LOG_DIR/lmdeploy_awq.log"

# 查找 PID
if [ ! -f "$PID_FILE" ]; then
    echo "[WARN] PID 文件不存在,尝试通过进程名查找..."
    PID=$(pgrep -f "lmdeploy serve api_server.*DeepSeek-R1-Distill-Qwen-32B-AWQ" | head -1)
    if [ -z "$PID" ]; then
        echo "[INFO] 未找到运行中的 LMDeploy 服务"
        exit 0
    fi
    echo "[INFO] 找到进程 PID: $PID"
else
    PID=$(cat "$PID_FILE")
    if ! kill -0 "$PID" 2>/dev/null; then
        echo "[WARN] PID $PID 已失效,清理 PID 文件"
        rm -f "$PID_FILE"
        exit 0
    fi
fi

echo "[INFO] 正在停止服务 (PID: $PID)..."

# 先尝试优雅终止
kill "$PID" 2>/dev/null

# 等待最多 10 秒
for i in {1..10}; do
    if ! kill -0 "$PID" 2>/dev/null; then
        echo "[INFO] 服务已正常退出 (耗时 ${i}s)"
        break
    fi
    sleep 1
done

# 如果还在,强制终止
if kill -0 "$PID" 2>/dev/null; then
    echo "[WARN] 进程未响应,执行强制终止 (kill -9)..."
    kill -9 "$PID" 2>/dev/null
    sleep 1
    echo "[INFO] 已强制终止"
fi

rm -f "$PID_FILE"
echo "[INFO] PID 文件已清理"

# 显示 GPU 状态
echo ""
echo "[INFO] 当前 GPU 显存状态:"
nvidia-smi --query-gpu=index,name,memory.used,memory.total --format=csv,noheader | grep -E "^[57],"

使用方式:

# 赋予执行权限
chmod +x /home/yxn/data_share/scripts/stop_lmdeploy.sh

# 执行脚本
bash ~/data_share/scripts/stop_lmdeploy.sh

在这里插入图片描述

5.3 日志与监控

# 实时查看最新日志
tail -f /home/yxn/data_share/logs/lmdeploy_*.log

# 查看最近 50 行日志
tail -50 /home/yxn/data_share/logs/lmdeploy_*.log

# 查看服务进程
ps aux | grep lmdeploy | grep -v grep

# 查看 GPU 实时状态(每秒刷新)
watch -n 1 nvidia-smi

# 仅查看 GPU 5 和 7 的状态
watch -n 1 nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,memory.used --format=csv -i 5,7

# 查看 API 调用统计(如果 LMDeploy 开启了 metrics)
curl http://localhost:60080/metrics 2>/dev/null | grep -E "lmdeploy.*(request|token)" | head -20

5.4 与其他模型共存(端口规划思路)

如果你的双 V100 服务器未来可能部署多个模型。合理的端口规划可以避免冲突:

模型端口TPGPU说明
DeepSeek-R1-Distill-Qwen-32B-AWQ6008025,7当前部署的推理主力
Qwen3.8-27B-Q4_K_M6008120,1通用/代码/Agent(后续部署)
Qwen2.5-14B-Instruct-AWQ600821单卡轻量级对话(可选)

Nginx 统一路由示例(如果你需要对外暴露统一端口,具体详见下一章节):

# /home/yxn/data_share/scripts/nginx.conf 片段
upstream deepseek_backend {
    server 127.0.0.1:60080;
}

upstream qwen_backend {
    server 127.0.0.1:60081;
}

server {
    listen 60443 ssl;
    
    location /v1/chat/completions {
        if ($arg_model ~ "^deepseek") {
            proxy_pass http://deepseek_backend;
        }
        if ($arg_model ~ "^qwen") {
            proxy_pass http://qwen_backend;
        }
        # 默认路由到 DeepSeek
        proxy_pass http://deepseek_backend;
    }
}

💡 端口选择原则:

  • 避开常见端口(22 SSH、80 HTTP、443 HTTPS、8000 开发常用)
  • 使用 60000-60099 段,与系统服务隔离
  • 不同模型使用连续端口,便于记忆和管理

5.5 开机自启(按需)

如果你希望服务器重启后自动启动 LMDeploy 服务,可以使用 systemd 服务:

# 创建 systemd 服务文件
sudo tee /etc/systemd/system/lmdeploy.service > /dev/null << 'EOF'
[Unit]
Description=LMDeploy Service - DeepSeek-R1-Distill-Qwen-32B-AWQ
After=network-online.target remote-fs.target
Wants=network-online.target

[Service]
Type=forking
User=yxn
Group=yxn
Environment=CUDA_VISIBLE_DEVICES=5,7
Environment=PYTHONNOUSERSITE=1
ExecStart=/home/yxn/data_share/scripts/start_lmdeploy.sh
ExecStop=/home/yxn/data_share/scripts/stop_lmdeploy.sh
Restart=on-failure
RestartSec=30
StandardOutput=append:/home/yxn/data_share/logs/lmdeploy-systemd.log
StandardError=append:/home/yxn/data_share/logs/lmdeploy-systemd-error.log

[Install]
WantedBy=multi-user.target
EOF

# 启用并启动服务
sudo systemctl daemon-reload
sudo systemctl enable lmdeploy
sudo systemctl start lmdeploy

# 查看状态
sudo systemctl status lmdeploy

⚠️ 注意:开机自启需要 root 权限。如果你的共享服务器不允许使用 systemd,可以改用 crontab 的 @reboot 方式。


5.6 生产化配置总结

组件路径说明
启动脚本/home/yxn/data_share/scripts/start_lmdeploy.sh一键启动,含路径检查和日志管理
停止脚本/home/yxn/data_share/scripts/stop_lmdeploy.sh安全停止,含残留进程清理
日志文件/home/yxn/data_share/logs/lmdeploy_YYYYMMDD_HHMMSS.log按日期命名,方便归档
监控命令watch -n 1 nvidia-smi实时查看 GPU 状态
systemd 服务/etc/systemd/system/lmdeploy.service开机自启(可选)

🎉 至此,你已经拥有了一个生产级的本地大模型服务,可以稳定运行、方便启停、易于监控,并且为后续多模型共存做好了端口规划。


六、Nginx 反向代理配置

6.1 为什么需要 Nginx?

上面我们已经成功启动了 LMDeploy 服务,直接通过 http://localhost:60080 就可以调用。但在生产环境中,直接暴露后端服务存在几个问题:

  1. 端口混乱:如果后续部署多个模型(Qwen3.8-27B、Qwen2.5-14B 等),每个模型占用不同端口,客户端需要记住多个端口号。
  2. 缺乏鉴权:LMDeploy 默认没有 API Key 认证,任何人都能调用你的服务。
  3. 缺少 SSL:直接使用 HTTP 明文传输,敏感数据可能被窃听。
  4. 无法统一管理:无法做流量控制、日志审计、负载均衡等。

Nginx 作为反向代理网关,可以统一解决这些问题:

  • 对外暴露一个端口(如 60443),根据请求中的 model 字段自动路由到不同后端。
  • 在 Nginx 层面添加 API Key 校验,阻止未授权访问。
  • 配置 SSL 证书,提供 HTTPS 加密访问。
  • 统一日志记录,方便排查问题。

6.2 安装 Nginx

在 (V100 计算服务器) 上安装 Nginx。由于根分区紧张,我们将 Nginx 的配置文件和数据放在共享盘 /home/yxn/data_share 上,但 Nginx 本体仍需安装在系统目录。

# 更新包索引
sudo apt update

# 安装 Nginx
sudo apt install -y nginx

# 验证安装
nginx -v
# 输出示例:nginx version: nginx/1.18.0 (Ubuntu)

💡 如果系统提示 Unable to locate package nginx,先执行 sudo apt update 再试。


6.3 自定义 Nginx 配置目录

默认配置在 /etc/nginx 下,但我们希望将配置集中管理,方便备份和迁移。我们在共享盘上创建自定义配置目录:

# 创建自定义配置目录
mkdir -p /home/yxn/data_share/nginx/conf
mkdir -p /home/yxn/data_share/nginx/ssl
mkdir -p /home/yxn/data_share/nginx/logs
mkdir -p /home/yxn/data_share/nginx/html  # 可选,用于静态页面

目录结构说明:

/home/yxn/data_share/nginx/
├── conf/          # Nginx 配置文件(主配置、站点配置)
├── ssl/           # SSL 证书和密钥
├── logs/          # 访问日志和错误日志
└── html/          # 静态文件(可选)

6.4 生成 API Key

API Key 用于客户端认证,防止未授权调用。我们使用 Python 生成一个 sk- 前缀的随机字符串:

# 生成 API Key
API_KEY=$(python3 -c "import uuid; print('sk-' + uuid.uuid4().hex)")
echo $API_KEY

# API_KEY=$(openssl rand -hex 32)  # ssl生成强口令 API Key(32 字节,64 位十六进制)

输出示例:

sk-9a1b2c3d4e5f6789012345678abcdef

将这个 Key 记录下来,后续在 Nginx 配置和客户端中使用。

💡 安全建议:不要将 API Key 硬编码在脚本中,建议存入环境变量或密钥管理服务。这里为了演示,我们将其写入配置文件。

在这里插入图片描述

6.5 配置 SSL 证书

为了让 Nginx 提供 HTTPS 加密访问,我们需要 SSL 证书。内网环境推荐使用自签名证书,无需域名,几分钟即可生成。如果你有公网域名,也可以使用 Let’s Encrypt 免费证书(本文仅简要提及)。

6.5.1 生成自签名证书

Step 1:生成私钥

openssl genrsa -out /home/yxn/data_share/nginx/ssl/privkey.pem 2048
  • genrsa:生成 RSA 私钥
  • 2048:密钥长度,2048 位足够安全

Step 2:生成证书签名请求(CSR)

openssl req -new \
  -key /home/yxn/data_share/nginx/ssl/privkey.pem \
  -out /home/yxn/data_share/nginx/ssl/server.csr \
  -subj "/C=CN/ST=Yunnan/L=Chuxiong/O=Local/OU=Dev/CN=192.168.5.23" \
  -addext "subjectAltName=IP:192.168.5.23,IP:192.168.5.4"

参数说明:

  • C:国家(China)
  • ST:省份
  • L:城市
  • O:组织名称
  • CN:重要,填写你的服务器 IP。这里填 192.168.5.23(内网 IP),如果客户端通过其他地址访问,请相应修改。
  • addext 后面跟 IP: 前缀表示这是 IP 地址,不是域名。

💡 CN 必须与客户端访问时使用的地址一致,否则浏览器会提示证书名称不匹配。

Step 3:自签名证书(有效期 365 天)

openssl x509 -req \
  -days 365 \
  -in /home/yxn/data_share/nginx/ssl/server.csr \
  -signkey /home/yxn/data_share/nginx/ssl/privkey.pem \
  -out /home/yxn/data_share/nginx/ssl/fullchain.pem
  • -days 365:证书有效期一年
  • -signkey:使用刚才生成的私钥自签名

Step 4:清理临时文件

rm -f /home/yxn/data_share/nginx/ssl/server.csr

Step 5:设置权限

chmod 600 /home/yxn/data_share/nginx/ssl/privkey.pem
chmod 644 /home/yxn/data_share/nginx/ssl/fullchain.pem

✅ 自签名证书生成完毕。现在你有 fullchain.pem(证书)和 privkey.pem(私钥)两个文件。

6.5.3 验证证书
 ls -lh /home/yxn/data_share/nginx/ssl/

检查证书内容:

openssl x509 -in /home/yxn/data_share/nginx/ssl/fullchain.pem -text -noout | grep -E "Subject:|Issuer:|Not Before|Not After"

输出:
在这里插入图片描述

💡 如果你有公网域名,可以使用 Let’s Encrypt 的 certbot 工具申请免费证书(需域名解析到服务器,且 80 端口可访问),此处不再赘述。

6.6 接入自定义配置(不改主配置)

我们的 /etc/nginx/nginx.conf 默认已包含:

include /etc/nginx/conf.d/*.conf;

没有的话需要在http模块下面添加上面这行。

接着我们在 /etc/nginx/conf.d/ 下放一个"跳板"文件,指向你自己的目录:

sudo tee /etc/nginx/conf.d/data_share.conf > /dev/null << 'EOF'
# 引入用户自定义目录下的所有站点配置
include /home/yxn/data_share/nginx/conf/*.conf;
EOF

这样所有站点配置都写在 /home/yxn/data_share/nginx/conf/ 下,完全不碰 /etc/nginx/nginx.conf,方便备份和迁移。

确保 nginx 能读取你的目录:

chmod 755 /home/yxn
chmod 755 /home/yxn/data_share
chmod 755 /home/yxn/data_share/nginx
chmod 755 /home/yxn/data_share/nginx/conf
chmod 644 /home/yxn/data_share/nginx/conf/*.conf

如果要编写 Nginx 主配置,我们可以将自定义配置目录纳入 Nginx 管理。首先备份默认配置,然后创建自己的主配置文件:

# 备份默认配置
sudo mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak

# 创建新的主配置,指向自定义目录
sudo tee /etc/nginx/nginx.conf > /dev/null << 'EOF'
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
    multi_accept on;
}

http {
    ##
    # Basic Settings
    ##
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # Logging Settings
    ##
    access_log /home/yxn/data_share/nginx/logs/access.log;
    error_log /home/yxn/data_share/nginx/logs/error.log;

    ##
    # Gzip Settings
    ##
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # Virtual Host Configs
    ##
    include /home/yxn/data_share/nginx/conf/*.conf;
}
EOF

💡 主配置中 include 指向了我们的自定义目录 /home/yxn/data_share/nginx/conf/*.conf,这样所有站点配置都可以放在那里。

6.7 编写站点配置(反向代理 + API Key 鉴权)

创建 /home/yxn/data_share/nginx/conf/api_gateway.conf:

# api_gateway.conf
# 大模型 API 统一网关

# 定义一组后端服务器,起一个别名deepseek_backend
upstream deepseek_backend {
    server 127.0.0.1:60080; # LMDeploy 实际监听的地址
    keepalive 32; # 与后端保持 32 个长连接,避免每次请求都新建 TCP 连接,降低延迟
}

# 预留其他模型上游
# upstream qwen_backend {
#     server 127.0.0.1:60081;
#     keepalive 32;
# }

server {
    listen 60443 ssl;  # 监听 60443 端口,并启用 SSL/TLS 加密
    server_name _;  # 匹配所有主机名(IP 访问、任意域名都接受)

    ssl_certificate /home/yxn/data_share/nginx/ssl/fullchain.pem;  # 服务器证书(公钥 + 中间证书链)
    ssl_certificate_key /home/yxn/data_share/nginx/ssl/privkey.pem;  # 私钥
    ssl_protocols TLSv1.2 TLSv1.3;  # 只允许 TLS 1.2/1.3,拒绝更老的 SSLv3/TLS 1.0/1.1
    ssl_ciphers HIGH:!aNULL:!MD5;  # 使用高强度加密套件,禁用无认证和 MD5
    ssl_prefer_server_ciphers on;  # 优先使用服务端指定的加密套件

    # 所有访问记录和错误都写到 data_share/nginx/logs/ 
    access_log /home/yxn/data_share/nginx/logs/api_access.log;
    error_log /home/yxn/data_share/nginx/logs/api_error.log;

    set $valid_api_key "sk-bcadaf67d3b046eabf82b8c4ec21f758";

    location = /health {
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }

    location = /v1/models {
        proxy_pass http://deepseek_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /v1/chat/completions {
        if ($http_authorization != "Bearer $valid_api_key") {
            return 401 '{"error":"Unauthorized","message":"Invalid API key"}';
        }

        proxy_pass http://deepseek_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
        proxy_buffering off;
        proxy_request_buffering on;
    }

    location /v1/ {
        if ($http_authorization != "Bearer $valid_api_key") {
            return 401 '{"error":"Unauthorized"}';
        }
        proxy_pass http://deepseek_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 120s;
        proxy_buffering off;
    }

    location / {
        return 404;
    }
}

# HTTP 重定向到 HTTPS(可选)
server {
    listen 80;
    server_name _;
    return 301 https://$host:60443$request_uri;
}

⚠️ 注意:上面的 API Key 校验使用了 if 指令,对于简单场景够用。但对于生产环境,建议使用更安全的方法(如 ngx_http_auth_request_module 或 OpenResty Lua)。这里为了演示,保持简洁。

请求流程图

客户端请求
    │
    ▼
┌─────────────────┐
│  Nginx:60443    │
│  (SSL 解密)     │
└────────┬────────┘
         │
    ┌────┴────┐
    ▼         ▼
 /health   /v1/models   ──► 直接响应 / 转发到后端(免鉴权)
    │
    ▼
 /v1/chat/completions
    │
    ▼
 检查 Authorization
    │
    ├── Key 错误 ──► 返回 401
    │
    └── Key 正确 ──► 转发到 127.0.0.1:60080 (LMDeploy)
                         │
                         ▼
                      模型推理
                         │
                         ▼
                      流式返回给客户端

一句话总结

Nginx 在这里充当保安 + 翻译 + 记录员:SSL 加密负责安全传输,API Key if 判断负责门禁验证,proxy_pass 负责转发给 LMDeploy,日志负责事后审计。LMDeploy 只暴露给本机,外网完全碰不到。

💡 更优雅的多模型路由建议使用 OpenResty(Nginx + Lua)或 Kong/APISIX 等 API 网关。

6.8 更新 LMDeploy 启动脚本,加入 API Key

为了让 LMDeploy 也识别相同的 API Key(双重校验),我们修改启动脚本,添加 --api-key 参数:

编辑 /home/yxn/data_share/scripts/start_lmdeploy.sh,在启动命令中加入:

# 在启动命令中添加 --api-key 参数
nohup lmdeploy serve api_server \
    "$MODEL_PATH" \
    --model-format awq \
    --tp $TP_SIZE \
    --dtype float16 \
    --session-len $SESSION_LEN \
    --cache-max-entry-count 0.85 \
    --model-name deepseek-r1-distill-qwen-32b-awq \
    --server-name 0.0.0.0 \
    --server-port $PORT \
    --api-keys "$API_KEY" \
    > "$LOG_FILE" 2>&1 &

💡 这样客户端请求时,既要在 Nginx 层通过 API Key 验证,LMDeploy 也会验证,双重保险。


6.9 重启 Nginx 并测试

# 检查配置语法
sudo nginx -t

# 如果语法正确,重启 Nginx
sudo systemctl restart nginx

# 查看状态
sudo systemctl status nginx

在这里插入图片描述

测试步骤:

  1. 不带 API Key 测试:
curl -k https://192.168.5.23:60443/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-r1-distill-qwen-32b-awq","messages":[{"role":"user","content":"你好"}]}'
# 应返回 401 Unauthorized
  1. 带错误的 API Key 测试:
curl -k https://192.168.5.23:60443/v1/chat/completions \
  -H "Authorization: Bearer wrongkey" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-r1-distill-qwen-32b-awq","messages":[{"role":"user","content":"你好"}]}'
# 应返回 401
  1. 带正确的 API Key 测试:
curl -k https://192.168.5.23:60443/v1/chat/completions \
  -H "Authorization: Bearer sk-a1b2c3d4e5f6789012345678abcdef01" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-r1-distill-qwen-32b-awq","messages":[{"role":"user","content":"你好"}]}'
# 应返回正常回复
  1. 健康检查:
curl -k https://192.168.5.23:60443/health
# 返回 OK

在这里插入图片描述

6.12 本章总结

组件路径/命令说明
Nginx 安装sudo apt install nginx系统级安装
自定义配置目录/home/yxn/data_share/nginx/conf/方便管理和备份
SSL 证书目录/home/yxn/data_share/nginx/ssl/存放 fullchain.pem 和 privkey.pem
日志目录/home/yxn/data_share/nginx/logs/访问日志和错误日志
API Keypython3 -c "import uuid; print('sk-' + uuid.uuid4().hex)"用于客户端鉴权
主配置/etc/nginx/nginx.conf指向自定义配置目录
站点配置/home/yxn/data_share/nginx/conf/api_gateway.conf反向代理 + 鉴权 + SSL
测试命令curl -k https://192.168.5.23:60443/v1/chat/completions -H "Authorization: Bearer YOUR_KEY"验证完整链路

🎉 至此,我们拥有了一个生产级的大模型 API 网关,具备 SSL 加密、API Key 鉴权、统一端口、日志审计等功能,并且为后续多模型扩展做好了准备。

七、踩坑实录:我浪费的那几个小时

7.1 官方 vLLM:SM7.0 的硬门槛

最初我自然想到用 vLLM 部署。本地恰好有一个 vllm-1.0.0-cp312-cp312-linux_x86_64.whl,但检查 METADATA 后发现这是上游官方版本。官方 vLLM 的 AWQ 内核(Marlin)硬编码了 get_min_capability() >= SM75,V100 的 SM70 直接被拒之门外。启动时会报:

AWQ is not supported on Volta / capability SM70

结论:官方 vLLM 路线在 V100 + AWQ 的场景下走不通。

7.2 1Cat-vLLM 社区版:三重版本地狱

既然官方不支持,那就找社区 fork。1Cat-vLLM 是唯一一个专门为 V100 (SM7.0) 解锁 AWQ 支持的 vLLM 分支,它集成了 FLASH_ATTN_V100 后端和 SM70 的 WMMA 内核。

我满怀希望地安装了 1cat_vllm-1.2.2 和 flash_attn_v100-1.2.2,然后——

坑一:glibc 2.32 缺失
ImportError: /lib/x86_64-linux-gnu/libc.so.6: 
version `GLIBC_2.32' not found

Ubuntu 20.04 原生 glibc 为 2.31,而 1Cat 的预编译 wheel 是在 glibc 2.32+ 的系统上构建的。我尝试在 Conda 内注入 sysroot_linux-64=2.34 并提供新版动态链接器,但子进程(worker)仍可能回落到系统 glibc,失败。

坑二:CUDA 12.9 与 torch cu129 的「不存在版本」

我决定从源码编译 flash-attention-v100。执行编译后,构建脚本检查 torch 的 CUDA 版本后抛出:

RuntimeError: CUDA version 12.8 < 12.9 is not supported.

原来最新源码强制要求 CUDA 12.9 工具链,并且要求 torch 2.10.0+cu129。

我尝试安装:

pip install torch==2.10.0+cu129     --index-url https://download.pytorch.org/whl/cu129

结果:

ERROR: No matching distribution found for torch==2.10.0+cu129

关键发现:PyTorch 官方从未发布过 torch 2.10.0+cu129 的稳定版。cu129 只存在于 nightly 预览渠道(版本号为 2.10.0.devXXXX+cu129),稳定性与兼容性无保障。

此时局面已非常清晰:

  • 预编译 wheel:glibc 不兼容 ❌
  • 源码编译:需要 CUDA 12.9 + 不存在的 torch cu129 ❌
  • 在共享服务器上升级系统 glibc 或 CUDA 驱动:风险极高 ❌

1Cat-vLLM 路线在此环境下性价比过低,被迫放弃。

7.3 磁盘爆满的紧急处理

在转向 LMDeploy 后,我直接执行了 pip install lmdeploy,结果——

No space left on device

根分区 100% 满了!紧急抢救:

pip cache purge                    # 释放 4GB+
conda clean --all -y               # 释放 2GB+
rm -f ~/models/*.run               # 删除 CUDA 安装包
rm -rf ~/.local/share/Trash/*      # 清空回收站

💡 教训:在共享服务器上,df -h / 应该在任何 pip install 之前执行。这次我差点因为 778MB 的剩余空间而连 LMDeploy 都装不上。


八、经验总结与推荐模型

8.1 老卡部署的核心认知

  1. V100 (SM7.0) 的 AWQ 支持是「特权」而非「标配」 官方 vLLM 的 AWQ/Marlin 内核仅编译到 SM75+。 遇到老显卡,首先应该查的是推理引擎的硬件支持矩阵,而不是模型有没有 AWQ 版本。
  2. 社区 fork 的「暗礁」往往比功能本身更致命 1Cat-vLLM 确实解锁了 SM70 的 AWQ,但它的预编译包要求 glibc 2.32+ 和 CUDA 12.9。 在 Ubuntu 20.04 (glibc 2.31) 这种「老系统」上,ABI 兼容性比功能支持更值得关注。
  3. torch 的 CUDA 版本命名陷阱 cu129 在 PyTorch 稳定版渠道中不存在。看到文档写 torch==2.10.0+cu129 时,务必去官网验证。

8.2 共享服务器的生存法则

  • 绝不碰系统 glibc 和驱动:升级 glibc 可能导致系统命令全部失效;升级驱动可能影响所有用户。
  • Conda 环境隔离是底线:PYTHONNOUSERSITE=1 + 独立 env,防止污染。
  • GPU 隔离用 CUDA_VISIBLE_DEVICES:比 Docker 的 --gpus 更轻量,在共享服务器上更实用。
  • 磁盘监控要前置:df -h / 应该在任何 pip install 之前执行。
  • 大文件操作落在 NFS:Conda 环境、模型文件、日志都应放在 NFS,根分区只留系统文件。

8.3 为什么 LMDeploy 是更优解?

维度1Cat-vLLMLMDeploy
V100 AWQ 支持✅(需绕过 ABI)✅(原生 TurboMind)
对 CUDA 版本要求12.9+11.3+(12.4 完美兼容)
对 glibc 要求2.32+2.31(Ubuntu 20.04 原生)
额外编译需源码编译 flash_attn_v100无需
稳定性依赖 nightly torch稳定版 PyTorch 即可

最终结论:对于需要在「老系统 + 老显卡」上跑 AWQ 量化的场景,LMDeploy 的 TurboMind 引擎是目前最省心的工业级方案。

8.4 同环境其他推荐模型

基于 双 V100 32GB + LMDeploy + AWQ 的这套环境,以下模型可直接「即插即用」:

模型大小推荐配置特点
Qwen2.5-14B-Instruct-AWQ14BTP=1, 单卡即可跑余量极大,上下文可开 32K,通用对话首选
Qwen2.5-32B-Instruct-AWQ32BTP=2, 同当前命令通用指令模型(非推理专用),Tool Calling 更好
Qwen3.5-9B-AWQ9BTP=1新一代架构,支持 Tool Calling 和 Reasoning

建议同时部署两个 32B 模型:DeepSeek-R1-Distill 用于复杂推理,Qwen2.5-32B 用于通用对话,通过不同端口提供服务。


九、附录:关键报错速查

报错原因解决方案
AWQ is not supported on Volta官方 vLLM 的 SM70 门槛换 LMDeploy 或 1Cat-vLLM
GLIBC_2.32 not found预编译 wheel 与系统 glibc 不兼容换 LMDeploy,或从源码编译(需解决 CUDA 版本)
CUDA version 12.8 < 12.9 is not supportedflash-attention-v100 最新源码要求 CUDA 12.9换 LMDeploy,或安装 CUDA 12.9 toolkit + nightly torch
No space left on device磁盘已满pip cache purge; conda clean --all; rm -f *.run
torch.cuda.OutOfMemoryError上下文太长或 cache 比例过高降低 --session-len 或 --cache-max-entry-count
No module named FLASH_ATTN_V1001Cat 的 attention 后端未正确安装确认 wheel 版本,或换 LMDeploy
NCCL error: unhandled cuda errornvidia-nccl-cu13 与 cu12 环境冲突卸载冲突包,重装 torch cu12x

写在最后

本文完整记录了一次「在受限环境中部署大模型」的实战过程。核心任务是在一台 Ubuntu 20.04 + glibc 2.31 + CUDA 12.4 + 8×V100 (SM7.0) 的共享服务器上,仅用 GPU5 + GPU7(NVLink 直连) 运行 DeepSeek-R1-Distill-Qwen-32B-AWQ。

核心决策

环节选择原因
推理框架LMDeploy官方 vLLM 不支持 SM70 的 AWQ;1Cat-vLLM 要求 glibc 2.32+/CUDA 12.9,在老系统上走不通
量化格式AWQ 4-bitFP16 的 32B 模型需 64GB 显存,双 V100 32GB 刚好卡死,AWQ 是唯一可行方案
GPU 拓扑GPU5 + GPU7同属 NUMA 1,NV2 直连,Tensor Parallel 通信延迟最低
目录结构data_share/ 集中管理根分区仅剩 778MB,所有环境、模型、日志、配置全部落在 NFS 共享盘

最终架构

客户端 (Windows/UpstreamKit 可选)
    │
    ▼
Nginx (60443, HTTPS + API Key 鉴权)
    │
    ▼
LMDeploy (60080, TurboMind, TP=2, AWQ)
    │
    ▼
GPU5 + GPU7 (V100 32GB, NVLink)

关键教训

  1. 老硬件上,推理引擎的选择比模型更重要。V100 的 SM7.0 是一道硬门槛,官方 vLLM 直接拒绝,社区 fork 又陷入 glibc/CUDA/torch 的三重版本地狱。
  2. LMDeploy 的 TurboMind 是「老系统 + 老显卡」的最优解——glibc 2.31 兼容、CUDA 12.4 兼容、零编译、一键安装。
  3. 共享服务器上,隔离和监控是底线。CUDA_VISIBLE_DEVICES 隔离 GPU、PYTHONNOUSERSITE=1 隔离 Python、df -h / 前置检查磁盘,缺一不可。
  4. 生产化不只是「能跑」。从裸命令到 start/stop 脚本、从 HTTP 到 Nginx HTTPS 网关、从开放端口到 API Key 鉴权,每一步都是把「Demo」变成「服务」。

如果你也在一台「不能动系统、不能升级驱动、磁盘见底」的老服务器上挣扎,希望这篇记录能帮你省下那几个小时。选对工具,老 V100 依然能跑 32B 推理模型。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐