29 KiB
29 KiB
In [1]:
import torch
from functools import partial
import cutlass
import cutlass.cute as cute
from cutlass.cute.runtime import from_dlpackIn [2]:
@cute.kernel
def naive_elementwise_add_kernel(
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
bdim, _, _ = cute.arch.block_dim()
thread_idx = bidx * bdim + tidx
# Map thread index to logical index of input tensor
m, n = gA.shape
ni = thread_idx % n
mi = thread_idx // n
# Map logical index to physical address via tensor layout
a_val = gA[mi, ni]
b_val = gB[mi, ni]
# Perform element-wise addition
gC[mi, ni] = a_val + b_valIn [3]:
@cute.jit
def naive_elementwise_add(
mA: cute.Tensor,
mB: cute.Tensor,
mC: cute.Tensor
):
num_threads_per_block = 256
m, n = mA.shape
kernel = naive_elementwise_add_kernel(mA, mB, mC)
kernel.launch(grid=((m * n) // num_threads_per_block, 1, 1),
block=(num_threads_per_block, 1, 1))
M, N = 2048, 2048
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
c = torch.zeros(M, N, device="cuda", dtype=torch.float16)
a_ = from_dlpack(a, assumed_align=16)
b_ = from_dlpack(b, assumed_align=16)
c_ = from_dlpack(c, assumed_align=16)
# Compile kernel
naive_elementwise_add_ = cute.compile(naive_elementwise_add, a_, b_, c_)
naive_elementwise_add_(a_, b_, c_)
# verify correctness
torch.testing.assert_close(c, a + b)In [4]:
def benchmark(callable, *, num_warmups, num_iterations):
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
torch.cuda.synchronize()
for _ in range(num_warmups):
callable()
start_event.record(stream=torch.cuda.current_stream())
for _ in range(num_iterations):
callable()
end_event.record(stream=torch.cuda.current_stream())
torch.cuda.synchronize()
elapsed_time = start_event.elapsed_time(end_event)
avg_time = elapsed_time / num_iterations
print(f"Average execution time: {avg_time:.4f} ms")
print(f"Throughput: {(3 * a.numel() * 2) / (avg_time / 1000) / 1e9:.2f} GB/s")In [5]:
benchmark(partial(naive_elementwise_add_, a_, b_, c_), num_warmups=5, num_iterations=100)Average execution time: 0.0385 ms Throughput: 653.44 GB/s
In [6]:
@cute.kernel
def vectorized_elementwise_add_kernel(
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
bdim, _, _ = cute.arch.block_dim()
thread_idx = bidx * bdim + tidx
# Map thread index to logical index of input tensor
m, n = gA.shape[1] # thread-domain
ni = thread_idx % n
mi = thread_idx // n
# Map logical index to physical address via tensor layout
a_val = gA[(None, (mi, ni))].load()
b_val = gB[(None, (mi, ni))].load()
print(f"[DSL INFO] sliced gA = {gA[(None, (mi, ni))]}")
print(f"[DSL INFO] sliced gB = {gB[(None, (mi, ni))]}")
# Perform element-wise addition
gC[(None, (mi, ni))] = a_val + b_valIn [7]:
@cute.jit
def vectorized_elementwise_add(
mA: cute.Tensor,
mB: cute.Tensor,
mC: cute.Tensor
):
threads_per_block = 256
gA = cute.zipped_divide(mA, (1, 4))
gB = cute.zipped_divide(mB, (1, 4))
gC = cute.zipped_divide(mC, (1, 4))
print(f"[DSL INFO] Tiled Tensors:")
print(f"[DSL INFO] gA = {gA}")
print(f"[DSL INFO] gB = {gB}")
print(f"[DSL INFO] gC = {gC}")
vectorized_elementwise_add_kernel(gA, gB, gC).launch(
grid=(cute.size(gC, mode=[1]) // threads_per_block, 1, 1),
block=(threads_per_block, 1, 1),
)
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
c = torch.zeros(M, N, device="cuda", dtype=torch.float16)
a_ = from_dlpack(a, assumed_align=16)
b_ = from_dlpack(b, assumed_align=16)
c_ = from_dlpack(c, assumed_align=16)
compiled_func = cute.compile(vectorized_elementwise_add, a_, b_, c_)
compiled_func(a_, b_, c_)
# verify correctness
torch.testing.assert_close(c, a + b)[DSL INFO] Tiled Tensors: [DSL INFO] gA = tensor<ptr<f16, gmem, align<16>> o ((1,4),(2048,512)):((0,1),(2048,4))> [DSL INFO] gB = tensor<ptr<f16, gmem, align<16>> o ((1,4),(2048,512)):((0,1),(2048,4))> [DSL INFO] gC = tensor<ptr<f16, gmem, align<16>> o ((1,4),(2048,512)):((0,1),(2048,4))> [DSL INFO] sliced gA = tensor<ptr<f16, gmem, align<8>> o ((1,4)):((0,1))> [DSL INFO] sliced gB = tensor<ptr<f16, gmem, align<8>> o ((1,4)):((0,1))>
In [8]:
benchmark(partial(compiled_func, a_, b_, c_), num_warmups=5, num_iterations=100)Average execution time: 0.0202 ms Throughput: 1244.98 GB/s
In [9]:
@cute.kernel
def elementwise_add_kernel(
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
tv_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
#--------------------------------
# slice for thread-block level view
#--------------------------------
blk_coord = ((None, None), bidx)
# logical coord -> address
blkA = gA[blk_coord] # (TileM, TileN) -> physical address
blkB = gB[blk_coord] # (TileM, TileN) -> physical address
blkC = gC[blk_coord] # (TileM, TileN) -> physical address
#--------------------------------
# compose for thread-index & value-index to physical mapping
#--------------------------------
# blockA: (TileM, TileN) -> physical address
# tv_layout: (tid, vid) -> (TileM, TileN)
# tidfrgA = blkA o tv_layout
# tidfrgA: (tid, vid) -> physical address
tidfrgA = cute.composition(blkA, tv_layout)
tidfrgB = cute.composition(blkB, tv_layout)
tidfrgC = cute.composition(blkC, tv_layout)
print(f"Composed with TV layout:")
print(f" tidfrgA: {tidfrgA.type}")
#--------------------------------
# slice for thread-level view
#--------------------------------
# `None` represent slice of the entire per-thread data
thr_coord = (tidx, None)
# slice for threads: vid -> address
thrA = tidfrgA[thr_coord] # (V) -> physical address
thrB = tidfrgB[thr_coord] # (V) -> physical address
thrC = tidfrgC[thr_coord] # (V) -> physical address
thrC[None] = thrA.load() + thrB.load()In [10]:
@cute.jit
def elementwise_add(
mA: cute.Tensor,
mB: cute.Tensor,
mC: cute.Tensor,
):
# mA layout: (M, N):(N, 1)
# TV layout map thread & value index to (16, 256) logical tile
# - contiguous thread index maps to mode-1 because input layout is contiguous on
# mode-1 for coalesced load-store
# - each thread load 8 contiguous element each row and load 4 rows
thr_layout = cute.make_layout((4, 32), stride=(32, 1))
val_layout = cute.make_layout((4, 8), stride=(8, 1))
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
print(f"Tiler: {tiler_mn}")
print(f"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(f"Tiled Input Tensors:")
print(f" gA: {gA.type}")
print(f" gB: {gB.type}")
print(f" gC: {gC.type}")
# Launch the kernel asynchronously
# Async token(s) can also be specified as dependencies
elementwise_add_kernel(
gA, gB, gC, tv_layout
).launch(
grid=[cute.size(gC, mode=[1]), 1, 1],
block=[cute.size(tv_layout, mode=[0]), 1, 1],
)
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
c = torch.zeros(M, N, device="cuda", dtype=torch.float16)
a_ = from_dlpack(a, assumed_align=16)
b_ = from_dlpack(b, assumed_align=16)
c_ = from_dlpack(c, assumed_align=16)
elementwise_add_ = cute.compile(elementwise_add, a_, b_, c_)
elementwise_add_(a_, b_, c_)
# verify correctness
torch.testing.assert_close(c, a + b)Tiler: (16, 256) TV Layout: ((32,4),(8,4)):((128,4),(16,1)) Tiled Input Tensors: gA: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gB: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gC: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> Composed with TV layout: tidfrgA: !cute.memref<f16, gmem, align<16>, "((32,4),(8,4)):((8,8192),(1,2048))">
In [11]:
benchmark(partial(elementwise_add_, a_, b_, c_), num_warmups=5, num_iterations=200)Average execution time: 0.0222 ms Throughput: 1133.58 GB/s
In [12]:
@cute.kernel
def elementwise_apply_kernel(
op: cutlass.Constexpr, # lambda function must be const expr to generate code at compile time
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
tv_layout: cute.Layout
):
tidx, _, _ = cute.arch.thread_idx()
bidx, _, _ = cute.arch.block_idx()
blk_coord = ((None, None), bidx)
# logical coord -> address
blkA = gA[blk_coord] # (TileM, TileN) -> physical address
blkB = gB[blk_coord] # (TileM, TileN) -> physical address
blkC = gC[blk_coord] # (TileM, TileN) -> physical address
tidfrgA = cute.composition(blkA, tv_layout)
tidfrgB = cute.composition(blkB, tv_layout)
tidfrgC = cute.composition(blkC, tv_layout)
print(f"Composed with TV layout:")
print(f" tidfrgA: {tidfrgA.type}")
thr_coord = (tidx, None)
# slice for threads: vid -> address
thrA = tidfrgA[thr_coord] # (V) -> physical address
thrB = tidfrgB[thr_coord] # (V) -> physical address
thrC = tidfrgC[thr_coord] # (V) -> physical address
#--------------------------------
# apply custom operation
#--------------------------------
thrC[None] = op(thrA.load(), thrB.load())
@cute.jit
def elementwise_op(
op: cutlass.Constexpr,
mA: cute.Tensor,
mB: cute.Tensor,
mC: cute.Tensor,
):
# mA layout: (M, N):(N, 1)
# TV layout map thread & value index to (16, 256) logical tile
# - contiguous thread index maps to mode-1 because input layout is contiguous on
# mode-1 for coalesced load-store
# - each thread load 8 contiguous element each row and load 4 rows
thr_layout = cute.make_layout((4, 32), stride=(32, 1))
val_layout = cute.make_layout((4, 8), stride=(8, 1))
tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout)
print(f"Tiler: {tiler_mn}")
print(f"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(f"Tiled Input Tensors:")
print(f" gA: {gA.type}")
print(f" gB: {gB.type}")
print(f" gC: {gC.type}")
# Launch the kernel asynchronously
# Async token(s) can also be specified as dependencies
elementwise_apply_kernel(
op, gA, gB, gC, tv_layout
).launch(
grid=[cute.size(gC, mode=[1]), 1, 1],
block=[cute.size(tv_layout, mode=[0]), 1, 1],
)
a = torch.randn(M, N, device="cuda", dtype=torch.float16)
b = torch.randn(M, N, device="cuda", dtype=torch.float16)
c = torch.zeros(M, N, device="cuda", dtype=torch.float16)
a_ = from_dlpack(a, assumed_align=16)
b_ = from_dlpack(b, assumed_align=16)
c_ = from_dlpack(c, assumed_align=16)
from operator import mul
elementwise_op(mul, a_, b_, c_)
# verify correctness
torch.testing.assert_close(c, mul(a, b))Tiler: (16, 256) TV Layout: ((32,4),(8,4)):((128,4),(16,1)) Tiled Input Tensors: gA: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gB: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gC: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> Composed with TV layout: tidfrgA: !cute.memref<f16, gmem, align<16>, "((32,4),(8,4)):((8,8192),(1,2048))">
In [13]:
def mul_relu(a, b):
tmp = a * b
return cute.where(tmp > 0, tmp, cute.full_like(tmp, 0))
# As we uses cute.where in customized operation, we need to create another relu function
def mul_relu_ref(a, b):
tmp = a * b
return torch.relu(tmp)
elementwise_op(mul_relu, a_, b_, c_)
# verify correctness
torch.testing.assert_close(c, mul_relu_ref(a, b))Tiler: (16, 256) TV Layout: ((32,4),(8,4)):((128,4),(16,1)) Tiled Input Tensors: gA: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gB: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> gC: !cute.memref<f16, gmem, align<16>, "((16,256),(128,8)):((2048,1),(32768,256))"> Composed with TV layout: tidfrgA: !cute.memref<f16, gmem, align<16>, "((32,4),(8,4)):((8,8192),(1,2048))">