V100 单卡部署 Qwen3.8-27B(AWQ)到 vLLM 完整实战

121 阅读9分钟

V100 单卡部署 Qwen3.8-27B(AWQ)到 vLLM 完整实战

面向需要在老架构 GPU(NVIDIA V100,SM70)上本地部署大模型的读者。全程基于真实踩坑整理,每一步给出可复制的命令和解释。文中所有敏感信息(IP、账号、路径)已用占位符替换。

为什么选 V100 + 27B 这个组合

V100 是 Volta 架构(SM70),2024 年后部署大模型会遇到一系列兼容问题:vLLM 的量化内核默认要求 Turing 及以上(SM75+),FlashAttention 也需要专门的老架构后端。很多人因此放弃在老卡上跑新模型。

Qwen3.8-27B 是 27B 参数的稠密多模态模型,48 层线性注意力 + 16 层全注意力,原生 262K 上下文。配合 W4A16(4bit 权重)量化后约 19.5GB,单张 32GB 的 V100 理论上能装下——但要让 vLLM 真正跑起来,需要打通几个关键环节。本文完整记录这个过程。

环境前提

项目配置
GPUNVIDIA Tesla V100-SXM2-32GB(SM70)
系统Ubuntu(Linux)
推理框架1Cat-vLLM 1.2.2(带 SM70 支持的分支)
模型philbert440/Qwen3.8-27B-W4A16-AWQ(compressed-tensors 格式,W4A16)
下载通道hf-mirror(国内镜像)

说明:服务器上的 vLLM 命名为 1cat-vllm,是一个针对 V100(SM70)做过适配的 vLLM 分支,后面会用到它特有的 SM70 反量化开关。

第一步:下载模型

国内直连 Hugging Face 不稳定,统一走镜像 hf-mirror.com。同时新版 huggingface_hub 默认走 xet 传输协议,在国内会遇到 401 认证错误,需要显式关闭。

创建下载脚本 download_model.py:

import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"          # 指向国内镜像
os.environ["HF_HUB_DISABLE_XET"] = "1"                        # 关闭 xet 协议,回退传统 LFS 下载
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"              # 关闭进度条,方便日志输出
from huggingface_hub import snapshot_download

snapshot_download(
    "philbert440/Qwen3.8-27B-W4A16-AWQ",                     # 模型仓库 ID
    cache_dir="/data/models",                                 # 下载缓存目录,改成你自己的路径
    max_workers=4,                                            # 4 线程并行下载
)

逐行解释:

  • HF_ENDPOINT:把默认的 huggingface.co 换成 hf-mirror.com,国内可直连。
  • HF_HUB_DISABLE_XET:huggingface_hub 新版本用 xet 协议传输大文件,直连 cas-server.xethub.hf.co 会被墙导致 401 Unauthorized。置 1 后回退到普通 HTTPS 下载。
  • max_workers=4:并发下载小配置文件,明显加快整体速度(大文件本身单线程限速由带宽决定)。

执行:

python download_model.py

模型共 14 个文件,核心是 model.safetensors(约 18.7GB,量化后的主体权重)和 model-mtp.safetensors(约 0.85GB,MTP 推测解码头,保留 BF16)。

第二步:理解模型目录结构

snapshot_download 会生成 HuggingFace 缓存结构,vLLM 直接指向它即可加载:

/data/models/
└── models--philbert440--Qwen3.8-27B-W4A16-AWQ/
    └── snapshots/
        └── <commit哈希>/          # 具体权重快照
            ├── config.json        # 模型结构配置
            ├── model.safetensors  # 量化主体权重
            ├── model-mtp.safetensors
            └── tokenizer.json     # 分词器

加载时给 vLLM 的模型路径指向 <commit哈希> 这一层(含 config.json 的目录)。

第三步:编写 vLLM 启动脚本

vLLM 启动脚本是核心。下面是我调通后保留的 start_vllm.sh,每一行都有注释说明它在做什么:

#!/bin/bash
set -e                                            # 任何一步失败立即退出,避免带错继续跑
export HOME=/data/scripts                        # 脚本运行的工作目录
export CUDA_VISIBLE_DEVICES=0                    # 只用 GPU 0(单卡环境)
export PATH=<python环境>/bin:$PATH               # 把 1cat-vllm 的 python 放进 PATH

# ===== V100(SM70) 专属环境变量 =====
export TORCH_CUDA_ARCH_LIST="7.0"                # 告诉 PyTorch 目标架构是 SM70(V100)
export CC=/usr/bin/gcc-12                        # CUDA 12 需要 gcc-12 编译兼容
export CXX=/usr/bin/g++-12
# 关键:Qwen3.8 的 GatedDeltaNet 线性注意力在 SM70 需要全前向保护,否则会崩
export VLLM_SM70_QWEN_GDN_FULL_FORWARD=1
# 关键:compressed-tensors W4A16 量化的反量化走 SM70 专用 dequant 路径
export VLLM_SM70_COMPRESSED_TENSORS_TURBOMIND=1

LOG=vllm_server.log                             # 日志文件
PY=<python环境>/bin/python

# 模型路径和对外服务名,可在命令行覆盖
MODEL=${1:-/data/models/models--philbert440--Qwen3.8-27B-W4A16-AWQ/snapshots/<哈希>/}
SERVED=${2:-qwen3.8-27b}

# ===== 启动 vLLM =====
$PY -m vllm.entrypoints.openai.api_server \
  --model $MODEL \                              # 模型目录
  --served-model-name $SERVED \                 # 对外暴露的模型名(OpenAI API 里用它)
  --trust-remote-code \                         # 允许执行模型目录里的自定义代码
  --attention-backend FLASH_ATTN_V100 \         # V100 专用的 FlashAttention 后端
  --tensor-parallel-size 1 \                    # 单卡
  --gpu-memory-utilization 0.95 \               # 允许使用 95% 显存
  --max-model-len 180000 \                      # 最大上下文 180K
  --max-num-seqs 1 \                            # 单并发(本地使用够用)
  --max-num-batched-tokens 8192 \               # 单次 prefill 最多处理 8192 token
  --dtype half \                                # 权重以 FP16 计算
  --kv-cache-dtype fp8_e5m2 \                   # KV cache 用 8bit 存储,省显存
  --enforce-eager \                             # 关闭 CUDA Graph(V100 上不稳定)
  --enable-auto-tool-choice \                   # 让模型可自动调用工具
  --tool-call-parser qwen3_coder \              # 用 Qwen 官方的工具调用解析器
  --limit-mm-per-prompt '{"image": 8}' \        # 多模态:每个 prompt 最多 8 张图
  --host 0.0.0.0 \                              # 监听所有网卡
  --port 8000 \                                 # OpenAI API 端口
  >> $LOG 2>&1                                  # 日志追加写入

这段脚本里有三个 V100 专属的关键点,单独说明:

1. VLLM_SM70_QWEN_GDN_FULL_FORWARD=1 Qwen3.8 用的是混合注意力:48 层 GatedDeltaNet 线性注意力。在 SM70 上它的前向路径默认会走不兼容的分支导致崩溃,这个环境变量强制走"全前向"保护路径。

2. VLLM_SM70_COMPRESSED_TENSORS_TURBOMIND=1 这是最容易踩的坑。模型是 compressed-tensors 格式(W4A16 INT4),vLLM 默认检查要求 GPU 算力 ≥75(Turing 及以上),V100 只有 70,直接报 Quantization scheme is not supported... Min capability: 75。这个环境变量让 1cat-vLLM 走 SM70 的 TurboMind 反量化路径,把最低算力要求降到 70,V100 才能加载。

3. --attention-backend FLASH_ATTN_V100 标准 vLLM 的 FlashAttention 后端不支持 SM70,必须用这个针对 V100 的后端。

第四步:启动并验证

bash start_vllm.sh '/data/models/.../<哈希>/' qwen3.8-27b

这条命令把模型路径和服务名作为参数传给脚本,后台拉起 vLLM。启动约 1-2 分钟,等待期间可以看日志确认模型加载成功:

tail -f vllm_server.log

出现下面两行说明 SM70 路径生效、KV cache 用上了 8bit:

Using original FlashQLA-SM70 TileLang GDN prefill path
Using explicit fp8_e5m2 KV cache with a quantized checkpoint on SM70 Flash-V100

然后验证 API 是否就绪:

curl http://<服务器IP>:8000/v1/models

返回的 JSON 里应该看到 "id": "qwen3.8-27b" 和 "max_model_len": 180000。这条命令向 vLLM 询问已加载的模型列表,确认服务名和上下文长度正确。

试一句推理:

curl http://<服务器IP>:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"qwen3.8-27b","messages":[{"role":"user","content":"1+1=?"}],"max_tokens":20}'

这条命令调用 OpenAI 兼容的对话接口,model 字段填启动时指定的服务名。模型正常返回 "2" 即部署成功。

第五步:KV cache 8bit 量化

--kv-cache-dtype fp8_e5m2 的作用是把 KV cache 用 8bit 浮点存储,而不是全精度 FP16。日志会打印:

Using fp8_e5m2 data type to store kv cache. It reduces the GPU memory footprint and boosts the performance.

这条日志在说明 KV cache 已经用 FP8 存储,显存占用减半。对 27B 这种长上下文模型,KV cache 是显存大头之一,8bit 存储是拉长上下文的关键前提。V100 本身不支持原生 FP8 计算,但存储层量化没问题,vLLM 在读取时会反量化到 FP16 参与计算,对精度影响很小。

第六步:显存实测与参数调优

部署过程中用 vLLM 日志里的内存报告确认显存去向:

Model loading took 20.44 GiB memory
Available KV cache memory: 7.71 GiB
GPU KV cache size: 239,504 tokens

这三行日志含义:

  • 20.44 GiB:模型权重实际占用。注意比文件的 19.5GB 大,因为 vision tower、embed、lm_head、MTP 头这些层保留 BF16,在显存里是 16bit 而非 4bit。
  • 7.71 GiB:vLLM 算完权重和激活后,留给 KV cache 的空间。
  • 239,504 tokens:这 7.71GB 能存下多少 KV(token 数)。

gpu_memory_utilization 与 max_num_batched_tokens 的关系

两个参数一起决定 KV cache 能有多少:

gpu_memory_utilizationmax_num_batched_tokensKV 可用能支撑的上下文
0.9081926.12 GiB约 190K
0.9581927.71 GiB约 239K
0.9820488.67 GiB约 272K
0.95163846.19 GiB约 191K
0.95327682.81 GiB约 81K(180K 放不下)

数据来自真实启动日志。规律很明确:

  • gpu_memory_utilization 越大,KV 空间越大,但挤压框架和激活的余量,OOM 风险升高。
  • max_num_batched_tokens 越大,单次 prefill 处理越快,但 vLLM 要预留更多临时激活显存,KV 空间被吃掉。8192→16384 少 1.5GB,16384→32768 直接少 3.4GB。

实测结论:180K 上下文 + 0.95 利用率下,max_num_batched_tokens 取 8192 是最佳平衡点——KV 有 7.71GB(180K 只需约 5.7GB),富余且安全,prefill 吞吐也正常。16384 能跑但 KV 余量只剩 0.4GB;32768 在 180K 下直接启动失败。V100 算力(FP16 约 112 TFLOPS)下 8192 token 的 batch 已经能让计算单元接近饱和,再加大收益有限。

第七步:把上下文从 64K 拉到 256K

Qwen3.8-27B 原生支持 262K 上下文,但单卡 V100 能不能装下取决于 KV cache 显存。直接设 --max-model-len 262144 启动会报错:

ValueError: To serve at least one request with the model's max seq len (262144),
(8.33 GiB KV cache is needed), which is larger than the available KV cache memory (6.12 GiB).
Based on the available memory, the estimated maximum model length is 189728.

这段报错信息量很大:vLLM 直接告诉你 256K 需要 8.33GB KV,当前只剩 6.12GB,当前显存最多支持 189K。256K 在默认配置下单卡放不下。

解决办法:把 gpu_memory_utilization 提到 0.98 且 max_num_batched_tokens 降到 2048,KV 可用升到 8.67GB,就能装下 256K(需 8.33GB)。实测成功:

Available KV cache memory: 8.67 GiB
GPU KV cache size: 272,690 tokens
max_model_len: 262144

但要说明两点:0.98 很激进,只剩约 2GB 余量,长上下文 prefill 有 OOM 风险;且 256K 预填充在 V100 上很慢。日常用 180K + 0.95 + 8192 是更均衡的配置(就是本文默认的启动脚本)。

踩坑总结

问题现象解决
下载 401CAS Client Error: 401 Unauthorized加 HF_HUB_DISABLE_XET=1 走镜像回退下载
算力不兼容Quantization scheme is not supported... Min capability: 75加 VLLM_SM70_COMPRESSED_TENSORS_TURBOMIND=1
线性注意力崩溃GDN 层报错加 VLLM_SM70_QWEN_GDN_FULL_FORWARD=1
FlashAttention 不支持注意力后端初始化失败用 --attention-backend FLASH_ATTN_V100
256K 显存不足8.33 GiB KV cache is needed...提到 0.98 + 降 batched tokens,或降到 180K
64K 太小日常长文不够按显存把 max-model-len 提到 180K

一句话总结

单张 V100(SM70)完全能跑 Qwen3.8-27B 到 180K 上下文,核心是三件事:用 1cat-vLLM 分支、开 SM70 反量化开关、按实测显存把 gpu_memory_utilization 和 max_num_batched_tokens 调到平衡点。256K 也能跑,但要牺牲一点稳定性。希望这篇能帮到同样在老卡上折腾部署的人。