|
| 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() |
0 commit comments