这次是在 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-8B 和 Qwen3-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,更清楚也更稳定。
后续可以继续做的事
这套方案现在适合小范围内测和自用。如果后面使用人数变多,我会优先考虑这几件事:
- 给 LLM 单独加队列和更明确的超时提示。
- 压测
--parallel 2,但前提是接受更短上下文或更多显存占用。 - Embedding / Rerank 做更严格的请求大小限制。
- 把服务部署脚本化,减少手工改 systemd 的步骤。
- WordPress 发布流程固定后,用 REST API 自动创建草稿,图片自动上传媒体库。
这次先把本地三模型服务打通,能被 New API 调用,能从公网白名单访问,也能在 32768 上下文下稳定返回。对两张 V100 来说,这个状态已经比较实用了。

Comments NOTHING