# 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=redefined-builtin, invalid-name, too-many-arguments, too-many-locals, too-many-positional-arguments """PTX MMA / ldmatrix / stmatrix intrinsics. mma.sync.aligned has 7 form_kinds per the PTX docs (f16 / tf32 / bf16 / fp64 / int8 / fp8 / subbyte). Each form_kind is one ``device_intrinsic`` registration; the (shape, layouts, dtypes) modifier slots are attrs. Body computes the per- fragment register counts at codegen time from M*N*bits/threads/frag_size and hand-builds the asm constraint list. ldmatrix / stmatrix each have a single PTX form (the .m8n8 .b16/.b8 variant that TIRx uses); ``num`` and ``trans`` are modifier attrs. """ import re from dataclasses import dataclass from tvm import DataType from ._schema import device_intrinsic from .registry import CODEGEN_REGISTRY, register_codegen from .types import PTXDataType from .utils import parse_str @dataclass class FragAttrs: reg_type: str # asm constraint letter (r / f / d) size: int # bit width per register slot (32 or 64) ptr_type: str # C type for the cast _FRAG_ATTRS_MAP = { PTXDataType.BIT1: FragAttrs("r", 32, "uint32_t"), PTXDataType.INT4: FragAttrs("r", 32, "uint32_t"), PTXDataType.UINT4: FragAttrs("r", 32, "uint32_t"), PTXDataType.INT8: FragAttrs("r", 32, "uint32_t"), PTXDataType.UINT8: FragAttrs("r", 32, "uint32_t"), PTXDataType.FLOAT8_E4M3FN: FragAttrs("r", 32, "uint32_t"), PTXDataType.FLOAT8_E5M2: FragAttrs("r", 32, "uint32_t"), PTXDataType.BIT16: FragAttrs("r", 32, "uint32_t"), PTXDataType.FLOAT16: FragAttrs("r", 32, "uint32_t"), PTXDataType.BFLOAT16: FragAttrs("r", 32, "uint32_t"), PTXDataType.TENSOR_FLOAT32: FragAttrs("r", 32, "uint32_t"), PTXDataType.INT32: FragAttrs("r", 32, "int32_t"), PTXDataType.FLOAT32: FragAttrs("f", 32, "float"), PTXDataType.FLOAT64: FragAttrs("d", 64, "double"), } def _parse_mma_shape(shape_str): match = re.search(r"m(\d+)n(\d+)k(\d+)", shape_str) if not match: raise ValueError(f"Cannot parse MMA shape: {shape_str!r}") return tuple(map(int, match.groups())) def _classify_mma_form(d_type, a_type, b_type): """Map (d, a, b) dtype triple to one of the 7 PTX form_kind tags.""" fp16 = {"float16", "fp16"} tf32 = {"tensor_float32", "tf32"} bf16 = {"bfloat16", "bf16"} fp64 = {"float64", "fp64"} int_a = {"int8", "uint8", "s8", "u8"} fp8 = {"e4m3", "e5m2", "float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2"} subbyte = {"int4", "uint4", "bit1", "s4", "u4", "b1", "int1", "uint1"} if a_type in fp16 and b_type in fp16: return "f16" if a_type in tf32 and b_type in tf32: return "tf32" if a_type in bf16 and b_type in bf16: return "bf16" if a_type in fp64 and b_type in fp64: return "fp64" if a_type in int_a and b_type in int_a: return "int8" if a_type in fp8 and b_type in fp8: return "fp8" if a_type in subbyte and b_type in subbyte: return "subbyte" raise ValueError( f"Unknown ptx.mma form for d_type={d_type!r}, a_type={a_type!r}, b_type={b_type!r}" ) def _frag(dtype_str): return _FRAG_ATTRS_MAP[PTXDataType.from_string(dtype_str)] def _mma_threads(shape, a_type): """Special case: m8n8k4 with f16 a/b uses 8 threads per fragment.""" m, n, k = _parse_mma_shape(shape) if m == 8 and n == 8 and k == 4 and a_type == "float16": return 8 return 32 # PTX dtype abbreviation -> element bit width. Used by _frag_count so that # callers passing the PTX abbreviation (e.g. "fp32") don't blow up in # ``DataType("fp32")``. _PTX_BITS = { "fp16": 16, "fp32": 32, "fp64": 64, "bf16": 16, "tf32": 32, # tensor-float32 packs 19 significant bits into a 32-bit slot "s8": 8, "u8": 8, "s32": 32, "s4": 4, "u4": 4, "b1": 1, "b16": 16, "e4m3": 8, "e5m2": 8, } def _frag_count(dtype, dim_a, dim_b, threads): if dtype in _PTX_BITS: bits = _PTX_BITS[dtype] else: bits = DataType(dtype).bits size = _frag(dtype).size return dim_a * dim_b * bits // threads // size # ============================================================================= # Shared helpers for the 7 mma form_kinds. # Args layout for each form: # (d_ptr_in, a_ptr_in, b_ptr_in [, c_ptr_in], shape, a_layout, b_layout, # d_type, a_type, b_type, c_type, no_c_ptr [, saturate or bit_op]) # n_attrs = 8 for f16/tf32/bf16/fp64/fp8 (last 8 = shape, layouts, 4 dtypes, no_c_ptr) # n_attrs = 9 for int8 (+ saturate) and subbyte (+ bit_op) # ============================================================================= def _mma_form_parts(args, *, has_saturate=False, has_bit_op=False): """Compute (helper_name, c_signature, body) for one mma form invocation. ``args`` is the full positional arg tuple as received by codegen. The trailing ``n_attrs`` (8 or 9) entries are attrs. """ n_extra = (1 if has_saturate else 0) + (1 if has_bit_op else 0) n_attrs = 8 + n_extra # Split off attr args from the tail (operand args are ahead). attrs = args[-n_attrs:] shape = parse_str(attrs[0]) a_layout = parse_str(attrs[1]) b_layout = parse_str(attrs[2]) d_type = parse_str(attrs[3]) a_type = parse_str(attrs[4]) b_type = parse_str(attrs[5]) c_type = parse_str(attrs[6]) no_c_ptr_raw = attrs[7] no_c_ptr = bool(int(no_c_ptr_raw)) if hasattr(no_c_ptr_raw, "value") else bool(no_c_ptr_raw) saturate = False bit_op = "" if has_saturate: s = attrs[8] saturate = bool(int(s)) if hasattr(s, "value") else bool(s) if has_bit_op: bit_op = parse_str(attrs[8]) # Fragment counts (same derivation as the contiguous form). m, n, k = _parse_mma_shape(shape) threads = _mma_threads(shape, a_type) d_cnt = _frag_count(d_type, m, n, threads) a_cnt = _frag_count(a_type, m, k, threads) b_cnt = _frag_count(b_type, k, n, threads) c_cnt = _frag_count(c_type, m, n, threads) # C signature: one void* per register, ordered D regs, A regs, B regs, # then C regs (only when the accumulator is used). sig_parts = ( [f"void* d_ptr{i}" for i in range(d_cnt)] + [f"void* a_ptr{i}" for i in range(a_cnt)] + [f"void* b_ptr{i}" for i in range(b_cnt)] ) if not no_c_ptr: sig_parts += [f"void* c_ptr{i}" for i in range(c_cnt)] sig = "(" + ", ".join(sig_parts) + ")" def _safe(s): return s.replace("::", "_").replace(".", "_") name = ( f"ptx_mma_{shape}_{a_layout}_{b_layout}" f"_{_safe(d_type)}_{_safe(a_type)}_{_safe(b_type)}_{_safe(c_type)}" f"{'_no_c_ptr' if no_c_ptr else ''}" f"{'_saturate' if saturate else ''}" ) d_frag = _frag(d_type) a_frag = _frag(a_type) b_frag = _frag(b_type) c_frag = _frag(c_type) saturate_inst = ".satfinite" if saturate else "" # PTX b1 mma requires a `.popc` suffix after the bit op (e.g. `.xor.popc`). bit_op_inst = f".{bit_op}.popc" if bit_op else "" d_type_inst = PTXDataType.from_string(d_type).to_string() c_type_inst = PTXDataType.from_string(c_type).to_string() a_type_inst = PTXDataType.from_string(a_type).to_string() b_type_inst = PTXDataType.from_string(b_type).to_string() def _slot_arr(start, cnt): return "{" + ", ".join(f"%{start + i}" for i in range(cnt)) + "}" args_template = ( f"{_slot_arr(0, d_cnt)}, {_slot_arr(d_cnt, a_cnt)}, " f"{_slot_arr(d_cnt + a_cnt, b_cnt)}, {_slot_arr(d_cnt + a_cnt + b_cnt, c_cnt)}" ) # Each register binds to its OWN pointer via *(T*)X_ptrN (scatter). d_outs = ", ".join( f'"=r"(*({d_frag.ptr_type}*)d_ptr{i})' if d_frag.reg_type == "r" else f'"={d_frag.reg_type}"(*({d_frag.ptr_type}*)d_ptr{i})' for i in range(d_cnt) ) a_inputs = ", ".join( f'"{a_frag.reg_type}"(*({a_frag.ptr_type}*)a_ptr{i})' for i in range(a_cnt) ) b_inputs = ", ".join( f'"{b_frag.reg_type}"(*({b_frag.ptr_type}*)b_ptr{i})' for i in range(b_cnt) ) if no_c_ptr: c_value = "0.f" if c_frag.reg_type == "f" else "0" c_inputs = ", ".join(f'"{c_frag.reg_type}"({c_value})' for _ in range(c_cnt)) else: c_inputs = ", ".join( f'"{c_frag.reg_type}"(*({c_frag.ptr_type}*)c_ptr{i})' for i in range(c_cnt) ) body = ( " asm volatile(\n" f' "mma.sync.aligned.{shape}.{a_layout}.{b_layout}{saturate_inst}' f'{d_type_inst}{a_type_inst}{b_type_inst}{c_type_inst}{bit_op_inst} "\n' f' "{args_template};\\n"\n' f" : {d_outs}\n" f" : {a_inputs}, {b_inputs}, {c_inputs}\n" " );" ) return name, sig, body def _register_mma_form(form_kind, *, has_saturate=False, has_bit_op=False): n_attrs = 8 + (1 if has_saturate else 0) + (1 if has_bit_op else 0) def _parts(*args, hs=has_saturate, hb=has_bit_op): return _mma_form_parts(args, has_saturate=hs, has_bit_op=hb) device_intrinsic( f"_ptx_mma_{form_kind}", n_attrs=n_attrs, helper_name=lambda *a: _parts(*a)[0], c_signature=lambda *a: _parts(*a)[1], body=lambda *a: _parts(*a)[2], ) # Form 1 — f16. Form 2 — tf32. Form 3 — bf16. Form 4 — fp64. Form 6 — fp8. # All share the same 8-attr layout (no saturate / bit_op). for _kind in ("f16", "tf32", "bf16", "fp64", "fp8"): _register_mma_form(_kind) del _kind # Form 5 — int8 (+ saturate). _register_mma_form("int8", has_saturate=True) # Form 7 — subbyte (+ bit_op for b1). _register_mma_form("subbyte", has_bit_op=True) @register_codegen("ptx_mma") def codegen_ptx_mma( shape, a_layout, b_layout, d_type, a_type, b_type, c_type, d_cnt, a_cnt, b_cnt, c_cnt, no_c_ptr, *rest, ): """Classify (d, a, b) dtype triple to one of 7 form_kinds and forward. ``rest`` = flattened per-register pointers (d_cnt + a_cnt + b_cnt + c_cnt of them) followed by ``saturate`` and optionally ``bit_op``. """ shape = parse_str(shape) a_layout = parse_str(a_layout) b_layout = parse_str(b_layout) d_type = parse_str(d_type) a_type = parse_str(a_type) b_type = parse_str(b_type) c_type = parse_str(c_type) d_cnt = int(d_cnt) a_cnt = int(a_cnt) b_cnt = int(b_cnt) c_cnt = int(c_cnt) no_c_ptr = bool(int(no_c_ptr)) if hasattr(no_c_ptr, "value") else bool(no_c_ptr) n_ptrs = d_cnt + a_cnt + b_cnt + (0 if no_c_ptr else c_cnt) ptrs = list(rest[:n_ptrs]) trailing = list(rest[n_ptrs:]) saturate = bool(trailing[0]) if trailing else False bit_op_v = "" if len(trailing) >= 2: bo = trailing[1] bit_op_v = parse_str(bo) if isinstance(bo, str) else (bo if bo is not None else "") kind = _classify_mma_form(d_type, a_type, b_type) # op_args are the flattened per-register pointers (already in PTX order: # D regs, A regs, B regs, then C regs unless no_c_ptr). op_args = ptrs attr_args = [shape, a_layout, b_layout, d_type, a_type, b_type, c_type, no_c_ptr] if kind == "int8": attr_args.append(saturate) elif kind == "subbyte": attr_args.append(bit_op_v) result = CODEGEN_REGISTRY[f"tirx._ptx_mma_{kind}"](op_args + attr_args) return result[0] if isinstance(result, tuple) else result # ============================================================================= # ldmatrix / stmatrix — m8n8 fragment load/store. PTX docs lists 3 ldmatrix # forms (m8n8 + m8n16 + m16n16); TIRx uses only the m8n8 form. 1 # device_intrinsic each. ``num`` (.x1/.x2/.x4) and ``trans`` are modifier # attrs; the asm body loops over per-register constraints based on # (num, dtype). # ============================================================================= def _ldmatrix_parts(*args): # args = (smem_ptr, dst0, dst1, ..., dst{N-1}, num, dtype, trans) # The last 3 entries are the codegen attrs (n_attrs=3). num = int(args[-3]) dtype = parse_str(args[-2]) trans_b = bool(int(args[-1])) if hasattr(args[-1], "value") else bool(args[-1]) if num not in (1, 2, 4): raise ValueError(f"ldmatrix .num must be one of {{1, 2, 4}}, got {num}") if dtype not in ("b16", "b8"): raise ValueError(f"ldmatrix dtype must be 'b16' or 'b8', got {dtype!r}") n_regs = num if dtype == "b16" else num // 2 trans_inst = ".trans" if trans_b else "" slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}" reg_decls = ", ".join(f"r{i}" for i in range(n_regs)) out_constraints = ", ".join(f'"=r"(r{i})' for i in range(n_regs)) dst_assigns = "\n".join(f" *(uint32_t*)dst{i} = r{i};" for i in range(n_regs)) name = f"ptx_ldmatrix_{num}_{dtype.replace('::', '_').replace('.', '_')}_{1 if trans_b else 0}" sig = "(void* smem_ptr, " + ", ".join(f"void* dst{i}" for i in range(n_regs)) + ")" body = ( f" uint32_t {reg_decls};\n" " unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n" " asm volatile(\n" f' "ldmatrix.sync.aligned.m8n8.x{num}{trans_inst}.shared.{dtype} ' f'{slot_list}, [%{n_regs}];"\n' f" : {out_constraints}\n" f' : "r"(addr));\n' f"{dst_assigns}" ) return name, sig, body device_intrinsic( "_ptx_ldmatrix_impl", n_attrs=3, c_signature=lambda *a: _ldmatrix_parts(*a)[1], helper_name=lambda *a: _ldmatrix_parts(*a)[0], body=lambda *a: _ldmatrix_parts(*a)[2], ) @register_codegen("ptx_ldmatrix") def codegen_ptx_ldmatrix(trans, num, dtype, smem_ptr, *dst_handles): trans = bool(trans) num = int(num) dtype = parse_str(dtype) if dtype.startswith("."): dtype = dtype[1:] n_regs = num if dtype == "b16" else num // 2 if len(dst_handles) != n_regs: raise ValueError( f"ldmatrix .x{num}.{dtype} codegen expects {n_regs} dst handles, got {len(dst_handles)}" ) result = CODEGEN_REGISTRY["tirx._ptx_ldmatrix_impl"]( [smem_ptr, *dst_handles, num, dtype, trans] ) return result[0] if isinstance(result, tuple) else result def _stmatrix_parts(*args): # args = (smem_ptr, src0, src1, ..., src{N-1}, trans, num, dtype, shape, space) # The last 5 entries are codegen attrs (n_attrs=5). trans_arg, num_arg, dtype_arg, shape_arg, space_arg = args[-5:] n_regs = int(num_arg) trans_b = bool(int(trans_arg)) if hasattr(trans_arg, "value") else bool(trans_arg) dtype = parse_str(dtype_arg) shape = parse_str(shape_arg) space = parse_str(space_arg) if dtype.startswith("."): dtype = dtype[1:] if n_regs not in (1, 2, 4): raise ValueError(f"stmatrix .num must be one of {{1, 2, 4}}, got {n_regs}") if dtype not in ("b16", "b8"): raise ValueError(f"stmatrix .type must be b16 or b8, got {dtype!r}") if shape not in ("m8n8", "m16n8"): raise ValueError(f"stmatrix .shape must be m8n8 or m16n8, got {shape!r}") if space not in ("shared", "shared::cta"): raise ValueError(f"stmatrix state space must be shared or shared::cta, got {space!r}") if shape == "m16n8" and not trans_b: raise ValueError("stmatrix .m16n8 requires .trans") trans_inst = ".trans" if trans_b else "" slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}" src_loads = "\n".join(f" uint32_t r{i} = *(uint32_t*)src{i};" for i in range(n_regs)) in_constraints = ", ".join(f'"r"(r{i})' for i in range(n_regs)) name = f"ptx_stmatrix_{shape}_{n_regs}_{1 if trans_b else 0}_{space.replace('::', '_')}_{dtype}" sig = "(void* smem_ptr, " + ", ".join(f"void* src{i}" for i in range(n_regs)) + ")" body = ( f"{src_loads}\n" " unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n" " asm volatile(\n" f' "stmatrix.sync.aligned.{shape}.x{n_regs}{trans_inst}.{space}.{dtype} ' f'[%{n_regs}], {slot_list};"\n' " :\n" f' : {in_constraints}, "r"(addr));' ) return name, sig, body device_intrinsic( "_ptx_stmatrix_impl", n_attrs=5, c_signature=lambda *a: _stmatrix_parts(*a)[1], helper_name=lambda *a: _stmatrix_parts(*a)[0], body=lambda *a: _stmatrix_parts(*a)[2], ) @register_codegen("ptx_stmatrix") def codegen_ptx_stmatrix(trans, num, dtype, shape, space, smem_ptr, *src_handles): trans = bool(trans) num = int(num) dtype_str = parse_str(dtype) if dtype_str.startswith("."): dtype_str = dtype_str[1:] n_regs = num if len(src_handles) != n_regs: raise ValueError( f"stmatrix .x{num}.{dtype_str} codegen expects {n_regs} src handles, " f"got {len(src_handles)}" ) result = CODEGEN_REGISTRY["tirx._ptx_stmatrix_impl"]( [smem_ptr, *src_handles, trans, num, dtype, shape, space] ) return result[0] if isinstance(result, tuple) else result