netcl.profiling — Timing and Autotuner
netcl.profiling — Timing and Autotuner
The netcl.profiling package is the performance-measurement layer of netcl. It
provides two families of tools: timing helpers for measuring wall-clock latency of code
sections and kernel launches, and an autotuner that benchmarks multiple kernel
implementations to pick the fastest one for the active device.
Long-form imports.
netcl/profiling/__init__.pyis empty. Every symbol must be imported from its submodule:
python from netcl.profiling.timing import EventTimer, ProfileRecord from netcl.profiling.autotuner import ( KernelProfiler, AutoTuner, TuningConfig, TuningResult, Timer, ProfileContext, get_autotuner, autotune_matmul, autotune_conv2d, )
Symbol Table
| Symbol | Path | Purpose |
|---|---|---|
ProfileRecord |
profiling/timing.py |
Dataclass holding name and duration_ms for one timed event |
EventTimer |
profiling/timing.py |
Wraps a cl.Event and extracts its GPU-side elapsed time |
KernelProfiler |
profiling/autotuner.py |
Times a kernel function over warmup + timed runs, returns median ms |
AutoTuner |
profiling/autotuner.py |
Benchmarks multiple kernel variants and caches the fastest |
TuningConfig |
profiling/autotuner.py |
Config dataclass for AutoTuner (warmup, runs, cache dir) |
TuningResult |
profiling/autotuner.py |
Result dataclass: kernel_name, time_ms, gflops, params |
get_autotuner |
profiling/autotuner.py |
Returns the global AutoTuner singleton |
autotune_matmul |
profiling/autotuner.py |
Convenience: tune matmul for given dimensions, return kernel name |
autotune_conv2d |
profiling/autotuner.py |
Convenience: tune conv2d for given dimensions, return kernel name |
Timer |
profiling/autotuner.py |
with block that measures wall-clock time; .elapsed_ms after exit |
ProfileContext |
profiling/autotuner.py |
Multi-run with block; accumulates timings and provides statistics |
EventTimer (profiling/timing.py)
EventTimer extracts GPU-side elapsed time from an OpenCL event that was recorded with
profiling enabled.
from netcl.profiling.timing import EventTimer, ProfileRecord
# The queue must be created with cl.command_queue_properties.PROFILING_ENABLE.
timer = EventTimer(queue)
# Run a kernel and get the cl.Event back:
evt = kernel(queue, global_size, local_size, *args)
record: ProfileRecord = timer.time_event(evt, name="my_kernel")
print(f"{record.name}: {record.duration_ms:.3f} ms")
EventTimer(queue=None)
| Parameter | Type | Purpose |
|---|---|---|
queue |
cl.CommandQueue \| None |
Optional; stored for reference but not used in timing |
EventTimer.time_event(event, name="")
| Parameter | Type | Purpose |
|---|---|---|
event |
cl.Event |
A completed or in-flight event; time_event calls event.wait() first |
name |
str |
Label stored in the returned ProfileRecord |
Returns a ProfileRecord(name, duration_ms). The duration is (event.profile.end - event.profile.start) * 1e-6 — real GPU-side nanosecond timestamps converted to milliseconds.
The queue must have been created with cl.command_queue_properties.PROFILING_ENABLE;
otherwise the event.profile attribute will raise.
ProfileRecord
@dataclass
class ProfileRecord:
name: str
duration_ms: float
KernelProfiler (profiling/autotuner.py)
KernelProfiler measures the wall-clock time of a Python callable that launches one or
more OpenCL kernels, with warmup discarding and a configurable number of timed runs.
Returns the median time in milliseconds.
from netcl.profiling.autotuner import KernelProfiler
profiler = KernelProfiler(queue)
def my_kernel():
return kernel(queue, global_size, local_size, *args)
median_ms = profiler.time_kernel(my_kernel, warmup=3, runs=5)
print(f"median: {median_ms:.3f} ms")
KernelProfiler(queue)
KernelProfiler.time_kernel(kernel_fn, *args, warmup=3, runs=5, **kwargs)
| Parameter | Default | Purpose |
|---|---|---|
kernel_fn |
— | Callable to time; called as kernel_fn(*args, **kwargs) each run |
*args |
— | Positional arguments forwarded to kernel_fn |
warmup |
3 |
Number of un-timed warmup calls |
runs |
5 |
Number of timed calls; calls queue.finish() after each |
**kwargs |
{} |
Keyword arguments forwarded to kernel_fn |
Returns float — the median wall-clock time in milliseconds across runs timed calls.
There is also time_kernel_with_events(kernel, queue, global_size, local_size, *kernel_args, warmup, runs) for when you have a raw cl.Kernel object and want OpenCL-event-based timing instead of wall-clock.
AutoTuner (profiling/autotuner.py)
AutoTuner benchmarks multiple kernel implementations for a given problem shape and
picks the fastest. Results are cached in memory and optionally persisted to disk so
subsequent runs skip re-benchmarking.
from netcl.profiling.autotuner import AutoTuner, TuningConfig, TuningResult
config = TuningConfig(
warmup_runs=3,
benchmark_runs=5,
cache_dir="/tmp/netcl_autotune",
enable_cache=True,
)
tuner = AutoTuner(config)
result: TuningResult = tuner.tune_matmul(M=512, N=512, K=512, dtype="float32", queue=queue)
print(f"best kernel: {result.kernel_name}, {result.time_ms:.2f} ms, {result.gflops:.1f} GFLOPS")
TuningConfig
| Field | Default | Purpose |
|---|---|---|
warmup_runs |
3 |
Warmup runs per candidate kernel |
benchmark_runs |
5 |
Timed runs per candidate kernel |
min_time_ms |
0.01 |
Minimum plausible time (sanity filter) |
cache_dir |
None |
Directory for the on-disk JSON cache; None = no disk cache |
enable_cache |
True |
Whether to use the disk cache |
TuningResult
@dataclass
class TuningResult:
kernel_name: str # e.g. "tiled_16", "winograd", "naive"
time_ms: float # median benchmark time
gflops: float | None # estimated throughput (None if not computed)
params: dict # extra params, e.g. {"tile_size": 16}
AutoTuner(config=None)
If config is None, a default TuningConfig() is used (no disk cache).
AutoTuner.tune_matmul(M, N, K, dtype, queue)
Benchmarks naive matmul and tiled matmul with tile sizes [8, 16, 32]. Returns the
TuningResult for the fastest variant. Result is cached in memory (and on disk if
cache_dir is set).
| Parameter | Type | Purpose |
|---|---|---|
M, N, K |
int |
Matrix dimensions |
dtype |
str |
"float32" etc. |
queue |
cl.CommandQueue |
Target device queue |
AutoTuner.tune_conv2d(N, C, H, W, F, KH, KW, stride, pad, dtype, queue)
Benchmarks naive conv2d, implicit GEMM (for 3×3 stride=1), tiled local (tile sizes
4 and 8), and Winograd F(2,3) (for 3×3 stride=1). Returns the fastest TuningResult.
| Parameter | Type | Purpose |
|---|---|---|
N, C, H, W |
int |
Input batch, channels, height, width |
F, KH, KW |
int |
Filter count and kernel size |
stride, pad |
int |
Convolution stride and padding |
dtype |
str |
"float32" etc. |
queue |
cl.CommandQueue |
Target device queue |
AutoTuner.get_best_kernel(cache_key)
Returns the cached TuningResult for a given key, or None if not cached.
AutoTuner.clear_cache()
Clears the in-memory cache.
Global Singleton (get_autotuner)
from netcl.profiling.autotuner import get_autotuner
tuner = get_autotuner() # creates singleton on first call
result = tuner.tune_matmul(512, 512, 512, "float32", queue)
The global AutoTuner is configured from environment variables:
| Variable | Default | Purpose |
|---|---|---|
NETCL_AUTOTUNE_CACHE |
None |
Disk cache directory; if unset, no cache |
NETCL_AUTOTUNE_WARMUP |
3 |
TuningConfig.warmup_runs |
NETCL_AUTOTUNE_RUNS |
5 |
TuningConfig.benchmark_runs |
Convenience Functions
autotune_matmul(M, N, K, dtype, queue) -> str
Calls get_autotuner().tune_matmul(...) and returns just the winning kernel name as a
string (e.g. "tiled_16" or "naive").
from netcl.profiling.autotuner import autotune_matmul
kernel_name = autotune_matmul(512, 512, 512, "float32", queue)
print(kernel_name) # e.g. "tiled_16"
autotune_conv2d(N, C, H, W, F, KH, KW, stride, pad, dtype, queue) -> str
Calls get_autotuner().tune_conv2d(...) and returns just the winning kernel name.
from netcl.profiling.autotuner import autotune_conv2d
kernel_name = autotune_conv2d(1, 3, 224, 224, 64, 3, 3, 1, 1, "float32", queue)
print(kernel_name) # e.g. "winograd" or "implicit_gemm"
Timer
Timer is a simple with block for measuring wall-clock time. Use it for quick
one-off measurements where you don't need warmup or multi-run statistics.
from netcl.profiling.autotuner import Timer
with Timer("forward pass") as t:
logits = model(x)
queue.finish()
print(t) # "forward pass: 12.345 ms"
print(t.elapsed_ms) # 12.345
| Property | Type | Purpose |
|---|---|---|
elapsed_ms |
float |
Wall-clock time in milliseconds; valid after __exit__ |
str(timer) returns "<name>: <elapsed_ms>.3f ms" (or just the number if name is empty).
ProfileContext
ProfileContext is a reusable with block that accumulates timings across multiple
runs and provides summary statistics.
from netcl.profiling.autotuner import ProfileContext
ctx = ProfileContext("training step")
for _ in range(20):
with ctx:
train_step()
queue.finish()
print(ctx.summary())
# "training step: mean=15.123ms, median=14.987ms, min=14.811ms, max=16.234ms, runs=20"
| Property | Type | Purpose |
|---|---|---|
mean_ms |
float |
Mean elapsed time in ms across all runs |
median_ms |
float |
Median elapsed time in ms |
min_ms |
float |
Minimum elapsed time in ms |
max_ms |
float |
Maximum elapsed time in ms |
times |
List[float] |
Raw per-run timings in ms |
| Method | Signature | Purpose |
|---|---|---|
summary |
() -> str |
One-line summary of all stats |
See also
- runtime API —
ProgramCache,Graph,KernelCapture,Scheduler - ops API — the elementary ops that
KernelProfilerandAutoTunerbenchmark - Quickstart — how to get a
queueto pass to these helpers