Performance Overview
Last updated on 2026-08-13 | Edit this page
Overview
Questions
- Is job wall-time the only way to study job performance?
- What are commonly used metrics to describe job performance?
- What are common workflows to evaluate performance?
- How can I find the bottlenecks in a given job?
Objectives
After completing this episode, participants should be able to …
- Create a comprehensive performance overview through dedicated tools.
- Explain the difference between sampling and tracing.
- Measure utilization and the impact of underlying hardware components.
- Determine if their job is affected by a typical performance bottleneck pattern.
Wall-time measurements with time do not tell us why
exactly an application is slower than expected. To learn more about the
why, we have to measure our applications behavior in more
detail and capture the utilization of underlying hardware.
The performance measurement tool
ClusterCockpit: A job monitoring service available on many clusters in NRW. Sampled measurements of the application are stored and visualized in a timeline for each job. It needs to be centrally provided by your HPC administration team and may not be available to you!
This tool may require access to performance counters, sometimes
granted by requesting --exclusive, but it really depends on
the system. If something covered in this episode isn’t available or
looks different on your cluster, check your cluster documentation or ask
your HPC support staff.
Let us set up our performance measurement tool by running an example
job with 8 cores. To give the job enough work to be worth measuring,
let’s go with \(2263 \times 2263\)
pixels and -spp=512, we repeat this experiment twice back
to back.
A single run finishes quickly and would give ClusterCockpit only a
handful of samples to plot, making the timelines look sparse and hard to
read. Running it twice stretches the job out to a longer time, giving
the sampling-based collectors enough time to gather a richer set of data
points, which makes for much clearer plots when we look at the metrics.
Additionally, we limit the execution time to 20 minutes to not wait too
long, before getting the results. It does not hurt us, if the job gets
cut off by the Slurm TIMEOUT, since the measurements will
still represent the applications behavior up to that point.
Submit a job that runs for at least \(5\) minutes, so it is picked up by cluster cockpit. The job could look like this:
BASH
#!/usr/bin/bash
#SBATCH --time=00:20:00
#SBATCH --partition=intelsr_devel
#SBATCH --ntasks=4
#SBATCH --cpus-per-task=1
#SBATCH --nodes=1
#SBATCH --exclusive
module purge
module load GCC/13.2.0 OpenMPI/4.1.6-GCC-13.2.0 CMake/3.27.6-GCCcore-13.2.0 Boost/1.83.0-GCC-13.2.0 libpng/1.6.40-GCCcore-13.2.0
for i in {1..3}; do
mpirun -- ./build/raytracer -width=2263 -height=2263 -spp=512 -threads=1 -png "img_${SLURM_JOB_ID}_$(date +%Y-%m-%d_%H%M%S).png"
done
We will call this job 1. Note the use of --exclusive
here, which books a whole node for the job. This is important to make
sure the performance measurements are not disturbed by concurrent jobs.
Additionally, ClusteCockpit collects some metrics only on a per-node
basis, so concurrent jobs would make these measurements less usable
Exercise: Run a second job with
raytracer_float4
Submit a second job using the raytracer_float4
executable instead of raytracer — keep the resolution,
-spp, core count, repeat count, and
--exclusive request all the same as the original job.
We will call this job 2.
There are other binaries next to the raytracer example,
called raytracer_float4, _float8,
_float16. We can start one of those in our job script
instead:
BASH
#!/usr/bin/bash
#SBATCH --time=00:20:00
#SBATCH --partition=intelsr_devel
#SBATCH --ntasks=4
#SBATCH --nodes=1
#SBATCH --cpus-per-task=1
#SBATCH --exclusive
module purge
module load GCC/13.2.0 OpenMPI/4.1.6-GCC-13.2.0 CMake/3.27.6-GCCcore-13.2.0 Boost/1.83.0-GCC-13.2.0 libpng/1.6.40-GCCcore-13.2.0
for i in {1..3}; do
mpirun -- ./build/raytracer_float4 -width=2263 -height=2263 -spp=512 -threads=1 -png "$(date +%Y-%m-%d_%H%M%S).png"
done
While the test jobs are running, let’s look into the relationship between computer hardware architecture and performance.
Hardware Architecture & Counters
Broadly categorized, a jobs performance is mostly dependent on
- CPU utilization, e.g. how quickly instructions can be send to the processor, how quickly and how much data can be read from memory, and the raw calculation capabilities of the CPU.
- Memory utilization may vary in terms of how much data is stored in memory, how often data in memory is written or read, and how quickly the data can be read and written to.
- Disk input and output affects jobs that work with amounts of data that exceed available memory capacities.
- Network input and output affects applications that rely on remote data, e.g. MPI applications that regularly share results between processes on multiple worker nodes.

Measurement Workflows
To learn how applications utilize the computers hardware, we employ third party tools that read usage metrics from performance counters, often implemented either in the operating system kernels (software) or in hardware.
Dedicated performance measurement tools often employ similar methods and rely on the same sources of information, but they my focus on different issues and use different data processing and visualization methods.
In general there are two approaches to performance measurements:
- Sampling: Read out performance counters and the application state at regular intervals during execution
- Tracing: Record every event and operation that occurs
Tracing is exact and allows for a very detailed analysis. On the other hand, it results in very large amounts of measurement data that even affects the applications performance during data collection. It may be impractical in some situations.
Sampling on the other hand is less exact and results in a statistical description of the applications behavior. Sampling has a smaller measurement overhead, but may suffer from, for example, slight mis-attributions of measurements to wrong sections of the code and fluctuating results between repeated measurements.
Measurement results are either stored and analysed in a timeline, or aggregated into a final measurement, often called a profile.
General Report
Going back to our Cluster Cockpit results, log in to the
ClusterCockpit web interface of your HPC system, as explained in your
cluster documentation. Go to My Jobs and click on the job 1
with the same Slurm job id, once it is available

As a first step, to go beyond a wall-time analysis, we employ the measurement tool to produce a general overview of our applications behavior. Here, we typically try to answer questions like:
- Are available CPU, memory, disk, and network capabilities utilized well?
- Dose the jobs performance depend on a particular hardware component?
- Is there an obvious contention point that could be eliminated?
First Overview
The job view of a particular job in ClusterCockpit begins with three summary panels for the job.

The Job Info panel summarizes Slurm metadata about the job, e.g. job ID, accounts, start time, duration, etc.

The Footprint panel summarizes a preconfigured set of performance characteristics for the job. It categorizes values in a traffic-light system, so yellow and red indicators motivates further investigation. The exact list and type of metrics depends on the ClusterCockpit configuration, which is prepared by the administrator of the service.
Here, all four values are not in acceptable range. We see why:
-
cpu_load (avg)is shown in red. Since we requested the node with--exclusive, we were given all \(96\) cores of the node, but our job only uses \(8\) of them. The footprint compares load against the full \(96\) cores that are requested by us, so it correctly flags that most of the requested resources are sitting idle. This is underutilization of requested resources. -
flops_any (avg)is Floating Point Operations per second. In this case, it is calculated as \(22.38 GF/s\), which is far below what the node can theoretically achieve. It can point to anything from not utilizing the hardware features to simply not needing many floating point operations at all. -
mem_used (avg)shows the memory utilization of the job. This, together with the bandwidth metric below, is exactly why we requested an exclusive node in the first place: memory- and energy-related hardware counters are not available at a per-core granularity. They are measured at socket level or node level rather than per core, so running exclusively is what keeps them meaningful for our job alone. -
mem_bw (avg)is the memory bandwidth, i.e. how much data is transferred to and from memory per second.
Granularity of Metrics
Not every metric ClusterCockpit shows describes your job. Some describe the whole CPU socket, or even the whole node, regardless of how many cores your job actually occupies. The underlying collectors read hardware performance counters that are simply wired up at a particular level of the machine, and they have no notion of which Slurm job is currently running. This is true regardless of which monitoring tool you use, but it is essential to understand for reading ClusterCockpit correctly.
Metrics come in three granularities:
-
Core-level: measured separately for each individual
CPU core, e.g.
cpu_user,ipc,flops_any. These are meaningful for your job even if other jobs share the same node, since each core is attributed to exactly one job. -
Socket-level: measured once for an entire CPU
socket (which may host several cores of several different jobs),
e.g.
mem_bwand the package power reading behind the Energy panel (pkg_pwr). If another job shares your socket, its memory traffic or power draw is mixed into your measurement. -
Node-level: measured once for the whole compute
node, e.g.
cpu_load,mem_usedand network or filesystem metrics. These are effected by every job on the node, not just yours.
Each metric plot in ClusterCockpit has a small drop-down menu as shown in image below, typically labelled core, socket, or node. This label tells you directly at which granularity that particular chart was measured — a core selection means one line per core of your job, while socket or node means the value is shared with (and possibly polluted by) other jobs. If the drop-down only offers socket or node, treat the value with more caution unless you know the node was exclusively yours

This is precisely why we requested --exclusive when
submitting our job earlier in this episode: on a node reserved
exclusively for our job, socket- and node-level metrics describe our job
alone, since there is nothing else running to mix into the measurement.
If you cannot use --exclusive, for example because your
allocation only ever needs a fraction of a node, keep an eye on which
granularity a given metric is measured at, and treat socket- or
node-level values as an upper bound that may include other jobs’
activity rather than a precise reading of your own.

More detailed plots for each individual metric are available and can be configured through the Select Metrics button.
In general, the footprint panel gives the most information on what issues our job may have and where to start the investigation. In our case:
- We can utilize all \(96\) cores instead of just \(8\) to make use of the full exclusive allocation.
- We can try to improve either
flops_anyormem_bw. To know which one actually makes sense to target, we need to look at the next panel, the Roofline plot.
How to read a Roofline Plot
Before looking at our job’s result, here’s what to expect on Roofline plot and how to understand the plot. Here is the example Roofline Plot:

In jobs performing floating point operations on data read from memory, i.e. any numeric operation, which are very common in HPC, a Roofline plot is a common visualization of the jobs performance.
- The x-axis is operational intensity: how many floating point operations are performed per byte loaded from memory. The y-axis is achieved performance: floating point operations per second.
- A diagonal line marks the maximum performance possible. \[P_{peak} = \min(\text{Memory Bandwidth} \times \text{Operational Intensity},\ \text{Flop/s}_{peak})\]
- A job with low intensity i.e. high memory transfers is pinned close to diagonal line: memory is the bottleneck, no matter how fast the CPU could otherwise compute.
- The two horizontal lines mark the maximum performance possible if CPU computation itself is the limit: lower line for scalar operations, a higher one once vectorized instructions are used.
- Where the diagonal and horizontal lines meet is the knee. Left of the knee = memory bound (data movement is the bottleneck). Right of the knee = compute bound (calculation capability is the bottleneck).
- The colored dots show measurements taken at different points in time during the job’s run, following a blue-to-red gradient: blue marks the start of the application, red marks the end. Their position relative to the rooflines shows how close each phase of execution came to the physical performance limits, and how that changed over the runtime.
Below is the Roofline plot for our application. 
Take a moment to look at where our raytracer’s dot(s) land on the plot.
- Is the job compute bound or memory bound? Which side of the knee do the measurements fall on?
- Do the dots sit close to the scalar roofline, or closer to the vectorized roofline? What does that tell you about whether the application is actually using vectorized instructions?
- Given where the job sits, which metric is more interesting for our
next investigation —
flops_any, ormem_bw? Which one, if improved, would actually move the dot closer to a roofline? - Does the dot’s position stay roughly constant over the blue-to-red timeline, or does it drift? What might cause a job to move further from (or closer to) the roofline as it runs?
CPU Metrics
CPU performance can be categorized in
- Front-end utilization: preparation and scheduling of instructions of program code provided through the cache hierarchy
- Computation: Arithmetic and logical operations with various data types, including the utilization of vectorized instructions, etc.
- Back-end utilization: loading and storing of data in the cache hierarchy
The front- and backend hardware of a physical CPU core is often duplicated to implement simultaneous multithreading (SMT, also called hyperthreading). Here, the arithmetic logical unit receives data and instructions from two independent threads to achieve a sufficient amount, which is a common limiting factor in everyday calculations. On HPC systems, the benefit of SMT is very much application-dependent. It is often disabled on HPC systems, since code is optimized to maximize computational intensity.
ClusterCockpit captures many dedicated CPU metrics and provides a timeline visualization for each.

cpu_user shows, per core, the share of CPU time spent
executing our application’s own instructions, as opposed to time spent
in kernel/system activity or sitting idle. A high cpu_user
near \(1\) is a good sign: the core is
busy, and busy doing our work rather than system overhead. A
core sitting noticeably below \(1\) is
worth a closer look. Some of its time is going to something other than
our application’s own instructions, whether that’s kernel activity,
waiting, or other overhead. A cpu_user of \(0\) means the core was idle for that
interval and not running our application at all.

flops_any captures any floating point operation on Intel
CPUs, at core-level granularity, with single- and double-precision
operations accumulated into the same value. Recall from the Footprint
panel that our job’s flops_any (avg) sat at only \(22.38\) GF/s, while the node’s theoretical
peak is \(8532\) GF/s.
That gap looks enormous, but it isn’t a fair comparison yet: the \(8532\) GF/s peak is for all \(96\) cores of the exclusive node, while our job only ever uses \(8\) of them. Scaled down to \(8\) cores, the theoretical peak is roughly \(711\) GF/s, so \(22.38\) GF/s is still well below what our \(8\) cores could theoretically achieve, but nowhere near the dramatic 96-core gap the raw numbers first suggest. This isn’t a contradiction of the Roofline plot classifying the job as compute bound either way: compute bound only means the CPU’s calculation capability is the limiting factor relative to how little data is moved, not that the job is issuing many floating point operations specifically.
Exercise: Compare flops_any
between job 1 and job 2
We now have two jobs to compare: our job1 (raytracer)
and a job2 run with the raytracer_float4 executable.
Look at the flops_any metric for both jobs. What’s
different between them, and why?

In our measurements, flops_any (avg) per core is close
to \(3\) GF/s for job 1, and close to
\(6\) GF/s for job 2, roughly a \(2\times\) increase.
raytracer_float4 uses SIMD: float4
corresponds to SSE2 vectorized instructions, where a single instruction
operates on \(4\) float operands at
once, instead of one operand at a time as in the original scalar
raytracer.
flops_any counts floating point operations regardless of
whether they were issued as scalar or vectorized instructions, each
vectorized instruction in job 2 is counted as \(4\) floating point operations rather than
\(1\). As a result,
flops_any (avg) for job 2 is noticeably higher than for job
1. For the same amount of work done, the vectorized version
issues far fewer instructions to do it, and each one accomplishes \(4\times\) as much.
This is also a good moment to connect back to the Roofline plot: job 2’s dots sit slightly higher on the plot than job 1’s, and also shift further right along the x-axis — since computing more FLOPs per byte loaded raises the operational intensity too. They don’t touch the upper, vectorized roofline yet — that would become more visible if we used the full node instead of just \(8\) of its \(96\) cores.
Our raytracer performs plenty of integer, branching, and
memory-addressing work per byte loaded, which does not count towards
flops_any. This is why a low flops_any is not
necessarily a red flag here. On top of that, our original raytracer is
not vectorized. It issues one scalar floating point operation at a time
rather than operating on multiple values per instruction, which further
caps how high flops_any can climb regardless of how well
the rest of the code performs.
Memory Metrics
Memory utilization is characterized in terms of used capacity, bandwidth and access latencies.

mem_used sits at approximately \(5\) GB for our job, shown as an almost
flat, straight line across the runtime, which is a good sign. A flat
line means memory consumption is stable once the application has
allocated what it needs. A slanted, steadily increasing line instead
would be a warning sign of a memory leak: memory that
is allocated but never freed, growing continuously over the job’s
runtime instead of leveling off.

mem_bw is a socket-level metric. It
measures how much memory is transferred to and from a given CPU socket
per second, not per individual core. Since our node has multiple
sockets, you’ll typically see a different mem_bw value
reported for each socket. This difference depends entirely on which
cores on each socket are actually active and running part of the
application: a socket with more of our job’s cores running on it will
show higher memory traffic than a socket with fewer (or none) of them
active.
Energy

Depending on the ClusterCockpit configuration, an energy demand is displayed below the job info panels. This measurement is highly dependent on hardware configurations of your HPC systems, so the estimates may vary. They are often based on CPU package power measurements, which are unlikely to cover the whole energy demand of the node, e.g. omitting disks, fans, etc. In other cases, the energy may be estimated from power supply measurements and scaled to CPU activity to get a more accurate estimate.
These estimates often still not include network, parallel filesystem components and cooling of the clusters. Nevertheless, the estimate is a great tool to identify the scale of the jobs energy demand.
Miscellaneous
Typically, many more measurements and perspectives on the data are available for each tool.

ClusterCockpit provides a detailed statistics table covering all measurements involved with a job. Alongside this table, you can also see your job script and Slurm info, which tells you how much of each resource was actually allocated to your job. The statistics table itself shows the max, min, and avg values for whichever metrics have been configured for your ClusterCockpit deployment.
How to identify a bottleneck?
Exercise: Match application behavior to hardware
Which parts of the computer hardware may become a point of contention for these application patterns:
- Calculating matrix multiplications
- Reading data from processes on other computers
- Calling many different functions from many equally likely if/else branches
- Writing very large files (TB)
- Comparing strings for matches
- Constructing a large simulation model
- Reading thousands of small files for each iteration
Maybe not the best questions, also missing something for accelerators.
- CPU (FLOPS), maybe the cache hierarchy if matrix elements do not align well to cache sizes
- I/O (network)
- CPU (Front-End), difficult to prepare instructions in time
- I/O (disk), bandwidth limited
- CPU (Back-End), getting strings through the caches
- Memory (capacity)
- I/O (disk)
Summary
Dedicated performance measurement tools are helpful to create reports of the general job behavior. These tools either trace every event, or sample the application and hardware state at regular intervals. Many tools are available, but some may have to be set up by the HPC system administrators, or rely on valid licenses.
The relationship between a job and the execution on physical hardware can become a very deep topic. One of these topics is the correct mapping of job processes to the requested number of CPU cores, addressed in the next episode.
- Performance tools measure data as regular samples or by tracing every event
- The data is either processed and visualized in a timeline or aggregated in a final profile
- Job performance relates closely to contention points in physical
hardware
- CPU utilization (front-end, ALU, back-end), multithreading, vectorization
- Memory utilization (capacity, bandwidth, latency)
- Disk I/O
- Network I/O