☰
自研推理框架、动态维度、模型导出、算子与计算图
2026/10/10 2:44:28 网站建设 项目流程

第30题:自研推理框架、动态维度、模型导出、算子与计算图

1. 核心回答

如果让我设计一个自研推理框架,我会把整个系统拆成四层:

模型导出 → 统一计算图 IR → 图优化与算子 Lowering → 后端 Kernel 执行

核心流程可以表示为:

Framework Model→Exported Graph→Unified IR→Graph Optimization→Backend Lowering→Kernel Execution \text{Framework Model} \rightarrow \text{Exported Graph} \rightarrow \text{Unified IR} \rightarrow \text{Graph Optimization} \rightarrow \text{Backend Lowering} \rightarrow \text{Kernel Execution}Framework Model→Exported Graph→Unified IR→Graph Optimization→Backend Lowering→Kernel Execution

其中需要重点解决四个工程问题:

  1. 如何把 PyTorch 等训练框架中的模型稳定导出;
  2. 如何用统一 IR 表达算子、Tensor 和计算依赖;
  3. 如何处理 Batch Size、Sequence Length 等动态维度;
  4. 如何处理后端暂时不支持的算子。

整个设计的基本原则是:

先保证模型语义正确,再进行图级和算子级性能优化。


2. 整体推理框架怎么设计

2.1 前端:模型导出

训练模型通常来自:

  • PyTorch;
  • TensorFlow;
  • ONNX;
  • 其他模型框架。

自研推理框架首先需要把这些不同模型转换成自己的统一表示。

例如 PyTorch 模型可以通过:

  • torch.export;
  • ONNX Export;

获得显式计算图。

导出阶段需要保存的不只有算子名称,还包括:

  • Tensor;
  • Tensor dtype;
  • Tensor shape;
  • 算子类型;
  • 算子属性;
  • 常量;
  • 输入输出关系;
  • 数据依赖;
  • 动态维度约束。

例如:

x ↓ MatMul ↓ Add ↓ GELU ↓ y

可以在内部表示为:

Tensor x ↓ MatMul(x, weight) ↓ Add(bias) ↓ GELU ↓ Tensor y

这样后续优化阶段可以直接分析整张计算图。


3. 什么是计算图和 IR

3.1 计算图

计算图可以抽象成:

G=(V,E) G=(V,E)G=(V,E)

其中:

  • VVV表示算子节点;
  • EEE表示 Tensor 数据依赖。

例如:

Y=ReLU⁡(XW+b) Y=\operatorname{ReLU}(XW+b)Y=ReLU(XW+b)

可以转换成:

X ----\ MatMul → Add → ReLU → Y W ----/ ↑ b

计算图让推理框架能够分析:

  • 哪些算子可以融合;
  • 哪些常量可以提前计算;
  • Tensor 生命周期是什么;
  • 哪些内存可以复用;
  • 哪些算子可以并行。

3.2 IR 的作用

IR 即 Intermediate Representation,中间表示。

它位于:

PyTorch / ONNX ↓ Unified IR ↓ CUDA / CPU / NPU

统一 IR 的价值在于把前端模型格式与底层硬件解耦。

例如一个 MatMul:

matmul(A, B)

在不同后端可以 Lower 成:

CUDA → cuBLAS GEMM CPU → oneDNN GEMM NPU → Vendor MatMul Kernel

这样增加新硬件时,主要增加新的 Backend 和 Kernel 映射,不需要重新设计整个模型前端。


4. 图优化阶段做什么

模型成功转换到 IR 后,可以执行一系列 Graph Pass。

4.1 常量折叠

例如:

y=x+(2×3) y=x+(2\times3)y=x+(2×3)

其中:

2×3=6 2\times3=62×3=6

可以在编译阶段直接计算。

最终图变成:

y=x+6 y=x+6y=x+6

这样运行时减少一次计算。


4.2 Dead Code Elimination

如果某个计算结果最终没有参与模型输出:

A → B → C \ D

其中 D 永远不会影响最终结果,就可以删除 D。


4.3 算子融合

例如:

MatMul ↓ Bias Add ↓ GELU

可以尝试融合成:

Fused MatMul + Bias + GELU

减少:

  • Kernel Launch;
  • 中间 Tensor;
  • 显存读写;
  • 内存带宽开销。

很多模型推理优化的收益来自减少内存访问,而不只是减少 FLOPs。


5. 动态维度怎么处理

动态 Shape 是自研推理框架中比较困难的一部分。

例如语言模型输入:

X∈RB×L×d X\in\mathbb{R}^{B\times L\times d}X∈RB×L×d

其中:

  • BBB:Batch Size;
  • LLL:Sequence Length;
  • ddd:Hidden Dimension。

通常ddd固定,但:

B, L B,\ LB,L

可能在运行时变化。

例如:

[1, 128, 4096] [8, 512, 4096] [16, 2048, 4096]

都可能输入同一个模型。


6. Symbolic Shape 怎么设计

6.1 使用符号表示动态维度

内部可以把 shape:

[batch, seq, 4096]

表示成:

[B,L,4096] [B,L,4096][B,L,4096]

其中BBB和LLL是 Symbolic Dimension。

例如矩阵:

X:[B,L,4096] X:[B,L,4096]X:[B,L,4096]

经过 Linear:

W:[4096,11008] W:[4096,11008]W:[4096,11008]

则输出可以推导为:

Y:[B,L,11008] Y:[B,L,11008]Y:[B,L,11008]

这里无需提前知道BBB和LLL的具体值。

这就是 Shape Inference。


6.2 Shape Constraint

动态维度通常仍然需要约束。

例如:

1≤B≤64 1\leq B\leq641≤B≤64

1≤L≤8192 1\leq L\leq81921≤L≤8192

还可能存在维度关系:

A.shape[0]=B.shape[0] A.shape[0]=B.shape[0]A.shape[0]=B.shape[0]

这些约束需要记录在 IR 或编译环境中。

PyTorchtorch.export当前也采用 Symbolic Dimension 和 Range Constraint 表达动态 Shape。


7. 为什么动态 Shape 不能完全无限制

如果所有 Shape 都完全未知,很多编译期优化就无法完成。

例如 GPU Kernel 的最优实现可能依赖:

  • Batch Size;
  • Sequence Length;
  • Matrix Shape;
  • Alignment;
  • Workspace Size。

因此实际工程中通常会使用有限范围动态。

例如:

MIN = [1, 1, 4096] OPT = [8, 512, 4096] MAX = [32, 4096, 4096]

TensorRT 的 Optimization Profile 就采用:

min / opt / max

描述动态输入范围。

optShape 用于重点选择高性能执行策略。

因此可以把动态 Shape 分成:

Symbolic Shape ↓ Shape Constraint ↓ Optimization Profile ↓ Runtime Shape

8. Runtime 怎么处理动态 Shape

运行时收到:

Input Shape = [6, 700, 4096]

框架首先检查:

shape∈valid range shape\in valid\ rangeshape∈validrange

如果合法:

  1. 绑定 Symbolic Shape;
  2. 完成剩余 Shape 推导;
  3. 选择对应执行计划;
  4. 分配 Workspace;
  5. 调用对应 Kernel。

如果输入 Shape 超过已有优化范围,可以根据框架设计:

  • 选择其他 Optimization Profile;
  • 触发新的 specialization;
  • 重新编译;
  • 回退到通用执行路径;
  • 返回明确的 Shape Error。

具体策略取决于实时性和部署要求。


9. 动态 Shape 的难点是什么

9.1 内存规划困难

静态 Shape 下:

Tensor A = 10 MB Tensor B = 20 MB Tensor C = 5 MB

编译阶段就能准确完成 Memory Planning。

动态 Shape 下:

size(A)=f(B,L) size(A)=f(B,L)size(A)=f(B,L)

实际大小只能在运行时确定。

因此需要:

  • 最大 Shape 预分配;
  • Runtime Allocation;
  • Memory Pool;
  • Workspace Reuse;

等策略。


9.2 Kernel 选择困难

某个 Kernel 可能在:

M=128

时最快。

另一个 Kernel 可能在:

M=4096

时最快。

因此通常需要:

shape→kernel shape\rightarrow kernelshape→kernel

的映射策略。

可以通过:

  • heuristic;
  • autotuning;
  • profile;
  • kernel cache;

选择执行方案。


10. 不支持的算子怎么处理

模型导出后经常会遇到:

Unsupported Operator

例如模型中存在自定义算子:

CustomAttention

后端没有对应 Kernel。

通常有三种处理方式。


10.1 算子分解

优先检查高层算子能否拆成已有基础算子。

例如:

CustomLinear

可以分解成:

MatMul + Add

如果基础算子已经受到后端支持,就无需新增 Kernel。

这种过程可以理解为:

Ophigh→{Oplow1,Oplow2,...} Op_{high} \rightarrow \{Op_{low}^{1},Op_{low}^{2},...\}Ophigh​→{Oplow1​,Oplow2​,...}

MLIR 的 Dialect Conversion 也提供类似的 Lowering 和 Rewrite 机制。


10.2 自定义 Kernel

如果算子无法有效分解,可以实现自己的 Kernel。

例如:

FlashAttentionOp

对应:

CUDA FlashAttention Kernel

然后注册:

IR Operator ↓ Backend Operator ↓ Kernel

需要同时定义:

  • 输入输出类型;
  • Shape Inference;
  • dtype 支持;
  • Kernel 参数;
  • Workspace;
  • 错误检查。

10.3 Backend Fallback

如果某个算子暂时没有优化实现,可以回退到已有运行时。

例如:

自研 CUDA Backend ↓ Unsupported Op ↓ PyTorch / ONNX Runtime / CPU Fallback

Fallback 可以提高模型覆盖率。

代价可能包括:

  • Host/Device Copy;
  • Tensor Layout 转换;
  • 额外同步;
  • 延迟增加。

因此生产环境需要记录:

Fallback Rate

如果大量算子都发生 fallback,自研推理框架通常无法获得预期性能收益。


11. Backend 和 Kernel 怎么组织

可以设计统一 Backend Interface:

Backend ├── CPUBackend ├── CUDABackend └── NPUBackend

每个 Backend 注册自己支持的 Operator。

例如:

MatMul ├── CPU → oneDNN ├── CUDA → cuBLAS └── NPU → Vendor GEMM

编译阶段根据:

  • Device;
  • dtype;
  • shape;
  • layout;
  • operator attributes;

选择 Kernel。

最终形成:

Operator+Shape+DType+Device→Kernel Operator + Shape + DType + Device \rightarrow KernelOperator+Shape+DType+Device→Kernel


12. 内存规划怎么做

推理性能不仅由计算速度决定。

GPU 推理中频繁:

cudaMalloc cudaFree

会产生明显开销。

因此编译阶段可以分析 Tensor Lifetime。

例如:

Tensor A: Node1 → Node3 Tensor B: Node4 → Node6

如果 A 在 Node3 后已经死亡,而 B 在 Node4 才创建,就可以让:

Memory(A)=Memory(B) Memory(A)=Memory(B)Memory(A)=Memory(B)

复用同一块显存。

这一过程叫:

Memory Planning / Memory Reuse

最终可以减少:

  • 峰值显存;
  • Allocation 次数;
  • 内存碎片。

13. 怎么验证自研推理框架是正确的

我会从四个维度验证。

13.1 数值正确性

比较:

Yreference Y_{reference}Yreference​

与:

Yengine Y_{engine}Yengine​

例如计算:

Error=max⁡∣Yreference−Yengine∣ Error= \max|Y_{reference}-Y_{engine}|Error=max∣Yreference​−Yengine​∣

针对 FP32、FP16、BF16、INT8 分别设置合理容差。


13.2 模型覆盖率

统计:

Coverage=Supported ModelsTotal Models Coverage= \frac{Supported\ Models} {Total\ Models}Coverage=TotalModelsSupportedModels​

同时报告:

  • Unsupported Op 数量;
  • Fallback Rate;
  • Dynamic Shape 覆盖范围;
  • 自定义算子数量。

13.3 性能

需要分别测:

  • Latency;
  • P50 / P95 / P99 Latency;
  • Throughput;
  • Time to First Token;
  • Tokens/s;
  • Compilation Time。

性能比较必须保持:

  • 相同硬件;
  • 相同模型;
  • 相同精度;
  • 相同输入 Shape;
  • 相同 Batch Size。

13.4 资源消耗

还需要测:

  • Peak GPU Memory;
  • Workspace;
  • CPU Memory;
  • GPU Utilization;
  • Kernel Launch 数量。

这样才能判断优化收益来自什么地方。


14. 如果面试官问“你真正做了哪些东西”

回答时需要明确区分:

模型导出 IR设计 Shape推导 Graph Pass Operator Lowering Kernel实现 Memory Planning Runtime Benchmark

然后指出自己真正负责的模块。

例如:

我主要负责 Dynamic Shape 和 Operator Lowering。模型导出后,我在 IR 中使用 Symbolic Dimension 表示动态 Batch 和 Sequence Length,并维护维度范围及等式约束。编译阶段先做 Shape Inference,再根据 Shape Profile 生成不同执行策略。对于后端不支持的高层算子,我优先通过 Rewrite Pattern 分解成基础算子;无法有效分解时再注册自定义 CUDA Kernel。最后通过跨 Shape 数值对齐测试、模型覆盖率、P95 延迟和峰值显存验证实现。

这种回答能够清楚体现实际工程边界。


15. 面试时可以压缩成下面这段

如果让我设计一个自研推理框架,我会采用“模型导出、统一 IR、图优化、Backend Lowering、Kernel Runtime”这条链路。

前端通过torch.export或 ONNX 获得计算图,然后转换成内部统一 IR。IR 中保存 Operator、Tensor、dtype、shape、attribute、常量以及数据依赖。

静态 Shape 可以在编译阶段完成 Shape Inference 和内存规划。对于动态 Batch 或 Sequence Length,我会使用 Symbolic Shape 表示,并维护 Range Constraint。编译时根据常见 Shape 建立 Optimization Profile,运行时绑定真实 Shape,再进行 Kernel 选择和 Workspace 分配。

图优化阶段执行常量折叠、无效节点删除、算子融合和内存复用。后端不支持的算子优先做 Operator Decomposition 或 Lowering;确实需要时实现 Custom Kernel;暂时无法支持时提供 Fallback,并监控 Fallback Rate。

最后从四个维度验收:数值正确性、模型和算子覆盖率、延迟与吞吐、显存和 Workspace。整个过程首先保证与原模型语义一致,然后再判断优化是否真正带来性能收益。


16. 来源

  1. PyTorch Documentation,torch.export:模型导出、Dynamic Shapes、Symbolic Dimension 与 Range Constraint。
  2. ONNX Documentation,Shape Inference:静态维度、符号维度及计算图 Shape 推导。
  3. NVIDIA TensorRT Documentation,Dynamic Shapes:Runtime Dimension 与 Optimization Profile 的min / opt / max机制。
  4. NVIDIA TensorRT Documentation,Dynamic Shapes: Advanced Topics:动态 Shape 的部分算子约束和 Shape Tensor。
  5. MLIR Documentation,Dialect Conversion:通过 Conversion Target、Rewrite Pattern 和 Type Converter 实现 Operator Lowering。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询