Performance and many cores¶
In this page we compute the same small ground states in several ways: with the kernels compiled or not, with one k-point or all k-points at once, in the two memory modes and on several cores. We will see that the energy does not change, and that only the time and the memory do. The measured timings, on many cells and against other codes, are in PERFORMANCE.md.
Where the time goes¶
Let us first ask where an SCF spends its time. At each iteration we solve one eigenproblem per k-point,
and the k-points only meet in the sum that gives the density and, in a metal, in the Fermi level. So the k-points are independent problems, which is where the parallel work is. Inside one k-point the work is a chain of small steps, one Fourier transform per band and small matrix products, and many threads do not help much there. This is why the code gives each process only a few threads, and leaves the other cores for the k-point pools of the section on pools.
The settings that decide this are environment variables, and they have to be
set before Python imports defuqumat. A normal launch on a CPU is
export DEFUQUMAT_THREADS=4 # threads of one process
export JAX_PLATFORMS=cpu # stay on the CPU even if a GPU is visible
python3 run.py
where run.py is a normal script with Calculator.from_file(...). In a
script, you can also set them with os.environ before the import. The
variables are:
DEFUQUMAT_THREADS: how many CPUs one process uses. If you do not set it, the code uses 4, because more threads on one k-point were measured to be slower.0oroffleaves the CPUs as they are. The code only reduces the set of CPUs it gets fromtasksetor Slurm, it never adds to it.OMP_NUM_THREADSdoes not set the number of threads here. A long run that stops and uses no CPU at all is a known stall of the thread pool at a small number of threads, and the cure is a largerDEFUQUMAT_THREADS, such as 8.JAX_PLATFORMS:cpuorcuda. If you do not set it, the code runs on a GPU when it finds one.DEFUQUMAT_CACHE_DIR: the folder where the compiled kernels are kept between runs,~/.cache/defuqumat/jaxif you do not set it.offswitches the cache off.DEFUQUMAT_MEMORY_MODE:speedormemory, the memory mode of the whole process, see below.DEFUQUMAT_K_BATCHandDEFUQUMAT_BAND_BATCH: how many k-points and how many bands are computed at the same time, see below.allmeans all of them.DEFUQUMAT_POOLS: the number of processes one calculation runs as, see the section on pools.DEFUQUMAT_MEM_FRACTION: the part of the GPU memory the code may take, 0.9 if you do not set it. Useoffwhen another program shares the card.DEFUQUMAT_GATHER_BUDGET: the size in MB above which each symmetrisation walks the symmetry operations one at a time, instead of holding a copy of the field for every operation. It is 64 if you do not set it, and0means that it always walks. The result changes only in its last digits.
Why is the first run of a script slow? The code is written in JAX, which compiles each kernel the first time it meets it, for the shapes of this crystal. Here we switch the cache off and compute the same SCF twice in one process:
import os
os.environ["DEFUQUMAT_CACHE_DIR"] = "off" # no kernels from earlier runs
import time
from defuqumat import Calculator
for run in (1, 2):
calc = Calculator.from_file("docs/figures/bands_silicon/si.in",
pseudo_dir="tests/data/pseudo")
start = time.perf_counter()
scf = calc.get_scf()
print(f"run {run}: {time.perf_counter() - start:5.1f} s, "
f"E = {scf.total_energy:.10f} Ry")
run 1: 10.7 s, E = -15.8392999169 Ry
run 2: 0.5 s, E = -15.8392999169 Ry
The first SCF takes 10.7 s and the second takes 0.5 s, with the same energy
to the last digit, so about 95% of the first run is compilation and not
physics. The second run reuses the kernels of the first, since the crystal,
the cutoff and the mesh are the same. With the cache switched on, as it is if
you do not set DEFUQUMAT_CACHE_DIR, the kernels are also kept on the disk,
so a second script with the same shapes does not compile them again. This is
why a time you measure has to be the time of a second call, never of the
first one. These times are from one desktop computer. On yours the two numbers
will be different, but the first run will still be much longer than the
second.
How many k-points at once: k_batch¶
We now choose how many k-points are solved at the same time. This is
k_batch. With k_batch=1 the code solves one k-point after the other,
so the memory holds the work of one k-point. With
k_batch=None it solves all k-points in one step, which needs the memory of
all of them at once. Here we take silicon on its \(6\times6\times6\) mesh and
compute the SCF both ways:
from defuqumat import Calculator
for k_batch in (1, None):
calc = Calculator.from_file("docs/figures/bands_silicon/si.in",
pseudo_dir="tests/data/pseudo", k_batch=k_batch)
scf = calc.get_scf()
print(f"k_batch={k_batch}: {calc.system.kpoints.nk} k-points, "
f"{scf.iterations} iterations, E = {scf.total_energy:.12f} Ry")
k_batch=1: 28 k-points, 7 iterations, E = -15.839299916940 Ry
k_batch=None: 28 k-points, 7 iterations, E = -15.839299916940 Ry
Both runs take 7 iterations and give the same total energy to all twelve
printed digits, which is what we expect, since k_batch changes only the
order of the work and not the equations. The 28 k-points are the irreducible
part of the shifted \(6\times6\times6\) mesh.
The values of k_batch are:
1: one k-point at a time. This is the default on a CPU, where it is also the fastest choice.- an integer: this number of k-points at a time.
None: all k-points at once. On a CPU this is slower and needs more memory, and on a GPU it is the fastest choice when it fits."fit": on a GPU, the largest number of k-points that fits on the card. This is the default in thememorymode on a GPU. On a CPU it gives one k-point at a time.
You give k_batch to the Calculator, or set DEFUQUMAT_K_BATCH for the
whole process. The bands have the same kind of setting, band_batch, which is
one band at a time on a CPU and all bands on a GPU.
One switch for the memory: memory_mode¶
On a GPU the first question is whether the calculation fits on the card.
memory_mode answers it with one setting, and it chooses the other settings
for you. "speed" keeps everything on the card at once. "memory" keeps only
a part of the k-points on the card, computes the projectors of the
pseudopotential only when they are used, and keeps the wavefunctions in the
memory of the host when not all k-points fit on the card at once. Before a run, calc.estimate() tells us what it will cost,
without allocating anything:
from defuqumat import Calculator
for mode in ("speed", "memory"):
calc = Calculator.from_file("docs/figures/bands_silicon/si.in",
pseudo_dir="tests/data/pseudo", memory_mode=mode)
size = calc.estimate()
print(f"{mode:6s} k-points at once {size.k_batch} of {size.nk}, "
f"bands at once {size.band_batch}, projectors {size.projectors}, "
f"peak {size.peak_bytes / 2**20:.1f} MB")
print(f"all k-points at once: peak {size.at_k_batch(None).peak_bytes / 2**20:.1f} MB")
speed k-points at once 1 of 28, bands at once 1, projectors store, peak 5.7 MB
memory k-points at once 1 of 28, bands at once 1, projectors rebuild, peak 4.8 MB
all k-points at once: peak 18.4 MB
On a CPU both modes solve one k-point and one band at a time, so the only
difference is the projectors, which the memory mode builds again when it
needs them instead of keeping them for all 28 k-points. This takes the peak
from 5.7 MB to 4.8 MB. If we instead solve all 28 k-points at once, the peak
grows to 18.4 MB, roughly three times more, and this is the memory that grows
with the k-mesh. For two atoms all of these numbers are small, and they
become the question for a cell of tens of atoms on a dense mesh.
The physics, and so the numbers, are the same in both modes up to round-off.
On a CPU the default is "speed", and on a GPU it is "memory". You give
memory_mode to the Calculator, or set DEFUQUMAT_MEMORY_MODE for the
whole process, and a setting you give for k_batch or band_batch wins over
the mode. The command defuqumat size scf.in prints the same estimate for an
input file, with every array in it, in GiB, so it is useful for large cells.
Many cores: k-point pools¶
We now run one calculation on several cores. With DEFUQUMAT_POOLS the
calculation runs as that number of processes, the pools, and each pool solves
its own part of the k-points. The pools add up the density and share it at
every iteration, so the result is the result of one process, up to the order
in which the k-points are added. You do not change the script. Here we take
silicon with one atom moved away from its site, so it has fewer symmetries and
more k-points, and run.py is:
from defuqumat import Calculator
from defuqumat.parallel import current_pools
calc = Calculator.from_file("docs/figures/forces_relaxation/si-displaced.in",
pseudo_dir="tests/data/pseudo")
scf = calc.get_scf()
forces = calc.get_forces()
pools = current_pools()
if pools.rank == 0:
print(f"{pools.size} pool(s), {calc.system.kpoints.nk} k-points: "
f"E = {scf.total_energy:.10f} Ry, max force {forces.max_force:.10f} Ry/bohr")
We run it once as one process, and then as two pools on one machine. Each pool gets its rank, and all pools get the address of the first one:
python3 run.py
export DEFUQUMAT_POOLS=2 DEFUQUMAT_COORDINATOR=localhost:23456 DEFUQUMAT_THREADS=1 JAX_PLATFORMS=cpu
DEFUQUMAT_POOL_RANK=0 python3 run.py &
DEFUQUMAT_POOL_RANK=1 python3 run.py
wait
1 pool(s), 18 k-points: E = -15.7874037086 Ry, max force 0.0603966971 Ry/bohr
[Gloo] Rank 0 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
[Gloo] Rank 1 is connected to 1 peer ranks. Expected number of connected peer ranks is : 1
2 pool(s), 18 k-points: E = -15.7874037086 Ry, max force 0.0603966971 Ry/bohr
The two [Gloo] lines are printed by the library that connects the pools,
and they say that each pool found the other one. One process and two pools
give the same energy and the same largest force to all printed digits, so
the pools change where the 18 k-points are solved and nothing else.
Under Slurm, the rank and the address come from the job, so the launch is
#SBATCH --signal=TERM@600
export DEFUQUMAT_POOLS=$SLURM_NTASKS DEFUQUMAT_THREADS=$SLURM_CPUS_PER_TASK
srun python3 run.py
with one Slurm task per pool and --cpus-per-task threads in each. The best
use of the cores is many pools with one or two cores each, since the threads
of one k-point give little. A calculation without symmetry, for example a
magnet with spin-orbit coupling, has the whole mesh as its k-points, and this
is where pools give the most. The ground state, the forces, the stress, the
relaxations, the band structure, the NSCF run and the density of states run
with pools, also for a spin spiral, a magnetic field and a
constrained moment. If you give checkpoint_dir, the SCF writes a checkpoint,
and it can continue with any number of pools, also with one process. The line
#SBATCH --signal=TERM@600 asks Slurm for a SIGTERM ten minutes before the end
of the job, and then all pools stop at the same iteration and write their
checkpoint.
GPUs¶
The same script runs on a GPU without any change, once JAX is installed with
GPU support. On a GPU the
defaults change: memory_mode is "memory", k_batch is "fit" and all
bands of a k-point are computed at once. In the memory mode, the memory on
the card grows very little with the k-mesh, so a dense mesh fits on a small card.
If you ask for "speed" and leave the other settings to it, the code first estimates the memory, and if the
calculation would not fit on the card it uses "memory" instead and gives a
warning. With band_precision="single" the bands of the SCF are computed in
single precision, while the density, the potential and the energies stay in
double precision. With band_precision="mixed" the SCF starts in single
precision and changes the bands to double precision in the same run, at the
first iteration whose threshold single precision cannot reach, so the state it
converges to is a double one. This is a mode for speed only, and no result in
this guide was computed in it. By default the code takes 90% of the memory of the card,
and DEFUQUMAT_MEM_FRACTION=off gives the default of JAX, which is what you
want when another program uses the same card. The speedups we measured on
several cards are in
PERFORMANCE.md.
Limits
With pools, only get_scf, get_forces, get_stress, get_relax,
get_bands, get_nscf and get_dos run. Every other method gives an
error that names it. More pools than k-points give an error in the SCF.
A band path or a grid with fewer k-points than pools runs, but each pool
then solves all of it. With pools, scf_solver='newton-krylov' runs with
its default Jacobian, jvp='finite-difference'. With jvp='autodiff' or
jvp='sternheimer' it gives an error. The forces and the stress give an
error with a method other than the default "autodiff". A starting state
(starting_from) from a run with pools also works when the k-points are
shared out differently in the new run. The states move to the pool that
needs them. A starting state from a run with pools, given to a run without
pools, gives an error. In that case you start from its density.
A fixed-spin-moment run (constrained_magnetization = 'fsm', 'atomic fsm'
or 'atomic fsm direction') also writes a checkpoint. This field changes at
every iteration, so the checkpoint stores the field that the scheme reached,
and a restart continues from that field. On bcc iron the restarted run takes
the same 26 iterations as a run without a stop. If you load such a state
yourself with load_state, you give calculation=, because the rest of the
field comes from the input file.
An applied field and a penalty constraint are saved normally.
checkpoint_every, the number of iterations between two checkpoints (10
if you do not give it), must be at least 1. A run that continues from its
checkpoint gives an error if you also give magnetization, because the
run already has its own. A checkpoint holds the wavefunctions, which can
be tens of gigabytes on a large cell, so you put checkpoint_dir on a
scratch directory. We have not timed the default of 10 on a large cell, so
you may need to set checkpoint_every yourself.
k_batch="fit" is chosen for the k-points of the SCF, and a band path or
an NSCF run uses the same number. It grows the number of k-points only when
all the bands of one k-point fit on the card, otherwise it stays at one
k-point. A function that gets k_batch="fit" as an argument with no
crystal to size it gives an error. DEFUQUMAT_K_BATCH=fit gives no error
there. A batch of k-points runs each Davidson step until the slowest
k-point of the batch has converged. This changes only the speed, and only
if the k-points need different numbers of steps, which they did not on any
cell we measured.
The memory mode only reduces the memory that grows with the k-mesh. The
memory of one k-point, of the dense grid or of the augmentation charges of
an ultrasoft or PAW dataset is the same in both modes. The memory of the
host has to hold all the wavefunctions. If you give your own k_batch or
projectors, or set DEFUQUMAT_K_BATCH, the code does not check that
"speed" fits, and it does not switch to "memory". A solve with more
bands than the SCF, such as a spectrum, is checked again for its own
number of bands, and it can switch to "memory" or to fewer bands at a
time for that solve only, with a warning. The piezoelectric tensor with
method="zstar_eu" and the spectra computed as a sum over states still
move all the wavefunctions to the card. A memory_mode you give to a
Calculator sets up that calculator, and its methods after the SCF follow
the same mode. These are the bands, the NSCF run, the DOS, the PDOS, STM,
transport, the structure factors, the spiral scan, the topological
invariants and the optical and magnetic spectra. The magnetic anisotropy
and the torque do not follow the memory_mode of the Calculator. They take it
from DEFUQUMAT_MEMORY_MODE and the platform.
band_precision="single" works only for the SCF of a collinear
norm-conserving calculation, with one or two spin channels. An ultrasoft
or PAW dataset, spinors, a spiral, DFT+U and a meta-GGA give an error.
The forces, the stress and every response give an error on a calculation
whose bands are in single precision, because a gradient taken through
single-precision bands carries its round-off as a plausible force. This
also holds for a Calculator built with "mixed", which keeps its
single-precision setup. For these, you converge again in double
precision, starting from the single-precision result with
starting_from. A conv_thr below \(10^{-7}\) Ry gives an error in single
precision, because the energy does not follow it there. In single
precision the lowest threshold of the eigensolver is
\(4\,\epsilon_{32}\,E_\mathrm{cut}\), with \(\epsilon_{32}\) the machine
precision of float32 and \(E_\mathrm{cut}\) the value of ecutwfc, because
\(10^{-13}\) Ry would stall the eigensolver at its budget. On a GPU, single
precision makes the float32 matrix products run at full precision, without
TF32, for the whole process. A calculation moved to a new geometry or to a
band path builds its projectors again in double precision, so it runs fully
in double precision and does not stay in single precision.
The code gives a RuntimeWarning, once per run, when the eigensolver uses
its whole budget of 100 steps and leaves bands not converged. The steps of
each iteration are in scf.history[i]["davidson_iterations"].
DEFUQUMAT_ETHR_MIN sets the lowest threshold of the eigensolver. If you
do not set it, this threshold is \(10^{-13}\) Ry in double precision, as in
pw.x, and the value above in single precision. A higher value costs
accuracy in the energy and in the forces.
On an ultrasoft or PAW dataset, the table of the augmentation charges
reaches \(|\mathbf G| = 2\sqrt{E_\mathrm{rho}}\), with \(E_\mathrm{rho}\) the
value of ecutrho, as in pw.x. A strain so large that \(|\mathbf G|\) goes
past this end gives NaN, not a value clamped at the end of the table,
because a clamped value would look smooth and of the right size but give
a wrong stress.
Every number we validated was measured with projectors="store", which
keeps the projectors in memory. The memory mode uses
projectors="rebuild", which builds them again when they are used.
The notebook for this page is 03, the eigensolver and performance.