Qwen3.8-27B双卡A800部署指南
Qwen3.8-27B双卡A800部署指南
一份部署 + 调优 + 基准 + Agent 接入的技术文档。
- 模型: Qwen3.8-27B (BF16 原生) · 硬件: 2× A800 80GB · 引擎: vLLM v0.26.0,端口 8003。
- 原生 262,144 上下文, fp8 KV Cache, 工具调用, 推理(思考)模式,reasoning_effort 全档位(标准枚举)按请求透传, MTP 投机解码。
文章目录
1. 背景与目标
A800 (Ampere sm80) 没有 FP8 张量核, 每次矩阵乘需把 FP8反量化回 BF16 → 在 A800 上 FP8 反而更慢。这已在 05_Qwen36双卡部署文档.md 中验证过同一个根因。
本次目标: 在空闲的 GPU 0,1 上双卡 (TP=2) 部署 Qwen3.8-27B, 满足:
| 需求 | 说明 |
|---|---|
| 262K 上下文 | KV 尽量驻留 GPU, 可用 fp8 KV 省显存 |
| 工具调用 | 各类 Agent (claude code / OpenAI agent) 接入 |
| MTP 投机解码 | 官方推荐低延迟配置, 本机实测启用 |
| 思考 / 推理等级 | reasoning_effort 原生 xhigh / medium / low, 经模板补丁别名归一支持标准全档位, 按请求透传 |
| 速度 / 显存 / 并发平衡 | 在 A800 上选最优精度 |
2. 软件与硬件环境
2.1 硬件
| 项 | 值 |
|---|---|
| GPU | 2× NVIDIA A800 80GB PCIe (Ampere, sm80) |
| Driver | 580.159.03 |
| CUDA (宿主) | 13.0 |
| 互联 | PCIe (A800 PCIe 版), NCCL 2.28.9 |
⚠️ A800 = Ampere sm80: 无 FP8/FP4 张量核, 与 H100(Hopper sm90)、B200(Blackwell)不同。
2.2 GPU 分布
| GPU | 用途 | 端口 |
|---|---|---|
| 0-1 | Qwen3.8-27B (本次, 双卡 TP2) | 8003 |
2.3 Docker 镜像版本
docker pull vllm/vllm-openai:v0.26.0
docker images | grep vllm
# vllm/vllm-openai:v0.26.0
- 本次使用
vllm/vllm-openai:v0.26.0。模型卡要求 vLLM 0.17.0+ 支持 Qwen3_5 架构, v0.26.0 满足且其为Qwen3_5MTP架构解析,CUDA graph 编译与编译优化 (norm_quant/act_quant fusion) 均正常。
2.4 模型文件
- 通过modelscope下载模型权重和配置文件。
pip install modelscope
modelscope download --model Qwen/Qwen3.8-27B --local_dir ./Qwen3.8-27B
| 模型 | 路径 | 体积 | 权重/卡 (TP2) |
|---|---|---|---|
| Qwen3.8-27B (BF16) | /shared/models/Qwen3.8-27B |
52 GB (18 shards) | 26.08 GiB |
generation_config.json: temperature=1.0, top_p=0.95, top_k=20。
3. 模型选型: BF16 vs FP8
3.1 核心事实: A800 没有 FP8 张量核
| GPU 代际 | 计算能力 (Compute Capability) | FP8 张量核 |
|---|---|---|
| A800 | sm80 (Ampere) |
❌ 无 |
| H100 | sm90 (Hopper) |
✅ 有 |
| B200 | sm100 (Blackwell) |
✅ 有 |
在 A800 上, vLLM 无法用 FP8 MMA 直接算, 必须先把 FP8 权重反量化到 BF16 再乘。
因此:
- BF16 原生: 直接跑 BF16 矩阵乘, 无额外开销, 最快。
- FP8: 权重体积减半, 但每次矩阵乘多一次反量化 → 预填充反而更慢。解码受 HBM 带宽限制, 权重变小省带宽、反量化+计算吃掉这些收益, 净结果持平。
3.2 实测对比 (§8 详表)
| 指标 | BF16 | FP8 | 结论 |
|---|---|---|---|
| 32K 预填充 | 5,832 tok/s | 4,655 tok/s | BF16 快 25% |
| 100K 预填充 | 3,718 tok/s | 3,248 tok/s | BF16 快 14% |
| 单流解码 | 59 tok/s | 59 tok/s | 持平 (带宽受限) |
| 并发 8 聚合 | 297 tok/s | 249 tok/s | BF16 略快 |
| KV 池 | 259.5 万 token | 321.7 万 token | FP8 多 24% |
| 权重/卡 | 26.08 GiB | 14.96 GiB | FP8 省 11 GiB |
3.3 结论: 选 BF16
- 速度: BF16 预填充全面领先, 解码持平 → 无理由换 FP8。
- 显存: FP8 省 11 GiB/卡 → KV 池多 24%。但对 Agent 场景, BF16 的 259 万 token KV 池已远超需求 (见 §9), 显存根本不构成瓶颈。
- 并发: BF16 并发 8 聚合 297 tok/s > FP8 249 tok/s。
- 关键洞察: 本模型是混合注意力, 只 16/64 层全注意力, 48 层是线性注意力 (常驻重复状态, 不存 KV)。所以 KV 缓存天生极小, “用 FP8 省显存放更多 KV” 的红利被混合注意力直接抹平 —— 这是本结论与普通 dense 模型不同的地方。
推荐:
Qwen3.8-27B(BF16), 配--kv-cache-dtype fp8(KV 本身用 fp8, 不损权重速度)。
FP8 权重版仅当需要把模型塞进更小显存、且接受更慢预填充时才有意义。
4. 模型架构解析
- Qwen3.8-27B 是 27B 稠密 (dense) 模型, 但用了与 Qwen3.5-2.4T 同源的混合注意力骨干:
- 64 层, 其中 16 层全注意力(每 4 层 1 层,
full_attention_interval: 4), 48 层线性注意力。 - 线性注意力用常驻重复状态(RNN 式), 无需 KV cache → 长上下文 KV 占用极低。
- 多模态: 含视觉塔 (
vision_config, 深度 27 的 encoder)。 - 内置 MTP 草稿头(
mtp_num_hidden_layers: 1), 支持投机解码。 - 原生上下文 262,144 token (RoPE
rope_theta=10000000, 无 rope_scaling, 不超 256K)。
⚠️ 262K 是硬上限: config.json 无 rope_scaling, 强上 512K 会 RoPE 越界出 NaN(同 Qwen3.6 结论)。要更长上下文需 YaRN 覆写, 本文按 256K 原生部署。
5. 部署步骤
5.1 一键脚本
mkdir scripts
vim scripts/deploy_qwen38.sh
bash scripts/deploy_qwen38.sh
scripts/deploy_qwen38.sh完整脚本内容:
#!/usr/bin/env bash
# =============================================================================
# Qwen3.8-27B 双卡部署脚本 (标准 vLLM v0.26.0, GPU 0-1, 端口 8003)
# -----------------------------------------------------------------------------
# 用途: A800 上 BF16 原生全速运行 (A800=Ampere sm80, 无 FP8 张量核, FP8 需反量化
# 到 BF16, 反而更慢)。混合注意力 (16/64 层全注意力, 48 层线性注意力) 使其
# KV 缓存极小, 双卡 80GB 显存轻松容纳 262K 上下文 + fp8 KVCache。
#
# 关键特性 (与官方 recipe 对齐):
# - 262144 原生上下文, KV 尽量驻留 GPU
# - fp8 KVCache (混合注意力 KV 小, fp8 再省一半)
# - 工具调用: --enable-auto-tool-choice + qwen3_coder parser
# - 推理: --reasoning-parser qwen3, 默认思考, reasoning_effort 透传
# - MTP 投机解码: --speculative-config '{"method":"mtp","num_speculative_tokens":3}'
# - 前缀缓存 + chunked prefill (agent 场景)
#
# 可覆盖环境变量:
# GPUS 设备, 默认 "device=0,1"
# TP_SIZE 张量并行, 默认 2
# PORT 端口, 默认 8003
# MAX_MODEL_LEN 上下文上限, 默认 262144 (256K 原生)
# MAX_BATCHED_TOKENS 预填充 chunk, 默认 8192 (官方案例甜点)
# GPU_MEM_UTIL 显存利用率, 默认 0.95 (若 OOM 降 0.93)
# WATERMARK 保留空闲 KV 比例, 默认 0.1
# MAX_NUM_SEQS 并发序列上限, 默认 256
# ENABLE_MTP 是否启用 MTP 投机解码, 默认 1
# KV_CACHE_DTYPE KV 缓存精度, 默认 fp8
# CHAT_TEMPLATE_PATCH 是否挂载补丁模板 (high/ultra/max -> xhigh), 默认 0
# =============================================================================
set -e
MODEL_HOST_PATH="${MODEL_HOST_PATH:-/shared/models/Qwen3.8-27B}"
MODEL_CTR_PATH="/model-qwen38"
IMAGE="${IMAGE:-vllm/vllm-openai:v0.26.0}"
CONTAINER="${CONTAINER:-vllm-qwen38}"
PORT="${PORT:-8003}"
SERVED_MODEL="${SERVED_MODEL:-Qwen3.8-27B}"
GPUS="${GPUS:-device=0,1}"
TP_SIZE="${TP_SIZE:-2}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-262144}"
MAX_BATCHED_TOKENS="${MAX_BATCHED_TOKENS:-8192}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.95}"
WATERMARK="${WATERMARK:-0.1}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-256}"
ENABLE_MTP="${ENABLE_MTP:-1}"
KV_CACHE_DTYPE="${KV_CACHE_DTYPE:-fp8}"
CHAT_TEMPLATE_PATCH="${CHAT_TEMPLATE_PATCH:-0}"
docker rm -f "${CONTAINER}" 2>/dev/null || true
echo "[deploy] ${CONTAINER} | GPUS=${GPUS} TP=${TP_SIZE} max-model-len=${MAX_MODEL_LEN} batch=${MAX_BATCHED_TOKENS} util=${GPU_MEM_UTIL} kv=${KV_CACHE_DTYPE} mtp=${ENABLE_MTP}"
ARGS=( "${MODEL_CTR_PATH}"
--host 0.0.0.0
--port "${PORT}"
--served-model-name "${SERVED_MODEL}"
--tensor-parallel-size "${TP_SIZE}"
--max-model-len "${MAX_MODEL_LEN}"
--gpu-memory-utilization "${GPU_MEM_UTIL}"
--trust-remote-code
--enable-prefix-caching
--enable-chunked-prefill
--watermark "${WATERMARK}"
--max-num-batched-tokens "${MAX_BATCHED_TOKENS}"
--max-num-seqs "${MAX_NUM_SEQS}"
--enable-auto-tool-choice
--tool-call-parser qwen3_coder
--reasoning-parser qwen3
--kv-cache-dtype "${KV_CACHE_DTYPE}"
)
if [ "${ENABLE_MTP}" = "1" ]; then
ARGS+=( --speculative-config '{"method":"mtp","num_speculative_tokens":3}' )
fi
MOUNTS=( -v "${MODEL_HOST_PATH}:${MODEL_CTR_PATH}" )
if [ "${CHAT_TEMPLATE_PATCH}" = "1" ]; then
PATCHED_TEMPLATE_HOST="$(cd "$(dirname "$0")/.." && pwd)/patches/qwen38_chat_template_high_alias.jinja"
MOUNTS+=( -v "${PATCHED_TEMPLATE_HOST}:/template-patched.jinja" )
ARGS+=( --chat-template /template-patched.jinja )
echo "[deploy] chat-template patch ON (high/ultra/max -> xhigh)"
fi
docker run -d --name "${CONTAINER}" \
--gpus "\"${GPUS}\"" \
--ipc=host --shm-size=16g \
-p ${PORT}:${PORT} \
"${MOUNTS[@]}" \
-e CUDA_DEVICE_ORDER=PCI_BUS_ID \
"${IMAGE}" \
"${ARGS[@]}"
echo "[deploy] started. logs: docker logs -f ${CONTAINER} | ready: curl localhost:${PORT}/v1/models"
- 环境变量可覆盖 (含 FP8 切换):
# 默认 BF16 部署
GPUS=device=0,1 TP_SIZE=2 PORT=8003 MAX_MODEL_LEN=262144 bash scripts/deploy_qwen38.sh
# 想切 FP8 权重 (GPU 6-7, 端口 8004):
MODEL_HOST_PATH=shared/models/Qwen3.8-27B-FP8 \
CONTAINER=vllm-qwen38fp8 PORT=8004 SERVED_MODEL=Qwen3.8-27B-FP8 GPUS=device=0,1 \
bash scripts/deploy_qwen38.sh
# 关 MTP 投机解码:
ENABLE_MTP=0 bash scripts/deploy_qwen38.sh
5.2 完整 docker run (等价展开)
docker rm -f vllm-qwen38 2>/dev/null || true
docker run -d --name vllm-qwen38 \
--gpus '"device=0,1"' \
--ipc=host --shm-size=16g \
-p 8003:8003 \
-v /shared/models/Qwen3.8-27B:/model-qwen38 \
-e CUDA_DEVICE_ORDER=PCI_BUS_ID \
vllm/vllm-openai:v0.26.0 \
/model-qwen38 \
--host 0.0.0.0 --port 8003 \
--served-model-name Qwen3.8-27B \
--tensor-parallel-size 2 \
--max-model-len 262144 \
--gpu-memory-utilization 0.95 \
--trust-remote-code \
--enable-prefix-caching \
--enable-chunked-prefill \
--watermark 0.1 \
--max-num-batched-tokens 8192 \
--max-num-seqs 256 \
--enable-auto-tool-choice \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3 \
--kv-cache-dtype fp8 \
--speculative-config '{"method":"mtp","num_speculative_tokens":3}'
- 进阶 (可选, 非默认):更长上下文 (>262K): 需 YaRN 覆写
rope_parameters(factor 按典型长度调, 如 524K 用 2.0):
vllm serve ... \
--max-model-len 1000000 \
--hf-overrides '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11,11,10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}'
⚠️ 静态 YaRN 会降低短文本质量, 仅在真需要长上下文时启用。
5.3 就绪检查
curl -s http://127.0.0.1:8003/v1/models # → id=Qwen3.8-27B, max_model_len=262144
启动耗时 (实测): 约 5.5 分钟 (权重加载 51.75 GiB + torch.compile 70s + CUDA graph 预热)。vllm-qwen38 | init engine took 194.43 s (compilation: 70.25 s)。
- 显存分配 (BF16)情况
weight 26.08 GiB | peak activation 2.31 GiB | CUDAGraph 0.75 GiB |
KV cache 45.99 GiB |GPU KV cache size: 2,595,079 tokens | Maximum concurrency @262K: 9.90x
6. 启动参数详解
| 参数 | 值 | 理由 |
|---|---|---|
--tensor-parallel-size 2 |
2 | 双卡 TP; 26 GB/卡权重 + 46 GB KV, 余量充足 |
--max-model-len 262144 |
256K | 模型原生上限; 不可超 (无 rope_scaling, §4) |
--gpu-memory-utilization 0.95 |
0.95 | 尽量用满显存 → KV 池 259 万 token; OOM 则降 0.93 |
--kv-cache-dtype fp8 |
fp8 | KV 缓存用 fp8_e4m3, 省一半, 提升 KV 容量 (不影响权重速度) |
--max-num-batched-tokens 8192 |
8192 | 预填充 chunk, 官方 recipe 甜点; 平衡吞吐与尾延迟 |
--max-num-seqs 256 |
256 | 并发序列上限 (Agent/批量) |
--watermark 0.1 |
0.1 | 永久保留 10% KV 空闲, 防前缀缓存占满抖动 |
--enable-prefix-caching |
开 | Agent 共享系统提示词命中前缀, 免重复预填充 |
--enable-chunked-prefill |
开 | 长预填充与解码交错, 保并发 |
--enable-auto-tool-choice |
开 | 工具调用 (Agent 必需) |
--tool-call-parser qwen3_coder |
- | Qwen 工具/函数解析 |
--reasoning-parser qwen3 |
- | 推理字段解析 (思考模式) |
--speculative-config '{"method":"mtp","num_speculative_tokens":3}' |
MTP | 投机解码, 内置 MTP 草稿头, ~1.4× 加速 |
--trust-remote-code |
开 | 模型含自定义代码 |
说明: 引擎自动给 KV 加 3 层 padding (适配线性注意力 non-uniform shape), 最多浪费 6.25% KV, 属正常。
7. 功能验证
7.1 思考 / 推理等级 (reasoning_effort)
模型原生只认
xhigh/medium/low, 与 vLLM 的标准枚举有落差; 用一行模板补丁做别名归一,把 vLLM 侧全部档位打通, 见 7.1.3 的代码改动。
7.1.1 模型原生支持的推理等级 (原版)
- Qwen3.8原始
chat_template.jinja只承认 三档:xhigh(默认) /medium/low,校验写死在模板里, 其余取值一律raise_exception:
{%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %}
{%- if resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
{{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~
'. Supported types are xhigh (default), medium, and low.') }}
{%- endif %}
- 模型思考开/关:
chat_template_kwargs: {"enable_thinking": false}直接给答案。 reasoning_effort既支持顶层字段(OpenAI 标准), 也支持放在chat_template_kwargs里,按请求独立设置, 互不影响。- 保留思考:
preserve_thinking默认 true, 保持多轮推理轨迹连续性 (Agent 推荐)。
| 方式 | 请求示例 | 实测 |
|---|---|---|
顶层 reasoning_effort |
"reasoning_effort":"xhigh" |
✅ 产生思考 (message.reasoning) |
顶层 reasoning_effort |
"reasoning_effort":"low" |
✅ 快速直答 |
chat_template_kwargs |
{"reasoning_effort":"medium"} |
✅ 生效 |
- 字段名注意: vLLM v0.26 返回推理内容在
message.reasoning(OpenAI chat) /content[type=thinking].thinking(Anthropic messages), 不是旧版reasoning_content。
- vLLM v0.26 的 OpenAI 层 (
protocol.py) 却按标准枚举接受none / minimal / low / medium / high / xhigh / max。二者落差导致的直接后果:Agent 发reasoning_effort: "high"时, vLLM 层校验通过、原版模板却因high不在三档内而raise_exception→ vLLM 400 拒请求, 推理无法激活。
因此原生支持范围是:
| 原生 effort | vLLM 侧校验 | 模板层 | 结果 |
|---|---|---|---|
xhigh (默认) |
✅ | ✅ | 长思考 |
medium |
✅ | ✅ | 中等思考 |
low |
✅ | ✅ | 短思考 |
none |
✅ | ❌ raise | 400 |
minimal |
✅ | ❌ raise | 400 |
high |
✅ | ❌ raise | 400 |
max |
✅ | ❌ raise | 400 |
7.1.2 完善后的推理等级
- 提供补丁模板
patches/qwen38_chat_template_high_alias.jinja, 只做别名归一, 把vLLM 标准枚举全部映射进模型原生三档:high/ultra/max → xhigh,minimal → low,none仍由enable_thinking独立关闭。启用方式:
CHAT_TEMPLATE_PATCH=1 bash scripts/deploy_qwen38.sh
补丁后全档位实测 (推理长度随 effort 递增, 均正常激活, 无 400):
| 客户端传入 | 归一后 | 行为 |
|---|---|---|
none |
思考关 (enable_thinking=false) |
直接答 |
minimal |
low |
思考, 较短 |
low |
low |
思考, 较短 |
medium |
medium |
思考, 中等 |
high |
xhigh |
思考, 较长 |
ultra |
xhigh |
思考, 较长 |
max |
xhigh |
思考, 较长 |
xhigh |
xhigh |
思考, 最长 |
映射原则: 上线看细档, 下线看粗档 —— 低于
medium的 (low/minimal) 归到low,
高于medium的 (high/ultra/max) 归到xhigh, 语义连续、单调递增。校验异常分支
(Unexpected reasoning effort) 保留兜底, 只是不再对标准枚举触发。
7.1.4 代码改动 (补丁模板 + 部署脚本)
- 补丁只改模板的 effort 枚举归一, 其余与官方
chat_template.jinja逐字一致;通过--chat-template覆盖生效, 不改动共享的模型 checkpoint, 重启即生效。
- 模板核心改动 (
patches/qwen38_chat_template_high_alias.jinja, 相对原版仅改一段):
{# 原版: 白名单三档, 其余 raise #}
{%- if enable_thinking is undefined or enable_thinking is true %}
{%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %}
{%- if resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
{{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~ '. Supported types are xhigh (default), medium, and low.') }}
{%- endif %}
{%- if resolved_reasoning_effort == 'xhigh' %}
{%- set reasoning_instructions = 'Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.' %}
{%- elif resolved_reasoning_effort == 'low' %}
{%- set reasoning_instructions = 'Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.' %}
{%- endif %}
{%- endif %}
{# 补丁: 先别名归一, 再进原三档; 异常分支保留兜底 #}
{%- if enable_thinking is undefined or enable_thinking is true %}
{%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %}
{%- if resolved_reasoning_effort in ('high', 'ultra', 'max') %}
{%- set resolved_reasoning_effort = 'xhigh' %}
{%- elif resolved_reasoning_effort in ('minimal',) %}
{%- set resolved_reasoning_effort = 'low' %}
{%- elif resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
{{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~ '. Supported types are xhigh (default)/high, medium, and low.') }}
{%- endif %}
{%- if resolved_reasoning_effort == 'xhigh' %}
{%- set reasoning_instructions = 'Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.' %}
{%- elif resolved_reasoning_effort == 'low' %}
{%- set reasoning_instructions = 'Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.' %}
{%- endif %}
{%- endif %}
- 全文件仅此 1 处脚本逻辑改动, 其余指令文本逐字一致。
② 部署脚本改动 (scripts/deploy_qwen38.sh):
# 环境变量开关 (默认 0 = 用原版模板)
CHAT_TEMPLATE_PATCH="${CHAT_TEMPLATE_PATCH:-0}"
# 参数数组里追加 --chat-template, 并只读挂载补丁模板
MOUNTS=( -v "${MODEL_HOST_PATH}:${MODEL_CTR_PATH}" )
if [ "${CHAT_TEMPLATE_PATCH}" = "1" ]; then
PATCHED_TEMPLATE_HOST="$(cd "$(dirname "$0")/.." && pwd)/patches/qwen38_chat_template_high_alias.jinja"
MOUNTS+=( -v "${PATCHED_TEMPLATE_HOST}:/template-patched.jinja" )
ARGS+=( --chat-template /template-patched.jinja )
echo "[deploy] chat-template patch ON (high/ultra/max -> xhigh)"
fi
- 要点: 补丁模板以只读挂载进容器 (
/template-patched.jinja), 通过--chat-template指向它覆盖模型的默认模板设置; 不改 checkpoint、不改镜像,CHAT_TEMPLATE_PATCH=0即可一键回退原版。
7.2 工具调用
curl -s -X POST http://127.0.0.1:8003/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model":"Qwen3.8-27B","max_tokens":300,
"tools":[{"type":"function","function":{"name":"get_weather","description":"查询城市天气",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
"messages":[{"role":"user","content":"查北京天气, 用工具"}]}'
# → tool_calls: [{"function":{"name":"get_weather","arguments":"{\"city\": \"北京\"}"}}]
# finish_reason: tool_calls
7.3 三种接入接口全部可用
| 接口 | 基本对话 | 工具调用 | 思考/推理 | 备注 |
|---|---|---|---|---|
/v1/chat/completions (OpenAI) |
✅ | ✅ | ✅ message.reasoning |
通用 |
/v1/messages (Anthropic) |
✅ | ✅ | ✅ content[type=thinking] + adaptive |
claude code 用 |
/v1/responses (OpenAI Responses) |
✅ | ✅ | ✅ | 新 API |
Anthropic 端 real 实测 (thinking:{"type":"adaptive"} 返回 thinking block + text, stop_reason=end_turn)。
8. 性能基准实测
全部实测: 随机文本避免前缀缓存干扰; 解码为 thinking 关 (纯生成速度)。
同硬件双容器轮流测, 避免跨容器争用; 数字摘取自benchmark_data/qwen38_*_result.json。
8.1 预填充 (冷启动, 无缓存)
| 段长 | BF16 | FP8 |
|---|---|---|
| 8K | 5,511 tok/s (2.7s) | 1,733 tok/s (首个请求编译抖动) |
| 32K | 5,832 tok/s (10.3s) | 4,655 tok/s |
| 100K | 3,718 tok/s (51.2s) | 3,248 tok/s |
预填充是稠密 27B 的计算瓶颈(infer_dense), 混合注意力 + chunked prefill 下吞吐随上下文下降较缓 (线性注意力层 O(N) 预填充功劳), 比纯注意力好很多。
150K 未测: 生成随机文本按 ~1.9× 膨胀, 150K 文本超过 262K 上限被 400 拒绝。
8.2 单流解码
| 精度 | 单流 tok/s |
|---|---|
| BF16 | 59 tok/s |
| FP8 | 59 tok/s |
稠密 27B 解码由 HBM 带宽决定 (每 token 要流式读取全部 26 GiB 权重), 接近 A800理论带宽上限 (~77 tok/s)。MTP 已把 59 推高 ~1.4× (见 §8.4); 不开 MTP 约 41 tok/s。
8.3 并发 (128 输出 token/请求, thinking 关)
| 并发 | BF16 聚合 | BF16 P50 | FP8 聚合 |
|---|---|---|---|
| 1 | 54 tok/s | 0.51s | 54 tok/s |
| 2 | 53 tok/s | 0.69s | 55 tok/s |
| 4 | 43 tok/s | 0.80s | 45 tok/s |
| 8 | 297 tok/s | 0.73s | 249 tok/s |
并发为何能放大吞吐? 稠密模型权重固定, 批量 N 个序列时每层权重只读一次、算 N 次,于是从"带宽受限"切换到"算力受限"。并发 8 时聚合 297 tok/s 远超单流 59, 正是批量化摊薄权重读取的结果。并发越高、批越大, 聚合吞吐越高 (可冲到 ~500+)。
8.4 MTP 投机解码实测
drafts_total = 1097 | draft_tokens = 3291 (每 draft 3 token)
accepted_tokens_total = 1564 → 接受率 47.5% → 每次模型前向产出 ~1.43 token
- MTP 把单流解码从 ~41 推到 59 tok/s(约 1.4×)。低并发时吞吐受 MTP 接受率抖动影响(故 c1-c4 聚合波动大); 高并发包提升吞吐。
9. 并发与容量估算
9.1 KV 池 (BF16, --kv-cache-dtype fp8)
GPU KV cache size = 2,595,079 token, 最大并发 (满 262K/请求) ≈ 9.9 个。
| Agent 场景 | 估算 |
|---|---|
| 平均上下文 50K | 259 万 / 50K ≈ 51 个 |
| 平均上下文 100K | 259 万 / 100K ≈ 25 个 |
| 满载 256K | 259 万 / 256K ≈ 9.9 个 |
fp8 KV 把池子从 ~130 万推到 259 万 (省一半)。对典型 Agent 负载(万级 ~ 10万级上下文)完全够, 几十个并发 Agent 不成问题。
9.2 与其它模型容量对比 (同双卡 80GB)
| 模型 | 类型 | KV 池 | 单流解码 | 预填充 32K |
|---|---|---|---|---|
| Qwen3.8-27B (本篇) | dense 27B | 259 万 | 59 | 5,832 |
| Qwen3.6-35B-A3B | 3B 激活 MoE | 355 万 | 165 | 25K+ |
Qwen3.6-35B-A3B 因激活小, 解码/预填充更快、池更大; Qwen3.8-27B 是更强/更新的稠密模型 (质量上限更高), 但吞吐更低。两者用途不同:追求极致吞吐用 A3B, 追求更高模型质量用 27B dense。
10. 运维/监控/排错
# 就绪
curl -s http://127.0.0.1:8003/v1/models
# KV 占用 / 排队
curl -s http://127.0.0.1:8003/metrics | grep -E 'kv_cache_usage_perc|num_requests_waiting'
# MTP 接受率
curl -s http://127.0.0.1:8003/metrics | grep -E 'spec_decode_num_(accepted|draft)_tokens_total'
# 重启 (注: 权重加载 + 编译约 5.5 分钟)
bash scripts/deploy_qwen38.sh
# 日志
docker logs -f vllm-qwen38
# 回退到 DSv4
export ANTHROPIC_BASE_URL="http://<宿主机IP>:8000"
- OOM 处理:
GPU_MEM_UTIL=0.95启动 OOM (显存竞争/图捕获) 时降 0.93:
GPU_MEM_UTIL=0.93 bash scripts/deploy_qwen38.sh
- KV 精度注意: 权重 FP8 时
kv_cache.py提示scaling factor 1.0 for fp8_e4m3——若精度敏感需确认 checkpoint 自带 k/v_scale; BF16 权重无此问题。
12. 结论
- A800 双卡 → 选 BF16, 不用 FP8 权重。根因: Ampere 无 FP8 张量核, FP8 预填充反而更慢,解码持平; 而混合注意力让 KV 极小, FP8 的省显存红利被抹平。
- 配置基线: TP=2, 262K 上下文,
--kv-cache-dtype fp8(KV 省一半), 0.95 显存,prefix caching + chunked prefill, 工具调用qwen3_coder/reasoning-parser,MTP 投机解码 (3 token), max-num-batched-tokens 8192。 - 能力全部就绪: 工具调用、思考模式、
reasoning_effortxhigh/medium/low 按请求透传、OpenAI / Anthropic / Responses 三接口。 - 实测成绩 (BF16, TP2): 32K 预填充 5,832 tok/s; 100K 3,718 tok/s; 单流解码 59 tok/s(MTP ~1.4×); 并发 8 聚合 297 tok/s; KV 池 259 晚token (fp8), 可支撑几十个 Agent。
更多推荐

所有评论(0)