from enum import Enum import functools, sys, ctypes, os, errno import numpy as np from functools import partialmethod from mlir import ir from mlir.dialects import arith, func, gpu, memref, nvgpu, scf, nvvm from mlir.extras import types as T from mlir import runtime as rt from tools import nvgpucompiler MLIR_DYNAMIC = -9223372036854775808 def const(value: int, ty=None): ty = T.index() if ty is None else ty if isinstance(value, ir.Value) and ( value.type.isinstance(value.type) or T.bool().isinstance(value.type) ): return value return arith.constant(ty, value) def get_type_size(ty): if ir.MemRefType.isinstance(ty): size = get_type_size(ty.element_type) for sz in ty.shape: size *= sz return size if ir.FloatType.isinstance(ty): return ir.FloatType(ty).width // 8 if ir.IntegerType.isinstance(ty): return ir.IntegerType(ty).width // 8 raise NotImplementedError(ty) def get_mlir_func_obj_ty(inputArgs): args = [] c_int_p = ctypes.c_int * 1 c_float_p = ctypes.c_float * 1 c_bool_p = ctypes.c_bool * 1 for arg in inputArgs: if isinstance(arg, bool): args.append(c_bool_p(arg)) elif isinstance(arg, int): args.append(c_int_p(arg)) elif isinstance(arg, float): args.append(c_float_p(arg)) elif isinstance(arg, np.ndarray): args.append( ctypes.pointer(ctypes.pointer(rt.get_ranked_memref_descriptor(arg))) ) else: raise NotImplementedError(arg) return args class Mbarriers: def __init__(self, number_of_barriers=1): self.mbar_ty = ir.Type.parse( "!nvgpu.mbarrier.group, num_barriers = " + str(number_of_barriers) + ">" ) self.mbar_group_op = nvgpu.mbarrier_create(self.mbar_ty) self.number_of_barriers = number_of_barriers def __getitem__(self, key): self.id_op = const(key) return self def init(self, count: int, predicate=None): count_op = const(count) if predicate is None: nvgpu.mbarrier_init(self.mbar_group_op, count_op, self.id_op) else: nvgpu.mbarrier_init( self.mbar_group_op, count_op, self.id_op, predicate=predicate ) def arrive(self, txcount: int = 0, predicate=None): if txcount != 0: txcount_op = const(txcount) nvgpu.mbarrier_arrive_expect_tx( self.mbar_group_op, txcount_op, self.id_op, predicate=predicate ) else: nvgpu.mbarrier_arrive( ir.Type.parse("!nvgpu.mbarrier.token"), self.mbar_group_op, self.id_op ) def try_wait(self, phase: bool = False, ticks: int = 10000000): ticks_op = const(ticks) phase_op = const(phase, T.bool()) nvgpu.MBarrierTryWaitParityOp( self.mbar_group_op, phase_op, ticks_op, mbarId=self.id_op, ) class TMA: """A class that builds a TMA descriptor.""" def __init__( self, tma_box_shape, memref_ty, swizzle=nvgpu.TensorMapSwizzleKind.SWIZZLE_NONE, l2promo=nvgpu.TensorMapL2PromoKind.L2PROMO_NONE, oob=nvgpu.TensorMapOOBKind.OOB_ZERO, interleave=nvgpu.TensorMapInterleaveKind.INTERLEAVE_NONE, ): self.swizzle = swizzle # mlir.nvgpu.TensorMapSwizzleKind self.l2promo = l2promo # mlir.nvgpu.TensorMapL2PromoKind self.oob = oob # mlir.nvgpu.TensorMapOOBKind self.interleave = interleave # mlir.nvgpu.TensorMapInterleaveKind self.tma_box_shape = tma_box_shape self.memref_ty = memref_ty # MemRefType self.tma_memref = ir.MemRefType.get(tma_box_shape, memref_ty.element_type) @property def tensormap_descriptor_ty(self): """Returns a tensormap descriptor type.""" tensorMemrefType = ir.MemRefType.get( self.tma_box_shape, self.memref_ty.element_type, memory_space=ir.Attribute.parse("3"), ) return nvgpu.TensorMapDescriptorType.get( tensorMemrefType, self.swizzle, self.l2promo, self.oob, self.interleave, ) def create_descriptor(self, device_ptr): tma_descriptor_ty = self.tensormap_descriptor_ty device_unranked_memref = memref.CastOp( ir.UnrankedMemRefType.get( self.memref_ty.element_type, self.memref_ty.memory_space ), device_ptr, ) self.tma_descriptor = nvgpu.TmaCreateDescriptorOp( tma_descriptor_ty, device_unranked_memref, map(const, self.tma_box_shape) ) return self.tma_descriptor.result def prefetch(self, predicate=None): nvgpu.tma_prefetch_descriptor(self.tma_descriptor, predicate=predicate) def load(self, dest, mbarrier: Mbarriers, coords=[0], predicate=None): nvgpu.TmaAsyncLoadOp( dest, mbarrier.mbar_group_op, self.tma_descriptor, coordinates=map(const, coords), mbarId=mbarrier.id_op, predicate=predicate, ) WARP_GROUP_SIZE = 128 # Number of threads in a warpgroup class Warpgroup: def __init__(self, primary_thread, register_size): assert (primary_thread % WARP_GROUP_SIZE) == 0 tidx = gpu.thread_id(gpu.Dimension.x) self.primary_thread = primary_thread self.register_size = register_size self.is_wg_primary = (tidx % WARP_GROUP_SIZE) == 0 self.wg_id = tidx / WARP_GROUP_SIZE self.is_me = self.wg_id == (primary_thread // WARP_GROUP_SIZE) def __enter__(self): if_op = scf.IfOp(self.is_me) self.ipoint_op = ir.InsertionPoint(if_op.then_block) self.ipoint_op.__enter__() if self.register_size < 64: nvvm.setmaxregister(self.register_size, nvvm.SetMaxRegisterAction.decrease) else: nvvm.setmaxregister(self.register_size, nvvm.SetMaxRegisterAction.increase) def __exit__(self, exc_type, exc_value, traceback): scf.yield_([]) self.ipoint_op.__exit__(exc_type, exc_value, traceback) return True class WGMMAType(Enum): Accumulator = 1 Descriptor = 2 class WGMMAMatrix: def __init__( self, matrix_type: WGMMAType, shape: list = None, desc: TMA = None, smem=None, ty=None, acc_op=None, ): if acc_op is None: self.M = shape[0] self.N = shape[1] self.ty = ty self.matrix_type = matrix_type self.desc = desc self.smem = smem if matrix_type is WGMMAType.Accumulator: self.acc_op = nvgpu.warpgroup_mma_init_accumulator(self.acc_ty) elif acc_op: self.acc_op = acc_op self.matrix_type = WGMMAType.Accumulator @property def acc_ty(self): parse_str = f"!nvgpu.warpgroup.accumulator>" return ir.Type.parse(parse_str) @property def wgmma_ty(self): parse_str = f"!nvgpu.warpgroup.descriptor>>" return ir.Type.parse(parse_str) def store_accumulator(self, dest): assert self.matrix_type == WGMMAType.Accumulator nvgpu.warpgroup_mma_store(self.acc_op, dest) def update_smem(self, smem): self.smem = smem def update_accumulator(self, acc_op): self.acc_op = acc_op def __matmul__(self, rhs): lhs = nvgpu.warpgroup_generate_descriptor( self.wgmma_ty, self.smem, self.desc.tma_descriptor ) rhs = nvgpu.warpgroup_generate_descriptor( rhs.wgmma_ty, rhs.smem, rhs.desc.tma_descriptor ) return [lhs, rhs] def __iadd__(self, matmulResult): lhs = matmulResult[0] rhs = matmulResult[1] acc_op = nvgpu.WarpgroupMmaOp( self.acc_op.type, lhs, rhs, self.acc_op, transposeB=True ) return WGMMAMatrix(WGMMAType.Accumulator, acc_op=acc_op) def get_dynamic_shared_memory(shape=None, ty=None, offset: int = 0): smem_space_str = "#gpu.address_space" smem_space = ir.Attribute.parse(smem_space_str) dynamic_smem = gpu.dynamic_shared_memory( ir.MemRefType.get((MLIR_DYNAMIC,), T.i8(), memory_space=smem_space) ) if shape is None: return dynamic_smem memref_ty = ir.MemRefType.get(shape, ty, memory_space=smem_space) return memref.view( ir.MemRefType.get( memref_ty.shape, memref_ty.element_type, memory_space=smem_space ), dynamic_smem, const(offset), [], ) def get_mlir_ty(arg): def get_mlir_ty_from_np(dtype): if dtype == np.float16: return T.f16() if dtype == np.float32: return T.f32() if dtype == np.float64: return T.f64() if dtype == np.int32: return T.i32() if dtype == np.int64: return T.i64() raise NotImplementedError(dtype) if isinstance(arg, bool): return T.bool() elif isinstance(arg, int): return T.index() elif isinstance(arg, float): return T.f32() elif isinstance(arg, np.ndarray): descriptor = rt.get_ranked_memref_descriptor(arg) dtype = get_mlir_ty_from_np(arg.dtype) shape = descriptor.shape return memref.MemRefType.get(shape, dtype) raise NotImplementedError(arg) class NVDSL: @staticmethod def mlir_gpu_launch(grid=(1, 1, 1), block=(1, 1, 1), smem=0): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): launch_op = gpu.LaunchOp( None, [], *map(const, grid), *map(const, block), dynamicSharedMemorySize=arith.constant(T.i32(), smem), ) launch_op.body.blocks.append(*([T.index()] * 12)) with ir.InsertionPoint(launch_op.body.blocks[0]): result = func(*args, **kwargs) gpu.terminator() return result return wrapper return decorator @staticmethod def mlir_func(funcBody): @functools.wraps(funcBody) def wrapper(*args, **kwargs): function_name = funcBody.__name__ def saveIR(module): """Save generated IR""" if True: # self.saveIR: # print(mlir_nvgpu_module) original_stdout = sys.stdout with open("nvdsl.mlir", "w") as f: sys.stdout = f print(module) sys.stdout = original_stdout def _binary_op(lhs, rhs, op: str, predAtt="") -> "ArithValue": """Generate MLIR's Arith dialects binary operations.""" rhs = const(rhs) if arith._is_float_type(lhs.type) and arith._is_float_type(rhs.type): op += "F" if op.startswith("Cmp"): predicateAttr = getattr(arith, f"CmpFPredicate").__dict__[ predAtt ] elif arith._is_integer_like_type( lhs.type ) and arith._is_integer_like_type(lhs.type): if op == "Div" or op == "Rem": op += "U" op += "I" if op.startswith("Cmp"): predicateAttr = getattr(arith, f"CmpIPredicate").__dict__[ predAtt ] else: raise NotImplementedError( f"Unsupported '{op}' operands: {lhs}, {rhs}" ) if op.startswith("Cmp"): op = getattr(arith, f"{op}Op") return op(predicateAttr, lhs, rhs).result else: op = getattr(arith, f"{op}Op") return op(lhs, rhs).result @ir.register_value_caster(ir.IndexType.static_typeid) @ir.register_value_caster(ir.F32Type.static_typeid) @ir.register_value_caster(ir.F16Type.static_typeid) @ir.register_value_caster(ir.F64Type.static_typeid) @ir.register_value_caster(ir.IntegerType.static_typeid) class ArithValue(ir.Value): """Overloads operators for MLIR's Arith dialects binary operations.""" def __init__(self, v): super().__init__(v) __add__ = partialmethod(_binary_op, op="Add") __sub__ = partialmethod(_binary_op, op="Sub") __mul__ = partialmethod(_binary_op, op="Mul") __truediv__ = partialmethod(_binary_op, op="Div") __mod__ = partialmethod(_binary_op, op="Rem") __xor__ = partialmethod(_binary_op, op="XOr") __lt__ = partialmethod(_binary_op, op="Cmp", predAtt="ult") __le__ = partialmethod(_binary_op, op="Cmp", predAtt="ule") __eq__ = partialmethod(_binary_op, op="Cmp", predAtt="eq") __ne__ = partialmethod(_binary_op, op="Cmp", predAtt="ne") __gt__ = partialmethod(_binary_op, op="Cmp", predAtt="ugt") __ge__ = partialmethod(_binary_op, op="Cmp", predAtt="uge") __and__ = partialmethod(_binary_op, op="And") __or__ = partialmethod(_binary_op, op="Or") def __str__(self): return ( super() .__str__() .replace(ir.Value.__name__, ArithValue.__name__) ) # Generate MLIR Context and start generating IR with ir.Context(), ir.Location.unknown(): types = [] for arg in args: types.append(get_mlir_ty(arg)) # Build IR module = ir.Module.create() with ir.InsertionPoint(module.body): fop = func.FuncOp(function_name, (types, [])) fop.attributes["llvm.emit_c_interface"] = ir.UnitAttr.get() with ir.InsertionPoint(fop.add_entry_block()): fargs = [] for i, a in enumerate(types): fargs.append(fop.arguments[i]) # Call user function body result = funcBody(*fargs, **kwargs) func.ReturnOp([]) # Save IR in a file # saveIR(module) # Verify the module module.operation.verify() # Compile and JIT MLIR module options = f"cubin-chip=sm_90a cubin-features=+ptx80 opt-level=3" support_lib = os.getenv("SUPPORT_LIB") if not os.path.exists(support_lib): raise FileNotFoundError( errno.ENOENT, os.strerror(errno.ENOENT), support_lib ) compiler = nvgpucompiler.NvgpuCompiler( options, opt_level=3, shared_libs=[support_lib] ) engine = compiler.compile_and_jit(module) # Convert input arguments to MLIR arguments newArgs = get_mlir_func_obj_ty(args) # Run the compiled program engine.invoke(function_name, *newArgs) return result return wrapper