Profiles a simple onnx graph including a singleGemm

The benchmark profiles the execution of Gemm for different types and configuration. That includes a custom operator only available on CUDA calling function cublasLtMatmul.

import pprint
from itertools import product
import numpy
from tqdm import tqdm
import matplotlib.pyplot as plt
from pandas import pivot_table, concat
from onnx import TensorProto
from onnx.helper import (
    make_model,
    make_node,
    make_graph,
    make_tensor_value_info,
    make_opsetid,
)
from onnx.checker import check_model
from onnx.numpy_helper import from_array
from onnx.reference import ReferenceEvaluator
from onnxruntime import InferenceSession, SessionOptions, get_available_providers
from onnxruntime.capi._pybind_state import (
    OrtValue as C_OrtValue,
    OrtDevice as C_OrtDevice,
)
from onnxruntime.capi.onnxruntime_pybind11_state import (
    NotImplemented,
    InvalidGraph,
    InvalidArgument,
)

try:
    from onnx_array_api.plotting.text_plot import onnx_simple_text_plot
    from onnx_array_api.ort.ort_profile import ort_profile
except ImportError:
    onnx_simple_text_plot = str
    ort_profile = None
try:
    from onnx_extended.reference import CReferenceEvaluator
except ImportError:
    CReferenceEvaluator = ReferenceEvaluator
from onnx_extended.args import get_parsed_args
from onnx_extended.ext_test_case import unit_test_going

try:
    from onnx_extended.validation.cuda.cuda_example_py import get_device_prop
    from onnx_extended.ortops.tutorial.cuda import get_ort_ext_libs
except ImportError:

    def get_device_prop():
        return {"name": "CPU"}

    def get_ort_ext_libs():
        return None


properties = get_device_prop()

if unit_test_going():
    default_dims = "32,32,32;64,64,64"
elif properties.get("major", 0) < 7:
    default_dims = "256,256,256;512,512,512"
else:
    default_dims = "2048,2048,2048;4096,4096,4096"

script_args = get_parsed_args(
    "plot_profile_gemm_ort",
    description=__doc__,
    dims=(default_dims, "dimensions to try for dims"),
    repeat_profile=(17, "number of time to call ORT for profiling"),
)

Device properties

{'clockRate': 1569000,
 'computeMode': 0,
 'concurrentKernels': 1,
 'isMultiGpuBoard': 0,
 'major': 6,
 'maxThreadsPerBlock': 1024,
 'minor': 1,
 'multiProcessorCount': 10,
 'name': 'NVIDIA GeForce GTX 1060',
 'sharedMemPerBlock': 49152,
 'totalConstMem': 65536,
 'totalGlobalMem': 6442319872}

Model to benchmark

It includes one Gemm. The operator changes. It can the regular Gemm, a custom Gemm from domain com.microsoft or a custom implementation from domain onnx_extended.ortops.tutorial.cuda.

def create_model(
    mat_type=TensorProto.FLOAT, provider="CUDAExecutionProvider", domain="com.microsoft"
):
    A = make_tensor_value_info("A", mat_type, [None, None])
    B = make_tensor_value_info("B", mat_type, [None, None])
    outputs = [make_tensor_value_info("C", mat_type, [None, None])]
    inits = []
    if domain != "":
        if provider != "CUDAExecutionProvider":
            return None
        f8 = False
        if domain == "com.microsoft":
            op_name = "GemmFloat8"
            computeType = "CUBLAS_COMPUTE_32F"
            node_output = ["C"]
        elif mat_type == TensorProto.FLOAT:
            op_name = "CustomGemmFloat"
            computeType = "CUBLAS_COMPUTE_32F_FAST_TF32"
            node_output = ["C"]
        elif mat_type == TensorProto.FLOAT16:
            op_name = "CustomGemmFloat16"
            computeType = "CUBLAS_COMPUTE_16F"
            node_output = ["C"]
        elif mat_type in (TensorProto.FLOAT8E4M3FN, TensorProto.FLOAT8E5M2):
            f8 = True
            op_name = "CustomGemmFloat8E4M3FN"
            computeType = "CUBLAS_COMPUTE_32F"
            node_output = ["C"]
            outputs = [
                make_tensor_value_info("C", TensorProto.FLOAT16, [None, None]),
            ]
            inits.append(from_array(numpy.array([1], dtype=numpy.float32), name="I"))
        else:
            return None
        node_kw = dict(
            alpha=1.0,
            transA=1,
            domain=domain,
            computeType=computeType,
            fastAccumulationMode=1,
            rowMajor=0 if op_name == "CustomGemmFloat8E4M3FN" else 1,
        )
        node_kw["name"] = (
            f"{mat_type}.{len(node_output)}.{len(outputs)}."
            f"{domain}..{node_kw['rowMajor']}.."
            f"{node_kw['fastAccumulationMode']}..{node_kw['computeType']}.."
            f"{f8}"
        )
        node_inputs = ["A", "B"]
        if f8:
            node_inputs.append("")
            node_inputs.extend(["I"] * 3)
        nodes = [make_node(op_name, node_inputs, node_output, **node_kw)]
    else:
        nodes = [
            make_node("Gemm", ["A", "B"], ["C"], transA=1, beta=0.0),
        ]
    graph = make_graph(nodes, "a", [A, B], outputs, inits)
    if mat_type < 16:
        # regular type
        opset, ir = 18, 8
    else:
        opset, ir = 19, 9
    onnx_model = make_model(
        graph,
        opset_imports=[
            make_opsetid("", opset),
            make_opsetid("com.microsoft", 1),
            make_opsetid("onnx_extended.ortops.tutorial.cuda", 1),
        ],
        ir_version=ir,
    )
    check_model(onnx_model)
    return onnx_model


print(onnx_simple_text_plot(create_model()))
opset: domain='' version=18
opset: domain='com.microsoft' version=1
opset: domain='onnx_extended.ortops.tutorial.cuda' version=1
input: name='A' type=dtype('float32') shape=['', '']
input: name='B' type=dtype('float32') shape=['', '']
GemmFloat8[com.microsoft](A, B, alpha=1.00, computeType=b'CUBLAS_COMPUTE_32F', fastAccumulationMode=1, rowMajor=1, transA=1) -> C
output: name='C' type=dtype('float32') shape=['', '']

A model to cast into anytype. numpy does not support float 8. onnxruntime is used to cast a float array into any type. It must be called with tensor of type OrtValue.

def create_cast(to, cuda=False):
    A = make_tensor_value_info("A", TensorProto.FLOAT, [None, None])
    C = make_tensor_value_info("C", to, [None, None])
    if cuda:
        nodes = [
            make_node("Cast", ["A"], ["Cc"], to=to),
            make_node("MemcpyFromHost", ["Cc"], ["C"]),
        ]
    else:
        nodes = [make_node("Cast", ["A"], ["C"], to=to)]
    graph = make_graph(nodes, "a", [A], [C])
    if to < 16:
        # regular type
        opset, ir = 18, 8
    else:
        opset, ir = 19, 9
    onnx_model = make_model(
        graph, opset_imports=[make_opsetid("", opset)], ir_version=ir
    )
    if not cuda:
        # OpType: MemcpyFromHost
        check_model(onnx_model)
    return onnx_model


print(onnx_simple_text_plot(create_cast(TensorProto.FLOAT16)))
opset: domain='' version=18
input: name='A' type=dtype('float32') shape=['', '']
Cast(A, to=10) -> C
output: name='C' type=dtype('float16') shape=['', '']

Profiling

The benchmark will run the following configurations.

types = [
    TensorProto.FLOAT8E4M3FN,
    TensorProto.FLOAT,
    TensorProto.FLOAT16,
    TensorProto.BFLOAT16,
    # TensorProto.UINT32,
    # TensorProto.INT32,
    # TensorProto.INT16,
    # TensorProto.INT8,
]
engine = [InferenceSession]
providers = [
    ["CUDAExecutionProvider", "CPUExecutionProvider"],
]
# M, N, K
# we use multiple of 8, otherwise, float8 does not work.
dims = [tuple(int(i) for i in line.split(",")) for line in script_args.dims.split(";")]
domains = ["onnx_extended.ortops.tutorial.cuda", "", "com.microsoft"]

Let’s cache the matrices involved.

def to_ort_value(m):
    device = C_OrtDevice(C_OrtDevice.cpu(), C_OrtDevice.default_memory(), 0)
    ort_value = C_OrtValue.ortvalue_from_numpy(m, device)
    return ort_value


def cached_inputs(dims, types):
    matrices = {}
    matrices_cuda = {}
    for m, n, k in dims:
        for tt in types:
            for i, j in [(m, k), (k, n), (k, m)]:
                if (tt, i, j) in matrices:
                    continue
                # CPU
                try:
                    sess = InferenceSession(
                        create_cast(tt).SerializeToString(),
                        providers=["CPUExecutionProvider"],
                    )
                    cpu = True
                except (InvalidGraph, InvalidArgument, NotImplemented):
                    # not support by this version of onnxruntime
                    cpu = False

                if cpu:
                    vect = (numpy.random.randn(i, j) * 10).astype(numpy.float32)
                    ov = to_ort_value(vect)
                    ovtt = sess._sess.run_with_ort_values({"A": ov}, ["C"], None)[0]
                    matrices[tt, i, j] = ovtt
                else:
                    continue

                # CUDA
                if "CUDAExecutionProvider" not in get_available_providers():
                    # No CUDA
                    continue
                sess = InferenceSession(
                    create_cast(tt, cuda=True).SerializeToString(),
                    providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
                )
                vect = (numpy.random.randn(i, j) * 10).astype(numpy.float32)
                ov = to_ort_value(vect)
                ovtt = sess._sess.run_with_ort_values({"A": ov}, ["C"], None)[0]
                matrices_cuda[tt, i, j] = ovtt
    return matrices, matrices_cuda


matrices, matrices_cuda = cached_inputs(dims, types)
print(f"{len(matrices)} matrices were created.")
8 matrices were created.

Let’s run the profiles

opts = SessionOptions()
r = get_ort_ext_libs()
if r is not None:
    opts.register_custom_ops_library(r[0])


data = []
pbar = tqdm(list(product(types, engine, providers, dims, domains)))
for tt, engine, provider, dim, domain in pbar:
    if "CUDAExecutionProvider" not in get_available_providers():
        # No CUDA.
        continue
    if (
        tt in {TensorProto.FLOAT8E4M3FN, TensorProto.FLOAT8E5M2}
        and properties.get("major", 0) < 9
    ):
        # f8 not available
        continue

    onx = create_model(tt, provider=provider[0], domain=domain)
    if onx is None:
        # Not available on this machine
        continue
    with open(f"plot_bench_gemm_profile_{tt}_{domain}.onnx", "wb") as f:
        f.write(onx.SerializeToString())
    k1 = (tt, dim[2], dim[0])
    k2 = (tt, dim[2], dim[1])

    pbar.set_description(f"t={tt} e={engine.__name__} p={provider[0][:4]} dim={dim}")

    try:
        sess = engine(onx.SerializeToString(), opts, providers=provider)
    except Exception:
        # Seomthing went wrong.
        continue

    the_feeds = {"A": matrices_cuda[k1], "B": matrices_cuda[k2]}
    out_names = ["C"]

    if ort_profile is None:
        raise ImportError("Could not import ort_profile from onnx-array-api.")
    df = ort_profile(
        onx,
        the_feeds,
        sess_options=opts,
        repeat=script_args.repeat_profile,
        as_df=True,
        providers=provider,
        first_it_out=True,
        agg=True,
    ).reset_index(drop=False)
    columns = ["xdim", "xdomain", "xdtype"] + list(df.columns)
    df["xdim"] = "x".join(map(str, dim))
    df["xdomain"] = {
        "onnx_extended.ortops.tutorial.cuda": "EXT",
        "": "ORT",
        "com.microsoft": "COM",
    }[domain]
    df["args_op_name"] = {
        "onnx_extended.ortops.tutorial.cuda": "CG",
        "": "Gemm",
        "com.microsoft": "G8",
    }[domain]
    df["xdtype"] = {1: "f32", 10: "f16", 16: "bf16", 17: "e4m3fn", 18: "e5m2"}[tt]
    df = df[columns]
    data.append(df)

    if unit_test_going() and len(data) >= 2:
        break
  0%|          | 0/24 [00:00<?, ?it/s]
t=1 e=InferenceSession p=CUDA dim=(256, 256, 256):   0%|          | 0/24 [00:00<?, ?it/s]
t=1 e=InferenceSession p=CUDA dim=(256, 256, 256):  29%|██▉       | 7/24 [00:01<00:02,  6.25it/s]
t=1 e=InferenceSession p=CUDA dim=(256, 256, 256):  29%|██▉       | 7/24 [00:01<00:02,  6.25it/s]
t=1 e=InferenceSession p=CUDA dim=(256, 256, 256):  29%|██▉       | 7/24 [00:01<00:02,  6.25it/s]
t=1 e=InferenceSession p=CUDA dim=(512, 512, 512):  29%|██▉       | 7/24 [00:01<00:02,  6.25it/s]
t=1 e=InferenceSession p=CUDA dim=(512, 512, 512):  42%|████▏     | 10/24 [00:01<00:01,  7.11it/s]
t=1 e=InferenceSession p=CUDA dim=(512, 512, 512):  42%|████▏     | 10/24 [00:01<00:01,  7.11it/s]
t=1 e=InferenceSession p=CUDA dim=(512, 512, 512):  46%|████▌     | 11/24 [00:01<00:01,  7.01it/s]
t=1 e=InferenceSession p=CUDA dim=(512, 512, 512):  46%|████▌     | 11/24 [00:01<00:01,  7.01it/s]
t=10 e=InferenceSession p=CUDA dim=(256, 256, 256):  46%|████▌     | 11/24 [00:01<00:01,  7.01it/s]
t=10 e=InferenceSession p=CUDA dim=(256, 256, 256):  54%|█████▍    | 13/24 [00:01<00:01,  6.52it/s]
t=10 e=InferenceSession p=CUDA dim=(256, 256, 256):  54%|█████▍    | 13/24 [00:01<00:01,  6.52it/s]
t=10 e=InferenceSession p=CUDA dim=(256, 256, 256):  58%|█████▊    | 14/24 [00:02<00:01,  6.88it/s]
t=10 e=InferenceSession p=CUDA dim=(256, 256, 256):  58%|█████▊    | 14/24 [00:02<00:01,  6.88it/s]
t=10 e=InferenceSession p=CUDA dim=(512, 512, 512):  58%|█████▊    | 14/24 [00:02<00:01,  6.88it/s]
t=10 e=InferenceSession p=CUDA dim=(512, 512, 512):  67%|██████▋   | 16/24 [00:04<00:03,  2.33it/s]
t=10 e=InferenceSession p=CUDA dim=(512, 512, 512):  67%|██████▋   | 16/24 [00:04<00:03,  2.33it/s]
t=10 e=InferenceSession p=CUDA dim=(512, 512, 512):  71%|███████   | 17/24 [00:04<00:02,  2.71it/s]
t=10 e=InferenceSession p=CUDA dim=(512, 512, 512):  71%|███████   | 17/24 [00:04<00:02,  2.71it/s]
t=16 e=InferenceSession p=CUDA dim=(256, 256, 256):  71%|███████   | 17/24 [00:04<00:02,  2.71it/s]
t=16 e=InferenceSession p=CUDA dim=(256, 256, 256):  83%|████████▎ | 20/24 [00:04<00:00,  4.60it/s]
t=16 e=InferenceSession p=CUDA dim=(256, 256, 256):  83%|████████▎ | 20/24 [00:04<00:00,  4.60it/s]
t=16 e=InferenceSession p=CUDA dim=(512, 512, 512):  83%|████████▎ | 20/24 [00:04<00:00,  4.60it/s]
t=16 e=InferenceSession p=CUDA dim=(512, 512, 512):  96%|█████████▌| 23/24 [00:04<00:00,  6.64it/s]
t=16 e=InferenceSession p=CUDA dim=(512, 512, 512):  96%|█████████▌| 23/24 [00:04<00:00,  6.64it/s]
t=16 e=InferenceSession p=CUDA dim=(512, 512, 512): 100%|██████████| 24/24 [00:04<00:00,  5.44it/s]

Results

if data:
    df = concat(data, axis=0)
    df.to_excel("plot_profile_gemm_ort.xlsx")
    df.to_csv("plot_profile_gemm_ort.csv")
    print(df.head().T)
                           0             1                      2                            3            4
xdim             256x256x256   256x256x256            256x256x256                  256x256x256  256x256x256
xdomain                  EXT           EXT                    EXT                          EXT          EXT
xdtype                   f32           f32                    f32                          f32          f32
it==0                      0             0                      0                            0            0
cat                     Node          Node                   Node                      Session      Session
args_node_index                                                 0
args_op_name              CG            CG                     CG                           CG           CG
args_provider                               CUDAExecutionProvider
event_name       fence_after  fence_before            kernel_time  SequentialExecutor::Execute    model_run
dur                        1            11                  77607                        83081       107891

Summary

if data:
    piv = pivot_table(
        df[df["it==0"] == 0],
        index=["xdim", "cat", "event_name"],
        columns=["xdtype", "xdomain", "args_op_name"],
        values=["dur"],
    )
    piv.reset_index(drop=False).to_excel("plot_profile_gemm_ort_summary.xlsx")
    piv.reset_index(drop=False).to_csv("plot_profile_gemm_ort_summary.csv")

    print()
    print("summary")
    print(piv)
summary
                                                     dur
xdtype                                              bf16        f16                f32
xdomain                                              ORT        EXT      ORT       EXT       ORT
args_op_name                                        Gemm         CG     Gemm        CG      Gemm
xdim        cat     event_name
256x256x256 Node    fence_after                      6.0        0.0      0.0       1.0       0.0
                    fence_before                    53.0        9.0      5.0      11.0      12.0
                    kernel_time                   3805.0   258186.0    653.0   77607.0     617.0
            Session SequentialExecutor::Execute   4922.0   276226.0   1047.0   83081.0    1230.0
                    model_run                    32872.0   293517.0  22503.0  107891.0   28256.0
512x512x512 Node    fence_after                      4.0        3.0      0.0      14.0       2.0
                    fence_before                    38.0        8.0     19.0      14.0      23.0
                    kernel_time                   2187.0  1677734.0    807.0  106929.0    1054.0
            Session SequentialExecutor::Execute   3389.0  1782222.0   1277.0  117346.0    1662.0
                    model_run                    95772.0  1824003.0  72224.0  197278.0  106903.0

plot

if data:
    print()
    print("compact")

    pivi = pivot_table(
        df[(df["it==0"] == 0) & (df["event_name"] == "kernel_time")],
        index=["xdim"],
        columns=["xdtype", "xdomain", "args_op_name"],
        values="dur",
    )
    print(pivi)

    print()
    print("not operator")

    pivinot = pivot_table(
        df[df["cat"] != "Node"],
        index=["xdim", "event_name"],
        columns=["xdtype", "xdomain"],
        values="dur",
    )
    print(pivinot)


if data:
    fig, ax = plt.subplots(2, 2, figsize=(12, 8))
    pivi.T.plot(
        ax=ax[0, 0],
        title="kernel time",
        kind="barh",
        logx=True,
    )
    pivinot.T.plot(
        ax=ax[1, 0],
        title="Global times",
        kind="barh",
        logx=True,
    )

    for i, name in enumerate(["fence_before", "fence_after"]):
        pivi = pivot_table(
            df[(df["it==0"] == 0) & (df["event_name"] == name)],
            index=["xdim"],
            columns=["xdtype", "xdomain", "args_op_name"],
            values="dur",
        )
        pivi.T.plot(
            ax=ax[i, 1],
            title=f"{name}",
            kind="barh",
            logx=True,
        )

    fig.tight_layout()
    fig.savefig("plot_bench_gemm_ort.png")
kernel time, fence_before, Global times, fence_after
compact
xdtype          bf16        f16              f32
xdomain          ORT        EXT    ORT       EXT     ORT
args_op_name    Gemm         CG   Gemm        CG    Gemm
xdim
256x256x256   3805.0   258186.0  653.0   77607.0   617.0
512x512x512   2187.0  1677734.0  807.0  106929.0  1054.0

not operator
xdtype                                      bf16       f16                f32
xdomain                                      ORT       EXT      ORT       EXT      ORT
xdim        event_name
256x256x256 SequentialExecutor::Execute   7871.5  148575.0  10831.5  520392.5   1321.0
            model_loading_array            135.0      87.0     95.0     266.0     67.0
            model_run                    23784.0  158797.5  23340.0  536924.5  16785.5
            session_initialization         684.0     435.0    396.0    1668.0    296.0
512x512x512 SequentialExecutor::Execute   2083.5  943891.5    979.0   62387.0   1372.0
            model_loading_array             75.0      98.0     97.0      83.0     96.0
            model_run                    52400.5  967147.5  39986.5  106062.0  58466.0
            session_initialization         358.0     480.0    565.0     394.0    571.0

Total running time of the script: (0 minutes 8.372 seconds)

Gallery generated by Sphinx-Gallery