# 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)。**这两条都过才算换成功。**