首页
/ MemPalace 小模型评估框架(model_eval)深度解析:用可复现的基准矩阵为 ≤4B 本地模型选型

MemPalace 小模型评估框架(model_eval)深度解析:用可复现的基准矩阵为 ≤4B 本地模型选型

2026-09-05 10:48:25作者:江焘钦

本文基于 MemPalace 仓库中的 benchmarks/model_eval/README.md 展开,完整讲解这套“小模型评估框架”如何围绕 (模型, 任务, 模式) 三元组,用冻结的合成数据集、生产代码路径与 Ollama 运行时指标,在本地 RTX 3090 级硬件上跑出一套包含准确率、TTFT/TPS、端到端延迟与显存占用(p50/p95)的 CSV 基准矩阵;读完后你可以独立复现发布的基准结果、对比基线漂移,并按贡献规范接入新模型或新推理后端。

一、框架定位:用数据替代“凭感觉”选模型

MemPalace 的本地模型任务(房间分类、实体抽取、记忆抽取)运行在 ≤4B 参数的 Ollama 模型上。这套评估框架的目标是“把基于直觉的模型选择替换为数据”(Replace vibe-based model selection with data):对每个 (model, task, mode) 组合输出准确率、延迟(TTFT、TPS、端到端 p50/p95)与显存占用。整个本地矩阵在 RTX 3090 上约 60 分钟跑完。

框架由 5 个核心 Python 模块组成,位于 benchmarks/model_eval/ 目录:

模块 职责
runner.py 执行单个 (model, task, mode) 组合,输出一行结果
orchestrator.py 按“候选模型 × 任务/模式 × 语言”展开矩阵,增量写 CSV
metrics.py 计时提取、百分位聚合、VRAM 轮询、嵌入相似度、主机信息收集
summarize.py 把结果 CSV 渲染成 Markdown 报告
candidates.yaml 候选模型清单,带分层(tier)元数据

任务实现按目录拆分在 benchmarks/model_eval/tasks/ 下:calibrationroom_classificationentity_extractionmemory_extraction,每个目录包含 prompts.py(提示词构建)与 score.py(打分逻辑)。

二、每个组合测什么:六项指标的定义与来源

README 明确了每个 (model, task, mode) 组合的输出指标:

  • Accuracy —— 任务特定的打分,对照带标签数据集;
  • TTFT(time-to-first-token) —— 来自 Ollama 响应的 prompt_eval_duration + load_duration 的近似值,取 N=20 次采样运行的 p50 与 p95;
  • TPS(tokens/second) —— 来自 eval_count / eval_duration,取 p50 与 p95;
  • e2e 延迟 —— 完整单次分类耗时,取 p50 与 p95;
  • VRAM resident —— 预热后的模型常驻显存,从 Ollama /api/ps 读取;
  • VRAM peak —— 推理期间的峰值 GPU 显存,用 nvidia-smi 每 500ms 轮询得到。

每个模型的第一次运行会被丢弃(消除缓存与 GPU 频率爬升的影响)。这些定义在源码中可以一一印证:

  • metrics.pyextract_timing() 从 Ollama /api/chat 响应中取 load_durationprompt_eval_durationeval_durationeval_count 等字段(单位纳秒),ttft_ms = (load_ns + prompt_eval_ns) / 1e6;e2e 以 Python 侧 time.perf_counter() 的墙钟为准,因为 total_duration 不含部分 HTTP 开销。
  • 百分位聚合用“nearest-rank”方法(metrics.py_p()),与 statsd、Datadog 等生产工具的报告口径一致。
  • VRAMPoller 在独立守护线程里每 500ms 调用一次 nvidia-smi --query-gpu=memory.used 记录峰值;只跟踪 GPU 0(单卡是这套基准的默认部署假设),nvidia-smi 不可用时静默降级为 None
  • vram_resident_mb()metrics.py)直接查 HTTP 的 /api/ps 而非 ollama ps CLI,因为旧版本 Ollama 的 CLI 缺少 --format 标志(针对 0.23.2 验证过)。

CSV 的完整列定义见 orchestrator.pyCSV_COLUMNSmodel_tag, task, mode, language, n_samples, accuracy, ttft_p50_ms, ttft_p95_ms, tps_p50, tps_p95, e2e_p50_ms, e2e_p95_ms, vram_resident_mb, vram_peak_mb, host, gpu, ollama_version, run_date, error, extras_json。已发布的基线 results/2026-05-10-z690-ex-glacial.csv 首行数据例如:qwen3:4b-instruct-2507-q4_K_Mroom_classification:closed 上 accuracy 0.61、e2e_p50 109.1ms、vram_resident 7481MB、vram_peak 22961MB(RTX 3090)。

硬件报告:每个结果文件都包含测试机元数据(CPU、GPU、VRAM 总量、Ollama 版本、OS、主机名),由 gather_host_info() 尽力收集(失败时静默降级而不中断基准)。关键结论是:速度数字不可跨机器移植,只能在同一套环境里做相对排名;准确率数字则可跨机器比较。

三、候选模型池:candidates.yaml 的分层体系

candidates.yaml 是候选模型的单一事实来源,每条记录包含 tagfamilysize_bvariantquantizationexpected_vram_mbtiernotes 字段。分层控制 orchestrator 的默认选择:

  • tier 1(必测集,约 10 个模型,~24 GB 磁盘):跨家族均衡的 q4_K_M 扫描,包括 qwen3:4b-instruct-2507-q4_K_M(推荐基线)、qwen3:4b-instruct-2507-q8_0(精度对照)、qwen2.5:3b-instruct-q4_K_Mgemma3:4b-it-q4_K_Mgemma3:4b-it-qat(量化感知训练对照)、gemma3:1b-it-q4_K_Mllama3.2:3b-instruct-q4_K_Mllama3.2:1b-instruct-q4_K_Mphi3.5:3.8b-mini-instruct-q4_K_M
  • tier modern(首轮搜索遗漏的家族)qwen3.5:4b-q4_K_Mgemma4:e2b-it-q4_K_Mgemma4:e4b-it-q4_K_Mgranite4.1:3b-q4_K_Mministral-3:3b。独立成层是为了 --candidates modern 能低成本重跑。
  • tier 2(显存受限用户的更小尺寸)qwen3:1.7bqwen3:0.6bqwen2.5:1.5bqwen2.5:0.5bgemma3:270m-it-q8_0
  • tier 3(上限/特例)qwen3:4b-instruct-2507-fp16 —— 领先者的全精度质量上限,约需 8.1 GB 显存(README 中 FP16 变体建议 ~14 GB 显存)。
  • tier cloud(Ollama 云托管参考模型)gpt-oss:20b-cloudgpt-oss:120b-cloudqwen3-coder:480b-clouddeepseek-v3.1:671b-clouddeepseek-v4-flash:clouddeepseek-v4-pro:cloudkimi-k2.6:cloud。云模型用于测量“本地最优 vs 大 100 倍模型”的准确率差距,不代表生产用途(隐私与成本取舍使本地模型成为默认)。
  • tier community(第三方 GGUF 微调):如 igorls/gemma4-e4b-classifier:latest 等分类器专用微调。
  • tier local(本地已拉取模型)gemma4:e4b

一个明确的政策值得注意:纯推理(reasoning)变体被按策略排除——MemPalace 的分类任务永远在禁用思考的模式下运行(详见第六节的 think=False 强制机制);要基准化混合模型时,用其 base tag,runner 会在每次调用上强制 think=False

四、任务集:四个任务、五种“任务/模式”组合

orchestrator 的默认任务矩阵 TASK_MODES 固定为 5 个组合:

任务 模式 说明 样本量
room_classification closed 闭集:模型从给定房间列表中选一个,或答 other 101
room_classification open 开集:模型自造 slug,用嵌入相似度打分 101
entity_extraction default 每条样本输出 JSON 实体列表 50(247 个真值实体)
memory_extraction default 每条样本输出结构化记忆项 40(55 条真值记忆,5 种类型)
calibration default 5 类句子类型分类,20 条样本,作为框架自身的健康检查 20

所有数据集是合成数据(不含真实个人信息),一次性生成后冻结,保证基准数字跨运行可比。扩展数据集时只能新增样本,不能替换已有样本,否则历史数字失去可比性。

数据集以 JSONL 存放于 benchmarks/model_eval/datasets/,例如:

  • 校准任务样本:{"id": "cal_001", "text": "Could you fix the indentation on this Python file?", "classes": ["question", "command", "statement", "exclamation", "greeting"]}
  • 房间分类样本:{"id": "rc_001", "agent": "Aria", "session_summary": "Spent the morning re-reading Hofstadter's strange-loop framing...", "include_messy_features": false},每条样本另配 room_lists.jsonl 中的候选房间列表。

从源码看,各任务的提示词与打分实现:

  • calibrationprompts.py 的 system 提示要求“只回答类表中的一个词,无解释、无标点、无引号”;score.py 做归一化后的精确匹配。它的定位是“任何合格的 ≤4B instruct 模型都应拿满分;如果准确率差,说明真正的任务跑起来之前框架本身已经坏了”。
  • room_classificationclosed 模式提示词 要求“原样复制列表中的一个房间 slug(保留 /-),或字面量 other”;score.py 做小写归一化精确匹配,并记录预测值是否落在房间列表内(in_room_list)。open 模式提示词要求“小写、连字符、无空格”的自造 slug,打分则走嵌入余弦相似度。
  • entity_extraction / memory_extraction:以 json_mode=True 调用,聚合指标分别包含 mean_f1 / mean_precision / mean_recall / valid_json_ratemean_coverage / mean_hallucination_rate / mean_type_accuracy / valid_json_rate(见 runner.py_aggregate_accuracy())。

跨语言基准是 README 未展开、但源码已内置的能力:数据集目录下为每个任务提供 dataset.{lang}.jsonl(de、es、fr、hi、it、ko、pt-BR、ru、zh 等 9 种语言)。runner.run() 的说明指出,非英语输入默认对照同一份英文真值打分(跨语言映射测试),仅当存在 labels.{lang}.jsonl 时(目前 memory_extraction 有韩语标签)才用语言特定标签;语言代码经过正则 ^[A-Za-z][A-Za-z0-9]*(?:[_-][A-Za-z0-9]+)?$ 校验,且解析后的数据集路径必须仍位于任务目录内,防止路径穿越。multilingual 的运行结果见 reports/2026-05-13-multilingual.md

五、端到端复现:从环境准备到冒烟测试

5.1 前置条件

  • GPU:NVIDIA 卡;Tier 1 集合最低约 10 GB VRAM,FP16 变体约 14 GB。发布数字来自 RTX 3090(24 GB)。
  • Ollama:按官方渠道安装(参见 ollama.com 下载页)。框架针对 Ollama 0.23.2 测试;更新版本应可用,更旧版本可能破坏 think 参数或 /api/ps 端点的响应形状(框架对后者有回退处理)。
  • Python:3.10+(项目用 uv 管理环境)。
  • 磁盘:完整本地候选集约 30 GB,仅 Tier 1 约 24 GB。
  • Ollama Cloud 账号(可选):仅重跑云层级上限测量时需要,运行前先 ollama signin

5.2 准备环境

在 mempalace 仓库根目录执行:

uv sync

5.3 拉取候选模型

完整候选清单在 candidates.yaml;orchestrator 的 tier 过滤只会实际运行“已本地安装”的模型。批量拉取 Tier 1 集合(必测集,约 24 GB)

# 读取 candidates.yaml,拉取所有 tier-1 模型 + 嵌入模型
uv run python -c "
import yaml
import subprocess
with open('benchmarks/model_eval/candidates.yaml') as f:
    cands = yaml.safe_load(f)['candidates']
for c in cands:
    if c.get('tier') == 1:
        print(f'pulling {c[\"tag\"]}')
        subprocess.run(['ollama', 'pull', c['tag']], check=True)
subprocess.run(['ollama', 'pull', 'nomic-embed-text'], check=True)
"

如需拉取 Tier 2(sub-3B 尺寸)、modern 层(Gemma 4、Granite 4.1、Ministral 3、Qwen 3.5)或 Tier 3(FP16 上限),替换过滤条件为 c.get('tier') in (1, 2)c.get('tier') == 'modern' 等即可。

云对比(可选,需 ollama signin):

for tag in gpt-oss:20b-cloud gpt-oss:120b-cloud qwen3-coder:480b-cloud \
           deepseek-v3.1:671b-cloud deepseek-v4-flash:cloud deepseek-v4-pro:cloud \
           kimi-k2.6:cloud; do
  ollama pull "$tag"
done

5.4 冒烟测试(30 秒以内)

在投入完整矩阵前,先用一个模型一个任务确认框架可用:

uv run python -m benchmarks.model_eval.runner \
  --model qwen3:4b-instruct-2507-q4_K_M \
  --task calibration \
  --mode default \
  --dataset-dir benchmarks/model_eval/datasets

预期看到约 20 次推理、输出 JSON 中 accuracy: 0.95(或接近)。如果准确率明显偏低,检查 ollama list 是否显示模型已加载,以及嵌入模型是否已拉取(即使 calibration 本身不用嵌入,open-set 与 memory 任务也需要它)。从源码看,runner 对嵌入任务是自动兜底的:_ensure_embed_model() 会先用一次 embed_text("ping") 探测,失败则自动 ollama pull(并设置 OLLAMA_HOST 指向被基准的同一端点),仍失败才抛错并写入结果的 error 字段。注意 runner.py--embed-model 的默认值是 embeddinggemma,而 room_classification/score.pymetrics.py 中相似度函数的签名默认是 nomic-embed-text——README 的批量拉取脚本拉的是 nomic-embed-text,复现时建议两者都就位。

六、跑矩阵:orchestrator 的关键参数

三档典型运行(README 原样保留):

仅 Tier 1(RTX 3090 上约 30–40 分钟)

uv run python -m benchmarks.model_eval.orchestrator \
  --candidates tier1 \
  --tasks all \
  --dataset-dir benchmarks/model_eval/datasets \
  --output benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv

全部本地(约 60–80 分钟):把 --candidates 换成 local仅云(约 25–50 分钟,n=30 控制成本)--candidates cloud --n 30,输出文件名带 cloud 标识。

orchestrator 的完整 CLI(见 orchestrator.py)值得逐项理解:

  • --candidatestier1 / tier2 / tier3 / all / tier<=N / local(一切非 cloud)/ cloud / modern / community,或任意精确模型 tag(支持逗号分隔列表;不在 yaml 中的 tag 会现场合成最小条目,无需改文件即可评估临时模型)——过滤逻辑见 load_candidates()
  • --tasksall 或逗号分隔列表,支持 task:mode 精确指定,例如 room_classification:closed,calibrationparse_tasks());
  • --output / --output-dir(二选一):前者把全部语言写进同一 CSV(共享文件句柄,避免缓冲交错);后者按语言写 <dir>/<lang>/YYYY-MM-DD-<host>.csv,便于多语言长运行的按语言 diff;
  • --languages:逗号分隔数据集语言,如 en,pt-BR,es,zhendataset.jsonl,其他值用 dataset.{lang}.jsonl
  • --n:每个任务只取前 N 条样本(调试/控成本用);
  • --num-ctx(默认 4096):每次请求的 Ollama 上下文窗口。README 与源码强调这是一个可比性开关——不设的话,默认 32k 的模型会预分配 4k 默认模型不存在的 KV cache,导致准确率、延迟、VRAM 全部失去可比性(runner.py);
  • --endpoint / --llm-providerollama | openai-compat | anthropropic)/ --embed-endpoint / --embed-model:端点解析规则是——嵌入模型常驻 Ollama,但当 LLM provider 也是 Ollama 时,--embed-endpoint 默认跟随 --endpoint,保证远程基准的打分与推理同主机;
  • --continue-on-error(默认开启):单个组合失败后继续跑剩余组合;--no-continue-on-error 则首个失败即中止。

运行中的行为与 README 描述一致:orchestrator 打印 [i/N] 进度,每个组合完成后打印 acc=… e2e_p50=…ms vram=…,并增量写 CSV(每行写入后 flush)——只要部分数据时 Ctrl-C 是安全的。

关键设计:复用生产代码路径

框架调用的是 mempalace.llm_client.get_provider("ollama", model=tag)provider.classify(...)——与生产完全相同的代码路径(见 runner.py)。对支持思考的模型,runner 每次都传 think=False,使混合模型保持在“快速分类模式”(_classify_with_timing() 的注释解释:MemPalace 的分类/抽取任务不会从扩展推理中获益,强制 think=False 让基准测到的是真实生产路径)。此外还有 strip_thinking_tokens()metrics.py)清理可能泄漏的 ... 思考块,保证只给最终答案打分。没有新的 HTTP 管线,没有重写 provider 抽象——基准测的就是要发货的那份代码

七、报告渲染与基线对比

7.1 渲染报告

uv run python -m benchmarks.model_eval.summarize \
  --csv benchmarks/model_eval/results/$(date -u +%Y-%m-%d)-$(hostname).csv \
  --output benchmarks/model_eval/reports/$(date -u +%Y-%m-%d)-$(hostname).md

输出是一份 Markdown 报告,包含按任务排名、生产选型建议、开集可行性、instruct 与 reasoning 对比(summarize.py)。

7.2 与已发布基线对比

已提交的基线 CSV 位于 benchmarks/model_eval/results/

漂移判据(README 原话的整理):

  • 准确率:应与基线相差约 1% 以内;更大漂移说明环境差异(Ollama 版本、模型 digest、系统提示词渲染不同)。
  • 速度:绝对毫秒数必然不同(GPU、驱动、散热、并发负载),应使用同一机器内部的相对排名作为可比较信号。
  • VRAM resident:同版本 Ollama 下应高度一致;peak 更噪(取决于测量时刻的其他 GPU 活动)。

reports/2026-05-10-analysis.md 记录了原始运行的完整解读,可作为复现目标的参照:首轮 15 个候选 × 5 个任务/模式 = 75 次运行,61.8 分钟完成;复现性抽查显示单项指标漂移 ≤0.7%;云层在闭集房间分类上领先本地约 0.30(最好 0.900 vs 0.610),但在开集上并未领先本地(0.587 vs 0.612);modern 层中 gemma4:e4b-it-q4_K_M 的开集得分 0.65 超过了全部已测模型(含 1T 级云模型)。

7.3 分享你的结果

若你的结果与已发布基线有实质性分歧:(1) 对分歧模型跑冒烟测试并保存 JSON 输出;(2) 在仓库开 issue,附 ollama --version、GPU 型号、CSV 与冒烟测试 JSON;(3) 若框架有 bug 则修复;若是模型行为确实变化(Ollama Cloud 模型轮换、上游新量化),官方会重跑并在报告中记录漂移。要把 CSV 挂到后续 PR 或对比研究,按 YYYY-MM-DD-yourhostname.csv 命名放入 results/,并在分析报告中引用。

八、贡献指南:加模型与接入新推理后端

8.1 添加遗漏的模型

已发布的候选清单“是一位工程师加一轮搜索”筛出来的,必然有遗漏。若你了解某个有竞争力的 ≤4B instruct 模型:

  1. 按现有 schema(tagfamilysize_bvariantquantizationexpected_vram_mbtiernotes)在 candidates.yaml 加一行;
  2. 先跑冒烟测试,再用现有 --candidates <你的tag> 过滤跑矩阵;
  3. 开 PR,附新增候选行 + CSV 结果 + 一行分析报告附录。

官方特别感兴趣的方向:function-calling 调优模型(Phi-4 mini、Nemotron-mini)、研究机构的近期 instruct 变体(Hermes、Dolphin、OpenHermes)、知名家族的量化感知训练变体。若一个模型有多个有竞争力的量化版本,选“在准确率噪声范围内”的更小那个(对应分析报告中“新版 ≠ 更好”的发现与量化甜点规则)。

8.2 接入其他推理后端

框架经由 mempalace.llm_client.get_provider() 接线,该函数已支持三种 provider:ollama(当前使用)、openai-compatanthropic。这意味着任何 OpenAI 兼容的本地服务器都应能低成本接入:LM Studio(默认 http://localhost:1234/v1)、llama.cpp server(http://localhost:8080/v1)、vLLM(--port 8000)、unsloth studio、Docker Model Runner、Hugging Face TGI/TEI。贡献者需要补齐的四块管线(README 明确列出):

  1. runner.py 与 orchestrator.py 里的后端选择旗标(如 --backend ollama|openai-compat|...),用 get_provider(backend_name, model=tag, endpoint=...) 构造对应 provider——runner/orchestrator 的 --llm-provider 参数其实已经为这一步留了接口(ollama | openai-compat | anthropropic 三选);
  2. metrics.py 里的后端特定计时提取:当前 extract_timing() 读 Ollama 的 eval_countprompt_eval_duration 等;其他后端报告方式不同(OpenAI 的 usage.completion_tokens、llama.cpp 的 tokens_per_second)。非 Ollama 后端目前这些列填零——优雅降级但失去每请求计时分解;
  3. 后端特定 VRAM 探测:Ollama 有 /api/ps,LM Studio 有自己的状态端点,llama.cpp 不直接暴露模型内存。非 Ollama 后端的 vram_resident_mb 会返回 None(源码已处理,见 runner.py);nvidia-smi 的峰值探测无论如何都可用;
  4. candidates.yaml 中按候选标记后端的字段(如 backend: lm-studio)。

实现新后端时,现有数据集与打分代码原样适用,准确率数字跨后端可比。PR 需附 runner/orchestrator 改动 + 一个来自你后端的 CSV,以便在已知模型上验证集成。更利基的方向(Apple MLX、Intel OpenVINO、AMD ROCm 运行时、Termux + llama.cpp 手机边缘运行时)欢迎分享方法论——跨运行时对比正是这套框架设计来支撑的后续研究。

九、框架维护者须知(三条已记录的坑)

README 末尾给维护者的三条注意事项,全部有实测依据:

  1. format: json 模式本地强制执行,但在 Ollama Cloud 上被忽略(Ollama 文档口径)。云模型“碰巧”输出 JSON 是其默认行为,而非 Ollama 强制的结果——kimi-k2.6:cloud 在 memory_extraction 上 valid_json_rate: 0.37 就是被记录的典型表现。
  2. memory_extraction 的 hallucination_rate 指标对“详尽型”模型过度惩罚。分析报告中人工抽查 5 个样本确认 0.36 的幻觉率是打分方法学产物而非模型缺陷;在后续修正打分前,应以 mean_coverage 为准。
  3. 云的可复现性差于本地gpt-oss:20b 两次运行间约有 6 个点的漂移。云数字应以区间而非点估计报告。

十、小结

benchmarks/model_eval 是一套自洽、可复现的小模型选型基础设施:candidates.yaml 定义分层候选池,runner.py 复用生产 provider 代码路径执行单组合并强制 think=False 与统一 num_ctxmetrics.py 从 Ollama 响应与 nvidia-smi 中提取六项指标,orchestrator.py 把矩阵增量落成带硬件元数据的 CSV,summarize.py 再渲染成可对比的报告。复现时记住三个可比性原则:准确率跨机器可比(漂移 >1% 需排查环境)、速度只做单机内相对排名、数据集只增不改;并参考 2026-05-10 分析报告 与三份基线 CSV 校准你的运行结果。

登录后查看全文
热门项目推荐
相关项目推荐