This document is relevant for: Inf2, Trn1, Trn2, Trn3
Tutorial: Validate, benchmark, and trace a kernel with nrtpy#
This topic guides you through using nrtpy to validate kernel correctness
against a NumPy reference, measure device-side execution latency, and capture
execution traces for performance analysis. When you have completed it, you will
have a repeatable test harness for iterating on NKI kernel quality.
Overview#
Once you have a compiled NEFF that executes without errors, the next steps are:
Validate correctness — compare kernel output against a known-good reference to catch numerical bugs.
Benchmark performance — measure NeuronCore execution time to establish a baseline and detect regressions.
Capture traces — save execution traces for profiling tools to analyze instruction scheduling, DMA overlap, and pipeline utilization.
This tutorial builds on Tutorial: Test and debug a NKI kernel end-to-end with nrtpy. You should have a working NEFF before starting.
Before you start#
This tutorial assumes that you have:
Completed Tutorial: Test and debug a NKI kernel end-to-end with nrtpy (or have a known-good NEFF).
A working nrtpy environment (see Get started with nrtpy).
Before you begin, complete the Get started with nrtpy setup. This tutorial uses the addition kernel NEFF from Tutorial: Test and debug a NKI kernel end-to-end with nrtpy. If you do not have it, any working NEFF will do — adjust tensor names, shapes, and the reference function accordingly.
Note
Auto-allocated output tensors may have a void dtype (for example,
|V2 for fp16) because libnrt does not always report the original
dtype. Use .view(np.float16) or .view(np.float32) on the NumPy
result to reinterpret the raw bytes into the correct type.
Step 1: Load the model and prepare test inputs#
In this step, you will load the NEFF and create multiple test input patterns to exercise different code paths in the kernel.
import numpy as np
from nrtpy import NrtpyModel, NrtpyTensor
model = NrtpyModel.load_from_neff("./add_neff/kernel.neff")
# Discover expected interface
print("Inputs:", list(model.input_tensors_info.keys()))
print("Outputs:", list(model.output_tensors_info.keys()))
# Prepare diverse test inputs (addition kernel: a + b)
test_cases = [
("ones+ones", np.ones((4, 3), dtype=np.float16), np.ones((4, 3), dtype=np.float16)),
("zeros+zeros", np.zeros((4, 3), dtype=np.float16), np.zeros((4, 3), dtype=np.float16)),
("neg+pos", np.full((4, 3), -3.0, dtype=np.float16), np.full((4, 3), 5.0, dtype=np.float16)),
("random", np.random.randn(4, 3).astype(np.float16), np.random.randn(4, 3).astype(np.float16)),
("large", np.full((4, 3), 1000.0, dtype=np.float16), np.full((4, 3), 2000.0, dtype=np.float16)),
]
Step 2: Validate correctness against NumPy#
In this step, you will execute the kernel for each test case and compare against a NumPy reference implementation.
def numpy_reference(a, b):
"""Reference implementation: element-wise addition."""
return a + b
passed = 0
failed = 0
for name, a_data, b_data in test_cases:
a_tensor = NrtpyTensor.from_numpy(a_data, name="a_input")
b_tensor = NrtpyTensor.from_numpy(b_data, name="b_input")
outputs = model(inputs={"a_input": a_tensor, "b_input": b_tensor})
# Reinterpret void dtype to float16
result = list(outputs.values())[0].numpy().view(np.float16)
expected = numpy_reference(a_data, b_data)
try:
np.testing.assert_allclose(
result.astype(np.float32), expected.astype(np.float32),
rtol=1e-3, atol=1e-3
)
print(f" PASS: {name}")
passed += 1
except AssertionError as e:
print(f" FAIL: {name}")
print(f" {e}")
failed += 1
print(f"\nResults: {passed} passed, {failed} failed")
Tip
For kernels with floating-point accumulation (matmul, reductions), use
looser tolerances (rtol=1e-2) and test with inputs that stress
precision: denormals, values near overflow, and mixed-sign inputs.
Step 3: Benchmark device-side latency#
In this step, you will measure the kernel’s NeuronCore execution time using device-side hardware tracing.
a_tensor = NrtpyTensor.from_numpy(np.ones((4, 3), dtype=np.float16), name="a_input")
b_tensor = NrtpyTensor.from_numpy(np.ones((4, 3), dtype=np.float16), name="b_input")
# Device-side timing: measures only NeuronCore execution
device_stats = model.benchmark(
inputs={"a_input": a_tensor, "b_input": b_tensor},
warmup_iter=10,
benchmark_iter=100,
mode="device",
)
print("Device-side benchmark (NeuronCore execution only):")
print(f" Mean: {device_stats.mean_ms:.4f} ms")
print(f" Min: {device_stats.min_ms:.4f} ms")
print(f" Max: {device_stats.max_ms:.4f} ms")
print(f" Std dev: {device_stats.std_dev_ms:.4f} ms")
print(f" Iterations: {device_stats.iterations}")
# Host-side timing: includes host-device round-trip overhead
host_stats = model.benchmark(
inputs={"a_input": a_tensor, "b_input": b_tensor},
warmup_iter=10,
benchmark_iter=100,
mode="host",
)
print(f"\nHost-side benchmark (total round-trip):")
print(f" Mean: {host_stats.mean_ms:.4f} ms")
print(f" Min: {host_stats.min_ms:.4f} ms")
print(f" Overhead vs device: {host_stats.mean_ms - device_stats.mean_ms:.4f} ms")
When to use each mode:
device — Use for kernel optimization. Measures only the NeuronCore compute and DMA time. Most accurate for comparing kernel variants.
host — Use when total latency matters (for example, in a serving path). Includes host-device communication overhead.
Step 4: Detect performance regressions#
In this step, you will save a baseline and compare against it after kernel changes.
import json
# Save baseline
baseline = {
"mean_ms": device_stats.mean_ms,
"min_ms": device_stats.min_ms,
"max_ms": device_stats.max_ms,
"std_dev_ms": device_stats.std_dev_ms,
}
with open("baseline_perf.json", "w") as f:
json.dump(baseline, f, indent=2)
print(f"Baseline saved: mean={baseline['mean_ms']:.4f} ms")
# Later, after a kernel change: compare against baseline
with open("baseline_perf.json") as f:
baseline = json.load(f)
new_stats = model.benchmark(
inputs={"a_input": a_tensor, "b_input": b_tensor},
warmup_iter=10,
benchmark_iter=100,
mode="device",
)
regression_threshold = 1.10 # 10% slower = regression
ratio = new_stats.mean_ms / baseline["mean_ms"]
if ratio > regression_threshold:
print(f"REGRESSION: {ratio:.2f}x slower ({new_stats.mean_ms:.4f} ms "
f"vs baseline {baseline['mean_ms']:.4f} ms)")
else:
print(f"OK: {ratio:.2f}x of baseline ({new_stats.mean_ms:.4f} ms)")
Step 5: Capture an execution trace#
In this step, you will save a .ntff execution trace file that can be
analyzed with Neuron profiling tools.
# Execute with trace capture
model(
inputs={"a_input": a_tensor, "b_input": b_tensor},
save_trace=True,
ntff_name="./kernel_trace.ntff",
)
print("Trace saved to ./kernel_trace.ntff")
The .ntff file captures device-side execution events including:
DMA transfer start/end times
Compute engine utilization
Instruction scheduling on each engine
Use this trace when:
Device-side benchmark shows unexpected latency and you need to identify the source of the bottleneck.
You want to verify that DMA and compute overlap as intended in your kernel.
You are comparing two kernel implementations and need instruction-level timing differences.
All complete! Now, let’s confirm everything works.
Confirmation#
You should have:
Validated correctness across multiple input patterns with no failures.
Established a device-side latency baseline.
A saved
.ntfftrace file for profiling.
Results: 5 passed, 0 failed
Device-side benchmark (NeuronCore execution only):
Mean: 0.0032 ms
Min: 0.0030 ms
Max: 0.0041 ms
Std dev: 0.0002 ms
Iterations: 100
Trace saved to ./kernel_trace.ntff
Congratulations! You now have a repeatable validation and benchmarking harness for your NKI kernels. If you encounter issues, see nrtpy troubleshooting.
Clean up#
rm -f baseline_perf.json kernel_trace.ntff
Next steps#
nrtpy model API — full
NrtpyModelAPI documentation.nrtpy tensor API — advanced tensor operations including multi-core allocation and in-place writes.
Tutorial: Test and debug a NKI kernel end-to-end with nrtpy — debug runtime errors from faulty kernels.
Further reading#
nrtpy (Neuron Runtime Python) — nrtpy overview and architecture.
This document is relevant for: Inf2, Trn1, Trn2, Trn3