mirror of
https://github.com/NVIDIA/cuda-samples.git
synced 2026-09-12 01:06:53 +08:00
Release 13.4 of the CUDA Samples supported by CUDA Toolkit 13.4. See Changelog for more information.
420 lines
14 KiB
Python
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())
|