SemIf 实战指南:用开放模型在本地 GPU 上实现"语义化 if"——直接从类型化选项读取概率的运行时决策方案
【免费下载链接】SemIfSemantic ifs from open models, on a 3090 at home. Independent; not affiliated with Jev or TypeSafe.项目地址: https://gitcode.com/gh_mirrors/op/SemIf
导读
SemIf(前身 OpenJev)是一个面向"运行时定义的语义化 if 决策"(runtime-defined semantic decisions)的独立研究项目:给定一段非结构化状态、一个运行时判据和一组类型化选项,它不生成任何回答文本,而是通过单次前向传播直接读取声明选项的原生概率,把大模型当作一个"if 语句评估器"使用,目标是在家用的单张 RTX 3090 上运行冻结的 4B 开放模型。本文将带你完整掌握 SemIf 的输入输出契约、四种执行模式(direct / serial / shared / reranker)、三种后端(Torch / MLX / llama.cpp)的选型与命令行用法,并深入其源码实现、性能实测与校准方法论,使你能够在自己机器上复现并部署这套决策流水线。
为什么需要"语义化 if":从文本生成到概率直读
大多数 Agent 决策其实都很小:路由这条请求、要不要重试、证据是否支持结论 X。对话模型当然能回答这些问题,但代价是它花大量时间生成一段文本,而软件随后又要把这段文本解析回一个if语句——生成、解析、修复 JSON,整条链路既慢又脆。
Jev 是 TypeSafe 的闭源服务,用于运行时定义的语义化决策。SemIf复现的是这一"接口模式"(interface pattern),而非 Jev 未公开的模型或训练过程:用开放模型、在家用硬件上提供同类的"给状态 + 判据 + 选项,返回每个选项的概率"能力。从源码结构看,其核心主张可以概括为四点(见 README.md):
- 运行时定义:判据(criterion)与选项描述随请求一起到达,而非预编译进提示词;
- 决策原生(decision-native):一次前向传播读取声明选项的 logits,不采样任何回答 token;
- 共享状态感知:一段长状态可以只 prefill 一次,然后分叉到多个判据上并行评估;
- 可审计:自有测试集、精确的评测脚本、逐行输出、模型 revision、提示词哈希与已知失败都被提交到仓库。
一句话概括项目定位:"Semantic ifs from open models, on a 3090 at home."(在家用 3090 上、用开放模型做语义化 if)。
核心原理:一次前向传播读出类型化选项概率
输入行契约
SemIf 的输入是逐行的 JSONL,每行必须包含id、state、question、options四个字段,其中state是被评估的非结构化证据,question是判据,options是类型化候选列表:
{ "id": "route-1", "state": "Customer cannot access an account after a password reset.", "question": "Which queue should handle this request?", "options": [ {"id": "access", "description": "Account access support."}, {"id": "billing", "description": "Billing support."} ] }仓库自带三个可直接运行的示例(examples/decisions.jsonl),覆盖了部署成功判定(yes/no/insufficient)、客服工单路由(account_access/billing/sales)与变更审批策略(required/not_required/insufficient)三类典型场景。
输入校验逻辑位于 src/semif_phase1/core.py 的validate_row,约束如下:
id与question必须是非空字符串;state必须是非空的字符串、JSON 对象或数组,且必须是有限的 JSON 兼容数据(json.dumps(..., allow_nan=False)校验);options必须是列表,数量在 2 到 16 之间(与字母表LETTERS = "ABCDEFGHIJKLMNOP"一一对应);- 每个 option 必须有字符串
id与description字段,且id全局唯一。
提示词构造与单 token 槽位验证
direct_messages(core.py)把输入行序列化为固定的 system/user 两轮消息:
- system 指令固定为:"Apply the supplied criterion to the supplied evidence. Choose exactly one listed option. Respond with only its uppercase letter, with no explanation or reasoning."(应用判据、只输出大写字母);
- user 内容是把
evidence、criterion、带字母编号的options组成的 JSON 载荷(ensure_ascii=False,保证中文等文本不被转义破坏)。
关键设计在 src/semif_phase1/direct.py 的encode_prompt:每个选项字母必须编码为恰好一个往返 token(encode 后 decode 回原字母,且与相邻 token 的拼接边界不改变切分),否则直接抛错——这是"从原生 logits 读取"能成立的前提。提示词通过apply_chat_template(..., enable_thinking=False)构造,并计算prompt_sha256作为提示词指纹。
一次前向传播与条件概率输出
score(direct.py)的流程极简:编码 → 单次前向 → 取出最后一个位置的完整词表 logits → 按选项槽位 token id 取值 → softmax:
with torch.inference_mode(): vocabulary = _forward(model, inputs)[0].float() # 只有最后一个位置的 logits selected = vocabulary[slots].cpu().tolist() return { "id": row["id"], "option_ids": [...], "probabilities": softmax(selected), "option_logits": selected, "input_tokens": len(ids), "forward_seconds": ..., "total_seconds": ..., "prompt_sha256": prompt_hash, "prompt_version": "direct-options-v1", "model": metadata, ... }_forward会检测模型是否支持logits_to_keep参数,从而只保留最后位置的 logits,省去整个词表的计算开销。返回的概率以给定选项为条件(conditional on the supplied options),输出中明确标注probability_status为"条件选项得分,未校准为决策置信度"——这是理解 SemIf 所有结果的先决条件。
安装与快速开始
环境要求与安装
SemIf 面向 Python 3.10+、CUDA 与能容纳 4B BF16 模型的 GPU(README 实测环境为单张 RTX 3090)。推荐流程:
python -m venv .venv . .venv/bin/activate export HF_HOME=/path/to/large-drive/huggingface pip install -e '.[test]'各可选依赖组见 pyproject.toml:test组(pytest)、mlx组(仅 darwin/arm64 的 mlx 与 mlx-lm 固定版本)、llamacpp组(llama-cpp-python 0.3.35)。命令入口semif-score由 [project.scripts] 注册到semif_phase1.cli:main。
命令行全参数说明
CLI 实现在 src/semif_phase1/cli.py,参数如下:
| 参数 | 取值 | 说明 |
|---|---|---|
--mode | direct/serial/shared/reranker | 必选,四种执行模式 |
--backend | torch(默认)/mlx/llamacpp | 推理后端 |
--model | 模型名或本地目录 | 必选 |
--revision | 40 位 commit 哈希 | 必选;远端模型必须钉死 commit,本地模型则作为 manifest/revision 标签 |
--input/--output | 路径 | 必选;输出必须是新文件,已存在即报错(防覆盖证据) |
--max-tokens | 正整数,默认 4096 | 输入 token 上限,超限直接报错、绝不截断 |
--device | auto/cuda/mps,默认auto | Torch 后端;auto 优先 CUDA 再 MPS |
--dtype | bfloat16(默认)/float16/float32 | 精度,改变它可能改变选项得分 |
--mlx-bits | 4 / 8 | 仅 MLX;内存内仿射量化,需未量化源检查点 |
--mlx-cache-limit-mib | 非负整数,默认 256 | 仅 MLX;限制闲置分配缓存(MiB),0 关闭 |
--gguf | 路径 | 仅 llamacpp;本地 GGUF 检查点 |
--llama-threads | 正整数 | 仅 llamacpp;CPU 线程数,默认全部可见核心 |
CLI 还会做一组前置一致性校验(cli.py),例如--mlx-bits必须配--backend mlx、--gguf必须配 llamacpp、MLX/llama.cpp 后端不支持reranker模式(reranker 强制走 Torch/CUDA,--device mps会被拒绝)。这些规则都有对应的测试用例,见 tests/test_cli.py。
第一个基准命令
CUDA_VISIBLE_DEVICES=0 semif-score \ --mode direct \ --model Qwen/Qwen3.5-4B \ --revision 851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a \ --input examples/decisions.jsonl \ --output results.jsonl每条结果包含:类型化选项得分(probabilities + option_logits)、计时、精确模型 revision(含 torch/transformers 版本)与prompt_sha256。CUDA_VISIBLE_DEVICES只暴露一块 GPU——Torch 加载器(core.py)强制单 GPU 可见,且检查检查点加载完整性(missing/mismatched keys 任一存在即抛错),远端模型必须提供 40 位 revision。
四种执行模式:从逐行打分到共享状态并行
direct:逐行独立打分
最朴素的模式,逐行完整编码、一次前向、读取概率。适用于状态各不相同、无法复用的场景,也是其余模式的基准。
serial:状态前缀缓存
当同一段状态需要连续回答多个判据时,src/semif_phase1/serial.py 的SerialPrefixScorer先把状态部分 prefill 一次并把原生 KV 缓存保存下来;后续每行若state相同则命中缓存,深拷贝一份缓存分支再只解码后缀部分。这依赖模型支持选择性末位 logits(logits_to_keep),否则直接报错。输出额外带有cache_hit、prefix_tokens、prefix_sha256、allowed_token_mass(选项 logits 质量占全词表的质量比)与full_vocab_argmax_id。
shared:共享状态并行评估
如果每一行都共享完全相同的 state,就用--mode shared:状态只 prefill 一次,然后通过cache.reorder_cache复制分支(CUDA 上批处理全部后缀)或逐行复制(MPS 上更快),一次性返回所有行的概率与聚合计时(prefill / replicate / suffix 分段计时)。约束:所有行的state必须完全一致、行id必须唯一;要求模型的原生缓存支持reorder_cache(src/semif_phase1/shared.py)。共享模式的结果行会附上shared_timing汇总字段。
注意:串行/共享这类"快速复用路径"属于实验性能力——README 实测在 777 个决策上,BF16 执行相对逐行新鲜打分改变了 5–6 个 argmax 结果,因此对精度敏感的场景应对比两种路径的输出。
reranker:官方风格 Qwen3 重排器读法
src/semif_phase1/reranker.py 把每个选项单独构造成"证据-候选答案"配对,读取yes/no两个单 token 的 logits,取 log-odds 作为该选项的兼容度,最后对 log-odds 做 softmax 归一化用于相对比较。它对检索类场景(code-rag、company-brain)使用检索指令,对普通决策使用决策指令;probability_status明确标注为"相对选项兼容度,未校准为类别概率"。从源码结构看,reranker 在检索排序上更强,而 direct 是更通用的决策基线。
三种后端:Torch / MLX / llama.cpp
Torch(CUDA 与 Apple MPS)
默认后端,经 core.py 的设备解析逻辑选择 cuda:0 或 MPS;Qwen3.5 系列走原生Qwen3_5ForCausalLM,并要求安装的 transformers 自带该类(缺失即报错)。加载时禁用trust_remote_code。
MLX(Apple Silicon 原生)
macOS arm64 上可安装pip install -e '.[test,mlx]'并在命令中加--backend mlx,详见 docs/MLX.md。src/semif_phase1/mlx_backend.py 提供 direct、serial、shared 三种模式(不支持 reranker)。实现要点:
- 强制 Apple Silicon(Darwin + arm64)与 Metal GPU 可用;
- 仅支持原生 Qwen3.5 文本模型(
model_type == "qwen3_5"),禁止自定义模型代码; - 支持
--mlx-bits 4/8的内存内仿射量化(group_size=64),但要求源检查点未量化; - 用
mx.set_cache_limit限制闲置分配缓存(默认 256 MiB,--mlx-cache-limit-mib可调),避免变量长度提示词挤占系统内存; - 对源检查点逐文件计算 SHA-256 存入 metadata(
source_artifact_sha256),本地 revision 标签之外再增加一层来源证据。
llama.cpp(纯 CPU + GGUF)
无 CUDA 设备时可用:pip install -e '.[test,llamacpp]',获取一个 GGUF 检查点(例如 Qwen3.5-4B 的 Q4_K_M 量化版),命令中加--backend llamacpp --gguf /path/to/model.gguf,--llama-threads限制 CPU 线程。实现细节(src/semif_phase1/llamacpp_backend.py)值得注意:
- 提示词构造始终使用钉死的参考 transformers tokenizer,因此
prompt_sha256与 Torch 后端逐行一致;llama.cpp 只负责前向执行; - 每次打分前用 GGUF 词表重新 tokenize 并与参考编码比对,不一致即报错;加载时还有独立的词表探针校验(
_verify_vocabulary); - 输出携带 GGUF 文件校验和,分数以量化权重为条件;direct 与前缀缓存路径可能因 llama.cpp 不同评估路径存在小幅数值差异,比较决策或概率时应带容差,而非逐位对比 logits;
- Qwen3.5 的混合线性注意力不支持序列拷贝,分支复用走 llama.cpp 自身的整序列状态保存/恢复(
save_state/restore_state); - 一个已加载后端只拥有一个有状态打分上下文。
跨后端的一致性设计
无论哪个后端,输入验证、提示词模板、槽位校验、softmax 与输出字段都由 core.py 与 direct.py 统一提供,后端仅替换"加载 + 前向"两层,这保证了prompt_sha256跨后端可对齐、结果可对比。
性能实测:直读概率 vs 生成式基线
决策 vs 紧凑生成数组
同一冻结 Qwen3.5-4B、同一自有状态、同一 21 条二元判据、单张 RTX 3090:
| 输出路径 | 时间(中位数,3 次) | 输出 token | 结果 |
|---|---|---|---|
| 直接读取类型化 logits | 1.023 s | 0 | 21 组概率对 |
| 自回归生成紧凑 JSON 数组 | 5.332 s | 111 | 合法有序 21 值数组 |
生成基线的中位首个 token 时间只有 0.489 s,但完成整个数组耗时是直接读取的5.21 倍。三次数组的输出全部合法且一致,与直接 argmax 在 21 条判据中 18 条一致——因此这是系统级对比,而非声称两种读法语义等价。精确提示词、输出、token 时间线与全部运行记录已提交至 results/raw/decision-vs-compact-array.json。
一段状态复用于 21 个决策
在自有的 37 状态 × 21 判据工作负载(777 个决策)上:
| 执行路径 | 决策/秒 | 777 个决策耗时 |
|---|---|---|
| 逐行新鲜 direct 打分 | 2.33 | 333.1 s |
| 串行前缀复用 | 10.75 | 72.3 s |
| 并行后缀 | 20.03 | 38.8 s |
| 原生 reranker | 1.86 | 417.3 s |
对应证据文件:自有 37×21 测试集、direct/复用评测脚本、reranker 评测脚本、原始计时 与逐行预测。注意快速复用路径是实验性的(前述 BF16 下 5–6/777 的 argmax 变化)。
质量评估与校准
浏览器模型阶梯(Browser model ladder)
| 系统 | 浏览器产物 | 下载体积 | Authored 平衡准确率 | Perturbation 平衡准确率 | TypeSafe 子集一致率 |
|---|---|---|---|---|---|
| Qwen3-0.6B | Q8_0 | 639 MB | 0.440 | 0.528 | 0.407 |
| MiniCPM5-2B | Q4_K_M | 1.56 GB | 0.686 | 0.693 | 0.637 |
| Qwen3.5-4B | Q4_K_M | 3.01 GB | 0.813 | 0.766 | 0.845 |
| 已发布的 Jev | 闭源托管服务 | — | — | — | 0.883 |
(原生 BF16 得分;浏览器构建使用量化 GGUF。Jev 数字为 TypeSafe 在相同 102 行子集上的公开结果。)
通用决策基线
| 冻结工作负载 | 行数 | direct logits(4B BF16) | EXL3 direct(27B, 5 bpw) | 原生 reranker(4B) | 已发布 Jev |
|---|---|---|---|---|---|
| Authored 决策,平衡准确率 | 144 | 0.813 | 0.958 | 0.625 | — |
| WANLI,平衡准确率 | 256 | 0.637 | — | 0.522 | — |
| TypeSafe 选定子集,模态一致率 | 102(20 例) | 0.845 | — | 0.560 | 0.883 |
| Every 判断网格,准确率 | 36 | 0.806 | — | 0.694 | — |
| Every 操作防火墙,组合准确率 | 10 动作 | 0.700 | — | 0.700 | — |
| Every 代码检索,Recall@1 | 6 查询 | 1.000 | — | 1.000 | — |
| Every 公司知识,Recall@1 | 7 查询 | 0.929 | — | 0.929 | — |
两点边界必须明确:其一,Jev 数字读取自 TypeSafe 公开记录,项目并未运行真实 Jev 端点,且对比覆盖的是能从公开产物对齐的 102 行,而非 TypeSafe 报告的 711 行聚合;其二,Qwen3.8-27B EXL3 桥接(见 exl3-bridge/README.md)使用与 4B 基线相同的 144 行 authored 数据、匹配的提示词哈希、选项、直接 logits 读法与指标,属于系统级质量对比而非受控的模型规模/量化消融(模型家族、规模、量化、运行时全部不同),且在 777 决策共享状态测试集上与钉死 4B 模型的抉择一致率为 84.43%,尚未在其他质量工作负载上运行。
概率校准:温度缩放
选项概率只有在"置信度与观测准确率匹配"时才有实用价值。SemIf 内置按工作负载拟合的温度缩放(per-workload temperature scaling),详见 docs/CALIBRATION.md:
| 工作负载 | 原始 ECE | 折外校准 ECE | 温度 |
|---|---|---|---|
| Authored 决策 | 0.068 | 0.038 | 1.23 |
| WANLI | 0.208 | 0.069 | 2.50 |
| Every judgments | 0.050 | 0.047 | 1.71 |
校准不改变所选的选项;WANLI 上改善显著,authored 与 Every 工作负载上区间有重叠。结论:务必在将要实际做决策的工作负载上做校准与验证,而不是跨负载复用温度。
可审计性与证据边界
SemIf 把"可复现"落实到提交物层面:自有测试集、精确评测脚本、逐行输出、模型 revision、提示词哈希、已知失败与校验和全部入库。README 强调模型权重与第三方源记录不随仓库分发(上游模型保留各自许可证),项目代码以 MIT 许可证 发布,第三方来源清单见 THIRD_PARTY.md。
文档导航
- docs/RESULTS.md — 质量、速度、扰动测试与声明边界
- docs/METHOD.md — 冻结提示词、指标与计时范围
- docs/REPRODUCE.md — 精确环境、钉死命令、扰动与验证
- docs/APPLE_SILICON.md — MPS 与可选 MLX 后端
- docs/CALIBRATION.md — 拟合温度、折外证据与应用
- exl3-bridge/README.md — 量化 27B 运行器与已提交证据
- demo/index.html — 交互式重放(可复现的速度对比演示)
- webgpu-demo/index.html — 纯浏览器 WebGPU 演示,无需等待名单
- results/phase1-summary.json — 机器可读汇总
- benchmarks/README.md — 测试集、运行器、选择 ID 与复现命令
- results/raw/ — 原始结果与校验和
- manifests/models.json — 模型清单
结语
SemIf 提供了一条与"生成文本再解析"完全不同的决策路径:把大模型的下一步 token 分布当作分类器直接消费,用单次前向传播换取数量级的延迟与输出 token 节省,同时以钉死的 revision、提示词哈希与逐行证据保证可复现性。它既是一个可直接运行的 CLI 工具(semif-score),也是一套可拆解的方法论——从输入契约、槽位校验到共享状态前缀缓存,再到按工作负载校准,每一层都有源码与测试支撑,适合作为自建"语义化 if"服务的最小可信基线。
【免费下载链接】SemIfSemantic ifs from open models, on a 3090 at home. Independent; not affiliated with Jev or TypeSafe.项目地址: https://gitcode.com/gh_mirrors/op/SemIf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考