mortred_model_server

如何添加新的DL模型

本页是 how_to_add_new_model.md 的中文版, 讲解统一后端层的结构与生命周期。按任务一步步操作的中文指南见 model-developer-guide.md(英文,路径导向)。

当前实现基于 BackendCvModel。旧版文档描述的 “自定义输入 → 模型内部输入 → session → 模型内部输出 → 自定义输出” 五层转换已经不存在,本页已按现行 API 重写。

Step 0:用脚手架生成样板(推荐)

# 查看支持的任务
python scripts/new_model.py --list-tasks

# 预览将要生成的文件,不落盘
python scripts/new_model.py --task classification \
    --name efficientnet --class EfficientNet \
    --backend mnn --dry-run

# 实际生成
python scripts/new_model.py --task classification \
    --name efficientnet --class EfficientNet \
    --backend mnn

会生成 5 个文件:

文件 需要你填的内容
src/models/classification/<file>.h 类骨架,已继承 BackendCvModel
src/models/classification/<file>.inl preprocess / postprocess / on_init 三个钩子
conf/model/classification/<name>/<name>_config.toml [SECTION] + .backend + .params
test/<file>_output_contract_unittest.cc 输出契约测试
docs/models/classification/<name>.md 文档骨架

此时模型可以直接编译,所有钩子返回 MODEL_NOT_IMPLEMENTED, 半成品不会被误当成能跑的模型启动。 src/models/object_detection/rtdetr_detector.* 是一个已入库的生成样例, 同时充当模板可编译性的哨兵。

脚手架还会打印两段它刻意不自动应用的片段:catalog 条目和测试目标注册。

Step 1:选择 IO 类型

IO 类型在 src/models/io/ 下,每个任务一个头文件。 common_input.h 存放共享输入(mat_input / file_input / base64_input / pair_mat_input),各任务头文件存放自己的 std_*_output

只 include 你需要的那个任务头。旧的 model_io_define.h 仍然可用, 但它是一个会把所有任务都拉进来的兼容聚合头。

可加载的图像输入走默认的 prepare_inputs 路径; 任务默认输出(std_*_output)是推荐选择。

Step 2:实现模型类

先读这几个参考实现:

template <typename INPUT, typename OUTPUT>
class MyModel : public jinq::models::BackendCvModel<INPUT, OUTPUT> {
  public:
    MyModel() : jinq::models::BackendCvModel<INPUT, OUTPUT>("MY_MODEL") {}

  private:
    // 图像 -> 命名输入张量(图像模型必须实现)
    std::vector<jinq::models::backend::NamedTensor> preprocess(const cv::Mat &image) override;

    // 命名输出张量 + 请求几何 -> 任务输出
    jinq::common::StatusCode postprocess(const std::vector<jinq::models::backend::NamedTensor> &outputs,
                                         const jinq::models::backend::InferenceContext &context,
                                         OUTPUT &output) override;

    // 可选:读取 [MY_MODEL.params] 中的模型专属键
    jinq::common::StatusCode on_init(const toml::table &params) override;
};

要点:

Step 3:写配置

[MY_MODEL]
[MY_MODEL.backend]
type = "mnn"                # mnn | onnx | tensorrt
model_file_path = "../weights/my_model/model.mnn"
device = "gpu"             # cpu | gpu
threads = 4
gpu_mem_limit_mb = 2048     # 仅 onnx+cuda;0 = 不限制;默认 2048
input_layout = "nhwc"       # 仅 mnn: auto | nhwc | nchw

[MY_MODEL.params]
score_threshold = 0.25

完整键参考见 about_model_configuration.md。 旧的 BACKEND_DICT / XXX_TRT / XXX_ONNX / XXX_MNN 三段式配置已经移除; 用 scripts/migrate_model_config.py 迁移 (先 --dry-run,CI 里用 --check)。

Step 4:在任务 catalog 里注册

每个任务在 src/factory/<task>_task.h 里有一个显式 catalog。 新增一个被服务的模型现在是一行加一个 creator—— 没有手写的 server 注册 lambda,也没有复制的 CvServerSpec 块:

// src/factory/my_task.h
template <typename INPUT, typename OUTPUT>
std::unique_ptr<BaseAiModel<INPUT, OUTPUT>> create_my_model(const std::string &name) {
    (void)name;
    return std::make_unique<MyModel<INPUT, OUTPUT>>();
}

using Output = jinq::models::io_define::my_task::std_my_task_output;
using Entry = jinq::factory::cv_catalog::CvModelEntry<Output>;

inline const std::vector<Entry> &catalog() {
    static const std::vector<Entry> entries = {
        Entry{"MY_MODEL", "My model display name", "MY_MODEL_SERVER",
              &create_my_model<jinq::server::Base64Input, Output>,
              &jinq::server::response::fill_my_task},
    };
    return entries;
}

剩下的交给 factory::cv_catalog::create_server(catalog(), "MY_MODEL", server_name): 它在 ServerFactory<BaseAiServer> 里注册 creator, 并构建通用的 CvModelServer<Output>

刻意存在两种形态:

一个任务有多个输出契约时,按契约拆成多个 typed catalog, 不要合并成一个类型擦除的列表—— 见 obj_detection_task.h 里的 catalog()face_catalog()

test/model_catalog_unittest.cc 会在 catalog 行引用了不存在的 TOML section 或 model_config_file_path,或 model / server section 跨任务重复时报错。

Step 5:验证

cmake --preset full && cmake --build --preset full
scripts/run_tests.sh build/full -R model_golden_test --output-on-failure

model_golden_registry.h 里的一个宏 注册 golden 用例(完整列表和两命令基线流程见 开发者指南)。 用 golden_drift_check.py 证明重构没有改变任何数值, 用 POSTPROCESS_CONTRACT_TEST 覆盖七项拒绝矩阵。

容差按任务定:检测用 score / box-IoU,稠密输出用指纹 diff。

参考