历史计划,不是用户手册。 现行接入步骤见 how_to_add_new_model.zh-cn.md 与 model-developer-guide.md。HTTP 活示例见 api-contract.zh-cn.md。 执行清单:model-developer-experience-todolist.zh-cn.md 基线:
main @ 5648960 refactor(models): validate latent and clip outputs
建议分支:refactor/models-p4-developer-experience
P0 到 P2 已完成模型层正确性基础:
InferenceContext;当前短板不再是正确性,而是开发体验:
memcpy;P4 的目标是:
让新增普通 CV 模型时,开发者只关注模型本身的 preprocess、参数和 decode;生命周期、契约、注册、测试骨架和文档骨架由基础设施提供。
P4 不引入重型框架,不做动态插件系统,不追求“一切皆配置”。优先提供:
所有新 API 必须构建在现有基础设施上:
InferenceContextTensorContractf32_output.hrequest_geometry.hBackendCvModelP4 不能降低 P0/P2 已达到的安全边界。
模型差异大的 decode 逻辑保留在各模型中。公共层只承接确定性重复逻辑:
不把 YOLO、SAM、diffusion 等差异强行塞入通用基类。
当前项目基线为 C++17,兼容性优先。P4 不依赖:
std::span;可以在 C++17 内通过小型 value type、builder 和静态断言达到目标。
当前新增一个模型通常需要接触以下位置:
| 序号 | 位置 | 内容 |
|---|---|---|
| 1 | src/models/<task>/*.h |
模型类与成员声明 |
| 2 | src/models/<task>/*.inl |
preprocess、postprocess、on_init |
| 3 | conf/model/<task>/<model>/*.toml |
backend 与模型参数 |
| 4 | src/factory/<task>_task.h |
model factory 与 server factory |
| 5 | server spec | section、display name、serializer、worker maker |
| 6 | test/*_unittest.cc |
contract 或行为测试 |
| 7 | test/model_golden_test.cc |
golden case |
| 8 | test/golden/* |
golden 数据 |
| 9 | weights / TRT profile / manifest | 权重与 engine 配置 |
| 10 | 文档 | 模型说明与接入说明 |
其中真正模型相关的只有:
其余流程应模板化或注册化。
P4 完成后,普通单图像模型的理想接入路径为:
1. 运行 scripts/new_model.py 生成模型骨架
2. 实现 preprocess pipeline
3. 实现 output reader + decode
4. 补模型参数与输出契约
5. 添加 catalog entry
6. 补 contract test 与 golden case
目标文件:
src/models/<task>/<model_family>/<model>.h
src/models/<task>/<model_family>/<model>.inl
conf/model/<task>/<model_family>/<model>.toml
test/<model>_output_contract_unittest.cc
test/golden/<model>.json
src/models/catalog/<task>.cpp
普通模型不再要求修改:
任务
std::memcpy 出现次数;convert_to_chw_vec 次数;-Werror;交付
docs/model-developer-experience-metrics.zh-cn.md
验收
新增:
src/models/backend/model_runtime.h
src/models/backend/model_runtime.cpp
test/model_runtime_unittest.cc
目标 API:
auto input = ImagePipeline(image)
.bgr_to_rgb()
.resize(network_size)
.to_float()
.scale(1.0f / 255.0f)
.mean_std(mean, std)
.nchw(input_name);
支持:
返回使用 C++17 可实现的 RuntimeResult<T>,不引入异常,不改变现有 StatusCode 边界。
目标 API:
auto scores = OutputReader(outputs, "output")
.f32()
.shape({1, class_count})
.finite();
if (!scores.ok()) {
return scores.status();
}
底层复用:
validated_f32_named_outputTensorContractrequire_finite_f32必须保留语义:
missing output -> MODEL_EMPTY_OUTPUT
dtype/rank/shape/buffer 错误 -> MODEL_OUTPUT_CONTRACT_FAILED
NaN/Inf -> MODEL_OUTPUT_CONTRACT_FAILED
目标 API:
auto result = ParamReader(params, "MY_MODEL")
.get("score_threshold", &score_threshold)
.min(0.0)
.max(1.0);
if (!result.ok()) {
return result.status();
}
统一能力:
目标 API:
auto input_info = SessionIoValidator(session())
.input("images")
.dtype(DType::F32)
.rank(4)
.nchw()
.channels(3)
.validate();
用于替代各模型 on_init 中重复的 session input shape 检查。
Phase 1 验收
-Werror 通过。新增:
src/models/catalog/model_entry.h
src/models/catalog/classification.cpp
src/models/catalog/object_detection.cpp
src/models/catalog/scene_segmentation.cpp
src/models/catalog/ocr.cpp
src/models/catalog/matting.cpp
src/models/catalog/enhancement.cpp
src/models/catalog/feature_point.cpp
src/models/catalog/depth.cpp
src/models/catalog/sam.cpp
src/models/catalog/clip.cpp
src/models/catalog/diffusion.cpp
ModelEntry 包含:
struct ModelEntry {
std::string model_section;
std::string server_section;
std::string display_name;
std::string task;
ModelCreator creator;
ResponseSerializerKind serializer;
};
每个任务维护一个显式 catalog,不使用全局静态注册,避免初始化顺序和隐藏副作用。
Factory 从 catalog 读取 entry,统一生成:
CvServerSpec;原手写函数保留兼容包装,逐步废弃。
新增:
test/model_catalog_unittest.cc
验证:
Phase 2 落地结果(as-built)
实际实现比原方案更克制,避免为单一形态造抽象:
src/models/catalog/model_entry.h # ModelEntry / ServedModelEntry + 校验函数
src/factory/cv_catalog.h # CvModelEntry<OUTPUT> + create_server
src/factory/model_catalog.h # ModelCatalogEntry<INPUT, OUTPUT> + create_model
src/factory/<task>_task.h # 每个任务自己的 catalog(),全部 header-only
inline catalog() 函数,不拆成独立 .cpp,也没有全局静态注册。ModelEntry 只保留 model_section + display_name;ServedModelEntry 再加
server_section;CvModelEntry<OUTPUT> 再加 worker creator 与 response filler。
没有引入 task 字符串、ModelCreator 类型擦除或 ResponseSerializerKind 枚举。ModelCatalogEntry,
不强行补 server section。catalog() 与 face_catalog()。test/model_catalog_unittest.cc 额外校验 server TOML 中的
model_config_file_path 指向的文件真实存在,并实际构造 CvModelServer<OUTPUT>。Phase 2 验收
新增:
scripts/new_model.py
templates/model/*.h.in
templates/model/*.inl.in
templates/model/*.toml.in
templates/model/*_contract_unittest.cc.in
templates/model/README.md.in
使用方式:
python scripts/new_model.py \
--task object_detection \
--name rtdetr \
--family rtdetr \
--backend tensorrt \
--input image \
--output boxes
生成:
Phase 3 落地结果(as-built)
scripts/new_model.py # CLI + 模板渲染 + 自测
templates/model/model_header.h.in
templates/model/model_impl.inl.in
templates/model/model_config.toml.in
templates/model/output_contract_unittest.cc.in
templates/model/model_readme.md.in
templates/model/tasks.json # 任务元数据表
与原方案的差异:
--task --name --class --backend --dry-run --force --list-tasks --check。
删掉 --family(与 --name 重复)和 --input(server 层固定 base64_input),
--output 由 tasks.json 按任务给出。check_consistency.py(第 11/12 条规则)而不是 gtest:
这是对源码文本的一致性校验,语义上属于 repo consistency checker。
校验 model_dir / io_namespace / output_type / catalog_header / catalog_function /
response_filler 全部真实存在,且 model-only 任务不得声明 server_section_suffix。--force 也只覆盖脚手架自己生成的五个文件。MODEL_NOT_IMPLEMENTED(wire code 7):未实现的钩子显式失败,
生成的 contract test 断言它,因此脚手架从生成那一刻起就是绿色且可编译的。rtdetr_detector 作为留存样例:--check 验证渲染行为,
rtdetr_detector_output_contract_unittest 验证生成的 C++ 真的能编译并实例化。Phase 3 验收
--force;--dry-run;--list-tasks;scripts/check_consistency.py 能识别生成物。将 model_io_define.h 拆为:
src/models/io/common_input.hpp
src/models/io/classification.hpp
src/models/io/object_detection.hpp
src/models/io/face_detection.hpp
src/models/io/scene_segmentation.hpp
src/models/io/ocr.hpp
src/models/io/matting.hpp
src/models/io/enhancement.hpp
src/models/io/feature_point.hpp
src/models/io/depth.hpp
src/models/io/clip.hpp
src/models/io/sam.hpp
src/models/io/diffusion.hpp
保留兼容聚合头 model_io_define.h。
Phase 4 落地结果(as-built)
src/models/io/
├── common_input.h # mat_input / file_input / base64_input / pair_mat_input
├── classification.h
├── object_detection.h # bbox + face_bbox(对应 catalog 与 face_catalog 两个输出契约)
├── scene_segmentation.h
├── ocr.h
├── matting.h
├── enhancement.h
├── feature_point.h
├── mono_depth_estimation.h
├── clip.h
├── segment_anything.h
└── diffusion.h
src/models/model_io_define.h # 纯聚合:12 行 #include,guard 不变
与原方案的差异:
.h,跟随 src/models 现有习惯,不用 .hpp。opencv2/opencv.hpp 收窄为
opencv2/core.hpp。IO 类型只需要 Mat / Rect / Rect2f / Point2f / Size,全部在 core。check_consistency.py 第 13 条规则要求聚合头只允许
#include "models/io/*.h"(禁止回填类型),IO 头禁止使用 opencv.hpp。templates/model/tasks.json 新增 io_header 字段,脚手架生成的新模型
直接 include 自己的任务 IO 头。实测数据(WSL / GCC 11 / -O2)
| 指标 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
| 只 include IO 头的 TU:头文件数 | 333 | 270 | -19% |
| 只 include IO 头的 TU:预处理行数 | 132,034 | 93,223 | -29.5% |
| 只 include IO 头的 TU:编译耗时(3 次中位) | 2.23 s | 1.25 s | -44% |
models 库目标 clean build |
18.6 s | 19.2 s | 噪声范围内 |
结论:models 库自身无可测变化(它的 .cpp 本来就拉 MNN/TRT 等更重的依赖),
收益集中在只依赖 IO 类型的叶子 TU 上;拆目录的即时价值是所有权隔离,
增量编译收益要等调用方逐步迁离聚合头之后才会出现。
Phase 4 验收
新增:
test/model_contract_test_util.h
test/model_golden_registry.h
Contract 测试目标:
POSTPROCESS_CONTRACT_TEST(
MyModel,
"output",
Shape{1, 1000},
DType::F32,
Context{.source = {640, 480}, .network = {224, 224}});
自动覆盖:
Golden 注册目标:
GOLDEN_CLASSIFICATION_CASE(
"mobilenetv2_classification",
"conf/model/classification/mobilenetv2/mobilenetv2_config.toml",
"demo_data/model_test_input/classification/xxx.JPEG",
create_mobilenetv2_classifier,
"mobilenetv2_classification");
统一处理:
Phase 5 落地结果(as-built)
test/model_contract_test_util.h # POSTPROCESS_CONTRACT_TEST:一行生成 7 个负向 TEST
test/model_golden_registry.h # GOLDEN_*_CASE:9 个按输出类型区分的注册宏 + 全部 helper
test/model_golden_test.cc # 906 行 -> 304 行
与原方案的差异:
零漂移硬校验(迁移前后)
| 指标 | 结果 |
|---|---|
| 用例数 | 27 → 27 |
| 用例名 | 完全一致 |
| 声明顺序 | 完全一致 |
| golden 基线文件 | 25 → 25,sha256 全部一致 |
| golden 测试结果 | 27/27 PASSED |
| contract canary | 7/7 PASSED |
model_golden_test.cc 行数 |
906 → 304(-66%) |
Phase 5 验收
迁移顺序:
选择该顺序的原因:
每个模型族迁移:
旧 preprocess -> ImagePipeline
手写 memcpy -> InputTensor builder
手写 TOML parse -> ParamReader
手写 output contract -> OutputReader
手写 session shape check -> SessionIoValidator
每个模型族验收:
model family contract tests 通过
CPU profile full check 通过
full GPU -Werror 编译通过
相关 GPU golden 不漂移
sanitizers 通过
适用:
新增:
src/models/backend/session_group.h
目标 API:
SessionGroup sessions;
sessions.declare("encoder", "encoder_backend");
sessions.declare("decoder", "decoder_backend");
统一处理:
Phase 7 验收
更新:
docs/how_to_add_new_model.md
docs/how_to_add_new_model.zh-cn.md
docs/model-contract-governance.md
README.md
README.zh-cn.md
新增开发者路径:
Phase 8 验收
template <typename T>
struct RuntimeResult {
StatusCode status = StatusCode::OK;
std::string error;
T value{};
bool ok() const { return status == StatusCode::OK; }
};
要求:
StatusCode API;class ImagePipeline {
public:
explicit ImagePipeline(const cv::Mat &image);
ImagePipeline &bgr_to_rgb();
ImagePipeline &rgb_to_bgr();
ImagePipeline &resize(const cv::Size &size);
ImagePipeline &to_float();
ImagePipeline &scale(float factor);
ImagePipeline &mean_std(const std::array<float, 3> &, const std::array<float, 3> &);
RuntimeResult<NamedTensor> nchw(const std::string &name) const;
RuntimeResult<NamedTensor> nhwc(const std::string &name) const;
};
约束:
class OutputReader {
public:
OutputReader(const std::vector<NamedTensor> &outputs, const std::string &name);
OutputReader &f32();
OutputReader &shape(std::vector<int64_t> shape);
OutputReader &finite();
RuntimeResult<F32OutputView> read() const;
};
注意:
F32OutputView 生命周期仍由调用方 outputs vector 持有;class ParamReader {
public:
ParamReader(const toml::table ¶ms, std::string log_prefix);
ParamReader &get(const std::string &key, int *value);
ParamReader &get(const std::string &key, int64_t *value);
ParamReader &get(const std::string &key, float *value);
ParamReader &get(const std::string &key, double *value);
ParamReader &get(const std::string &key, bool *value);
ParamReader &get(const std::string &key, std::string *value);
ParamReader &min(double value);
ParamReader &max(double value);
ParamReader &non_empty();
RuntimeResult<void> validate() const;
};
由于现有 BaseAiModel 是模板类型,catalog 初期按任务拆分,避免一开始就设计全任务类型擦除。
P4 明确不做:
_m_ 成员;std::any 掩盖类型边界;每个阶段至少执行:
python scripts/check_consistency.py
python scripts/gen_openapi.py --check
clang-format --dry-run --Werror <changed files>
涉及代码时执行:
tests-only check
tests-only -Werror check
CPU profile full check
full GPU -Werror build
相关 contract tests
完整 GPU golden
TSAN
ASan/UBSan
最终验收:
CI 全绿
CodeQL 全绿
CPU profile 全绿
full Werror 全绿
完整 GPU golden 全绿
无 golden drift
模型 catalog 覆盖率 100%
新增普通模型接触点 <= 5
模型源码中手写 memcpy 显著减少
脚手架生成后可直接编译
新模型文档路径完整
| 指标 | 当前目标 |
|---|---|
| 新增普通模型修改文件数 | 从约 8-10 个降到 3-5 个 |
模型手写 std::memcpy |
仅保留特殊模型,普通模型为 0 |
| 手写 CHW/HWC 打包 | 普通图像模型为 0 |
| factory/server 重复代码 | 减少 60% 以上 |
| contract test 样板 | 由模板自动生成异常矩阵 |
| golden case 样板 | 每个 case 降低到一次注册 |
| 新模型初始编译失败成本 | 脚手架生成后即可编译 |
| 新开发者需要理解的 backend 细节 | 接近 0 |
| 风险 | 控制措施 |
|---|---|
| 过度抽象 | 每阶段只解决已量化问题,不做万能框架 |
| 行为漂移 | 每个模型族迁移后跑 golden |
| API 复杂化 | 新 API 必须比旧代码更短、更明确 |
| 编译时间上升 | toolkit 放 .cpp,避免 header-only 大实现 |
| catalog 初始化顺序问题 | 显式 catalog,不做全局静态注册 |
| 类型擦除过度 | catalog 按任务拆分,保留模板边界 |
| 测试迁移风险 | golden case 名称和文件保持不变 |
| 文档漂移 | 文档示例必须来自可编译代码或测试 |
1. Phase 0: metrics
2. Phase 1: runtime toolkit
3. Phase 2: catalog
4. Phase 3: scaffolder
5. Phase 4: IO split
6. Phase 5: test registry
7. Phase 6: migrate simple families
8. Phase 7: multi-session template
9. Phase 8: docs and adoption guide
建议拆分 PR:
PR 1: P4 metrics + runtime toolkit
PR 2: model catalog
PR 3: scaffolder
PR 4: IO split
PR 5: golden/contract test registry
PR 6-10: model family migration
PR 11: multi-session template
PR 12: developer docs
P4 聚焦模型开发体验。生产化阶段聚焦:
推荐执行顺序:
P4 先落地基础 toolkit 和 catalog
后续生产化阶段再基于稳定 catalog 做全量模型 benchmark
这样 benchmark 可以自动遍历 catalog,不需要再维护一份模型清单。