硬件平台
- GPU: 2× Tesla V100-SXM2-16GB (通过双卡显卡坞连接,NVLink 互联)
- CPU: Intel Xeon E5-2680 v4 @ 2.40GHz (14核/28线程)
- 内存: 39GB DDR4
- llama.cpp: v9263 (6a257d446), GCC 13.3.0, CUDA 13.0, Driver 580.126.20
模型配置概要
| 项目 | 主模型 (Qwen3.5-27B) | Embedding 模型 |
|---|---|---|
| 模型文件 | Huihui-Qwen3.6-35B-A3B-Claude-4.7-Opus-abliterated.i1-Q4_K_M.gguf | Qwen3-Embedding-0.6B.Q4_K_M.gguf |
| 参数量 | 34.66B | 595M |
| 文件大小 | 21.2 GiB | 390 MiB |
| 上下文 | 225,280 tokens (训练上限 262,144) | 8,192 tokens (per slot 2,816) |
| GPU 分配 | 全部层 (tensor-split 1,1) | ❌ 彻底禁用 (device = none) |
| KV Cache | q8_0, flash-attn, kv-offload, kv-unified | N/A |
| 量化精度 | Q4_K_M | Q4_K_M |
| 推理模式 | Reasoning on, budget=1024 | Pooling=cls |
GPU 显存分配
Device=none 前(旧配置)
| GPU | 总量 | 已用 | 占用率 | 说明 |
|---|---|---|---|---|
| GPU 0 | 16,384 MiB | 15,045 MiB | 91.8% | 主模型 ~13,980MiB + Embed 734MiB + Xorg |
| GPU 1 | 16,384 MiB | 13,101 MiB | 80.0% | 主模型 ~12,776MiB + Embed 308MiB + Xorg |
Device=none 后(预期)
| GPU | 说明 |
|---|---|
| GPU 0 | 预计 ~14,311 MiB(释放 734MiB),空余 ~2 GiB |
| GPU 1 | 预计 ~12,776 MiB(释放 308MiB),空余 ~3.6 GiB |
两卡合计释放约 1 GiB 显存。
推理性能实测
主模型 (Qwen3.5-27B alias) 性能数据
| 指标 | 请求1(冷启动) | 请求2(已预热) | 分析 |
|---|---|---|---|
| Prompt Eval | 13 tokens / 143.59ms | 264 tokens / 604.91ms | — |
| PP 速度 | 90.54 t/s (11.05 ms/tok) | 436.43 t/s (2.29 ms/tok) | ⚠ 请求1 被初始推理拉慢 |
| TG 速度 | 98.62 t/s (10.14 ms/tok) | 102.94 t/s (9.71 ms/tok) | 稳定 ~100 t/s |
| Reasoning Budget | 1024 → 自然结束 (~100 tok) | 1024 → 自然结束 (~100 tok) | 推理预算充足 |
| 总耗时 | 1,745.67ms (171 tok) | 2,402.14ms (449 tok) | 含推理 token |
| Graphs Reused | 156 次复用 | 339 次复用 | 计算图缓存生效 |
关键性能结论
- Text Generation 稳定在 ~100 t/s:35B 模型在双 V100 上达到 100 t/s 是合理的,NVLink 互联使跨卡通信延迟更低,tensor-split 效率更高。
- Prompt Processing 预热后达 436 t/s:264 tokens 仅 605ms,说明 flash-attn + q8_0 KV cache 对长上下文 prompt 处理有效。
- 冷启动性能差:首次请求 PP 仅 90 t/s,因为触发模型加载(从磁盘加载 21GB GGUF 到显存)和推理预算初始化。
--no-warmup跳过了预热步。 - Reasoning Budget 自然结束:2 次请求推理预算均在 ~100 decoded tokens 时自然结束,未用满 1024 预算,说明模型有能力自行判断推理深度。
KV Cache 量化精度分析:q4 vs q8
KV Cache 显存构成
在当前 225K 上下文的配置下,KV cache 是显存占用的第二大来源(仅次于模型权重 26.8 GiB)。其大小取决于:
KV cache per token = 2 (K+V) × n_layers × d_head × n_kv_heads × dtype_bytes
Qwen3.6-35B 为 GQA 架构,n_kv_heads 远小于 n_heads,KV cache 相对较小。实测 slot checkpoint 中 260 tokens 占 62.813 MiB,其中包含 KV cache 及 slot 管理元数据。
支持的数据类型
| 类型 | 每元素大小 | 性能表现 | 质量表现 | 适用场景 |
|---|---|---|---|---|
| f16 | 2 bytes | 基线 | 无损 | 调试/质量验证用 |
| q8_0 | 1 byte | ⬇ 50% 显存,速度≈f16 | 极轻微损失(PPL +0.01~0.05) | 当前配置,推荐作为平衡点 |
| q4_0 | 0.5 byte | ⬇ 75% 显存,速度≈q8 | 轻度损失(PPL +0.1~0.3) | 显存紧张时的过渡方案 |
| q4_1 | 0.5 byte | 同 q4_0 | 略好于 q4_0(启用 min) | 相比 q4_0 提升不大 |
| iq4_nl | 0.5 byte | ⬇ 75% 显存,稍慢于 q4_0 | 接近 q8_0(PPL +0.05~0.1) | q4 中最推荐,非线性量化保留 outlier |
q4 vs q8 关键差异分析
- 显存节省:q4(含 iq4_nl)比 q8 节约 50% 的 KV cache 显存。q8 比 f16 节约 50%。从 f16→q8→q4 是逐级半减的关系。
- 质量影响:
- q8_0:多数评测中与 f16 无显著差异,对长上下文推理质量几乎无影响
- q4_0:在短上下文(<8K)下损失可忽略,长上下文(>32K)下累积误差可能影响检索准确率
- iq4_nl:通过非线性量化保留 KV cache 中的异常值,长上下文下质量显著优于 q4_0,接近 q8_0
- 速度差异:
- PP(Prompt Processing):q4 因数据量更小,内存带宽压力降低,可能比 q8 略快
- TG(Text Generation):V100 没有原生 int4 tensor core,q4 反量化开销稍高。实测差距通常在 3-5% 以内
- flash-attn 对量化精度的敏感性:flash-attn 算法在 q8 和 q4 上的加速比几乎一致
- 与 flash-attn 的交互:flash-attn 处理的是注意力计算的内存访问模式,不直接涉及 KV cache 的存储格式。q4+q8 结合 flash-attn 兼容性良好。但如果 flash-attn 的 tile size 与 q4 块大小不匹配,可能影响加速效果。
本机推演与建议
| 配置 | KV cache 估算占用 | 显存余量提升 | 推荐度 |
|---|---|---|---|
| 当前 q8_0 | 基线 | — | ✅ 质量与速度的平衡点 |
| iq4_nl | ~q8_0 的 50% | 释放约 1.5-2 GiB/卡 | ⭐ 高(需要更大余量时首选) |
| q4_0 | ~q8_0 的 50% | 同上 | 🟡 可接受(长上下文质量略降) |
| q8_0 + 降低 ctx | 65K 时约为 225K 的 29% | 释放 4-6 GiB/卡 | ⭐ 高(与 q4 组合效果更佳) |
结论:当前 q8_0 配置是合理的平衡点。如果 device=none 释放 ~1 GiB 后显存仍有压力,优先考虑 iq4_nl(质量损失最小)或 降低 ctx-size 到 65K(按你 50K+8K 的实际用量)。q4_0 在长上下文下的累积误差可能影响 RAG 检索准确率,不推荐。
# 方案A:仅改 kv cache 类型(保质量)
cache-type-k = iq4_nl
cache-type-v = iq4_nl
# 方案B:降低上下文(最有效)
ctx-size = 65536
KV Cache 与显存分析
KV Cache 占用实测
- 请求2 后保存 idle slot 到 prompt cache:170 tokens 占 64.581 MiB → 0.38 MiB/token
- checkpoint 创建:260 tokens 占 62.813 MiB → 0.24 MiB/token(slots 内不重复存储)
- 对比配置说明中声称的 0.031 MiB/token:实际通过 kv-unified 共享后 checkpoint 存储接近此值,但 prompt cache 因存储完整状态略大。
缓存上限 8,192 MiB,最大缓存 225,280 tokens。当前 cache state 仅有 1 prompt 占据 64.581 MiB,远未满。
显存瓶颈
- 修复前:GPU 0 占用 91.8%,剩余仅 ~1.3 GiB
- 修复后(device=none):GPU 0 预计 ~87%,空余 ~2 GiB;GPU 1 ~78%,空余 ~3.6 GiB
- 当前 batch-size 2048 + ubatch-size 512 下,KV cache + 模型权重基本占满
- 如需增大上下文或并行数,需降低 batch/ubatch 或 KV cache 量化到 iq4_nl
CPU 线程调优参考
| 线程数 | Batch / UB | PP (50K) | TG | 备注 |
|---|---|---|---|---|
| 8 | 2048 / 1024 | — | — | 基线配置 |
| 12 | 2048 / 256 | — | ~70 t/s | ubatch 过小限制 PP |
| 12 | 2048 / 512 | ~300 t/s | ~73 t/s | 当前最优平衡点 |
| 12 | 2048 / 1024 | — | — | ubatch 增加拉低 TG |
| 24 | 4096 / 512 | ~150 t/s | ~75 t/s | batch 翻倍但 TG 无明显提升 |
| 24 | 4096 / 1024 | ~150 t/s | ~72 t/s | 更多 CPU 但 TG 反而略降 |
当前 12 线程 + 2048/512 在 PP/TG 平衡上属于较优选择。PP 主要受 GPU(V100)限制,增加线程对 PP 帮助有限;24 线程时 TG 仅 +2 t/s 但 PP 降一半,得不偿失。
Prompt Cache 行为
- 启用:首次启动时 prompt cache 为空,后续 idle slot 会被保存到 cache
- 请求2 的 slot 分配:使用 LRU 选择 slot 2(上次空闲),slot 3 被保存到 cache(64.581 MiB)
- 缓存命中:第二请求的 prompt 与缓存不相似 (sim=0.000),需完全重新计算
- 有效场景:多用户重复相同 system prompt 时,缓存可复用首部 token,大幅减少 PP 时间
- 当前缓存上限 8 GiB,足够存储约 12 万个完整状态 token
Embedding 模型分析
显存泄漏根因:CUDA Primary Context
embedding 模型配置了 main-gpu = -1 和 n-gpu-layers = 0(CPU-only),但仍然在两卡上占用 ~1 GiB 显存。根因在于 llama.cpp router 模式的子进程 spawn 机制:
- Router(server.cpp:89-95)在启动时调用
llama_backend_init()加载 CUDA backend,但跳过设备枚举(common_params_print_info的print_devices=false),router 自身不创建 CUDA primary context - 当 embed 请求触发模型加载时,router 通过
server-models.cpp的get_environment()(第130-151行)捕获全部环境变量,再用subprocess.hspawn 子进程 - 子进程完整初始化后,
common_params_print_info(params, true)枚举设备 → CUDA primary context 被创建 → 显存分配(即使模型不加载到 GPU)
从 common.cpp 源码确认:
// common.cpp:386
// device enumeration creates a primary context on CUDA backends,
// skip it when the caller does not own any device
解决方案:device = none
llama.cpp v9000+ 引入了 --device 参数,语义为 "comma-separated list of devices to use for offloading (none = don't offload)"。
配置方式:在 ini 的 [embed] 段落中加入 device = none,ini key 通过 server-model-meta::update_args() 中的 preset.to_args() 转换为 CLI arg --device none 传给子进程。
源码验证(common.cpp:1503-1504):
if (!params.devices.empty()) {
mparams.devices = params.devices.data();
}
// 空向量 → mparams.devices = nullptr → 完全不接触 GPU
当 device = none 时,params.devices 为空向量,设备枚举被跳过,CUDA primary context 不会被创建,显存占用归零。
Embedding 模型配置(修复后)
[embed]
model = /home/x99/gguf/Qwen3-Embedding-0.6B.Q4_K_M.gguf
chat-template-file = /home/x99/gguf/Qwen3-Embedding-0.6B.jinja
ctx-size = 8192
parallel = 2
kv-unified = 1
device = none # 彻底禁用 GPU,跳过 CUDA primary context
no-warmup = 0 # 预热
mlock = 1 # 锁内存防 swap
threads = 12
batch-size = 512
ubatch-size = 512
pooling = cls
embedding = 1
- parallel 从 3 降为 2(embedding 并发通常 1-2 路,足够)
kv-unified = 1保持共享上下文device = none后n-gpu-layers和main-gpu不再需要(保留也无影响)
优化建议
- Embedding 显存泄漏 ✅ 已解决:添加
device = none跳过 CUDA primary context 创建,两卡释放 ~1 GiB。 - KV Cache 压缩:如需要更多显存余量,优先尝试
cache-type-k = iq4_nl / cache-type-v = iq4_nl(质量接近 q8_0,显存减半),或直接降低ctx-size = 65536(按 50K+8K 实际用量)。详见 §"KV Cache 量化精度分析"。 - 并行数调优:n_parallel=4(auto)对当前使用场景偏多,可考虑
--parallel 2。 - 上下文调小:n_ctx=225,280 接近模型上限 262,144,实际 input 50K + output 8K 的场景下设为 65,536 即可释放大量 KV cache 显存。
- 启用 warmup:主模型移除
--no-warmup(或设为 0)避免首次请求 PP 慢。
Router 模式注意事项
- 使用
--models-preset的多模型路由模式标记为 experimental,但功能稳定可用 - 子进程绑定 127.0.0.1 而非 0.0.0.0(安全,但如需外部访问需注意)
- 模型按需加载:首次请求触发加载(~1.16s),后续请求直接服务
- 子进程间通过 stdin/stdout pipe 与 router 通信,对外统一暴露 8080 端口
- 子进程通过
get_environment()(server-models.cpp:130-151)继承父进程全部环境变量。要修改子进程环境变量需在 router 启动前设置。