llvm/clang/test/CodeGen/builtins-nvptx-mma.py

# This script generates all variants of wmma builtins, verifies that clang calls
# correct LLVM intrinsics, and checks that availability of specific builtins is
# constrained by the correct PTX version and the target GPU variant.

# Dummy test run to avoid lit warnings.
# RUN: echo "This is not a real test. It's a generator for builtins-nvpts-mma.cu" >/dev/null

from __future__ import print_function

import argparse
from collections import defaultdict
from itertools import product
from string import Template


class MMAFrag:
    def __init__(self, geom, frag, ptx_elt_type):
        self.geom = geom
        self.frag = frag
        self.ptx_type = ptx_elt_type

    def __repr__(self):
        return "%s:%s:%s" % (self.geom, self.frag, self.ptx_type)


class MMAOp:
    def __init__(self, a, b, c, d, b1op=""):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.b1op = b1op

    def __repr__(self):
        return "{A:%s, B:%s, C:%s, D:%s}" % (self.a, self.b, self.c, self.d)


def make_mma_ops(geoms, types_a, types_b, types_c, types_d, b1ops=None):
    ops = []
    if b1ops is None:
        b1ops = [""]
    for geom, type_a, type_c in product(geoms, types_a, types_c):
        for type_b, type_d in product(
            types_b if types_b else [type_a], types_d if types_d else [type_c]
        ):
            ops += [
                MMAOp(
                    MMAFrag(geom, "a", type_a),
                    MMAFrag(geom, "b", type_b),
                    MMAFrag(geom, "c", type_c),
                    MMAFrag(geom, "d", type_d),
                    b1op,
                )
                for b1op in b1ops
            ]
    return ops


def make_ldst_ops(geoms, frags, types):
    return [
        MMAFrag(geom, frag, ptx_type)
        for (geom, frag, ptx_type) in product(geoms, frags, types)
    ]


def get_mma_ops():
    return (
        make_mma_ops(["m16n16k8"], ["tf32"], [], ["f32"], [])
        + make_mma_ops(["m16n16k16", "m32n8k16", "m8n32k16"], ["bf16"], [], ["f32"], [])
        + make_mma_ops(["m8n8k4"], ["f64"], [], ["f64"], [])
        + make_mma_ops(
            ["m16n16k16", "m32n8k16", "m8n32k16"],
            ["f16"],
            [],
            ["f16", "f32"],
            ["f16", "f32"],
        )
        + make_mma_ops(
            ["m16n16k16", "m32n8k16", "m8n32k16"], ["s8", "u8"], [], ["s32"], []
        )
        + make_mma_ops(["m8n8k32"], ["s4", "u4"], [], ["s32"], [])
        + make_mma_ops(
            ["m8n8k128"], ["b1"], [], ["s32"], [], [".xor.popc", ".and.popc"]
        )
    )


def get_ldst_ops():
    # NOTE: fragemts are from the point of view of PTX.
    # fragment `d` is only for store ops, others for both loads and stores.
    return (
        make_ldst_ops(
            ["m16n16k16", "m32n8k16", "m8n32k16"],
            ["a", "b"],
            ["f16", "u8", "s8", "bf16"],
        )
        + make_ldst_ops(
            ["m16n16k16", "m32n8k16", "m8n32k16"], ["c", "d"], ["f16", "f32", "s32"]
        )
        + make_ldst_ops(["m8n8k32"], ["a", "b"], ["s4", "u4"])
        + make_ldst_ops(["m8n8k128"], ["a", "b"], ["b1"])
        + make_ldst_ops(["m8n8k32", "m8n8k128"], ["c", "d"], ["s32"])
        + make_ldst_ops(["m8n8k4"], ["a", "b", "c", "d"], ["f64"])
        +
        # TF32 m16n16k8 is odd.
        # For fragment 'C' it uses __mma_*tf32*_m16n16k8_ld_c
        # but 'D' calls __mma_m16n16k8_st_c_*f32*.
        make_ldst_ops(["m16n16k8"], ["a", "b", "c"], ["tf32"])
        + make_ldst_ops(["m16n16k8"], ["d"], ["f32"])
    )


def is_geom_supported(geom):
    # geometries for FP and ints.
    if geom in ["m8n32k16", "m32n8k16"]:
        return ptx_version >= 61
    # geometries for sub-ints.
    if geom in ["m8n8k32", "m8n8k128"]:
        return ptx_version >= 63 and gpu_arch >= 75
    if geom == "m16n16k16":
        return ptx_version >= 60
    if geom in ["m16n16k8", "m8n8k4"]:
        return ptx_version >= 70 and gpu_arch >= 80
    assert False  # Unexpected geometry.


def is_type_supported(ptx_type):
    if ptx_type in ["s8", "u8", "s32"]:
        return ptx_version >= 63 and gpu_arch >= 72
    if ptx_type in ["s4", "u4", "b1"]:
        return ptx_version >= 63 and gpu_arch >= 75
    if ptx_type in ["bf16", "tf32", "f64"]:
        return ptx_version >= 70 and gpu_arch >= 80
    return ptx_version >= 60 and gpu_arch >= 70


def is_rnd_supported(op):
    # rnd is only supported for FP64 WMMA
    return op.a.ptx_type == "f64"


def is_mma_variant_supported(op, layout_a, layout_b, satf):
    if not (is_type_supported(op.a.ptx_type) and is_geom_supported(op.a.geom)):
        return False

    if satf and not op.a.ptx_type in ["f16", "s8", "u8", "s4", "u4"]:
        return False

    # sub-integer types require row/col layout.
    if op.a.ptx_type in ["s4", "u4", "b1"]:
        return layout_a == "row" and layout_b == "col"
    return True


def is_ldst_variant_supported(frag, layout):
    if not (is_type_supported(frag.ptx_type) and is_geom_supported(frag.geom)):
        return False
    if frag.ptx_type in ["s4", "u4", "b1"]:
        # sub-integer types require sm_75 and ptx63, row/col layout for a/b.
        return (
            (frag.frag == "a" and layout == "row")
            or (frag.frag == "b" and layout == "col")
            or frag.frag in ["c", "d"]
        )
    return True


def get_builtin_prefix(frag):
    prefix = None
    if frag.geom in ["m16n16k16", "m32n8k16", "m8n32k16"]:
        if frag.ptx_type in ["f16", "f32"]:
            prefix = "__hmma"
        elif frag.ptx_type == "bf16":
            prefix = "__mma_bf16"
        else:
            prefix = "__imma"
    elif frag.geom == "m8n8k32":
        prefix = "__imma"  # sub-integers
    elif frag.geom == "m8n8k128":
        prefix = "__bmma"
    elif frag.geom == "m8n8k4":
        prefix = "__dmma"
    elif frag.geom == "m16n16k8":
        if frag.ptx_type == "f32":
            prefix = "__mma"
        else:
            prefix = "__mma_tf32"
    assert prefix
    return prefix


def get_ldst_builtin_name(frag):
    prefix = get_builtin_prefix(frag)

    if prefix == "__hmma":
        suffix = "" if frag.frag in ["a", "b"] else frag.ptx_type
    elif prefix in ["__dmma", "__mma_bf16", "__mma_tf32"]:
        suffix = "" if frag.frag in ["a", "b", "c"] else frag.ptx_type
    else:
        suffix = "" if frag.frag == "c" else frag.ptx_type
        if suffix == "s32":
            suffix = "i32"

    if frag.frag == "d":
        ifrag = "c"
        op = "st"
    else:
        ifrag = frag.frag
        op = "ld"

    name = "%s_%s_%s_%s%s" % (
        prefix,
        frag.geom,
        op,
        ifrag,
        "_" + suffix if suffix else "",
    )
    return name


def get_mma_builtin_name(op):
    prefix = get_builtin_prefix(op.a)

    if prefix == "__hmma":
        suffix = op.d.ptx_type + op.c.ptx_type
    elif prefix in ["__mma_bf16", "__mma_tf32"]:
        suffix = op.d.ptx_type
    else:
        suffix = op.a.ptx_type

    name = "{prefix}_{geom}_mma{b1op}_{suffix}".format(
        prefix=prefix, geom=op.a.geom, b1op=op.b1op.replace(".", "_"), suffix=suffix
    )
    return name


def get_required_sm(frag, b1op=""):
    if frag.ptx_type in ["f64", "bf16", "tf32"]:
        return 80
    if frag.ptx_type in ["u4", "s4", "b1"]:
        if b1op == ".and.popc":
            return 80
        return 75
    if frag.ptx_type in ["s8", "u8"]:
        return 72
    if frag.ptx_type == "s32":
        if frag.geom in ["m8n8k32", "m8n8k128"]:  # s4/u4/b1
            return 75
        else:  # s8/u8
            return 72
    if frag.ptx_type in ["f16", "f32"]:
        if frag.geom == "m16n16k8":
            return 80
        else:
            return 70
    assert False


def get_required_ptx(frag, b1op=""):
    if frag.ptx_type == "b1" and b1op == ".and.popc":
        return 71
    if frag.ptx_type in ["f64", "bf16", "tf32"]:
        return 70
    if frag.ptx_type in ["f16", "f32"]:
        if frag.geom == "m16n16k16":
            return 60
        if frag.geom == "m16n16k8":
            return 70
        return 61
    return 63


def get_src_dst_prefix(frag):
    if frag.ptx_type == "f32":
        return "f"
    if frag.ptx_type == "f64":
        return "d"
    if frag.ptx_type == "tf32" and frag.frag in ["c", "d"]:
        return "f"
    return ""


def gen_wmma_ldst_tests(results):
    load_template = """
  // CHECK${check_suffix}: call {{.*}} @${intrinsic}
  // expected-error-re@+1 {{'${builtin}' needs target feature (sm_${min_sm}{{.*}},(ptx${min_ptx}{{.*}}}}
  ${builtin}(${dst}, ${src}, ldm, ${blayout});
""".rstrip()
    intrinsic_template = (
        "llvm.nvvm.wmma.${geom}.${op}.${frag}.${ilayout}.stride.${itype}"
    )

    for frag, layout in sorted(product(get_ldst_ops(), ["row", "col"]), key=str):

        if not is_ldst_variant_supported(frag, layout):
            continue

        src_dst_prefix = get_src_dst_prefix(frag)

        min_sm = get_required_sm(frag)
        min_ptx = get_required_ptx(frag)
        # TF32 uses f32 for accumulator loads.
        if frag.geom == "m16n16k8" and frag.frag == "c":
            assert frag.ptx_type == "tf32"
            itype = "f32"
        else:
            itype = frag.ptx_type

        params = {
            "check_suffix": "_PTX%d_SM%d" % (min_ptx, min_sm),
            "builtin": get_ldst_builtin_name(frag),
            "min_ptx": min_ptx,
            "min_sm": min_sm,
            "dst": src_dst_prefix + "dst",
            "src": src_dst_prefix + "src",
            "blayout": 0 if layout == "row" else 1,
            "intrinsic": Template(intrinsic_template).substitute(
                {
                    "frag": frag.frag,
                    "geom": frag.geom,
                    "ilayout": layout,
                    "itype": itype,
                    "op": "store" if frag.frag == "d" else "load",
                }
            ),
        }
        results[(min_ptx, min_sm)] += Template(load_template).substitute(params)

    return results


def mma_signature(op):
    if op.a.ptx_type == "f16":
        # FP16 ops identified by accumulator & result type.
        return "%s.%s" % (op.d.ptx_type, op.c.ptx_type)
    else:
        # other ops are identified by input type.
        return op.a.ptx_type


# Get numeric value for rowcol parameter of the builtin
# AFAICT it uses the encoding accepted by NVVM intrinsics:
# https://docs.nvidia.com/cuda/nvvm-ir-spec/index.html#nvvm-intrin-warp-level-matrix-mma
def get_ilayout(a, b):
    return {"row.row": 0, "row.col": 1, "col.row": 2, "col.col": 3}[a + "." + b]


def gen_wmma_mma_tests(results):
    mma_template = """
  // CHECK${check_suffix}: call {{.*}} @${intrinsic}
  // expected-error-re@+1 {{'${builtin}' needs target feature (sm_${min_sm}{{.*}},(ptx${min_ptx}{{.*}}}}
  ${builtin}(${dst}, ${asrc}, ${asrc}, ${csrc}, ${ilayout}${maybe_satf});
""".rstrip()
    intrinsic_template = "llvm.nvvm.wmma.${geom}.mma${b1op}.${alayout}.${blayout}.${intrinsic_signature}${satf}"

    for op, alayout, blayout, satf in sorted(
        product(get_mma_ops(), ["row", "col"], ["row", "col"], [".satfinite", ""]),
        key=str,
    ):

        if not is_mma_variant_supported(op, alayout, blayout, satf):
            continue

        asrc_prefix = get_src_dst_prefix(op.a)
        csrc_prefix = get_src_dst_prefix(op.c)
        ddst_prefix = get_src_dst_prefix(op.d)
        if op.a.ptx_type == "b1":  # .b1 MMA has no satf argument.
            isatf_arg = ""
        else:
            isatf_arg = ", 1" if satf else ", 0"
        min_sm = get_required_sm(op.a, op.b1op)
        min_ptx = get_required_ptx(op.a, op.b1op)
        params = {
            "check_suffix": "_PTX%d_SM%d" % (min_ptx, min_sm),
            "builtin": get_mma_builtin_name(op),
            "min_ptx": min_ptx,
            "min_sm": min_sm,
            "dst": ddst_prefix + "dst",
            "asrc": asrc_prefix + "src",
            "csrc": csrc_prefix + "src",
            "ilayout": get_ilayout(alayout, blayout),
            "maybe_satf": isatf_arg,
            "intrinsic": Template(intrinsic_template).substitute(
                {
                    "geom": op.a.geom,
                    "alayout": alayout,
                    "blayout": blayout,
                    "intrinsic_signature": mma_signature(op),
                    "satf": satf,
                    "b1op": op.b1op,
                }
            ),
        }
        results[(min_ptx, min_sm)] += Template(mma_template).substitute(params)

    return results


def gen_tests():
    results = gen_wmma_ldst_tests(defaultdict(str))
    results = gen_wmma_mma_tests(results)

    run_template = r"""
//
// *** DO NOT EDIT ***
//
//  This test has been automatically generated by
//  builtins-nvtx-mma.py --ptx=${ptx} --gpu-arch=${sm}
//
// Make sure we can handle all builtins available on sm_${sm} with PTX${ptx}
// ${run}: %clang_cc1 -triple nvptx64-unknown-unknown -target-cpu sm_${sm} \
// ${run}:            -fcuda-is-device -target-feature +ptx${ptx} \
// ${run}:            -DPTX=${ptx} -DSM=${sm} \
// ${run}:            -S -emit-llvm -o - -x cuda %s \
// ${run}:   | FileCheck -check-prefixes=${check_labels} %s
// Verify that all builtins have correct constraints.
// ${run}: %clang_cc1 -triple nvptx-unknown-unknown \
// ${run}:   -target-cpu sm_60 -target-feature +ptx42 \
// ${run}:   -DPTX=${ptx} -DSM=${sm} -fcuda-is-device -S -o /dev/null -x cuda \
// ${run}:   -verify %s
"""

    def supported_variants(ptx, sm, results):
        return [(ptx_, sm_) for ptx_, sm_ in results if ptx_ <= ptx and sm_ <= sm]

    print(
        Template(run_template).substitute(
            {
                "run": "RUN",  # To avoid lit misinterpreting the template
                "ptx": ptx_version,
                "sm": gpu_arch,
                "check_labels": ",".join(
                    [
                        "CHECK_PTX%d_SM%d" % (ptx_, sm_)
                        for ptx_, sm_ in supported_variants(
                            ptx_version, gpu_arch, results
                        )
                    ]
                ),
            }
        )
    )

    print(
        """
#if !defined(CUDA_VERSION)
#define __device__ __attribute__((device))
#define __global__ __attribute__((global))
#define __shared__ __attribute__((shared))
#define __constant__ __attribute__((constant))

typedef unsigned long long uint64_t;
#endif

// CHECK-LABEL: test_wmma_buitins
__device__ void test_wmma_buitins(int *src, int *dst,
                                  float *fsrc, float *fdst,
                                  double *dsrc, double *ddst, int ldm) {
"""
    )

    for (ptx, sm), tests in sorted(results.items()):
        print()
        print("#if (PTX >= %d) && (SM >= %d)" % (ptx, sm))
        print(tests)
        print("#endif // (PTX >= %d) && (SM >= %d)" % (ptx, sm))

    print("}")


parser = argparse.ArgumentParser()
parser.add_argument("--ptx", type=int, default=60)
parser.add_argument("--gpu-arch", type=int, default=70)
args = parser.parse_args()
ptx_version = args.ptx
gpu_arch = args.gpu_arch

gen_tests()