以OneFlow为例探索MLIR的实际开发流程
1
前言
本文提到的Op和Operation是一回事,没有严格区分。
2
OneFlow是如何和MLIR结合的?
oneflow.jit.xxx
)还没有正式开放,我这里仍然以Lazy计算图(Job)为例来讲解OneFlow和MLIR的结合过程。 git clone git@github.com:Oneflow-Inc/oneflow.git
cd oneflow && mkdir build && cd build
cmake-C ../cmake/caches/cn/fast/mlir-cuda-75.cmake -DBUILD_TESTING=ON .. && ninja
os.environ["ONEFLOW_MLIR_ENABLE_ROUND_TRIP"] = '1'
os.environ["ONEFLOW_MLIR_ENABLE_CODEGEN_FUSERS"] = '1'
@flow.unittest.skip_unless_1n1d()
class TestFuseBiasAddGeLUCPUMLIR(oneflow.unittest.TestCase):
def test_fused_bias_add_gelu_graph(test_case):
data = np.random.randn(1, 2, 3)
bias_data = np.random.randn(2)
x = flow.tensor(data, dtype=flow.float32)
bias = flow.tensor(bias_data, dtype=flow.float32)
y_eager = flow.gelu(flow._C.bias_add(x, bias, axis=1))
class FuseBiasAddGeLUGraph(flow.nn.Graph):
def __init__(self):
super().__init__()
def build(self, x):
return flow.gelu(flow._C.bias_add(x, bias, axis=1))
bias_add_gelu = FuseBiasAddGeLUGraph()
y_lazy = bias_add_gelu(x)
test_case.assertTrue(np.array_equal(y_eager.numpy(), y_lazy.numpy()))
ir_pass
文件夹记录了经过OneFlow MLIR优化前后的计算图( .prototxt
) 以及 MLIR的表达式( *.mlir
),还有一个 *.mlir.dot
文件可以用 graphviz
打开来可视化MLIR表达式的计算图。oneflow/api/python/ir.cpp
中有下面两行代码: REGISTER_JOB_PASS("IRRoundTripBeforeAD", IRRoundTrip<kBeforeAD>);
REGISTER_JOB_PASS("IRRoundTrip", IRRoundTrip<kAfterAD>);
RoundTrip
即往返的意思, BeforeAD
可以理解为反向之前, kAfterAD
可以理解为反向之后,这里通过将OneFlow Job和MLIR的互转过程注册为OneFlow Job的一个Pass来建立OneFlow计算图和MLIR的联系。在执行OneFlow脚本时,如果想使能MLIR作用于OneFlow计算图,开启 ONEFLOW_MLIR_ENABLE_ROUND_TRIP=1
环境变量即可。 https://github.com/BBuf/tvm_mlir_learn
中) 。 oneflow/ir/include/OneFlow/OneFlowEnums.td
,OneFlow Dialect Operation的一些通用前端接口定义在 oneflow/ir/include/OneFlow/OneFlowEnums.td
。这里我们以Reshape Operation为例子来简单说明一下这个Operation有哪些组成部分: def OneFlow_ReshapeOp : OneFlow_BaseOp<"reshape", [NoSideEffect, DeclareOpInterfaceMethods<UserOpCompatibleInterface>]> {
let input = (ins
AnyType:$in
);
let output = (outs
AnyType:$out
);
let attrs = (ins
AnyI64ElementsAttr:$shape
);
}
OneFlow_ReshapeOp
这个名字下划线之前的是Dialect的名字,后面是这个Dialect下的Operation的名字。然后这个Operation继承了 OneFlow_BaseOp
基类,并声明了约束和前端接口,接下来定义了Operation的输入,输出和属性就结束了。 REGISTER_USER_OP("reshape")
.Input("in")
.Output("out")
.Attr<Shape>("shape")
...
oneflow/ir/oneflow-translate
,主要做的事情就是遍历Job的OpGraph,对节点和边分别进行处理最后转换成一个MLIR表达式,同时在计算完成后可以基于MLIR表达式重写Job。这里的整体逻辑偏复杂,因为要处理OneFlow Job OpGraph里面各种类型Operation和边的转化,这里不继续深入讲解,因为它也不是我这篇文章要讨论的点,感兴趣的可以直接阅读代码。 3
OneFlow IR如何执行?
oneflow/ir/include/OneFlow/OneFlowOps.td
容易发现这里还定义了一个 OneFlow_MlirJitOp
,这个自定义的Op就是用来执行MLIR表达式的,它里面实现了CPU和GPU的Kernel(源码在 oneflow/ir/oneflow-extension/extension.cpp
)用来加载MLIR提供的JIT执行引擎运行最终得到的LLVM IR。那么LLVM IR又是怎么来的呢?这是通过OneFlow MLIR表达式逐级下降之后得来的,具体下降过程如下: void AddLowerToLinalgMemRefPasses(PassManager& pm) {
pm.addPass(createLowerOneFlowToTosaPass()); // lower-oneflow-to-tosa
pm.addPass(createCSEPass()); // cse
pm.addNestedPass<FuncOp>(tosa::createTosaToLinalg()); // tosa-to-linalg-on-tensors
auto p = createLinalgElementwiseOpFusionPass();
assert(p->initializeOptions("allow-folding-unit-dim-reshapes=true").succeeded());
pm.addNestedPass<FuncOp>(std::move(p)); // linalg-fuse-elementwise-ops
pm.addNestedPass<FuncOp>(createLinalgBufferizePass()); // linalg-bufferize
pm.addNestedPass<FuncOp>(createTensorBufferizePass()); // tensor-bufferize
pm.addPass(createTensorConstantBufferizePass()); // tensor-constant-bufferize
pm.addPass(createFuncBufferizePass()); // func-bufferize
pm.addPass(createBufferResultsToOutParamsPass()); // buffer-results-to-out-params
pm.addPass(createCanonicalizerPass()); // canonicalize
pm.addNestedPass<FuncOp>(createFinalizingBufferizePass()); // finalizing-bufferize
}
LogicalResult LowerModuleToLLVM(mlir::MLIRContext* context, ModuleOp module) {
mlir::PassManager pm(context);
AddLowerToLinalgMemRefPasses(pm);
pm.addNestedPass<FuncOp>(createConvertLinalgToLoopsPass()); // convert-linalg-to-loops
pm.addNestedPass<FuncOp>(createLowerToCFGPass()); // convert-scf-to-std
pm.addPass(createConvertLinalgToLLVMPass()); // convert-linalg-to-llvm
pm.addPass(createMemRefToLLVMPass()); // convert-memref-to-llvm
pm.addPass(createLowerToLLVMPass()); // convert-std-to-llvm
pm.addPass(createReconcileUnrealizedCastsPass());
return pm.run(module);
}
MlirJitOp
的Kernel时触发的( oneflow/ir/oneflow-extension/extension.cpp
),调用也是作为一个MLIR的Pass被加入到了优化流程中。JIT调用流程Pass的实现可以精简为: class OutlineJitFunctionPass : public OutlineJitFunctionPassBase<OutlineJitFunctionPass> {
void runOnOperation() override {
Operation* op = getOperation();
RewritePatternSet patterns(op->getContext());
oneflow::populateFuserPasses(patterns);
(void)applyPatternsAndFoldGreedily(op, std::move(patterns));
}
};
std::unique_ptr<Pass> createOutlineJitFunctionPass() {
return std::make_unique<OutlineJitFunctionPass>();
}
LogicalResult ApplyRoundTripPatterns(RoundTripOneFlowJobWrapperInterface& job_wrapper,
MLIRContext* context, OwningModuleRef& module) {
mlir::PassManager pm(context);
pm.addNestedPass<mlir::FuncOp>(::mlir::createCanonicalizerPass());
if (job_wrapper.IsLastIRPass() && std::getenv("ONEFLOW_MLIR_ENABLE_CODEGEN_FUSERS") != nullptr) {
pm.addPass(oneflow::createOutlineJitFunctionPass());
}
...
}
第一个问题是如何做Op融合。上面的JIT执行流程只考虑了不断Lowering,那么假如在OneFlow Dialect中有一些Operation是可以融合的,这个时候应该怎么做呢?很简单,我们沿用一下MLIR的DRR规则,还是用TableGen语法在
oneflow/ir/include/OneFlow/OneFlowPatterns.td
中写一系列的Fuse Pattern即可,比如bias_add
+gelu
这两个Op可以融合成OneFlow中的fused_bias_add_gelu
Op,那么就可以写如下的规则。
def IsGPU: Constraint<CPred<"$0.getValue().equals(\"gpu\")">, "is GPU device">;
def FusedBiasAddGeluPattern : Pat<
(
OneFlow_GeluOp : $gelu_op
(
OneFlow_BiasAddOp
$a,
$b,
$bias_add_op_name,
$bias_add_device_tag,
$bias_add_device_name,
$bias_add_scope_symbol_id,
$bias_add_hierarchy,
$axis
),
$gelu_op_name,
$gelu_device_tag,
$gelu_device_name,
$gelu_scope_symbol_id,
$gelu_hierarchy
),
(OneFlow_FusedBiasAddGeluOp $a, $b,
$gelu_op_name,
$gelu_device_tag,
$gelu_device_name,
$gelu_scope_symbol_id,
$gelu_hierarchy,
$axis
),
[
(IsGPU $bias_add_device_tag),
(IsGPU $gelu_device_tag)
]
>;
gelu
和 bias_add
就将其进行融合为一个 fused_bias_add_gelu_op
,在CUDA上可以减少读写来提升执行效率。 第二个问题是如何让OneFlow的一些Operation享受MLIR基础设施中的更多优化?在多级Dialect 逐层下降时可以看到OneFlow的MLIR表达式的每个子函数都会被Lower。第一次会将其Lower到Tosa Dialect,这个时候如果这个子函数中的某个Operation没有定义转换到Tosa Dialect的方法,那么就不能Lower到Tosa Dialect。自然也就不能进一步下降为Linalg Dialect,享受不到一些循环变化带来的优化(我感觉可以类比TVM的scheduler优化)。
为了解决这种情况我们需要额外再定义一个Pass来将当前需要转换为Tosa的Op或者模式提取成一个函数,里面的oneflow op都能够lower到tosa,然后生成一个 oneflow mlir jit op 来 call 这个函数:
def IsNotNestedInJit: Constraint<CPred<"(!$0.getDefiningOp()->getParentOfType<::mlir::FuncOp>()->hasAttr(\"llvm.emit_c_interface\"))">, "">;
def OutlineMulCast : NativeCodeCall<"::mlir::oneflow::OutlineMulCast($_builder, $0, $1)">;
// TODO: remove attr binding if possible
def MulCastPattern : Pat<
(
OneFlow_ScalarMulByTensorOp : $mul_op
(
OneFlow_CastOp : $cast_op
$cast_x,
$cast_op_name,
$cast_device_tag,
$cast_device_name,
$cast_scope_symbol_id,
$cast_hierarchy,
$cast_dtype
),
$scalar,
$mul_op_name,
$mul_device_tag,
$mul_device_name,
$mul_scope_symbol_id,
$mul_hierarchy
),
(OutlineMulCast $mul_op, $cast_op),
[
(IsNotNestedInJit $mul_op)
]
>;
::llvm::SmallVector<::mlir::Value, 4> OutlineMulCast(::mlir::PatternRewriter& rewriter,
mlir::OpResult mul_res,
mlir::OpResult cast_res) {
if (auto mul_op = llvm::dyn_cast<ScalarMulByTensorOp>(mul_res.getDefiningOp())) {
if (auto cast_op = llvm::dyn_cast<CastOp>(cast_res.getDefiningOp())) {
// TODO: extract a function to generate op name for jit op from ops being fused
SmallString<64> op_name_storage;
auto op_name =
(cast_op.op_name() + "__FUSE__" + mul_op.op_name()).toStringRef(op_name_storage);
SmallVector<::mlir::Value, 2> operands;
operands.push_back(cast_op.in());
operands.push_back(mul_op.scalar());
SmallVector<::mlir::Value, 1> results;
results.push_back(mul_op.y());
NamedAttrList attributes =
GetJitOpAttributes(rewriter, op_name, operands.size(), results.size(), mul_op);
SmallVector<Operation*, 4> ops = {cast_op, mul_op};
auto function =
GetOrInsertFuncOp(rewriter, mul_op->getLoc(), op_name, operands, results, ops);
auto created = rewriter.create<MlirJitOp>(mul_op.getLoc(), function, attributes, operands);
assert(DumpAssembly(rewriter, created).succeeded());
cast_op->dropAllUses();
cast_op.erase();
return created->getResults();
}
}
return {};
}
void populateFuserPasses(::mlir::RewritePatternSet& patterns) {
patterns.add<MulCastPattern>(patterns.getContext());
}
本文分享自微信公众号 - OneFlow(OneFlowTechnology)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
沈冰阳:强化学习在推荐冷启动优化中的实践探索!
分享嘉宾:沈冰阳 58集团 算法高级工程师 编辑整理:吴祺尧加州大学圣地亚哥分校 出品平台:DataFunTalk 导读:58招聘是国内最大的蓝领招聘平台,是58集团的四大核心业务之一,每天有着上千万的职位在平台上发布,同时也有百万量级的求职者在平台上进行简历投递,达成海量连接并促进大量的成功就业。招聘业务是一个典型的双边匹配任务。我们会向B端客户(招聘者)连接C端的用户(求职者)。与业界其他推荐系统相比,58职位推荐面临着更明显的冷启动问题以及蓝领用户兴趣发散的问题。本次分享想跟大家讨论我们针对以上问题使用强化学习的模型设计与应用实践。 今天的介绍会围绕下面五点展开: 58招聘业务概述 强化学习概述 推荐中的强化学习算法 强化学习在招聘冷启动的实践 总结和展望 01 58招聘业务概述 首先和大家分享下58的招聘业务场景。 首先求职者对系统给其推荐的职位进行点击,然后对感兴趣的职位进行简历投递、发起微聊、拨打电话等操作,相当于求职者单边的意向表达。然后招聘者对于求职者发起的单边连接进行回复,比如简历反馈、微聊回复、接通电话等。在进行沟通后,双方最终达成面试和入职。推荐算法工程师其实是在...
- 下一篇
突破技术限制,实现Web端静默打印
作为Web开发的同僚们,估计都有一个共同的烦恼,Web端为什么不能够像 CS端那样直接打印预览?直接移除掉打印预览界面不就可以了? 真实情况是Web端受限于浏览器的权限,无法直接访问打印机等本机资源。所以,在Web上实现无预览和打印并不是一个简单的问题,而是突破权限、突破平台的问题。这就导致了用户在打印报表时,至少需要点击两个按钮才能完成打印,如果是需要批量打印的场景,用户则需要重复多次点击按钮,非常麻烦。 而不了解详细内情的甲方则会非常不解,这么简单的一个诉求,只是省去点击一个操作步骤,为什么你们办不到呢? 这种对于我们卑微乙方的灵魂拷问,让我们也很头疼,不是我们办不到,是客观条件不允许我们办到。 作为一个资历比较老的 Wyn Enterprise(读音:One)嵌入式BI和ActiveReports报表控件的技术顾问,这两年接触的客户,基本不再使用 Winform或WPF了,大部分项目都迁移到B/S 端,采用前后端分离的架构,或者 MVC。 在这种情况下,客户都会面临需要打印却无法直接连接打印机,进行默认打印的能力。如果是物流行业的企业或者生产制造企业,网页端打印,都是批量性的操作...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Windows10,CentOS7,CentOS8安装Nodejs环境