Performance profiling¶
OpenPFC records per-step frames in memory during a run and performs a single MPI collective export at the end (no per-step disk I/O). The API lives under include/openpfc/kernel/profiling/; the JSON-driven pfc::ui::App reads a profiling configuration block.
Configuration (App JSON / TOML)¶
"profiling": {
"enabled": true,
"format": "json",
"output": "my_run_profile",
"memory_samples": false,
"print_report": false,
"regions": ["custom/kernel", "custom/solver"],
"run_id": "",
"export_metadata": {}
}
Field |
Meaning |
|---|---|
enabled |
If true, allocate a |
format |
|
output |
Base path without extension. |
memory_samples |
If true, each frame stores RSS ( |
print_report |
If true, print a TimerOutputs.jl-style table after file export. |
regions |
Optional array of extra |
run_id |
If non-empty, profiling export uses schema v3 (namespaced HDF5/JSON under |
export_metadata |
Optional object merged into export metadata (HDF5 attributes on the run group, JSON |
Default catalog paths (always present): communication, fft, gradient.
Avoid extra region names that collide with reserved HDF5 names under /openpfc/profiling/ (frame_scalars, frame_metric_names) so file creation does not fail.
HDF5 export requires configuring CMake with OpenPFC_ENABLE_HDF5=ON and a system HDF5 installation.
Tungsten CPU (spectral pipelines)¶
The CPU Tungsten model (apps/tungsten/include/tungsten/cpu/tungsten_model.hpp) uses OPENPFC_PROFILE with nested paths:
gradient/mean_field— mean-field filter:gradient/mean_field/forward,gradient/mean_field/multiply,gradient/mean_field/backwardgradient/evolve— exponential integration step:gradient/evolve/forward,gradient/evolve/multiply,gradient/evolve/backward
List these under profiling.regions in the input file if you want a fixed catalog from step one. format values hdf5 and both require OpenPFC_ENABLE_HDF5=ON at build time.
What each frame stores (export schema)¶
After gather on rank 0, each frame has:
scalars: array ofdoublevalues, in the same order as rootframe_metric_names(see below). Semantics:
Index |
Name ( |
Description |
|---|---|---|
0 |
step |
Simulation step index ( |
1 |
mpi_rank |
MPI rank of the row. |
2 |
wall_step |
|
3 |
rss_bytes |
Process RSS if |
4 |
model_heap_bytes / 5 heap_secondary_bytes |
Allocator-reported bytes (OpenPFC: model vs second bucket, e.g. FFT workspace) if |
regions: nested JSON object tree mirroring/-separated paths inregion_paths, each node holdinginclusiveandexclusiveseconds (same semantics as before).
Inclusive time for a path is the wall time of that region (nested scopes included). Exclusive time is inclusive minus the sum of children’s inclusive times (TimerOutputs-style nesting). Manual record_time(path, dt) adds dt to both inclusive and exclusive for that path.
wall_step: set explicitly (e.g. barrier duration from measure_barriered) or via set_frame_metric_elapsed_since_begin("wall_step") after begin_frame. OpenPFC App uses openpfc_begin_frame_with_step_and_rank and openpfc_end_frame_step_wall_and_memory from openpfc_frame_metrics.hpp.
Region times: use ProfilingTimedScope, add_recorded_time (additive), or assign_recorded_time(path, seconds) (overwrite scratch for that path). App records the FFT meter with assign_recorded_time("fft", fft_seconds) after each step (path must exist in the catalog; default with_defaults_and_extras includes fft). Tests may use openpfc_end_frame_with_fft_region_wall_and_memory to set the fft region time and default scalars in one call.
communication: halo wait paths call record_time when a profiling context is active during step().
Interpretation: wall_step includes all work in step(). Compare with the sum of region times only where regions are non-overlapping or use exclusive times for nested hierarchies.
Runtime API¶
ProfilingMetricCatalog— immutable ordered path list;with_defaults_and_extras(extra_paths)orfrom_paths_only(paths)(no built-in regions).ProfilingSession(catalog, frame_metric_names)— second argument is the ordered list of per-frame scalar names (use{}for region timers only).set_frame_metric_elapsed_since_begin(name)fills a metric with elapsed time sincebegin_frame. OpenPFCAppuses helpers inopenpfc_frame_metrics.hpp(openpfc_begin_frame_with_step_and_rank,openpfc_end_frame_step_wall_and_memory, etc.) andProfilingSession::openpfc_default_frame_metrics()for the default name list.ProfilingContextScope— RAII: set the thread-local active session for the duration ofstep()so low-level code can callrecord_timewithout passing pointers everywhere.record_time(std::string_view path, double seconds)— add elapsed time to a catalog path (no-op if no session or unknown path).assign_recorded_time(path, seconds)on the session — set inclusive/exclusive for that path for the current frame (authoritative meter values, etc.).ProfilingTimedScope— RAII nested timer usingsteady_clock; push/pop stack on the active session for inclusive/exclusive accounting. Paths are auto-registered in the catalog on first use (ensure_path); all MPI ranks must use the same path strings.ProfilingManualScope— same stack asProfilingTimedScope, butstop()ends the interval before destruction,restart(path)stops and starts a new region (reuse one variable without nested braces), optional default construct +start(path). Still LIFO with other scopes.OPENPFC_PROFILE("path") { … }— macro (unique local name via__LINE__/__COUNTER__) wrappingProfilingTimedScope.PFC_PROFILE_SCOPE("path")is the single-statement form.print_profiling_timer(std::ostream &, const ProfilingSession &, const ProfilingPrintOptions &)— aggregate all committed frames and print a hierarchical table (section / ncalls / time / %tot / avg).print_profiling_timer(std::ostream &, const ProfilingPrintOptions &)usescurrent_session()when it has frames (no session pointer).print_profiling_timer(std::ostream &, MPI_Comm, const ProfilingSession &, const ProfilingPrintOptions &)— optional MPI path: whenmpi_aggregate_stdoutis true andMPI_Comm_size> 1, all ranks must call it; rank 0 gathers and prints cross-rank combined statistics (mpi_aggregate_stat:mean/sum/min/max/medianon per-rank per-path totals). Whenmpi_aggregate_stdoutis false or the communicator has size 1, only rank 0 prints the local session (same as the overload withoutMPI_Comm).
ProfilingPrintOptions controls title, ascii_lines, sort_by_time, show_exclusive_column, wall_denominator_metric (%tot denominator), mpi_aggregate_stdout, and mpi_aggregate_stat.
Canonical string constants: kProfilingRegionFft, kProfilingRegionCommunication, kProfilingRegionGradient in profiling/names.hpp.
Include the umbrella header:
#include <openpfc/kernel/profiling/profiling.hpp>
Example: examples/profiling_timer_report.cpp builds a session with an empty catalog, auto-registers paths via OPENPFC_PROFILE and ProfilingManualScope, and prints print_profiling_timer.
Export format (schema versions 2 and 3)¶
Full specification, HDF5 layout, gather row order, and migration from schema 1: profiling_export_schema.md.
Summary: rank 0 writes schema_version: 2 or 3 with ranks: one entry per MPI process, each with mpi_rank, n_frames, and frames (nested regions mirror /-separated paths). total_frames is the sum of per-rank frame counts. HDF5 v2 stores the hierarchy under /openpfc/profiling/ranks/<id>/; v3 nests the same payload under /openpfc/profiling/runs/<run_id>/… for collision-free merges (see the linked doc).
Local / CI without Slurm: set export OPENPFC_PROFILING_RUN_ID=mytest so exports use v3 and a stable namespace for debugging.
Python (JSON, schema 2)¶
import json
with open("my_run_profile.json") as f:
d = json.load(f)
assert d["schema_version"] == 2
names = d["frame_metric_names"]
wall_i = names.index("wall_step")
wall = []
fft_inc = []
for rank_entry in d["ranks"]:
for fr in rank_entry["frames"]:
wall.append(fr["scalars"][wall_i])
fft_inc.append(fr["regions"]["fft"]["inclusive"])
Python (HDF5, schema 2)¶
import h5py
with h5py.File("my_run_profile.h5", "r") as f:
g = f["openpfc/profiling"]
names = [n.decode() if isinstance(n, bytes) else n for n in g["frame_metric_names"][:]]
wall_i = names.index("wall_step")
r0 = g["ranks/0"]
fs = r0["frame_scalars"][:, :]
wall = fs[:, wall_i]
fft = r0["fft/inclusive"][:]
Schema v3 (namespaced run): if g.attrs["schema_version"] == 3, open run_id = list(f["openpfc/profiling/runs"].keys())[0] (or use a known job id), then replace g with f["openpfc/profiling/runs"][run_id] in the snippet above (frame_metric_names, ranks/0, …).
MPI and memory¶
finalize_and_export uses MPI_Gatherv to assemble all ranks’ frames on rank 0, then writes files. Memory on root scales with n_frames_total × (|frame_metric_names| + 2 × |region_paths|) × sizeof(double) plus JSON overhead. The catalog must be identical on every rank (same profiling.regions, same frame metric name list, and defaults).
CMake¶
Option |
Role |
|---|---|
|
Optional HDF5 linkage for |
|
|