Meganeura has two complementary profiling outputs:
- Perfetto traces show CPU spans, submissions, waits, and GPU passes on a timeline.
- Structured session profiles retain repeated hardware timestamp samples and attach each dispatch to its selected pipeline, workgroup geometry, execution phase, and coarse kernel family.
The structured profile is the default tool for explaining a benchmark gap. It is reproducible across Vulkan and Metal, can be collected by Inferena after the ordinary benchmark without changing its reported latency, and produces JSON that can be compared across revisions. RenderDoc and vendor-specific capture tools remain useful for a single unexplained shader, but are not part of the benchmark protocol.
Run the small self-contained example:
MEGANEURA_GPU_TIMING=1 \
cargo run --release --example profile_session -- gap-profile.jsonThe example first measures the ordinary grouped-pass execution, then collects five instrumented samples. The JSON records both times and their ratio.
For paper workloads, use Inferena's profile option so the profile and normal measurement share the exact graph, inputs, precision policy, and revision:
INFERENA_MEGANEURA_PATH=../meganeura \
./run.sh --frameworks meganeura --model Whisper-tiny \
--profile --profile-samples 5 --results-dir results/gap-studyInferena writes one sidecar per execution mode under
results/gap-study/profiles/.
Set MEGANEURA_GPU_TIMING=1 before constructing the first GPU context. Blade
then allocates hardware timestamp queries. During structured capture,
Meganeura deliberately records one compute pass per timed plan dispatch — see
"Plans larger than the timestamp budget" below for what happens to the rest.
Once the
completion fence signals, Blade resolves every pass start and the final
completion timestamp directly onto the process monotonic clock; no follow-up
execution is needed.
A dispatch interval starts at its calibrated pass-start timestamp and ends at the next pass start, or at the submission completion timestamp for the final pass. It therefore includes work and any inter-pass queue gap before the next timestamp. This is appropriate for ranking end-to-end dispatch costs, but it is not an instruction-level kernel metric.
One-pass-per-dispatch execution is more intrusive than normal execution, which groups dispatches and uses inline barriers. Never substitute the profiled wall time for benchmark latency. The artifact includes:
- the normal benchmark median supplied by the caller;
- every profiled wall-time and timestamped-GPU sample;
- the instrumentation wall-time ratio;
- per-dispatch median, quartiles, and raw samples;
- phase and family aggregates;
- pipeline variants and driver-reported executable statistics, when available;
- device, driver, plan, barrier, and memory metadata.
The family shares use the sum of each dispatch's median so they add to 100%. The separate median of each family's per-run total is retained as well.
Call meganeura::profiler::capture_session_profile after the normal benchmark
and pass the normal median through CaptureOptions::unprofiled_median_ms.
The input-preparation closure runs once before each replay.
The collector describes dispatches in the compiled execution plan. Disable optimizer, gradient-accumulation, and gradient-clipping passes before capture; those passes are appended by the runtime and do not have plan metadata. The collector rejects a timestamp-count mismatch instead of silently assigning an auxiliary pass to the wrong shader.
Blade writes at most limits::PASS_COUNT timestamps per submission and
silently skips the rest, so no single replay can measure a larger plan — and a
decode or training step of a real model runs well past that.
The collector therefore splits such a plan into windows of dispatch indices
and replays the session once per window. The window's dispatches each get
their own timestamped pass; everything outside it still executes, but batched
into one grouped pass on either side, which keeps the plan's barriers while
costing only two of the timestamp slots however many dispatches are out there.
The preparation closure runs before every replay, so each window observes the
same execution, and the per-dispatch results are stitched back together. Every
dispatch is still measured samples times; only the number of replays grows.
measurement.window_count reports how many replays one sample took and is 1
for a plan that fits, in which case capture behaves exactly as it did before.
CaptureOptions::max_timed_passes_per_replay lowers the budget below Blade's
limit, cutting the plan into more and smaller windows so that each replay
perturbs the schedule less.
When window_count exceeds 1, each profiled_wall_samples_ms entry is the sum
of all replay wall times needed for that sample. Consequently,
instrumentation_wall_ratio reports total capture cost rather than the
perturbation of one execution. gpu_total_samples_ms likewise sums a sample's
windows, so both aggregates still describe one complete profile sample.
Start with the largest family and dispatch deltas across GPUs or revisions. Use driver pipeline statistics to check register pressure and spilling, then inspect the generated WGSL/SPIR-V for the small set of suspect pipelines.
Use RenderDoc only when resource bindings, barriers, generated shader state, or a driver-specific anomaly needs visual inspection. For instruction throughput, occupancy, cache behavior, or tensor-core utilization, use the vendor tool for the affected platform (for example Nsight, Radeon GPU Profiler, or Xcode GPU capture). Those captures are supporting forensic evidence, not portable benchmark artifacts.
examples/profile_training.rs provides a fixed, synthetic F+L+B roster with
normal timing blocks before and after capture, full profiled-result comparisons
before the next profiled execution, exact dispatch contracts, provenance and
telemetry. It enables timestamp queries through GpuOptions, with no environment
variable required. The retained protocol and results
separate localization from optimizer-backed timing and candidate acceptance.
Those records show why both controls matter: all 45 profiled full states match bitwise, but short normal timing blocks can still drift badly. Before/after normal samples are not interleaved candidate comparisons. Timing-enabled contexts also do not reproduce the profiler-disabled production configuration.
The current phase label is the scheduled prefix/suffix around the last loss
dispatch. Independent gradient seeds can be hoisted into the prefix. Use shader
direction, requires_full_precision and node origins for semantic attribution;
do not assume everything labeled forward belongs to the undifferentiated graph.