双 V100 本地部署 Qwen:Embedding、Reranker、LLM 和 New API 接入记录

cc 发布于 2026-06-27 235 次阅读


这次是在 ESXi 8 上的一台 Ubuntu 24.04 虚拟机里,直通两张 Tesla V100 32GB,部署一组本地 Qwen 服务。最后跑起来三类模型:向量、重排和 LLM,外面再接一层 New API v0.13.2。

这篇主要记录我实际落地时的选择和命令。域名、内网 IP、服务用户这些信息都做了脱敏,照着复现时换成自己的环境即可。

最终结果

最终架构如下:

ESXi 8 Ubuntu VM: 10.10.10.10

GPU0 Tesla V100 32GB
  └─ Qwen3.6-27B GGUF Q4_K_M
     └─ llama.cpp / OpenAI compatible / port 8003

GPU1 Tesla V100 32GB
  ├─ Qwen3-Embedding-8B
  │  └─ FastAPI / OpenAI-style embeddings / port 8001
  └─ Qwen3-Reranker-4B
     └─ FastAPI / Jina-compatible rerank / port 8002

Cloud Nginx: llm.example.com
  ├─ /v1/embeddings       -> 10.10.10.10:8001
  ├─ /v1/rerank           -> 10.10.10.10:8002
  ├─ /v1/models           -> 10.10.10.10:8003
  ├─ /v1/chat/completions -> 10.10.10.10:8003
  └─ /v1/completions      -> 10.10.10.10:8003

当前环境版本:

Ubuntu: 24.04.4 LTS
Kernel: 6.8.0-124-generic
NVIDIA driver: 580.159.03
CUDA Toolkit: 12.6.3
Docker: 29.6.0
NVIDIA Container Toolkit: 1.19.1
PyTorch: 2.12.1+cu126
llama.cpp: beac530

运行状态:

systemctl is-active qwen3-embedding.service qwen3-reranker.service qwen3-llm.service nvidia-v100-tuning.service
active
active
active
active

显存状态:

nvidia-smi --query-gpu=index,name,driver_version,memory.used,memory.free,memory.total,utilization.gpu,ecc.mode.current --format=csv,noheader
0, Tesla V100-PCIE-32GB, 580.159.03, 19178 MiB, 13317 MiB, 32768 MiB, 0 %, Disabled
1, Tesla V100-PCIE-32GB, 580.159.03, 23182 MiB, 9313 MiB, 32768 MiB, 0 %, Disabled

LLM 上下文已经从 8192 调到 32768:

model Qwen3.6-27B
n_ctx 32768
n_ctx_train 262144
n_embd 5120

模型选择

最终选型:

向量: Qwen/Qwen3-Embedding-8B
重排: Qwen/Qwen3-Reranker-4B
LLM: bartowski/Qwen_Qwen3.6-27B-GGUF, Q4_K_M

一开始考虑过两个 8B:Qwen3-Embedding-8BQwen3-Reranker-8B。但新增 LLM 之后,两张 V100 的分工要重新安排。Embedding 和 Reranker 如果都用 8B 放在一张 32GB V100 上,余量会比较紧,长文本或者并发请求时容易出问题。

最后保留 8B 向量模型,把重排降到 4B。这样召回质量尽量保住,重排也够用,GPU1 还能留出约 9GB 左右显存。LLM 则选择 Qwen3.6 27B 的 GGUF Q4_K_M,用 llama.cpp 跑在 GPU0 上。

不用 vLLM 的原因也很现实:V100 是 sm_70,很多新版本推理栈对它并不友好。llama.cpp + GGUF 对这台机器更稳,显存也更可控。

基础依赖

先把系统整理成一台稳定的 GPU 推理机器。

sudo apt update
sudo apt install -y \
  build-essential dkms linux-headers-$(uname -r) \
  curl wget git git-lfs aria2 ca-certificates gnupg lsb-release \
  cmake ninja-build pkg-config python3-venv python3-pip python3-dev \
  libssl-dev libffi-dev zlib1g-dev libjpeg-dev \
  tmux htop nvtop unzip zip rsync

安装 NVIDIA driver:

sudo apt install -y ubuntu-drivers-common
ubuntu-drivers devices
sudo apt install -y nvidia-driver-580-server
sudo reboot

安装 CUDA Toolkit 12.6:

cd /tmp
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt update
sudo apt install -y cuda-toolkit-12-6

写入 CUDA 环境变量:

cat <<'EOF' | sudo tee /etc/profile.d/cuda.sh
export PATH=/usr/local/cuda/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:${LD_LIBRARY_PATH:-}
EOF

source /etc/profile.d/cuda.sh

非交互 SSH 里可能没有加载 /etc/profile.d/cuda.sh,所以验证时建议直接用完整路径:

/usr/local/cuda-12.6/bin/nvcc --version | tail -n 1

安装 Docker 和 NVIDIA Container Toolkit:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

. /etc/os-release
CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME}

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $CODENAME stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list >/dev/null

sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

创建 PyTorch CUDA 环境:

mkdir -p /home/qwen/venvs
python3 -m venv /home/qwen/venvs/torch-cu126
source /home/qwen/venvs/torch-cu126/bin/activate

python -m pip install --upgrade pip setuptools wheel
python -m pip install --index-url https://download.pytorch.org/whl/cu126 torch torchvision torchaudio
python -m pip install \
  fastapi 'uvicorn[standard]' transformers accelerate datasets \
  huggingface_hub safetensors sentencepiece pydantic \
  numpy scipy pandas scikit-learn tqdm \
  nvidia-ml-py gpustat nvitop einops pyyaml rich

验证:

source /home/qwen/venvs/torch-cu126/bin/activate
python - <<'PY'
import torch
print(torch.__version__)
print(torch.version.cuda)
print(torch.cuda.is_available())
print(torch.cuda.device_count())
print([torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())])
PY

输出:

2.12.1+cu126
12.6
True
2
['Tesla V100-PCIE-32GB', 'Tesla V100-PCIE-32GB']

ECC 和 V100 调优

一开始两张卡 ECC 状态不一致:GPU0 是 Disabled,GPU1 是 Enabled。为了显存和行为一致,最后统一关闭。

nvidia-smi --query-gpu=index,ecc.mode.current,ecc.mode.pending,memory.total --format=csv,noheader

sudo nvidia-smi -i 0 -e 0
sudo nvidia-smi -i 1 -e 0
sudo reboot

重启后确认:

0, Disabled, Disabled, 32768 MiB
1, Disabled, Disabled, 32768 MiB

再创建 V100 tuning 服务:

cat <<'EOF' | sudo tee /etc/systemd/system/nvidia-v100-tuning.service
[Unit]
Description=Apply Tesla V100 persistence, power limit, and application clocks
After=nvidia-persistenced.service
Wants=nvidia-persistenced.service

[Service]
Type=oneshot
ExecStart=/usr/bin/nvidia-smi -pm 1
ExecStart=/usr/bin/nvidia-smi -pl 250
ExecStart=/usr/bin/nvidia-smi -ac 877,1380
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now nvidia-v100-tuning.service

这里有个需要注意的点:nvidia-v100-tuning.service 不要写 After=multi-user.target。之前这样写过,systemd 出现 ordering cycle,重启时会删除 Qwen 服务启动任务。最后只保留:

After=nvidia-persistenced.service
Wants=nvidia-persistenced.service

部署 Embedding 和 Reranker

准备目录:

mkdir -p /home/qwen/qwen3-services/logs
mkdir -p /home/qwen/qwen3-services/backups
mkdir -p /home/qwen/.cache/huggingface

生成内部 API key:

sudo sh -c 'umask 077; openssl rand -hex 32 > /etc/qwen-api-token'
sudo chown root:root /etc/qwen-api-token
sudo chmod 600 /etc/qwen-api-token

文档里不保存明文 key。后续需要配置 New API 或 Nginx 时再读:

sudo cat /etc/qwen-api-token

下载模型:

source /home/qwen/venvs/torch-cu126/bin/activate
export HF_HOME=/home/qwen/.cache/huggingface
export HF_HUB_DISABLE_XET=1

python - <<'PY'
from huggingface_hub import snapshot_download
for model_id in [
    'Qwen/Qwen3-Embedding-8B',
    'Qwen/Qwen3-Reranker-4B',
]:
    print('downloading', model_id)
    snapshot_download(model_id, cache_dir='/home/qwen/.cache/huggingface')
PY

Embedding systemd:

[Unit]
Description=Qwen3 Embedding 8B API (GPU1)
After=network-online.target nvidia-v100-tuning.service
Wants=network-online.target

[Service]
Type=simple
User=qwen
Group=qwen
WorkingDirectory=/home/qwen/qwen3-services
Environment=CUDA_VISIBLE_DEVICES=1
Environment=HF_HOME=/home/qwen/.cache/huggingface
Environment=HF_HUB_DISABLE_XET=1
Environment=MODEL_ID=Qwen/Qwen3-Embedding-8B
Environment=BATCH_SIZE=1
Environment=MAX_LENGTH=4096
ExecStart=/home/qwen/venvs/torch-cu126/bin/uvicorn embedding_service:app --host 0.0.0.0 --port 8001 --app-dir /home/qwen/qwen3-services
Restart=on-failure
RestartSec=10
TimeoutStartSec=600

[Install]
WantedBy=multi-user.target

Reranker systemd:

[Unit]
Description=Qwen3 Reranker 4B API (GPU1)
After=network-online.target nvidia-v100-tuning.service
Wants=network-online.target

[Service]
Type=simple
User=qwen
Group=qwen
WorkingDirectory=/home/qwen/qwen3-services
Environment=CUDA_VISIBLE_DEVICES=1
Environment=HF_HOME=/home/qwen/.cache/huggingface
Environment=HF_HUB_DISABLE_XET=1
Environment=MODEL_ID=Qwen/Qwen3-Reranker-4B
Environment=BATCH_SIZE=1
Environment=MAX_LENGTH=2048
ExecStart=/home/qwen/venvs/torch-cu126/bin/uvicorn reranker_service:app --host 0.0.0.0 --port 8002 --app-dir /home/qwen/qwen3-services
Restart=on-failure
RestartSec=10
TimeoutStartSec=600

[Install]
WantedBy=multi-user.target

为了避免重启时模型一起抢加载,Reranker 增加了等待 Embedding ready 的 drop-in:

[Unit]
After=qwen3-embedding.service
Requires=qwen3-embedding.service

[Service]
ExecStartPre=/usr/local/bin/wait-http-ready http://127.0.0.1:8001/health 900

部署 LLM

编译 llama.cpp:

cd /home/qwen
git clone https://github.com/ggml-org/llama.cpp.git
cd /home/qwen/llama.cpp
cmake -S . -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j$(nproc)

/home/qwen/llama.cpp/build/bin/llama-server --version | head -n 2

下载 GGUF:

sudo apt install -y aria2
mkdir -p /home/qwen/models/qwen3.6-27b-gguf

aria2c -c -x 8 -s 8 -k 4M \
  --summary-interval=30 \
  --timeout=60 \
  --max-tries=0 \
  --retry-wait=15 \
  --allow-overwrite=true \
  --auto-file-renaming=false \
  -d /home/qwen/models/qwen3.6-27b-gguf \
  -o Qwen_Qwen3.6-27B-Q4_K_M.gguf \
  https://huggingface.co/bartowski/Qwen_Qwen3.6-27B-GGUF/resolve/main/Qwen_Qwen3.6-27B-Q4_K_M.gguf

如果下载中途 403,通常是 Hugging Face/Xet 临时签名 URL 过期。不要删文件,重新执行同一条 aria2c 即可继续。

LLM systemd:

[Unit]
Description=Qwen3.6 27B GGUF LLM API (GPU0)
After=network-online.target nvidia-v100-tuning.service qwen3-reranker.service
Wants=network-online.target qwen3-reranker.service

[Service]
Type=simple
User=qwen
Group=qwen
WorkingDirectory=/home/qwen/llama.cpp
Environment=CUDA_VISIBLE_DEVICES=0
ExecStartPre=/usr/local/bin/wait-http-ready http://127.0.0.1:8002/health 900
ExecStart=/home/qwen/llama.cpp/build/bin/llama-server --host 0.0.0.0 --port 8003 --model /home/qwen/models/qwen3.6-27b-gguf/Qwen_Qwen3.6-27B-Q4_K_M.gguf --alias Qwen3.6-27B --ctx-size 32768 --n-gpu-layers 999 --parallel 1 --reasoning off --api-key-file %d/internal_api_key
Restart=on-failure
RestartSec=10
TimeoutStartSec=1200

[Install]
WantedBy=multi-user.target

--reasoning off 要保留。不开这个参数时,Qwen3.6 可能把短回答放到 reasoning_content,接到 New API 里会出现 content 为空的体验。

启动服务:

sudo systemctl daemon-reload
sudo systemctl enable qwen3-embedding.service qwen3-reranker.service qwen3-llm.service

sudo systemctl restart qwen3-embedding.service
sudo systemctl restart qwen3-reranker.service
sudo systemctl restart qwen3-llm.service

反代和鉴权

公网只用一个域名:

https://llm.example.com

本机端口:

8001  Qwen3-Embedding-8B   /v1/embeddings
8002  Qwen3-Reranker-4B    /v1/rerank
8003  Qwen3.6-27B          /v1/models, /v1/chat/completions, /v1/completions

Nginx 里按路径转发。Embedding 和 Rerank 传 X-Internal-API-Key,LLM 传 Authorization

location = /v1/embeddings {
    proxy_pass http://10.10.10.10:8001/v1/embeddings;
    proxy_set_header X-Internal-API-Key $http_x_internal_api_key;
    proxy_read_timeout 300s;
}

location = /v1/rerank {
    proxy_pass http://10.10.10.10:8002/v1/rerank;
    proxy_set_header X-Internal-API-Key $http_x_internal_api_key;
    proxy_read_timeout 300s;
}

location = /v1/models {
    proxy_pass http://10.10.10.10:8003/v1/models;
    proxy_set_header Authorization $http_authorization;
    proxy_read_timeout 300s;
}

location = /v1/chat/completions {
    proxy_pass http://10.10.10.10:8003/v1/chat/completions;
    proxy_buffering off;
    proxy_set_header Authorization $http_authorization;
    proxy_read_timeout 600s;
}

location = /v1/completions {
    proxy_pass http://10.10.10.10:8003/v1/completions;
    proxy_buffering off;
    proxy_set_header Authorization $http_authorization;
    proxy_read_timeout 600s;
}

如果上游配置里写过全局的:

proxy_set_header Authorization "";

LLM 的 location 里必须显式改回:

proxy_set_header Authorization $http_authorization;

否则 /v1/models/v1/chat/completions 会 401。

New API v0.13.2 接入

New API 里建议建两个渠道:

Jina 渠道   -> Qwen3-Embedding-8B + Qwen3-Reranker-4B
OpenAI 渠道 -> Qwen3.6-27B

Jina 渠道:

渠道类型: Jina
Base URL: https://llm.example.com
模型: Qwen3-Embedding-8B, Qwen3-Reranker-4B

Header Override:

{"X-Internal-API-Key":"{api_key}"}

OpenAI 渠道:

渠道类型: OpenAI
Base URL: https://llm.example.com
模型: Qwen3.6-27B

OpenAI 渠道默认会发:

Authorization: Bearer <key>

Base URL 不要带 /v1。正确写法是:

https://llm.example.com

验证

本机验证:

embedding_ok Qwen/Qwen3-Embedding-8B 2 1024 0.9999
rerank_ok Qwen/Qwen3-Reranker-4B [(0, 0.9971), (1, 0.0021)]
llm_ok Qwen3.6-27B 测试

公网验证:

models 200
chat 200
embeddings 200
rerank 200

LLM 上下文:

model Qwen3.6-27B
n_ctx 32768
n_ctx_train 262144
n_embd 5120

当前并发能力

现在 LLM 启动参数是:

--ctx-size 32768
--parallel 1

这意味着同一时刻只有 1 个 LLM 生成槽。多个用户同时请求时,不是并行生成,而是排队,或者在 New API / Nginx / 客户端侧超时。

按现在配置,比较实际的判断是:

LLM 同时活跃生成: 1 个请求
短问短答、低频内测: 约 3-5 人可以共用
偶发使用、不同时提问: 约 5-10 人可以挂着用
多人同时长文本生成: 不适合,会明显排队

Embedding 和 Reranker 在 GPU1,不会占 GPU0,但它们也是 BATCH_SIZE=1,更适合作为 RAG 链路里的低并发服务,不适合作为高 QPS 公共服务。

New API 侧建议:

LLM 渠道并发: 1
Embedding / Rerank 渠道并发: 1-2

这次需要注意的点

第一,ECC 状态要统一。一开始两张 V100 的 ECC 状态不一致,虽然服务也能跑,但显存和行为不够一致。机器还没进生产网,所以直接统一关闭 ECC。

第二,nvidia-v100-tuning.service 不能写 After=multi-user.target。这个会造成 systemd ordering cycle,重启时 Qwen 服务的启动任务可能被删掉。

第三,Hugging Face 大文件下载 403 不一定是文件坏了。很多时候是临时签名 URL 到期,重新跑同一条 aria2 命令即可断点续传。

第四,LLM 要保留 --reasoning off。否则短回答可能出现在 reasoning_content,New API 里看起来像没有正常返回。

第五,New API 里不要把所有模型都塞进一个 Jina 渠道。Embedding / Rerank 走 Jina,LLM 走 OpenAI,更清楚也更稳定。

后续可以继续做的事

这套方案现在适合小范围内测和自用。如果后面使用人数变多,我会优先考虑这几件事:

  1. 给 LLM 单独加队列和更明确的超时提示。
  2. 压测 --parallel 2,但前提是接受更短上下文或更多显存占用。
  3. Embedding / Rerank 做更严格的请求大小限制。
  4. 把服务部署脚本化,减少手工改 systemd 的步骤。
  5. WordPress 发布流程固定后,用 REST API 自动创建草稿,图片自动上传媒体库。

这次先把本地三模型服务打通,能被 New API 调用,能从公网白名单访问,也能在 32768 上下文下稳定返回。对两张 V100 来说,这个状态已经比较实用了。

此作者没有提供个人介绍。
最后更新于 2026-06-28