欢迎光临
我们一直在努力

007、MLIR的Location(位置信息)与诊断系统

MLIR的Location(位置信息)与诊断系统

一个让我熬夜到凌晨三点的bug

去年做AI编译器的时候,遇到一个诡异的崩溃:模型转换过程中,某个算子融合pass莫名其妙地生成了非法IR。最让人抓狂的是,错误信息只给了个“Operation has no valid location”,连个行号都没有。我盯着那段IR看了两个小时,愣是没找到是哪个pass、哪行代码搞出来的问题。

后来翻MLIR源码才发现,问题出在创建Operation时忘了设置Location。这个教训让我意识到:Location在MLIR里不是可有可无的装饰,而是调试系统的命脉。今天就把这块硬骨头啃透。

Location的本质:IR里的“GPS坐标”

MLIR的Location系统,说白了就是给每个Operation、每个Block、每个Value打上的“GPS坐标”。它记录了这个IR节点是从哪里来的——可能是源文件的行号,可能是某个pass的调用栈,也可能是你手动构造IR时指定的名字。

看个最基础的例子:

// 这个函数定义带上了文件位置信息
func.func @add(%a: f32, %b: f32) -> f32 attributes {location = "my_ops.py:42"} {
// 每个op都可以有自己的位置
%0 = arith.addf %a, %b : f32 loc("inline_optimizer:123")
return %0 : f32
}

这里每个loc(…)就是Location。别小看这玩意儿,没有它,你面对几十万行IR的时候就像在黑暗里找针。

Location的四种形态(踩坑实录)

MLIR定义了四种Location类型,我按实用频率排序:

1. FileLineColLoc(最常用,但最容易忘)

// 正确姿势:创建op时带上位置
auto loc = FileLineColLoc::get(&ctx, "source.py", 42, 10);
auto op = builder.create<arith::AddFOp>(loc, ...);

// 踩过坑:这样写出来的op没有位置信息
auto op = builder.create<arith::AddFOp>(...); // 别这样写!调试时你会哭

FileLineColLoc记录文件名、行号、列号。在Python前端绑定里,这玩意儿直接从Python的__line__和__file__提取。但有个坑:如果你在pass里创建新op,记得用builder.getUnknownLoc()兜底,否则MLIR会报“missing location”警告。

2. CallSiteLoc(调试多级编译的利器)

这个我是在做多级pass pipeline时发现的。当你的pass A调用了pass B,B又调用了pass C,最后C报错时,CallSiteLoc能告诉你完整的调用链:

// 假设pass C创建了一个op,它的调用栈是A->B->C
auto calleeLoc = FileLineColLoc::get(&ctx, "pass_c.cpp", 100, 5);
auto callerLoc = FileLineColLoc::get(&ctx, "pass_b.cpp", 50, 3);
auto callSiteLoc = CallSiteLoc::get(calleeLoc, callerLoc);
// 再往上加一层
auto topCallerLoc = FileLineColLoc::get(&ctx, "pass_a.cpp", 20, 1);
auto finalLoc = CallSiteLoc::get(callSiteLoc, topCallerLoc);

实际调试时,这个链能让你一眼看出是哪个pass的哪一步出了问题。我见过有人把所有pass的Location都设成同一个文件,结果报错时根本不知道是哪个pass干的——典型的自找麻烦。

3. NameLoc(手动构造IR时的救命稻草)

写测试用例或者手动构造IR时,FileLineColLoc不适用,因为根本没有源文件。这时候NameLoc就派上用场了:

// 给这个op起个有意义的名字
auto loc = NameLoc::get(StringAttr::get(&ctx, "my_custom_op_creation"));
auto op = builder.create<arith::AddFOp>(loc, ...);

经验之谈:名字要起得有辨识度,别用“temp_op_1”这种。我习惯用“pass名_阶段名_序号”的格式,比如“constant_folding_step3_op5”。

4. FusedLoc(合并多个来源)

当多个源文件的信息需要合并到一个op上时,用FusedLoc:

SmallVector<Location> locs = {
FileLineColLoc::get(&ctx, "a.py", 10, 1),
FileLineColLoc::get(&ctx, "b.py", 20, 5)
};
auto fusedLoc = FusedLoc::get(&ctx, locs);

这个在算子融合场景下特别有用——融合后的op应该保留所有原始op的位置信息,方便回溯。

诊断系统:Location的“消费者”

Location本身只是数据,真正发挥作用的是MLIR的诊断系统(DiagnosticEngine)。这套系统负责把Location转换成人类可读的错误信息。

基础用法:emitError/emitWarning

// 在pass里报错
auto op = getOperation();
return op->emitError("这个op的shape不匹配,别想蒙混过关");

// 带位置信息的警告
op->emitWarning("这里可能有性能问题,建议检查一下") << "额外信息: " << someValue;

注意emitError返回的是InFlightDiagnostic对象,它支持流式操作。但有个坑:如果你在emitError之后没有return,MLIR会继续执行,导致重复报错。我见过有人写:

op->emitError("错了");
// 忘了return,继续执行
op->emitError("又错了"); // 这里会报两次

自定义诊断处理器

默认情况下,诊断信息会打印到stderr。但在生产环境里,你可能想捕获这些信息做日志分析:

// 注册自定义处理器
auto &diagEngine = ctx.getDiagEngine();
diagEngine.registerHandler([](Diagnostic &diag) -> LogicalResult {
// 这里可以写自己的处理逻辑
if (diag.getSeverity() == DiagnosticSeverity::Error) {
// 记录到日志系统
myLogger.logError(diag.str());
// 也可以选择不打印到stderr
return success();
}
return failure(); // 返回failure表示继续使用默认处理器
});

这个机制让我在调试分布式编译时省了不少事——把错误信息统一收集到中心日志服务器,而不是散落在各个节点的stderr里。

实战:写一个带Location追踪的Pass

下面是我实际项目中用到的模式,专门用来追踪pass内部的错误:

struct MyPass : public PassWrapper<MyPass, OperationPass<func::FuncOp>> {
void runOnOperation() override {
auto funcOp = getOperation();

// 遍历所有op
funcOp.walk([&](Operation *op) {
// 检查op的合法性
if (auto addOp = dyn_cast<arith::AddFOp>(op)) {
// 这里踩过坑:直接报错不包含上下文
// op->emitError("类型不对"); // 别这样写

// 正确做法:带上op的详细信息
auto diag = op->emitError("AddFOp类型检查失败");
diag.attachNote(op->getLoc()) << "原始位置: " << op->getLoc();
diag.attachNote() << "操作数类型: " << addOp.getOperand(0).getType();

// 如果是在pass内部创建的op,记录创建原因
if (auto nameLoc = op->getLoc().dyn_cast<NameLoc>()) {
diag.attachNote() << "创建来源: " << nameLoc.getName();
}
}
});
}
};

这里的关键是attachNote——它允许你在一个诊断信息里附加多条注释,每条都可以有自己的Location。这样报错时,你能看到完整的错误链。

调试技巧:Location的“逆向工程”

有时候你拿到一个op,想知道它的Location是怎么来的。MLIR提供了几个实用方法:

// 获取op的Location
auto loc = op->getLoc();

// 判断Location类型
if (auto fileLoc = loc.dyn_cast<FileLineColLoc>()) {
llvm::errs() << "文件: " << fileLoc.getFilename()
<< " 行: " << fileLoc.getLine()
<< " 列: " << fileLoc.getColumn() << "\\n";
}

// 打印完整的Location树(包含嵌套的CallSiteLoc)
loc.print(llvm::errs());

有个小技巧:在mlir-opt命令行加上–mlir-print-debuginfo,所有IR都会带上Location信息。配合–mlir-print-local-scope,能看清每个op的作用域。

个人经验总结

  • Location不是装饰,是基础设施。我见过太多人图省事不设Location,结果调试时花十倍时间。每次创建op,哪怕是在测试代码里,也要设一个NameLoc。

  • 诊断信息要“有话说”。emitError("错了")这种信息等于没写。好的诊断应该包含:什么错了、为什么错、怎么改。比如“shape不匹配,期望[4,3]但得到[4,5],请检查reshape操作的参数”。

  • 善用attachNote。一个错误往往有多个原因,用attachNote把每个原因都列出来,比写一大段文字清晰得多。

  • 生产环境要自定义诊断处理器。默认的stderr输出在分布式环境里就是灾难。把诊断信息结构化输出到日志系统,配合ELK等工具做分析。

  • Location的传播要显式处理。在pass里创建新op时,如果新op是从旧op衍生出来的,记得把旧op的Location传过去。我习惯写一个工具函数:

  • Location propagateLocation(Operation *srcOp, StringRef reason) {
    auto loc = srcOp->getLoc();
    return NameLoc::get(
    StringAttr::get(srcOp->getContext(),
    (Twine(reason) + "_from_" + loc.getAsString()).str()),
    loc);
    }

    这样每个op都带着“族谱”,出了问题能一路追溯到源头。

    最后说一句:Location和诊断系统是MLIR里最容易被忽视但最有价值的部分之一。花时间理解它,绝对值得。下次你的pass报错时,你会感谢自己当初多花了十分钟写Location。

    赞(0)
    未经允许不得转载:171主机测评 » 007、MLIR的Location(位置信息)与诊断系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址