从镜像升级到参数优化,llama.cpp让35B模型跑出170t/s

从镜像升级到参数优化,llama.cpp让35B模型跑出170t/s

深夜两点,你盯着终端里跳动的t/s数字,从100到150再到170——这感觉就像给自己改装的赛车调ECU参数,每一档提升都带着成就感。llama.cpp的Docker镜像刚更新了56个版本,我花了整个周末把线上服务从b9803升到b9859,顺便把两个35B模型的推理速度从120t/s左右直接推到170t/s。这篇文章就是你的调优手册,每一步都能复制。

为什么要动它

我手头有两个35B参数的GGUF模型:Qwen3.6-35B-A3B(带多模态)和Ornith-1.0-35B。它们跑在双卡NVIDIA GPU上,每张卡各管一个。之前用的镜像版本是b9803,跑起来也就120-130t/s的水平,对于需要流式输出的场景来说,这个速度刚够用,但离“流畅”差口气。
翻了下llama.cpp的GitHub仓库,发现从b9803到b9859(6月28日到7月2日),竟然有56个版本的更新。重点是几个关键修复和优化:
b9856:CUDA Flash Attention加上了restrict约束和PDL优化,意味着FA的性能会显著提升
b9851:修复了CUDA FA mask stride的整数溢出——这是个真正会让推理结果出错的bug
b9850:Qwen3Next模型修复、DFLASH修复,对Qwen3系列用户来说是必修课
升级命令简单得让人怀疑是不是漏了什么步骤:

docker compose pull
docker compose up -d

两条命令,端口映射不变,容器自动重建。但我告诉你,真正的技术活是在容器跑起来之后才开始的。

参数调优:暴露出我之前写的配置有多蠢

新镜像启动后,我习惯先看一眼help输出和启动日志。这不看不知道,一看吓一跳——原来之前的配置里藏着好几个冗余和无效参数。
第一个坑:环境变量和CLI参数重复
之前我的docker-compose.yml里这样写:

environment:
  - LLAMA_ARG_FLASH_ATTN=on    # 冗余!
  - LLAMA_ARG_N_GPU_LAYERS=99
command: >
  --flash-attn on               # 这里也写了

LLAMA_ARG_FLASH_ATTN这个环境变量和--flash-attn on参数是同一个意思。llama.cpp会优先认CLI参数,环境变量的值根本不生效。这两个同时存在,就像给同一个开关配了两个遥控器——但只有一个能真正控制灯。我当时这么写是因为早期版本习惯用环境变量,后来改成CLI参数时没清理干净。改掉。
第二个发现:新版默认值比我的手动配置更优
之前我用的是--spec-draft-n-max 2,意思是MTP(多令牌预测)最多生成2个候选token。新版本把这个默认值改成了3。对于Qwen3这类原生支持MTP的模型,多一个候选意味着更多的匹配机会。实测接受率从约55%提升到62%,推理速度大约加快了15-20%。
第三个新参数:--jinja
这个参数看着不起眼,但效果显著。Qwen3模型内置了Jinja2格式的对话模板(chat template)。加上--jinja后,/v1/chat/completions接口能正确区分system、user、assistant各个角色的消息。之前没有这个参数,多轮对话时助理消息经常串格式,系统提示词被当成用户输入。现在一个参数全搞定。
第四个移除:无效的--cache-reuse
启动日志里我看到两行警告:
W srv load_model: cache_reuse is not supported by multimodal, it will be disabled
W srv load_model: cache_reuse is not supported by this context, it will be disabled
第一个警告是因为我用了多模态模型(Qwen3.6-35B-A3B),第二个是因为上下文类型不兼容。这个参数从始至终没起过作用,我还一直以为它提升着性能。果断删掉。
第五个新参数:--reasoning-preserve
新版启动时给出了提示:
I srv init: chat template supports preserving reasoning, consider enabling it via –reasoning-preserve
这个参数的作用是保留模型的思考链(reasoning tokens)。Qwen3这类模型在回答问题前,会先发出一段思考过程,比如“嗯,用户问的是XX问题,我首先需要理解意图,然后分步骤回答…”如果没有这个参数,历史记录里只能看到最后的回答,看不到思考过程。对于需要审计或调试的场景非常有用。
调整后的配置干净了许多:

services:
  lla-qw35-gpu0:
    image: ghcr.io/ggml-org/llama.cpp:server-cuda
    container_name: lla-qw35-gpu0
    restart: unless-stopped
    runtime: nvidia
    ports:
      - "11434:8080"
    volumes:
      - ./models:/models:ro
    environment:
      - CUDA_VISIBLE_DEVICES=0
      - LLAMA_ARG_N_GPU_LAYERS=99
      - GGML_CUDA_GRAPH_OPT=1
    command: >
      -m /models/Qwen3.6-35B-A3B-UD-IQ4_XS.gguf
      --mmproj /models/mmproj-Qwen3.6-35B-A3B-F16.gguf
      --host 0.0.0.0 --port 8080
      --ctx-size 131072
      --cache-type-k q8_0 --cache-type-v q4_0
      --flash-attn on --parallel 1
      -ngl 99 -t 8 -tb 4
      -b 2048 -ub 1024
      --mlock --no-mmap
      --image-min-tokens 1024
      --jinja --reasoning-preserve
      --spec-type draft-mtp
      --spec-draft-type-k q4_0 --spec-draft-type-v q4_0

重启容器后,我对着终端深呼吸。这次调优能不能看到效果,就看这把了。

检验成果:数字不会说谎

我用OpenAI兼容API的/v1/completions接口做测试,请求生成100个token,采集服务器返回的timing数据。
Qwen3.6-35B-A3B(GPU 0)
Prompt: 10 tokens, 125.5 t/s
生成: 100 tokens, 169.5 t/s (5.9ms/tok)
MTP: draft=103, accepted=64 (接受率 62%)
Ornith-1.0-35B(GPU 1)
Prompt: 10 tokens, 68.3 t/s
生成: 100 tokens, 152.6 t/s (6.6ms/tok)
看着屏幕上跳动的169.5 t/s,我一瞬间还有点不敢相信。这个数字意味着什么?如果你用流式输出,用户几乎感觉不到等待——生成了100个token,只需要0.59秒。对比我之前在b9803版本上的120t/s,提升了约40%
关键发现是Qwen3.6-35B-A3B的MoE(混合专家)架构在这里的优势——虽然总参数量是35B,但每个token实际只激活约3B参数。这意味着推理时真正在计算的部分更小,速度快是符合预期的。MTP推测解码的62%接受率意味着每步推理平均生成1.86个token,相比无推测解码的理论速度接近翻倍。Ornith虽然用的是ngram-mod推测解码,也跑到了152.6 t/s。

DFLASH:看起来很香但先别急

新版本还增加了一个叫draft-dflash的推测解码方式,已经列入了--spec-type的可选列表:
–spec-type none,draft-simple,draft-eagle3,draft-mtp,draft-dflash,
ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache
但DFLASH需要额外一个独立的草稿模型文件,通过--spec-draft-model指定。不像MTP是直接利用模型内置的多预测头,零额外显存开销。如果没有现成的draft GGUF文件,MTP依旧是Qwen3系列的最佳选择。

三个你跑不掉的坑

  1. 重复参数:llama.cpp支持环境变量和CLI参数两种方式,同时设置时CLI优先。建议只用一种。如果你的启动日志里没出现你预期的config值,检查一下是不是同时写了两个地方。
  2. --cache-reuse不是万能药:这个参数在多模态模型和部分上下文类型下明确不支持。启动后花30秒检查日志中的warning,不要像我一样配置了几个月才发现它根本没工作。
  3. server-cuda这个tag指向最新版:每次docker compose pull可能拉到不同的构建版本。如果出现了意料之外的兼容性问题,先检查system_fingerprint字段确认版本号。
    最后,如果你想马上验证这个配置,这里是你只需要复制粘贴一次的命令块:
# 1. 拉取最新镜像
docker pull ghcr.io/ggml-org/llama.cpp:server-cuda
# 2. 启动(假设模型在 ./models/ 目录下)
docker run --gpus all -p 8080:8080 \
  -v ./models:/models:ro \
  ghcr.io/ggml-org/llama.cpp:server-cuda \
  -m /models/你的模型.gguf \
  --host 0.0.0.0 --port 8080 \
  --ctx-size 131072 \
  --cache-type-k q8_0 --cache-type-v q4_0 \
  --flash-attn on --parallel 1 \
  -ngl 99 -t 8 -tb 4 \
  --jinja --reasoning-preserve \
  --spec-type draft-mtp
# 3. 验证服务是否正常
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"default","messages":[{"role":"user","content":"Hello"}]}'
# 返回200且包含"choices"字段即为正常

这此升级和优化,让我不仅提升了单个模型的性能,更清理了配置里的“垃圾参数”。两个35B模型跑到了169.5 t/s和152.6 t/s,对于本地部署的LLM服务来说已经是非常流畅的体验。
如果你也用过llama.cpp的MTP参数,或者遇到过类似的配置冗余,欢迎在评论区聊聊你的优化方案。你最头痛的参数是哪一个?是context-size的取舍,还是flash-attn的坑?


皖ICP备2025105865号-2|皖公网安备34010402704739号