# 双卡 A100 80G 全栈升级方案（完整详细版）

## Context

机器升级到 A100×2 80G（总显存 160GB），当前单卡已用 56GB。
目标：推理/embedding/reranker 全部换成 Qwen3 系列，同时支持双卡微调。
现有架构：vLLM(8100) + embedding_server.py(8200) + rerank_server.py(8300) + LLaMA-Factory(7860，训练时切换)。

---

## 一、模型性能对比分析

### 1.1 推理模型：Qwen3.5-27B-AWQ vs Qwen3.6-27B BF16

| 维度 | Qwen3.5-27B-AWQ | Qwen3.6-27B BF16 | 提升 |
|------|----------------|-----------------|------|
| 精度 | INT4（AWQ 量化） | BF16（全精度） | 质量提升显著 |
| 显存占用 | ~15-18 GB | ~54 GB | 增加 ~36GB |
| 推理速度（tokens/s） | ~80-120 | ~50-80 | 略慢（精度换速度） |
| 数学/推理能力 | 基准 | +5-10%（量化损失恢复） | 明显 |
| 代码生成 | 基准 | +3-8% | 明显 |
| 中文理解 | 基准 | +3-5% | 轻微 |
| 长文本一致性 | 基准 | +8-12%（量化在长文本劣化更多） | 明显 |
| 工具调用准确率 | 基准 | +5-8% | 明显 |

**结论**：AWQ INT4 量化会导致约 3-8% 的综合性能损失，在复杂推理、长文本、工具调用场景损失更大。换 BF16 后这些场景会有明显提升，但吞吐量略降（可通过双卡 tp=2 的并行度部分补偿）。

---

### 1.2 Embedding 模型：bge-m3 vs Qwen3-Embedding-8B

| 维度 | bge-m3 | Qwen3-Embedding-8B | 对比 |
|------|--------|-------------------|------|
| 参数量 | 568M | 8B | Qwen3 大 14x |
| 向量维度 | **1024** | **4096** | Qwen3 高 4x |
| 显存占用（FP16） | ~1.2 GB | ~16 GB | Qwen3 多 ~15GB |
| MTEB 中文榜 | ~65-68 | ~72-75（估算） | Qwen3 高 ~7% |
| 多语言支持 | 100+ 语言 | 主要中英文 | bge-m3 更广 |
| 最大输入长度 | 8192 tokens | 32768 tokens | Qwen3 长 4x |
| 语义理解深度 | 基准 | 更强（8B 参数） | Qwen3 明显更好 |
| instruction-aware | 不支持 | 支持（query/passage 区分） | Qwen3 更精准 |

**重要注意**：向量维度从 1024 → 4096，**已有知识库的向量索引必须全部重建**，否则维度不匹配无法检索。

### 1.2.1 向量维度提升的影响分析

**对向量化（Embedding）的影响**

| 影响维度 | 1024 维（bge-m3） | 4096 维（Qwen3-Embedding-8B） |
|---------|-----------------|------------------------------|
| 语义区分度 | 相似文本容易混淆 | 细粒度语义差异可区分 |
| 同义词/近义词处理 | 基础 | 更精准 |
| 长文本语义压缩 | 信息损失较多 | 保留更多细节 |
| 存储空间（每条向量） | 4KB（float32） | 16KB（float32） |
| 建库速度 | 快 | 慢约 2-3x |
| 向量索引大小 | 基准 | 4x 更大 |

**对检索（RAG 推理）的影响**

1. **召回精度提升**：4096 维向量在高维空间中余弦相似度计算更能区分真正相关和表面相似的文档，减少噪声召回。

2. **检索速度略降**：向量维度 4x，FAISS/pgvector 的 ANN 搜索计算量增加，单次检索耗时约增加 1.5-2x。对于税务知识库这类中小规模（<100万条）影响不大。

3. **知识库必须重建**：1024 维索引和 4096 维向量完全不兼容，切换后所有文档必须重新 embed 并重建索引，这是本次升级最重要的前置操作。

4. **query/passage 分离的额外收益**：Qwen3-Embedding-8B 支持 instruction-aware，检索时 query 用 `prompt_name="query"`，建库时用 `prompt_name="passage"`，两者在向量空间中经过专门对齐，在非对称检索场景（短问题 vs 长文档）效果提升比 bge-m3 更明显。

**实际建议**：知识库重建前，先用少量样本对比两个模型的召回效果，确认 Qwen3-Embedding-8B 在税务文档上确实有提升，再全量重建。

---

### 1.3 Reranker 模型：bge-reranker-v2-m3 vs Qwen3-Reranker-8B

| 维度 | bge-reranker-v2-m3 | Qwen3-Reranker-8B | 对比 |
|------|-------------------|------------------|------|
| 参数量 | 568M | 8B | Qwen3 大 14x |
| 显存占用（FP16） | ~1.2 GB | ~16 GB | Qwen3 多 ~15GB |
| BEIR 榜 nDCG@10 | ~54-56 | ~60-63（估算） | Qwen3 高 ~7% |
| 中文重排效果 | 基准 | 明显更好 | Qwen3 优 |
| 最大输入长度 | 512 tokens | 1024 tokens（建议配置） | Qwen3 长 2x |
| 推理速度 | 快（模型小） | 慢约 3-5x | bge 更快 |
| 长文档重排 | 截断损失大 | 更完整 | Qwen3 优 |

---

### 1.4 多轮对话能力对比

#### AWQ 量化对多轮对话的影响

多轮对话是量化损失最明显的场景之一，原因在于：

1. **上下文累积误差**：每一轮的输出作为下一轮的输入，INT4 量化的精度误差会随轮次累积放大。BF16 全精度在长对话中保持更稳定的语义一致性。

2. **KV Cache 精度**：vLLM 的 `--kv-cache-dtype auto` 在 BF16 模型下会使用 BF16 存储 KV cache，比 AWQ 模型下的 FP16/INT8 KV cache 精度更高，长对话中历史信息保留更准确。

3. **指令跟随一致性**：量化模型在多轮中容易"忘记"早期轮次的约束（如角色设定、格式要求）。BF16 在这方面表现更稳定。

| 多轮对话维度 | Qwen3.5-27B-AWQ | Qwen3.6-27B BF16 | 提升幅度 |
|------------|----------------|-----------------|---------|
| 短对话（≤5轮）一致性 | 良好 | 良好 | 轻微 |
| 中等对话（5-20轮）一致性 | 中等，偶有漂移 | 好，稳定 | +10-15% |
| 长对话（20轮+）一致性 | 明显漂移 | 较稳定 | +20-30% |
| 角色/指令跟随保持 | 基准 | 明显更好 | +10-20% |
| 上下文引用准确率 | 基准 | 更准确 | +8-15% |
| 32K 长上下文利用 | 受量化限制 | 充分利用 | 显著 |

#### 与 max-model-len 的关系

当前配置 `--max-model-len 32768`，BF16 模型能更充分利用这 32K 上下文窗口：

- AWQ 量化在超过 16K tokens 的上下文时，注意力计算精度下降明显
- BF16 在 32K 范围内注意力精度均匀，多轮对话历史可以被更准确地引用

#### 对本项目的实际影响

项目中 `tool-call-parser qwen3_coder` + `reasoning-parser qwen3` 的组合说明有工具调用场景。多轮工具调用（如连续查税务数据、多步计算）在 BF16 下：
- 工具调用参数生成更准确（减少 JSON 格式错误）
- 多步推理链（chain-of-thought）更连贯
- 中间结果引用更准确

---

## 二、显存分配详细规划

### 2.1 推理模式（日常运行）

```
GPU 0 (80GB)
├── Qwen3.6-27B BF16 (tp=2, 本卡 ~27GB)
├── Qwen3-Embedding-8B FP16 (~16GB)
├── CUDA context + 碎片 (~2GB)
└── 剩余 ~35GB → vLLM KV cache (gpu_memory_utilization=0.50 控制)

GPU 1 (80GB)
├── Qwen3.6-27B BF16 (tp=2, 本卡 ~27GB)
├── Qwen3-Reranker-8B FP16 (~16GB)
├── CUDA context + 碎片 (~2GB)
└── 剩余 ~35GB → vLLM KV cache
```

`gpu-memory-utilization=0.50` 的含义：vLLM 在每张卡上最多使用 50% 显存（40GB）用于模型权重 + KV cache。模型权重约 27GB，剩余 ~13GB 给 KV cache，支持约 32K context × 32 并发。

### 2.2 训练模式（微调时）

```bash
bash switch_mode.sh training  # 停止 vLLM，释放 ~54GB
```

```
GPU 0 (80GB)
├── Qwen3-Embedding-8B (~16GB，保持运行)
├── Qwen3.6-27B 训练（DDP rank 0）
│   ├── 模型权重 BF16: ~54GB / 2 = ~27GB
│   ├── 梯度: ~27GB
│   ├── 优化器状态: ~54GB（Adam，可用 8-bit Adam 减半）
│   └── 激活值（gradient checkpointing ON）: ~3-5GB
└── 合计约 ~57-75GB → 需要 8-bit Adam + gradient checkpointing

GPU 1 (80GB)
├── Qwen3-Reranker-8B (~16GB，保持运行)
└── Qwen3.6-27B 训练（DDP rank 1）：同上
```

**训练显存优化必须开启**：
- `gradient_checkpointing: true`（激活值重计算，省 ~30% 显存）
- `optim: adamw_8bit`（8-bit Adam，优化器状态省 ~50%）
- `lora_rank: 16`（只训练 LoRA 参数，大幅减少梯度显存）

---

## 三、参数配置详解

### 3.1 vLLM 启动参数（`start_vllm.sh`）

```bash
python -m vllm.entrypoints.openai.api_server \
  --model /lsinfo/ai/hellotax_ai/llm_service/base_models/Qwen3.6-27B \
  --host 0.0.0.0 \
  --port 8100 \
  --served-model-name Qwen3.6-27B \
  --tensor-parallel-size 2 \        # 双卡 tensor parallel
  --max-model-len 32768 \            # 最大上下文长度
  --dtype bfloat16 \                 # 明确指定 BF16
  --gpu-memory-utilization 0.50 \   # 每卡 50%，给 embedding/reranker 留空间
  --kv-cache-dtype auto \
  --max-num-seqs 32 \
  --trust-remote-code \
  --api-key sk-local \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --reasoning-parser qwen3
  # 去掉：--quantization awq
  # 去掉：--enforce-eager
  # 去掉：--disable-custom-all-reduce（多卡需要 NCCL）
  # 暂时去掉：--speculative-config（tp=2 兼容性待验证）
```

**参数说明**：
- `--tensor-parallel-size 2`：模型权重按 attention head 切分到两卡，每卡只存一半权重
- `--gpu-memory-utilization 0.50`：vLLM 在每卡上预分配 40GB，其中 ~27GB 是模型权重，~13GB 是 KV cache
- `--dtype bfloat16`：BF16 比 FP16 数值范围更大，Qwen3 官方推荐

### 3.2 Embedding 服务参数（`start_embedding_vllm.sh`）

```bash
export EMBEDDING_MODEL_PATH=/lsinfo/ai/hellotax_ai/llm_service/base_models/Qwen3-Embedding-8B
export SERVED_MODEL_NAME=Qwen3-Embedding-8B
export CUDA_VISIBLE_DEVICES=0   # 固定 GPU 0
```

`embedding_server.py` 中 encode 调用需区分 query/passage：
```python
# query 时（检索问题）
embeddings = model.encode(texts, prompt_name="query", normalize_embeddings=True)
# passage 时（建库文档）
embeddings = model.encode(texts, prompt_name="passage", normalize_embeddings=True)
```

### 3.3 Reranker 服务参数（`start_reranker_vllm.sh`）

```bash
export RERANK_MODEL_PATH=/lsinfo/ai/hellotax_ai/llm_service/base_models/Qwen3-Reranker-8B
export SERVED_MODEL_NAME=Qwen3-Reranker-8B
export CUDA_VISIBLE_DEVICES=1   # 固定 GPU 1
```

`rerank_server.py` 中 tokenizer max_length 从 512 → 1024：
```python
inputs = tokenizer(pairs, padding=True, truncation=True, max_length=1024, ...)
```

### 3.4 LLaMA-Factory 微调参数（`training_configs/qwen_sft.yaml`）

```yaml
### Model
model_name_or_path: /lsinfo/ai/hellotax_ai/llm_service/base_models/Qwen3.6-27B
trust_remote_code: true

### Method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 16
lora_target: all
# 去掉 quantization_bit: 8，双卡 BF16 不需要量化

### 显存优化（必须开启）
bf16: true
gradient_checkpointing: true
optim: adamw_8bit

### Dataset
dataset: identity,alpaca_en_demo
template: qwen
cutoff_len: 4096
max_samples: 1000

### Output
output_dir: /lsinfo/ai/hellotax_ai/llm_service/trained_models/qwen3.6-27B-sft-lora
logging_steps: 10
save_steps: 500
overwrite_output_dir: true

### Train
per_device_train_batch_size: 1
gradient_accumulation_steps: 8    # 等效 batch_size = 1×8×2卡 = 16
learning_rate: 1.0e-4
num_train_epochs: 3
lr_scheduler_type: cosine
warmup_ratio: 0.1
ddp_timeout: 180000000
ddp_find_unused_parameters: false  # 双卡 DDP 必须加
```

`run_training.sh` 改用 torchrun 双卡启动：
```bash
torchrun --nproc_per_node=2 \
  "${INSTALL_DIR}/src/llamafactory/launcher.py" "${CONFIG_FILE}" 2>&1 | tee "${LOG_FILE}"
```

---

## 四、训练/推理切换流程

### 4.1 切换到训练模式

```bash
bash scripts/llm/switch_mode.sh training
```

内部流程（`switch_mode.sh` 已实现）：
1. Redis 写 `llm:service:status = switching`
2. 停止 vLLM（kill PID，释放 ~54GB）
3. 启动 LLaMA-Factory WebUI（7860）
4. Redis 写 `llm:service:status = training`
5. embedding(8200) 和 reranker(8300) **保持运行不停**

然后执行微调：
```bash
bash scripts/llm/run_training.sh sft
# 或 DPO
bash scripts/llm/run_training.sh dpo
```

### 4.2 切换回推理模式

```bash
bash scripts/llm/switch_mode.sh inference
```

内部流程：
1. Redis 写 `llm:service:status = switching`
2. 停止 LLaMA-Factory WebUI
3. 启动 vLLM（双卡 tp=2）
4. Redis 写 `llm:service:status = inference`

### 4.3 API 自动触发切换

`training_center` 通过 `LlamaFactoryPlatform._notify_switch_mode()` 在任务提交时自动调用：
```
POST http://localhost:8000/internal/switch_mode
{"mode": "training", "training_info": {...}}
```
训练完成且无 pending 任务时自动切回 inference。需确保 `INTERNAL_API_TOKEN` 环境变量已设置。

---

## 五、改动文件清单

| 文件 | 改动内容 |
|------|---------|
| `scripts/llm/start_vllm.sh` | 模型路径、去量化、加 tp=2、调 gpu_util、去 enforce-eager |
| `scripts/llm/start_embedding_vllm.sh` | 加环境变量、固定 CUDA_VISIBLE_DEVICES=0 |
| `scripts/llm/start_reranker_vllm.sh` | 加环境变量、固定 CUDA_VISIBLE_DEVICES=1 |
| `llm_service/servers/embedding_server.py` | 默认路径/模型名、加 input_type 字段、encode 传 prompt_name |
| `llm_service/servers/rerank_server.py` | 默认路径/模型名、max_length 512→1024 |
| `scripts/llm/switch_mode.sh` | 更新模型路径说明文字 |
| `scripts/llm/run_training.sh` | llamafactory-cli → torchrun --nproc_per_node=2 |
| `base_platform/app/db/init_db.py` | MODEL_SEEDS 三处模型名/ID 更新 |

---

## 六、前提条件

1. 三个模型已下载到 `base_models/`：
   ```bash
   python -c "
   from modelscope import snapshot_download
   snapshot_download('Qwen/Qwen3.6-27B',       local_dir='Qwen3.6-27B')
   snapshot_download('Qwen/Qwen3-Embedding-8B', local_dir='Qwen3-Embedding-8B')
   snapshot_download('Qwen/Qwen3-Reranker-8B',  local_dir='Qwen3-Reranker-8B')
   "
   ```
2. `nvidia-smi` 可见两张 A100（CUDA device 0 和 1）
3. `sentence-transformers >= 3.0`（支持 `prompt_name` 参数）
   ```bash
   /lsinfo/ai/hellotax_ai/llm_service/venv_embed/bin/pip install -U sentence-transformers
   ```
4. 知识库向量索引需重建（维度 1024 → 4096 不兼容）

---

## 七、验证步骤

```bash
# 1. 停止所有服务
bash scripts/llm/stop_all.sh

# 2. 按顺序启动（embedding/reranker 先占卡，再启 vLLM）
bash scripts/llm/start_embedding_vllm.sh
bash scripts/llm/start_reranker_vllm.sh
bash scripts/llm/start_vllm.sh

# 3. 确认显存分布（每卡约 43GB）
nvidia-smi

# 4. 验证推理
curl http://localhost:8100/v1/models
curl -H "Authorization: Bearer sk-local" \
  http://localhost:8100/v1/chat/completions \
  -d '{"model":"Qwen3.6-27B","messages":[{"role":"user","content":"你好"}]}'

# 5. 验证 embedding（注意 input_type，返回维度应为 4096）
curl http://localhost:8200/v1/embeddings \
  -d '{"input":"税务筹划问题","input_type":"query"}'

# 6. 验证 reranker
curl http://localhost:8300/v1/rerank \
  -d '{"query":"增值税","documents":["增值税是流转税","企业所得税计算"]}'

# 7. 验证微调切换
bash scripts/llm/switch_mode.sh training
nvidia-smi  # 确认 vLLM 已释放显存
bash scripts/llm/run_training.sh sft
nvidia-smi  # 确认两卡均有训练显存占用
```
