netcl wiki
api

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__.py is 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 APIProgramCache, Graph, KernelCapture, Scheduler
  • ops API — the elementary ops that KernelProfiler and AutoTuner benchmark
  • Quickstart — how to get a queue to pass to these helpers