Skip to content

Commit 94705a4

Browse files
authored
Merge pull request #37 from NVIDIA/cutile-example
Add a example to show use for a cuTile kernel
2 parents bbde17b + d324e4e commit 94705a4

2 files changed

Lines changed: 146 additions & 1 deletion

File tree

examples/12_cutile.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Example 12: cuTile
6+
==================
7+
8+
This example shows how to use Nsight Python with cuTile kernels and compare
9+
kernel times with PyTorch for different problem sizes.
10+
11+
New concepts:
12+
- Profiling cuTile kernels for different problem sizes
13+
"""
14+
15+
import math
16+
17+
import cuda.tile as ct
18+
import torch
19+
20+
import nsight
21+
22+
23+
@ct.kernel # type: ignore[untyped-decorator]
24+
def vec_add_kernel_1d(
25+
a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, TILE: ct.Constant[int]
26+
) -> None:
27+
"""
28+
cuTile kernel for 1D element-wise vector addition using direct tiled loads/stores.
29+
30+
Each block processes a `TILE`-sized chunk of the vectors.
31+
This approach is efficient when the total dimension is a multiple of `TILE`,
32+
or when out-of-bounds accesses are implicitly handled by the calling context
33+
(e.g., by padding or ensuring input sizes match grid dimensions).
34+
35+
Args:
36+
a: Input tensor A.
37+
b: Input tensor B.
38+
c: Output tensor for the sum (A + B).
39+
TILE (ct.Constant[int]): The size of the tile (chunk of data) processed by each
40+
block. This must be a compile-time constant.
41+
"""
42+
# Get the global ID of the current block along the first dimension.
43+
# In a 1D grid, this directly corresponds to the index of the tile.
44+
bid = ct.bid(0)
45+
46+
# Load TILE-sized chunks from input vectors 'a' and 'b'.
47+
# `ct.load` automatically distributes the load operation across the threads
48+
# within the block, bringing the specified tile of data into shared memory
49+
# or registers. The `index=(bid,)` specifies which tile to load based on the block ID.
50+
a_tile = ct.load(a, index=(bid,), shape=(TILE,))
51+
b_tile = ct.load(b, index=(bid,), shape=(TILE,))
52+
53+
# Perform the element-wise addition on the loaded tiles.
54+
# This operation happens in parallel across the threads within the block.
55+
sum_tile = a_tile + b_tile
56+
57+
# Store the resulting TILE-sized chunk back to the output vector 'c'.
58+
# `ct.store` writes the computed tile back to global memory, again
59+
# distributing the store operation across threads.
60+
ct.store(c, index=(bid,), tile=sum_tile)
61+
62+
63+
def vec_add_1d(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
64+
"""Helper function to launch the kernel."""
65+
output = torch.empty_like(x)
66+
67+
N = x.shape[0] # Get the total size of the 1D vector
68+
69+
# Heuristic for TILE size:
70+
# Choose a power of 2, up to 1024, that is greater than or equal to N.
71+
# This helps in efficient memory access patterns on the GPU.
72+
# Handle N=0 gracefully to avoid log2(0) errors.
73+
TILE = min(1024, 2 ** math.ceil(math.log2(N))) if N > 0 else 1
74+
75+
# Calculate the grid dimensions for launching the kernel.
76+
# `math.ceil(N / TILE)` determines the number of blocks needed to cover
77+
# the entire vector. Each block processes a `TILE`-sized chunk.
78+
grid = (math.ceil(N / TILE), 1, 1) # (blocks_x, blocks_y, blocks_z)
79+
80+
ct.launch(
81+
torch.cuda.current_stream(), grid, vec_add_kernel_1d, (x, y, output, TILE)
82+
)
83+
84+
return output
85+
86+
87+
# Define sizes to test
88+
sizes = [2**i for i in range(21, 26)]
89+
90+
91+
@nsight.analyze.plot(
92+
filename="12_cutile.png",
93+
title="Vector Addition: cuTile/PyTorch",
94+
ylabel="kernel duration (us)",
95+
)
96+
@nsight.analyze.kernel(
97+
configs=sizes,
98+
runs=10,
99+
)
100+
def benchmark_cutile(n: int) -> None:
101+
"""
102+
Compare cuTile and PyTorch kernel times.
103+
104+
The plot will show:
105+
- Y-axis: kernel time
106+
- X-axis: Problem size (n)
107+
"""
108+
x = torch.randn(n, device="cuda")
109+
y = torch.randn(n, device="cuda")
110+
111+
# PyTorch
112+
with nsight.annotate("torch"):
113+
_ = x + y
114+
115+
# cuTile kernel
116+
with nsight.annotate("cuTile"):
117+
_ = vec_add_1d(x, y)
118+
119+
120+
def main() -> None:
121+
benchmark_cutile()
122+
print("✓ cuTile benchmark complete! Check '12_cutile.png'")
123+
print("\nWhat this example demonstrates:")
124+
print("\nPlotting cuTile and PyTorch kernel times for different problem sizes!")
125+
126+
127+
if __name__ == "__main__":
128+
main()

examples/test_examples.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-26 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

44
import importlib
5+
from typing import Any
56

67
import pytest
8+
import torch
9+
10+
11+
def get_cuda_dev_cc_major(device_id: int) -> Any:
12+
props = torch.cuda.get_device_properties(device_id)
13+
return props.major
714

815

916
def test_00_minimal() -> None:
@@ -65,3 +72,13 @@ def test_10_combine_kernel_metrics() -> None:
6572
def test_11_output_csv() -> None:
6673
output_csv = importlib.import_module("examples.11_output_csv")
6774
output_csv.main()
75+
76+
77+
# skip cuTile test on CC 9.x as Cuda Toolkit 13.2 used for testing does not support cuda-tile on CC 9.x (Hopper)
78+
@pytest.mark.skipif(
79+
get_cuda_dev_cc_major(0) == 9,
80+
reason="cuda-tile not supported on CC 9.x in CUDA Toolkit 13.2",
81+
) # type: ignore[untyped-decorator]
82+
def test_12_cutile() -> None:
83+
cutile = importlib.import_module("examples.12_cutile")
84+
cutile.main()

0 commit comments

Comments
 (0)