文档日期:2026-08-17|本文档为公开脱敏版 (原内网 IP 与用户名已替换为
<THOR_IP>/%USERPROFILE%占位符)设备:Jetson Thor(Blackwell T5000)|引擎:llama.cpp(GGUF)|量化:Q8_0(~29.0 GB)+ MTP 投机解码
状态:✅ 部署完成且接入 WorkBuddy(服务
http://<THOR_IP>:8080,模型thor-qwen38-q8,实测对话通过)
一、实验目的
在 Jetson Thor(128GB 统一内存、ARM aarch64、sm_110a)上,用 llama.cpp + Unsloth Q8_0 GGUF + MTP 投机解码 部署 Qwen3.8-27B,验证:
- 最新 llama.cpp 能否识别
qwen35新架构; - Q8_0(~29GB)在 Thor 带宽墙下的真实推理速度;
- 思考档位(reasoning_effort)与多模态(mmproj)是否可用;
- 能否接入 WorkBuddy / Pi 作为 OpenAI 兼容端点。
二、实验环境
2.1 硬件(Thor)
| 项 | 值 |
|---|---|
| SoC | NVIDIA Jetson Thor(Blackwell T5000) |
| 统一内存 | 128 GB LPDDR5X(物理可用 ~122 GB) |
| 内存带宽 | ~273 GB/s(本机瓶颈,非容量) |
| 计算能力 | sm_110a(Blackwell) |
| 架构 | aarch64(ARM) |
2.2 软件栈(Thor)
| 项 | 值 |
|---|---|
| 系统 | JetPack R39.2 |
| CUDA | 13.2.1(/usr/local/cuda/bin/nvcc) |
| llama.cpp 源码 | /opt/llama.cpp-src(若 .git 正常则 git pull;若丢失见阶段 1 恢复方案) |
| 编译产物 | /opt/llama.cpp-src/build/bin/llama-server |
| 模型目录 | /opt/models/Qwen3.8-27B/ |
2.3 模型规格(Qwen3.8-27B)
- 类型:Dense 27.78B,Apache 2.0,原生视觉(文本/图像/视频→文本)
- 架构:
Qwen3_5ForConditionalGeneration,64 层 = 48 层 Gated DeltaNet 线性注意力 + 16 层 Gated Attention - 原生上下文 262K(YaRN 可扩 1M),内置 MTP 草稿头(投机解码用)
- 默认开启思考模式(Thinking),effort 可由客户端经
reasoning_effort控制(none/low/medium/high/xhigh);本实验 WorkBuddy 默认设为low
2.4 量化选型说明(为什么是 Q8_0)
| 量化 | 大小 | 说明 |
|---|---|---|
| UD-IQ2_XXS | 9.0 GB | 质量损失大,不推荐 |
| IQ4_XS | 15.7 GB | 速度与质量甜点 |
| Q4_K_M | 17.1 GB | 最常用甜点 |
| Q8_0(本实验) | 29.0 GB | 高质量,Thor 内存富裕装得下 |
| BF16 | 53.8 GB | 最高质量,但最慢 |
⚠️ 速度与质量的权衡 :Thor 是带宽瓶颈,每 token 需搬运的权重越多越慢。Q8_0(~29G) 比 Q4_K_M(~17G) 多搬运约 1.7× 数据 → 预计 Q8_0 单流 ~9--12 tok/s,Q4_K_M ~15--20 tok/s(实测 Q8_0 达 17.2 tok/s,MTP 生效后超出预估,见实验结果表)。选 Q8_0 = 用速度换质量;若后续嫌慢,可无缝换 Q4_K_M(命令仅改文件名)。
三、实验步骤
阶段 1:更新并重新编译 llama.cpp(必须)
⚠️ 早于 b8001 的旧版 llama.cpp 不支持
qwen35架构 ,加载会报unknown architecture错误。需更新至含 b8001+ 的提交。
🔧 若git pull报fatal: not a git repository:源码在但.git丢了(多半是拷贝/解包而非完整克隆)。二选一恢复:方案 A(推荐,最干净)--- 重新克隆:
bashcd /opt mv llama.cpp-src llama.cpp-src-old # 旧版留作备份,编译成功后再删 git clone --depth 1 https://github.com/ggerganov/llama.cpp.git llama.cpp-src # 若 GitHub 直连慢,用镜像: # git clone --depth 1 https://ghproxy.com/https://github.com/ggerganov/llama.cpp.git llama.cpp-src方案 B(保留旧 build 目录,增量重编)--- 原地重建 git:
bashcd /opt/llama.cpp-src rm -rf .git git init -q git remote add origin https://github.com/ggerganov/llama.cpp.git git fetch --depth 1 origin master git checkout -B master FETCH_HEAD恢复后继续下面步骤。注意:
--depth 1是浅克隆,仓库不含 git tag,后续--version看不到 build 号(见下方版本核对说明)。
若已按上方恢复方案重新克隆,当前代码已是 master 最新,下方 git pull 为 no-op(或直接进入编译)。
bash
cd /opt/llama.cpp-src
git pull # 拉取至含 b8001+ 的提交(支持 qwen35 + qwen3vl_merger)
export PATH=/usr/local/cuda/bin:$PATH
export CUDACXX=/usr/local/cuda/bin/nvcc
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=110a
cmake --build build -j$(nproc)
-
预期:编译成功,
build/bin/llama-server存在。 -
版本核对(重要) :
build/bin/llama-server --version可能显示0.1.0-dev (build 1, commit <hash>)而非b8001------这是浅克隆无 tag 的兜底串,不表示版本旧 。判断是否支持架构,请用源码确认(任一条有输出即通过):bashgrep -rn "qwen35" /opt/llama.cpp-src/src/llama-model.cpp grep -rn "qwen3vl_merger\|qwen35" /opt/llama.cpp-src/src/ | head -
参考实测:版本串显示
0.1.0-dev (build 1, commit 4197155)(浅克隆无 tag 不显示 build 号,架构支持以源码grep qwen35确认) 编译成功
阶段 2:获取 Q8_0 权重与视觉 projector
权重文件(HuggingFace unsloth/Qwen3.8-27B-GGUF):
- 文本:
Qwen3.8-27B-Q8_0.gguf(29.0 GB) - 视觉(可选,多模态才要):
mmproj-F16.gguf(~0.93 GB)
传输策略(Thor 出口 ~5MB/s,本机侧下载更快):
-
本机拉取:
pip install -U "huggingface_hub[cli]"后bashhf download unsloth/Qwen3.8-27B-GGUF Qwen3.8-27B-Q8_0.gguf mmproj-F16.gguf --local-dir /opt/models/Qwen3.8-27B⚠️
--local-dir必须用绝对路径/opt/models/Qwen3.8-27B!若写成相对路径./Qwen3.8-27B,文件会下到当前目录 下(如/opt/llama.cpp-src/Qwen3.8-27B/),启动时仍报No such file------这是部署中常见的坑(下载 100% 但服务秒退)。若已下错位置,用mv /opt/llama.cpp-src/Qwen3.8-27B /opt/models/挪正。 -
本机起 HTTP 服务器(端口 8801),Thor 用
aria2c局域网直拉(实测 ~115 MB/s)。 不要用 Thor 直连 hf-mirror/Xet(限速 5MB/s,且 Xet 签名易过期 403)。
- 传输方式(任选其一):① 本机 HTTP+aria2(~115 MB/s) ② Thor 直连
hf download(注意--local-dir相对路径坑见下) - 文件落盘:
/opt/models/Qwen3.8-27B/大小:Qwen3.8-27B-Q8_0.gguf28G +mmproj-F16.gguf885M(均为完整文件)
⚠️ 启动前必做的前置检查(跳过后阶段 3 会秒退------这是部署中常见的坑):
bashls -lh /opt/models/Qwen3.8-27B/期望看到两个文件:
Qwen3.8-27B-Q8_0.gguf(~29G)和mmproj-F16.gguf(~0.93G)。若ls报No such file or directory或文件不全,说明阶段 2 未完成,先回去下载/传输,不要直接进阶段 3 ------否则 llama-server 启动即报failed to open GGUF file ... (No such file or directory)并退出,8080 不监听,冒烟测试全部无反应。
阶段 3:启动服务(MTP + 关键 flag)
下方命令不含行内注释(bash 中反斜杠续行后若跟空格/注释会破坏续行)。各参数含义见紧随其后的「关键 flag 说明」表。
bash
/opt/llama.cpp-src/build/bin/llama-server \
-m /opt/models/Qwen3.8-27B/Qwen3.8-27B-Q8_0.gguf \
--mmproj /opt/models/Qwen3.8-27B/mmproj-F16.gguf \
-ngl 99 --flash-attn on \
--ctx-size 262144 \
--cache-type-k q8_0 --cache-type-v q8_0 \
--spec-type draft-mtp --spec-draft-n-max 3 \
--parallel 1 \
--jinja \
--reasoning-format deepseek \
-b 2048 -ub 512 \
--host 0.0.0.0 --port 8080 \
--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.0 \
2>&1 | tee /tmp/qwen38.log
- 若不需要视觉,删除
--mmproj那一行即可(省 ~0.93GB)。 - 如需把日志单独落盘,末尾已接
2>&1 | tee /tmp/qwen38.log。 - 启动耗时(加载+初始化):<8 s(启动后 8 s 即见
listening on http://0.0.0.0:8080;mmap 懒加载,权重不预载) 成功监听 8080 - 日志确认架构识别:
grep -i "qwen35\|architecture" /tmp/qwen38.log→qwen35✅(new llama_model_qwen35,架构正常识别)
关键 flag 说明(本模型专属)
| flag | 作用与为何必加 |
|---|---|
-m ...Q8_0.gguf |
主模型权重,Q8_0 量化 GGUF,~29GB。llama.cpp 只认 GGUF,不认 HF safetensors |
--mmproj ...mmproj-F16.gguf |
视觉投影器,多模态必需;纯文本 Coding Agent 可删除此行省 ~0.93GB |
-ngl 99 |
GPU 卸载层数,99=尽可能全卸载。Thor 为统一内存,指用 Blackwell 算力 |
--flash-attn on |
强制 FlashAttention,更快的前缀缓存、更低显存(默认 auto) |
--ctx-size 262144 |
KV 缓存窗口(单位 token)。本模型原生 262K,且线性层不占 O(N) KV,长上下文几乎免费 |
--cache-type-k/q8_0 |
KV 缓存量化。必须用 q8_0;q4_0/q4_1 会把注意力静默退回 CPU,prefill 从 ~1000 tok/s 掉到 ~35 |
--spec-type draft-mtp --spec-draft-n-max 3 |
MTP 投机解码(~2× 提速),需 GGUF 含 MTP 头;若被剥离则报错,删掉即可 |
--parallel 1 |
llama-server 默认 n_parallel=4,48 个线性注意力层会按 4 槽分配循环状态,多占 ~0.44GiB |
--jinja |
启用官方 chat 模板。不用它则走默认模板,思考块(<think>)多轮嵌套导致历史截断、模型「失忆」 |
--reasoning-format deepseek |
思考块提取格式。取值只有 deepseek / none (不是按模型厂商选):deepseek = 把思考轨迹放进响应的 message.reasoning_content 字段(DeepSeek API 约定,也是 OpenAI 兼容客户端/WorkBuddy 通用解析字段);none = 思考留在正文。需配合 --jinja,且仅对非流式响应生效。叫 deepseek 只是因为字段约定源于 DeepSeek API,与模型是 Qwen 无关 |
-b 2048 -ub 512 |
批大小 / 微批大小 |
--host/--port |
监听地址与端口,0.0.0.0 允许局域网访问 |
--temp/--top-p/--top-k/--min-p |
采样参数 |
--image-min-tokens 1024 |
(可选,启动日志提示)Qwen-VL 视觉 grounding 任务精度需要 ≥1024 图像 token;不处理视觉定位可省略 |
--reasoning-preserve |
(可选,启动日志提示)新版 llama.cpp 支持在对话中保留/传递上一轮思考痕迹,避免多轮思考丢失 |
💡 关于上下文长度 :上述主命令用原生
--ctx-size 262144(不加任何 rope flag),质量最好,是日常 / Coding Agent 的推荐默认。只有真要塞 500K~1M 文档/代码库时,才使用下方的 YaRN 变体。
阶段 3 变体:开启官方 1M 长上下文(YaRN)
⚠️ 官方重要提醒 :Qwen 明确指出静态 YaRN 的缩放因子对长短输入一视同仁,开启后会略微降低短文本质量。因此------
- 日常 / Coding Agent → 不要开 YaRN ,用原生 262K(
--ctx-size 262144,不加任何 rope flag),质量最好。- 只有真要塞 500K~1M 文档/代码库时才开下面这套。
原生上下文 262,144;YaRN factor=4.0 把它扩展至 ≈1.05M。命令(同样不含行内注释):
bash
/opt/llama.cpp-src/build/bin/llama-server \
-m /opt/models/Qwen3.8-27B/Qwen3.8-27B-Q8_0.gguf \
--mmproj /opt/models/Qwen3.8-27B/mmproj-F16.gguf \
-ngl 99 --flash-attn on \
--ctx-size 1000000 \
--cache-type-k q8_0 --cache-type-v q8_0 \
--rope-scaling yarn \
--rope-scale 4.0 \
--yarn-orig-ctx 262144 \
--override-kv qwen35.context_length=int:1000000 \
--spec-type draft-mtp --spec-draft-n-max 3 \
--parallel 1 --jinja \
--reasoning-format deepseek \
-b 2048 -ub 512 \
--host 0.0.0.0 --port 8080 \
--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.0 \
2>&1 | tee /tmp/qwen38.log
YaRN 关键参数说明(4 项必配 + 1 项可选)
| flag | 作用 |
|---|---|
--rope-scaling yarn |
选 YaRN 缩放(非 linear/NTK)。Qwen3.8 配置 rope_type=yarn |
--rope-scale 4.0 |
缩放因子 = 目标上下文 / 原生上下文。Qwen 官方对 1M 给 4.0 (≈1.05M)。若典型上下文 ~524K,改 2.0 更稳、对短文本损伤更小 |
--yarn-orig-ctx 262144 |
告诉 YaRN 模型原本训练在 262K 上,缩放以它为基准。漏填会退默认 |
--override-kv qwen35.context_length=int:1000000 |
GGUF 元数据上限默认 262K;这条抬到 1M,否则 --ctx-size 1000000 被拒。密集模型用 qwen35,MoE 用 qwen35moe (本模型是密集 qwen35) |
--yarn-beta-fast/-slow |
一般不改,留 -1(自动)。仅当长程检索丢东西再调 |
Thor 内存现实(1M 下)
- 权重 Q8_0 ~29GB + KV:因 48 层是 Gated DeltaNet(循环状态 O(1),不随长度线性增长),只有 16 层 Gated Attention 吃 O(N) KV,比纯注意力 27B 节省很多;但 1M token 的 KV 仍要 数十 GB (以启动日志
kv cache行 /--verbose为准)。 - 粗估:262K 全量 KV ≈16GB,1M ≈4× → ~64GB;+29GB 权重 ≈ 93GB ,逼近 Thor 122GB 上限。建议先
--ctx-size 262144(原生、无 YaRN)或524288(factor=2.0)验证,再上 1M。 - prefill 1M token 在 Thor ~273GB/s 带宽下极慢(分钟级),仅适合「一次灌入、反复问答」的文库场景,不适合交互式逐字对话。
验证 override 键名 :启动日志若报 unknown metadata key 或拒绝超长上下文,先确认 arch 前缀------
bash
grep -i qwen35 /tmp/qwen38.log
若前缀是 qwen3_5 而非 qwen35,把 --override-kv 键改成 qwen3_5.context_length=int:1000000。
阶段 4:冒烟测试 + 功能验证
先确认服务真的活着(权重缺失时 llama-server 秒退,这里必然失败):
bashps aux | grep llama-server | grep -v grep # ① 有进程 ss -tlnp | grep 8080 # ② 8080 正在监听 tail -5 /tmp/qwen38.log # ③ 日志末尾是 listening 而非报错① ② ③ 全过再往下测;任一失败 → 回到阶段 2 前置检查 / 阶段 4 排错表。
4.1 基础对话(curl)
bash
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"用一句话介绍你自己。"}],"max_tokens":200}'
- 预期结果:返回正常 首 token 延迟(TTFT) ~0.7 s 生成速度 17.2 tok/s(MTP 生效)
4.2 思考档位控制(降低 effort 换速度)
bash
# 关闭思考模式
curl -s http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"1+1=?"}],"max_tokens":100,"enable_thinking":false}'
# 降档到 low(更快)
curl -s http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"1+1=?"}],"max_tokens":100,"reasoning_effort":"low"}'
- 预期结果:思考输出可用(默认 effort 未显式指定时,
reasoning_content返回完整思考过程)low档位与enable_thinking=false按需验证
4.3 多模态(仅当阶段3加了 --mmproj)
bash
curl -s http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":[
{"type":"text","text":"图里有什么?"},
{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,'"$(base64 -w0 photo.jpg)"'"}}]}]}'
- 预期结果:图像输入被接受;若被忽略,检查是否加载了
--mmproj
阶段 5:性能基准测试
用一段固定 prompt(如「写一个 Python CLI,扫描文件夹里 Markdown 的失效相对链接,含单元测试」),记录:
- 输出 ~4096 tokens 的总耗时 → 生成速度 tok/s
- TTFT(首 token 延迟)
- 长上下文 prefill:60K prompt 总耗时(对照:SGLang/llama.cpp x86 ~40s 量级)
评估口径(参考社区):<8 tok/s 能用但慢;8--20 舒适;20+ 快。Q8_0 在 Thor 上实测 17.2 tok/s(MTP 生效),落入「舒适」档,显著高于带宽墙预估的 9--12。
阶段 6:接入 WorkBuddy / Pi
接入本地端点有两条路:GUI 添加(简单,适合大多数人) 或 直接改 JSON(进阶,字段最全) 。两者等价------GUI 保存后本质也是写
models.json;GUI 没暴露的高级字段,最后用 JSON 补齐即可。
6.1 方式一:WorkBuddy GUI 添加(简单,推荐)
在 WorkBuddy 设置里找自定义模型 / 自定义端点 / 模型管理入口(不同版本菜单位置和叫法略有差异,以你界面上的实际入口为准),新增一条,填三个必填项:
| 字段(GUI 上的叫法) | 填什么 | 说明 |
|---|---|---|
| 名称 / 显示名 | Thor Qwen3.8-27B Q8 |
模型选择器里显示的名字,随意 |
| Base URL / API 地址 | http://<THOR_IP>:8080/v1 |
OpenAI 兼容根地址(注意带 /v1 ,别只填到 :8080) |
| API Key | local-thor(任意非空字符串) |
llama.cpp 不校验 key,随便填、不为空即可 |
| 模型 ID(如 GUI 提供) | thor-qwen38-q8 |
若 GUI 让你填模型名,就填这个 |
保存后刷新,模型选择器里就能看到它,直接开聊。GUI 会把这条配置落到 %USERPROFILE%\.workbuddy\models.json------效果与手写 JSON 完全等价。
⚠️ GUI 通常只暴露基础字段。如果还想控制这些高级项,GUI 没有对应输入框 → 转 6.2 手改 JSON 补齐:
maxInputTokens:强烈建议 65536(设太大 = 无限制,agent 会塞入超长上下文 → prefill 超时 → 500,详见第五章 5.4)reasoning.defaultEffort:思考档位默认low(none关闭,xhigh最强但最慢)supportsImages/supportsToolCall/supportsReasoning:能力开关,影响 WorkBuddy 是否向你开放这些功能
6.2 方式二:直接改 JSON(进阶,字段最全)
%USERPROFILE%\.workbuddy\models.json 追加(字段对齐本机既有 DeepSeek 条目,思考默认开启):
json
{
"id": "thor-qwen38-q8",
"name": "Thor Qwen3.8-27B Q8",
"vendor": "Custom",
"url": "http://<THOR_IP>:8080/v1/chat/completions",
"apiKey": "local-thor",
"supportsToolCall": true,
"supportsImages": true,
"supportsReasoning": true,
"useCustomProtocol": false,
"reasoning": {
"defaultEffort": "low",
"supportedEfforts": ["none", "low", "medium", "high", "xhigh"]
},
"maxInputTokens": 65536,
"maxOutputTokens": 16384
}
reasoning.defaultEffort设为"low"(思考开启,但较轻量------Thor 上 Q8_0 实测 17.2 tok/s,xhigh 会明显变慢)。更难的任务可在 WorkBuddy 里手动切到medium/high/xhigh;none为关闭思考。若想要模型原生最强思考,把"defaultEffort"改成"xhigh"即可。⚠️
maxInputTokens为什么是 65536 而不是 262144 :服务端--ctx-size保持 262144 不变(模型支持长上下文),但 WorkBuddy 侧把它当作单次请求的输入上限。若设成 262144,agent 会把巨量上下文全塞进来(实测曾一次塞入 92,329 tokens)→ Thor 上 prefill 要 300 秒+ → 客户端超时 → 报 500(详见第五章 5.4 实战案例)。65536 对 agent 场景绰绰有余(单轮通常几千 token),prefill 几秒内完成。
Pi (%USERPROFILE%\.pi\agent\models.json,contextWindow 同样建议 65536,原因同上)。Pi 没有 GUI,只能改 JSON------它是纯 CLI 工具,配置只有这一个入口:
json
{"providers":{"thor-local":{"baseUrl":"http://<THOR_IP>:8080/v1","apiKey":"local-thor",
"models":{"thor-qwen38-q8":{"contextWindow":65536,"maxOutputTokens":16384,"supportsReasoning":true}}}}}
- 验证:WorkBuddy 可调用(实测对话通过) Pi 可调用(配置写入后即可用)
停止与清理 :停止服务用 pkill llama-server(或 Ctrl-C);杀进程后 jtop 仍显示数十 GB 占用属正常(NVIDIA 驱动缓存池),执行 sync && echo 3 > /proc/sys/vm/drop_caches 即可释放,非内存泄漏。
四、已知坑与排错
| 现象 | 原因 | 解决 |
|---|---|---|
unknown architecture / 拒绝加载 |
llama.cpp 版本 < b8001(qwen35 未支持) | 更新并重编(阶段1) |
| prefill 极慢(~35 tok/s) | KV cache 用了 q4_0 静默退回 CPU | 改 --cache-type-k/q8_0 |
| 多轮「失忆」、历史截断 | 默认模板思考块(<think>)多轮嵌套 |
加 --jinja(用修正模板) |
| 多占 0.44GiB | llama-server 默认 4 槽 | 加 --parallel 1 |
--spec-type draft-mtp 报错 |
GGUF 被剥离 MTP 头 | 删掉该 flag(仅失投机解码提速) |
| 图像被静默忽略 | 未加载 mmproj | 加 --mmproj mmproj-F16.gguf |
启动命令秒退,日志报 failed to open GGUF file ... (No such file or directory) |
模型权重/mmproj 未传到 Thor(阶段2 未完成) | 完成阶段 2 并做前置检查(ls -lh /opt/models/Qwen3.8-27B/)后再启动 |
hf download 已 100% 但启动仍报 No such file |
--local-dir 用了相对路径,文件下到当前目录(如 /opt/llama.cpp-src/Qwen3.8-27B/) |
用绝对路径重下,或 mv /opt/llama.cpp-src/Qwen3.8-27B /opt/models/ 挪正 |
| 冒烟测试 curl 无反应 / connection refused | 服务没起来(权重缺失秒退、端口被占、未加 --host 0.0.0.0) |
按阶段 4 开头「三连检查」定位:ps → ss → tail 日志 |
| 杀进程后 jtop 仍显示数十 GB 占用 | NVIDIA 驱动缓存池(KReclaimable),非泄漏 | sync && echo 3 > /proc/sys/vm/drop_caches |
WorkBuddy 报 500 / parse_error |
输入上下文过大 → prefill 超时 → 客户端超时重试于脏连接(详见 5.4 实战案例) | 把 models.json 的 maxInputTokens 降到 65536 |
五、运行中错误排查(读日志方法论)
本节是实际排查「WorkBuddy 调 Thor 报 500」的完整复盘,沉淀为通用方法。排查运行期问题的第一动作永远是:看日志
/tmp/qwen38.log(启动命令已2>&1 | tee /tmp/qwen38.log落盘)。
5.1 三连检查(先判断服务死活,30 秒)
bash
ps aux | grep llama-server | grep -v grep # ① 进程在不在(有输出=活着)
ss -tlnp | grep 8080 # ② 8080 是否监听
tail -5 /tmp/qwen38.log # ③ 日志末尾是 listening 还是报错
① ② ③ 全过 → 服务活着,问题在请求侧(往下看日志);任一失败 → 服务没起来,去阶段 2/3 排错。
5.2 日志关键行解读表(读日志要会看什么)
| 日志片段(节选) | 含义 | 判断 |
|---|---|---|
I srv llama_server: model loaded |
模型加载成功 | ✅ 正常 |
I srv llama_server: listening on http://0.0.0.0:8080 |
服务已监听,可接受请求 | ✅ 正常 |
I llama_model_loader: arch = qwen35 |
架构识别成功(qwen35) | ✅ 正常 |
I slot print_timing: prompt processing, n_tokens = 92329, ... tokens per second |
prefill 进度:已处理的 token 数 / 当前速度 | ⚠️ 长 prompt 会很大,用于判断是否超时根因 |
I slot print_timing: n_gen = 157, tg = 12.61 tokens per second |
生成速度(tg=实际输出 tok/s) | ✅ 正常,即速度 |
I slot print_timing: draft acceptance = 0.61 |
MTP 投机接受率(0.61=61% 被接受) | ✅ >0.3 即有效 |
I slot print_timing: total time = ... / 92517 tokens |
整次请求总耗时/总 token | ⚠️ 判断慢在哪 |
W srv operator(): got exception: {"error":...} |
请求处理异常,客户端 500 的直接来源 | ❌ 看 error 里 message |
E gguf_init_from_file: failed to open GGUF file ... |
权重文件缺失/路径不对(启动即退) | ❌ 回阶段 2 |
E ... unknown architecture ... |
版本太旧不支持 qwen35 | ❌ 回阶段 1 重编 |
5.3 常用日志操作
bash
tail -f /tmp/qwen38.log # 实时跟踪(排错首选)
tail -100 /tmp/qwen38.log # 最近 100 行
grep -i "exception\|failed\|error" /tmp/qwen38.log # 只看错误
grep "print_timing" /tmp/qwen38.log | tail -5 # 最近几次请求的计时汇总
grep "draft acceptance" /tmp/qwen38.log | tail -3 # MTP 接受率趋势
5.4 实战案例:WorkBuddy 报 500 / parse_error
现象 :WorkBuddy 对话报 500|Trace ID: ...,提示"切换模型或重试"。
排查过程(就是读日志):
-
三连检查 → 进程在、8080 监听、日志末尾有
listening→ 服务活着,问题在请求侧。 -
tail -60 /tmp/qwen38.log→ 发现异常前一刻有一条巨大请求的计时:I slot print_timing: prompt eval time = 301350.38 ms / 92329 tokens (3.26 ms/token, 306.38 tokens per second) I slot print_timing: total time = 316184.13 ms / 92517 tokens -
紧跟着:
W srv operator(): got exception: {"error":{"code":500,"message":"[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'P'",...}}
解读(两层):
- 第一层 :
parse error ... last read: 'P'--- 请求 body 首个字符是P,不是{。这是请求头没被完整消费、body 从错误位置开始读 的典型症状,常见于客户端超时后重试连接把脏数据发过来。 - 第二层(根因) :为什么会超时?看那条 92,329 tokens 的 prefill------WorkBuddy 把 92K 上下文全塞进来了 (
maxInputTokens当时是 262144,等于没限制),Thor 上 prefill 92K 要 301 秒(5 分钟)。客户端等不了 → 超时 → 连接被复用/重试 → 脏连接上收到残缺请求 → 500。
根因一句话 :不是 WorkBuddy 对 llama.cpp 支持不好,而是 maxInputTokens 设太大 + 慢 prefill 撞上客户端超时。llama.cpp 本身把 92K 请求处理完了(total time 316s 是完整跑完的),是客户端先放弃了。
解决 :models.json 里 thor-qwen38-q8.maxInputTokens 262144 → 65536。效果:WorkBuddy 不再塞超长上下文,prefill 秒级完成,500 消失。
预防与后续:
- 若还偶发 500 → 再降到 32768;或看是否是多轮对话历史累计过长(换新会话)。
- 服务端
--ctx-size保持 262144 不冲突------限制的是客户端单次发送上限,不是服务端能力。 - 真需要 100K+ 长上下文(文库问答)→ 这类场景本质不适合 300s prefill 的交互式对话,建议换 vLLM 或接受慢速。
5.5 排查口诀
进程 → 端口 → 日志 → 找
got exception→ 看异常前那条print_timing的 n_tokens 和耗时。99% 的"客户端 500 / 无反应"都能在这条链路里定位到根因。
六、实验结果记录表
| 指标 | 预期 | 实测 |
|---|---|---|
| llama.cpp 版本 | ≥ b8001(或源码含 qwen35) | 0.1.0-dev (build 1, commit 4197155),源码含 qwen35 ✅ |
| 架构识别 | qwen35 | qwen35 ✅(new llama_model_qwen35) |
| 加载耗时 | --- | 启动即监听(mmap 懒加载);首次推理含权重换入共 7.2 s |
| 峰值内存占用(含 KV+mmproj) | ~30 GB@32K / ~38 GB@128K / ~46 GB@262K | 进程 RSS ~2.8 GB(mmap 懒加载,权重走页缓存;以 jtop 为准) |
| 单流生成速度(Q8_0) | ~9--12 tok/s | 17.2 tok/s(MTP 生效,超预期) |
| prefill 速度 | --- | 89.4 tok/s(62 tok 用时 693 ms) |
| TTFT(短 prompt) | --- | ~0.7 s(prompt eval 693ms) |
| MTP 提速比 | ~1.5--2× | draft acceptance 0.56(71/126),mean len 2.69 |
| 思考输出 | reasoning_content 字段 | ✅ 返回完整思考过程于 message.reasoning_content |
| 视觉可用 | 是/否 | mmproj 加载成功,未实测图像输入 |
| WorkBuddy 接入 | 成功 | ✅ 已配置并实测对话通过;后因 maxInputTokens 过大遇 500,降 65536 解决(见 5.4) |
| Pi 接入 | 成功 | 未配置(配置见阶段 6) |
七、结论与后续
- 结论 :Q8_0 在 Thor 上可行 ,实测单流 17.2 tok/s(MTP 投机解码生效,显著高于带宽墙预估的 9--12),思考模式正常输出,多模态/mmproj 加载成功,OpenAI 兼容端点可用。
- 若嫌慢 :换
Q4_K_M(~17GB)命令仅改文件名,预期速度升至 ~20+ tok/s。 - 若需更高并发/吞吐:可转用 vLLM 源码编译 + NVFP4(~14GB,Blackwell 4-bit 张量核,参照 GB10 实测 ~25 tok/s),代价是 ARM 编译。
- 内存富余利用:Thor 128GB 下可放心开 128K--262K 上下文(KV 仅 8--16GB),无需降上下文保显存。
- 后续可测 :① 图像输入实测(4.3,mmproj 已加载);② 长上下文 prefill(60K prompt);③ WorkBuddy 接入后的多轮 agent 场景吞吐(基础对话已验证,多轮思考保留待测);④ 各
reasoning_effort档位的速度/质量差异实测。
附:本实验依据社区实测(unsloth GGUF、smeltcore RTX3090/5060Ti 配方、locallyuncensored 量化表、GB10 同代 Blackwell 实测)整理;Thor 具体数值以本次实测为准。