Running a simulation
Two files control a run. sph.init chooses the kind of calculation, and
sph.input sets everything else. Both must be present.
The minimum sph.init is three lines, for example:
&INITT
INAME='erg'
&END
INAME is a three-letter code naming the setup routine. Two of them cover
most work:
ergBuild a star from a stellar-evolution profile, a MESA model for instance, and relax it into an SPH star. This is where nearly every project starts, and it is the subject of Relaxing a star.
hypPut two already-relaxed bodies on a Keplerian orbit and let them meet: a collision or fly-by between two stars, or between a star and a compact object. See A collision.
The usual sequence is therefore two runs: an erg run per star to make it,
then one hyp run to collide them.
The other thirteen initialization scripts
Code |
Sets up |
|---|---|
|
a single polytrope, from |
|
a single polytrope with a compact core |
|
a corotating binary |
|
a binary plus a single star |
|
a binary plus a black hole |
|
a binary encountering a single star |
|
a star whose particle-mass scheme varies with position |
|
a rescaling of an existing model |
|
a triple system |
|
an encounter with a supermassive black hole |
|
a general-relativistic setup |
|
a restart from an existing dump |
|
particles laid out from an ASCII image |
sph.init gives the same list with the routine each one calls.
Each code selects one initialization script, and they live in files named
initialize_*.f in parallel_bleeding_edge/src: initialize_parent.f
for erg, initialize_hyperbolic.f for hyp, and so on.
Which script you choose also decides which other files the run needs: a
stellar-evolution profile for erg, a relaxed star or two for hyp. See
Input files for what those are and where they come from.
The important thing about these routines is how little they do. A setup routine
runs once, before the first step, and its only job is to decide where the
particles start: their positions, masses, velocities and internal energies. It
then hands that state to the integrator and takes no further part. Everything
after that is the same code regardless of which INAME you chose: the
hydrodynamics, the gravity, the timestep control, and what gets written to
disk.
Two consequences are worth keeping in mind. A setting that controls the
evolution means the same thing for every INAME, so advice about cn1 or
nnopt carries across unchanged. A setting that describes the starting
state is read only by the routine that needs it, so e0 does nothing in a
relaxation and equalmass does nothing in a collision, and neither will warn
you.
See sph.input for every setting and its default.
Choosing the number of ranks
mpirun -np 8 ./my_run_gpu_sph
Worth testing on your own machine, but sixteen ranks is often a sensible ceiling. Beyond that, communication costs eat into the gains, and leaving cores free lets you run several simulations at once.
ngravprocs sets how many ranks compute gravity. It cannot exceed the number
of GPUs present: ask for more and it is clamped to what you have. Leave it at
0 to have it detected automatically.
GPU or CPU
make builds the GPU version and make cpu the CPU one. The GPU takes
over only the gravity: neighbour finding and the SPH sums run on the CPU either
way. That is why the two are indistinguishable on a small model and diverge
sharply on a large one. Gravity is the part that grows fastest with the number
of particles, and it is the only part the card touches.
The card computes gravity in single precision. Positions are sent to it
relative to the gas centroid weighted by \(1/h^2\), with the subtraction
done in double precision, so a star far from the origin, as near a
supermassive black hole, keeps its close separations. That point sits on a
dense core, and midway between two identical stars. Gas in two dense places
far apart, such as two stars many thousands of smoothing lengths from each
other, still loses precision on the card, and is better run with make cpu.
What was measured
Relaxing a one-solar-mass polytrope, INAME='erg' aside, with:
&input
n=N, nnopt=22, tf=1.0, dtout=1000.0, nrelax=1, treloff=0,
starmass=1.0, starradius=1.0,
&end
on four MPI ranks, on one machine: an RTX 5080 with the CPU build using all
four ranks for gravity. Both builds were checked to produce the same answer,
with identical W, U and total energy to every digit written, so the two
columns are the same work, not merely similar work.
\(N\) |
Iterations |
GPU, per step |
CPU, per step |
CPU/GPU |
CPU, whole run |
|---|---|---|---|---|---|
1 000 |
23 |
82 ms |
75 ms |
0.9 |
1.7 s |
4 000 |
30 |
68 ms |
69 ms |
1.0 |
2.1 s |
16 000 |
43 |
68 ms |
171 ms |
2.5 |
7.4 s |
64 000 |
64 |
129 ms |
1817 ms |
14 |
116 s |
128 000 |
80 |
225 ms |
7104 ms |
32 |
568 s |
The per-step columns are the comparison worth making. The iteration count is
not a constant of the experiment: every run above stops at the same tf, but
a bigger model takes shorter timesteps and so needs more of them, 23 steps at
\(N=1000\) against 80 at \(N=128\,000\). A table of total wall times
alone would mix that in with the cost of a step and show neither clearly.
What this means in practice
Below about \(N=4000\) it makes no difference. The GPU is fractionally slower, because there is not enough work to cover the cost of moving it to the card. By \(N=16\,000\) the CPU is taking two and a half times as long, and by \(N=128\,000\) it is thirty times.
So a relaxation of a few thousand particles is perfectly comfortable without a card, and the tutorials in this documentation are sized to run either way. A production model of \(10^5\) particles is not: the same job that occupies a GPU for a few minutes will occupy the CPU build for hours.
These figures are one machine and one kind of run. The protocol is written above so you can repeat it on yours rather than trusting the numbers, and it is worth doing before committing to a long job.
Warning
If you have built both versions, ./*_sph matches two files and the shell
hands you the alphabetically first, which is the CPU build. It runs, just
more slowly, and nothing says so. The give-away is the first lines of
output: the GPU build reports SPHgrav found 1 CUDA devices and
is running on ... with gpu 0, and the CPU build reports neither.
Block timesteps
By default every particle advances with one shared timestep, the smallest that any particle needs. In an encounter with a black hole, a handful of gas particles close to the black hole can need steps thousands of times shorter than the rest of the star, and the whole star then pays for them.
Setting nblock=1 gives each particle its own step instead, a power-of-2
fraction of the largest step dtmaxblk (dtout when dtmaxblk is zero
or less). The smallest allowed step is dtmaxblk/2**nbinmax. Each particle
is integrated with its own kick-drift-kick leapfrog. Particles that are not
active in a substep are drifted to the current time, and their smoothing
lengths and densities are predicted rather than solved. For the force
calculation their positions also include the half acceleration term, so that
they sit on the parabola of their step rather than its chord. Without it, a
star falling in a strong external field (a black hole’s) sees its inactive
particles lag behind by up to a dt**2/8, and momentum and energy drift at a rate
the timestep limits do not control. Two safeguards keep neighbours
consistent:
nblimit(default 2) keeps every particle’s step within a factor2**nblimitof its neighbours’ steps, and a particle whose neighbour suddenly needs a much shorter step is woken up early (Saitoh & Makino 2009).Any inactive particle with an active particle inside its kernel is refreshed: its smoothing length, density and related sums are re-solved at the current positions, so that the active particles see up-to-date values. The hydrodynamic forces then come from the same routine as with shared steps, using the neighbour lists of the active and refreshed particles.
When only a few particles are active, the work of a substep is arranged so that its cost depends on those particles rather than on the whole star. Their neighbours are found in a search tree that is kept across substeps and rebuilt only when particles have moved too far for it to be trusted, with a search radius enlarged to guarantee that every current neighbour is found. The active and refreshed particles are dealt out evenly across MPI ranks, whatever their particle numbers, and the black hole to gas timestep check is shared the same way.
The black hole to gas timestep limits from cn5, cn6 and cn7 are
applied to both members of each pair. Energies are written only when all
particles are in step, every dtmaxblk, and so is restartrad.sph: at the
first such time at least 1000 substeps after the previous one. Output files and
restarts work the same way in both modes, so a run can be restarted with or
without block timesteps.
Block timesteps need a dynamical run (nrelax=0) with nintvar=2,
ncooling=0, nkernel=2 and nselfgravity=1, and the code
stops with a message otherwise. They have been tested with the CPU gravity
build only.
Block timesteps work with automatic jumps ahead (rjump or
rjumpfactor). A jump waits for the next full synchronization, when every
particle is at the same time, and the block stepper then starts afresh from
the state the jump leaves, as it does on a restart.
In a star and black hole encounter, block timesteps should use a few times less CPU
time than shared steps. They may need cn5 and cn6 tightened together
(say, cn5=cn6=0.0025 rather than 0.01) to conserve energy as well as the
shared-step run.
nblockfull and dtforce exist for testing only. nblockfull=1
recomputes every particle’s smoothing length and hydrodynamics at every
substep, and dtforce replaces the shared timestep with a fixed value.
A run that starts from rest at t=0 across a discontinuity, such as a shock
tube, has \(\dot u=0\) at first, so the cn3 limit cannot act on the
first step and that step can be several times too long. nstartramp=1
makes the first step 1/64 of what the limits allow and lets each later step at
most double until the limits take over. By default (nstartramp=-1) it is
on only for a new shock-tube run, that is a periodic box read with
iname='pbx' and nrelax=0, and nstartramp=0 turns it off there too.
A restart never ramps.
Restarting
A restartrad.sph file is written every few iterations, overwriting the
previous one. If one is present when a run starts, it is used automatically.
You can also restart from any snapshot by renaming it to restartrad.sph.
A restart is not only a way to recover from a crash. It is also how settings
are changed part-way through a calculation, since sph.input is reread every
time: artificial viscosity can be turned off, the output interval changed, or an
orbital jump armed once you have seen how the first passage went. See
Jumping ahead on a wide orbit.
Note
Output files are numbered per stage, not per rank. A fresh run writes
log0.sph and energy0.sph, the first resume writes log1.sph and
energy1.sph, and so on. A resumed run does not append to
energy0.sph, which matters if you are parsing an energy trace across a
restart.