Dheemanth 5443602d89
CUDA 13.4 samples update - v13.4-public
Release 13.4 of the CUDA Samples supported by CUDA Toolkit 13.4.
See Changelog for more information.
2026-09-09 17:07:08 -05:00

420 lines
14 KiB
Python

# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * 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.
# * Neither the name of NVIDIA CORPORATION 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 ``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 OWNER 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.
"""
Persistent Program Cache with cuda.core
This sample demonstrates how to persist and reuse compiled CUDA artifacts
with cuda.core. A generated matmul + epilogue kernel is compiled to CUBIN
on a cache miss, stored in a disk-backed FileStreamProgramCache, and loaded
from that cache on later runs.
"""
from __future__ import annotations
import argparse
import sys
import time
from dataclasses import dataclass
from pathlib import Path
try:
import cupy as cp
import numpy as np
from cuda.core import (
Device,
EventOptions,
LaunchConfig,
ObjectCode,
Program,
ProgramOptions,
launch,
)
from cuda.core.utils import FileStreamProgramCache, make_program_cache_key
except ImportError as e:
print(f"Error: Required package not found: {e}")
print("Please install from requirements.txt:")
print(" pip install -r requirements.txt")
sys.exit(1)
KERNEL_NAME = "matmul_epilogue"
DEFAULT_CACHE_DIR = Path.home() / ".cache" / "cuda-samples" / "persistentProgramCache"
# Tiled matrix multiply followed by bias and an optional ReLU.
KERNEL_TEMPLATE = r"""
#define TILE_SIZE {tile_size}
extern "C" __global__
void {kernel_name}(const float* __restrict__ A,
const float* __restrict__ B,
const float* __restrict__ bias,
float* __restrict__ C,
int M, int N, int K)
{{
__shared__ float As[TILE_SIZE][TILE_SIZE];
__shared__ float Bs[TILE_SIZE][TILE_SIZE];
int tx = threadIdx.x;
int ty = threadIdx.y;
int row = blockIdx.y * TILE_SIZE + ty;
int col = blockIdx.x * TILE_SIZE + tx;
float acc = 0.0f;
int num_tiles = (K + TILE_SIZE - 1) / TILE_SIZE;
for (int tile = 0; tile < num_tiles; ++tile) {{
int a_col = tile * TILE_SIZE + tx;
int b_row = tile * TILE_SIZE + ty;
As[ty][tx] = (row < M && a_col < K) ? A[row * K + a_col] : 0.0f;
Bs[ty][tx] = (b_row < K && col < N) ? B[b_row * N + col] : 0.0f;
__syncthreads();
#pragma unroll
for (int i = 0; i < TILE_SIZE; ++i) {{
acc += As[ty][i] * Bs[i][tx];
}}
__syncthreads();
}}
if (row < M && col < N) {{
float value = acc + bias[col];
C[row * N + col] = {epilogue_expr};
}}
}}
"""
EPILOGUE_EXPRESSIONS = {
"identity": "value",
"relu": "value > 0.0f ? value : 0.0f",
}
# Return value from compile_or_load_kernel: the launchable kernel plus cache
# status, artifact size, and timings for comparing miss and hit paths.
@dataclass
class CacheResult:
kernel: object
status: str
key_hex: str
artifact_bytes: int
cache_lookup_ms: float
compile_ms: float | None
cache_store_ms: float | None
module_load_ms: float
@property
def host_preparation_ms(self) -> float:
"""Total cache lookup, compile/store, and module-load time."""
total = self.cache_lookup_ms + self.module_load_ms
if self.compile_ms is not None:
total += self.compile_ms
if self.cache_store_ms is not None:
total += self.cache_store_ms
return total
def elapsed_ms(start: float) -> float:
return (time.perf_counter() - start) * 1000.0
def build_kernel_source(tile_size: int, epilogue: str) -> str:
"""Generate a specialized matmul + epilogue CUDA kernel."""
return KERNEL_TEMPLATE.format(
tile_size=tile_size,
kernel_name=KERNEL_NAME,
epilogue_expr=EPILOGUE_EXPRESSIONS[epilogue],
)
def cache_clear(cache_dir: Path) -> None:
with FileStreamProgramCache(cache_dir) as cache:
cache.clear()
def compile_or_load_kernel(
source: str,
device: Device,
cache_dir: Path,
) -> CacheResult:
"""Compile the generated kernel or load its CUBIN bytes from cache."""
options = ProgramOptions(std="c++17", arch=f"sm_{device.arch}")
program = Program(source, code_type="c++", options=options)
# For typical use, pass cache=cache to Program.compile(...). This sample
# uses explicit cache operations so the miss and hit paths are visible.
key = make_program_cache_key(
code=source,
code_type="c++",
options=options,
target_type="cubin",
)
key_hex = key.hex()
# This is the core persistent-cache flow: derive the same key for the same
# source/options/target, load cached CUBIN bytes on a hit, or compile and
# store the artifact on a miss.
with FileStreamProgramCache(cache_dir) as cache:
# These timings are included to compare cache misses and hits.
start = time.perf_counter()
cached = cache.get(key)
cache_lookup_ms = elapsed_ms(start)
if cached is not None:
start = time.perf_counter()
module = ObjectCode.from_cubin(cached, name="persistentProgramCache")
kernel = module.get_kernel(KERNEL_NAME)
module_load_ms = elapsed_ms(start)
return CacheResult(
kernel=kernel,
status="HIT",
key_hex=key_hex,
artifact_bytes=len(cached),
cache_lookup_ms=cache_lookup_ms,
compile_ms=None,
cache_store_ms=None,
module_load_ms=module_load_ms,
)
start = time.perf_counter()
module = program.compile("cubin")
compile_ms = elapsed_ms(start)
artifact_bytes = len(bytes(module.code))
start = time.perf_counter()
cache[key] = module
cache_store_ms = elapsed_ms(start)
start = time.perf_counter()
kernel = module.get_kernel(KERNEL_NAME)
module_load_ms = elapsed_ms(start)
return CacheResult(
kernel=kernel,
status="MISS",
key_hex=key_hex,
artifact_bytes=artifact_bytes,
cache_lookup_ms=cache_lookup_ms,
compile_ms=compile_ms,
cache_store_ms=cache_store_ms,
module_load_ms=module_load_ms,
)
def make_host_inputs(m: int, n: int, k: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed=123)
a = rng.standard_normal((m, k)).astype(np.float32)
b = rng.standard_normal((k, n)).astype(np.float32)
bias = rng.standard_normal(n).astype(np.float32)
return a, b, bias
def host_reference(
a: np.ndarray,
b: np.ndarray,
bias: np.ndarray,
epilogue: str,
) -> np.ndarray:
out = a @ b
out += bias.reshape(1, -1)
if epilogue == "relu":
out = np.maximum(out, 0.0)
return out.astype(np.float32, copy=False)
def print_cache_report(result: CacheResult) -> None:
print(f"Cache status: {result.status}")
print(f"Cache key: {result.key_hex[:24]}...")
print(f"Artifact size: {result.artifact_bytes / 1024:.1f} KiB")
print(f"Cache lookup: {result.cache_lookup_ms:.3f} ms")
if result.compile_ms is None:
print("Compile time: skipped")
else:
print(f"Compile time: {result.compile_ms:.3f} ms")
if result.cache_store_ms is not None:
print(f"Cache store: {result.cache_store_ms:.3f} ms")
print(f"Module load: {result.module_load_ms:.3f} ms")
print(f"Host prep time: {result.host_preparation_ms:.3f} ms")
def run_with_cache(
device_id: int,
m: int,
n: int,
k: int,
tile_size: int,
epilogue: str,
cache_dir: Path,
clear_cache: bool,
warmup: int,
iterations: int,
) -> bool:
if clear_cache:
cache_clear(cache_dir)
device = Device(device_id)
device.set_current()
cp.cuda.Device(device_id).use()
stream = device.create_stream()
cp.cuda.Stream.from_external(stream).use()
try:
print("\nPersistent Program Cache")
cc = device.compute_capability
print(f"Device: {device.name}")
print(f"Compute Capability: {cc.major}.{cc.minor}")
print(f"Cache directory: {cache_dir}")
print(f"Workload: C = {epilogue}(A @ B + bias)")
print(f"Matrix sizes: M={m}, N={n}, K={k}")
print(f"Tile size: {tile_size}")
print(f"Timed launches: warmup={warmup}, iterations={iterations}")
print()
source = build_kernel_source(tile_size=tile_size, epilogue=epilogue)
cache_result = compile_or_load_kernel(
source=source,
device=device,
cache_dir=cache_dir,
)
print_cache_report(cache_result)
host_a, host_b, host_bias = make_host_inputs(m, n, k)
expected = host_reference(host_a, host_b, host_bias, epilogue)
d_a = cp.asarray(host_a)
d_b = cp.asarray(host_b)
d_bias = cp.asarray(host_bias)
d_c = cp.empty((m, n), dtype=cp.float32)
stream.sync()
grid = ((n + tile_size - 1) // tile_size, (m + tile_size - 1) // tile_size)
block = (tile_size, tile_size)
config = LaunchConfig(grid=grid, block=block)
kernel_args = (
d_a.data.ptr,
d_b.data.ptr,
d_bias.data.ptr,
d_c.data.ptr,
np.int32(m),
np.int32(n),
np.int32(k),
)
for _ in range(warmup):
launch(stream, config, cache_result.kernel, *kernel_args)
stream.sync()
event_options = EventOptions(timing_enabled=True)
start_event = device.create_event(options=event_options)
end_event = device.create_event(options=event_options)
stream.record(start_event)
for _ in range(iterations):
launch(stream, config, cache_result.kernel, *kernel_args)
stream.record(end_event)
end_event.sync()
kernel_time_ms = (end_event - start_event) / iterations
stream.sync()
actual = cp.asnumpy(d_c)
max_error = float(np.max(np.abs(actual - expected)))
ok = bool(np.allclose(actual, expected, rtol=1e-3, atol=1e-3))
print(f"Kernel time: {kernel_time_ms:.3f} ms")
print(f"Max error: {max_error:.6f}")
print(f"Validation: {'PASSED' if ok else 'FAILED'}")
return ok
finally:
cp.cuda.Stream.null.use()
stream.close()
def positive_int(value: str) -> int:
parsed = int(value)
if parsed <= 0:
raise argparse.ArgumentTypeError("value must be positive")
return parsed
def cache_dir_arg(value: str) -> Path:
return Path(value).expanduser().resolve()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Persistent cuda.core program cache for generated CUDA kernels"
)
parser.add_argument("--device", type=int, default=0, help="CUDA device id")
parser.add_argument("--m", type=positive_int, default=512, help="Rows of A and C")
parser.add_argument("--n", type=positive_int, default=512, help="Columns of B and C")
parser.add_argument("--k", type=positive_int, default=512, help="Columns of A / rows of B")
parser.add_argument(
"--tile-size",
type=int,
choices=(16, 32),
default=16,
help="Compile-time tile size specialization",
)
parser.add_argument(
"--epilogue",
choices=tuple(EPILOGUE_EXPRESSIONS),
default="relu",
help="Compile-time epilogue specialization",
)
parser.add_argument(
"--cache-dir",
type=cache_dir_arg,
default=DEFAULT_CACHE_DIR,
help=f"Program cache directory (default: {DEFAULT_CACHE_DIR})",
)
parser.add_argument("--clear-cache", action="store_true", help="Clear the cache before running")
parser.add_argument("--warmup", type=positive_int, default=5, help="Warmup launches")
parser.add_argument("--iterations", type=positive_int, default=20, help="Timed launches")
return parser.parse_args()
def main() -> int:
args = parse_args()
ok = run_with_cache(
device_id=args.device,
m=args.m,
n=args.n,
k=args.k,
tile_size=args.tile_size,
epilogue=args.epilogue,
cache_dir=args.cache_dir,
clear_cache=args.clear_cache,
warmup=args.warmup,
iterations=args.iterations,
)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())