Semantic Router 模型训练体系:目录所有权与 semantic-router.training/v2 控制面契约详解
Semantic Router 模型训练体系:目录所有权与 semantic-router.training/v2 控制面契约详解
本文围绕 Semantic Router 仓库中 src/training/README.md 讲解的模型训练树组织方式与训练控制面契约展开:先梳理 src/training/ 各目录的发布所有权与模型制品映射关系,再逐节解析 semantic-router.training/v2 契约中的 Profile、冻结 RunSpec、Worker 生命周期状态机、能力目录规划以及 Make 生成与校验流程。读完后,你可以理解仓库如何做到"公开制品可追溯到本地 owner"、Go/Python/TypeScript 三端如何共享同一份契约,以及一份分类器工件从训练产出到资格绑定经历了哪些可验证的检查。
训练树是唯一的权威来源:目录所有权与制品映射
src/training/README.md 开篇即定义了该目录树的定位:它是"产生、评估和打包 Semantic Router 所维护模型"的规范来源(canonical source)。仓库内维护的每一类公开模型制品,都通过 model_artifacts.json 映射到其本地 owner——即负责该制品训练、评测与导出的代码目录。
顶层目录职责
README 给出的所有权分工如下表,每个"发布持有型"(release-owning)family 目录都配有聚焦的 README、机器可读配置、显式的数据/输出路径、训练与导出入口,以及轻量级契约测试。模型权重、数据集、缓存、checkpoint 和运行日志一律不入库(stays outside Git):
| 目录 | 职责 |
|---|---|
src/training/control_plane/ |
共享训练契约校验与分类器溯源绑定 |
src/training/model_classifier/ |
意图、策略、反馈、模态、PII 与安全分类器 |
src/training/model_embeddings/ |
文本、重排序与多模态嵌入族 |
src/training/model_eval/ |
跨家族评测工具 |
src/training/model_experiment/ |
非发布 owner 的实验 |
src/training/model_selection/ |
学习型模型选择研究 |
src/training/kv_mapper/ |
跨模型 KV mapper 制品、岭回归拟合与蒸馏(issue #2976) |
公开制品如何映射到本地 owner
model_artifacts.json 用 schema_version: 1 的结构维护两个集合,恰好对应 README 中"Vela 集合包含共享的 Base、Embedding、Reranker 和八个分类器;历史 MOM 制品保留其原始训练 owner"的表述:
vela-10-router-models:以vllm-sr/Vela-1.0-Encoder-307M基座为中心的一组制品。例如 Base 编码器的 owner 是 src/training/model_embeddings/mmbert_32k,入口为foundation.py;Vela-1.0-Encoder-307M-Embedding与-Reranker同属该目录,入口为newbase_training.py;而-Domain、-Guard、-Safety、-FactCheck、-Feedback、-Modality等分类器制品统一由 src/training/model_classifier/sequence_repair/train.py 产出;-PII由pii_model_fine_tuning_lora/train_repair.py产出,-Hazard由 src/training/model_classifier/safety_classifier 的train_vela_hazard.py产出。mom-multilingual-embed与mom-multilingual-class:历史 MOM 系列,如vllm-sr/mmbert-embed-32k-2d-matryoshka(owner 同为 mmbert_32k,入口embedder.py,配置configs/embedder.json)、vllm-sr/mmbert32k-intent-classifier-merged(owner 为classifier_model_fine_tuning_lora/ft_linear_lora.py,artifact_type区分merged与lora两种形态)。
每条记录除 owner 与 entrypoint 外,还可携带 config、task、artifact_base_model 等字段,把"这个公开制品由哪个脚本、在哪个配置下、基于哪个基座训练出来"固定为可查询的事实。
README 同时给出了导航路径:选择模型家族时,从公开文档 website/docs/training/training-overview.md 入手;具体环境、数据准备、命令、评测与工件格式则以各训练器旁的 README 为准。
训练控制面契约:semantic-router.training/v2
契约定位与 v1 到 v2 的演进
README 对契约的定位非常明确:semantic-router.training/v2 定义了选择器训练与神经微调的共享资源与消息,它是面向未来管理端(management)与 worker 实现的契约——HTTP 路由、持久化、调度以及 Console 训练流程并未由本包实现。这与 src/semantic-router/pkg/trainingcontract/training-v2.openapi.yaml 中 "Contract for the planned Go management API; these routes are not yet registered" 的声明一致。
版本升级的背景写得很具体:当 Router 内嵌的 Candle 与 ONNX Runtime 运行时被移除后,分类器改为在 model runtime 上接受资格认定(qualification),v1 文档随即被拒绝。在源码中这一版本由 src/semantic-router/pkg/trainingcontract/types.go 的常量固化:
const Version = "semantic-router.training/v2"
单一事实来源:Go 类型 → JSON Schema → Console / Python
README 描述的生成链路在仓库中可完整对应:
- 规范 Go 契约位于 src/semantic-router/pkg/trainingcontract/,它生成 training-v2.schema.json 与 Console 类型 dashboard/frontend/src/generated/trainingContract.ts;
- src/training/control_plane/contracts.py 的 Python 校验直接消费这份 schema,而不是维护第二套模型:
CONTRACT_ROOT = (
Path(__file__).resolve().parents[2] / "semantic-router/pkg/trainingcontract"
)
SCHEMA = json.loads((CONTRACT_ROOT / "training-v2.schema.json").read_text())
def validate(message: dict, definition: str) -> None:
"""Check message structure against the versioned wire schema."""
schema = {**SCHEMA, "$ref": f"#/$defs/{definition}"}
Draft202012Validator(schema, format_checker=FormatChecker()).validate(message)
注意其文档字符串划出的职责边界:资源关系与 run 状态转换归 Go 管理端所有,分类器 bundle 语义保留在既有溯源校验器中——这正是后文 Profiles/provenance 与 Worker 生命周期两节要分别展开的原因。
核心资源与消息(来自 Go 契约)
types.go 定义了三类目标契约(tagged union):
Selector Target = "selector.model-choice/v1"
LabelScores Target = "signal.label-scores/v1"
Spans Target = "signal.spans/v1"
Profile 结构是带 target_contract 的标签联合体,分别携带 SelectorProfile(候选模型列表 + 观测字段)、ClassifierProfile(label_mapping)或 SpanProfile(标签 + offset_unit: unicode-codepoint)。README 强调:这些形状彼此独立,声明其中一种并不暗示 worker 支持它。
其余关键类型与 README 描述一一对应:
RunSpec:在提交时冻结数据集身份、trainer 版本与 JSON 参数;可选的base_model(ModelRef,仓库 + 40 位 revision)属于冻结的 run spec,使不同基座模型可以共用同一快照。注释明确"trainer 特定参数校验属于 capability planner"。RunGraph= run + 任务列表 +RunOutputs;RunOutputs索引已发布的artifact_ids、evaluation_ids、qualification_ids,让客户端仅凭 run ID 即可发现产出。ArtifactVariantSpec.Files将逻辑相对文件名(如config.json、tokenizer、权重分片名)映射到受控文件句柄(handle+sha256:digest + 字节数),客户端据此重建模型的完整文件布局;注释强调这些名字描述的是工件布局,绝非服务器文件系统路径。EvaluationSpec引用已存在的 variant,"评测不铸造新工件";QualificationSpec携带 variant、runtime、compatible与收据文件;BindingProposalSpec则由 variant ID + qualification ID + 名称构成,即 README 所说"资格证据为绑定提案提供 variant 与 qualification ID"。Metadata要求schema_version固定为semantic-router.training/v2,id匹配^[a-z]+_[a-zA-Z0-9-]+$;DataSnapshot无更新操作,其身份包含字节内容与预处理步骤。
错误码:开放的 APIError.code
OpenAPI 契约规定 APIError.code 是开放字符串:v2 内可以新增错误码而不 bump 契约版本,既有码保持语义不变;客户端必须接受未知码,并退化为用 HTTP 状态码与 message 处理通用错误。已列出的 well-known 码包括:400 invalid_request、401 unauthenticated、403 forbidden、404 not_found、409 conflict、500 internal_error(见 training-v2.openapi.yaml 的 components.responses.Error)。
Profiles 与 provenance:结构接受不等于资格认定
快照、冻结与归属划分
Profiles 描述数据集与输出形状;Go 管理端拥有资源身份、所有权、profile 兼容性与状态转换,Python 只负责消息结构校验。溯源信封(Provenance,见 types.go)把产出资源链接到其 run、task、attempt、snapshot 与 trainer;可选的 manifest_bundle 字段保留既有的 Router Model 溯源契约(见 src/training/model_eval/provenance/README.md)。
validate_classifier_provenance 的完整校验链
在发布分类器资格证据之前,src/training/control_plane/provenance.py 的 validate_classifier_provenance 执行 README 描述的整套检查,源码可逐条印证:
- 加载并校验 bundle:
load_validated_bundle(directory)加载 worker 拥有的溯源 bundle;directory与resolve_file由 worker adapter 私有提供——resolver 返回的是 schema 校验过的、正在被资格认定的 variant 的不可变受控文件,本地路径始终不离开 adapter。 - 绑定 profile 与 manifest 的 label_mapping:任何 manifest 的
label_mapping与profile["classifier"]["label_mapping"]不一致即抛ManifestError。 - 绑定基座模型:
kind == "run"的 manifest 中base_model必须等于冻结 run spec 中的{repository, revision},否则拒绝。 - variant 文件与评测过工件清单一致:
_validate_variant_files要求 variant 声明的全部文件名、digest、size 与某份"已被评测"(其 id 出现在 evaluation manifests 中)的 artifact manifest 完全相等,否则报 "variant files do not match an evaluated artifact manifest"。 - 逐字节验证:对每个文件经
resolve_file解析真实路径,核对st_size与file_digest,任何缺失或不一致都会拒绝。
README 的结论在代码层面得到强化:"仅对 worker 结果的结构接受(structural acceptance)并不能使工件获得资格"。
Worker 生命周期:状态机与 attempt 幂等
README 给出的 run 状态转换是:
pending -> running -> succeeded | failed
pending | running -> cancelling -> cancelled
failed | cancelled -> pending (显式重试)
其中 skipped 是仅针对 task 的依赖结果。在 types.go 中,这些状态与三个 Target 一样被固化为契约常量,Attempt 类型携带 number(minimum 1)、worker_handle、起止时间与 diagnostic,支撑 attempt 历史。
关键语义(README 原文要点):
- Worker 接收冻结的输入、解析后的输入 variant 以及 run/task/attempt ID,只执行任务;依赖调度与 run 状态推导归 Go 管理端。
- 提交、检查、取消都基于
attempt_id;submit 对该 ID 幂等,即使响应丢失也幂等。管理端必须在派发前持久化 attempt 身份,并在重启后对其做协调(reconcile);worker job handle 只是不透明的确认回执。 - Worker 上报 running / succeeded / failed / cancelled,只有成功结果才发布输出。管理端校验后,把资源 ID、run 输出 ID 与 task 结果一起发布;评测与资格认定复用已发布的 variant ID;协议非法的最终结果成为带诊断信息的 failed attempt。
这些语义同样体现在 OpenAPI 路由描述中:POST /runs 的幂等键以认证 owner 为作用域——相同 key + 相同解码 spec 返回既有 run,spec 不同则返回 409;POST /runs/{id}/cancel 记录取消意图,pending/running 进入 cancelling,待执行中的任务落定后变为 cancelled,重复取消返回当前 graph;POST /runs/{id}/retry 仅接受 failed/cancelled 的 run(活跃或成功则 409),保留 run/task ID、冻结 spec、已发布输出与 attempt 历史,需重新执行的工作获得新 attempt ID,并发重试不会产生重复 attempt。
能力目录与规划:无中央开关的声明式组合
能力层(capability layer)在独立可扩展的 trainer、架构、硬件、工件、runtime 能力之间解析一次请求的训练结果,不依赖中央 switch 或硬编码 UI 枚举。能力 ID 遵循 <domain>/<name>@<version> 格式,README 给出的示例与 src/semantic-router/pkg/trainingcontract/capability.go 中的目录实现相呼应:trainer/hf-peft@v1、architecture/hf-modernbert@v1、hardware/rocm@v1、runtime/model-runtime@v1。
两个设计要点值得注意:
- 训练硬件与推理资格认定硬件相互独立。一个在 CUDA/ROCm GPU 上训练出的神经模型,可以被规划并在多个 runtime 目标上资格认定(例如
runtime/model-runtime@v1,即 model runtime 的 OpenAPI 2.x 契约),目标可以是 CPU 也可以是 AMD GPU。内置目录支持在 model runtime 上从 Safetensors checkpoint 资格认定 ModernBERT 的 label-score 与 span 分类器,以及在原生 runtime 上资格认定 selector。 - 格式转换由已注册规则调度。当某个已注册 runtime 只接受另一种格式时,planner 会安排该规则提供的转换任务。
客户端交互面只有两个端点:GET /capabilities 发现支持的描述符,POST /capabilities/plan 校验拟议组合。不支持的组合返回稳定的机器可读原因码——如 INCOMPATIBLE_HARDWARE、INCOMPATIBLE_ARCHITECTURE、UNSUPPORTED_TARGET、MISSING_FORMAT_CONVERSION——以及可操作的补救信息(见 planner.go 与 OpenAPI 中 planTraining 的响应定义)。
生成与校验:Make 目标与跨语言测试
README 的 "Generate and verify" 一节给出的操作序列在 tools/make/golang.mk 中可以精确核对:
training-contract-generate: ## Generate the shared training schema and Console types
@cd src/semantic-router && go generate ./pkg/trainingcontract
training-contract-check: harness-venv-install ## Check training contracts without trainers or native libraries
@cd src/semantic-router && go run ../../tools/codegen/trainingcontract/main.go --root ../.. --check
@cd src/semantic-router && go test ./pkg/trainingcontract
@$(AGENT_PYTHON) -m unittest src.training.control_plane.test_contracts
- 修改 Go 类型或默认能力描述符后,从仓库根目录运行
make training-contract-generate; - 随后运行
make training-contract-check,它依次检查:生成漂移(generated drift)、Go 语义、Python schema 校验、provenance 兼容性。该目标被设计为无需训练器、无需原生库即可运行(依赖仅harness-venv-install)。 - Console 侧在
dashboard/frontend下运行npx vitest run src/utils/trainingContract.test.ts验证 TS 消费。
跨语言测试的公共夹具位于 src/semantic-router/pkg/trainingcontract/testdata/,在 Go、Python、TypeScript 三端演练 management/worker 交互与输出发现。README 特别澄清了两点边界:这些夹具的 ID 与 digest 只是示例,不是可下载的模型工件;这些检查也不需要训练器、GPU 或运行中的管理端服务,但当 handler 真正实现后,它们不能替代 HTTP 集成测试。
小结
src/training/ 的 README 实际上定义了一套"训练侧的仓库治理规范":
- 可追溯:每个公开模型制品都通过 model_artifacts.json 绑定到具体的训练目录与入口脚本,Vela 十件套与历史 MOM 制品各有明确 owner;
- 单一契约:
semantic-router.training/v2由 Go 类型生成 JSON Schema 与 Console TS 类型,Python 直接消费 schema,避免多语言模型漂移; - 强资格门槛:分类器工件必须通过 label_mapping 绑定、基座 revision 绑定、variant 与评测清单一致、逐字节 digest 校验四重检查才谈得上资格认定;
- 幂等且可恢复的 worker 语义:attempt_id 幂等提交、仅成功结果发布、输出与任务结果原子公开,配合取消/重试的状态机约束;
- 声明式能力规划:以能力 ID 组合替代中央开关,用稳定原因码表达不可规划的组合。
对于希望深入某一具体模型家族的读者,建议从对应目录的 README 出发(如 src/training/model_classifier/safety_classifier/README.md 或 src/training/model_eval/README.md);对于关注契约演进的读者,则可从 src/semantic-router/pkg/trainingcontract/ 的类型与测试入手。