# mypy: allow-untyped-defs import logging from dataclasses import asdict, dataclass from typing import Any, Optional import torch from torch._dynamo.utils import counters from torch._inductor.codegen.cutedsl.cutedsl_template import CuteDSLTemplate from torch._inductor.runtime.triton_compat import tl from torch._inductor.template_heuristics.cutedsl import get_groupgemm_configs from torch._inductor.virtualized import V from torch.utils._triton import has_triton from ..ir import ChoiceCaller, Layout, TensorBox from ..lowering import register_lowering from ..select_algorithm import ( autotune_select_algorithm, ExternKernelChoice, realize_inputs, TritonTemplate, ) from ..utils import ( get_gpu_shared_memory, get_num_sms, has_free_symbols, use_aten_gemm_kernels, use_blackwell_cutedsl_grouped_mm, use_nv_universal_gemm_template, use_triton_template, ) from .mm_common import ( _is_static_problem, check_supported_striding, load_kernel_template, persistent_grouped_mm_grid, ) log = logging.getLogger(__name__) aten = torch.ops.aten @dataclass class Config: kwargs: dict[str, int] num_stages: int num_warps: int _NV_CONFIGS = [ Config( { "BLOCK_M": block_size_m, "BLOCK_N": block_size_n, "BLOCK_K": block_size_k, "NUM_CONSUMER_GROUPS": 1, }, num_stages=num_stages, num_warps=num_warps, ) for block_size_m in [16, 32, 64, 128] for block_size_n in [64, 128, 256] for block_size_k in [64, 128, 256] for num_stages in [3, 4] for num_warps in [4, 8] ] def grouped_mm_configs(): return _NV_CONFIGS def early_config_prune(g, m, dtsize, configs, named_args): pruned_configs = [] for config in configs: kw = config.kwargs BLOCK_M, BLOCK_N, BLOCK_K, num_stages, num_warps, num_consumer_groups = ( kw["BLOCK_M"], kw["BLOCK_N"], kw["BLOCK_K"], config.num_stages, config.num_warps, getattr(config, "num_consumer_groups", 0), ) # 1. Prune NV configs depending on g and m. if not has_free_symbols((g, m)): a_is_2d, b_is_2d = named_args["A_IS_2D"], named_args["B_IS_2D"] m_avg = m // g if a_is_2d and not b_is_2d else m if m_avg <= 16: if BLOCK_M > 32: continue elif m_avg <= 32: if BLOCK_M > 64: continue elif m_avg <= 64: if BLOCK_M <= 16: continue else: if BLOCK_M <= 32: continue # 2. make sure we have enough smem max_shared_memory = get_gpu_shared_memory() required_shared_memory = (BLOCK_M + BLOCK_N) * BLOCK_K * num_stages * dtsize if required_shared_memory > max_shared_memory: continue use_warp_specialization = num_consumer_groups >= 1 # 3. make sure we can partition for ws if use_warp_specialization: if num_warps != 4: continue # "tritongpu-warp-spec-data-partition" m_slice = BLOCK_M // num_consumer_groups n_slice = BLOCK_N // num_consumer_groups if m_slice < 64 and n_slice < 256: continue pruned_configs.append(config) return pruned_configs triton_grouped_mm_template = TritonTemplate( name="grouped_mm", grid=persistent_grouped_mm_grid, source=load_kernel_template("triton_mm_grouped"), ) triton_scaled_grouped_mm_template = TritonTemplate( name="scaled_grouped_mm", grid=persistent_grouped_mm_grid, source=load_kernel_template("triton_mm_grouped"), ) cutedsl_grouped_mm_template = CuteDSLTemplate( name="grouped_gemm_cutedsl", source=load_kernel_template("cutedsl_mm_grouped"), ) def grouped_mm_args( mat1: TensorBox, mat2: TensorBox, offs: Optional[TensorBox], layout=None, out_dtype=None, ): mat1, mat2 = realize_inputs(mat1, mat2) if offs is not None: realize_inputs(offs) mat1_size = mat1.get_size() mat2_size = mat2.get_size() m1dim, m2dim = len(mat1_size), len(mat2_size) assert m1dim == 2 or m1dim == 3 assert m2dim == 2 or m2dim == 3 if layout is None: from torch._inductor.ir import FixedLayout if out_dtype is None: out_dtype = mat1.get_dtype() alignment = 16 // out_dtype.itemsize if m1dim == 2: if m2dim == 2: assert offs is not None out_size = [offs.get_size()[0], mat1_size[0], mat2_size[1]] else: out_size = [mat1_size[0], mat2_size[-1]] else: if m2dim == 2: out_size = [mat1_size[1], mat2_size[1]] else: out_size = [mat1_size[0], mat1_size[1], mat2_size[-1]] size_padded = (out_size[-1] + alignment - 1) // alignment * alignment if len(out_size) == 2: out_stride = [size_padded, 1] else: out_stride = [out_size[1] * size_padded, size_padded, 1] layout = FixedLayout( mat1.get_device(), out_dtype, out_size, out_stride, ) else: assert out_dtype is None, "out_dtype is ignored if layout is specified." return (mat1_size, mat2_size, layout, mat1, mat2, offs) aten__grouped_mm = ExternKernelChoice( torch._grouped_mm, "at::_grouped_mm", op_overload=aten._grouped_mm.default, has_out_variant=False, ) aten__scaled_grouped_mm = ExternKernelChoice( torch._scaled_grouped_mm, "at::_scaled_grouped_mm", op_overload=aten._scaled_grouped_mm.default, has_out_variant=False, ) def can_use_triton_kernel( mat_a: TensorBox, mat_b: TensorBox, offs: Optional[TensorBox], bias: Optional[TensorBox], scale_result: Optional[TensorBox], ) -> bool: if not ( torch.cuda.is_available() and torch.cuda.get_device_capability() >= (9, 0) and not torch.version.hip ): return False if not has_triton(): return False # The _grouped_mm()/_scaled_grouped_mm() operator do not support # bias nor scale_result yet. if bias is not None: return False if scale_result is not None: return False if len(mat_a.get_size()) == 2 or len(mat_b.get_size()) == 2: return offs is not None else: return offs is None def create_offsets(offs_box, m1_is_2d, m2_is_2d, m, n, k, alignment): if m1_is_2d: if m2_is_2d: end = k else: end = m else: if m2_is_2d: end = n else: return None end_hint = V.graph.sizevars.optimization_hint(end) noffs_hint = V.graph.sizevars.optimization_hint(offs_box.get_size()[0]) offs = torch.arange(1, noffs_hint + 1, dtype=torch.float32) * ( end_hint / noffs_hint ) offs[:-1] = (offs[:-1] / alignment).round() * alignment offs[-1] = end_hint return offs.to(dtype=offs_box.get_dtype(), device=offs_box.get_device()) def _tuned_grouped_mm_common( operator_name: str, algorithm_name: str, extern_kernel_choice: ExternKernelChoice, kernel_template: TritonTemplate, mat_a: TensorBox, mat_b: TensorBox, scale_a: Optional[TensorBox] = None, scale_b: Optional[TensorBox] = None, offs: Optional[TensorBox] = None, bias: Optional[TensorBox] = None, scale_result: Optional[TensorBox] = None, out_dtype: Optional[torch.dtype] = None, use_fast_accum: Optional[bool] = None, layout: Optional[Layout] = None, ) -> TensorBox: assert (scale_a is None) == (scale_b is None) assert scale_result is None or scale_a is not None m1_size, m2_size, layout, mat_a, mat_b, offs = grouped_mm_args( mat_a, mat_b, offs, layout=layout, out_dtype=out_dtype ) counters["aten_mm_info"][operator_name] += 1 log_message = f"Tuned {operator_name}: mat1_shape=%s, mat2_shape=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s" log.info( log_message, m1_size, m2_size, mat_a.get_dtype(), mat_b.get_dtype(), layout, ) if scale_a is not None and scale_b is not None: check_supported_striding(mat_a, mat_b) # workaround for Inductor not supporting optional tensor input arguments input_nodes: list[Any] = [mat_a, mat_b] if scale_a is not None: input_nodes.append(realize_inputs(scale_a)) if scale_b is not None: input_nodes.append(realize_inputs(scale_b)) if offs is not None: input_nodes.append(realize_inputs(offs)) if use_fast_accum is None: aten_choice = extern_kernel_choice.bind( input_nodes, layout, out_dtype=out_dtype, ) else: aten_choice = extern_kernel_choice.bind( input_nodes, layout, out_dtype=out_dtype, use_fast_accum=use_fast_accum, ) if use_fast_accum is None: use_fast_accum = False choices: list[ChoiceCaller] = [] if use_aten_gemm_kernels(): choices.append(aten_choice) _, is_nonzero = _is_static_problem(layout) # Checking only for the equality of corresponding dims of # multiplicands here, relying on meta function checks for # everything else. if len(m1_size) == 2: if len(m2_size) == 2: m, k1 = m1_size k2, n = m2_size g = offs.get_size()[0] k = V.graph.sizevars.check_equals(k1, k2) a_is_2d, b_is_2d = True, True else: g1 = offs.layout.size[0] m, k1 = m1_size g2, k2, n = m2_size g = V.graph.sizevars.check_equals_and_simplify(g1, g2) k = V.graph.sizevars.check_equals(k1, k2) a_is_2d, b_is_2d = True, False else: if len(m2_size) == 2: g1 = offs.layout.size[0] g2, m, k1 = m1_size k2, n = m2_size g = V.graph.sizevars.check_equals_and_simplify(g1, g2) k = V.graph.sizevars.check_equals(k1, k2) a_is_2d, b_is_2d = False, True else: g1, m, k1 = m1_size g2, k2, n = m2_size g = V.graph.sizevars.check_equals_and_simplify(g1, g2) k = V.graph.sizevars.check_equals(k1, k2) a_is_2d, b_is_2d = False, False if ( is_nonzero and use_triton_template(layout) and can_use_triton_kernel(mat_a, mat_b, offs, bias, scale_result) ): scaled = scale_a is not None a_is_k_major = mat_a.get_stride()[-1] == 1 b_is_k_major = mat_b.get_stride()[-2] == 1 triton_has_make_tensor_descriptor = hasattr(tl, "make_tensor_descriptor") triton_has_experimental_make_tensor_descriptor = hasattr( tl, "_experimental_make_tensor_descriptor" ) use_tma_load = ( triton_has_make_tensor_descriptor or triton_has_experimental_make_tensor_descriptor ) kwargs = { "SCALED": scaled, "A_IS_2D": a_is_2d, "B_IS_2D": b_is_2d, "A_IS_K_MAJOR": a_is_k_major, "B_IS_K_MAJOR": b_is_k_major, "USE_FAST_ACCUM": use_fast_accum, "NUM_SMS": get_num_sms(), "USE_TMA_LOAD": use_tma_load, "USE_EXPERIMENTAL_MAKE_TENSOR_DESCRIPTOR": triton_has_experimental_make_tensor_descriptor, } for config in early_config_prune( g, m, mat_a.dtype.itemsize, grouped_mm_configs(), kwargs ): kernel_template.maybe_append_choice( choices, input_nodes=input_nodes, layout=layout, num_stages=config.num_stages, num_warps=config.num_warps, **kwargs, **config.kwargs, ) if use_blackwell_cutedsl_grouped_mm( mat_a, mat_b, layout, a_is_2d, b_is_2d, offs, bias, scale_result ): for config in get_groupgemm_configs(): kwargs = dict( ACC_DTYPE="cutlass.Float32", ) cutedsl_grouped_mm_template.maybe_append_choice( choices, input_nodes=input_nodes, layout=layout, **kwargs, **asdict(config), ) if ( is_nonzero and a_is_2d and not b_is_2d and offs is not None and use_nv_universal_gemm_template(layout, m, n, k, mat_a, mat_b, offs, g) ): from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm import ( add_nv_universal_grouped_gemm_choices, ) add_nv_universal_grouped_gemm_choices( choices, layout, input_nodes, accumulator_type=torch.float32, ) input_gen_fns = {} if offs is not None: input_offs_idx = 2 if scale_a is None else 4 alignment = 16 // mat_a.dtype.itemsize input_gen_fns[input_offs_idx] = lambda x: create_offsets( x, a_is_2d, b_is_2d, m, n, k, alignment ) return autotune_select_algorithm( algorithm_name, choices, input_nodes, layout, input_gen_fns=input_gen_fns ) @register_lowering(aten._grouped_mm.default, type_promotion_kind=None) def tuned_grouped_mm( mat_a: TensorBox, mat_b: TensorBox, offs: Optional[TensorBox] = None, bias: Optional[TensorBox] = None, out_dtype: Optional[torch.dtype] = None, layout: Optional[Layout] = None, ) -> TensorBox: """Auto-tuning for _grouped_mm() operator.""" return _tuned_grouped_mm_common( "aten._grouped_mm.default", "grouped_mm", aten__grouped_mm, triton_grouped_mm_template, mat_a, mat_b, None, None, offs, bias, None, out_dtype, None, layout, ) @register_lowering(aten._scaled_grouped_mm.default, type_promotion_kind=None) def tuned_scaled_grouped_mm( mat_a: TensorBox, mat_b: TensorBox, scale_a: TensorBox, scale_b: TensorBox, offs: Optional[TensorBox] = None, bias: Optional[TensorBox] = None, scale_result: Optional[TensorBox] = None, out_dtype: Optional[torch.dtype] = None, use_fast_accum: bool = False, layout: Optional[Layout] = None, ) -> TensorBox: """Auto-tuning for _scaled_grouped_mm() operator.""" # matching _scaled_grouped_mm_cuda Blas.cpp implementation out_dtype = out_dtype or torch.bfloat16 return _tuned_grouped_mm_common( "aten._scaled_grouped_mm.default", "scaled_grouped_mm", aten__scaled_grouped_mm, triton_scaled_grouped_mm_template, mat_a, mat_b, scale_a, scale_b, offs, bias, scale_result, out_dtype, use_fast_accum, layout, )