Profiling never changes generated MSL, and every API is a no-op when no profiler is active — the disabled path allocates nothing.
Quick start
How it works
enigma.profile()installs aProfilerin a contextvar. The runtime checks it once per call; when empty, dispatch takes the untimed fast path.enigma.scope(name, metrics=...)pushesnameonto a contextvar scope stack and records one event on exit. Scopes nest; the stack is the call path.- When a profiler is active, dispatch goes through the Swift runtime’s timed path, which reads Metal command-buffer GPU timestamps — so
gpu_time_usis GPU time, not CPU wall clock. Profiler.add_eventstamps the current scope stack onto each event (call_path) and runs any registered kernel hook to attach metrics.- Aggregation (
key_averages), the call tree (tree), and the exporters all read the same event list.
Dispatch metadata
Profiledgpu_dispatch events carry extra metadata (visible in events() and Chrome traces):
One-shot vs prepared dispatch
execute() is one-shot: its table includes library load, pipeline creation, buffer creation, readback, and release. PreparedKernel.dispatch() reuses all of that.
Scopes, metrics, derived throughput
Attach semantic work counts to a scope and the profiler derives throughput from measured GPU time:flops and bytes drive the derivation; any other keys pass through to exports untouched.
Kernel hooks
Register the work formula once; every profiled dispatch of that kernel gets metrics automatically:Call-path analysis
The same kernel called from different contexts stays distinguishable:Unbiased benchmarking on unified memory
Apple Silicon shares one physical memory between CPU and GPU. That removes explicit transfers but creates two biases:- Cold-start bias — the first dispatches pay pipeline creation, driver work, and page-residency costs. Timing them overstates kernel cost.
- Warm-cache bias — after a few iterations the working set is resident in the shared cache, so tight repeat loops on small buffers report bandwidth no cold-data workload will see. If you are measuring DRAM bandwidth, size the working set well beyond the chip’s last-level cache.
enigma.benchmark_kernel is built around these constraints:
- Times come from Metal GPU timestamps only — no profiler events, no Python timing inside the measured region.
- Warmup runs on the untimed fast path.
- The full distribution is reported. Compare kernels by median (robust to thermal/scheduler outliers);
minis best-case;p90exposes tail variance.
Use
benchmark_kernel to compare kernels; use profile_kernel / enigma.profile() to understand where time goes.Xcode GPU capture (.gputrace)
In-shader clock intrinsics do not exist on Apple Silicon, so per-instruction timing is not available from Python. For intra-kernel analysis (instruction mix, memory traffic, occupancy timelines), hand the profiled region to Xcode’s GPU debugger instead:
- The path must end in
.gputrace(ValueErrorotherwise). - Capture starts when the
profile()context enters and stops when it exits; everything dispatched inside is in the trace. - Without
MTL_CAPTURE_ENABLED=1(or before anyMetalRuntimeexists) entering the context raisesRuntimeError— capture never fails silently.
