犀牛鸟 2026 研究笔记 estelledc.github.io

Ch18: 算子抽象——设计哲学的分水岭

Part 4: 核心抽象深度对比 前置章节:Ch17: 数据容器——同一个问题的六种回答 后续章节:Ch19: 推理调度——从按需递归到图分区


核心问题

推理引擎的工作是执行神经网络中的每一层计算——卷积、矩阵乘法、激活函数、归一化等等。每种计算在引擎内部被抽象为一个”算子”(Operator)。但”算子”到底应该是什么?它应该只负责计算,还是也负责推导输出形状?一个算子应该是一个整体,还是应该分成”描述”和”实现”两部分?当同一个算子有 CPU float32、CPU int8、GPU OpenCL 三种实现时,怎么组织这些实现的关系?

这些问题的回答定义了一个框架的”灵魂”——算子抽象是整个推理引擎中最能体现设计哲学的地方。六个框架在这个问题上的分歧之大,甚至可以说它们根本不是在做”同一件事”。


餐厅菜单的类比

想象六家不同风格的餐厅,它们都提供”番茄炒蛋”这道菜,但在菜单设计、后厨组织和点单流程上截然不同:

ncnn 像一家家庭小餐馆——每道菜由一位厨师从头到尾完成,从估算用料到炒菜出锅都是他一个人(shape 推导和 compute 耦合在 Layer 类中)。菜单是开店前就印好的固定菜单(编译时注册表),不支持运营中新增菜品。

MNN 像一家米其林餐厅的厨房——有三条并行的工作线:一条是前台菜单(Op,用 FlatBuffers 声明),一条是真正做菜的厨师团队(Execution,分 onResize 备菜和 onExecute 炒菜两步),还有一条是”万能替补”——如果某个厨师不会做某道特殊菜,替补可以用基本技法组合出来(GeometryComputer 降级)。三层架构让这家餐厅能应对几乎任何菜品需求。

ORT 像一个美食广场的管理方——它不雇佣任何厨师,而是制定了一套标准的摊位规格和点单流程(OpKernel + KernelRegistry)。每个入驻的商户(Execution Provider)按照统一接口注册自己的菜品实现。而且菜单有版本号(opset 版本化注册),去年的”番茄炒蛋 v1”和今年改良过的”番茄炒蛋 v13”可以同时存在。

TNN 像一家连锁餐厅——采用总部和门店分离的管理模式。总部(BaseLayer)负责菜品标准化、用料估算(shape 推导),各门店(LayerAcc)负责按当地口味(CPU/GPU/DSP 等不同硬件)实际烹饪(compute)。开新门店时由”区域经理”(Device::CreateLayerAcc)来指派厨师。

Paddle-Lite 像一家自助餐厅——食材台上的每道菜都贴着三个标签:来自哪个产地(Target)、什么级别的用料(Precision)、什么摆盘风格(Layout)。顾客点菜时,系统按这三个维度自动匹配最合适的那份(三维 kernel 派发)。

MACE 像一家融合料理餐厅——主厨(Operation)不直接上手,而是带着一群”专项助手”(Delegator),每个助手精通特定风格的烹饪。当需要做一道菜时,主厨根据五个维度(运行时、数据类型、通道数等)从助手库中惰性选择最合适的那位。但如果菜品是”整桌宴席”(Hexagon 整图下推),就绕过助手体系直接外包给宴会厅。

类比到此为止,让我们看看每个框架的实际代码结构。


ncnn:Layer 单类继承,简单到极致

ncnn 的算子抽象是六个框架中最简单的——只有一个类 Layer,所有具体算子都直接继承它。没有 Op/Kernel 分离,没有工厂模式,没有注册表查找——一个类搞定一切。

Layer 基类

// ncnn/src/layer.h(简化版)
class Layer {
public:
    // 加载算子的参数和权重
    virtual int load_param(const ParamDict& pd);
    virtual int load_model(const ModelBin& mb);

    // 形状推导 + 计算耦合在一起
    virtual int forward(const Mat& bottom_blob, Mat& top_blob,
                        const Option& opt) const;
    virtual int forward_inplace(Mat& bottom_top_blob,
                                const Option& opt) const;
    virtual int forward(const std::vector<Mat>& bottom_blobs,
                        std::vector<Mat>& top_blobs,
                        const Option& opt) const;

    std::string type;
    int typeindex;
    int one_blob_only;
    int support_inplace;
};

注意关键设计决策:shape 推导和 compute 是耦合的forward 方法既负责确定输出的大小(通过直接创建 top_blob),也负责执行实际计算。没有单独的”推导输出形状”阶段。

具体算子与平台特化

// ncnn/src/layer/convolution.h
class Convolution : public Layer {
public:
    int load_param(const ParamDict& pd) override;
    int load_model(const ModelBin& mb) override;
    int forward(const Mat& bottom_blob, Mat& top_blob,
                const Option& opt) const override;

    int num_output;
    int kernel_w, kernel_h;
    int stride_w, stride_h;
    int pad_left, pad_right, pad_top, pad_bottom;
    Mat weight_data;
    Mat bias_data;
};

// ARM 平台特化版本:继承通用版,覆盖 forward
// ncnn/src/layer/arm/convolution_arm.h
class Convolution_arm : public Convolution {
public:
    int forward(const Mat& bottom_blob, Mat& top_blob,
                const Option& opt) const override;
    Mat weight_data_tm; // ARM NEON 预打包权重
};

编译时注册表

ncnn 的算子注册在编译时完成——通过宏定义生成一个静态的创建函数数组:

// 编译时生成的注册代码(简化版)
static const layer_registry_entry layer_registry[] = {
    {"Convolution", Convolution_final_layer_creator},
    {"ReLU",        ReLU_final_layer_creator},
    {"Pooling",     Pooling_final_layer_creator},
};

// _final_layer_creator 根据编译选项选择最优实现
// 编译了 ARM NEON → Convolution_arm
// 编译了 AVX-512 → Convolution_x86_avx512
// 啥也没有     → Convolution(通用版)

覆盖链是:通用 → ARM/x86 → AVX2/AVX-512/NEON → Vulkan。编译时就确定了一条固定的覆盖路径。

设计取舍

ncnn 的算子抽象是六个框架中最简单的,也是最”死板”的。简单意味着代码路径清晰、开发者容易理解、编译优化友好。死板意味着:不能在运行时动态选择后端(编译时就定了),不能轻松添加新硬件后端(需要修改注册表生成逻辑),shape 推导和 compute 耦合(不能单独做形状推导而不执行计算)。


MNN:三层架构的精密分工

MNN 的算子抽象是六个框架中最精细的——分为 Op 声明层、Execution 执行层、GeometryComputer 降级层三个层次。

第一层:Op(FlatBuffers 声明)

MNN 的算子元数据用 FlatBuffers schema 定义——意味着算子参数可以直接从模型文件中零拷贝读取:

// MNN/schema/default/MNN.fbs(简化版)
table Convolution2DCommon {
    padX: int;
    padY: int;
    kernelX: int = 1;
    kernelY: int = 1;
    strideX: int = 1;
    strideY: int = 1;
    dilateX: int = 1;
    dilateY: int = 1;
    outputCount: int;
    group: int = 1;
    padMode: PadMode = CAFFE;
    relu: bool = false;
    relu6: bool = false;
}

Op 层只是”声明”——描述算子类型和参数,不包含计算逻辑。

第二层:Execution(onResize + onExecute)

Execution 是实际执行计算的类,核心在于把”准备”和”执行”分成两步:

// MNN/source/core/Execution.hpp(简化版)
class Execution {
public:
    Execution(Backend* backend) : mBackend(backend) {}

    // 第一步:根据输入形状做准备(分配工作空间、重排权重等)
    virtual ErrorCode onResize(const std::vector<Tensor*>& inputs,
                                const std::vector<Tensor*>& outputs);

    // 第二步:执行实际计算
    virtual ErrorCode onExecute(const std::vector<Tensor*>& inputs,
                                 const std::vector<Tensor*>& outputs);

protected:
    Backend* mBackend;
};

onResizeonExecute 的分离支持动态形状推理。当输入形状变化时只需重新 onResize,不变时可跳过——连续推理相同尺寸输入时节省大量初始化开销。

// CPU Convolution 实现示例
class CPUConvolution : public Execution {
public:
    ErrorCode onResize(const std::vector<Tensor*>& inputs,
                        const std::vector<Tensor*>& outputs) override {
        auto input = inputs[0];
        // 根据输入形状选最优算法:Im2Col+GEMM? Winograd? 直接卷积?
        mImpl = selectBestImpl(input->shape(), mCommon);
        // 分配工作空间
        mWorkspace.reset(Tensor::createDevice<float>(
            {mImpl->workspaceSize()}));
        mBackend->onAcquireBuffer(mWorkspace.get(), Backend::DYNAMIC);
        return NO_ERROR;
    }

    ErrorCode onExecute(const std::vector<Tensor*>& inputs,
                         const std::vector<Tensor*>& outputs) override {
        return mImpl->execute(inputs[0], outputs[0], mWorkspace.get());
    }

private:
    std::shared_ptr<ConvolutionImpl> mImpl;
    std::shared_ptr<Tensor> mWorkspace;
};

第三层:GeometryComputer(降级)

GeometryComputer 是 MNN 最独特的设计——把复杂算子”降级”成若干基本算子的组合:

// 例:LayerNorm 的降级实现(简化版)
class GeometryLayerNorm : public GeometryComputer {
    bool onCompute(const Op* op,
                   const std::vector<Tensor*>& inputs,
                   const std::vector<Tensor*>& outputs,
                   Context& context,
                   CommandBuffer& buffer) const override {
        // LayerNorm = Mean + Sub + Square + Mean + Add(eps) + Sqrt + Div + Mul + Add
        // 每一步都是基本算子,后端只要支持这些基本算子就行
        // 中间张量用 MEMORY_VIRTUAL 避免内存分配
        return true;
    }
};

GeometryComputer 的意义在于:当一个新后端只实现了 20 个基本算子时,通过降级就能”间接支持”上百个复杂算子。新后端不需要为每个算子都写专门实现。

三层协作流程

引擎执行一个算子时的查找顺序:

  1. 当前 Backend 有该算子的 Execution 实现?有则直接用(最高性能)
  2. 没有,GeometryComputer 有该算子的降级规则?有则降级成基本算子组合
  3. 都没有,报错

这种”三层兜底”机制让常用算子有手写的高性能实现,罕见算子通过降级来支持。


ONNX Runtime:OpKernel 与 opset 版本化

ORT 的算子抽象围绕两个核心概念构建:OpKernel 和 KernelRegistry,并引入了opset 版本化这个独特概念。

OpKernel 基类

// onnxruntime/core/framework/op_kernel.h(简化版)
class OpKernel {
public:
    explicit OpKernel(const OpKernelInfo& info) : info_(info) {}

    // 唯一的核心方法:执行计算
    virtual Status Compute(OpKernelContext* context) const = 0;

    const OpKernelInfo& Info() const { return info_; }

protected:
    OpKernelInfo info_;
};

和 ncnn Layer 对比:OpKernel 只有一个 Compute 方法,参数加载的职责交给了 OpKernelInfo,OpKernel 自身只关注计算。

opset 版本化注册

ONNX 标准中同一个算子在不同版本的规范中可能有不同语义。ORT 通过版本化注册来管理:

// opset 5 到 13 的 Reshape
ONNX_CPU_OPERATOR_VERSIONED_KERNEL(
    Reshape, 5, 13,
    KernelDefBuilder()
        .TypeConstraint("T", DataTypeImpl::AllFixedSizeTensorTypes()),
    Reshape_5_13
);

// opset 14 及以后的 Reshape
ONNX_CPU_OPERATOR_KERNEL(
    Reshape, 14,
    KernelDefBuilder()
        .TypeConstraint("T", DataTypeImpl::AllFixedSizeTensorTypes())
        .InputMemoryType(OrtMemTypeCPUInput, 1),
    Reshape_14
);

版本化注册是 ORT 独有的——其他五个框架都没有 opset 版本概念。好处是完美兼容新旧模型:opset 7 和 opset 18 的模型可以用同一个引擎运行。

KernelRegistry:分层查找

class KernelRegistry {
public:
    Status Register(KernelCreateInfo&& create_info);

    // 根据节点信息查找匹配的 kernel
    Status TryFindKernel(const Node& node,
                          ProviderType exec_provider,
                          const KernelCreateInfo** out) const;

private:
    // key: (op_name, domain, exec_provider)
    KernelCreateMap kernel_creator_fn_map_;
};

每个 ExecutionProvider 有自己的注册表,查找时依次搜索:EP 注册表 → 全局注册表。

具体 kernel 实现

class Reshape_14 : public OpKernel {
public:
    explicit Reshape_14(const OpKernelInfo& info) : OpKernel(info) {}

    Status Compute(OpKernelContext* context) const override {
        const Tensor* input = context->Input<Tensor>(0);
        const Tensor* shape_tensor = context->Input<Tensor>(1);

        auto shape_data = shape_tensor->Data<int64_t>();
        TensorShape output_shape(shape_data, shape_tensor->Shape().Size());

        Tensor* output = context->Output(0, output_shape);
        if (input->DataRaw() != output->MutableDataRaw()) {
            memcpy(output->MutableDataRaw(), input->DataRaw(),
                   input->SizeInBytes());
        }
        return Status::OK();
    }
};

设计取舍

ORT 的算子抽象是最”标准化”的。opset 版本化确保向后兼容性,分层 KernelRegistry 让第三方后端独立注册 kernel。代价是间接层较多(版本匹配 + 类型约束匹配 + EP 查找),以及同一算子多版本实现的代码膨胀。


TNN:BaseLayer + LayerAcc 双层分离

TNN 的算子抽象介于 ncnn 的极简和 MNN 的精细之间——把算子分成 BaseLayer(shape 推导)和 LayerAcc(硬件计算)两层。

BaseLayer:硬件无关的形状推导

// TNN/source/tnn/layer/base_layer.h(简化版)
class BaseLayer {
public:
    virtual Status InferOutputShape(bool ignore_error = false);
    virtual Status InferOutputDataType();

    virtual Status Init(Context* context, LayerParam* param,
                         LayerResource* resource,
                         std::vector<Blob*>& inputs,
                         std::vector<Blob*>& outputs,
                         AbstractDevice* device);
    virtual Status Forward();

protected:
    LayerParam* param_ = nullptr;
    LayerResource* resource_ = nullptr;
    std::vector<Blob*> input_blobs_;
    std::vector<Blob*> output_blobs_;
    std::shared_ptr<LayerAcc> layer_acc_; // 指向硬件特定实现
};

LayerAcc:硬件特定的计算

class LayerAcc {
public:
    virtual Status Init(Context* context, LayerParam* param,
                         LayerResource* resource,
                         const std::vector<Blob*>& inputs,
                         const std::vector<Blob*>& outputs);
    virtual Status Reshape(const std::vector<Blob*>& inputs,
                            const std::vector<Blob*>& outputs);
    virtual Status Forward(const std::vector<Blob*>& inputs,
                            const std::vector<Blob*>& outputs);
};

Device::CreateLayerAcc 工厂方法

BaseLayer 初始化时通过 Device 工厂创建对应硬件的 LayerAcc:

Status BaseLayer::Init(Context* context, ..., AbstractDevice* device) {
    // 1. 推导输出形状(硬件无关,只写一次)
    InferOutputShape();

    // 2. 让 Device 创建硬件特定的 LayerAcc
    layer_acc_ = device->CreateLayerAcc(param_->type,
                                         input_blobs_, output_blobs_);

    // 3. 初始化 LayerAcc
    layer_acc_->Init(context, param_, resource_,
                      input_blobs_, output_blobs_);
    return TNN_OK;
}

// 前向计算直接委托
Status BaseLayer::Forward() {
    return layer_acc_->Forward(input_blobs_, output_blobs_);
}

和 ncnn 的对比

ncnn 中 Convolution 和 Convolution_arm 通过继承连接——如果要同时支持 ARM、x86、OpenCL 三个后端,需要建继承树,shape 推导代码可能在基类和子类间重复。TNN 把 shape 推导提取到 BaseLayer 中(只写一次),硬件计算放到 LayerAcc 中(每个后端各写一套),通过工厂方法在运行时组装。添加新后端更清晰——写一套 LayerAcc,注册到 Device。

设计取舍

TNN 的双层设计是 ncnn 单层设计的”结构化升级”——解决了 shape/compute 耦合问题,多后端支持更有条理。但相比 MNN 三层架构,缺少 GeometryComputer 这样的万能降级机制——如果某个后端缺少某个算子的 LayerAcc,就直接报错,没有自动降级的兜底。


Paddle-Lite:OpLite + KernelLite 三维派发

Paddle-Lite 的算子抽象最大的特色是三维 kernel 派发——每个 kernel 都在 Target(硬件)、Precision(精度)、Layout(布局) 三个维度上精确标定位置。

OpLite:算子声明

// paddle-lite/lite/core/op_lite.h(简化版)
class OpLite {
public:
    // 初始化:从 Scope 中获取输入输出变量
    virtual bool CheckShape() const;
    virtual bool InferShapeImpl() const;

    // 关键:附着到特定 kernel
    virtual bool AttachImpl(const cpp::OpDesc& op_desc,
                             lite::Scope* scope);

    // 获取所有可选 kernel 的 Place 列表
    virtual std::vector<Place> GetKernelPlaces() const;

protected:
    std::shared_ptr<KernelLite> kernel_; // 被选中的 kernel
    lite::Scope* scope_;
};

KernelLite:三维坐标标定

// paddle-lite/lite/core/kernel.h(简化版)
class KernelLite {
public:
    virtual void Run() = 0;

    // 这个 kernel 的"坐标":属于哪个硬件、什么精度、什么布局
    Place place() const { return place_; }

    // kernel 的别名(用于调试和选择)
    virtual std::string name() const = 0;

protected:
    Place place_; // Place = {TargetType, PrecisionType, DataLayoutType}
};

// Place 结构体:三维坐标
struct Place {
    TargetType target;       // Host / X86 / ARM / OpenCL / Metal / ...
    PrecisionType precision; // Float / Int8 / FP16 / Int32 / ...
    DataLayoutType layout;   // NCHW / NHWC / NC4HW4 / Any / ...
};

三维派发机制

当调度器需要为一个 Op 选择 kernel 时,它会根据当前的”有效 Place”列表进行匹配:

// 简化版的 kernel 选择逻辑
KernelLite* SelectBestKernel(OpLite* op,
                              const std::vector<Place>& valid_places) {
    auto candidates = op->GetKernelPlaces();

    // 按优先级匹配
    for (const auto& place : valid_places) {
        for (auto& kernel : candidates) {
            if (kernel.place().target == place.target &&
                kernel.place().precision == place.precision &&
                kernel.place().layout == place.layout) {
                return &kernel;  // 精确匹配
            }
        }
    }

    // 降级匹配:放宽 layout 要求
    for (const auto& place : valid_places) {
        for (auto& kernel : candidates) {
            if (kernel.place().target == place.target &&
                kernel.place().precision == place.precision) {
                return &kernel;  // 忽略 layout 差异
            }
        }
    }

    return nullptr; // 没找到
}

注册宏

// 注册一个 ARM float32 NCHW 的 Convolution kernel
REGISTER_LITE_KERNEL(
    conv2d,                         // 算子名
    kARM,                           // Target
    kFloat,                         // Precision
    kNCHW,                          // Layout
    arm::ConvCompute<PRECISION(kFloat)>, // 实现类
    def                             // kernel 别名
)
.BindInput("Input", {LiteType::GetTensorTy(TARGET(kARM),
            PRECISION(kFloat), DATALAYOUT(kNCHW))})
.BindInput("Filter", {LiteType::GetTensorTy(TARGET(kARM),
            PRECISION(kFloat), DATALAYOUT(kNCHW))})
.BindOutput("Output", {LiteType::GetTensorTy(TARGET(kARM),
            PRECISION(kFloat), DATALAYOUT(kNCHW))})
.Finalize();

// 同一个算子,ARM int8 的实现
REGISTER_LITE_KERNEL(
    conv2d, kARM, kInt8, kNCHW,
    arm::ConvCompute<PRECISION(kInt8)>,
    int8_out
)
// ...
.Finalize();

MIR pass 中的自动类型转换

Paddle-Lite 的三维派发不是孤立的——它和 MIR(Model Intermediate Representation)优化 pass 紧密配合。当输入 Tensor 的 Place 和选中 kernel 要求的 Place 不匹配时,MIR 会自动插入类型转换算子(比如 layout_cast、precision_cast)来桥接差异。这个过程在 Ch21 图优化中会详细讲。

设计取舍

Paddle-Lite 的三维派发是六个框架中最”显式”的 kernel 选择机制。每个 kernel 的能力边界被精确标定在三维空间中的一个点上,调度器的工作就是在这个空间中找到最佳匹配点。好处是清晰可调试——你可以精确知道某个 Op 在某个硬件+精度+布局组合下会用哪个 kernel。代价是注册的代码量巨大——每个算子在每个硬件×精度×布局组合下都需要一条注册宏,算子数和后端数的笛卡尔积让注册表膨胀很快。


MACE:Operation + Delegator 五维惰性选择

MACE 的算子抽象有一个独特的设计——Delegator 体系。Operation(算子)不直接包含计算逻辑,而是把实际计算委托给 Delegator,Delegator 的选择基于五个维度,而且是惰性的——直到第一次执行时才确定用哪个 Delegator。

Operation 基类

// mace/core/ops/operation.h(简化版)
class Operation {
public:
    explicit Operation(OpConstructContext* context);
    virtual ~Operation() = default;

    // 推导输出形状
    virtual MaceStatus InferOutputShape(
        const std::vector<const Tensor*>& inputs,
        std::vector<Tensor*>* outputs) = 0;

    // 初始化(分配资源)
    virtual MaceStatus Init(OpInitContext* context);

    // 前向计算
    virtual MaceStatus Run(OpContext* context) = 0;

protected:
    // Delegator:实际计算的委托对象
    std::unique_ptr<delegator::DelegatorBase> delegator_;
    OpConditionContext condition_context_;
};

Delegator:五维惰性选择

Delegator 的选择基于五个维度:

// mace/ops/delegator/delegator_registry.h(简化版)
struct DelegatorParam {
    RuntimeType runtime_type;  // CPU / GPU / APU / ...
    DataType data_type;        // FLOAT / HALF / INT8 / ...
    ImplType impl_type;        // REF / NEON / ...
    // 加上算子特定的维度,如:
    int input_channels;        // 输入通道数(决定用哪种卷积实现)
    int kernel_size;           // 卷积核大小
};

// 注册 Delegator 的宏
MACE_REGISTER_DELEGATOR(
    registry,
    Conv2dDelegator,                    // Delegator 类
    delegator::Conv2dParam,             // 参数类型
    MACE_DELEGATOR_KEY(                 // 五维 key
        Conv2d,                         // 算子名
        RuntimeType::RT_CPU,            // 运行时
        float,                          // 数据类型
        ImplType::NEON                  // 实现方式
    )
);

“惰性选择”意味着 Delegator 不在 Operation 构造时创建,而是在第一次 Run 时才根据输入的实际特征(通道数、核大小等)查注册表选择最优实现:

MaceStatus Conv2dOp::Run(OpContext* context) {
    if (delegator_ == nullptr) {
        // 第一次执行:根据实际输入选择 Delegator
        DelegatorParam param;
        param.runtime_type = context->runtime_type();
        param.data_type = inputs[0]->dtype();
        param.input_channels = inputs[0]->dim(1);
        param.kernel_size = filter->dim(2);

        delegator_ = DelegatorRegistry::Global()->Create(param);
    }
    return delegator_->Compute(inputs, outputs);
}

OpenCL 不走 Delegator

MACE 的 Delegator 体系有一个重要的例外——OpenCL 后端不走 Delegator。OpenCL 的算子实现直接在 Operation 子类中完成:

// mace/ops/opencl/conv_2d.h
class OpenCLConv2dKernel : public OpenCLConv2dBase {
public:
    MaceStatus Compute(OpContext* context,
                        const Tensor* input,
                        const Tensor* filter,
                        const Tensor* bias,
                        Tensor* output) override;

private:
    cl::Kernel kernel_;       // OpenCL kernel
    uint32_t kwg_size_;       // kernel work group size
    std::vector<index_t> input_shape_; // 缓存的输入形状
};

原因是 OpenCL kernel 的选择逻辑和 CPU 完全不同——CPU 上可以根据通道数在 Winograd、Im2Col+GEMM、直接卷积之间选择,而 OpenCL 上更多是根据 work group size、local memory 大小、Image vs Buffer 等 GPU 特定因素来选择,这些因素不适合用 Delegator 的五维 key 来表达。

Hexagon 整图下推

MACE 的另一个例外是 Hexagon DSP 后端——它不是逐算子执行,而是把整个图(或图的一部分)作为一个整体下推到 Hexagon 的 HVX 运行时:

// mace/runtimes/hexagon/hexagon_hta_wrapper.h
class HexagonHTAWrapper {
public:
    // 把整个网络设置进 Hexagon
    bool SetupGraph(const NetDef& net_def);

    // 整图执行
    bool ExecuteGraph(const Tensor& input, Tensor* output);

    // 不是逐算子调用——是整图作为一个黑盒执行
};

这种”整图下推”和 Delegator 的”逐算子委托”是完全不同的执行范式——Hexagon 的算子实现对 MACE 来说是不可见的,MACE 只负责把数据传进去、拿结果出来。

设计取舍

MACE 的 Operation + Delegator 设计是六个框架中最”多态”的。惰性选择让 Delegator 的决策延迟到运行时——可以根据输入的实际特征做最优选择,而不是编译时或初始化时就定死。但 OpenCL 不走 Delegator、Hexagon 整图下推这两个例外,暴露出 Delegator 体系的局限性——它为 CPU 上的分支选择设计得很好,但面对根本不同的执行范式(GPU shader、DSP 整图)时就力不从心了,需要走完全不同的代码路径。


横向对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
层数 1层(Layer) 3层(Op-Execution-Geometry) 2层(OpKernel+Registry) 2层(BaseLayer+LayerAcc) 2层(OpLite+KernelLite) 2层(Operation+Delegator)
shape/compute分离 耦合 分离(onResize/onExecute) 分离(shape inference独立) 分离(BaseLayer推导) 分离(InferShape独立) 分离(InferOutputShape独立)
后端选择时机 编译时 运行时 运行时(EP注册) 运行时(Device工厂) MIR优化时 运行时(惰性)
版本管理 opset版本化
降级/兜底 GeometryComputer 无(EP自己兜底)
kernel选择维度 1维(平台) 1维(Backend) 2维(EP+opset) 1维(Device) 3维(Target+Precision+Layout) 5维(Runtime+DType+Impl+…)
动态形状支持 弱(每次重跑forward) 强(onResize可选) 中等(Reshape方法) 中等 中等

核心取舍分析

六种算子抽象反映了三个根本性的设计取舍。

第一个取舍是层数 vs 简单性。ncnn 一层搞定一切,代码路径最短、学习成本最低,但灵活性差——shape 推导和计算绑在一起,添加新后端要改继承树。MNN 三层架构灵活性最高——Op 声明、Execution 执行、Geometry 降级各司其职——但认知复杂度也最高。TNN、ORT、Paddle-Lite、MACE 各自取了不同的中间点。

第二个取舍是编译时确定 vs 运行时决策。ncnn 在编译时就确定了每个算子的实现(通用 → ARM → NEON 的覆盖链),优点是零运行时开销,缺点是一个二进制文件只能服务一种硬件配置。MACE 的 Delegator 走到另一个极端——运行时才根据输入特征选择实现,优点是能做最优选择,缺点是第一次执行有额外的查找开销。

第三个取舍是算子覆盖率 vs 实现成本。没有 GeometryComputer 的框架(ncnn、TNN、Paddle-Lite、MACE),每支持一个新算子就需要在每个后端都写一份实现——算子数 x 后端数 = 总实现数。MNN 的 GeometryComputer 打破了这个线性关系——新后端只需实现核心基本算子,复杂算子通过降级自动获得支持。ORT 虽然没有降级机制,但它的 EP 插件模型允许后端只”认领”自己能高效执行的算子,其余算子自动 fallback 到 CPU EP——这是一种不同形式的兜底。

对于竞赛选手来说,ncnn 的单层 Layer 设计是最容易上手的——你只需要继承 Layer、实现 forward 方法就能添加一个新算子。理解了 ncnn 的设计后,再去看 MNN 的三层架构就能体会到”为什么需要这个复杂性”——因为当你需要支持几十个后端和上千个算子时,ncnn 的简单设计会变成维护噩梦。


本章小结

算子抽象是六个框架设计哲学分歧最大的地方。ncnn 用一个 Layer 类搞定一切,MNN 用三层架构(Op + Execution + GeometryComputer)做精密分工,ORT 用 OpKernel + opset 版本化注册实现标准化,TNN 用 BaseLayer + LayerAcc 做 shape/compute 分离,Paddle-Lite 用三维坐标(Target + Precision + Layout)精确标定每个 kernel,MACE 用 Delegator 做五维惰性选择但 OpenCL/Hexagon 走例外路径。

这些算子抽象并不是孤立运作的——它们需要被”调度”起来,按照正确的顺序执行。下一章我们将看到,当 ncnn 选择”从输出反向按需递归”、MNN 选择”四步流水线”、ORT 选择”图分区 + 执行计划”时,调度策略如何和算子抽象紧密咬合。


导读目录:README.md 上一章:Ch17: 数据容器——同一个问题的六种回答 下一章:Ch19: 推理调度——从按需递归到图分区