Skip to content
Skip to lesson
Back to Topics
LearnInference & Production ScaleAI-Generated Kernel Engineering
🚀HardInference Optimization

AI-Generated Kernel Engineering

Turn generated GPU code into a promotable library kernel through explicit operator contracts, hidden correctness tests, sanitizer gates, fair benchmarks, reproducible receipts, and controlled rollout.

32 min read
Learning path
Step 153 of 197 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Imagine reviewing a generated extension. Its author reports a 1.4x speedup on one H100 shape, [2, 4096]. You change the row width to 1009 and the numerical test fails. You restore the width but transpose the input, and it fails again. What has the speedup established? Only that one timed call looked promising. It hasn't established the operator's accepted domain.

Generated source needs the same scrutiny as other unfamiliar code. Models can reason about kernels and use compiler or device tools, but a plausible explanation doesn't establish what the loaded binary computes. Require a precise contract, independent tests, scoped runtime checks, fair measurements, and a recovery plan. Each supplies evidence that the others can't replace.

The GPU Runtime Execution Lab made host submission, device streams, static storage, fallback paths, and timing scopes explicit. Here, inspect the candidate behind that dispatch and build evidence for its correctness and safety before promotion.

One realistic operator carries this process throughout the chapter: weighted root mean square normalization (RMSNorm), weighted_rmsnorm(x, weight, eps). It rescales each row across its last dimension and applies a learned feature weight. For an example row [3.0, 4.0], the mean square is (9+16)/2=12.5(9 + 16) / 2 = 12.5. Divide each entry by 12.5+ϵ\sqrt{12.5 + \epsilon}, then multiply by its corresponding weight. The positive scalar ϵ\epsilon ensures an all-zero row never divides by zero.

For row index ii, feature index jj, and row width HH:

yi,j=xi,j wj(1H∑k=1Hxi,k2+ϵ)−1/2y_{i,j} = x_{i,j}\, w_j\left(\frac{1}{H}\sum_{k=1}^{H}x_{i,k}^{2}+\epsilon\right)^{-1/2}

The reference implementation is pure . A candidate may use CUDA, , or compiler code. The evidence goals stay the same, while language/tool-specific checks can differ. The executable examples run on CPU and expose limitations in evaluator logic before you test a device implementation.

AI kernel generation dynamics and common failure modes

Three concrete defects are worth constructing in a test, rather than assuming a generator will avoid them:

  1. Missing ordering: A CUDA shared-memory consumer needs the appropriate synchronization with its producers. A race can occur within one block on one SM; it doesn't require 132 SMs, clock changes, or a busy GPU. Triton normally manages dependencies for its generated execution, so don't mechanically insert CUDA-style barriers into its source. Inspect the language and generated-code contract.
  2. Incorrect tails: A global-load mask doesn't establish that reduction padding contains the additive identity. Test widths around tile boundaries, including 127, 128, 129, and 1009. Separately check whether a shared-memory index stays in its allocation. Uninitialized in-bounds data and out-of-bounds access are different defects.
  3. Wrong address mapping: Correctness can survive a poor access pattern. For 32 aligned FP32 lane loads, contiguous addresses span four 32-byte sectors; a sufficiently spaced pattern can touch 32. Those are warp address counts, not a prediction of HBM bytes or runtime. Cache state, reuse, active lanes, and instruction layout matter. A small cached test can be a valid measurement for that workload while failing to represent a large streaming workload.

Write the operator contract before the prompt

Prompt engineering can't repair an underspecified operation. A code generator needs both the exact mathematical formula and the library contract surrounding it. PyTorch custom operators declare mutation, aliasing, and schemas via torch.library.custom_op, while torch.library.opcheck verifies registration, fake-tensor integration, and compilation mechanics rather than numerical correctness.[1] Treat those as distinct obligations.

Here is the contract for the running operator:

Contract axisweighted_rmsnorm requirementRejection example
Shapex has shape [..., H], H > 0; empty outer dimensions are valid; weight has shape [H]; output shape matches xCandidate assumes batch is 2 or H is 4096
Dtypex and weight share float16, bfloat16, or float32; low-precision reduction accumulates in float32Candidate sums squares in float16
StrideOrdinary PyTorch strided inputs, including read-only expanded views, accepted; unsupported fast-path layouts route to referenceCandidate reads tensors as contiguous without checking
DeviceBoth tensors on same CUDA device; CPU path routes to reference implementationCandidate copies through host or mismatched device
AliasingFunctional operation returns fresh output and leaves inputs unchangedCandidate overwrites x through borrowed pointer
AutogradFast path is inference-only; active autograd routes to differentiable referenceCandidate silently detaches tensors needing gradients
Scalareps is an ordinary Python int or float, excluding bool, staying finite and strictly positive in FP32A tiny positive value rounds to zero in reduction dtype
NumericsMatch reference within dtype-specific tolerances; NaNs match positions and signed infinities matchCandidate passes only finite, unit-scale random values
ErrorsWrapper rejects invalid shape, dtype, device, or eps under versioned error contractCandidate silently casts or truncates values

The dispatch predicate is an active gate in the contract. Suppose the first generated kernel supports CUDA, contiguous x and weight, H divisible by 128, and all three dtypes. The wrapper must verify those conditions, check the target GPU architecture, and confirm that backward autograd isn't needed; any other valid call routes to the reference. Invalid public inputs must still raise the declared library error. A fast-path restriction is acceptable; an unannounced semantic limitation isn't.

A contiguous last dimension alone doesn't justify flattening rows: sliced outer dimensions leave gaps between them. Either compute row addresses from strides or require the entire input tensor to be contiguous. For ordinary reverse-mode autograd, route to the differentiable reference when torch.is_grad_enabled() and an input's requires_grad are both true. Forward AD, transforms such as vmap, and compiler integration need their own declared support and dispatch tests; the reverse-mode predicate alone doesn't establish them.

Use a prompt specification that exposes obligations without leaking private evaluation fixtures:

Candidate specification
Implement weighted_rmsnorm(x, weight, eps) for the declared CUDA fast path.

Semantics:
  xf, wf = float32(x), float32(weight)
  y = (xf * rsqrt(mean(xf ** 2, dim=-1, keepdim=True) + float32(eps))) * wf
  cast y back to x.dtype

Contract:
  x: [..., H], weight: [H], same supported floating dtype and CUDA device
  return fresh output; never mutate or alias x or weight
  wrapper dispatches unsupported shapes or strides to reference
  wrapper uses differentiable reference when active autograd needs a backward path

Deliver:
  source, build command, dispatch predicate, claimed hardware target,
  correctness command, sanitizer command, benchmark command, and known limits

Forbidden:
  input-value lookup tables, incorrect shape branches, host callbacks,
  network access, and changes outside isolated build directory

Exact evaluation shapes and adversarial values stay outside the model context. Structural rules remain visible so the generator implements the intended program rather than guessing benchmark inputs.

Isolate generated code and the toolchain

Generated kernel source is untrusted code. Compiling it invokes preprocessors, linkers, build scripts, dynamic linkers, and shell hooks. Run the compiler and generated binaries in a disposable sandbox with a pinned toolchain image, no credentials, no network access, tight CPU and memory limits, a wall-clock timeout, and a single writable build directory. Expose only public smoke fixtures there. An external orchestrator calls the model provider without exposing API keys to the build worker. Allow-list compiler commands and artifact types. Keep the evaluator, hidden fixtures, promotion registry, and production codebase completely outside the sandbox.

Device execution requires another boundary. Untrusted code can access other allocations in its own CUDA context, hang work, or exercise the driver attack surface. A function signature containing only input/output pointers isn't a memory-access sandbox. Use an isolated GPU worker with no production traffic or credentials; keep unrelated secrets and evaluator state outside its accessible process and device context. Enforce an external timeout and a documented worker/device recovery procedure. Killing a host process isn't a universal guarantee of immediate device recovery, and a device reset can affect other users of that device. A host container alone doesn't isolate every hardware or driver failure.

These boundaries mirror secure code-generation agent architectures: model output receives only the capabilities required for its task, avoiding ambient authority over the repository or secrets. Record every tool call, compiler invocation, exit code, and artifact hash. Reject attempts to modify the harness, reference, timing harness, or test suites.

Catch a candidate that only handles the visible shape

A minimal evaluator demonstrates incomplete domain coverage immediately. The CPU example below inspects a deliberately flawed candidate that computes the operator for the public contiguous 2 x 4 float32 shape, but returns zeros for any other input.

reject-hardcoded-candidate.py
import torch

torch.set_num_threads(1)

def reference(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
    mean_square = x.float().square().mean(dim=-1, keepdim=True)
    normalized = x.float() * torch.rsqrt(mean_square + eps)
    return (normalized * weight.float()).to(x.dtype)

def bad_candidate(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
    public_shape = tuple(x.shape) == (2, 4)
    if public_shape and x.is_contiguous() and x.dtype == torch.float32:
        mean_square = x.square().mean(dim=-1, keepdim=True)
        return x * torch.rsqrt(mean_square + eps) * weight
    return torch.zeros_like(x)

def check(name: str, x: torch.Tensor, weight: torch.Tensor) -> bool:
    expected = reference(x, weight, 1e-5)
    actual = bad_candidate(x, weight, 1e-5)
    try:
        torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-6)
        passed = True
    except AssertionError:
        passed = False
    disp = "PASS" if passed else "FAIL"
    print(f"{name}: {disp}")
    return passed

base = torch.tensor(
    [[0.5, -1.0, 2.0, -0.25], [3.0, 0.25, -2.0, 1.0]],
    dtype=torch.float32,
)
weight4 = torch.tensor([1.0, 0.5, -0.75, 2.0])
cases = [
    ("visible-contiguous-2x4", base, weight4),
    ("post-freeze-scale-large", base * 3.0, weight4),
    ("post-freeze-scale-small", base * 0.01, weight4),
    ("post-freeze-negated", -base, weight4),
    ("hidden-shape-3x5", torch.arange(15.0).reshape(3, 5) + 0.5, torch.ones(5)),
    ("hidden-noncontiguous-2x4", torch.arange(8.0).reshape(4, 2).t() + 0.5, weight4),
]

assert cases[-1][1].stride() == (1, 2)
passed = [check(name, x, weight) for name, x, weight in cases]
assert passed == [True, True, True, True, False, False]
disp = "PROMOTE" if all(passed) else "REJECT"
print("candidate disposition:", disp)
Output
visible-contiguous-2x4: PASS
post-freeze-scale-large: PASS
post-freeze-scale-small: PASS
post-freeze-negated: PASS
hidden-shape-3x5: FAIL
hidden-noncontiguous-2x4: FAIL
candidate disposition: REJECT

Scaling and negation preserve tensor shape, so the candidate's hardcoded branch still passes them. The new shape and transposed view expose the missing domain. Shape specialization is acceptable when a wrapper routes unsupported calls to a verified fallback. Returning zeros for unhandled inputs violates the operator contract.

Six gates before a performance claim

A trusted evaluator can run these checks automatically. Its results still need interpretation: sampled tests don't prove correctness over an infinite domain, and sanitizer coverage is limited to the paths exercised. Keep the evaluator immutable while candidates change.

Diagram showing 1. Reference Oracle Functional PyTorch contract, 2. Compile & Static Check ptxas register spills & smem, 3. Numerical & Contract Tests Tails, values, strides, mutation, and 4. Scoped Safety Checks Sanitizers & repeated calls.
1. Reference Oracle Functional PyTorch contract, 2. Compile & Static Check ptxas register spills & smem, 3. Numerical & Contract Tests Tails, values, strides, mutation, and 4. Scoped Safety Checks Sanitizers & repeated calls.

1. Reference oracle

The reference defines this lesson's operational semantics: FP32 squaring, reduction, reciprocal square root, normalization, and weighting, followed by an output cast. FP32 still rounds and can overflow. This isn't an exact-real-arithmetic oracle. If your application needs stable normalization of arbitrarily large finite values, change the reference and contract deliberately; matching an overflowing reference doesn't establish that stronger property.

2. Compilation check and static inspection

Build for the declared target and retain compiler diagnostics. For example, nvcc -O3 -lineinfo -arch=sm_90 -Xptxas=-v ... requests H100's baseline target and verbose assembler resource reports. Use sm_90a when the kernel requires its architecture-specific features, and record the narrower compatibility contract.[2] Inspect:

  • Registers and spills: Record registers per thread and spill/local-memory reports. Register pressure and allocation constraints can cause spilling. Local memory is device-backed but cached; CUDA 13 also offers opt-in shared-memory spilling. A spill is a performance question, not automatic semantic rejection or proof of catastrophic slowdown.
  • Shared memory: Resource allocation can limit resident blocks. In a hypothetical 99 KiB budget, 64 KiB per block permits only one block by that constraint. Registers, threads, allocation granularity, and architecture also matter; lower occupancy doesn't necessarily mean slower execution.
  • Artifacts and actual dispatch: cuobjdump and nvdisasm reveal symbols, instructions, and resource usage.[3] A symbol's presence doesn't prove it ran, rule out a host fallback, or establish the absence of input-value shortcuts. Combine inspection with a trusted wrapper, loaded-artifact provenance, and execution traces.

3. Multi-scale numerical verification

A candidate that passes on powers of two can fail on boundary conditions. Test families must probe diverse dimensions and memory alignments:

  • Powers of two and neighbors: Include H∈{127,128,129,511,512,513,4096}H \in \{127,128,129,511,512,513,4096\}. A power-of-two width alone doesn't establish pointer alignment or coalescing.
  • Prime row lengths: Odd and prime dimensions like H∈{1009,4099}H \in \{1009, 4099\} test edge-tile boundary masks and thread reduction cleanup loops.
  • Non-contiguous views and ragged strides: Transposed matrices (x.t()), strided slices, and non-zero storage offsets test pointer arithmetic.
  • Adversarial values: Zeros, subnormals, mixed magnitudes, signs, and exact NaN/infinity placements test the declared operational policy. 101910^{19} squared is finite in FP32, whereas 2×10192\times10^{19} squared overflows; reduction intermediates can overflow too. Matching these cases doesn't certify general IEEE 754 compliance.

Componentwise numerical acceptance requires a declared formula:

∣c−r∣≤atol+rtol⋅∣r∣|c - r| \le \mathrm{atol} + \mathrm{rtol} \cdot |r|

PyTorch 2.14's torch.testing.assert_close implements this finite-value rule. Matching NaNs requires equal_nan=True; its default is False. Signed infinities must agree, and dtype/device/layout checks are enabled by default; stride checking is optional.[4] Set tolerances before evaluating candidates. Report errors on finite entries and non-finite mismatches separately; relative error near zero needs a declared denominator policy.

The script below audits input preservation, memory aliasing, and non-finite policies:

audit-rmsnorm-contract.py
import math
import torch

torch.set_num_threads(1)

def reference(x, weight, eps):
    if x.layout != torch.strided or weight.layout != torch.strided:
        raise ValueError("Need ordinary strided tensors.")
    if x.ndim < 1 or x.shape[-1] <= 0 or weight.shape != (x.shape[-1],):
        raise ValueError("Need x[..., H], H > 0, and weight[H].")
    if x.dtype not in (torch.float16, torch.bfloat16, torch.float32) or weight.dtype != x.dtype:
        raise ValueError("Need the same supported floating dtype.")
    if x.device != weight.device or x.device.type not in ("cpu", "cuda"):
        raise ValueError("Need one supported device.")
    if type(eps) not in (float, int):
        raise ValueError("eps must be an ordinary numeric scalar.")
    try:
        eps_value = float(eps)
    except OverflowError as exc:
        raise ValueError("eps is too large.") from exc
    if not math.isfinite(eps_value) or eps_value <= 0:
        raise ValueError("eps must be finite and positive.")
    eps32 = float(torch.tensor(eps_value, dtype=torch.float32, device="cpu"))
    if not math.isfinite(eps32) or eps32 <= 0:
        raise ValueError("eps must stay finite and positive in FP32.")
    with torch.autocast(device_type=x.device.type, enabled=False):
        xf, wf = x.float(), weight.float()
        scale = torch.rsqrt(xf.square().mean(dim=-1, keepdim=True) + eps32)
        return ((xf * scale) * wf).to(x.dtype)

def value_bytes(tensor):
    flat = torch.empty(tensor.numel(), dtype=tensor.dtype, device=tensor.device)
    flat.copy_(tensor.detach().reshape(-1))
    return flat.view(torch.uint8)

def audit(candidate, x, weight, eps=1e-5):
    expected = reference(x, weight, eps)
    snapshots = [value_bytes(t) for t in (x, weight)]
    def metadata(t):
        return (t.shape, t.stride(), t.storage_offset(), t.dtype, t.device,
                t.untyped_storage().data_ptr())
    before_metadata = [metadata(t) for t in (x, weight)]
    try:
        actual = candidate(x, weight, eps)
        assert isinstance(actual, torch.Tensor)
        assert actual is not x and actual is not weight, "returned input object"
        torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-6, equal_nan=True)
        for current, before, original_meta in zip((x, weight), snapshots, before_metadata, strict=True):
            assert metadata(current) == original_meta, "input metadata changed"
            assert torch.equal(value_bytes(current), before), "input mutated"
            if actual.numel() and current.numel():
                assert actual.untyped_storage().data_ptr() != current.untyped_storage().data_ptr(), "output aliases input"
    except (AssertionError, RuntimeError, TypeError, ValueError):
        return False
    return True

def mutate_after_computing(x, weight, eps):
    y = reference(x, weight, eps)
    x.add_(1)
    return y

def scrub_nans(x, weight, eps):
    return reference(x, weight, eps).nan_to_num()

def raises_error(x, weight, eps):
    raise RuntimeError("candidate failed")

base = torch.tensor([[3., 4.], [0., 0.]])
weight = torch.tensor([1., 2.])
special = torch.tensor([[float("nan"), 1.], [float("inf"), 2.]])
cases = [
    ("reference ordinary", reference, base.clone(), weight),
    ("reference empty outer dimension", reference, torch.empty(0, 2), weight),
    ("reference expanded input", reference, base[:1].expand(3, 2), weight),
    ("reference non-finite policy", reference, special, weight),
    ("correct values but mutated input", mutate_after_computing, base.clone(), weight),
    ("aliased zero output", lambda x, w, e: x, torch.zeros(1, 2), weight),
    ("empty output returns input", lambda x, w, e: x, torch.empty(0, 2), weight),
    ("wrong output dtype", lambda x, w, e: reference(x, w, e).double(), base, weight),
    ("silently replaced NaNs", scrub_nans, special, weight),
    ("candidate raised an error", raises_error, base, weight),
]
results = [audit(fn, x, w) for _, fn, x, w in cases]
assert results == [True, True, True, True, False, False, False, False, False, False]
for (name, *_), passed in zip(cases, results, strict=True):
    disp = "PASS" if passed else "FAIL"
    print(f"{name}: {disp}")
Output
reference ordinary: PASS
reference empty outer dimension: PASS
reference expanded input: PASS
reference non-finite policy: PASS
correct values but mutated input: FAIL
aliased zero output: FAIL
empty output returns input: FAIL
wrong output dtype: FAIL
silently replaced NaNs: FAIL
candidate raised an error: FAIL

This small helper checks logical input bytes, metadata, some aliases, output attributes, and values on these FP32 CPU cases. Its fixed tolerance isn't a policy for every dtype and reduction length. It doesn't inspect unused storage, prove the absence of temporary mutations, establish every empty-view alias relationship, or verify gradients and concurrent GPU calls. It also runs the candidate in the same process, so it isn't the security boundary for malicious code. A real evaluator records worker failures externally and tests active-autograd fallback, streams, repeated calls, and operator registration separately.

Can a zero-returning candidate still pass a closeness test? Try a row whose correct output is tiny. The absolute tolerance can dominate:

a-tolerance-is-a-policy.py
import torch

torch.set_num_threads(1)
x = torch.tensor([[1e-8, -1e-8]])
expected = x * torch.rsqrt(x.square().mean(dim=-1, keepdim=True) + 1.0)
zeros = torch.zeros_like(expected)

def accepts(atol):
    try:
        torch.testing.assert_close(zeros, expected, rtol=1e-5, atol=atol)
        return True
    except AssertionError:
        return False

assert accepts(1e-6) and not accepts(1e-10)
large = torch.tensor([1e19, 2e19], dtype=torch.float32)
finite_squares = torch.isfinite(large.square()).tolist()
assert finite_squares == [True, False]
direct = large * torch.rsqrt(large.square() + 1e-5)
wide = large.double() * torch.rsqrt(large.double().square() + 1e-5)
assert direct[1] == 0 and abs(float(wide[1]) - 1.0) < 1e-12
print("zero output accepted, atol=1e-6:", accepts(1e-6))
print("zero output accepted, atol=1e-10:", accepts(1e-10))
print("finite FP32 squares at 1e19, 2e19:", finite_squares)
print(f"normalizing 2e19, direct FP32 vs FP64: {direct[1]:.1f}, {wide[1]:.1f}")
Output
zero output accepted, atol=1e-6: True
zero output accepted, atol=1e-10: False
finite FP32 squares at 1e19, 2e19: [True, False]
normalizing 2e19, direct FP32 vs FP64: 0.0, 1.0

The first acceptance follows the chosen tolerance. The final line exposes a different limitation: direct FP32 squaring loses the large finite input, although wider arithmetic yields a normalized value near one. Decide which semantics and errors the application accepts before generation. Tightening tolerances afterward only for a disliked candidate would change the evaluation contract.

Test the evaluator's expected relations too

A scale change is a useful new input, but it doesn't imply the output stays unchanged. In RMSNorm, keeping epsilon fixed changes its size relative to the mean square. Near zero, that effect is large. Predict whether the correct reference passes a test requiring reference(100 * x, eps) == reference(x, eps).

a-false-invariance-test.py
import torch

torch.set_num_threads(1)
x = torch.tensor([[1e-3, -1e-3]], dtype=torch.float32)
weight = torch.ones(2)
eps, scale = 1e-5, 100.0

def reference(values, epsilon):
    mean_square = values.square().mean(dim=-1, keepdim=True)
    return values * torch.rsqrt(mean_square + epsilon) * weight

original = reference(x, eps)
fixed_epsilon = reference(scale * x, eps)
scaled_epsilon = reference(scale * x, scale**2 * eps)
naive_pass = torch.allclose(fixed_epsilon, original, rtol=1e-5, atol=1e-6)
coupled_pass = torch.allclose(scaled_epsilon, original, rtol=1e-5, atol=1e-6)
assert not naive_pass and coupled_pass
print(f"original={original[0, 0]:.6f}, "
      f"fixed epsilon={fixed_epsilon[0, 0]:.6f}, "
      f"scaled epsilon={scaled_epsilon[0, 0]:.6f}")
print("naive invariance accepts correct reference:", naive_pass)
print("coupled relation accepts correct reference:", coupled_pass)
Output
original=0.301511, fixed epsilon=0.999500, scaled epsilon=0.301511
naive invariance accepts correct reference: False
coupled relation accepts correct reference: True

For positive scale aa, the exact-arithmetic relation is f(ax,w,a2ϵ)=f(x,w,ϵ)f(ax,w,a^2\epsilon)=f(x,w,\epsilon). Both terms inside the square root scale by a2a^2. A negative scale adds a sign change; zero makes the scaled epsilon invalid under this contract. Floating-point overflow, underflow, and rounding limit the relation's numerical use. Run the candidate and reference on each transformed input; don't replace their comparison with an unverified invariance rule.

4. Compute Sanitizer gating

Numerical tests don't reveal undefined behavior that survived by luck. NVIDIA Compute Sanitizer isolates execution hazards into four dedicated tools:[5]

  • memcheck: Detects out-of-bounds memory accesses, misaligned addresses, and device heap errors.
  • racecheck: Intercepts shared memory hazards, including Read-After-Write (RAW), Write-After-Read (WAR), and Write-After-Write (WAW) data races.
  • initcheck: Checks global memory by default. The current documentation also supports --initcheck-address-space shared or all for shared-memory checks; record the installed tool version.
  • synccheck: Detects unsupported synchronization usage, including nonuniform conditional block barriers. It isn't a general detector of every possible deadlock.

Invoke the sanitizer with an explicit non-zero exit code:

terminal
compute-sanitizer --tool memcheck --error-exitcode 1 ./test_runner
compute-sanitizer --tool racecheck --error-exitcode 1 ./test_runner
compute-sanitizer --tool initcheck --initcheck-address-space all --error-exitcode 1 ./test_runner
compute-sanitizer --tool synccheck --error-exitcode 1 ./test_runner

Run memcheck first, then the narrower tools on representative paths. Unexplained reports block promotion. Clean reports establish results for those instrumented runs, not universal memory safety or every cross-stream/global-memory race. Retain exit codes, timeouts, tool versions, and reports; instrumentation timing isn't the performance benchmark.

5. Clean CUDA event benchmarking

A host timer around asynchronous submission doesn't establish completion time. A synchronized time.perf_counter() interval can measure completed wall time; CUDA events measure a stream interval, which can include host submission gaps and other ordered work.[6] Declare which interval matters.[7]

  • Same comparison contract: Use identical inputs, layouts, numerical policy, dispatch scope, and synchronization boundaries. Compare against the deployed baseline, including compiled or vendor paths when relevant. Measure full wrapper cost separately from the isolated kernel if users pay both.
  • Warm and cold separately: Compile, load modules, initialize events, and warm the tested path before warm measurements. A fixed 20–50 calls doesn't guarantee steady clocks or stable execution. Record first-use costs and verify stability for the workload.
  • Paired order: Interleave baseline and candidate, for example A-B-B-A, to reduce order effects. This doesn't cancel arbitrary thermal or frequency changes. Record conditions and repeated paired results.
  • Declared cache state: Reusing a working set smaller than H100 SXM's 50 MB L2 can encourage hits without guaranteeing residency. A sufficiently large rotating set encourages misses without guaranteeing every access reaches HBM. Use profiler traffic/cache evidence when interpreting either. A warm-cache benchmark is useful when it matches deployment.
  • Enough samples: Keep raw distributions and confidence/variation information. Name whether each sample is one completed call or an average over a batch; percentiles of batch averages aren't individual-call tails. Small samples don't establish reliable P99/P99.9 behavior.

6. Roofline sanity check

Use the Roofline model with a declared byte count and the compute ceiling for the instructions actually relevant to the kernel.[8] Tensor Core matmul peaks don't describe scalar FP32 RMSNorm instructions.

For weighted RMSNorm, calculate the arithmetic intensity (II):

  • Work proxy: Squaring, reduction, normalization, and weighting give roughly 4H4H operations per row. Division, epsilon addition, and reciprocal square root add constant work, with instruction-dependent cost. This proxy isn't a measured executed-instruction count.
  • Logical traffic: Counting an input read, weight read, and output write per row gives 6H6H bytes for FP16. Across NN rows, that's 6NH6NH. If the common weight is fetched only once, unique operand/output payload is (2NH+H)×2(2NH+H)\times2 bytes. Input rereads, spills, caches, and write behavior further change actual traffic at each memory level.
  • Logical intensity: 4H/(6H)≈0.674H/(6H)\approx0.67 operations/byte suggests investigating memory behavior. Small calls can instead be launch-bound, and dependency chains can be latency-bound before bandwidth saturates.

The quantity below is effective logical bandwidth, not measured HBM bandwidth:

Bandwidthlogical=6⋅N⋅HLatencyseconds\text{Bandwidth}_{\text{logical}} = \frac{6 \cdot N \cdot H}{\text{Latency}_{\text{seconds}}}

It can exceed H100 SXM's 3.35 TB/s HBM peak when some logical accesses are served by caches or reused. That alone doesn't prove skipped work. If independently measured HBM traffic divided by correctly scoped elapsed time appears above the matching hardware ceiling, audit counter definitions, units, timing, and device specifications as well as candidate behavior.

which-bytes-did-you-count.py
# Hypothetical traffic and elapsed time, not a GPU measurement.
rows, width = 10_400, 1_000
elapsed_seconds = 12e-6
logical_bytes = 6 * rows * width
assumed_hbm_bytes = 30_000_000
logical_tbs = logical_bytes / elapsed_seconds / 1e12
hbm_tbs = assumed_hbm_bytes / elapsed_seconds / 1e12
assert logical_tbs > 3.35 and hbm_tbs < 3.35
print(f"hypothetical logical TB/s: {logical_tbs:.2f}")
print(f"hypothetical HBM TB/s: {hbm_tbs:.2f}")
Output
hypothetical logical TB/s: 5.20
hypothetical HBM TB/s: 2.50

Both rates use the same time. They answer different questions because their byte counts differ. Neither arithmetic result proves that a kernel exists, is correct, or achieved these rates.

The refinement feedback loop: hardware-guided iteration

KernelAgent's March 2026 primary description presents a hardware-guided loop for optimizing Triton inference kernels.[9] A similar loop can expose public diagnostics while keeping the final acceptance suite untouched:

Diagram showing Public refinement, Generator, Build & public checks, and Timing & profiler hypotheses.
Public refinement, Generator, Build & public checks, and Timing & profiler hypotheses.

The feedback payload provides three categories of actionable signals:

  1. Compiler reports: Registers, spills, shared memory, and target compatibility suggest experiments. Reducing unrolling may reduce register pressure while also reducing instruction-level parallelism; measure the tradeoff.
  2. Sanitizer reports: Preserve source attribution when available. A hazardous access identifies an observed failure, not necessarily the unique missing barrier or root cause. Without line information, exact source attribution may be unavailable.
  3. Profiler hypotheses: Record the exact available metric names and tool/device version. Current Nsight documentation describes smsp__pcsamp_warps_issue_stalled_* samples, attributed to the waiting consumer instruction:[8]
SignalWhat it establishesNext discriminating check
Long scoreboardDependency on an L1TEX operation, including local/global/texture accessesInspect the producer, cache/sector traffic, and eligible warps; distinguish latency from saturated bandwidth
Short scoreboardDependency on a MIO operation; shared memory, special math, or dynamic branching can contributeIdentify the instruction and inspect shared bank service before proposing padding
BarrierA warp waits for sibling warps at a block barrierInspect work before the barrier and required communication; shuffles alone don't combine multiple warps

A high percentage isn't a command to apply one optimization. Improve the critical path and completed runtime, rather than trying to eliminate every stall category.

Treat logs as untrusted text and keep evaluator permissions separate from model instructions. Redacting raw tensors alone doesn't remove shape, distribution, or repeated-feedback leakage. Predeclare a repair budget suited to the task; five iterations is one possible choice, not a universal security threshold. Freeze source, dispatch, reference, and policy before the final holdout. If its feedback informs another revision, retire it as a holdout and use fresh acceptance evidence.

The promotion evidence ladder: from lab to production infrastructure

Passing laboratory unit tests is necessary, but it doesn't prove that a kernel will run reliably under messy production traffic. The promotion evidence ladder establishes four progressive validation tiers:

Four stages of generated-kernel review: scoped laboratory evidence, resource-budgeted shadow comparison, monitored canary routing, and signed production approval with a healthy-worker recovery plan.
Each stage answers a different question. Shadow outputs stay out of user responses, but shadow work still consumes resources. Disabling future candidate dispatch doesn't undo a failed invocation or repair a poisoned CUDA process.

Tier 1: Lab verification

The tested paths meet the contract, unexplained sanitizer reports are resolved, artifacts and dispatch are inspected, and comparable measurements meet the declared latency/memory budgets. Zero spills isn't a universal acceptance criterion. Record untested paths and restrictions alongside the evidence.

Tier 2: Shadow execution

Use approved, sampled inputs for a budgeted shadow comparison while the trusted path supplies user responses. Prefer separate capacity when fault isolation matters. On the same GPU, a different stream still competes for SMs, memory bandwidth, cache, and allocator capacity; asynchronous execution doesn't guarantee zero latency impact. Respect privacy and input-retention rules when copying production tensors.

An asynchronous worker compares candidate outputs against reference tensors. Shadow execution audits three critical metrics under production traffic:

  • Numerical consistency: Verify that real input distributions (such as varying context lengths and sparsity) don't trigger unexpected precision divergence or NaNs.
  • Memory growth: Track live allocations, reserved/cached memory, host RSS, workspace, and repeated-workload trends separately. A caching allocator's high-water mark or retained virtual address space alone doesn't prove a leak.
  • Compilation/cache cardinality: Track specializations, code/module bytes, compile time, and eviction policy. A new shape doesn't necessarily compile a new artifact; unbounded specialization can create resource pressure.

Tier 3: Canary routing

Route a small fraction of live traffic (e.g., 1% ramping to 5%) directly to the candidate kernel behind a dynamic feature flag. Execution is restricted to tested GPU architectures via dispatch predicates.

During canary routing, automated tripwires monitor three failure indicators:

  • Latency: Monitor request-level latency and sufficient per-invocation samples. A tail regression can arise from queueing, compilation, clocks, resource contention, or kernel work; it doesn't uniquely identify warp serialization.
  • Device/process health: Stop candidate routing on relevant CUDA failures and investigate driver events. Classify faults rather than assuming every hardware report has the same cause or recovery procedure.
  • Downstream quality: Measure operator error and application-specific logit/quality changes. Numerical tolerance doesn't guarantee identical logits or generated tokens, particularly near an argmax tie or under sampling.

Test disabling future dispatch, draining work, worker replacement, and safe retry separately. CUDA 13.4 documents that cudaErrorIllegalAddress leaves the process inconsistent and requires termination/relaunch; calling the reference in that process isn't a recovery path.[10] A feature flag doesn't cancel in-flight work or guarantee millisecond propagation, lossless retries, or repair of corrupted inputs. Preserve request state outside the failed worker when recovery depends on it.

Tier 4: Production gate

Under this example rollout policy, promotion registers the reviewed artifact and dispatch predicate. The service retains a disable path, healthy-worker recovery/reference target, and signed evidence receipt. These controls surround the kernel; the device binary alone doesn't supply them.

Read benchmark scores within their boundary

Research benchmarks measure model capabilities across standardized harnesses, but they don't certify production readiness:

BenchmarkUseful signalBoundary
KernelBenchOriginal paper's 250 workloads; fast_p counts correct candidates exceeding speed threshold p[11]Public task shapes are part of the specification; doesn't certify a general library domain
KernelBench-VerifiedH200, TF32-enabled baseline, four value transformations, and peak allocated-memory tracking[12]Single-turn best-of-five; 247 active tasks after three exclusions; shapes stay fixed, and alternate distributions gate correctness rather than timing
KernelBench-X176 tasks, 15 categories, outlier checks, precision variants, and six-GPU evaluation[13]Its FLOP/byte efficiency values use an idealized task model, not measured executed instructions or HBM traffic
SOL-ExecBench235 B200-targeted problems with SOLAR-derived hardware/fusion bounds[14]SOL score also depends on a fixed software baseline; shape-based bounds miss value-dependent optimizations and need review when violated

This is an October 10, 2026 reading of the cited papers, not a current leaderboard ranking. Compare task coverage, baseline precision, candidate budget, hardware, conditioning on correctness, and aggregation before comparing scores. TF32 may be an appropriate deployed baseline when its numerical policy is acceptable; it isn't an interchangeable exact-FP32 reference. Evaluate your application's domain separately.

The September 2026 preprint Measuring the Checker asks a different question: how many injected kernel defects can an evaluator detect? Its method admits a defect into the scoring denominator only with a concrete input that exposes it, while rejecting proposed inputs that also fail the unmodified implementation.[15] This makes evaluator coverage measurable against a chosen fault model. It doesn't estimate the probability that a generated kernel is correct. The preprint's appendix still flags a mutation-rule count discrepancy; use the methodology and inspect the released definitions before adopting its reported coverage as your own validation.

Preserve a promotion receipt

Bind evidence to source, loaded artifacts, dispatch, reference/policy, toolchain, target, and workload. Here is an unsigned template, not a successful benchmark or production approval. Null fields are missing evidence and block signing:

promotion-receipt.json
{
  "payload": {
    "operator": "weighted_rmsnorm",
    "contract_version": "3",
    "status": "pending_device_validation",
    "candidate_source_sha256": null,
    "loaded_artifact_sha256": null,
    "dispatch": "valid_inputs && same_cuda_device && tested_arch_and_features && x_contiguous && weight_contiguous && H%128==0 && supported_inference_mode",
    "approval_identity": {
      "reviewer": null,
      "signing_key_id": null
    },
    "target": {
      "gpu_uuid": null,
      "architecture_and_features": null,
      "driver": null,
      "compiler_toolkit": null,
      "pytorch_cuda_build": null,
      "pytorch_version": null,
      "compiler_flags": null
    },
    "correctness": {
      "suite_and_policy_sha256": null,
      "reference_sha256": null,
      "status": "not_run",
      "case_results_uri_and_sha256": null
    },
    "sanitizers": {
      "tool_version": null,
      "commands_and_report_hashes": null,
      "status": "not_run"
    },
    "benchmark": {
      "protocol_and_workload_sha256": null,
      "baseline_revision_and_precision": null,
      "timing_and_cache_scope": null,
      "raw_samples_uri_and_sha256": null,
      "status": "not_run"
    },
    "memory": {
      "live_reserved_workspace_and_cache_report": null
    },
    "rollout": {
      "feature_flag_and_scope": null,
      "healthy_worker_recovery_and_reference_target": null,
      "revalidation_policy": null
    }
  },
  "approval": {
    "canonical_payload_sha256": null,
    "signature": null
  }
}

A reviewer checks source/licensing, dispatch, evidence, and the tested disable/recovery plan. Hashes bind bytes; they don't prove a result is truthful. Define the canonical serialization of payload, sign those bytes with an authorized key, and keep the signature/hash in the separate approval envelope. Verify against a trusted key registry and retain immutable linked artifacts. Revalidate when hardware, software, policy, or specialization changes.

Promotion rubric

Promotion uses hard gates before evaluating performance scores:

GatePromote conditionRequired evidence
ContractStable schema, semantics, domain, fallback, and aliasingVersioned operator specification
CorrectnessNo unresolved failures in the declared coverageCase results, tolerance policy, and known coverage limits
SafetyNo unexplained runtime-check reports; isolation/recovery testedScoped sanitizer reports, repeated/concurrent tests, and worker outcomes
ArtifactLoaded artifact belongs to the recorded build of reviewed source; dispatch inspectedBuild provenance, hashes, compiler logs, dependencies, and traces
PerformancePaired improvement on target workload with baseline parityRaw CUDA event samples and hardware records
MemoryPeak, workspace, host, and cache growth remain within budgetAllocator traces and cache cardinality
OperationsReviewer, canary, alarms, owner, disable path, and worker recovery readySigned receipt and tested rollout/recovery plan

No speedup justifies bypassing a safety or contract gate. Once all hard gates pass, teams can rank candidates by latency, maintainability, and resource utilization.

Mastery check

Evaluation rubric

  • Foundational: Distinguish between generated source code, benchmark pass rates, and production promotion criteria.
  • Intermediate: Design a multi-scale hidden test suite, Compute Sanitizer verification plan, and Roofline sanity check for a GPU operator.
  • Advanced: Turn profiler symptoms into discriminating experiments and design rollout recovery that survives a failed CUDA process.

Diagnose a misleading pass

SymptomHypothesis to testNext check
Random values pass, but a sliced view failsTest varied values but left memory layouts contiguousAdd stride, offset, and non-contiguous view tests
Output values match, but caller's next layer corruptsKernel mutated input tensors or aliased storageVerify input byte hashes before and after execution
Effective logical bandwidth exceeds HBM peakReuse/cache changes the physical byte count; timing or computation may also be wrongCheck correctness, completed timing, and independently profiled traffic
Fixed-shape latency is excellent, but host RAM growsCompilation cache, retained objects, or allocator high-water behaviorMeasure live objects/cache cardinality and repeat a fixed workload
Kernel runs on one GPU and fails on anotherTarget compatibility, resources, driver, or an untested code pathInspect errors and artifact/dispatch target before diagnosing
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A generated weighted RMSNorm kernel passes a contiguous [2, 4096] smoke test and reports a 1.4x speedup. Which missing check most directly blocks correctness promotion?
2.A candidate is fast on a small cached shape but slow on large inputs. Which next check best tests the hypothesis that its address mapping is inefficient?
3.A generated kernel supports only contiguous memory where H is divisible by 128. How must the production wrapper handle non-conforming inputs?
4.What does a budgeted shadow comparison establish before canary routing?
5.Logical bytes divided by completed kernel time give 5.2 TB/s on an H100 SXM with 3.35 TB/s peak HBM. Which conclusion is justified?
6.Which NVIDIA Compute Sanitizer tool detects shared memory Read-After-Write (RAW) data races caused by missing thread barriers?
7.Why might an evaluator use a sufficiently large rotating buffer set for an intended streaming workload?
8.An Nsight Compute profile shows many short-scoreboard samples. What should the reviewer do before recommending shared-memory padding?
9.What is the primary risk of providing full compiler error traces and hidden test outputs directly to an LLM repair agent across many iterations?
10.All laboratory and canary gates pass for an AI-generated kernel. What final artifact makes production adoption operational, accountable, and reversible?

10 questions remaining.

References

The Custom Operators Manual

PyTorch Contributors · 2026 · Official PyTorch documentation

https://docs.pytorch.org/docs/main/library.html

NVIDIA CUDA Compiler Driver NVCC

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/

CUDA Binary Utilities

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-binary-utilities/

torch.testing.assert_close

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/2.14/testing.html#torch.testing.assert_close

Compute Sanitizer

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/compute-sanitizer/ComputeSanitizer/

CUDA semantics

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/2.14/notes/cuda.html

PyTorch Benchmark

PyTorch Contributors · 2026 · Official tutorial

https://docs.pytorch.org/tutorials/recipes/recipes/benchmark.html

Nsight Compute Profiling Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-compute/ProfilingGuide/

KernelAgent: Hardware-Guided GPU Kernel Optimization via Multi-Agent Orchestration

Cheng, K., Wang, L., Khuu, J., Saroufim, M., Chi, W., Wang, J., & Isaacson, J. · 2026 · PyTorch Blog

https://pytorch.org/blog/kernelagent-hardware-guided-gpu-kernel-optimization-via-multi-agent-orchestration/

CUDA Runtime API Error Types

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/cuda/cuda-runtime-api/cuda_runtime_api/group__CUDART__TYPES.html

KernelBench: Can LLMs Write Efficient GPU Kernels?

Ouyang, A., Guo, S., Arora, S., Zhang, A. L., Hu, W., Ré, C., & Mirhoseini, A. · 2025 · arXiv

https://arxiv.org/abs/2502.10517

KernelBench-Verified: Do LLM-Generated Kernels Actually Beat PyTorch?

Zhang, Y., Yu, P., Wang, J., Fan, M., Reed, J., Mirhoseini, A., & Su, W. · 2026 · arXiv

https://arxiv.org/abs/2607.16241

KernelBench-X: A Comprehensive Benchmark for Evaluating LLM-Generated GPU Kernels

Wang, H., Zhang, J., Jiang, K., Wang, H., Chen, J., & Zhu, J. · 2026 · arXiv

https://arxiv.org/abs/2605.04956

SOL-ExecBench: Speed-of-Light Benchmarking for Real-World GPU Kernels Against Hardware Limits

Lin, E., Modi, S., Hari, S. K. S., Huang, Q., Ye, Z., Qin, N., Zhou, F., Zhang, Y., Wang, J., Damani, S., Peri, D., Xie, O., Kane, A., Maor, M., Behar, M., Cao, T., Mehta, R., Singh, V., Mailthody, V. S., Chen, T., Ye, Z., Chen, H., Chen, T., Grover, V., Chen, W., Liu, W., Chung, E., Ceze, L., Bringmann, R., Zeller, C., Lightstone, M., Kozyrakis, C., & Shi, H. · 2026 · arXiv

https://arxiv.org/abs/2603.19173

Measuring the Checker: Mutation Analysis for GPU-Kernel Benchmark Oracles

Du, M., et al. · 2026 · arXiv preprint

https://arxiv.org/abs/2609.22220

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.