v4.5 tag update (#3202)
* Python DSL examples reorganization. * v4.5 tag update.
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from typing import Type
|
||||
|
||||
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
import cutlass.cute.testing as testing
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
|
||||
"""
|
||||
An Elementwise Addition Example using CuTe DSL.
|
||||
|
||||
This example kernel copies data from global memory to register memory (rmem), performs the elementwise
|
||||
addition operation, and stores the result back to global memory.
|
||||
|
||||
Primary goals of this example are to demonstrate how basic global memory copies can be expressed in
|
||||
CuTe DSL and illustrate canonical partitioning patterns in CuTe. It also implements canonical
|
||||
predication for tensors whose shape is not multiple of tile size to guard OOB reads.
|
||||
|
||||
Thread-value (or TV) layouts are central to canonical partitioning patterns in CuTe. They provide a
|
||||
mapping from thread and a thread's value to the set of coordinates within a tile that we have sliced
|
||||
out from a data tensor.
|
||||
|
||||
The input tensors are row-major layout, that leading dimension is the right most dimension. In order
|
||||
to efficiently copy data from global memory, we must map threads contiguously on row dimension.
|
||||
|
||||
Thread ID mapping to 2D coordinates with layout `(4,32):(32,1)`:
|
||||
|
||||
+----+----+----+----+-----+----+
|
||||
| | 0 | 1 | 2 | ... | 31 |
|
||||
+----+----+----+----+-----+----+
|
||||
| 0 | T0 | T1 | T2 | ... | T31|
|
||||
+----+----+----+----+-----+----+
|
||||
| 1 |T32 |T33 |T34 | ... |T63 |
|
||||
+----+----+----+----+-----+----+
|
||||
| 2 |T64 |T65 |T66 | ... |T95 |
|
||||
+----+----+----+----+-----+----+
|
||||
| 3 |T96 |T97 |T98 | ... |T127|
|
||||
+----+----+----+----+-----+----+
|
||||
|
||||
As Ampere GPU supports a maximum of 128bit per load/store instruction and each element is 32bit, we
|
||||
can load 4 elements per instruction. Having additional contiguous values allows for vectorization
|
||||
across threads (coalesced accesses) and is required for saturating the memory bandwidth.
|
||||
|
||||
We use `(4,4):(4,1)` as the val layout in this example. Notice that the major mode is the same as
|
||||
the major mode of the input tensor - without which vectorization would not be possible.
|
||||
|
||||
If you already know the TV layout you want to use for your tiled copy, CuTe DSL provides utility
|
||||
`cute.make_layout_tv` to build the tiled copy type around it and the atom of your choice.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thr_layout = cute.make_layout((4, 32), stride=(32, 1))
|
||||
val_layout = cute.make_layout((4, 4), stride=(4, 1))
|
||||
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
|
||||
|
||||
# Tile input tensor to thread blocks: ((TileM,TileN),(RestM,RestN))
|
||||
gA = cute.zipped_divide(mA, tiler_mn)
|
||||
|
||||
Then we can build tiled copy for input and output tensors with `cute.make_tiled_copy_tv` utility, which
|
||||
infers the tiler and tv layout for the tiled copy automatically, where `tiler` is the tile size per thread
|
||||
block and `tv_layout` is the TV layout which maps thread index and inter-thread index of data array per
|
||||
thread to logical coordinates of elements in input and output tensors.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
blkA = gA[((None, None), bidx)] # (TileM,TileN)
|
||||
|
||||
copy_atom_load = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), gA.element_type)
|
||||
tiled_copy_A = cute.make_tiled_copy_tv(copy_atom_load, thr_layout, val_layout)
|
||||
|
||||
# get slice of tiled_copy_A for current thread
|
||||
thr_copy_A = tiled_copy_A.get_slice(tidx)
|
||||
|
||||
# partition per thread block tensor as source of tiled copy
|
||||
thrA = thr_copy_A.partition_S(blkA)
|
||||
|
||||
# allocate fragment for gmem->rmem
|
||||
frgA = cute.make_fragment_like(thrA)
|
||||
|
||||
# copy data from global memory to register memory
|
||||
cute.copy(copy_atom_load, thrA, frgA)
|
||||
|
||||
|
||||
To run this example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python examples/ampere/elementwise_add.py --M 3 --N 12
|
||||
python examples/ampere/elementwise_add.py --M 1024 --N 512
|
||||
python examples/ampere/elementwise_add.py --M 1024 --N 1024 --benchmark --warmup_iterations 2 --iterations 1000
|
||||
|
||||
To collect performance with NCU profiler:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Don't iterate too many times when profiling with ncu
|
||||
ncu python examples/ampere/elementwise_add.py --M 2048 --N 2048 --benchmark --iterations 10 --skip_ref_check
|
||||
"""
|
||||
|
||||
|
||||
@cute.kernel
|
||||
def elementwise_add_kernel(
|
||||
gA: cute.Tensor,
|
||||
gB: cute.Tensor,
|
||||
gC: cute.Tensor,
|
||||
cC: cute.Tensor, # coordinate tensor
|
||||
shape: cute.Shape,
|
||||
thr_layout: cute.Layout,
|
||||
val_layout: cute.Layout,
|
||||
):
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
bidx, _, _ = cute.arch.block_idx()
|
||||
|
||||
# slice for CTAs
|
||||
# logical id -> address
|
||||
blk_coord = ((None, None), bidx)
|
||||
blkA = gA[blk_coord] # (TileM,TileN)
|
||||
blkB = gB[blk_coord] # (TileM,TileN)
|
||||
blkC = gC[blk_coord] # (TileM,TileN)
|
||||
blkCrd = cC[blk_coord] # (TileM, TileN)
|
||||
|
||||
# Note: these prints only run at compile/jit time
|
||||
print("[DSL INFO] Sliced Tensors per thread block:")
|
||||
print(f"[DSL INFO] blkA = {blkA.type}")
|
||||
print(f"[DSL INFO] blkB = {blkB.type}")
|
||||
print(f"[DSL INFO] blkC = {blkC.type}")
|
||||
print(f"[DSL INFO] blkCrd = {blkCrd.type}")
|
||||
|
||||
# # declare the atoms which will be used later for memory copy
|
||||
copy_atom_load = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), gA.element_type)
|
||||
copy_atom_store = cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), gC.element_type)
|
||||
|
||||
tiled_copy_A = cute.make_tiled_copy_tv(copy_atom_load, thr_layout, val_layout)
|
||||
tiled_copy_B = cute.make_tiled_copy_tv(copy_atom_load, thr_layout, val_layout)
|
||||
tiled_copy_C = cute.make_tiled_copy_tv(copy_atom_store, thr_layout, val_layout)
|
||||
|
||||
thr_copy_A = tiled_copy_A.get_slice(tidx)
|
||||
thr_copy_B = tiled_copy_B.get_slice(tidx)
|
||||
thr_copy_C = tiled_copy_C.get_slice(tidx)
|
||||
|
||||
thrA = thr_copy_A.partition_S(blkA)
|
||||
thrB = thr_copy_B.partition_S(blkB)
|
||||
thrC = thr_copy_C.partition_S(blkC)
|
||||
|
||||
# allocate fragments for gmem->rmem
|
||||
frgA = cute.make_fragment_like(thrA)
|
||||
frgB = cute.make_fragment_like(thrB)
|
||||
frgC = cute.make_fragment_like(thrC)
|
||||
|
||||
thrCrd = thr_copy_C.partition_S(blkCrd)
|
||||
frgPred = cute.make_rmem_tensor(thrCrd.shape, cutlass.Boolean)
|
||||
|
||||
print("[DSL INFO] Sliced Tensors per thread:")
|
||||
print(f"[DSL INFO] thrA = {thrA.type}")
|
||||
print(f"[DSL INFO] thrB = {thrB.type}")
|
||||
print(f"[DSL INFO] thrC = {thrC.type}")
|
||||
print(f"[DSL INFO] thrCrd = {thrCrd.type}")
|
||||
|
||||
for i in range(0, cute.size(frgPred), 1):
|
||||
val = cute.elem_less(thrCrd[i], shape)
|
||||
frgPred[i] = val
|
||||
|
||||
# Print per thread predicate mask
|
||||
# if tidx == 0 and bidx == 0:
|
||||
# cute.printf("block_dim = {}", cute.arch.grid_dim())
|
||||
# cute.printf("shape = {}", shape)
|
||||
# cute.print_tensor(thrA)
|
||||
# cute.print_tensor(thrB)
|
||||
# cute.print_tensor(frgPred)
|
||||
|
||||
##########################################################
|
||||
# Move data to reg address space
|
||||
##########################################################
|
||||
|
||||
cute.copy(copy_atom_load, thrA, frgA, pred=frgPred)
|
||||
cute.copy(copy_atom_load, thrB, frgB, pred=frgPred)
|
||||
|
||||
# if tidx == 0 and bidx == 0:
|
||||
# cute.print_tensor(frgA)
|
||||
# cute.print_tensor(frgB)
|
||||
|
||||
# Load data before use. The compiler will optimize the copy and load
|
||||
# operations to convert some memory ld/st into register uses.
|
||||
result = frgA.load() + frgB.load()
|
||||
|
||||
# Save the results back to registers. Here we reuse b's registers.
|
||||
frgC.store(result)
|
||||
|
||||
# Copy the results back to c
|
||||
cute.copy(copy_atom_store, frgC, thrC, pred=frgPred)
|
||||
|
||||
|
||||
@cute.jit
|
||||
def elementwise_add(mA, mB, mC, copy_bits: cutlass.Constexpr = 128):
|
||||
dtype = mA.element_type
|
||||
vector_size = copy_bits // dtype.width
|
||||
|
||||
thr_layout = cute.make_ordered_layout((4, 32), order=(1, 0))
|
||||
val_layout = cute.make_ordered_layout((4, vector_size), order=(1, 0))
|
||||
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
|
||||
|
||||
print("[DSL INFO] Input Tensors:")
|
||||
print(f"[DSL INFO] mA = {mA.type}")
|
||||
print(f"[DSL INFO] mB = {mB.type}")
|
||||
|
||||
print("[DSL INFO] Tiling Parameters:")
|
||||
print(f"[DSL INFO] tiler_mn = {tiler_mn} per thread block")
|
||||
print(f"[DSL INFO] tv_layout = {tv_layout}")
|
||||
|
||||
gA = cute.zipped_divide(mA, tiler_mn) # ((TileM,TileN),(RestM,RestN))
|
||||
gB = cute.zipped_divide(mB, tiler_mn) # ((TileM,TileN),(RestM,RestN))
|
||||
gC = cute.zipped_divide(mC, tiler_mn) # ((TileM,TileN),(RestM,RestN))
|
||||
print("[DSL INFO] Tiled Tensors:")
|
||||
print(f"[DSL INFO] gA = {gA.type}")
|
||||
print(f"[DSL INFO] gB = {gB.type}")
|
||||
print(f"[DSL INFO] gC = {gC.type}")
|
||||
|
||||
idC = cute.make_identity_tensor(mC.shape)
|
||||
cC = cute.zipped_divide(idC, tiler=tiler_mn)
|
||||
print(f"[DSL INFO] coord tensor = {cC.type}")
|
||||
|
||||
kernel_name = f"cutlass_dsl_elementwise_add_kernel"
|
||||
elementwise_add_kernel.set_name_prefix(kernel_name)
|
||||
elementwise_add_kernel(gA, gB, gC, cC, mC.shape, thr_layout, val_layout).launch(
|
||||
grid=[cute.size(gC, mode=[1]), 1, 1],
|
||||
block=[cute.size(tv_layout, mode=[0]), 1, 1],
|
||||
)
|
||||
|
||||
|
||||
def run_elementwise_add(
|
||||
M,
|
||||
N,
|
||||
dtype: Type[cutlass.Numeric],
|
||||
is_a_dynamic_layout=False,
|
||||
is_b_dynamic_layout=False,
|
||||
is_result_dynamic_layout=False,
|
||||
skip_ref_check=False,
|
||||
benchmark=True,
|
||||
warmup_iterations=2,
|
||||
iterations=200,
|
||||
):
|
||||
import torch
|
||||
import cutlass.torch as cutlass_torch
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
raise RuntimeError("Ampere GPU is required to run this example!")
|
||||
|
||||
print("\nRunning Elementwise Add test with:")
|
||||
print(f"Tensor dimensions: [{M}, {N}]")
|
||||
print(f"Input and Output Data type: {dtype}")
|
||||
|
||||
torch_dtype = cutlass_torch.dtype(dtype)
|
||||
if dtype.is_integer:
|
||||
a = torch.randint(0, 10, (M, N), device=torch.device("cuda"), dtype=torch_dtype)
|
||||
b = torch.randint(0, 10, (M, N), device=torch.device("cuda"), dtype=torch_dtype)
|
||||
else:
|
||||
a = torch.randn(M, N, device=torch.device("cuda"), dtype=torch_dtype)
|
||||
b = torch.randn(M, N, device=torch.device("cuda"), dtype=torch_dtype)
|
||||
|
||||
c = torch.zeros_like(a)
|
||||
|
||||
print("Input tensor shapes:")
|
||||
print(f"a: {a.shape}, dtype: {a.dtype}")
|
||||
print(f"b: {b.shape}, dtype: {b.dtype}")
|
||||
print(f"c: {c.shape}, dtype: {c.dtype}\n")
|
||||
|
||||
if not is_a_dynamic_layout:
|
||||
a_tensor = from_dlpack(a).mark_layout_dynamic()
|
||||
else:
|
||||
a_tensor = a
|
||||
|
||||
if not is_b_dynamic_layout:
|
||||
b_tensor = from_dlpack(b).mark_layout_dynamic()
|
||||
else:
|
||||
b_tensor = b
|
||||
|
||||
if not is_result_dynamic_layout:
|
||||
c_tensor = from_dlpack(c).mark_layout_dynamic()
|
||||
else:
|
||||
c_tensor = c
|
||||
|
||||
elementwise_add.set_name_prefix("host_prefix")
|
||||
|
||||
print("Compiling kernel with cute.compile ...")
|
||||
start_time = time.time()
|
||||
compiled_func = cute.compile(
|
||||
elementwise_add, a_tensor, b_tensor, c_tensor, options="--generate-line-info"
|
||||
)
|
||||
compilation_time = time.time() - start_time
|
||||
print(f"Compilation time: {compilation_time:.4f} seconds")
|
||||
|
||||
print("Executing vector add kernel...")
|
||||
|
||||
# Get current CUstream from torch
|
||||
current_stream = cutlass_torch.current_stream()
|
||||
|
||||
if not skip_ref_check:
|
||||
compiled_func(a_tensor, b_tensor, c_tensor)
|
||||
print("Verifying results...")
|
||||
torch.testing.assert_close(a + b, c)
|
||||
print("Results verified successfully!")
|
||||
|
||||
if not benchmark:
|
||||
return
|
||||
|
||||
def generate_tensors():
|
||||
if dtype.is_integer:
|
||||
a = torch.randint(
|
||||
0, 10, (M, N), device=torch.device("cuda"), dtype=torch_dtype
|
||||
)
|
||||
b = torch.randint(
|
||||
0, 10, (M, N), device=torch.device("cuda"), dtype=torch_dtype
|
||||
)
|
||||
else:
|
||||
a = torch.randn(M, N, device=torch.device("cuda"), dtype=torch_dtype)
|
||||
b = torch.randn(M, N, device=torch.device("cuda"), dtype=torch_dtype)
|
||||
|
||||
c = torch.zeros_like(a)
|
||||
|
||||
if not is_a_dynamic_layout:
|
||||
a_tensor = from_dlpack(a).mark_layout_dynamic()
|
||||
else:
|
||||
a_tensor = a
|
||||
|
||||
if not is_b_dynamic_layout:
|
||||
b_tensor = from_dlpack(b).mark_layout_dynamic()
|
||||
else:
|
||||
b_tensor = b
|
||||
|
||||
if not is_result_dynamic_layout:
|
||||
c_tensor = from_dlpack(c).mark_layout_dynamic()
|
||||
else:
|
||||
c_tensor = c
|
||||
|
||||
return testing.JitArguments(a_tensor, b_tensor, c_tensor)
|
||||
|
||||
avg_time_us = testing.benchmark(
|
||||
compiled_func,
|
||||
workspace_generator=generate_tensors,
|
||||
workspace_count=10,
|
||||
warmup_iterations=warmup_iterations,
|
||||
iterations=iterations,
|
||||
)
|
||||
|
||||
# Print execution results
|
||||
print(f"Kernel execution time: {avg_time_us / 1e3:.4f} ms")
|
||||
print(
|
||||
f"Achieved memory throughput: {(3 * a.numel() * dtype.width // 8) / (avg_time_us / 1e6) / 1e9:.2f} GB/s"
|
||||
)
|
||||
print(f"First few elements of result: \n{c[:3, :3]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="example of elementwise add to demonstrate the numpy/pytorch as input for kernels"
|
||||
)
|
||||
parser.add_argument("--M", default=1024, type=int)
|
||||
parser.add_argument("--N", default=1024, type=int)
|
||||
parser.add_argument("--warmup_iterations", default=2, type=int)
|
||||
parser.add_argument("--iterations", default=100, type=int)
|
||||
parser.add_argument("--skip_ref_check", action="store_true")
|
||||
parser.add_argument("--benchmark", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
run_elementwise_add(
|
||||
args.M,
|
||||
args.N,
|
||||
dtype=cutlass.Float32,
|
||||
is_a_dynamic_layout=True,
|
||||
is_b_dynamic_layout=True,
|
||||
is_result_dynamic_layout=True,
|
||||
skip_ref_check=args.skip_ref_check,
|
||||
benchmark=args.benchmark,
|
||||
warmup_iterations=args.warmup_iterations,
|
||||
iterations=args.iterations,
|
||||
)
|
||||
print("\nPASS")
|
||||
@@ -0,0 +1,399 @@
|
||||
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
import argparse
|
||||
import operator
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import List, Type
|
||||
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.cute as cute
|
||||
import cutlass.cute.testing as testing
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
|
||||
import cutlass
|
||||
|
||||
"""
|
||||
An Elementwise Apply Example using CuTe DSL.
|
||||
|
||||
This example kernel demonstrates the meta-programming capability of the CuTe DSL by allowing
|
||||
customization of elementwise operations through lambda functions. The kernel copies data from
|
||||
global memory to register memory (rmem), applies a user-defined operation to the elements,
|
||||
and stores the result back to global memory.
|
||||
|
||||
Primary goals of this example:
|
||||
1. Demonstrate meta-programming capability by passing lambda functions to customize elementwise operations
|
||||
2. Show how to apply different operations (add, multiply, etc.) using the same kernel structure
|
||||
3. Illustrate how to parameterize CUDA kernels with operation types at compile time
|
||||
|
||||
To run this example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run with addition operation
|
||||
python examples/ampere/elementwise_apply.py --M 1024 --N 512 --op add
|
||||
|
||||
# Run with multiplication operation
|
||||
python examples/ampere/elementwise_apply.py --M 1024 --N 512 --op mul
|
||||
|
||||
# Run with subtraction operation
|
||||
python examples/ampere/elementwise_apply.py --M 1024 --N 512 --op sub
|
||||
|
||||
# Benchmark performance
|
||||
python examples/ampere/elementwise_apply.py --M 2048 --N 2048 --op add --benchmark --warmup_iterations 2 --iterations 10
|
||||
|
||||
The example demonstrates how to express complex CUDA kernels with customizable operations
|
||||
while maintaining high performance through efficient memory access patterns.
|
||||
"""
|
||||
|
||||
|
||||
@cute.kernel
|
||||
def elementwise_apply_kernel(
|
||||
op: cutlass.Constexpr,
|
||||
mInputs: List[cute.Tensor],
|
||||
mC: cute.Tensor,
|
||||
cC: cute.Tensor, # coordinate tensor
|
||||
shape: cute.Shape,
|
||||
tv_layout: cute.Layout, # (tid, vid) -> logic coord
|
||||
):
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
bidx, bidy, _ = cute.arch.block_idx()
|
||||
|
||||
###############################################################################
|
||||
# Slice to local tile of thread block
|
||||
###############################################################################
|
||||
blk_crd = ((None, None), (bidx, bidy))
|
||||
|
||||
# Leverage the meta-programming capability of the DSL to slice the tensors for each input
|
||||
# All for loops below on input tensors would be fully unrolled automatically at compile time
|
||||
# logical coord -> memory address
|
||||
gInputs = [t[blk_crd] for t in mInputs] # (TileM, TileN)
|
||||
gC = mC[blk_crd] # (TileM, TileN)
|
||||
gCrd = cC[blk_crd] # (TileM, TileN)
|
||||
|
||||
print("[DSL INFO] Sliced Tensors per thread block:")
|
||||
for i in cutlass.range_constexpr(len(gInputs)):
|
||||
print(f"[DSL INFO] ctaInputs{i} = {gInputs[i].type}")
|
||||
print(f"[DSL INFO] gC = {gC.type}")
|
||||
print(f"[DSL INFO] gCrd = {gCrd.type}")
|
||||
|
||||
###############################################################################
|
||||
# Compose with thread block TV layout to map thread & value indices to memory address
|
||||
###############################################################################
|
||||
# (tid, vid) -> memory address
|
||||
tidfrgInputs = [cute.composition(t, tv_layout) for t in gInputs]
|
||||
tidfrgC = cute.composition(gC, tv_layout)
|
||||
tidfrgCrd = cute.composition(gCrd, tv_layout)
|
||||
|
||||
# repeat None like vid to remove hierarchy of layout
|
||||
thr_crd = (tidx, cute.repeat_like(None, tidfrgInputs[0][1]))
|
||||
|
||||
###############################################################################
|
||||
# Slice to local tile of thread
|
||||
###############################################################################
|
||||
# vid -> address
|
||||
thrInputs = [t[thr_crd] for t in tidfrgInputs] # (V)
|
||||
thrC = tidfrgC[thr_crd] # (V)
|
||||
thrCrd = tidfrgCrd[thr_crd]
|
||||
|
||||
print("[DSL INFO] Sliced Tensors per thread:")
|
||||
for i in cutlass.range_constexpr(len(thrInputs)):
|
||||
print(f"[DSL INFO] thrInputs{i} = {thrInputs[i].type}")
|
||||
print(f"[DSL INFO] thrC = {thrC.type}")
|
||||
print(f"[DSL INFO] thrCrd = {thrCrd.type}")
|
||||
|
||||
###############################################################################
|
||||
# Compute predicate for out of boundary checks
|
||||
###############################################################################
|
||||
frgPred = cute.make_rmem_tensor(thrCrd.shape, cutlass.Boolean)
|
||||
print(f"[DSL INFO] frgPred = {frgPred.type}")
|
||||
|
||||
for i in cutlass.range_constexpr(cute.size(frgPred)):
|
||||
frgPred[i] = cute.elem_less(thrCrd[i], shape)
|
||||
|
||||
# if tidx == 0 and bidx == 0:
|
||||
# cute.print_tensor(frgPred)
|
||||
|
||||
##########################################################
|
||||
# Load data and compute result
|
||||
##########################################################
|
||||
|
||||
# Load data before use. The compiler will optimize the copy and load
|
||||
# operations to convert some memory ld/st into register uses.
|
||||
result = op(*[thrInput.load() for thrInput in thrInputs])
|
||||
thrC.store(result)
|
||||
|
||||
|
||||
@cute.jit
|
||||
def elementwise_apply(
|
||||
op: cutlass.Constexpr, inputs, result: cute.Tensor, stream: cuda.CUstream
|
||||
):
|
||||
"""CUDA kernel applying binary operator on each element of two n-D input tensors in
|
||||
CuTe Python and store to result tensor.
|
||||
|
||||
:param op: Binary operator or lambda function to apply element-wise
|
||||
:type op: cutlass.Constexpr
|
||||
:param a: First input tensor
|
||||
:type a: cute.Tensor
|
||||
:param b: Second input tensor
|
||||
:type b: cute.Tensor
|
||||
:param result: Output tensor to store the results of op(a, b)
|
||||
:type result: cute.Tensor
|
||||
:return: None
|
||||
:rtype: None
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Example 1: Adding two tensors
|
||||
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32, device="cuda")
|
||||
y = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32, device="cuda")
|
||||
result = torch.empty_like(x)
|
||||
elementwise_apply(operator.add, from_dlpack(x), from_dlpack(y), from_dlpack(result))
|
||||
# result:
|
||||
# tensor([[6.0, 8.0],
|
||||
# [10.0, 12.0]], device='cuda:0')
|
||||
|
||||
# Example 2: Using a lambda function
|
||||
elementwise_apply(lambda a, b: a * a + b * b, from_dlpack(x), from_dlpack(y), from_dlpack(result))
|
||||
# result:
|
||||
# tensor([[ 2., 8.],
|
||||
# [ 54., 512.]], device='cuda:0')
|
||||
"""
|
||||
|
||||
# Baseline: naive TV layout
|
||||
# * mA layout: (4096, 4096):(4096, 1)
|
||||
# * TV layout map to (512, 4) tile
|
||||
# * tidx maps to mode-0 but input layout is contiguous on mode-1, performance will be bad
|
||||
# tv_layout = cute.make_layout((128, (4, 4)), stride=(4, (512, 1)))
|
||||
# cta_tiler = (512, 4)
|
||||
|
||||
# Opt-1: better TV layout with better 1D thread layout (SOL with 1D thread layout)
|
||||
# * mA layout: (4096, 4096):(4096, 1)
|
||||
# * TV layout map to (4, 512) tile
|
||||
# * tidx maps to mode-1 which is leading mode of input tensor for coalesced load
|
||||
# tv_layout = cute.make_layout((128, (4, 4)), stride=(16, (4, 1)))
|
||||
# cta_tiler = (4, 512)
|
||||
|
||||
# Opt-2: 2D tile but worse
|
||||
# * mA layout: (4096, 4096):(4096, 1)
|
||||
# * TV layout map to (128, 16) logical tile
|
||||
# * V layout is bad as contiguous mode is not on right-most
|
||||
# * `cute.copy` only supports vectorize when stride-1 of v-layout on right-most )
|
||||
# tv_layout = cute.make_layout(((32, 4), (4, 4)), stride=((4, 512), (1, 128)))
|
||||
# cta_tiler = (128, 16)
|
||||
|
||||
# Opt-3: SOL with 2D thread tile
|
||||
# * mA layout: (4096, 4096):(4096, 1)
|
||||
# * TV layout map to (64, 256) logical tile
|
||||
# * tidx maps to mode-1 and input layout is contiguous on mode-1 for coalesced load-store
|
||||
|
||||
# Use 128bit(16B) load as canonicalized form of val_layout then recast to target element-type
|
||||
coalesced_ldst_bytes = 16
|
||||
|
||||
# Compile time validation: expect same element type for all input tensors
|
||||
assert all(t.element_type == inputs[0].element_type for t in inputs)
|
||||
dtype = inputs[0].element_type
|
||||
|
||||
thr_layout = cute.make_ordered_layout((4, 64), order=(1, 0))
|
||||
val_layout = cute.make_ordered_layout((16, coalesced_ldst_bytes), order=(1, 0))
|
||||
val_layout = cute.recast_layout(dtype.width, 8, val_layout)
|
||||
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
|
||||
|
||||
print("[DSL INFO] Input Tensors:")
|
||||
for i, t in enumerate(inputs):
|
||||
print(f"[DSL INFO] inputs{i} = {t}")
|
||||
print(f"[DSL INFO] result = {result}")
|
||||
|
||||
print("[DSL INFO] Tiling Parameters:")
|
||||
print(f"[DSL INFO] tiler_mn = {tiler_mn} per thread block")
|
||||
print(f"[DSL INFO] tv_layout = {tv_layout}")
|
||||
|
||||
print("[DSL INFO] Tiled Tensors:")
|
||||
mInputs = [cute.zipped_divide(input, tiler_mn) for input in inputs]
|
||||
# ((TileM, TileN), (RestM, RestN))
|
||||
mC = cute.zipped_divide(result, tiler_mn)
|
||||
|
||||
# (RestM, RestN) -> (RestN, RestM)
|
||||
remap_block = cute.make_ordered_layout(
|
||||
cute.select(mInputs[0].shape[1], mode=[1, 0]), order=(1, 0)
|
||||
)
|
||||
for i, t in enumerate(mInputs):
|
||||
print(f"[DSL INFO] gInputs{i} = {mInputs[i]}")
|
||||
mInputs[i] = cute.composition(t, (None, remap_block))
|
||||
print(f"[DSL INFO] gInputs{i} (remapped) = {mInputs[i]}")
|
||||
|
||||
mC = cute.composition(mC, (None, remap_block))
|
||||
print(f"[DSL INFO] gC = {mC}")
|
||||
|
||||
idC = cute.make_identity_tensor(result.shape)
|
||||
cC = cute.zipped_divide(idC, tiler=tiler_mn)
|
||||
print(f"[DSL INFO] coord tensor = {cC}")
|
||||
|
||||
# Launch the kernel asynchronously
|
||||
# Group input tensors into a list as a single argument
|
||||
elementwise_apply_kernel(op, mInputs, mC, cC, result.shape, tv_layout).launch(
|
||||
# Compute production at each mode of mC.shape[1] to get multi-dimensional grid size
|
||||
grid=cute.product_each(mC.shape[1]),
|
||||
block=[cute.size(tv_layout, mode=[0]), 1, 1],
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
|
||||
@cutlass.dsl_user_op
|
||||
def leaky_relu(x, alpha, *, loc=None, ip=None):
|
||||
return cute.where(x > 0, x, alpha * x, loc=loc, ip=ip)
|
||||
|
||||
|
||||
def leaky_relu_ref(x, alpha):
|
||||
import torch
|
||||
|
||||
return torch.where(x > 0, x, alpha * x)
|
||||
|
||||
|
||||
def run_and_verify(
|
||||
op,
|
||||
M,
|
||||
N,
|
||||
dtype: Type[cutlass.Numeric],
|
||||
skip_ref_check=False,
|
||||
benchmark=True,
|
||||
warmup_iterations=2,
|
||||
iterations=100,
|
||||
):
|
||||
import torch
|
||||
import cutlass.torch as cutlass_torch
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
raise RuntimeError("NVIDIA GPU is required to run this example!")
|
||||
|
||||
if op == "leaky_relu":
|
||||
op = partial(leaky_relu, alpha=0.01)
|
||||
ref_op = partial(leaky_relu_ref, alpha=0.01)
|
||||
num_inputs = 1
|
||||
else:
|
||||
op = getattr(operator, op)
|
||||
ref_op = op
|
||||
num_inputs = 2
|
||||
|
||||
# Create non default CUDA stream from PyTorch
|
||||
torch_stream = torch.cuda.Stream()
|
||||
# Get the raw stream pointer as a CUstream
|
||||
current_stream = cuda.CUstream(torch_stream.cuda_stream)
|
||||
|
||||
print("\nRunning Elementwise Apply test with:")
|
||||
print(f"Tensor dimensions: [{M}, {N}]")
|
||||
print(f"Input and Output Data type: {dtype}")
|
||||
print(f"Warmup iterations: {warmup_iterations}")
|
||||
print(f"Measurement iterations: {iterations}\n")
|
||||
|
||||
torch_dtype = cutlass_torch.dtype(dtype)
|
||||
|
||||
# Allocate tensors with random values.
|
||||
inputs = [
|
||||
torch.randn(M, N, device=torch.device("cuda"), dtype=torch_dtype)
|
||||
for _ in range(num_inputs)
|
||||
]
|
||||
c = torch.zeros_like(inputs[0])
|
||||
|
||||
print("Input tensor shapes:")
|
||||
for i in range(num_inputs):
|
||||
print(f"inputs[{i}]: {inputs[i].shape}, dtype: {inputs[i].dtype}")
|
||||
print(f"c: {c.shape}, dtype: {c.dtype}\n")
|
||||
|
||||
epsilon = 1.2
|
||||
if op in (operator.truediv, operator.floordiv):
|
||||
inputs[1] = torch.where(inputs[1] == 0, torch.tensor(epsilon), inputs[1])
|
||||
|
||||
inputs_ = [from_dlpack(t, assumed_align=16) for t in inputs]
|
||||
c_ = from_dlpack(c, assumed_align=16).mark_layout_dynamic()
|
||||
|
||||
print("Compiling kernel with cute.compile ...")
|
||||
start_time = time.time()
|
||||
compiled_fn = cute.compile[cute.GenerateLineInfo(True)](
|
||||
elementwise_apply, op, inputs_, c_, current_stream
|
||||
)
|
||||
compilation_time = time.time() - start_time
|
||||
print(f"Compilation time: {compilation_time:.4f} seconds")
|
||||
|
||||
if not skip_ref_check:
|
||||
print("Executing elementwise apply kernel...")
|
||||
compiled_fn(inputs_, c_, current_stream)
|
||||
print("Verifying results...")
|
||||
torch.testing.assert_close(ref_op(*inputs), c)
|
||||
print("Results verified successfully!")
|
||||
print(f"First few elements of result: \n{c[:3, :3]}")
|
||||
|
||||
if not benchmark:
|
||||
return
|
||||
|
||||
# When compiled we inlined op in the kernel, so we do not pass it when benchmarking
|
||||
|
||||
print("Benchmarking elementwise apply kernel...")
|
||||
avg_time_us = testing.benchmark(
|
||||
compiled_fn,
|
||||
kernel_arguments=testing.JitArguments(inputs_, c_, current_stream),
|
||||
warmup_iterations=warmup_iterations,
|
||||
iterations=iterations,
|
||||
use_cuda_graphs=True,
|
||||
stream=current_stream,
|
||||
)
|
||||
|
||||
num_elements = sum(input.numel() for input in inputs) + c.numel()
|
||||
|
||||
# Print execution results
|
||||
print(f"Kernel execution time: {avg_time_us / 1e3:.4f} ms")
|
||||
print(
|
||||
f"Achieved memory throughput: {(num_elements * dtype.width // 8) / (avg_time_us * 1000):.2f} GB/s"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Demonstration of building customizable elementwise CUDA kernels using the CuTe DSL"
|
||||
)
|
||||
parser.add_argument("--M", default=4096, type=int)
|
||||
parser.add_argument("--N", default=4096, type=int)
|
||||
parser.add_argument("--op", default="add", type=str)
|
||||
parser.add_argument("--warmup_iterations", default=2, type=int)
|
||||
parser.add_argument("--iterations", default=100, type=int)
|
||||
parser.add_argument("--skip_ref_check", action="store_true")
|
||||
parser.add_argument("--benchmark", action="store_true")
|
||||
args = parser.parse_args()
|
||||
run_and_verify(
|
||||
args.op,
|
||||
args.M,
|
||||
args.N,
|
||||
dtype=cutlass.Float32,
|
||||
warmup_iterations=args.warmup_iterations,
|
||||
iterations=args.iterations,
|
||||
skip_ref_check=args.skip_ref_check,
|
||||
benchmark=args.benchmark,
|
||||
)
|
||||
print("\nPASS")
|
||||
Reference in New Issue
Block a user