什么是ASC-IR:为昇腾算子定义MLIR方言的完整指南(TableGen实战)
什么是ASC-IR:为昇腾算子定义MLIR方言的完整指南(TableGen实战)
一句话理解:ASC-IR 是 pyasc 项目基于 MLIR 框架自定义的中间表示(IR)方言,它用标准化的"操作(Op)+ 类型(Type)"体系描述昇腾算子的计算逻辑,再借助 TableGen 工具自动生成 C++ 代码与 Python 绑定,最终把 Python 代码翻译成 Ascend C 算子并在昇腾 AI 处理器上加速运行。
如果你想在昇腾芯片上开发算子,但不想写 C++,pyasc 提供了一个 Python 编程接口——它和 Ascend C 接口一一对应,且遵守 Python 原生语法。而支撑这一切的"幕后英雄",就是本文的主角 ASC-IR(MLIR 方言) 和 TableGen 代码生成技术。
1. ASC-IR 在 pyasc 架构中的位置
先看 pyasc 的整体架构图。中间一层的「扩展 Dialect」正是核心:左侧的 ASC-IR 基于 MLIR 扩展定义了 Ascend C 的类和接口,右侧的 EmitAsc-IR 则负责把 MLIR 转换为 C++ 代码类。
简单说,整条链路是:
| 层 | 干什么 | 代表目录 |
|---|---|---|
| Python 界面 | 提供 Kernel 开发接口,与基础/高阶 API 1:1 对应 | python/asc/language/ |
| 扩展 Dialect | ASC-IR:用 MLIR 描述算子计算图 | include/ascir/ |
| Ascend C | 基础 API & 高阶 API | 由后端代码生成 |
| 基础组件 | 毕昇编译器、Runtime、Profiling 工具 | — |
详细模块说明可参考 docs/architecture_introduction.md。
2. 编译全流程:Python 代码如何变成算子
下面的流程图展示了从 @asc.jit 装饰的 Python 代码到最终输出的完整旅程,ASC-IR 正处于承上启下的中心位置:
流程要点:
- 前端:Python AST Visitor 遍历语法树,调用 pybind 接口创建 ASC-IR 节点;
- ASC-IR 定义模块:由 Interface、Attribute、Type、Operation 四件套构成(这正是 MLIR 方言的标准结构);
- 后端:Translation 执行代码生成,调用 CodeEmitter 输出的封装方法,产出 Ascend C 代码;
- 编译运行:毕昇编译器生成二进制,Runtime 加载执行。
💡 想亲眼看看编译产物?设置环境变量 PYASC_DUMP_PATH 即可在编译完成后查看中间生成的 ASC-IR 文件和 Ascend C 代码,详见 docs/architecture_introduction.md 的"编译和运行模块"章节。
3. 方言四件套:一个 .td 文件读懂 ASC-IR
ASC-IR 的方言定义全部集中在 include/ascir/Dialect/ 目录下,用 TableGen(.td 文件)编写。四个构建块分别是:
3.1 Type:类型定义
MLIR 中的"类型"描述了数据的形状和含义。ASC-IR 在 Core/Base.td 中定义了类型基类,例如张量类型 BaseTensorType 携带了 shape 和 elementType 两个参数——这就是 Ascend C 中张量概念的 MLIR 表达。
// 核心思想:用声明式语法定义"张量 = 形状 + 元素类型"
class AscendC_BaseTensorType<string name, string typeMnemonic, ...>
: AscendC_Type<name, typeMnemonic, [AscendC_BaseTensorTypeInterface, ...]> {
let parameters = (ins ArrayRefParameter<"int64_t">:$shape,
"Type":$elementType);
}
3.2 Operation:操作定义
Op 是 MLIR 的"积木块"。以 Core/Tensor.td 为例,GlobalTensor::GetPhyAddr 方法被定义为一个 Op:
def AscendC_GlobalTensorGetPhyAddrOp
: APIOp<"global_tensor.get_phy_addr", "GetPhyAddr", [AscMemberFunc]> {
let summary = "Call `AscendC::GlobalTensor::GetPhyAddr` method";
let arguments = (ins AscendC_GlobalTensor:$tensor, Optional<UI64>:$offset);
let results = (outs AnyRankedOrUnrankedMemRef:$result);
}
注意命名约定:APIOp 的第二个参数 "GetPhyAddr" 对应 Ascend C 的真实 C++ 方法名,这正是"Python 接口与 Ascend C 一一对应"的底层保证。按 API 分类,Op 定义分布在 Basic/(42 个 .td,覆盖向量、标量、数据搬运等基础 API)和 Adv/(9 个 .td,覆盖 Matmul、Softmax 等高阶 API)。
3.3 Interface 与 Attribute
接口(Interface)让不同 Op 共享统一的方法契约,如 Interfaces.td 中的张量接口;属性(Attribute)则承载 Op 的附加配置参数。
3.4 Pass:方言的"加工流水线"
Op 生成后还有一系列 Pass 做图变换,全部在 Transforms/Passes.td 中声明,例如:
| Pass | 作用 |
|---|---|
ascendc-insert-sync | 插入核内同步操作 |
ascendc-hoist-ub-allocation | 把张量分配提升到函数根部 |
ascendc-detect-kernel-type | 检测 kernel 是纯 Vector 还是混合模式 |
对应的实现代码在 lib/Dialect/Asc/Transforms/,每个 Pass 一个 .cpp 文件,一一对应。
4. TableGen 实战:.td 如何变成 C++ 和 Python
.td 文件本身不可执行,pyasc 自研了一套 TableGen 后端放在 lib/TableGen/,构建时自动运行并产出多种代码:
| 生成器 | 产物 |
|---|---|
| GenAPITypes.cpp / GenAPITypedefs.cpp | API 类型 C++ 定义 |
| GenOpEmitDecls.cpp / GenOpEmitDefs.cpp | Op 到 C++ 代码的发射规则(Emit 声明与实现) |
| GenPybindDefs.cpp | Python 绑定——让前端 AST Visitor 能通过 pybind 创建 IR 节点 |
这就是"定义一次,三处受益":同一个 def AscendC_GlobalTensorGetPhyAddrOp,既生成了 MLIR 的 Op 类,又生成了发射 C++ 代码的 Emitter 逻辑,还生成了 Python 侧可 import 的接口。
5. 上手路径:从读示例到读测试
🚀 建议新手按以下顺序探索:
- 跑一个最小算子:examples/01_add/add.py 与 examples/02_add_framework/add_framework.py,配合
PYASC_DUMP_PATH查看生成的 ASC-IR; - 对照测试用例理解 Op 语义:如 test/Target/AscendC/matmul.mlir 展示了 Ascend C Op 文本化后的 IR 长什么样;
- 深入方言定义:通读 include/ascir/Dialect/Asc/IR/Core/ 的 6 个 .td 文件;
- 进阶到代码生成:浏览 lib/Target/AscendC/ 下按 API 分类的 Emitter 实现(Core/GlobalTensor.cpp 等)。
6. 小结
- ASC-IR = MLIR 方言:用 Op/Type/Interface/Attribute 四件套把 Ascend C 世界映射到 IR 世界;
- TableGen 是杠杆:一份 .td 定义,同时驱动 C++ 后端和 Python 前端代码生成,保证接口严格 1:1 对应;
- 学习路线:跑示例 → dump 中间产物 → 读 .td 定义 → 读 Pass 与 Emitter,即可完整掌握昇腾算子的编译原理。
掌握了 ASC-IR 方言的定义方式,你就有资格参与 pyasc 的贡献了——编写规范见 CONTRIBUTING.md。
更多推荐


所有评论(0)