v100双显卡本地部署DeepSeek-R1-Distill-Qwen-32B-AWQ大模型
在本地 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 | 显存占用 | 状态 | 说明 |
|---|---|---|---|
| 0 | 971 MiB | ❌ 占用中 | 已有其他用户的 Python 进程 |
| 1-4 | 0 MiB | ⚠️ 空闲但未选 | 属于 NUMA 0,与目标卡跨 NUMA |
| 5 | 0 MiB | ✅ 目标显卡 | 与 GPU7 同属 NUMA 1,NVLink 直连 |
| 6 | 0 MiB | ⚠️ 空闲但未选 | 属于 NUMA 1,但与 GPU7 仅 PIX 连接 |
| 7 | 0 MiB | ✅ 目标显卡 | 与 GPU5 通过 NV2 高速互联 |
最终选择:GPU5 + GPU7,理由:
- 同属 NUMA 1 域,CPU 亲和性一致(12-23,36-47)
- NV2 直连,Tensor Parallel 通信延迟最低
- 两张卡完全空闲(0 MiB),且与被占用的 GPU0 物理隔离
- 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 的核心原因:
- 显存账本:FP16 的 32B 模型权重约 64GB,两张 V100 32GB 加起来刚好 64GB 卡边,几乎没有 KV Cache 空间,上下文只能压到 1024 tokens 以下,实用性极低
- AWQ vs GPTQ:在 V100 (SM7.0) 上,LMDeploy 的 TurboMind 引擎对 AWQ 的 kernel 支持更成熟且计算效率更高,且 AWQ 的量化精度通常优于 GPTQ
- 双卡 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-20GBwmywmywww/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+) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 社区 Fork | 1Cat-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 最终对比表
| 维度 | 官方 vLLM | 1Cat-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 赢在"恰好匹配":
- ✅ 原生支持 V100 的 AWQ(其他主流框架要么不支持,要么要折腾)
- ✅ 对系统版本要求宽松(glibc 2.31 + CUDA 12.4 完全满足)
- ✅ 安装零编译(pip install 后直接能用)
- ✅ 性能优秀(TurboMind 引擎针对量化模型深度优化)
- ✅ 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/lmd | Python 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-format | awq | 必须,声明模型为 AWQ 4-bit 量化格式。TurboMind 引擎对 AWQ 的支持在 V100 (sm70) 上原生可用。 |
--tp | 2 | Tensor Parallel=2,两张卡并行。必须与 CUDA_VISIBLE_DEVICES 暴露的卡数一致。 |
--dtype | float16 | V100 不支持 BF16,必须显式指定 FP16。 |
--session-len | 8192 | 最大上下文长度。AWQ 4-bit 下双卡显存充裕,8192 是安全起点,稳定后可尝试 16384。 |
--cache-max-entry-count | 0.8 | KV Cache 最多占用单卡显存的 80%,留 20% 余量防 OOM。 |
--model-name | 自定义 | OpenAI API 兼容的模型名,客户端调用时使用。 |
--server-port | 60080 | 避开常用的 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 memory | KV Cache 占用过高 | 降低 --cache-max-entry-count 到 0.7 或 0.6 |
gemm_config.in is not found 警告 + 崩溃 | 未指定 --tp 或 tp 数不对 | 确保 --tp 2 且与 CUDA_VISIBLE_DEVICES 卡数一致 |
启动后卡在 Loading weights | NFS 读取慢,正常现象 | 耐心等待,双 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 服务器未来可能部署多个模型。合理的端口规划可以避免冲突:
| 模型 | 端口 | TP | GPU | 说明 |
|---|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-32B-AWQ | 60080 | 2 | 5,7 | 当前部署的推理主力 |
| Qwen3.8-27B-Q4_K_M | 60081 | 2 | 0,1 | 通用/代码/Agent(后续部署) |
| Qwen2.5-14B-Instruct-AWQ | 60082 | 1 | 单卡 | 轻量级对话(可选) |
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 就可以调用。但在生产环境中,直接暴露后端服务存在几个问题:
- 端口混乱:如果后续部署多个模型(Qwen3.8-27B、Qwen2.5-14B 等),每个模型占用不同端口,客户端需要记住多个端口号。
- 缺乏鉴权:LMDeploy 默认没有 API Key 认证,任何人都能调用你的服务。
- 缺少 SSL:直接使用 HTTP 明文传输,敏感数据可能被窃听。
- 无法统一管理:无法做流量控制、日志审计、负载均衡等。
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

测试步骤:
- 不带 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
- 带错误的 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
- 带正确的 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":"你好"}]}'
# 应返回正常回复
- 健康检查:
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 Key | python3 -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 老卡部署的核心认知
- V100 (SM7.0) 的 AWQ 支持是「特权」而非「标配」 官方 vLLM 的 AWQ/Marlin 内核仅编译到 SM75+。 遇到老显卡,首先应该查的是推理引擎的硬件支持矩阵,而不是模型有没有 AWQ 版本。
- 社区 fork 的「暗礁」往往比功能本身更致命 1Cat-vLLM 确实解锁了 SM70 的 AWQ,但它的预编译包要求 glibc 2.32+ 和 CUDA 12.9。 在 Ubuntu 20.04 (glibc 2.31) 这种「老系统」上,ABI 兼容性比功能支持更值得关注。
- 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-vLLM | LMDeploy |
|---|---|---|
| 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-AWQ | 14B | TP=1, 单卡即可跑 | 余量极大,上下文可开 32K,通用对话首选 |
| Qwen2.5-32B-Instruct-AWQ | 32B | TP=2, 同当前命令 | 通用指令模型(非推理专用),Tool Calling 更好 |
| Qwen3.5-9B-AWQ | 9B | TP=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 supported | flash-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_V100 | 1Cat 的 attention 后端未正确安装 | 确认 wheel 版本,或换 LMDeploy |
NCCL error: unhandled cuda error | nvidia-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-bit | FP16 的 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)
关键教训
- 老硬件上,推理引擎的选择比模型更重要。V100 的 SM7.0 是一道硬门槛,官方 vLLM 直接拒绝,社区 fork 又陷入 glibc/CUDA/torch 的三重版本地狱。
- LMDeploy 的 TurboMind 是「老系统 + 老显卡」的最优解——glibc 2.31 兼容、CUDA 12.4 兼容、零编译、一键安装。
- 共享服务器上,隔离和监控是底线。
CUDA_VISIBLE_DEVICES隔离 GPU、PYTHONNOUSERSITE=1隔离 Python、df -h /前置检查磁盘,缺一不可。 - 生产化不只是「能跑」。从裸命令到
start/stop脚本、从 HTTP 到 Nginx HTTPS 网关、从开放端口到 API Key 鉴权,每一步都是把「Demo」变成「服务」。
如果你也在一台「不能动系统、不能升级驱动、磁盘见底」的老服务器上挣扎,希望这篇记录能帮你省下那几个小时。选对工具,老 V100 依然能跑 32B 推理模型。
更多推荐


所有评论(0)