Measuring performance in TRUST#
A key feature for code optimisation is performance measurement. TRUST offers different solutions, using TRUST internal features or external libraries.
TRUST counters#
In order to obtain statistics on the performance of a test case, TRUST uses counters. Counters are C++ objects.
Their main purpose is to serve as time watches for parts of the code you deem important. They also track additional metrics:
Timing statistics:
Minimum, maximum, average and standard deviation of the measured time per time step
Usage metrics:
An integer called
counttracking the number of times the counter has been started and stoppedA custom integer called
quantity, mainly used to measure the quantity of bytes exchanged during communication operations, or to store the number of iterations of the linear solver
Identification:
std::stringattributesdescriptionandfamily. A description is mandatory for each counter. The family attribute is by default set to"None"but can be used as a key for regrouping the counters you want to parse together after your computation.
Level management: Counters also have a certain level. The level of a counter is represented by an integer. It makes sure that you know when you open your counter. Indeed, if a counter of level 1 is running, you can only start a counter of level 2. Otherwise, the code will stop. In the same logic, you can only close the most recently opened counter. Because of this interlock structure, counters also have another interesting metric called alone_time. It is the elapsed time where the considered counter is the last opened one (i.e., excluding time spent in nested counters). This metric is printed in the CSV output file and helps identify the intrinsic cost of each code section.
Example of the level hierarchy:
statistics().begin_count(STD_COUNTERS::total_execution_time, -1); // Level -1 - reserved to the total execution time counter
statistics().begin_count(STD_COUNTERS::timeloop, 0); // Level 0 - reserved to counters of the global steps
statistics().begin_count(STD_COUNTERS::convection, 1); // Level 1 - only possible while Level 0 is running
statistics().begin_count(STD_COUNTERS::mpi_recv, 2); // Level 2 - only possible while Level 1 is running
statistics().end_count(STD_COUNTERS::mpi_recv, 1 , nb_exchanged_bytes);
statistics().end_count(STD_COUNTERS::convection);
statistics().end_count(STD_COUNTERS::timeloop);
statistics().end_count(STD_COUNTERS::total_execution_time);
This ensures proper nesting and helps track where time is spent in your code hierarchy.
Those counter objects are managed by the Perf_counters class. In practice, you will only need to interact with the Perf_counters class (not with individual counter objects directly). The Perf_counters class follows a singleton pattern and a Pimpl pattern, such that the implementation of the class is hidden in the Perf_counters.cpp file. The unique instance of Perf_counters can be called inside the code by using:
statistics()
The unique instance of Perf_counters will be created at the first statistics() call.
The counters managed by the Perf_counters instance are separated in two types:
the standard counters used by default in TRUST and identified by a
STD_COUNTERSkey (STD_COUNTERSis an enum class),custom counters that can be created inside the code and that are identified by a
std::string.
The basic API for counters in TRUST is as follows:
#include <Perf_counters.h>
statistics().begin_count(MY_COUNTER_KEY, level);
{
// The block of code I want to have statistics about
}
statistics().end_count(MY_COUNTER_KEY, count, quantity);
MY_COUNTER_KEY is either a STD_COUNTERS if you want to open a standard TRUST counter or the std::string that corresponds to the description of the custom counter you try to open. In the statistics().begin_count() function, the level parameter is optional. If omitted, the counter will automatically use the one defined at the creation of the counter. If you are having trouble determining the level of your counter, you can use the function statistics().get_last_opened_counter_level() to know the level of the last opened counter. The count parameter specifies how much to increment the counter’s total count (i.e., how many times to record this begin/end cycle). It is set by default to 1. The quantity parameter specifies how much to increment the quantity attribute of your counter and is by default set to 0.
During a TRUST computation, TRUST statistics are measured through 3 main steps:
Computation start-up
Time loop
Post-resolution
At the end of each step, the counters are reset and statistics are printed in the two files:
MY_TRUST_CASE_NAME.TUMY_TRUST_CASE_NAME_csv.TU
The first file contains aggregated stats that are the most commonly used alongside some information regarding the environment of your computation (date, OS, CPU model, GPU model if you run a GPU computation, number of CPU processors used, …). It has been designed to be human readable, but it is not easy to parse it informatically. The second file has been created for easily parsing each and every counter’s data with your favorite csv parsing tool, for example pandas.
The first time steps can take more time thant the rest, if you want to discard them of your stats, add the following line of code right before the time loop:
statistics().set_nb_time_steps_elapsed(int n) ;
Standard TRUST counters#
Here is the list of the standard TRUST counters:
Key |
Description |
Family |
Is_communication |
Is_gpu |
|---|---|---|---|---|
total_execution_time |
Total time |
None |
False |
False |
computation_start_up |
Computation start-up |
None |
False |
False |
timeloop |
Time loop |
None |
False |
False |
backup_file |
Back-up operations |
None |
False |
False |
system_solver |
Linear solver resolutions Ax=B |
None |
False |
False |
petsc_solver |
Petsc solver |
None |
False |
False |
implicit_diffusion |
Solver for implicit diffusion |
None |
False |
False |
compute_dt |
Computation of the time step dt |
None |
False |
False |
turbulent_viscosity |
Turbulence model::update |
None |
False |
False |
convection |
Convection operator |
None |
False |
False |
diffusion |
Diffusion operator |
None |
False |
False |
gradient |
Gradient operator |
None |
False |
False |
divergence |
Divergence operator |
None |
False |
False |
rhs |
Source terms |
None |
False |
False |
postreatment |
Post-treatment operations |
None |
False |
False |
restart |
Read file for restart |
None |
False |
False |
matrix_assembly |
Nb matrix assembly for implicit scheme: |
None |
False |
False |
update_variables |
Update ::mettre_a_jour |
None |
False |
False |
mpi_sendrecv |
MPI_send_recv |
MPI_sendrecv |
true |
False |
mpi_send |
MPI_send |
MPI_sendrecv |
true |
False |
mpi_recv |
MPI_recv |
MPI_sendrecv |
true |
False |
mpi_bcast |
MPI_broadcast |
MPI_sendrecv |
true |
False |
mpi_alltoall |
MPI_alltoall |
MPI_sendrecv |
true |
False |
mpi_allgather |
MPI_allgather |
MPI_sendrecv |
true |
False |
mpi_gather |
MPI_gather |
MPI_sendrecv |
true |
False |
mpi_partialsum |
MPI_partialsum |
MPI_allreduce |
true |
False |
mpi_sumdouble |
MPI_sumdouble |
MPI_allreduce |
true |
False |
mpi_mindouble |
MPI_mindouble |
MPI_allreduce |
true |
False |
mpi_maxdouble |
MPI_maxdouble |
MPI_allreduce |
true |
False |
mpi_sumfloat |
MPI_sumfloat |
MPI_allreduce |
true |
False |
mpi_minfloat |
MPI_minfloat |
MPI_allreduce |
true |
False |
mpi_maxfloat |
MPI_maxfloat |
MPI_allreduce |
true |
False |
mpi_sumint |
MPI_sumint |
MPI_allreduce |
true |
False |
mpi_minint |
MPI_minint |
MPI_allreduce |
true |
False |
mpi_maxint |
MPI_maxint |
MPI_allreduce |
true |
False |
mpi_barrier |
MPI_barrier |
MPI_allreduce |
true |
False |
gpu_library |
GPU_library |
GPU_library |
false |
true |
gpu_kernel |
GPU_kernel |
GPU_kernel |
false |
true |
gpu_copytodevice |
GPU_copyToDevice |
GPU_copy |
false |
true |
gpu_copyfromdevice |
GPU_copyFromDevice |
GPU_copy |
false |
true |
gpu_malloc_free |
GPU_allocations |
GPU_alloc |
false |
true |
interprete_scatter |
Scatter_interprete |
None |
true |
false |
virtual_swap |
DoubleVect/IntVect::virtual_swap |
None |
true |
False |
read_scatter |
Scatter::read_domaine |
None |
true |
False |
parallel_meshing |
Parallel meshing |
None |
False |
False |
IO_EcrireFicPartageBin |
write |
IO |
False |
False |
IO_EcrireFicPartageMPIIO |
MPI_File_write_all |
IO |
False |
False |
You can use them whenever you need to.
Custom TRUST counters#
As explained above, on top of standard counters, you can also create and use custom counters. To create a new custom counter, you just need to add the following in your code:
statistics().create_custom_counter(std::string counter_description, int counter_level, std::string counter_family = "None", bool is_comm=false, bool is_gpu=false);
Then, you can open and close your new counter, using std::string counter_description as your new counter key. All of the custom counters will be printed in both TU files.
Note
Warning: if a custom counter already has your std::string counter_description, the function statistics().create_custom_counter() will not create another counter.
External profilers#
Some external profilers can be directly used on a TRUST data file. To find the appropriate option to use them, run:
trust -h
Below, we present the three most useful ones.
Perf#
First, install the Perf library if you don’t have it yet.
Then, you just need to:
trust -perf MY_DATA_FILE.data
Perf is dedicated to CPU profiling. It will enable you to locate the main performance bottlenecks inside your code.
Heaptrack#
First, make sure the Heaptrack package is installed on your computer.
Then, you just need to:
trust -heaptrack MY_DATA_FILE.data
Heaptrack is dedicated to monitor memory usage (allocations: their number and size). It will enable you to find excessive allocations and possibly memory leaks.
Perf and Heaptrack work best with Hotspot, a GUI for visualizing profiling data.
Nsight system#
Normally, Nsight system is already available in TRUST. You can also install it alone or alongside the Cuda Toolkit.
Then, you just need to:
trust -nsys MY_DATA_FILE.data
Nsight system is dedicated to GPU profiling. It will enable you to locate the main performance bottlenecks inside your code. It detects the code not yet ported on GPU, and helps you visualize the data copy between host and device memory. Nsight system is also useful with CPU-only runs thanks to the rich labeling of the code.