系列位置:与同专栏「引子篇」「12GB 上 QLoRA / 训练环境篇」衔接;未读前文也可单篇跟做,训练侧需已有一份全精度底座与(可选)两份 LoRA adapter。
摘要 :HF 拉全精度底座 → INT4(W4A16)量化 →vllm serve→ 双 QLoRA 按请求model切换 。含校准集下载卡住时的处理、--max-model-len必写 、避开vllm._C的工作目录、adapter 与整模区别、curl / 鉴权 / OpenAI 客户端示例与合并踩坑表。
为什么推理要和训练「分家」;vLLM 和 Ollama 不是一条协议
训练侧若用 conda (例如名为 rag-ft 的环境)已跑通 QLoRA,推理仍建议单独建 venv (例如 python -m venv .venv),避免和 transformers / trl / bitsandbytes 抢版本。
安装(只当用户、不开发 vLLM 源码时最常见):
bash
source /path/to/vllm-venv/bin/activate
pip install vllm
若要从 vLLM 源码树可编辑安装,按官方仓库的 installation 文档走(pip install -e . 等),此处不展开。
装完确认:
bash
vllm --help
vllm serve --help
和 Ollama :vLLM 提供的是 /v1/chat/completions 这类 OpenAI 兼容 HTTP ;Ollama 是另一套 API。业务里并行实验时各接各的,不必把 vLLM 硬塞进 Ollama 适配层。
起服前照旧 nvidia-smi ,对空闲显存有数。
量化之前:用 Hugging Face 把全精度底座拉到本地
INT4 脚本、from_pretrained、vLLM 第一条参数,都要 标准 HF 模型目录 。Hub 上的 Instruct / Thinking 等不同后缀仓库,下载时把模型 id 换成你实际使用的那一个即可。
工具
bash
pip install -U "huggingface_hub[cli]" hf_transfer
先登录(Thinking / 门控仓库强烈建议)
CLI 拉大仓库时,先登录 能明显减少中途断流、权限被拒 。huggingface-cli login 或新版 hf auth login ;也可设环境变量 HF_TOKEN。私有/门控模型必须 token。
下载
bash
export HF_HUB_ENABLE_HF_TRANSFER=1
mkdir -p /path/to/models
经典 / 新版 CLI 二选一 (以本机 hf --help 为准):
bash
huggingface-cli download Qwen/Qwen3-4B-Thinking-2507 \
--local-dir /path/to/models/base/Qwen3-4B-Thinking-2507 \
--local-dir-use-symlinks False
# 或:hf download Qwen/Qwen3-4B-Thinking-2507 \
# --local-dir /path/to/models/base/Qwen3-4B-Thinking-2507
国内可配 HF_ENDPOINT 或镜像(如 https://hf-mirror.com,以你网络为准)。
可选:Transformers 能否加载
bash
python - <<'PY'
from transformers import AutoTokenizer, AutoModelForCausalLM
model_dir = "/path/to/models/base/Qwen3-4B-Thinking-2507"
tok = AutoTokenizer.from_pretrained(model_dir, use_fast=True)
_ = AutoModelForCausalLM.from_pretrained(model_dir, device_map="auto", torch_dtype="auto")
print("HF model load ok")
PY
显存紧时只测 tokenizer 或 device_map="cpu"。
建议的目录约定(底座 / 量化产物 / LoRA / merge)
方便和后文命令对齐,可按需建:
bash
mkdir -p /path/to/models/base /path/to/models/int4 \
/path/to/models/lora /path/to/models/merged /path/to/data/qlora
- 全精度底座 :例如
.../base/Qwen3-4B-Thinking-2507 - INT4 输出 :例如
.../int4/Qwen3-4B-Thinking-2507-W4A16-G128 - 两份 adapter :例如
.../lora/adapter-a、.../lora/adapter-b(各含adapter_config.json、权重) - merge 产物 (可选):
.../merged/...
INT4(W4A16)量化:依赖、脚本与校准集下载
硬件
vLLM 侧 INT4 W4A16 通常要求 NVIDIA Ampere 及以上(compute capability > 8.0);新卡对照官方量化文档。
依赖
在已激活的 vLLM/量化用 venv里:
bash
pip install llmcompressor datasets
最小量化脚本
将下面保存为例如 scripts/quantize_int4.py,把 MODEL_ID / SAVE_DIR 改成你的路径(MODEL_ID 指上节拉下来的 全精度 HF 目录):
python
from pathlib import Path
from datasets import load_dataset
from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import GPTQModifier
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "/path/to/models/base/Qwen3-4B-Thinking-2507"
SAVE_DIR = "/path/to/models/int4/Qwen3-4B-Thinking-2507-W4A16-G128"
NUM_CALIBRATION_SAMPLES = 128
MAX_SEQUENCE_LENGTH = 2048
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="auto",
dtype="auto",
trust_remote_code=True,
)
ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft")
ds = ds.shuffle(seed=42).select(range(NUM_CALIBRATION_SAMPLES))
def preprocess(example):
text = tokenizer.apply_chat_template(example["messages"], tokenize=False)
return {"text": text}
def tokenize(sample):
return tokenizer(
sample["text"],
padding=False,
max_length=MAX_SEQUENCE_LENGTH,
truncation=True,
add_special_tokens=False,
)
ds = ds.map(preprocess)
ds = ds.map(tokenize, remove_columns=ds.column_names)
recipe = GPTQModifier(targets="Linear", scheme="W4A16", ignore=["lm_head"])
Path(SAVE_DIR).mkdir(parents=True, exist_ok=True)
oneshot(
model=model,
dataset=ds,
recipe=recipe,
max_seq_length=MAX_SEQUENCE_LENGTH,
num_calibration_samples=NUM_CALIBRATION_SAMPLES,
)
model.save_pretrained(SAVE_DIR, save_compressed=True)
tokenizer.save_pretrained(SAVE_DIR)
print(f"INT4 model saved to: {SAVE_DIR}")
执行:
bash
python scripts/quantize_int4.py
卡在 HuggingFaceH4/ultrachat_200k 时的三条路(很重要)
量化脚本默认要从 Hub 拉 ultrachat_200k,网络不稳时会一直卡。可以:
-
最快先试 :若脚本或环境支持 离线 fallback (或你已缓存过数据),可强制离线试跑:
HF_HUB_OFFLINE=1 python scripts/quantize_int4.py(若脚本报缺数据,再用 2/3。)
-
镜像 :
export HF_ENDPOINT=https://hf-mirror.com
python scripts/quantize_int4.py(镜像域名以你环境为准。)
-
预下载数据集到本地 ,再改脚本里
load_dataset指向本地目录(最稳、最利于复现):
bash
huggingface-cli download HuggingFaceH4/ultrachat_200k \
--repo-type dataset \
--local-dir /path/to/models/datasets/ultrachat_200k
下载完成后在脚本中改为从该路径加载(具体 API 以 datasets 文档为准,例如 load_dataset("path/to/dir", ...))。
量化阶段 CUDA OOM :减少 NUM_CALIBRATION_SAMPLES、缩短 MAX_SEQUENCE_LENGTH、或换更小底座试跑。
成功后应得到完整 INT4 目录,例如:.../int4/Qwen3-4B-Thinking-2507-W4A16-G128。
vllm serve:单底座验收(INT4 已就绪)
工作目录(防 vllm._C)
不要在 vLLM 源码仓库根目录里直接起服务 ,容易 import 到未编译的本地包。习惯:cd /tmp (或任意干净目录),再用 venv 里的 vllm 可执行文件绝对路径。
12GB 必写 --max-model-len
有的模型 config.json 里上下文极大 (例如量级 262144 )。若不显式压 --max-model-len ,vLLM 会按超大上下文去预留 KV,直接报 需要几十 GiB KV cache 而启动失败 。务必写成业务可承受的值(如 2048 / 4096),再按显存微调。
起服示例(两档,按 OOM 情况试)
较宽松(示例):
bash
cd /tmp
/path/to/vllm-venv/bin/vllm serve /path/to/models/int4/Qwen3-4B-Thinking-2507-W4A16-G128 \
--served-model-name qwen3-4b-int4 \
--host 127.0.0.1 \
--port 8000 \
--dtype auto \
--max-model-len 2048 \
--gpu-memory-utilization 0.75
仍紧张时再压上下文 + 降 utilization + enforce-eager(依官方说明与实测):
bash
cd /tmp
/path/to/vllm-venv/bin/vllm serve /path/to/models/int4/Qwen3-4B-Thinking-2507-W4A16-G128 \
--served-model-name qwen3-4b-int4 \
--host 127.0.0.1 \
--port 8000 \
--dtype auto \
--max-model-len 1024 \
--gpu-memory-utilization 0.65 \
--enforce-eager
说明:
--served-model-name:客户端 JSON 里的model字段写这个短名最省事。- 鉴权 :需要时加
--api-key my-secret,请求头带Authorization: Bearer my-secret。 - 端口占用 :改
--port 8001等。
进程默认占前台;长期跑可用 tmux / systemd / nohup 自行包一层。
验收:OpenAI 兼容 curl / Python
curl(无鉴权)
bash
curl -sS http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-4b-int4",
"messages": [{"role": "user", "content": "用一句话介绍你自己。"}],
"max_tokens": 64
}'
返回 JSON 含 choices 即通过。model 须与 --served-model-name(或未改时的默认名)一致。
curl(启用了 --api-key)
bash
curl -sS http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-secret" \
-d '{
"model": "qwen3-4b-int4",
"messages": [{"role": "user", "content": "用一句话介绍你自己。"}],
"max_tokens": 64
}'
OpenAI Python 客户端(可选)
bash
pip install openai
python
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="not-needed", # 未设 --api-key 时可占位
)
r = client.chat.completions.create(
model="qwen3-4b-int4",
messages=[{"role": "user", "content": "你好,简单回一句。"}],
max_tokens=64,
)
print(r.choices[0].message.content)
若启用了 --api-key,把 api_key= 换成真实 token。
提醒 :请求里的 max_tokens 不能超过服务端的有效上下文预算;输入长度 + max_tokens 须在 max-model-len 之内,否则易报错或截断异常。
双 QLoRA:结论、起服与请求
先把概念说清楚(避免和「整模」打架)
- 可行 :一个 INT4(或 BF16)底座 + 两份 QLoRA adapter,按请求切换。
- vLLM 原生 :
--enable-lora+--lora-modules,不必先 merge。 - 「adapter 能用」 :指 serve 时 base 路径 + 运行时挂上 LoRA。
- 「adapter 不能当整模」 :指 不能把 adapter 目录单独当成
vllm serve的第一个 model 路径(那只是一包增量权重)。 - merge 时机 :要交付单一整模目录 、或要简化线上 LoRA 管理、或发版冻结时再做;日常迭代优先不 merge。
训练侧产出自检(两份 adapter 已训练完成时)
双 LoRA 上线前,建议确认:
- 两个目录各有
adapter_model.safetensors(或等价) 、adapter_config.json - 有训练日志;
base_model、数据版本、超参、A/B 任务边界有记录 - 底座与 adapter 兼容性 :在 BF16 全量底座 上训的 adapter,挂到 INT4 整模 上有时要对齐实测;更稳是 量化底座与训练底座同源、版本锁定
起服:同一进程挂两份 LoRA
bash
cd /tmp
/path/to/vllm-venv/bin/vllm serve /path/to/models/int4/Qwen3-4B-Thinking-2507-W4A16-G128 \
--host 127.0.0.1 \
--port 18080 \
--dtype auto \
--max-model-len 2048 \
--gpu-memory-utilization 0.65 \
--enable-lora \
--lora-modules \
domain-a=/path/to/lora/adapter-a/final \
domain-b=/path/to/lora/adapter-b/final
注意:
--host只能是 IP/主机名 ,端口用--port,勿写成--host 18080。--lora-modules:name=path,多组空格 分隔;路径里有空格时整段加引号。- 双 LoRA 更吃显存 :
max-model-len与gpu-memory-utilization要一起拧;max-model-len仍必写,避免 config 默认超大上下文直接把 KV 撑爆。
请求里用 model 切 adapter
bash
curl -sS http://127.0.0.1:18080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "domain-a",
"messages": [{"role": "user", "content": "请回答 A 领域问题"}],
"max_tokens": 128
}'
"model": "domain-b" 即换另一份 LoRA。
何时再 merge
- 给外部只认「一个模型目录」时
- 要减少运行时 LoRA 管理复杂度时
- 最终版本冻结时
评测与迭代阶段默认 不 merge。
踩坑速查(合并版)
| 现象 | 方向 |
|---|---|
vllm._C / import 异常 |
离开源码根目录;用 venv 里安装的 vllm |
| 启动报巨大 KV / GiB cache | 写小 --max-model-len;检查 config 默认上下文是否离谱 |
| CUDA OOM(推理) | 降 max-model-len、gpu-memory-utilization;双 LoRA 更保守;试 --enforce-eager |
| CUDA OOM(量化) | 减校准条数、缩短序列、换小底座试跑 |
| 量化卡在 ultrachat | HF_HUB_OFFLINE=1 、HF_ENDPOINT 镜像 、或 预下载数据集改本地路径(见上文 §) |
| 量化脚本报错 | 确认 llmcompressor;GPU 架构是否满足 INT4 |
| Chat / 模板报错 | 模型需 chat template ;少数需 --chat-template 指向模板文件(见 vLLM OpenAI 兼容服务文档) |
| 客户端 model 对不上 | model 对齐 --served-model-name 或 --lora-modules 里的 name |
| 端口占用 | 换 --port |
建议在笔记里固定记录(复现用)
| 项 | 内容 |
|---|---|
完整 vllm serve |
int4 路径、served-model-name、lora-modules、max-model-len、port、是否 enforce-eager |
| 显存 | idle / 首 token / 稳态 |
| 路由规则 | 何种请求走 domain-a / domain-b |
| 版本 | vLLM、驱动、底座 commit、adapter 训练快照是否同源 |