add-cuda-kernel
Step-by-step tutorial for adding new CUDA kernels to Oasr
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Step-by-step tutorial for adding new CUDA kernels to Oasr
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
| name | add-cuda-kernel |
| description | Step-by-step tutorial for adding new CUDA kernels to Oasr |
This tutorial walks through adding a simple element-wise scale operation to Oasr. We'll implement scale(x, factor) = x * factor to demonstrate the complete workflow, with references to real kernels (norm, activation, conv, gemm) throughout.
Add a new operation that scales each element of a tensor by a scalar factor:
x and scalar factorx * factor (element-wise)include/Create include/oasr/scale.cuh:
#pragma once
#include <cuda_runtime.h>
#include <cuda_fp16.h>
#include <cuda_bf16.h>
namespace oasr {
/*!
* \brief Element-wise scale kernel
* \tparam T Data type (half, __nv_bfloat16, float)
* \param input Input tensor
* \param output Output tensor
* \param factor Scale factor
* \param n Number of elements
*/
template <typename T>
__global__ void ScaleKernel(const T* input, T* output, T factor, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
output[idx] = input[idx] * factor;
}
}
/*!
* \brief Launch scale kernel
* \tparam T Data type
* \param input Input pointer
* \param output Output pointer
* \param factor Scale factor
* \param n Number of elements
* \param stream CUDA stream
*/
template <typename T>
cudaError_t ScaleLauncher(const T* input, T* output, T factor, int n,
cudaStream_t stream = nullptr) {
const int threads = 256;
const int blocks = (n + threads - 1) / threads;
ScaleKernel<T><<<blocks, threads, 0, stream>>>(input, output, factor, n);
return cudaGetLastError();
}
} // namespace oasr
Key points:
oasr:: pattern (or oasr::<family>:: for larger families, e.g. oasr::norm::, oasr::activation::)Real examples:
include/oasr/activation.cuh -- oasr::activation::GLU<T>(), oasr::activation::Swish<T>()include/oasr/norm.cuh -- oasr::norm::LayerNorm<T>(), oasr::norm::RMSNorm<T>()include/oasr/conv/conv1d.cuh -- Depthwise/pointwise conv1d kernelsinclude/oasr/gemm/gemm.cuh -- CUTLASS GEMM kernelscsrc/Create csrc/scale.cu:
#include <oasr/scale.cuh>
#include "tvm_ffi_utils.h"
using namespace oasr;
void scale_run(TensorView output, TensorView input, double factor) {
CHECK_INPUT(input);
CHECK_INPUT(output);
int n = 1;
for (int i = 0; i < input.ndim(); ++i) {
n *= input.size(i);
}
cudaStream_t stream = get_stream(input.device());
DISPATCH_DLPACK_DTYPE_TO_CTYPE_FP16(input.dtype(), c_type, [&] {
cudaError_t status = ScaleLauncher<c_type>(
static_cast<const c_type*>(input.data_ptr()),
static_cast<c_type*>(output.data_ptr()),
static_cast<c_type>(factor),
n,
stream
);
TVM_FFI_ICHECK(status == cudaSuccess)
<< "Failed to run ScaleLauncher: " << cudaGetErrorString(status);
return true;
});
}
Key points:
include/oasr/ and "tvm_ffi_utils.h" (TVM-FFI utils only in csrc/)TensorView (alias for tvm::ffi::TensorView) as tensor typeOptional (alias for tvm::ffi::Optional<TensorView>) for optional tensorsCHECK_INPUT(x) macro to verify tensor is on CUDAget_stream(device)DISPATCH_DLPACK_DTYPE_TO_CTYPE_FP16 (handles FP32/FP16/BF16)static_cast<T*>(x.data_ptr())TVM_FFI_ICHECK<< operatorAvailable validation macros (from csrc/tvm_ffi_utils.h):
| Macro | Purpose |
|---|---|
CHECK_INPUT(x) | Verify tensor is on CUDA |
CHECK_DIM(expected, x) | Verify dimensionality |
CHECK_DEVICE(x, y) | Same-device check |
CHECK_LAST_DIM_CONTIGUOUS_INPUT(x) | Contiguity check |
Available dispatch macros:
| Macro | Dtypes |
|---|---|
DISPATCH_DLPACK_DTYPE_TO_CTYPE_FP16(dtype, c_type, ...) | FP32, FP16, BF16 |
DISPATCH_DLPACK_DTYPE_TO_CTYPE_FP32_FP16(dtype, c_type, ...) | Alias for the above |
DISPATCH_DLPACK_HALF_DTYPE(dtype, c_type, ...) | FP16, BF16 only (for GEMM/conv) |
TVM-FFI Error Handling:
TVM_FFI_ICHECK(condition) << "message" -- Assert with error message (used inside dispatch macros or when you need a simple assertion)TVM_FFI_THROW(ValueError) << "message" -- Throw ValueError with custom message (standard runtime error handling)TVM_FFI_THROW(TypeError) << "message" -- Throw TypeError<< to chain multiple values in the error messageWhen to use TVM_FFI_THROW vs TVM_FFI_LOG_AND_THROW:
TVM_FFI_THROW: Use for normal runtime error handling. This is the standard way to report errors that will be caught and propagated to Python.
void scale_run(TensorView output, TensorView input, double factor) {
if (!input.device().device_type == kDLCUDA) {
TVM_FFI_THROW(ValueError) << "Input must be a CUDA tensor";
}
}
TVM_FFI_LOG_AND_THROW: Use only in cases where:
This variant logs the error message before throwing, ensuring visibility even if the exception doesn't propagate correctly.
void check_weights_shape(std::string which_weights) const {
if (which_weights != "gemm1" && which_weights != "gemm2") {
// Internal error that should never happen - use LOG_AND_THROW
TVM_FFI_LOG_AND_THROW(InternalError)
<< "Internal error: which_weights = " << which_weights;
}
}
Real example (from csrc/activation.cu):
void glu(TensorView output, TensorView input) {
CHECK_INPUT(input);
CHECK_INPUT(output);
CHECK_LAST_DIM_CONTIGUOUS_INPUT(input);
CHECK_LAST_DIM_CONTIGUOUS_INPUT(output);
unsigned int batch_size = input.size(0);
unsigned int seq_len = input.size(1);
unsigned int channels = input.size(2) / 2;
cudaStream_t stream = get_stream(input.device());
DISPATCH_DLPACK_DTYPE_TO_CTYPE_FP16(input.dtype(), c_type, [&] {
cudaError_t status = activation::GLU<c_type>(
static_cast<const c_type*>(input.data_ptr()),
static_cast<c_type*>(output.data_ptr()),
batch_size, seq_len, channels, stream);
TVM_FFI_ICHECK(status == cudaSuccess)
<< "GLU kernel failed: " << cudaGetErrorString(status);
return true;
});
}
csrc/Create csrc/scale_jit_binding.cu:
#include "tvm_ffi_utils.h"
// Forward declaration
void scale_run(TensorView output, TensorView input, double factor);
// Export to TVM-FFI
TVM_FFI_DLL_EXPORT_TYPED_FUNC(run, scale_run);
Key points:
"tvm_ffi_utils.h" for TVM-FFI macros and type aliasesTVM_FFI_DLL_EXPORT_TYPED_FUNC(exported_name, function) -- the exported name is how Python accesses itReal example (from csrc/activation_jit_binding.cu):
#include "tvm_ffi_utils.h"
// Forward declarations of launcher functions
void glu(TensorView output, TensorView input);
void swish(TensorView output, TensorView input);
// TVM-FFI symbol exports
TVM_FFI_DLL_EXPORT_TYPED_FUNC(glu, glu);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(swish, swish);
Real example (from csrc/norm_jit_binding.cu):
#include "tvm_ffi_utils.h"
void layernorm(TensorView output, TensorView input, TensorView weight,
Optional bias_opt, double eps);
void rmsnorm(TensorView output, TensorView input, TensorView weight,
Optional bias_opt, double eps);
// ... more forward declarations ...
TVM_FFI_DLL_EXPORT_TYPED_FUNC(layernorm, layernorm);
TVM_FFI_DLL_EXPORT_TYPED_FUNC(rmsnorm, rmsnorm);
// ... more exports ...
Note: Multiple launcher functions can be exported from a single binding file, as the norm and activation families do.
oasr/jit/Create oasr/jit/scale.py:
from .core import gen_jit_spec, JitSpec
from . import env
def gen_scale_module() -> JitSpec:
"""Generate JIT spec for scale kernel."""
return gen_jit_spec(
"scale",
[
env.OASR_CSRC_DIR / "scale.cu",
env.OASR_CSRC_DIR / "scale_jit_binding.cu",
],
)
Key points:
gen_jit_spec and JitSpec from .core, paths from .envgen_jit_spec() auto-detects GPU architecture and sets default NVCC flags.cu + binding .cu from csrc/~/.cache/oasr/jit/Real examples:
# oasr/jit/activation.py
def gen_activation_module() -> JitSpec:
return gen_jit_spec(
"activation",
[env.OASR_CSRC_DIR / "activation.cu",
env.OASR_CSRC_DIR / "activation_jit_binding.cu"],
)
# oasr/jit/norm.py
def gen_norm_module() -> JitSpec:
return gen_jit_spec(
"norm",
[env.OASR_CSRC_DIR / "norm.cu",
env.OASR_CSRC_DIR / "norm_jit_binding.cu"],
)
Oasr uses CompilationContext to manage CUDA architecture targets. This is critical because some kernels only work on specific GPU architectures (e.g., Hopper SM90, Blackwell SM100).
Automatic Detection (default):
from oasr.compilation_context import CompilationContext
ctx = CompilationContext()
# Automatically detects all GPUs in the system
# For SM90+, adds 'a' suffix (e.g., 9.0a for Hopper)
# Result: ctx.TARGET_CUDA_ARCHS = {(9, '0a'), (10, '0a'), ...}
Manual Override (via environment variable):
export OASR_CUDA_ARCH_LIST="8.0 9.0a 10.0a"
# Now only these architectures will be compiled
When creating a JIT module, specify which major SM versions are supported:
from oasr.jit.core import gen_jit_spec
from oasr.jit import current_compilation_context
def gen_my_hopper_only_module():
"""Example: Kernel works on SM90 and later supported architectures."""
nvcc_flags = current_compilation_context.get_nvcc_flags_list(
# Explicitly list supported SM versions -- no automatic future compatibility
supported_major_versions=[9, 10, 11, 12] # SM90, SM100, SM110, SM120
)
return gen_jit_spec(
name="my_hopper_kernel",
sources=sources,
extra_cuda_cflags=nvcc_flags,
)
def gen_my_blackwell_only_module():
"""Example: Kernel only works on SM100 (Blackwell)."""
nvcc_flags = current_compilation_context.get_nvcc_flags_list(
supported_major_versions=[10] # SM100 only
)
return gen_jit_spec(
name="my_blackwell_kernel",
sources=sources,
extra_cuda_cflags=nvcc_flags,
)
def gen_my_universal_module():
"""Example: Kernel works on all architectures (default)."""
# No need to call get_nvcc_flags_list -- gen_jit_spec auto-detects
return gen_jit_spec(
name="my_universal_kernel",
sources=sources,
)
What Happens:
RuntimeError: No supported CUDA architectures found for major versions [9, 10, 11, 12]| Supported Versions | Architectures | Use Case |
|---|---|---|
None | All available GPUs | Universal kernels (default) |
[9, 10, 11, 12] | SM90, SM100, SM110, SM120 | Hopper, Blackwell |
[10, 11, 12] | SM100, SM110, SM120 | Blackwell only |
[12] | SM120 | Specific architecture only |
[8, 9, 10, 11, 12] | SM80, SM90, SM100, SM110, SM120 | Ampere, Hopper, Blackwell |
oasr/Create oasr/scale.py:
import functools
from typing import Optional
import torch
from .api_logging import oasr_api
@functools.cache
def _get_scale_module():
"""Get or compile scale module (cached)."""
from oasr.jit.scale import gen_scale_module
return gen_scale_module().build_and_load()
@oasr_api
def scale(input: torch.Tensor, factor: float,
out: Optional[torch.Tensor] = None) -> torch.Tensor:
"""Element-wise scale operation.
Parameters
----------
input : torch.Tensor
Input tensor (CUDA).
factor : float
Scale factor.
out : Optional[torch.Tensor]
Output tensor (if None, allocate new tensor).
Returns
-------
output : torch.Tensor
Scaled tensor (input * factor).
Examples
--------
>>> import torch
>>> import oasr
>>> x = torch.randn(1024, dtype=torch.float16, device="cuda")
>>> y = oasr.scale(x, 2.0)
>>> torch.allclose(y, x * 2.0)
True
"""
if out is None:
out = torch.empty_like(input)
# Call TVM-FFI function (output first in C++ convention)
_get_scale_module().run(out, input, float(factor))
return out
Key points:
@functools.cache to cache the compiled module (compile once per process)@oasr_api decorator (from oasr.api_logging) enables debug loggingout=None) but passed first to the C++ TVM-FFI functionReal example (from oasr/activation.py):
@functools.cache
def _get_activation_module():
from oasr.jit.activation import gen_activation_module
return gen_activation_module().build_and_load()
@oasr_api
def glu(input: torch.Tensor, out: Optional[torch.Tensor] = None) -> torch.Tensor:
"""Gated Linear Unit activation."""
if out is None:
out = torch.empty(
input.shape[:-1] + (input.shape[-1] // 2,),
device=input.device, dtype=input.dtype,
)
_get_activation_module().glu(out, input) # output first!
return out
@oasr_api
def swish(input: torch.Tensor, out: Optional[torch.Tensor] = None) -> torch.Tensor:
"""Swish (SiLU) activation: x * sigmoid(x)."""
if out is None:
out = torch.empty_like(input)
_get_activation_module().swish(out, input)
return out
@backend_requirement and @supported_compute_capability DecoratorsFor kernels with compute capability requirements or multiple backend choices, Oasr provides two decorators (in oasr.utils):
@supported_compute_capability DecoratorMarks a function with its supported CUDA compute capabilities:
from oasr.utils import supported_compute_capability
@supported_compute_capability([80, 86, 89, 90, 100, 103, 110, 120])
def _my_check_function(input, output):
"""Supports SM80 (Ampere) through SM120 (Blackwell)."""
# Validation logic here
return True
@backend_requirement DecoratorEnforces backend and problem size requirements at runtime. There are three usage patterns:
Pattern 1: Single Backend (No Backend Choices)
For kernels with only one implementation:
from oasr.utils import backend_requirement, supported_compute_capability
@supported_compute_capability([80, 86, 89, 90, 100, 103, 110, 120])
def _check_my_kernel(input, output):
"""Validate inputs. Must return True if valid."""
if input.shape[-1] > 256:
raise ValueError("Head dimension must be <= 256")
return True
@backend_requirement(
backend_checks={}, # Empty dict = no backend parameter
common_check=_check_my_kernel,
)
def my_kernel(input, output):
# Kernel implementation
pass
Pattern 2: Multiple Backends
For kernels with multiple implementation backends (e.g., CUTLASS, cuDNN):
@supported_compute_capability([80, 86, 89, 90])
def _cutlass_check(q, k, v, backend):
"""CUTLASS backend: Ampere through Hopper."""
if q.shape[-1] > 256:
raise ValueError("CUTLASS: head_dim must be <= 256")
return True
@supported_compute_capability([75, 80, 86, 89, 90, 100])
def _cudnn_check(q, k, v, backend):
"""cuDNN backend: Turing through Blackwell."""
return True
@backend_requirement(
backend_checks={
"cutlass": _cutlass_check,
"cudnn": _cudnn_check,
},
common_check=None, # Optional: shared validation for all backends
)
def attention(q, k, v, backend="cutlass"):
if backend == "cutlass":
# CUTLASS implementation
pass
elif backend == "cudnn":
# cuDNN implementation
pass
Pattern 3: Auto Backend Selection
For kernels that can automatically select the best backend:
def _heuristic_func(suitable_backends, q, k, v, backend):
"""Return backends in order of preference."""
if q.shape[-1] <= 128:
preferred = ["cutlass", "cudnn"]
else:
preferred = ["cudnn", "cutlass"]
return [b for b in preferred if b in suitable_backends]
@backend_requirement(
backend_checks={
"cutlass": _cutlass_check,
"cudnn": _cudnn_check,
},
common_check=_common_validation,
heuristic_func=_heuristic_func, # Required when backend="auto" is used
)
def attention(q, k, v, backend="auto"):
if backend == "auto":
backend = attention.suitable_auto_backends[0]
# ... rest of implementation
@backend_requirementThe decorator adds these methods to the wrapped function:
# Check if a backend is supported (optionally for a specific CC)
scale.is_backend_supported("cutlass") # True/False
scale.is_backend_supported("cutlass", cc=90) # True/False for Hopper
# Check if any backend supports this compute capability
scale.is_compute_capability_supported(90) # True/False
# Check if a backend exists
scale.has_backend("cutlass") # True/False
# Check if there are multiple backend choices
scale.has_backend_choices() # True/False
skip_check Keyword ArgumentThe decorator adds a skip_check keyword argument to bypass validation for performance-critical code paths:
# Normal call with validation
result = scale(x, 2.0)
# Skip validation for performance (use with caution!)
result = scale(x, 2.0, skip_check=True)
Check functions must:
True if validation passesValueError with descriptive message if validation fails@supported_compute_capability to specify supported architecturestests/Create tests following the flat tests/test_<kernel>.py layout. The conftest.py provides: device (CUDA, skips if unavailable), dtype/dtype_all fixtures, batch_seq_hidden common shapes, and get_rtol_atol(dtype) helper.
Create tests/test_scale.py:
import pytest
import torch
import oasr
class TestScale:
"""Tests for oasr.scale() functional API."""
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
@pytest.mark.parametrize("size", [128, 1024, 4096])
def test_scale_correctness(self, dtype, size):
"""Test scale operation correctness."""
x = torch.randn(size, dtype=dtype, device="cuda")
factor = 3.14
y = oasr.scale(x, factor)
expected = x * factor
if dtype == torch.float32:
rtol, atol = 1e-5, 1e-6
else:
rtol, atol = 1e-3, 1e-3
torch.testing.assert_close(y, expected, rtol=rtol, atol=atol)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_scale_destination_passing(self, dtype):
"""Test scale with pre-allocated output."""
x = torch.randn(1024, dtype=dtype, device="cuda")
out = torch.empty_like(x)
factor = 2.0
result = oasr.scale(x, factor, out=out)
# Should return the same tensor
assert result.data_ptr() == out.data_ptr()
expected = x * factor
torch.testing.assert_close(result, expected, rtol=1e-3, atol=1e-3)
def test_scale_cpu_error(self):
"""Test that CPU tensors raise an error."""
x = torch.randn(128, dtype=torch.float32)
with pytest.raises(Exception):
oasr.scale(x, 2.0)
Key points:
pytest.mark.parametrize for multiple configurationsget_rtol_atol() from conftest)result.data_ptr() == out.data_ptr()Real example (from tests/test_activation.py):
class TestGLU:
@pytest.mark.parametrize(
"batch_size,seq_len,channels",
[(2, 128, 256), (4, 256, 512)],
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
def test_glu(self, batch_size, seq_len, channels, dtype):
x = torch.randn(batch_size, seq_len, 2 * channels, device="cuda", dtype=dtype)
output = oasr.glu(x)
expected = F.glu(x, dim=-1).to(dtype)
torch.testing.assert_close(output, expected, rtol=1e-2, atol=1e-2)
def test_glu_destination_passing(self, dtype):
x = torch.randn(2, 128, 512, device="cuda", dtype=torch.float16)
out = torch.empty(2, 128, 256, device="cuda", dtype=torch.float16)
result = oasr.glu(x, out=out)
assert result.data_ptr() == out.data_ptr()
When your kernel has architecture requirements, add skip checks:
import pytest
import torch
from oasr.utils import is_sm90a_supported
def test_hopper_kernel():
if not is_sm90a_supported(torch.device("cuda")):
pytest.skip("SM90a is not supported on this GPU")
# Test code here
...
Register your kernel in oasr/aot.py so users with oasr-jit-cache can skip JIT compilation.
Edit oasr/aot.py:
def gen_all_modules() -> List:
from oasr.jit.activation import gen_activation_module
from oasr.jit.norm import gen_norm_module
from oasr.jit.conv import gen_conv_module, gen_conv2d_module, gen_cudnn_conv2d_module
from oasr.jit.gemm import gen_gemm_module, gen_bmm_module, gen_group_gemm_module
from oasr.jit.scale import gen_scale_module # NEW
return [
gen_activation_module(),
gen_norm_module(),
gen_conv_module(),
gen_conv2d_module(),
gen_cudnn_conv2d_module(),
gen_gemm_module(),
gen_bmm_module(),
gen_group_gemm_module(),
gen_scale_module(), # NEW
]
Edit oasr/__init__.py:
from .scale import scale as scale
Add "scale" to the __all__ list.
# The kernel compiles automatically on first use
pytest tests/test_scale.py -v
# Run a single test
pytest tests/test_scale.py::TestScale::test_scale_correctness -v
All new kernels should have benchmarks. This helps track performance regressions and allows users to compare against other implementations.
Benchmarks follow the routines + thin-wrapper pattern: a single routine module in benchmarks/routines/<family>.py exposes the kernel(s) to both the unified CLI (oasr_benchmark.py) and to per-kernel bench_*.py scripts that act as thin wrappers around run_standalone(). Reference: benchmarks/routines/activation.py + benchmarks/bench_glu.py.
Create benchmarks/routines/scale.py:
"""Scale family benchmark routines."""
from __future__ import annotations
import argparse
from typing import Any
import torch
import oasr
from benchmarks.routines.bench_utils import (
BenchResult,
OutputWriter,
bench_fn,
check_close,
compute_bandwidth_tb_s,
dtype_size,
parse_dtype,
run_main,
)
SUBROUTINES = ["scale"]
# ---------------------------------------------------------------------------
# Default configs
# ---------------------------------------------------------------------------
DEFAULT_CONFIGS: dict[str, list[dict[str, Any]]] = {
"scale": [
{"size": 1024},
{"size": 4096},
{"size": 16384},
{"size": 65536},
{"size": 262144},
],
}
PROFILE_CONFIGS: dict[str, tuple] = {
"scale": (65536,),
}
def get_default_configs() -> dict[str, list[dict[str, Any]]]:
return DEFAULT_CONFIGS
# ---------------------------------------------------------------------------
# Setup functions -- return (oasr_fn, pytorch_fn) closures
# ---------------------------------------------------------------------------
def setup_scale(size, dtype=torch.float16):
x = torch.randn(size, device="cuda", dtype=dtype)
factor = 2.0
def oasr_fn():
return oasr.scale(x, factor)
def pytorch_fn():
return x * factor
return oasr_fn, pytorch_fn
# ---------------------------------------------------------------------------
# CLI args (used by oasr_benchmark.py)
# ---------------------------------------------------------------------------
def parse_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--size", type=int, default=None, help="Number of elements")
# ---------------------------------------------------------------------------
# run_test -- entry point invoked by oasr_benchmark.py
# ---------------------------------------------------------------------------
def _scale_bytes(size, dtype):
"""Bytes accessed: read input + write output."""
return 2 * size * dtype_size(dtype)
def run_test(args: argparse.Namespace, output: OutputWriter) -> None:
subroutine = getattr(args, "subroutine", "scale")
dtype_str = getattr(args, "dtype", "float16")
dtype = parse_dtype(dtype_str)
do_check = getattr(args, "refcheck", False)
allow_mismatch = getattr(args, "allow_output_mismatch", False)
dry_run_iters = getattr(args, "dry_run_iters", 5)
num_iters = getattr(args, "num_iters", 30)
use_cuda_events = getattr(args, "use_cuda_events", False)
configs = _resolve_configs(args, subroutine)
for cfg in configs:
oasr_fn, pytorch_fn = setup_scale(cfg["size"], dtype)
fn_map = get_fn_map(subroutine, oasr_fn, pytorch_fn)
backends = getattr(args, "backends", None) or list(fn_map.keys())
bytes_accessed = _scale_bytes(cfg["size"], dtype)
shape_str = f"[{cfg['size']}]"
if do_check and "torch" in backends and any(b in fn_map and b != "torch" for b in backends):
passed, max_diff = check_close(oasr_fn(), pytorch_fn())
if not passed:
print(f" [ERROR] Output mismatch for {shape_str} (max_diff={max_diff:.6f})")
if not allow_mismatch:
continue
for backend in backends:
if backend not in fn_map:
print(f" [WARNING] Unknown backend '{backend}', skipping")
continue
median_ms, std_ms = bench_fn(
fn_map[backend],
dry_run_iters=dry_run_iters,
num_iters=num_iters,
use_cuda_events=use_cuda_events,
)
bw = compute_bandwidth_tb_s(bytes_accessed, median_ms)
output.write_result(BenchResult(
routine="scale",
subroutine=subroutine,
backend=backend,
shape=shape_str,
dtype=dtype_str,
median_ms=median_ms,
std_ms=std_ms,
bandwidth_tb_s=bw,
))
def _resolve_configs(args, subroutine):
size = getattr(args, "size", None)
if size is not None:
return [{"size": size}]
return DEFAULT_CONFIGS.get(subroutine, DEFAULT_CONFIGS["scale"])
def get_fn_map(subroutine, cuda_fn, torch_fn):
"""Return {backend_name: fn} -- backend names match what users pass to --backends."""
return {"cuda": cuda_fn, "torch": torch_fn}
# ---------------------------------------------------------------------------
# Standalone entry -- used by bench_scale.py thin wrapper
# ---------------------------------------------------------------------------
def run_standalone(variant: str = "scale") -> None:
subs = [variant]
pcfg = {k: PROFILE_CONFIGS[k] for k in subs if k in PROFILE_CONFIGS}
setup_funcs = {sub: _make_profile_setup(sub) for sub in subs if sub in PROFILE_CONFIGS}
def benchmark():
output = OutputWriter()
for sub in subs:
output.write_header(f"{sub.upper()} Kernel Benchmark")
for cfg in DEFAULT_CONFIGS.get(sub, []):
oasr_fn, pytorch_fn = setup_scale(cfg["size"], torch.float16)
bytes_accessed = _scale_bytes(cfg["size"], torch.float16)
shape_str = f"[{cfg['size']}]"
for backend, fn in get_fn_map(sub, oasr_fn, pytorch_fn).items():
median_ms, std_ms = bench_fn(fn)
bw = compute_bandwidth_tb_s(bytes_accessed, median_ms)
output.write_result(BenchResult(
routine="scale", subroutine=sub, backend=backend,
shape=shape_str, dtype="float16",
median_ms=median_ms, std_ms=std_ms,
bandwidth_tb_s=bw,
))
output.finalize()
run_main(f"{variant.upper()} Kernel", pcfg, setup_funcs, benchmark)
def _make_profile_setup(subroutine):
cfg_tuple = PROFILE_CONFIGS[subroutine]
def _setup():
return setup_scale(*cfg_tuple)
return _setup
Key points:
SUBROUTINES, parse_args, run_test, get_default_configs, run_standalone are the contract the routine registry expects (see benchmarks/routines/__init__.py).setup_*() functions return two closures (oasr_fn, pytorch_fn) that take no arguments -- this is what bench_fn consumes and what the profile path replays."cuda" / "torch") are family-conventional. Norm/Conv1D/Activation use cuda/torch; GEMM/Conv2D use cutlass/torch. Match the family your kernel belongs to.compute_bandwidth_tb_s for memory-bound kernels and compute_gemm_tflops / compute_bmm_tflops for compute-bound ones.Edit benchmarks/routines/__init__.py and add the routine to ROUTINE_REGISTRY:
ROUTINE_REGISTRY: dict[str, str] = {
"gemm": "benchmarks.routines.gemm",
"norm": "benchmarks.routines.norm",
# ...
"scale": "benchmarks.routines.scale", # NEW
}
This makes python benchmarks/oasr_benchmark.py --routine scale --subroutine scale ... work.
Create benchmarks/bench_scale.py:
#!/usr/bin/env python3
"""OASR Scale Benchmark -- CUDA vs PyTorch."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from benchmarks.routines.scale import run_standalone
if __name__ == "__main__":
run_standalone("scale")
For families with multiple subroutines (e.g. activation), create one thin wrapper per subroutine (bench_glu.py, bench_swish.py) -- each calls run_standalone("<subroutine>"). See benchmarks/bench_glu.py and benchmarks/bench_swish.py.
# Standalone (thin wrapper)
python benchmarks/bench_scale.py
# Unified CLI
python benchmarks/oasr_benchmark.py --routine scale --subroutine scale \
--backends cuda torch --size 4096 --dtype float16 --refcheck -vv
# Profiling mode (NVTX markers for Nsight Compute)
ncu --set full -o scale_profile python benchmarks/bench_scale.py --profile --target oasr
Benchmark utilities (from benchmarks/routines/bench_utils.py):
| Function | Purpose |
|---|---|
bench_fn(fn, ...) | Time a function, returns (median_ms, std_ms) |
profile_kernel(name, fn, ...) | Run with NVTX markers for Nsight Compute |
check_close(actual, expected, ...) | Compare tensors, returns (passed, max_diff) |
compute_bandwidth_tb_s(bytes, ms) | Memory bandwidth for memory-bound kernels |
compute_gemm_tflops(M, N, K, ms) | TFLOPS for compute-bound GEMM-like kernels |
BenchResult(...) | Structured result dataclass |
OutputWriter() | Manages terminal [PERF] lines + CSV output |
run_main(title, pcfg, setup, fn) | Standard standalone main with --profile support |
-> For complete benchmarking guide, see .claude/skills/benchmark-kernel/SKILL.md
When adding a new kernel, look at these existing families as references:
| Family | Kernel Header | Launcher | Binding | JIT Generator | Python API |
|---|---|---|---|---|---|
| Activation | include/oasr/activation.cuh | csrc/activation.cu | csrc/activation_jit_binding.cu | jit/activation.py | activation.py |
| Norm | include/oasr/norm.cuh | csrc/norm.cu | csrc/norm_jit_binding.cu | jit/norm.py | norm.py |
| Conv1D | include/oasr/conv/conv1d.cuh | csrc/conv.cu | csrc/conv_jit_binding.cu | jit/conv.py | conv.py |
| Conv2D | include/oasr/conv/conv2d.cuh | csrc/conv2d.cu | csrc/conv2d_jit_binding.cu | jit/conv.py | conv.py |
| GEMM | include/oasr/gemm/gemm.cuh | csrc/gemm.cu | csrc/gemm_jit_binding.cu | jit/gemm.py | gemm.py |
| BMM | include/oasr/gemm/bmm.cuh | csrc/bmm.cu | csrc/bmm_jit_binding.cu | jit/gemm.py | gemm.py |
| Group GEMM | include/oasr/gemm/group_gemm.cuh | csrc/group_gemm.cu | csrc/group_gemm_jit_binding.cu | jit/gemm.py | gemm.py |
include/oasr/scale.cuh # NEW: CUDA kernel definition
csrc/scale.cu # NEW: TVM-FFI launcher
csrc/scale_jit_binding.cu # NEW: TVM-FFI binding
oasr/jit/scale.py # NEW: JIT generator
oasr/scale.py # NEW: Python API
oasr/__init__.py # MODIFIED: Export API
oasr/aot.py # MODIFIED: Register AOT
tests/test_scale.py # NEW: Unit tests
benchmarks/routines/scale.py # NEW: Benchmark routine module
benchmarks/routines/__init__.py # MODIFIED: Register routine
benchmarks/bench_scale.py # NEW: Standalone benchmark wrapper