# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Rewrite all-reduce operation to customized all-reduce impl with IPC memory. The pass is written in Python for experiment, fast development. """ import tvm from tvm import relax from tvm.ir.module import IRModule from tvm.relax.expr import Expr, Var from tvm.relax.expr_functor import PyExprMutator, PyExprVisitor, mutator, visitor @tvm.transform.module_pass(opt_level=0, name="IPCAllReduceRewrite") class IPCAllReduceRewrite: """Rewrite all-reduce operation to customized all-reduce impl with IPC memory.""" def __init__(self, allreduce_strategy: int) -> None: """Constructor Parameters ---------- allreduce_strategy : int The all-reduce strategy. Only "1" and "2" are supported. "1" stands for one-shot, and "2" stands for two-shot. """ self.allreduce_strategy = allreduce_strategy def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule: """IRModule-level transformation""" fcustom_allreduce = tvm.get_global_func( "runtime.disco.cuda_ipc.custom_allreduce", allow_missing=True ) if fcustom_allreduce is None: # Customized allreduce is not available. return mod binding_replacement_map = _Visitor(self.allreduce_strategy).visit(mod) return _Rewriter(mod, binding_replacement_map).transform() @visitor class _Visitor(PyExprVisitor): # pylint: disable=abstract-method def __init__(self, allreduce_strategy: int) -> None: self.allreduce_strategy = allreduce_strategy self.alloc_map: dict[Var, relax.Call] = {} self.binding_replacement_map: dict[relax.Expr, relax.Expr] = {} self.builtin_alloc_tensor_op = tvm.ir.Op.get("relax.builtin.alloc_tensor") self.reshape_op = tvm.ir.Op.get("relax.reshape") def visit(self, mod: IRModule) -> dict[relax.Expr, relax.Expr]: """Entry point""" for _, func in mod.functions_items(): if isinstance(func, relax.Function): self.alloc_map.clear() self.visit_expr(func) return self.binding_replacement_map def visit_var_binding_(self, binding: relax.VarBinding): super().visit_var_binding_(binding) if ( isinstance(binding.value, relax.Call) and binding.value.op == self.builtin_alloc_tensor_op ): self.alloc_map[binding.var] = binding.value elif isinstance(binding.value, relax.Var) and binding.value in self.alloc_map: self.alloc_map[binding.var] = self.alloc_map[binding.value] elif ( isinstance(binding.value, relax.Call) and binding.value.op == self.reshape_op and binding.value.args[0] in self.alloc_map ): self.alloc_map[binding.var] = self.alloc_map[binding.value.args[0]] def visit_call_(self, call: relax.Call) -> None: # pylint: disable=arguments-renamed if ( not isinstance(call.op, relax.ExternFunc) or call.op.global_symbol != "runtime.disco.allreduce" or call.args[1].values[0] != 0 ): # Return if the call is not a summation all-reduce. return assert len(call.args) == 4 allreduce_input, _strategy, _ingroup, allreduce_output = call.args alloc_tensor = self.alloc_map.get(allreduce_input, None) if alloc_tensor is None or alloc_tensor.args[3].value != "global": # Return if the allocation of all-reduce input is not recorded, # or the scope of the allocation is not global. return # Set the scope of the alloc_tensor to IPC memory. alloc_tensor = self.alloc_map[allreduce_input] self.binding_replacement_map[alloc_tensor] = relax.op.builtin.alloc_tensor( alloc_tensor.args[0], alloc_tensor.args[1], alloc_tensor.args[2], relax.StringImm("ipc_memory"), ) self.binding_replacement_map[call] = relax.Call( relax.ExternFunc("runtime.disco.cuda_ipc.custom_allreduce"), # The "cuda_ipc.custom_allreduce" implementation does not # yet support num_groups>1, and therefore does not use the # `in_group` argument. [allreduce_input, relax.prim_value(self.allreduce_strategy), allreduce_output], ) @mutator class _Rewriter(PyExprMutator): """Rewrite the IRModule according to the binding replacement provided by the visitor.""" def __init__( self, mod: IRModule, binding_replacement_map: dict[relax.Expr, relax.Expr] ) -> None: super().__init__(mod) self.mod = mod self.binding_replacement_map = binding_replacement_map def transform(self) -> IRModule: """Entry point""" for g_var, func in self.mod.functions_items(): if isinstance(func, relax.Function): updated_func = self.visit_expr(func) self.builder_.update_func(g_var, updated_func) return self.builder_.get() def visit_call_(self, call: relax.Call) -> Expr: # pylint: disable=arguments-renamed return ( super().visit_call_(self.binding_replacement_map[call]) if call in self.binding_replacement_map else super().visit_call_(call) )