Mini-Infer (20): 优化器的骨架 — Pass Manager 架构设计

1. 设计哲学:像编译器一样思考

在编译器领域(如 LLVM),优化通常被组织成一系列的 Pass (遍)。每一个 Pass 只做一件特定的事情(比如“死代码消除”),然后优化器(Pass Manager)按顺序执行这些 Pass。

Mini-Infer 借鉴了这种设计,同时也参考了 TensorRT 的 IOptimizationProfile 概念。

我们需要两个核心组件:

  1. OptimizationPass: 定义“怎么优化”的接口(策略模式)。
  2. GraphOptimizer: 定义“何时优化”的管理器(执行管线)。

2. 定义契约:OptimizationPass

首先,我们定义所有优化算法必须遵守的基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// mini_infer/graph/graph_optimizer.h

class OptimizationPass {
public:
explicit OptimizationPass(const std::string& name) : name_(name) {}
virtual ~OptimizationPass() = default;

/**
* @brief 核心接口:对图进行一次优化
* @param graph 待优化的图指针
* @param num_modifications [输出] 记录该 Pass 修改了多少处图结构
* @return 执行状态
*/
virtual core::Status apply(Graph* graph, int& num_modifications) = 0;

const std::string& name() const { return name_; }

protected:
std::string name_;
};

这个接口非常简单但强大:

  • apply: 这是唯一需要实现的方法。具体的优化逻辑(比如遍历图、查找模式、修改节点)都在这里实现。
  • num_modifications: 这是一个关键的反馈机制。如果返回 0,说明图结构已经稳定,或者该优化不适用。这为未来的“定点迭代”(反复优化直到不再变化)提供了基础。

3. 调度中心:GraphOptimizer

有了 Pass,我们需要一个容器来管理它们的执行顺序,并提供日志和统计功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// mini_infer/graph/graph_optimizer.h

class GraphOptimizer {
public:
// 1. 注册 Pass:构建优化流水线
void add_pass(std::shared_ptr<OptimizationPass> pass);

// 2. 执行优化:一键运行所有 Pass
core::Status optimize(Graph* graph);

// 3. 统计信息:用于分析优化效果
struct Statistics {
int total_passes = 0;
int total_modifications = 0;
std::vector<std::pair<std::string, int>> pass_results;
};
const Statistics& get_statistics() const { return stats_; }

// ...
private:
std::vector<std::shared_ptr<OptimizationPass>> passes_;
Statistics stats_;
};

实现逻辑 (graph_optimizer.cpp)

optimize 函数不仅是简单的循环调用,它还集成了日志记录和统计,这对于调试复杂的图优化非常重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
core::Status GraphOptimizer::optimize(Graph* graph) {
if (!graph) return core::Status::ERROR_INVALID_ARGUMENT;

// 重置统计信息
stats_ = Statistics();
stats_.total_passes = static_cast<int>(passes_.size());

MI_LOG_INFO("[GraphOptimizer] Starting optimization with " +
std::to_string(stats_.total_passes) + " passes");

// 顺序执行每一个 Pass
for (auto& pass : passes_) {
int num_modifications = 0;

// 执行具体的优化逻辑
auto status = pass->apply(graph, num_modifications);

if (status != core::Status::SUCCESS) {
MI_LOG_ERROR("[GraphOptimizer] Pass failed: " + pass->name());
return status;
}

// 记录统计
stats_.total_modifications += num_modifications;
stats_.pass_results.push_back({pass->name(), num_modifications});

// 打印详细日志
if (verbose_) {
MI_LOG_INFO("[GraphOptimizer] Pass completed: " + pass->name() +
", modifications: " + std::to_string(num_modifications));
}
}

return core::Status::SUCCESS;
}

4. 为什么要这样设计?

这种 Pass Manager 架构带来了极大的灵活性:

  1. 解耦 (Decoupling)GraphOptimizer 不需要知道具体的优化算法(Fusion, DCE 等)。添加新的优化只需要写一个新的 OptimizationPass 子类并注册即可。
  2. 可配置性 (Configurability):我们可以根据不同的硬件后端(CPU vs GPU)注册不同的 Pass 列表。例如,某些 GPU 特有的融合策略在 CPU 上可能并不适用。
  3. 可观测性 (Observability):通过 Statistics 和日志,我们可以清楚地看到每个 Pass 到底做了什么。如果模型跑得慢,我们可以检查是不是“算子融合”Pass 没有生效(修改数为 0)。

5. 总结与展望

本篇我们构建了图优化的“骨架”。我们现在拥有了一个能够调度、执行和监控优化的系统。

但是,目前的优化器还是“空转”的,因为它还没有注册任何具体的 Pass。