Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

400 lines
17 KiB
Python

"""A compiler pass that rewrites IR for pipeline parallelism."""
from typing import Dict, List, Optional, Tuple # noqa: UP035
import tvm
from tvm import relax, tirx
from tvm.ir.module import IRModule
from tvm.relax.expr_functor import PyExprMutator, PyExprVisitor, mutator, visitor
@tvm.transform.module_pass(opt_level=0, name="PipelineParallelRewrite")
class PipelineParallelRewrite:
"""A compiler pass that rewrites IR for pipeline parallelism."""
def transform_module(
self,
mod: IRModule,
_ctx: tvm.transform.PassContext,
) -> IRModule:
"""IRModule-level transformation"""
return _PipelineParallelRewriter(mod.clone()).transform()
@mutator
class _PipelineParallelRewriter(PyExprMutator):
def __init__(self, mod: IRModule):
super().__init__(mod)
self.mod = mod
self.old_packed_params_var: relax.Var
self.new_main_packed_params_var: relax.Var
self.new_stage_func_packed_params: relax.Var
self.undefined_shape_vars_remap: Dict[tirx.Var, tirx.Var] # noqa: UP006
self.undefined_param_shape_vars_remap: Dict[tirx.Var, tirx.Var] # noqa: UP006
def transform(self) -> IRModule:
"""Entry point of the transformation"""
for g_var, func in self.mod.functions_items():
if not isinstance(func, relax.Function) or "pipeline_parallel_stages" not in func.attrs:
continue
num_stages = int(func.attrs["pipeline_parallel_stages"])
if num_stages == 1:
continue
pipeline_stages, stage_send_vars, stage_receive_vars = _extract_pipeline_stages(func)
assert len(pipeline_stages) == num_stages, (
"Number of pipeline stages mismatches: "
f"expecting {num_stages} stages, but {len(pipeline_stages)} are found in the IR."
)
required_func_params = _analyze_required_func_params(pipeline_stages, func.params)
assert "num_input" in func.attrs
num_input = int(func.attrs["num_input"])
assert (
len(func.params) == num_input + 1
and isinstance(func.params[num_input], relax.Var)
and func.params[num_input].name_hint == "packed_params"
), 'Only the extra "packed_params" parameter is allowed'
self.old_packed_params_var = func.params[num_input]
self.new_main_packed_params_var = relax.Var("packed_params", relax.ObjectType())
for required_params in required_func_params:
for i, param in enumerate(required_params):
if param.same_as(self.old_packed_params_var):
required_params.pop(i)
break
func_output = func.body.body
assert isinstance(func_output, relax.Var)
stage_func_gvs = []
caller_args_list = []
for i in range(num_stages):
stage_func_gv, caller_args = self._create_stage_func(
g_var.name_hint + f"_stage{i}",
pipeline_stages[i],
required_func_params[i],
stage_receive_vars[i],
stage_send_vars[i],
func.attrs,
func_output=func_output if i == num_stages - 1 else None,
)
stage_func_gvs.append(stage_func_gv)
caller_args_list.append(caller_args)
# Create and update the entry function, which dispatches toz the stage functions
# according to the disco worker group id.
bb = relax.BlockBuilder()
params = [*list(func.params[:-1]), self.new_main_packed_params_var]
with bb.function(g_var.name_hint, params=params):
dispatch_func_args = []
for stage_func_gv, caller_args in zip(stage_func_gvs, caller_args_list):
dispatch_func_args.append([stage_func_gv, *caller_args])
output = bb.emit(
relax.op.call_builtin_with_ctx(
"mlc.multi_gpu.DispatchFunctionByGroup",
args=[dispatch_func_args],
ty_args=relax.ObjectType(),
)
)
dispatch_func_gv = bb.emit_func_output(output)
dispatch_func = bb.finalize()[dispatch_func_gv]
self.builder_.update_func(g_var, dispatch_func)
return self.builder_.finalize()
def _create_stage_func(
self,
func_name: str,
stage_bindings: List[relax.Binding], # noqa: UP006
required_func_params: List[relax.Var], # noqa: UP006
stage_receive_vars: List[relax.Var], # noqa: UP006
stage_send_vars: List[relax.Var], # noqa: UP006
func_attrs: tvm.ir.DictAttrs,
func_output: Optional[relax.Var],
) -> Tuple[tvm.ir.GlobalVar, List[relax.Expr]]: # noqa: UP006
self.undefined_shape_vars_remap = {}
self.undefined_param_shape_vars_remap = {}
# Prepare the func parameters (except the shape variables and packed params)
params, args = self._prepare_stage_func_params_and_args(required_func_params)
for new_param, old_param in zip(params, required_func_params):
self.set_var_remap(old_param, new_param)
# Create new packed params
self.new_stage_func_packed_params = relax.Var("packed_params", relax.ObjectType())
self.set_var_remap(self.old_packed_params_var, self.new_stage_func_packed_params)
new_func_outputs = []
with self.builder_.function(func_name, pure=False):
with self.builder_.dataflow():
# Emit the tensors received from last stage.
for receive_var in stage_receive_vars:
new_receive_var = self.builder_.emit(
relax.call_dps_packed(
"runtime.disco.recv_from_prev_group",
args=[],
out_ty=self._update_struct_info(receive_var.ty),
),
name_hint=receive_var.name_hint,
)
self.set_var_remap(receive_var, new_receive_var)
# Process the bindings in this stage.
for stage_binding in stage_bindings:
if stage_binding.var in stage_send_vars or stage_binding.var.same_as(
func_output
):
assert isinstance(stage_binding, relax.VarBinding)
new_var = self.builder_.emit_output(
self.visit_expr(stage_binding.value),
name_hint=stage_binding.var.name_hint,
)
self.set_var_remap(stage_binding.var, new_var)
new_func_outputs.append(new_var)
else:
self.visit_binding(stage_binding)
# Emit the calls to send tensors to the next stage.
for send_var in stage_send_vars:
new_send_var = self.get_var_remap(send_var)
self.builder_.emit(
relax.Call(
relax.ExternFunc("runtime.disco.send_to_next_group"),
args=[new_send_var],
ty_args=None,
)
)
# Create the param for the shape variables.
shape_var_params = []
shape_var_args = []
for (
shape_var_arg,
shape_var_param,
) in self.undefined_shape_vars_remap.items():
if shape_var_arg not in self.undefined_param_shape_vars_remap:
shape_var_params.append(shape_var_param)
shape_var_args.append(shape_var_arg)
params.append(relax.Var("s", relax.ShapeType(shape_var_params)))
args.append(relax.ShapeExpr(shape_var_args))
# Add the packed params.
params.append(self.new_stage_func_packed_params)
args.append(self.new_main_packed_params_var)
# Conclude the function.
if func_output is not None:
assert len(new_func_outputs) == 1
new_gv = self.builder_.emit_func_output(
(
new_func_outputs[0]
if len(new_func_outputs) == 1
and isinstance(new_func_outputs[0].ty, relax.TupleType)
else new_func_outputs
),
params=params,
)
new_func = (
self.builder_.get()[new_gv]
.with_attrs(func_attrs)
.with_attr("num_input", len(params) - 1)
.without_attr("global_symbol")
.without_attr("pipeline_parallel_stages")
)
self.builder_.update_func(new_gv, new_func)
return new_gv, args
def visit_var_binding_(self, binding: relax.VarBinding) -> None:
if not isinstance(binding.value, relax.TupleGetItem):
super().visit_var_binding_(binding)
return
tuple_value = self.visit_expr(binding.value.tuple_value)
if not tuple_value.same_as(self.new_stage_func_packed_params):
super().visit_var_binding_(binding)
return
assert isinstance(binding.var.ty, relax.TensorType)
cur_num_undefined_param_shape_vars = len(self.undefined_param_shape_vars_remap)
new_tensor_struct_info = self._update_struct_info(
binding.var.ty, self.undefined_param_shape_vars_remap
)
has_new_undefined_shape_var = (
len(self.undefined_param_shape_vars_remap) != cur_num_undefined_param_shape_vars
)
self.undefined_shape_vars_remap = {
**self.undefined_shape_vars_remap,
**self.undefined_param_shape_vars_remap,
}
ret_sinfo = (
new_tensor_struct_info if not has_new_undefined_shape_var else relax.ObjectType()
)
call = relax.call_pure_packed(
"vm.builtin.tuple_getitem",
self.new_stage_func_packed_params,
relax.prim_value(binding.value.index),
ty_args=ret_sinfo,
)
new_binding_var = self.builder_.emit(call, binding.var.name_hint)
if has_new_undefined_shape_var:
new_binding_var = self.builder_.match_cast(
new_binding_var, new_tensor_struct_info, binding.var.name_hint + "_cast"
)
self.set_var_remap(binding.var, new_binding_var)
def visit_call_(self, call: relax.Call) -> relax.Call:
call = super().visit_call_(call)
return relax.Call(
call.op,
call.args,
call.attrs,
ty_args=[self._update_struct_info(struct_info) for struct_info in call.ty_args],
)
def _prepare_stage_func_params_and_args(
self,
required_func_params: List[relax.Var], # noqa: UP006
) -> Tuple[List[relax.Var], List[relax.Expr]]: # noqa: UP006
params: List[relax.Var] = [] # noqa: UP006
args: List[relax.Expr] = [] # noqa: UP006
for required_param in required_func_params:
struct_info = self._update_struct_info(required_param.ty)
params.append(relax.Var(required_param.name_hint, struct_info))
args.append(required_param)
return params, args
def _update_struct_info(
self,
struct_info: relax.Type,
undefined_var_remap: Optional[Dict[tirx.Var, tirx.Var]] = None, # noqa: UP006
) -> relax.Type:
if undefined_var_remap is None:
undefined_var_remap = self.undefined_shape_vars_remap
if isinstance(struct_info, relax.TensorType):
return (
relax.TensorType(
self._update_shape(struct_info.shape.values, undefined_var_remap),
struct_info.dtype,
)
if struct_info.shape is not None and isinstance(struct_info.shape, relax.ShapeExpr)
else struct_info
)
if isinstance(struct_info, relax.ShapeType):
return (
relax.ShapeType(self._update_shape(struct_info.values, undefined_var_remap))
if struct_info.values is not None
else struct_info
)
if isinstance(struct_info, relax.ObjectType):
return relax.ObjectType()
if isinstance(struct_info, relax.TupleType):
return relax.TupleType(
[self._update_struct_info(field_sinfo) for field_sinfo in struct_info.fields]
)
return struct_info
def _copy_undefined_var(
self,
expr: tirx.Expr,
undefined_var_remap: Dict[tirx.Var, tirx.Var], # noqa: UP006
) -> None:
def _visit_expr(e: tirx.Expr) -> None:
if isinstance(e, tirx.Var) and e not in undefined_var_remap:
new_var = tirx.Var(e.name, e.ty)
undefined_var_remap[e] = new_var
tirx.stmt_functor.post_order_visit(expr, _visit_expr)
def _update_shape(
self,
shape: List[tirx.Expr], # noqa: UP006
undefined_var_remap: Dict[tirx.Var, tirx.Var], # noqa: UP006
) -> List[tirx.Expr]: # noqa: UP006
new_shape = []
for v in shape:
self._copy_undefined_var(v, undefined_var_remap)
new_shape.append(tirx.stmt_functor.substitute(v, undefined_var_remap))
return new_shape
def _extract_pipeline_stages(
func: relax.Function,
) -> Tuple[List[List[relax.Binding]], List[List[relax.Var]], List[List[relax.Var]]]: # noqa: UP006
pipeline_stages: List[List[relax.Binding]] = [] # noqa: UP006
stage_send_vars: List[List[relax.Var]] = [] # noqa: UP006
stage_receive_vars: List[List[relax.Var]] = [] # noqa: UP006
# Requiring that the function has only one body block which is a dataflow block
assert isinstance(func.body, relax.SeqExpr)
assert len(func.body.blocks) == 1
assert isinstance(func.body.blocks[0], relax.DataflowBlock)
bindings = func.body.blocks[0].bindings
boundary_var = None
current_stage_bindings: List[relax.Binding] = [] # noqa: UP006
current_stage_receive_vars: List[relax.Var] = [] # noqa: UP006
for binding in bindings:
if (
isinstance(binding, relax.VarBinding)
and isinstance(binding.value, relax.Call)
and binding.value.op == tvm.ir.Op.get("relax.call_pure_packed")
and binding.value.args[0].global_symbol == "mlc.pipeline_parallel_stage_boundary"
):
assert len(current_stage_bindings) > 0
pipeline_stages.append(current_stage_bindings)
assert all(receive_var is not None for receive_var in current_stage_receive_vars)
stage_receive_vars.append(current_stage_receive_vars)
args = binding.value.args[1:]
assert len(args) >= 1 and all(isinstance(arg, relax.Var) for arg in args)
stage_send_vars.append(list(args))
boundary_var = binding.var
current_stage_bindings = []
current_stage_receive_vars = [boundary_var] if len(args) == 1 else [None for _ in args]
elif (
isinstance(binding, relax.VarBinding)
and isinstance(binding.value, relax.TupleGetItem)
and binding.value.tuple_value.same_as(boundary_var)
):
current_stage_receive_vars[binding.value.index] = binding.var
else:
current_stage_bindings.append(binding)
assert len(current_stage_bindings) > 0
pipeline_stages.append(current_stage_bindings)
assert all(receive_var is not None for receive_var in current_stage_receive_vars)
stage_receive_vars.append(current_stage_receive_vars)
stage_send_vars.append([])
return pipeline_stages, stage_send_vars, stage_receive_vars
def _analyze_required_func_params(
pipeline_stages: List[List[relax.Binding]], # noqa: UP006
func_params: List[relax.Var], # noqa: UP006
) -> List[List[relax.Var]]: # noqa: UP006
analyzer = _RequiredFuncParamAnalyzer(func_params)
required_func_params: List[List[relax.Var]] = [] # noqa: UP006
for stage_bindings in pipeline_stages:
required_params: List[relax.Var] # noqa: UP006
required_params = analyzer.run(stage_bindings)
required_func_params.append(required_params)
return required_func_params
@visitor
class _RequiredFuncParamAnalyzer(PyExprVisitor):
"""The IR visitor which analyzes the required func parameters in each pipeline stage."""
def __init__(self, func_params: List[relax.Var]) -> None: # noqa: UP006
self.func_params = set(func_params)
self.required_params: List[relax.Var] # noqa: UP006
def run(self, stage_bindings: List[relax.Binding]) -> List[relax.Var]: # noqa: UP006
"""Entry point of the visitor."""
self.required_params = []
for binding in stage_bindings:
self.visit_binding(binding)
return self.required_params
def visit_var_(self, var: relax.Var) -> None:
if var in self.func_params:
if var not in self.required_params:
self.required_params.append(var)