# model-zy
**Repository Path**: ccbtr/model-zy
## Basic Information
- **Project Name**: model-zy
- **Description**: No description available
- **Primary Language**: Python
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-19
- **Last Updated**: 2026-09-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# model-zy —— 本地大模型推理服务 + OpenAI 兼容网关
一套可直接运行的部署:**Trae → 网关(8001) → llama_cpp.server(8000) → GGUF 模型**
目录自包含:所有脚本用「脚本位置」当工作目录,整个目录可以整体搬走。
## 目录内容
| 文件 | 作用 |
|---|---|
| `model.conf` | **模型配置(唯一事实来源)** —— 换模型只改这里;`run_model.sh`/`run_gateway.sh`/`wait_model.sh` 都从它读 |
| `start_server.py` | 启动推理服务;参数全部从 `model.conf` 读;带 `--check` 预校验(不加载权重) |
| `api_key.txt` | 推理服务鉴权密钥(600;模板见 `api_key.txt.example`)|
| `proxy_server.py` | 网关:OpenAI 兼容层 + 多格式工具调用解析 + 空回复/旁白补救 + 重复调用熔断 |
| `proxy_server.orig.py` | 修复前的旧网关(仅对照,别跑)|
| `run_model.sh` | 起推理服务(幂等;**不装依赖**)|
| `run_gateway.sh` | 起网关;模型名/端口/密钥都从 `model.conf` 与 `api_key.txt` 读 |
| `wait_model.sh` | 等推理服务就绪(探 `/v1/models`)|
| `remote_verify.py` | 基础验收:10 项断言,含三轮 agent 循环 |
| `probe_multistep.py` | **核心指标**:多步决策能力(拿结果 → 决定下一步),验收线 ≥4/5 |
| `probe_native_template.py` | 零停机对比「模型自带模板 vs 通用模板」,手工渲染 prompt 打 `/v1/completions` |
| `probe_narrate_fix.py` | 把 trace 里「只说话不动手」的那一轮请求重放,A/B 验证补救 |
| `replay_trae_request.py` | 重放「最近一次真实客户端请求」,排查「模型为什么不调工具」 |
| `bench_speed.py` | 生成速度基准(固定 prompt + max_tokens,报 tok/s)|
| `gguf_meta.py` | 只读解析 GGUF 元数据(KV 大小、`tokenizer.chat_template`…),不加载权重 |
| `网关修复说明.md` | 完整诊断与修复记录(9 个根因、29 处改动、每步证据)|
| `logs/` | `llama-cpp.log`(推理服务)、`gateway.log` / `gateway.out`(网关)、`gateway-trace.jsonl`(`--dump-requests` 落的原始请求+返回)、`archive/`(历史日志)|
## 首次准备(装机)
本机是 GPU 开发镜像,**关键依赖已经装好了**,通常不需要重装:
```bash
/root/miniconda3/bin/python -V # 3.14.x
/root/miniconda3/bin/python -c "import llama_cpp, fastapi, uvicorn; print(llama_cpp.__version__)" # 0.3.35
```
若要重建环境,注意三件事(**别直接 `pip install llama-cpp-python --force-reinstall`**):
1. **PyPI 上默认的 wheel 是 CPU-only**。要 CUDA 版得用源码编译(`CMAKE_ARGS="-DGGML_CUDA=on" FORCE_CMAKE=1 pip install llama-cpp-python --no-cache-dir`),
而源码编译需要 `nvcc` —— **本机当前没有 nvcc**,所以别轻易重装,容易把能用的环境搞坏。
2. 网络:PyPI 走国内源(已配阿里云/清华,可用);**GitHub 不通**,所以别指望从 GitHub 拉 wheel 或源码。
3. 网关只需要 `fastapi` / `uvicorn` / `httpx` / `pydantic` / `sse_starlette`,普通 `pip install` 即可。
密钥:
```bash
cd /root/workspace/model-zy
head -1 api_key.txt # 已有;重建见 api_key.txt.example
chmod 600 api_key.txt # 必须是 600:网关和启动脚本都从这里读
```
## 日常启停
```bash
cd /root/workspace/model-zy
/root/miniconda3/bin/python start_server.py --check # 先校验配置(秒级,不加载模型)
bash run_model.sh # 起推理服务(加载约 30-60 秒)
bash wait_model.sh # 等就绪
bash run_gateway.sh # 起网关(会自动停掉旧的)
```
两个服务都用 `setsid` 启动(`ppid=1`),**关掉 SSH / 终端不会死**。
## 配置怎么改(`model.conf`)
| 项 | 说明 |
|---|---|
| `model_path` / `model_alias` | 模型文件路径 / 客户端里填的模型名 |
| `chat_format` | **最要命的一项**,见下 |
| `n_ctx` | 上下文窗口,受显存限制(见下)|
| `offload_kqv` | KV 放显存(true,快)还是主机内存(false,慢但省显存)|
| `type_k` / `type_v` | KV 量化(1=8bit)。**注意:这个新架构上没生效**,实测仍是 f16 |
| `verbose` | 打开 llama.cpp 自己的日志(排障必开:KV 大小、显存分配失败的真实原因都在这)|
### `chat_format` —— 选错会废掉多步工具能力
先查模型有没有自带工具模板:
```bash
/root/miniconda3/bin/python gguf_meta.py <模型.gguf> tokenizer.chat_template
```
| 情况 | 怎么写 |
|---|---|
| 模板里出现 `tool_call` / `function` / `tools` | `chat_template.default` ← **模型原生模板,优先** |
| 没有自带模板 / 自带模板不带工具 | `chatml-function-calling`(或 `functionary-v2`)|
**为什么这项要命**(实测,同一份权重、同一个 Q4_K_M 量化、同一台 4090,只换模板):
| 喂的模板 | 「读完目录列表 → 去读 app.conf」|
|---|---|
| 模型原生模板 | **4/4 ~ 5/5** 做对 |
| `chatml-function-calling` | **0/5**(5 次都又跑一遍 `ls -la`,看着像模型能力不行)|
机制:多轮工具能力很大一部分建立在「**历史按训练格式呈现**」上。模板不匹配时,模型认不出
「我已经调过 X 了」,就退化成复读最可能的续写(重复同一条命令 / 只回一句旁白)。
### `n_ctx` 与显存(实测数据)
> 下表是**单卡 24G + Q4(15.4G)** 的实测。**三卡 + F16(54G)** 的情形见「多卡」一节 ——
> KV 占用是同一个 64 KB/token,但**生成速度差 2.5 倍**,别把两套数字混着算。
这个模型 65 层里只有 **16 层真注意力**,但 KV 是 **f16 ≈ 64 KB/token**(`type_k/type_v` 设 8bit 在这代架构上没生效):
| n_ctx | KV 占用 | 能否放显存 |
|---|---|---|
| 64K | 4.1 GB | ✓ |
| **96K** | **6.1 GB** | ✓ **当前设置** |
| 128K | 8.2 GB | ✗ OOM(报 `failed to allocate compute pp buffers`)|
| 256K | 16.4 GB | ✗ 只能放主机内存 |
显存 24G,模型权重占 15.4G,能腾给 KV 的约 7.5G。两种取向:
- **96K + KV 放显存** → 生成 **39.6 tok/s**(当前)
- **256K + KV 放主机内存** → 生成 **4.9 tok/s**(GPU 利用率仅 ~20%,PCIe 搬 KV 成瓶颈)
改完 `n_ctx` / `offload_kqv` / `chat_format` 都要**重启推理服务**。
### 多卡(3090 / 4090 这类 Ampere/Ada)
**已在 3×RTX3090 + F16(54G,两分片) 上真跑过**:`tensor_split = 1.2,1.2,0.8` → 24/24/16 层
(每卡占 18.15 GB,这是权重按层分摊,**不是重复加载**)。上机照下面的顺序走。
```ini
# model.conf
tensor_split = 1,1 # 几张卡写几个权重;同型号两张卡就是 1,1;24G+12G 用 2,1
split_mode = layer # 默认,按层切
main_gpu = 0 # 放输出层等小张量的那张卡
n_gpu_layers = -1 # 所有层都上 GPU(已是当前值)
flash_attn = true # 3090/4090 都支持,保持 true
```
```bash
/root/miniconda3/bin/python start_server.py --check # 先看它打印的多卡计划
```
`--check` 会打印检测到的每张卡、分卡方式和各卡分到的比例,并且**权重个数和显卡数不一致时直接拦下**
(这是最容易犯的错)。它还会用的就是 `llama-cpp-python` 的真实 `ModelSettings` 做字段校验,
所以字段名写错也会在这一步暴露。
**三种切分怎么选**
| 模式 | 适用 | 代价 |
|---|---|---|
| `layer`(默认)| 单流生成最快;KV 随层分散到各卡 → **多卡能同时放大能开的上下文** | 每个 token 仍要依次走完所有层,单流不会更快 |
| `row`(张量并行)| 单卡真装不下、或要长上下文时省显存;提示处理(PP)更快 | 每步都要多卡同步,生成(TG)通常更慢 |
| `tensor` | 新的张量并行实现,需要较新的 llama.cpp/CUDA | 同上 |
**预期别搞错**:多卡的主要收益是**总显存**(更大的模型 / 更长的上下文),不是单流速度。
本模型 KV 是 f16 ≈ 64 KB/token,`layer` 模式下按层分摊 —— 所以几张卡合起来的
「能开的上下文」≈ 各卡可腾出显存之和 ÷ 64 KB。
**生成速度的物理上限(先算,再决定换不换硬件)**
生成(TG)是**显存带宽瓶颈**:每吐一个 token,都要把全部权重读一遍。所以
```
TG 上限 ≈ 单卡显存带宽 ÷ 权重体积 (多卡 layer 模式下 ≈ 单卡带宽,与卡数无关)
```
实测对得上(同一台 3×RTX3090,936 GB/s):
| 权重 | 体积 | 实测生成 | 说明 |
|---|---|---|---|
| **F16** | 54 GB | **15.9 tok/s** | 理论 17.3 → 已跑满 **92% 带宽**,没有优化空间 |
| Q8 | ~27 GB | ~30 tok/s(按定律推算)| 两张卡就装得下 |
| Q4 | 16.5 GB | **39.6–42.3 tok/s** | 单卡,实测 |
**所以要"更快"只有两条路**:降精度(F16→Q8≈2 倍,→Q4≈4 倍),或换掉带宽更高的卡。
**加卡不会让单流生成变快**(layer 切分是流水线:一个 token 仍要依次穿过所有卡)。
**上机后怎么确认真的用上了多卡**(`verbose = true` 时看 `logs/llama-cpp.log`):
```bash
grep -aE "offloading .* repeating layers|CUDA[0-9]+ model buffer size|CUDA[0-9]+ KV buffer size|flash_attn" logs/llama-cpp.log | tail
```
应该能看到**每张卡各一行** `CUDA0/CUDA1 ... buffer size`,以及 `flash_attn = 1`。
如果只看到 `CUDA0`,说明分卡没生效(多半是 `tensor_split` 个数不对或只有一张卡被识别)。
**其余注意**
- `n_gpu_layers = -1` 是「所有层上 GPU」;显存仍然不够时它会把一部分层留在 CPU(生成会明显变慢),
日志里 `offloading N/M layers` 的 N < M 就是这个情况。
- flash attention 在 Ampere/Ada 上没问题;换到更老的卡(如 V100 之前)可能启动报错 —— 那就把
`flash_attn` 改成 `false`。
- 这一版 `llama-cpp-python 0.3.35` 的服务端**没有 `n_parallel`(并发槽位)参数**,所以多卡
不会带来「多客户端并发」的收益(它一次只处理一个请求);要并发得换官方 `llama-server` 或 vLLM。
- `offload_kqv` 在多卡时更要保持 `true`(KV 跟着层走,各自留在自己的卡上);放主机内存会让
每张卡都要经 PCIe 取 KV,多卡反而更慢。
- 只想用其中几张卡时,不用改配置 —— 用 `CUDA_VISIBLE_DEVICES` 限定即可(编号会重排成 0..N-1):
`CUDA_VISIBLE_DEVICES=0,1 bash run_model.sh`(此时 `tensor_split` 也要按这两张卡写)。
- 单卡机器上把这一整段保持默认即可,行为与现在完全一致(`tensor_split` 留空、`split_mode = layer`)。
## agent 一轮要等多久:延迟拆解(实测)
客户端每轮都会把**完整上下文重发**(Trae 实测:13.7K 字符 system + 24 个工具 schema ≈ 1.5 万 token),
所以一轮 ≈ 下面三段之和:
| 段 | 耗时(3×3090 + F16)| 说明 |
|---|---|---|
| **prompt 处理(PP)** | **12.5 秒 / 1.5 万 token** | 每一轮都要重算。这个混合 SSM 架构**用不了前缀缓存**(日志原话 `prefix-match found but partial kv removal not supported, re-evaluating full prompt`)|
| **思考(thinking)** | **~31 秒**(在简单对话里是最大头)| 不关思考时,连「你好」都会先想几百 token |
| 生成正文 | ~1 秒(几十个 token)| F16 就只有 16 tok/s,长回答会线性变慢 |
实测同一句「你好」:**思考开 44.5 秒 → 思考关 15.5 秒**。关掉的开关见 `model.conf` 里
注释掉的 `chat_template_kwargs`(**默认保留思考**,需要提速时取消注释;关掉后多步工具调用验收仍是 4/4,
但复杂推理会变弱)。
想再把 15.5 秒压低,按性价比排序:① Trae 里关掉用不到的工具(24→8 个大约省 5–8 秒)
② `split_mode = row` 换 PP 速度(张量并行,PP 快数倍,TG 略降)③ 换 vLLM(有前缀缓存)
④ 降精度(见上一节)。
## 队列放大:客户端超时重试会把模型堵死(`n_seq_max = 1`)
`llama-cpp-python 0.3.35` 的服务端是 **一次只处理一个请求**(`n_seq_max = 1`,没有批处理、没有抢占)。
所以客户端**超时后重发**不是在"多问一次",而是**在队尾再排一次**:
```
客户端超时 → 重发 → 排在正在生成的那个后面 → 自己的等待更久 → 再次超时 → 再重发 …(越堆越长)
```
实测证据:同一个请求,模型空闲时 **0.9 秒**;另一端(Trae)正在生成时 **54.5 秒**。而且
**每个废掉的尝试都要重算 1.5 万 token 的 prompt**(12.5 秒 GPU 时间),队列里堆几个就明显变卡。
**两道闸**(默认开):
| 闸 | 参数 | 作用 |
|---|---|---|
| 单次生成上限 | `--max-tokens-cap 2048` | Trae 会给 `max_tokens=16384`,在 16 tok/s 下跑满就是 **17 分钟**,期间所有请求排队 → 压到 2048(最坏 ~2 分钟)|
| 并发同请求合并 | `--no-dedupe-inflight` 可关 | 同一请求(消息条数+角色序列+最后一条用户消息+工具名集合 相同)**并发**进来时,合并成一次上游调用,两边拿到同一结果;请求结束后即移除,所以用户主动"重新生成"不受影响 |
另外建议:**客户端的超时别小于模型最坏耗时**,否则就是自己给自己制造重试风暴。
## 网关侧开关(`run_gateway.sh` 里的 `GATEWAY_ARGS`)
| 参数 | 用途 | 换模型后 |
|---|---|---|
| `--nonstream-when-tools` | 规避 llama-cpp-python「stream+tools 直接抛异常」;**旁白补救也依赖它** | 换官方 llama-server / vLLM 后可关 |
| `--think-preopened` | 上游生成提示已开 ``(原生模板就是这样):流式解析先抠掉 `` 之前的推理 | 用原生模板就保留 |
| `--tool-alias Bash=RunCommand,...` | 模型爱用 Claude 风格工具名,Trae 里叫 `RunCommand` | 视客户端而定(Trae 需要)|
| `--repeat-limit 3` | 重复调用熔断:同参数调 3 次就拦下,并把「换一条路」的提示塞给它 | 保留(弱模型刚需)|
| `--history-calls-text auto` | 上游用原生模板时,把历史里的结构化 `tool_calls` 改写成原生文本(否则上游必 500)。`auto`=撞到就自动开并粘住 | 保留 |
| `--max-tokens-cap 2048` | 单次生成上限,防止一次长生成把队列堵死(见上一节)| 按模型速度调 |
| `--dump-requests ` | 落盘客户端完整请求 + 上游原始返回,排查「为什么不调工具」的利器 | 保留(含完整对话,注意轮转)|
| `--append-system`(默认**关闭**)| 全局注入系统提示 | **默认别开** —— 会一起影响其它任务的命令生成;需要时 `GATEWAY_APPEND_SYSTEM='...' bash run_gateway.sh`,或按单次请求加 `x-gateway-append-system` 头 |
`--no-salvage-narrate` / `--empty-retry N` / `--tool-args-object` 等微调项见 `proxy_server.py --help`。
## 验收与排查
```bash
cd /root/workspace/model-zy
# ① 基础验收(10 项,含三轮 agent 循环)—— 应 10/10
/root/miniconda3/bin/python remote_verify.py --base http://127.0.0.1:8001
# ② 核心指标:多步决策能力 —— 合格线 ≥4/5
/root/miniconda3/bin/python probe_multistep.py logs/gateway-trace.jsonl http://127.0.0.1:8001 5
# ③ 生成速度(流式口径;密钥自动取同目录 api_key.txt)
/root/miniconda3/bin/python bench_speed.py http://127.0.0.1:8001 3
# ④ 客户端形态的首字延迟(照 Trae 的真实请求体量发;量「半天不回」就用它)
/root/miniconda3/bin/python probe_agent_latency.py http://127.0.0.1:8001 \
--text 你好 --trace logs/gateway-trace.jsonl --runs 3
**③ 的读数怎么理解**:模型一次只处理一个请求,所以「总耗时」会包含**排队等别人**的时间。
本脚本把两个数分开报:
- **TTFT**(首 token 延迟):含排队 —— 另一端(比如 Trae 正在跑任务)占用模型时,可能几十秒
- **生成速率**:只看首 token→末 token 的窗口,**不受排队影响**(本机实测 ~42 tok/s)
所以「忽快忽慢」基本都是排队,不是模型变慢。别用非流式的总耗时当速度。
# 排查
curl -s http://127.0.0.1:8001/health # 第一件事:看 upstream_ok
grep -aE "旁白|重复调用|参数名纠正|重采样|原生模板" logs/gateway.log | tail
/root/miniconda3/bin/python replay_trae_request.py logs/gateway-trace.jsonl http://127.0.0.1:8001
```
## 已知坑(都是库层面的,网关兜住了)
| 症状 | 真实原因 | 谁兜的 |
|---|---|---|
| 多轮工具历史必 500:`Tool call arguments ... were passed as a JSON string` | 模型原生模板要 `arguments` 是**对象**,而 llama-cpp-python 的请求模型只收 **JSON 字符串**(传对象则 pydantic 报一堆校验错)| 网关 `--history-calls-text`:把历史工具调用改写成原生文本再发上游 |
| 正文里冒出推理过程 | 原生模板把 `` 写进生成提示 → 模型只返回 ``,没有开始标签 | `--think-preopened` + 孤立闭标签处理(流式/非流式都做了)|
| 客户端显示一行命令但什么都没执行 / 建了个空名字计划项 | 模型把工具调用写成**纯文本**(`functions.RunCommand: {...}`、混合 XML)| 网关多格式解析 + 参数名纠正 |
| 一直重复同一条命令;点「继续」还是同一句话 | 小模型的复读倾向(上下文里 ≥2 个同样样例就锁死);客户端把「没有工具调用」当任务结束 | 熔断(含「返回相同 + 参数微调」判据)+ 旁白补救 |
## 换模型 checklist
1. 把新模型放到 `/root/damodel-tmp/`(该盘 50G,注意余量),改 `model.conf` 的 `model_path`;
2. `gguf_meta.py <模型.gguf> tokenizer.chat_template` 看有没有自带工具模板 → 决定 `chat_format`;
3. 按新模型规模重算 `n_ctx`(KV 按 ~64 KB/token 估;不同架构/层数会变,以 llama.cpp 启动时自报的
`llama_kv_cache: size = ...` 为准);**多卡机器同时改 `tensor_split`(几张卡几个权重)**;
4. `start_server.py --check` → `pkill -f "start_server[.]py"` → `run_model.sh && wait_model.sh` → `run_gateway.sh`;
5. 跑验收:`remote_verify.py`(10/10)+ `probe_multistep.py`(≥4/5)。**这两条都过才算换成功。**