Tutorial: Adding an AOT CUDA/C++ Kernel
SkillDev toolsWalks your agent through adding a prebuilt CUDA/C++ kernel to rapid_llm's native extension, from scaffolding to tests.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Tutorial: Adding an AOT CUDA/C++ Kernel skill
About this capability
Step-by-step tutorial for adding a heavyweight AOT CUDA/C++ kernel to rapid_llm's optional native extension (csrc/ tree + torch library registration + optional wheel build), covering first-time build scaffolding, KernelSpec registration, tests, and benchmarks. Use when a kernel depends on CUTLASS or
What this skill tells your AI
The instructions your AI receives, as published by harleyszhang/rapid_llm in .claude/skills/add-sgl-kernel/SKILL.md and read by ahel’s review.
The AOT counterpart of add-jit-kernel — the flow sglang's sgl-kernel package uses, adapted to rapid_llm. Walks through scale(x, factor) = x * factor (FP16/BF16/FP32). rapid_llm has no prebuilt native extension yet, so the first AOT kernel also lands the scaffolding in this tutorial; later kernels repeat Steps 1-6 only.
Path decision (choose before writing code)
| Path | When to use | Skill |
|---|---|---|
| Triton (default) | Expressible in Triton; supplies the never-failing native floor row | triton-kernel-writing |
| JIT CUDA | CUDA C++ needed but no heavyweight C++ dependency; single op, fast iteration | add-jit-kernel |
| AOT extension (this skill) | CUTLASS / large C++ project, shared C++ infra, ship prebuilt in the wheel, no compile cost tolerated at first run | add-sgl-kernel |
| External library | flashinfer / deepgemm / deepep / flashmla already ships a proven implementation | add-op-backend |
Every new kernel ships with tests and a benchmark in the same PR — same bars as every other kernel path.
Repository map (first kernel creates the tree)
csrc/ # NEW tree: AOT extension sources
├── include/rapid_llm_ext.h # public C++ declarations + dispatch macros
├── elementwise/scale.cu # kernel + launcher
└── common_extension.cc # TORCH_LIBRARY_FRAGMENT registration
setup.py # NEW: optional CUDAExtension, RAPID_LLM_BUILD_EXT=1
rapid_llm/kernels/aot.py # NEW once: available() probe
rapid_llm/kernels/ops/<group>/<op>_aot.py # thin wrapper over torch.ops.rapid_llm.*
rapid_llm/kernels/ops/<group>/__init__.py # KernelSpec row (backend="aot")
tests/kernels/, benchmarks/kernels/ # same bars as every kernel
Hard constraint: pyproject.toml builds a pure-Python wheel and the README promises a working CPU-only install. The extension stays optional: built only under RAPID_LLM_BUILD_EXT=1, imported only behind the lazy KernelSpec target, and its row carries an available probe so CPU-only or extension-less installs degrade to the native row. Never import rapid_llm._C at package top level.
Step 1: kernel + launcher in csrc/elementwise/scale.cu
Same body as the add-jit-kernel example, two differences: no PYBIND11_MODULE (registration lives in common_extension.cc, Step 3) and the launcher is declared in the public header (Step 2). The conventions carry over unchanged: TORCH_CHECK validation around the launch, at::cuda::getCurrentCUDAStream() + OptionalCUDAGuard, AT_DISPATCH_FLOATING_TYPES_AND2, C10_CUDA_KERNEL_LAUNCH_CHECK() after launch, ASCII only, const T* __restrict__, int64_t for numel-derived indexing. A kernel that syncs or assembles per-step host data is not CUDA-graph capture-safe — its spec row must say graph_safe=False.
Arch-gated code (SM90+ CUTLASS paths): enforce with TORCH_CHECK in the launcher and skip logic in tests.
Step 2: declaration in csrc/include/rapid_llm_ext.h
#pragma once
#include <torch/extension.h>
// elementwise
void scale(at::Tensor& out, const at::Tensor& input, double factor);
Step 3: registration in csrc/common_extension.cc
#include <torch/extension.h>
#include "rapid_llm_ext.h"
TORCH_LIBRARY_FRAGMENT(rapid_llm, m) {
// elementwise
m.def("scale(Tensor! out, Tensor input, float factor) -> ()");
m.impl("scale", torch::kCUDA, &scale);
}
Tensor!marks the mutable out-argument; the schema is whattorch.compileand the dispatcher see, so keep it exact.- Schema scalar types stay PyTorch types (
float); the C++ signature still takesdouble—torch::Libraryrequires it. - One
TORCH_LIBRARY_FRAGMENTper extension; new ops appendm.def/m.implpairs here.
Step 4: build wiring setup.py (once)
"""Optional native extension. Pure-Python install remains the default."""
import os
from setuptools import setup
ext_modules = []
cmdclass = {}
if os.environ.get("RAPID_LLM_BUILD_EXT") == "1":
from torch.utils.cpp_extension import BuildExtension, CUDAExtension
ext_modules.append(
CUDAExtension(
name="rapid_llm._C",
sources=[
"csrc/common_extension.cc",
"csrc/elementwise/scale.cu", # keep sorted
],
extra_compile_args={"cxx": ["-O3"], "nvcc": ["-O3"]},
)
)
cmdclass["build_ext"] = BuildExtension
setup(ext_modules=ext_modules, cmdclass=cmdclass)
Build:
RAPID_LLM_BUILD_EXT=1 pip install -e .
# limit host load:
RAPID_LLM_BUILD_EXT=1 MAX_JOBS=2 pip install -e .
TORCH_CUDA_ARCH_LIST controls the gencode set; set it explicitly on build machines so the wheel is not pinned to the build GPU. When the source tree outgrows one list (CUTLASS generators, per-arch files), migrate to CMake — that is a deliberate trade, not the default.
Step 5: availability probe rapid_llm/kernels/aot.py (once)
"""Availability of the optional AOT extension for KernelSpec rows."""
from __future__ import annotations
import importlib.util
def available() -> bool:
return importlib.util.find_spec("rapid_llm._C") is not None
Step 6: Python wrapper + KernelSpec row
ops/elementwise/scale_aot.py — thin like every wrapper: allocate out, one call, return. Validation lives in the C++ launcher.
def scale(input: torch.Tensor, factor: float,
out: torch.Tensor | None = None) -> torch.Tensor:
if out is None:
out = torch.empty_like(input)
torch.ops.rapid_llm.scale.default(out, input, factor)
return out
Row in ops/elementwise/__init__.py:
register(
KernelSpec(
name="aot/scale",
op="scale",
backend="aot",
target="rapid_llm.kernels.ops.elementwise.scale_aot:scale",
available="rapid_llm.kernels.aot:available",
dtypes=("bf16", "fp16", "fp32"),
golden=GoldenRecord(verified=True, max_abs_diff=0.0, baseline="native/scale"),
priority=UNMEASURED,
)
)
backend="aot"is the family label; the exactly-one-native-row-per-op rule is untouched — an optional extension must never be the floor.- Extension absent -> row filtered,
explain()shows why,nativewins. That fallback is what keeps CPU-only installs working. goldenrecords the measured diff against the native baseline; unverified rows stay out of default dispatch. Register the module inkernels/__init__.py_EXPORTS.
Step 7: tests and benchmark (required, same bars as JIT)
- Tests:
tests/kernels/test_scale_aot.pyperwrite-test— dtype x size (tail sizes) x factor, oracle istests/reference.py, CPU/shape-mismatch inputs raise. Skip cleanly when the extension is absent so CPU-only CI stays green. - Benchmark:
benchmarks/kernels/via thekernel-microbenchmarkharness; arms are the AOT row, thenativerow, and the JIT row if one exists. The result feedspriority; unmeasured rows lose ties.
Step 8: validate
RAPID_LLM_BUILD_EXT=1 pip install -e .
pytest tests/kernels/test_scale_aot.py -q
python -m benchmarks.kernels.<...> # the harness entry for the op
Also verify the negative path in CI: an install without RAPID_LLM_BUILD_EXT must import the package, filter the aot/* rows, and pass the full test suite on the native floor.
Troubleshooting
- Undefined symbol at import: the
.cuis missing fromsourcesinsetup.py— the build succeeded but never compiled the file. torch.ops.rapid_llm.scalemissing:m.def/m.implnot incommon_extension.cc, or the wheel was built withoutRAPID_LLM_BUILD_EXT=1.- Build OOM / too slow: lower
MAX_JOBS; CUTLASS TUs are heavy — budget one job per 2 GB RAM. - Wrong-arch wheel: set
TORCH_CUDA_ARCH_LISTexplicitly on the build machine. - Async CUDA errors:
CUDA_LAUNCH_BLOCKING=1; memory errors:compute-sanitizer --tool memcheck python ....
Cross-references
add-jit-kernel— the lightweight counterpart; try it first unless a heavyweight C++ dependency forces AOTtriton-kernel-writing— the default kernel pathadd-op-backend— external-library path; KernelSpec registration detail, dispatch correctness tests,explain()kernel-microbenchmark— the only sanctioned timing harnesswrite-test— test admission ruleslocate-numeric-divergence— when the new row disagrees with the baselinerules/kernel-conventions.md— layering, indexing, and registration rules that bind.cucode too
Summary of files (first kernel)
csrc/elementwise/scale.cu # NEW: kernel + launcher
csrc/include/rapid_llm_ext.h # NEW: declaration
csrc/common_extension.cc # NEW: TORCH_LIBRARY_FRAGMENT registration
setup.py # NEW: optional CUDAExtension (RAPID_LLM_BUILD_EXT=1)
rapid_llm/kernels/aot.py # NEW once: available() probe
rapid_llm/kernels/ops/elementwise/scale_aot.py # NEW: wrapper
rapid_llm/kernels/ops/elementwise/__init__.py # NEW group (or MODIFIED): KernelSpec row
rapid_llm/kernels/__init__.py # MODIFIED: _EXPORTS entry
tests/kernels/test_scale_aot.py # NEW: tests
benchmarks/kernels/... # NEW: benchmark per harness
Signals
- GitHub stars
- 195
- Forks
- 33
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
add-sgl-kernel- Source
- github.com/harleyszhang/rapid_llm