0.6.0
Loading...
Searching...
No Matches
Scalability study with notus.py

The following guidelines describe how to run a weak, strong, or strong-node scalability study with the notus.py script.

Introduction

A scalability study measures how the computation time of a case evolves when more computing resources are used. Unlike a convergence study, it says nothing about the quality of the solution: it only tells you how efficiently Notus exploits parallelism. The scalability mode of notus.py automates it — it generates one run per resource configuration, collects the CPU times, and reports the speedup and parallel efficiency.

Note
A meaningful scalability study requires large meshes and a real HPC cluster (several nodes, e.g. Curta). A local shared-memory machine can be used to test that the workflow runs, but the numbers will not be representative.

The three submodes

Scalability is run through a submode, given right after scalability:

Submode What stays fixed What varies Question answered
weak Problem size per core Number of nodes Can I solve a bigger problem in the same time if I add resources?
strong Total problem size Number of nodes Can I solve the same problem faster by adding nodes?
strong-node Total problem size MPI procs on one node Same as strong, but within a single node.

In weak mode the mesh given is per core and grows with the number of cores; in strong and strong-node modes the mesh is the total mesh and stays constant.

Preparing the NTS file

As with convergence, the resolution is injected by the script, so the NTS file must use the injected variables rather than hard-coded values:

  grid {
     grid_type regular;
     number_of_cells (N_CELLS_X, N_CELLS_Y, N_CELLS_Z);
     number_of_ghost_cells 2;
  }

The script also injects TIME_ITERATIONS (how many time steps each run performs). The case should run for exactly that many iterations so the timings are comparable.

The case must measure its CPU time, otherwise no .cputime file is produced and there is nothing to post-process. Enable the relevant measurements in the system block:

  system {
     output_directory PREFIX;
     measure_cpu_time;
     measure_time_iteration_cpu_time;
  }

You can build a per-run output directory from the injected mesh size, which keeps the runs from overwriting each other:

  string PREFIX = "case_" + i2s(N_CELLS_X);

A real 2D case header therefore looks like:

  system {
     output_directory PREFIX;
     measure_cpu_time;
     measure_time_iteration_cpu_time;
  }

  domain {
     spatial_dimension 2;
     corner_1_coordinates (0.0, 0.0);
     corner_2_coordinates (channel_length, channel_length);
  }

  grid {
     grid_type regular;
     number_of_cells (N_CELLS_X, N_CELLS_Y);
     number_of_ghost_cells 2;
  }

Choosing the resource configurations

Each submode needs a progression that defines the list of configurations to run (number of nodes for weak/strong, number of MPI procs for strong-node). Exactly one of these three options is required:

Option Meaning Example → configurations
--arithmetic MIN DIFF MAXTERMS Arithmetic progression: MIN, MIN+DIFF, MIN+2·DIFF, … --arithmetic 4 4 4 → 4, 8, 12, 16, 20
--geometric MIN RATIO MAXEXP Geometric progression: MIN·RATIO⁰, MIN·RATIO¹, … (exponent from 0) --geometric 4 4 4 → 4, 16, 64, 256, 1024
--custom N1 N2 N3 … Explicit list of configurations. --custom 4 6 9 13 18 → 4, 6, 9, 13, 18

Running a weak scalability study

In weak mode the mesh is given per core and the total mesh grows with the number of cores, so the problem size per core is constant.

  ./notus.py scalability weak --arithmetic 4 4 4 --mpi-procs-per-node 16 --threads 2 --cells-per-direction 30 30 30 --time-iterations 300 path/to/case.nts
Argument Role Default
--mpi-procs-per-node N MPI processes per node. required
--cells-per-direction NX NY [NZ] Mesh per core (2 or 3 values). required
--time-iterations N Number of time steps performed per run. required
--threads N OpenMP threads per MPI process. 1
--dimension {2,3} Dimension of the case. 3
one of --arithmetic/--geometric/--custom Node-count progression. required

Running a strong scalability study

In strong mode the total mesh is fixed and spread over a growing number of nodes:

  ./notus.py scalability strong --geometric 4 4 4 --mpi-procs-per-node 32 --dimension 2 --cells-per-direction 500 500 --time-iterations 60 path/to/case.nts

The arguments are the same as weak, except --cells-per-direction is now the total mesh, not the per-core mesh.

Running a strong-node scalability study

strong-node keeps the total mesh fixed but varies the number of MPI processes on a single node, which is convenient when you do not have several nodes available. It does not take --mpi-procs-per-node (the progression already gives the number of procs):

  ./notus.py scalability strong-node --custom 4 6 9 13 18 --dimension 2 --cells-per-direction 100 100 --time-iterations 150 path/to/case.nts

Reading the output

Each run writes a .cputime file; the post-processing step then prints one table per timer (total time, solver, etc.) and writes three result files to the output directory (-o/--directory, default scalability_<submode>):

  • <case>_<submode>_times — raw CPU times (normalized by the number of time iterations),
  • <case>_<submode>_speedup — speedup relative to one core (not produced in weak mode),
  • <case>_<submode>_efficiency — parallel efficiency,

together with a .md summary. By default the tables are written in space-separated format (--output-format ssv); use --output-format csv for comma-separated files.

A printed table looks like this (strong mode, where speedup is shown):

  Total time: 2 threads, 32 cores per node

  N cpu │ Nodes │     Mesh      │  total time  │  Speedup  │ Efficiency
  ──────┼───────┼───────────────┼──────────────┼───────────┼───────────
    32  │   1   │ (500, 500, 1) │ 1.0000       │ 1.000000  │   1.000
    64  │   2   │ (500, 500, 1) │ 0.5200       │ 1.923077  │   0.962
   128  │   4   │ (500, 500, 1) │ 0.2700       │ 3.703704  │   0.926

The header line recalls the number of OpenMP threads and the number of cores per node. The columns are the number of CPUs (N cpu), the number of nodes, the mesh, the timer value, the speedup (omitted in weak mode), and the parallel efficiency. One such table is printed per timer (total time, momentum solver, Poisson solver, etc.).

How speedup and efficiency are defined

Times are first normalized by the number of time iterations, so a run that performed more steps is comparable to one that performed fewer. From the normalized time on \(N\) cores \(t(N)\):

  • Speedup is the time on one core divided by the time on \(N\) cores, \(S(N) = t(1) / t(N)\). The one-core time is extrapolated from the smallest configuration. Ideal speedup is \(S(N) = N\).
  • Parallel efficiency is the speedup divided by the number of cores, \(E(N) = S(N) / N\), so ideal efficiency is \(1\). In weak mode efficiency is computed directly as \(t(1)/t(N)\), since the work per core is constant.

For a perfectly scaling code the speedup doubles when the resources double and the efficiency stays close to 1.

Sizing the case

The numbers are only meaningful if each core has enough work. Following the cluster usage recommendations, aim for about 60,000 cells per core in 3D and 10,000 cells per core in 2D to stay in the range of maximum efficiency of a full-MPI Notus run. Too few cells per core makes communication dominate and the efficiency collapses, which reflects the sizing, not the code. See Notus usage on clusters for resource-allocation guidance.

Failed or unfinished jobs

During post-processing, each run is validated through its .cputime file. A run is flagged as ERROR when the file is missing (the job did not finish, or has not finished yet in a dual-pass run) or empty (the simulation crashed before writing its timings). Failed jobs are ignored so the rest of the study can still be processed, and the group is reported with status ERROR and a warning. If every job failed, the script reports that there is nothing to post-process. At least two valid runs are needed to produce a table.

Overwriting a previous run

If a group of jobs from a previous run already exists in the output directory, the script asks for confirmation before overwriting. Answering n skips that case; answering y deletes the old files (including .cputime) and relaunches.

Cluster submission

Scalability is almost always run on a cluster through the dual-pass job manager: a first pass submits the jobs (--stage submit), and a second pass collects the results once the jobs have finished (--stage post-process).

  ./notus.py scalability weak --dual-pass --stage submit       --batch-template path/to/template --batch-command "sbatch {BATCH_FILE}" --arithmetic 4 4 4 --mpi-procs-per-node 16 --threads 2 --cells-per-direction 30 30 30 --time-iterations 300 path/to/case.nts
  ./notus.py scalability weak --dual-pass --stage post-process --batch-template path/to/template --batch-command "sbatch {BATCH_FILE}" --arithmetic 4 4 4 --mpi-procs-per-node 16 --threads 2 --cells-per-direction 30 30 30 --time-iterations 300 path/to/case.nts

See Submitting options for details on the dual-pass job manager and batch templates.

Validating scalability against a reference

The scalability mode described above only measures performance; it does not check it against anything. To turn a scalability study into a pass/fail test, Notus provides a separate non-regression-scalability mode. The difference in a nutshell:

scalability non-regression-scalability
Parameters Given on the command line (--arithmetic, --cells-per-direction, …) Read from @ directives inside the NTS file
Output CPU times, speedup, efficiency tables Same tables compared to stored references
Database Not used Efficiency values compared to a per-machine database

In non-regression-scalability mode, each measured efficiency is compared to the reference stored in the database. The comparison uses a relative tolerance (in percent), because CPU times depend on the machine, and the printed table gains reference and pass/fail columns:

  Total time: 2 threads, 32 cores per node

  N cpu │ Nodes │     Mesh      │ total time │ Speedup │ Reference speedup │ Efficiency │ Reference efficiency │ Tolerance │ Difference │ Pass
  ──────┼───────┼───────────────┼────────────┼─────────┼───────────────────┼────────────┼──────────────────────┼───────────┼────────────┼──────
    32  │   1   │ (500, 500, 1) │ 1.0000     │ 1.000   │ 1.000             │ 1.000      │ 1.000                │ 10 %      │ 0.000%     │ PASS
    64  │   2   │ (500, 500, 1) │ 0.5200     │ 1.923   │ 1.900             │ 0.962      │ 0.950                │ 10 %      │ 1.263%     │ PASS

A run is PASS when, for every configuration, the relative difference between the measured and reference efficiency stays within the tolerance; otherwise it is FAIL. A configuration whose reference is missing from the database is reported as ERROR (not in database).

The reference values are produced and stored with the database mode; see Scalability non-regression testing with notus.py for the full description of this mode, and Manipulate the non-regression database for how to add references (remember --directory non-regression-scalability when adding scalability results).

FAQ

Which submode should I use?

Use strong to check whether a fixed problem runs faster on more nodes, weak to check whether a proportionally larger problem stays at constant time, and strong-node to study parallel behaviour within a single node when several nodes are not available.

In which mode is the mesh per core, and in which is it total?

In weak mode --cells-per-direction is the mesh per core (the total mesh grows with the number of cores). In strong and strong-node it is the total mesh.

Why is there no speedup file after a weak run?

Speedup compares against a single-core run of the same problem, which does not apply to a weak study (the problem grows with the resources). Weak scalability is assessed through efficiency instead.

My run stops with "Cannot run a 3D case with a 2D mesh".

The number of values in --cells-per-direction must match --dimension. With --dimension 3 you must give three values; with --dimension 2, give two. If you pass three values in 2D, the third is ignored with a warning. Since --dimension defaults to 3, set it explicitly to 2 for 2D cases.

I get "Missing cputime file" and no table is produced.

Each run must write a .cputime file, which only happens if the case enables measure_cpu_time (and measure_time_iteration_cpu_time for per-iteration timers) in its system block. If the file is missing in a dual-pass run, also check that the batch jobs have actually finished before launching the post-process stage.