# 在 32GB 车规级 GPU 上部署 Qwen3.8-27B-AWQ-MTP：硬件、配置与全量实测

> 单张 32GB SM80 显卡，跑 27B 混合注意力模型，256K 上下文 + MTP 投机解码 + 工具调用全开。本文记录完整的部署参数、踩坑过程和实测速度表。

---

## 1. 硬件环境

| 部件 | 规格 |
|------|------|
| GPU | **NVIDIA DRIVE-PG199-PROD**，32 GiB 显存，**SM80（Ampere，compute capability 8.0）** |
| CPU | Intel Xeon W-2125 @ 4.00GHz（6 核） |
| 内存 | 40 GB |
| 驱动 / CUDA | NVIDIA 595.84；CUDA 13.3 工具链（随 Python 环境安装） |
| 存储 | 1 TB（模型 + 环境占用约 60GB） |

几个平台特性，后面所有决策都和它们有关：

- **SM80 没有原生 FP8 张量核**（fp8e4nv 是 SM89+/SM90 才有的）——这是 KV cache 压缩方案选择的分水岭；
- **nvidia-smi 不可用**（Drive OS 车规平台），改用 `pynvml`（`nvmlDeviceGetMemoryInfo` / `nvmlDeviceGetTemperature`）监控；
- 显卡为车规功耗设计，长时间满载后温度 44→74°C，SM 时钟稳定 1260MHz，无降频。

---

## 2. 模型：Qwen3.8-27B-AWQ-MTP

社区量化版 [shawnw3i/Qwen3.8-27B-AWQ-MTP](https://huggingface.co/shawnw3i/Qwen3.8-27B-AWQ-MTP)（基于 Qwen/Qwen3.8-27B）：

- **27.3B dense**，**混合注意力**：64 层 = **48 层 linear_attention（GDN）+ 16 层 full_attention**，每 4 层一次全注意力；
- 多模态（27 层视觉塔），训练上下文 **262,144 tokens**；
- **AWQ W4A16**（group_size 128，Marlin kernel），磁盘 19GB：11.4GB int4 打包权重 + **6GB 未量化的 BF16 部分**（embedding、lm_head、linear_attn 的 in_proj、视觉塔、MTP 模块）——"18GB 模型"里只有 65% 是 Q4；
- **内置 MTP（Multi-Token Prediction）投机解码模块**（1 层 transformer，权重在 `model_extra_tensors.safetensors`），与主模型**共享 embedding/lm_head**，显存只多 ~3GB；
- 下载工具：`ftllm download <repo> --tool aria2c -x 8 -j 4`（多线程秒级拉满带宽）。

---

## 3. 部署：vLLM 0.27.1 + pm2

### 软件栈

- Python 3.13.13 + uv 虚拟环境（`/root/llm/.venv`），vLLM **0.27.1**（PyPI 最新），torch 2.13.0+cu130，flashinfer 0.6.16.post3；
- pm2 托管（`vllm-qwen38-27b-awq`，端口 5000，OpenAI 兼容 API）。

### 最终启动命令（`run-qwen38-27b-awq.sh`）

```bash
export VLLM_USE_FLASHINFER_SAMPLER=0   # 绕开 flashinfer 采样 kernel 的 CUB 编译崩溃（见 §5）

vllm serve /root/llm/models/Qwen3.8-27B-AWQ-MTP \
    --host 0.0.0.0 --port 5000 \
    --attention-backend FLASHINFER \
    --kv-cache-dtype fp8 \
    --max-model-len 262144 \
    --reasoning-parser qwen3 \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder \
    --speculative-config '{"method":"mtp","num_speculative_tokens":3}' \
    --max-num-seqs 1 \
    --gpu-memory-utilization 0.98 \
    --served-model-name qwen38-27b-awq-mtp \
    --enable-log-requests \
    --enable-prompt-tokens-details \
    --enable-prefix-caching
```

关键参数为什么这么定（全部经过实测对比）：

| 参数 | 选择 | 理由 |
|------|------|------|
| `--attention-backend FLASHINFER` | FlashInfer | SM80 上唯一能 **fp8 KV** 的后端；长上下文 decode 稳定（见 §4.2） |
| `--kv-cache-dtype fp8` | fp8 | KV 池 285,575 tokens，是 fp16 的 2 倍，撑起 256K 上下文 |
| `--max-model-len 262144` | 训练上限 | KV 池恰好覆盖完整 256K |
| `--speculative-config` MTP × 3 | 3 drafts | 2/3/4 实测，3 是全场景最优点（§4.3） |
| `--max-num-seqs 1` | 单并发 | **MTP 能装进 32GB 的关键**（Mamba cache / graph 池 / 工作区全压到单槽） |
| `--gpu-memory-utilization 0.98` | 压满 | 32GB 卡跑 27B 的余量策略 |
| `--tool-call-parser qwen3_coder` | XML 模板 | 模型 chat template 是 `<tool_call>` XML 格式 |

运行态：显存 30.4/32 GiB，启动 ~90 秒（kernel 编译缓存命中），pm2 save 持久化。

---

## 4. 实测速度

> 测试方法：每个上下文长度用**独立随机文本**（开头互不相同，避免 prefix cache 跨长度污染）；cold=首次请求，warm=同 prompt 二次请求（命中前缀缓存）；生成任务为写 ~1500 字说明文（约 800~1000 tokens，保证 decode tps 准确）；MTP 接受率取自 `/metrics` 计数器差值。

### 4.1 最终配置总览（FLASHINFER + fp8 + 256K + MTP3）

| 上下文 | 模式 | TTFT | prefill t/s | decode t/s | MTP 接受率 | 缓存命中 |
|--------|------|------|-------------|------------|-----------|---------|
| 0 | cold | 0.15s | 231* | 68.8 | 46.7% | 0 |
| 1K | cold | 0.34s | 2,391 | 69.6 | 46.4% | 0 |
| 32K | cold | 10.36s | 2,372 | 70.1 | 47.4% | 0 |
| 32K | **warm** | **1.17s** | **21,034** | 70.1 | 47.4% | 22,400 |
| 128K | cold | 60.38s | 1,625 | 69.8 | 49.1% | 0 |
| 128K | **warm** | **2.05s** | **48,086** | 69.8 | 49.1% | 96,000 |
| 240K | cold | 155.35s | 1,184 | 62.0 | 48.5% | 0 |
| 240K | **warm** | **4.33s** | **42,522** | 62.0 | 48.5% | 180,800 |

\* 0 上下文仅 33 tokens，prefill 被固定开销摊薄，无参考意义。

**要点**：
- **decode 跨上下文几乎恒定（62~70 t/s）**——这是 FlashInfer split-KV 内核的功劳（对比见下）；
- **prefix cache 命中时 240K 的 TTFT 只要 4.3 秒**（冷启动 155s），固定前缀场景体验接近短上下文；
- 长上下文 cold prefill 变慢是物理规律（attention 计算量随长度增长）。

### 4.2 注意力后端 × KV 精度对比（同一写作任务、MTP3）

| 上下文 | FLASH_ATTN + fp16 | FLASHINFER + fp16 | **FLASHINFER + fp8** | TURBOQUANT 4-bit |
|--------|-------------------|-------------------|----------------------|------------------|
| 0 | 89.1 t/s | 69.0 | 68.8 | 143.4* |
| 1K | 91.3 | 71.2 | 69.6 | 141.1* |
| 32K | 54.2 | 70.8 | 70.1 | 63.0* |
| 128K | **22.1** | 61.1 | **69.8** | 86.7* |
| 240K | — | — | 62.0 | — |
| 最大上下文 | 128K | 128K | **256K** | 256K（但输出崩坏） |

\* TurboQuant 数字**不可信**：无校准 4-bit KV 导致长生成退化为重复循环（`** ** **` / `!!!!`），速度测的是"循环复读机"。

**结论**：FA2 的 decode 成本随上下文线性增长（128K 掉到 22 t/s）；FlashInfer 靠 split-KV（KV 切块多 SM 并行 + 在线 softmax 合并）保持恒定；fp8 KV 因为 KV 读取带宽减半，在 128K 还额外 +14%。

### 4.3 MTP draft 数量调优（2 / 3 / 4）

| 上下文 | spec=2 decode | spec=3 decode | spec=4 decode | 接受率 2/3/4 |
|--------|---------------|---------------|---------------|--------------|
| 0 | 65.4 | **68.8** | 68.7 | 59% / 47% / 39% |
| 1K | 62.9 | **69.6** | 69.1 | 55% / 46% / 40% |
| 32K | 66.7 | **70.1** | 66.3 | 63% / 47% / 38% |
| 128K | 66.0 | **69.8** | 65.7 | 63% / 49% / 40% |

接受率随 draft 数单调下降（猜得越多越难命中），每步产出在 3 时最高（~1.9 token/步）。

### 4.4 任务类型对 decode 的影响（MTP 是内容相关的）

| 任务 | 单步 | decode | 说明 |
|------|------|--------|------|
| 摘要 / 短问答（greedy） | 12.3ms | ~240 t/s | 输出模板化，接受率高 |
| 复述上文（100% 接受率） | 27.8ms | 115~144 t/s | 接受率拉满但 MTP 前向开销大 |
| 写 1500 字说明文 | ~28ms | 62~75 t/s | 自由长文，接受率 ~47% |
| 代码 / 工具调用 | — | 正常 | `qwen3_coder` parser 全通（含并行工具调用闭环） |

> ⚠️ 教训：**"MTP 接受率高 = 快"不成立**。复述任务接受率 100%，但每步要跑 3 次 MTP 前向（~28ms/步），比接受率 67% 的摘要（12.3ms/步）还慢。速度 = 接受率 × 单步吞吐，两者都要看。

### 4.5 与 llama.cpp 的对比（同一模型家族、同卡）

| 指标 | llama.cpp（Q4_K_M GGUF） | vLLM + FLASHINFER + MTP（AWQ） |
|------|-------------------------|-------------------------------|
| decode（写作） | ~38 t/s | **62~70 t/s** |
| prefill | ~950-1,000 t/s | **1,200~2,400 t/s** |
| 最大上下文 | 203K（无 MTP） | **256K（带 MTP）** |
| 长上下文 decode 稳定性 | 随长度下降 | **恒定** |

---

## 5. 踩坑记录（每个都花了真金白银的时间）

1. **flashinfer 0.6.16 + CUDA 13.3 的 CUB 头文件不兼容**：JIT 编译 `sampling.cuh` 报 `BlockAdjacentDifference has no member FlagHeads` → EngineCore 崩溃循环。解法：`VLLM_USE_FLASHINFER_SAMPLER=0`（采样走 vLLM 原生路径；attention kernel 用另一套头文件，不受影响）。
2. **SM80 开不了 fp8 KV？** 默认后端确实不行：FLASH_ATTN 报"requires FA3 on SM90 or FA4 on SM100"，TRITON 报"fp8e4nv requires SM89+"。**但 FLASHINFER 后端支持 SM80 + fp8 KV**（存储压缩 + 软件反量化），代码层面 `supports_compute_capability: 8.0~12.1` 明确放行。别被第一个报错劝退。
3. **FA2 长上下文 decode 崩塌**：batch=1 时 FA2 无并行度可挖，128K 上下文单步 90ms；FlashInfer 的 split-KV 把长 KV 切块并行，单步恒定 ~28ms。长上下文服务请直接选 FlashInfer。
4. **MTP 装不进 32GB**：主模型 18.6GB + MTP 模块 ~3GB + KV + graph 池超出预算，报错还误导（"MTP 需要再加载一份权重"是错的——日志确认 `Sharing target model embedding weights`，drafter 只多 ~1GB）。真正的解法是 `--max-num-seqs 1`（Mamba cache / graph 池 / 工作区全部单槽化）。
5. **TurboQuant 无校准 4-bit KV 输出崩坏**：短问答正常，长生成退化为 `** ** **` / `!!!!` 循环。这类激进量化必须配 TurboQuant 校准过的 checkpoint，无校准变体（`_nc`）别碰。
6. **"隐形思考 token"**：vLLM 0.27 的 qwen3 reasoning parser 对 Qwen3.8 的思考 token 处理不完整——模型思考时 token 不计入 content 也不计入 reasoning_content（但计入 usage），表现为"生成 1600 tokens 正文 0 字"的假数据。测速时用 `chat_template_kwargs: {"enable_thinking": false}` 规避；真实服务里则要留意长思考请求的"空回答"（社区建议服务端固定 `reasoning_effort: medium`）。
7. **FlashInfer + 投机解码的 cudagraph 降级**（[vLLM #49547](https://github.com/vllm-project/vllm/issues/49547)）：自动降级 PIECEWISE，约 -16%，SM80 无 workaround（trtllm-gen 路径要 SM100+），等上游 RFC #49488。已知税，接受。
8. **prefix caching 别关**：实测关闭后摘要 decode 从 240 掉到 127 t/s（Mamba cache 模式切换的副作用），还损失 warm 秒级 TTFT。

---

## 6. 结论

在 32GB SM80 平台上，Qwen3.8-27B-AWQ-MTP 的最优部署形态是：

> **FlashInfer 后端 + fp8 KV cache + 256K 上下文 + MTP×3 + 单并发 + prefix caching**

- 写作类任务稳定 **62~70 t/s**（跨 0~240K 上下文持平），摘要/问答类 ~240 t/s；
- 256K 全量上下文 + 工具调用 + 推理模式全开；
- 相比 llama.cpp：decode 快 1.6~1.8 倍，prefill 快 2 倍以上，上下文从 203K 提到 256K；
- 硬件的物理边界（SM80 无 FP8 核、车规功耗）决定了 fp8 只能是"存储压缩"，但这已经够用。

*测试脚本：`benchmarks/bench_ctx_cold_warm.py`（冷/热 + MTP 接受率）、`bench_ctx_cold_warm_128k.py`（128K 变体）、`bench_ctx_prefill_decode.py`（prefill 专项）。*
