Files
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

446 lines
18 KiB
Python

# 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.
# pylint: disable=import-outside-toplevel, unused-argument
"""StableHLO frontend of Relax."""
from collections.abc import Callable
from typing import Any
import tvm
from tvm import relax, tirx
class StableHLOImporter:
"""An importer from StableHLO to Relax."""
from jaxlib import mlir
from jaxlib.mlir.dialects import stablehlo
def __init__(self) -> None:
from jaxlib import mlir
self._nodes: dict[str | mlir.ir.Operation, relax.Expr] = {}
self.block_builder: relax.BlockBuilder = None
self.create_convert_map()
@staticmethod
def _convert_data_type(input_type):
"""converts the data type from mlir to tvm."""
from jaxlib import mlir
if mlir.ir.ShapedType.isinstance(input_type):
input_type = mlir.ir.ShapedType(input_type).element_type
input_type = str(input_type)
if input_type == "f16":
return "float16"
elif input_type in ["f32", "F32Type"]:
return "float32"
elif input_type in ["f64", "F64Type"]:
return "float64"
elif input_type == "i1":
return "bool"
elif input_type == "i8":
return "int8"
elif input_type == "i16":
return "int16"
elif input_type == "i32":
return "int32"
elif input_type == "i64":
return "int64"
elif input_type == "ui8":
return "uint8"
elif input_type == "ui16":
return "uint16"
elif input_type == "ui32":
return "uint32"
elif input_type == "ui64":
return "uint64"
else:
raise NotImplementedError(f"input_type {input_type} is not handled yet")
def _attr2value(self, node) -> Any | list[Any]:
import numpy as np
from jaxlib import mlir
if mlir.ir.IntegerAttr.isinstance(node):
int_attr = mlir.ir.IntegerAttr(node)
return int_attr.value
if mlir.ir.FloatAttr.isinstance(node):
float_attr = mlir.ir.FloatAttr(node)
return float_attr.value
if mlir.ir.DenseIntElementsAttr.isinstance(node):
dense_attr = mlir.ir.DenseIntElementsAttr(node)
elif mlir.ir.DenseFPElementsAttr.isinstance(node):
dense_attr = mlir.ir.DenseFPElementsAttr(node)
else:
raise ValueError("Unsupported Attribute type: " + str(type(node)))
ret = []
for val in dense_attr:
ret.append(val)
shape = self.get_shape(node.type)
dtype = self._convert_data_type(node.type)
return np.asarray(ret, dtype).reshape(shape).tolist()
def retrieve_operands(self, node):
return self._retrieve_operands(node.operands)
def _retrieve_operands(self, node):
from jaxlib import mlir
# the operand is one of the inputs of FuncOp
if isinstance(node, mlir.ir.Operation):
return self._nodes[node]
if isinstance(node, tuple):
return tuple(self._retrieve_operands(x) for x in node)
if isinstance(node, list | mlir.ir.OpOperandList):
return [self._retrieve_operands(x) for x in node]
if isinstance(node, dict):
return {self._retrieve_operands(k): self._retrieve_operands(v) for k, v in node.items()}
if isinstance(node, mlir.ir.Value):
if isinstance(node.owner, mlir.ir.Block):
block_arg = mlir.ir.BlockArgument(node)
return self._nodes["arg" + str(block_arg.arg_number)]
return self._retrieve_operands(node.owner)
return node
def get_shape(self, inpt_type) -> list[Any]:
"""Get the shape from Type like tensor<?x?xf32>"""
from jaxlib import mlir
shape_type = inpt_type
if isinstance(shape_type, mlir.ir.Type):
shape_type = mlir.ir.ShapedType(shape_type)
ret = []
for i in range(shape_type.rank):
# get_dim_size
if shape_type.is_dynamic_dim(i):
n = tirx.Var("n", "int64")
ret.append(n)
else:
ret.append(shape_type.get_dim_size(i))
return ret
@staticmethod
def _promote_binary_op_args(lhs, rhs):
if not isinstance(lhs, relax.Expr) and not isinstance(rhs, relax.Expr):
msg = "Both the lhs and the rhs are not expressions."
raise AssertionError(msg)
if isinstance(lhs, relax.Expr) and isinstance(rhs, relax.Expr):
return lhs, rhs
if isinstance(lhs, relax.Expr):
assert isinstance(lhs.ty, relax.TensorType)
return lhs, relax.const(rhs, lhs.ty.dtype)
assert isinstance(rhs.ty, relax.TensorType)
return relax.const(lhs, rhs.ty.dtype), rhs
def _call_binary_op(self, op, lhs, rhs):
lhs, rhs = StableHLOImporter._promote_binary_op_args(lhs, rhs)
return self.block_builder.emit(op(lhs, rhs))
def _add(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
if isinstance(lhs, relax.Var) or isinstance(rhs, relax.Var):
return self._call_binary_op(relax.op.add, lhs, rhs)
return lhs + rhs
def _maximum(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.maximum(lhs, rhs))
def _minimum(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.minimum(lhs, rhs))
def _divide(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
if isinstance(lhs, relax.Var) or isinstance(rhs, relax.Var):
return self._call_binary_op(relax.op.divide, lhs, rhs)
return lhs / rhs
def _multiply(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
if isinstance(lhs, relax.Var) or isinstance(rhs, relax.Var):
return self._call_binary_op(relax.op.multiply, lhs, rhs)
return lhs * rhs
def _subtract(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
if isinstance(lhs, relax.Var) or isinstance(rhs, relax.Var):
return self._call_binary_op(relax.op.subtract, lhs, rhs)
return lhs - rhs
def _broadcast_in_dim(self, node: mlir.ir.Operation) -> relax.Expr:
operands = self.retrieve_operands(node)
data = operands[0]
# broadcast_dims = self._attr2value(node.attributes["broadcast_dimensions"])
shape = self.get_shape(node.result.type)
# scalar
if len(shape) == 0:
return data
return self.block_builder.emit(relax.op.broadcast_to(data, shape))
def _const(self, node: mlir.ir.Operation) -> relax.Expr:
const_value = self._attr2value(node.attributes["value"])
dtype = self._convert_data_type(node.result.type)
return relax.const(const_value, dtype)
def _dot_general(self, node: mlir.ir.Operation) -> relax.Expr:
lhs, rhs = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.matmul(lhs, rhs))
def _convolution(self, node) -> relax.Expr:
from jaxlib import mlir
x, weight = self.retrieve_operands(node)
shaped_type = mlir.ir.ShapedType(node.result.type)
out_dtype = self._convert_data_type(shaped_type.element_type)
strides = self._attr2value(node.attributes["window_strides"])
padding = self._attr2value(node.attributes["padding"])
lhs_dilation = self._attr2value(node.attributes["lhs_dilation"])
rhs_dilation = self._attr2value(node.attributes["rhs_dilation"])
if len(lhs_dilation) > 0:
lhs_dilation = lhs_dilation[0]
if len(rhs_dilation) > 0:
rhs_dilation = rhs_dilation[0]
dilation = (lhs_dilation, rhs_dilation)
groups = self._attr2value(node.attributes["batch_group_count"])
conv2d = relax.op.nn.conv2d(
x,
weight,
strides=strides,
padding=padding[0],
dilation=dilation,
groups=groups,
data_layout="NHWC",
kernel_layout="HWIO",
out_dtype=out_dtype,
)
return self.block_builder.emit(conv2d)
def _reshape(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
if isinstance(data, list):
assert len(data) == 1
data = data[0]
new_shape = self.get_shape(node.result.type)
return self.block_builder.emit(relax.op.reshape(data, new_shape))
def _reduce(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
dimensions = self._attr2value(node.attributes["dimensions"])
if node.body is not None:
reducer_op = node.body.blocks[0].operations[0].OPERATION_NAME
assert reducer_op == "stablehlo.add", f"reducer {reducer_op} in reduce is not supported"
return self.block_builder.emit(relax.op.sum(data[0], axis=dimensions))
def _reduce_window(self, node: mlir.ir.Operation) -> relax.Expr:
operands = self.retrieve_operands(node)
window_dimensions = self._attr2value(node.attributes["window_dimensions"])
window_dilations = self._attr2value(node.attributes["window_dilations"])
if node.body is not None:
reducer_op = node.body.blocks[0].operations[0].OPERATION_NAME
assert reducer_op == "stablehlo.maximum", (
f"the reducer {reducer_op} in reduce_window is not supported"
)
pool_size = []
for i, window_dim in enumerate(window_dimensions):
if window_dim == 0:
pool_size.append(0)
else:
dilated_window_size = (window_dim - 1) * window_dilations[i] + 1
pool_size.append(dilated_window_size)
strides = self._attr2value(node.attributes["window_strides"])
# padding = self._attr2value(node.attributes["padding"])
# TODO (yongwww): Infer the layout automatically
layout = "NHWC"
ret = self.block_builder.emit(
relax.op.nn.max_pool2d(
operands[0],
pool_size=pool_size[1:3], # HW
strides=strides[1:3],
padding=[1, 1],
dilation=window_dilations[1:3],
layout=layout,
)
)
return ret
def _rsqrt(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.rsqrt(data[0]))
def _sin(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.sin(data[0]))
def _sinh(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.sinh(data[0]))
def _cos(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.cos(data[0]))
def _cosh(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.cosh(data[0]))
def _sqrt(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.sqrt(data[0]))
def _round(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.round(data[0]))
def _exp(self, node: mlir.ir.Operation) -> relax.Expr:
data = self.retrieve_operands(node)
return self.block_builder.emit(relax.op.exp(data[0]))
def _return(self, node: mlir.ir.Operation) -> relax.Expr:
outputs = self.retrieve_operands(node)
return self.block_builder.emit_output(self.nodes[outputs])
def create_convert_map(self):
from jaxlib import mlir
self.convert_map: dict[str, Callable[[mlir.ir.Operation], relax.Var]] = {
"stablehlo.add": self._add,
"stablehlo.broadcast_in_dim": self._broadcast_in_dim,
"stablehlo.constant": self._const,
"stablehlo.convolution": self._convolution,
"stablehlo.cosine": self._cos,
"stablehlo.cosh": self._cosh,
"stablehlo.divide": self._divide,
"stablehlo.dot_general": self._dot_general,
"stablehlo.exponential": self._exp,
"stablehlo.maximum": self._maximum,
"stablehlo.minimum": self._minimum,
"stablehlo.multiply": self._multiply,
"stablehlo.reshape": self._reshape,
"stablehlo.reduce": self._reduce,
"stablehlo.reduce_window": self._reduce_window,
"stablehlo.round_nearest_afz": self._round,
"stablehlo.rsqrt": self._rsqrt,
"stablehlo.sine": self._sin,
"chlo.sinh": self._sinh,
"stablehlo.sqrt": self._sqrt,
"stablehlo.subtract": self._subtract,
"func.return": self._return,
"stablehlo.return": self._return,
}
def from_stablehlo(self, model, input_info: list[tuple[tuple[int], str]]) -> tvm.IRModule:
"""Convert a StableHLO Module to a Relax program.
Parameters
----------
model : mlir.ir.Module
The StableHLO Module to convert.
input_info : List[Tuple[Tuple[int], str]]
A list of shapes and data types of input tensors.
Returns
-------
output : tvm.IRModule
The result IRModule with entry function "main"
"""
from jaxlib import mlir
from jaxlib.mlir.dialects import stablehlo
assert isinstance(model, mlir.ir.Module)
block: mlir.ir.Block = model.body.operations[0].regions[0].blocks[0]
# inputs of the function
inputs = []
for idx, arg in enumerate(block.arguments.types):
arg_shape = mlir.ir.ShapedType(arg)
ipt_shape = self.get_shape(arg_shape)
ipt_dtype = self._convert_data_type(arg_shape.element_type)
ipt_name = "arg" + str(idx)
ipt_var = relax.Var(f"arg{idx}", relax.TensorType(ipt_shape, ipt_dtype))
self._nodes[ipt_name] = ipt_var
inputs.append(ipt_var)
# TODO (yongwww): Handle mlir.ir.Module with multiple functions
# Initialize the block builder with a function and a dataflow block.
# Raise error if the input stablehlo op is impure
func_name = "main"
self.block_builder = relax.BlockBuilder()
with self.block_builder.function(name=func_name, params=inputs.copy()):
output = None
with self.block_builder.dataflow():
block = model.body.operations[0].regions[0].blocks[0]
for operation in block.operations:
if isinstance(operation, mlir.dialects.func.ReturnOp | stablehlo.ReturnOp):
operation = operation.operands[0].owner
# TODO (yongwww): handle multiple outputs
output = self.block_builder.emit_output(self._nodes[operation])
break
if isinstance(operation, mlir.ir.OpView):
op_name = operation.operation.name
assert op_name in self.convert_map, f"Unsupported operation {op_name}"
self._nodes[operation] = self.convert_map[op_name](operation)
else:
raise ValueError(f"Unsupported op {operation}")
assert output is not None
self.block_builder.emit_func_output(output)
mod = self.block_builder.get()
return mod
def from_stablehlo(
stablehlo_module,
input_info: list[tuple[tuple[int], str]] | None = None,
) -> tvm.IRModule:
"""Convert a StableHLO Module to a Relax program
Parameters
----------
stablehlo_module : Union[str, mlir.ir.Module]
The StableHLO Module to convert.
input_info : List[Tuple[Tuple[int], str]]
A list of shapes and data types of input tensors.
Returns
-------
output : tvm.IRModule
The result IRModule with entry function "main"
"""
from jax._src.interpreters import mlir as jax_mlir
if isinstance(stablehlo_module, str):
# TODO (yongwww): support the serialized bytecode format of StableHLO
# model using stablehlo.deserialize_portable_artifact(ir) if the python
# binding is ready
context = jax_mlir.make_ir_context()
stablehlo_module = jax_mlir.ir.Module.parse(stablehlo_module, context)
return StableHLOImporter().from_stablehlo(stablehlo_module, input_info)