mortred_model_server

P4:Modern Model Developer Experience 改造计划

历史计划,不是用户手册。 现行接入步骤见 how_to_add_new_model.zh-cn.mdmodel-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

1. 背景与目标

P0 到 P2 已完成模型层正确性基础:

当前短板不再是正确性,而是开发体验:

P4 的目标是:

让新增普通 CV 模型时,开发者只关注模型本身的 preprocess、参数和 decode;生命周期、契约、注册、测试骨架和文档骨架由基础设施提供。

2. 设计原则

2.1 保持简单

P4 不引入重型框架,不做动态插件系统,不追求“一切皆配置”。优先提供:

2.2 保持既有正确性契约

所有新 API 必须构建在现有基础设施上:

P4 不能降低 P0/P2 已达到的安全边界。

2.3 不隐藏复杂 decode

模型差异大的 decode 逻辑保留在各模型中。公共层只承接确定性重复逻辑:

不把 YOLO、SAM、diffusion 等差异强行塞入通用基类。

2.4 不立即升级 C++20

当前项目基线为 C++17,兼容性优先。P4 不依赖:

可以在 C++17 内通过小型 value type、builder 和静态断言达到目标。

3. 当前新增模型路径

当前新增一个模型通常需要接触以下位置:

序号 位置 内容
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 文档 模型说明与接入说明

其中真正模型相关的只有:

其余流程应模板化或注册化。

4. P4 目标开发路径

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

普通模型不再要求修改:

5. 分阶段改造计划

Phase 0:基线与度量

任务

  1. 固化当前模型层指标:
    • 模型数量;
    • 新增模型平均修改文件数;
    • std::memcpy 出现次数;
    • 手写 convert_to_chw_vec 次数;
    • 手写 output contract 次数;
    • factory/server 重复代码规模;
    • tests-only 与 full build 编译时间。
  2. 写一个开发体验基准文档。
  3. 明确每阶段必须保持绿色:
    • tests-only;
    • -Werror
    • CPU profile;
    • full GPU golden;
    • sanitizers。

交付

docs/model-developer-experience-metrics.zh-cn.md

验收

Phase 1:Model Runtime Toolkit

新增:

src/models/backend/model_runtime.h
src/models/backend/model_runtime.cpp
test/model_runtime_unittest.cc

5.1 ImagePipeline

目标 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 边界。

5.2 OutputReader

目标 API:

auto scores = OutputReader(outputs, "output")
                  .f32()
                  .shape({1, class_count})
                  .finite();

if (!scores.ok()) {
    return scores.status();
}

底层复用:

必须保留语义:

missing output -> MODEL_EMPTY_OUTPUT
dtype/rank/shape/buffer 错误 -> MODEL_OUTPUT_CONTRACT_FAILED
NaN/Inf -> MODEL_OUTPUT_CONTRACT_FAILED

5.3 ParamReader

目标 API:

auto result = ParamReader(params, "MY_MODEL")
                  .get("score_threshold", &score_threshold)
                  .min(0.0)
                  .max(1.0);
if (!result.ok()) {
    return result.status();
}

统一能力:

5.4 SessionIoValidator

目标 API:

auto input_info = SessionIoValidator(session())
                      .input("images")
                      .dtype(DType::F32)
                      .rank(4)
                      .nchw()
                      .channels(3)
                      .validate();

用于替代各模型 on_init 中重复的 session input shape 检查。

Phase 1 验收

Phase 2:模型目录与注册治理

新增:

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,统一生成:

原手写函数保留兼容包装,逐步废弃。

新增:

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

Phase 2 验收

Phase 3:脚手架生成器

新增:

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

生成:

  1. 模型类骨架;
  2. preprocess TODO;
  3. output contract TODO;
  4. decode TODO;
  5. TOML 配置;
  6. catalog entry 提示;
  7. contract test;
  8. golden 文件占位提示;
  9. 模型文档骨架;
  10. 验证命令。

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              # 任务元数据表

与原方案的差异:

Phase 3 验收

Phase 4:模型 IO 拆分

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 不变

与原方案的差异:

实测数据(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 验收

Phase 5:测试基础设施注册化

新增:

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 验收

Phase 6:按模型族渐进迁移

迁移顺序:

  1. classification;
  2. matting;
  3. scene segmentation;
  4. enhancement;
  5. OCR;
  6. object detection;
  7. feature point;
  8. depth;
  9. FastSAM;
  10. CLIP;
  11. diffusion。

选择该顺序的原因:

每个模型族迁移:

旧 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 通过

Phase 7:多 session 模型模板

适用:

新增:

src/models/backend/session_group.h

目标 API:

SessionGroup sessions;
sessions.declare("encoder", "encoder_backend");
sessions.declare("decoder", "decoder_backend");

统一处理:

Phase 7 验收

Phase 8:文档与开发者引导

更新:

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 验收

6. 代码设计草案

6.1 RuntimeResult

template <typename T>
struct RuntimeResult {
    StatusCode status = StatusCode::OK;
    std::string error;
    T value{};

    bool ok() const { return status == StatusCode::OK; }
};

要求:

6.2 ImagePipeline

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;
};

约束:

6.3 OutputReader

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;
};

注意:

6.4 ParamReader

class ParamReader {
  public:
    ParamReader(const toml::table &params, 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;
};

6.5 ModelEntry

由于现有 BaseAiModel 是模板类型,catalog 初期按任务拆分,避免一开始就设计全任务类型擦除。

7. 明确不做的事情

P4 明确不做:

  1. 不引入 Spring 式 DI 容器;
  2. 不做运行时 C++ 插件加载;
  3. 不做完全 YAML/DSL 化模型定义;
  4. 不把所有 decode 抽到基类;
  5. 不引入跨任务全局万能 Model 基类;
  6. 不机械重命名全部 _m_ 成员;
  7. 不为追求语法现代而升级 C++20;
  8. 不在 P4 中混入性能优化或部署改造;
  9. 不用大规模 std::any 掩盖类型边界;
  10. 不牺牲现有 golden 稳定性换取代码量下降。

8. 测试与验收矩阵

每个阶段至少执行:

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 显著减少
脚手架生成后可直接编译
新模型文档路径完整

9. 量化目标

指标 当前目标
新增普通模型修改文件数 从约 8-10 个降到 3-5 个
模型手写 std::memcpy 仅保留特殊模型,普通模型为 0
手写 CHW/HWC 打包 普通图像模型为 0
factory/server 重复代码 减少 60% 以上
contract test 样板 由模板自动生成异常矩阵
golden case 样板 每个 case 降低到一次注册
新模型初始编译失败成本 脚手架生成后即可编译
新开发者需要理解的 backend 细节 接近 0

10. 风险与控制

风险 控制措施
过度抽象 每阶段只解决已量化问题,不做万能框架
行为漂移 每个模型族迁移后跑 golden
API 复杂化 新 API 必须比旧代码更短、更明确
编译时间上升 toolkit 放 .cpp,避免 header-only 大实现
catalog 初始化顺序问题 显式 catalog,不做全局静态注册
类型擦除过度 catalog 按任务拆分,保留模板边界
测试迁移风险 golden case 名称和文件保持不变
文档漂移 文档示例必须来自可编译代码或测试

11. 建议实施顺序

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

12. 与生产化阶段的关系

P4 聚焦模型开发体验。生产化阶段聚焦:

推荐执行顺序:

P4 先落地基础 toolkit 和 catalog
后续生产化阶段再基于稳定 catalog 做全量模型 benchmark

这样 benchmark 可以自动遍历 catalog,不需要再维护一份模型清单。