k4bench.analysis.components¶
k4bench.analysis.components ¶
Per-component costs of one k4run job, as the k4BenchAuditor recorded them.
A Gaudi job is a set of components — algorithms and services — and the auditor
times every audited call of each: algorithm executions per event, and the
initialize/start/stop/finalize of algorithms and services. Every recorded cost
but those in :data:INCLUSIVE_METRICS is a self cost, what the call spent
outside the audited calls nested inside it, so costs add up: a sequencer's own
cost is its bookkeeping, and the costs of an event's components sum to the cost
of its audited calls. The event time in
_events.json spans the first to the last top-level algorithm, so with
several top-level algorithms it exceeds the components' summed wall_s by
the time spent between them, which no component is charged for.
Metrics recorded for every call:
wall_s— wall-clock time;cpu_s— CPU time of the calling thread;peak_rss_increase_mb— how much the call raised the process's peak RSS. Zero for most calls once the job has warmed up; nonzero names the component that pushed the high-water mark, which is what a peak-memory regression needs.
Recorded for every call too, but absent from files k4bench 0.0.47's auditor
wrote (:data:THREAD_METRICS):
minor_page_faults— pages mapped in from memory: first touches of fresh memory, or of files already cached;major_page_faults— pages the thread waited to read from disk or CVMFS;voluntary_context_switches— times the thread blocked, on I/O or a lock; together with the major faults, whywall_sexceedscpu_s;involuntary_context_switches— times it was preempted: a sign the host was busy and the call's times are noisier.
Where the hardware performance counters could be read (:data:HARDWARE_METRICS):
instructions— user-space instructions the calling thread retired. They follow the work done more than the host's load, so they show changes too small for time to;cycles— user-space CPU cycles the calling thread was counted for. Their ratio, instructions per cycle, depends on the processor, the instruction mix and memory behaviour alike, so it does not by itself say why a call took longer.
Both are counted on the calling thread only: work a library hands to threads of its own is not included.
Where they could not, :attr:ComponentTiming.hardware_counters_error says why.
When the job preloaded the auditor's allocation counter, as k4bench k4run
does (:data:ALLOCATION_METRICS):
allocations— heap blocks allocated (malloc and its relatives, and C++ new); a resized block counts as one;allocated_bytes— the heap memory they take, malloc's 8-byte header and rounding to 16 bytes included (a mapped page multiple for large blocks); the byte metrics below count the same way;net_allocated_bytes— allocated minus freed, what the call kept on the heap. Negative for a call that freed more than it allocated;peak_heap_bytes— the most heap the call held at once beyond what its thread held when it started: the memory the component needs;largest_allocation_bytes— its largest single block.
The last two are not self costs (:data:INCLUSIVE_METRICS): they include
nested calls, whose memory was held during the call too, so they do not add up,
and a component called several times in one event or phase is charged its
largest. Memory a library maps from the kernel itself, as pool allocators do,
is not counted.
Nesting is tracked per thread, so the above holds for a serial job. When calls
ran concurrently (threads > 1), cpu_s still sums to the event's work,
but the wall_s of calls that overlapped on different threads sums to more
than the event's elapsed time (the event time is in _events.json), and since
the peak RSS is the process's, a call's peak_rss_increase_mb may include
memory allocated by other threads meanwhile. A thread's heap is what it
allocated less what it freed, so a block one thread allocated and another freed
stays on the first thread's heap: its calls' peak_heap_bytes may then count
blocks that were no longer held, and net_allocated_bytes is shifted the same
way. Everything else is counted per thread and stays exact.
A missing value is NaN, never zero: an algorithm that did not run in an
event (a filtered sequence) has no cost there, which is different from a cost
too small to resolve.
ComponentTiming
dataclass
¶
ComponentTiming(components: DataFrame, execute: dict[str, DataFrame], lifecycle: dict[str, DataFrame], phases: dict[str, tuple[int, int]], process_start_epoch_ns: int | None, measurement_overhead_ns: float | None, threads: int, hardware_counters_error: str | None)
One label's _components.json, as frames.
components is indexed by component name, in the order the auditor first
saw each component, with columns category (algorithm, service or
other), type (the Gaudi type), impl (the class doing the work: the
type, or the class a wrapper delegates to), library (the shared object
defining the type, symlinks resolved), impl_library (the one defining
impl; empty where it could not be found, and for a wrapped component in
files the auditor of k4bench 0.0.49 or earlier wrote), parent (the
enclosing sequencer's name, or None, also for an algorithm several
sequencers share, which no sequencer's inclusive cost then claims) and
execute_calls.
execute maps each metric to an events × components frame indexed by
event number. lifecycle maps each non-event phase (initialize,
start, stop, finalize, or a custom audited section) to a
components × metrics frame. A non-event call made during an execution, such
as a service initialized on first use, appears there and also stays in the
executing component's execute cost. phases holds each phase's
(begin, end) in ns since the Unix epoch, execute included, spanning
only the calls made outside event processing. hardware_counters_error
is why the job has no :data:HARDWARE_METRICS, such as
"perf_event_open: Function not implemented" in a container whose seccomp
profile refuses the counters; it is None when they were counted, and in
files the auditor of k4bench 0.0.48 or earlier wrote.
from_json
classmethod
¶
from_json(raw: Mapping, *, source: str | Path | None = None) -> ComponentTiming
Build from a parsed _components.json, refusing an inconsistent one.
The schema version is checked by the caller
(:func:~k4bench.plugin.schema.validate_component_schema).
Source code in k4bench/analysis/components.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |
children ¶
sequencers ¶
Components that enclose others: their own cost is the bookkeeping of running their members, and the auditor's measurement overhead for those calls.
Source code in k4bench/analysis/components.py
algorithms ¶
Algorithms that do work of their own: neither services nor
:meth:sequencers, whose own time is bookkeeping.
Source code in k4bench/analysis/components.py
descendants ¶
Every component nested under component, at any depth.
inclusive ¶
Per-event metric of each component including its descendants — a
sequencer's cost with everything it ran. A component that did not run
in an event stays NaN there; a descendant that did not run counts
as zero, but one that ran without a value (hardware counters that could
not be read) leaves the total NaN. For wall_s in a job with
threads > 1, descendants that ran concurrently make this more than
the elapsed time. Refused for a metric that already includes nested
calls (:data:INCLUSIVE_METRICS).
Source code in k4bench/analysis/components.py
phase_component_seconds ¶
Each component's own wall time in phase: for execute, summed
over the job's events (a component that did not run in an event
spent nothing there), otherwise its lifecycle call. Self times, so
they add up to the phase, but for the time between the event loop's
events, which no component is charged for. Empty for a phase the job
did not record.
Source code in k4bench/analysis/components.py
phase_seconds ¶
Duration of each recorded phase, plus configure: from process
start to the first audited initialize, which is the Python options
processing and everything before the auditor existed.
Source code in k4bench/analysis/components.py
without_warm_up ¶
frame's events but the warm_up event, found by its number: a job
whose recorded events start after it keeps its first. None keeps
every event.
Source code in k4bench/analysis/components.py
event_loop_time ¶
Each component's own time in the event loop, the warm_up event left
out (:func:without_warm_up). wall is :attr:ComponentTiming.execute's
wall_s, where NaN is an event the component did not run in.
Indexed by component, with :data:EVENT_LOOP_TIME_COLUMNS:
seconds, its own time summed over the events left;events, how many events are left, the same for every component;executed_events, how many of those it ran in, andcoverage, their share ofevents;seconds_per_event,secondsoverevents: what it adds to an event on average, the events it was skipped in included, so it adds up across components;seconds_when_run,secondsoverexecuted_events: what it costs in an event it runs in. The two agree for a component run in every event.
A component that ran in none of the events left, or with no event left,
has no time: seconds and the ratios are NaN, never zero.