Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use global counter in thermostats #3884

Draft
wants to merge 17 commits into
base: python
Choose a base branch
from

Conversation

jngrad
Copy link
Member

@jngrad jngrad commented Sep 3, 2020

Description of changes:

fweik and others added 7 commits September 3, 2020 19:37
In the previous implementation, thermostat noise for different seeds
was correlated, the seed just shifted the sequence. For example with
seed offset X, the cross-correlation used to have a peak at lag X.

Co-authored-by: Jean-Noël Grad <[email protected]>
The SD external library uses Philox internally for both the CPU and
GPU implementations. SD now uses the global counter instead of the
simulation time divided by the time step, which are both mutable
and can lead to correlated sequences when changed by the user.
Remove overhead from the std::function wrapper and heap allocation.
Reduce sample sizes and increase tolerances.
@KaiSzuttor
Copy link
Member

wouldn't it be a good idea to have a global that monotonically increases with time. It may be useful also outside of the thermostat context at some point?

@jngrad
Copy link
Member Author

jngrad commented Sep 4, 2020

wouldn't it be a good idea to have a global that monotonically increases with time. It may be useful also outside of the thermostat context at some point?

Yes, this is how the global counter is implemented. But we cannot rewind it by resetting the system time. It could be made a property of the integrator, or a property of the system to be able to re-use it elsewhere.

This counter is incremented by the integrator and is completely
dissociated from the thermostat infrastructure.
Pass the current value of the integrator_counter to the force
kernels as a parameter instead of relying on external linkage.
There are two exceptions: thermalized bonds and constraints,
which were not designed to be extensible in this way, and
still rely on external linkage.
Header file thermostat.hpp no longer includes integrate.hpp.
Fixes clang-analyzer-deadcode.DeadStores warning.
@jngrad
Copy link
Member Author

jngrad commented Sep 8, 2020

Using the same global counter in all thermostats solves the correlation issue but creates new problems. Some thermostats have specific needs: DPD needs extra incrementation when a DPD particle interacts with a constraint, and LB is not incremented when the integrator is steepest descent. Using a global counter incremented by the integrator means thermostat.cpp must include integrator.hpp and thermostat users such as src/core/constraints/ShapeBasedConstraint.cpp must include both integrator.hpp and thermostat.hpp. Having one counter per thermostat removes these issues.

@jngrad
Copy link
Member Author

jngrad commented Sep 8, 2020

New attempt without the global counter in #3888.

@KaiSzuttor
Copy link
Member

KaiSzuttor commented Sep 9, 2020

IIRC, the idea of the counter based RNG is that by knowing the value of the counter and two particle ids we determine one random number (in fact we could get 4). If we need more than one random number per particle pair and integration, the whole logic brakes down, right?

@KaiSzuttor
Copy link
Member

LB is not incremented when the integrator is steepest descent

what do you mean by that?

@KaiSzuttor
Copy link
Member

DPD needs extra incrementation when a DPD particle interacts with a constraint

why?

@KaiSzuttor
Copy link
Member

Using a global counter incremented by the integrator means thermostat.cpp must include integrator.hpp

I think the counter state should be passed as an argument to all methods that need to know about it

force1 +=
dpd_pair_force(p, part_rep, ia_params, dist_vec, dist, dist * dist);
// Additional use of DPD here requires counter increase
dpd_rng_counter_increment();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this counter has never been incremented twice... the control flow is in this if branch or in the one below

@@ -144,3 +151,9 @@ int integrate_set_npt_isotropic(double ext_pressure, double piston,
bool zdir_rescale, bool cubic_box);

#endif

extern Utils::Counter<uint64_t> integrator_counter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those declarations should come before the #endif of the header guard

@jngrad
Copy link
Member Author

jngrad commented Sep 9, 2020

IIRC, the idea of the counter based RNG is that by knowing the value of the counter and two particle ids we determine one random number (in fact we could get 4). If we need more than one random number per particle pair and integration, the whole logic brakes down, right?

True for consumers of the thermostats. However, the thermostats functions can generate more than 1 random number per tuple (counter, seed, pid1, pid2) using the salt, because they are the one to call the RNG functions. This is used e.g. in NpT with NPTISO0_HALF_STEP1 and NPTISO0_HALF_STEP2. This cannot be used in DPD because the salt has to be known at compile time, while the DPD thermostat needs to generate an arbitrary number of random numbers per unique tuple, depending on the number of shape-based constraints in the system.

On a side-note, adding a new salt means shifting all the following salts in the RNGSalt enum, which is a silent change: scripts from one espresso minor version will generate different random numbers with the next espresso minor version where salts are shifted.


LB is not incremented when the integrator is steepest descent

what do you mean by that?

The function that increments the LB counters (CPU and GPU) is not called when using steepest descent:

if (integ_switch != INTEG_METHOD_STEEPEST_DESCENT) {
lb_lbfluid_propagate();

With a global counter, the LB RNG sequence will be incremented anyway, just like the other thermostats. Looking again at the code, this should not pose an issue.


DPD needs extra incrementation when a DPD particle interacts with a constraint

why?

Looking at the code, I would say it prevents correlation in the noise when you have more than one shape-based constraint. The shape-based constraint has a particle representation Particle ShapeBasedConstraint::part_rep which is default-constructed, i.e. its particle id is always -1, presumably to ensure real particles are numbered continuously.

This makes DPD quite different, because its counter gets out of sync with the number of integration steps, and gets incremented by system.integrator.run(0) while other thermostats don't. This is also why we have this logic at line 191 of the checkpoint test:

def test_thermostat_DPD(self):
thmst = system.thermostat.get_state()[0]
self.assertEqual(thmst['type'], 'DPD')
self.assertEqual(thmst['kT'], 1.0)
self.assertEqual(thmst['seed'], 42 + 6)


Using a global counter incremented by the integrator means thermostat.cpp must include integrator.hpp

I think the counter state should be passed as an argument to all methods that need to know about it

I tried in 486612d but this means adding an extra argument to all classes in the constraint infrastructure just for DPD. Or adding a getter function to get the value of the global counter variable (to avoid exposing it via extern), in which case you need to include integrate.hpp.

@KaiSzuttor
Copy link
Member

This cannot be used in DPD because the salt has to be known at compile time, while the DPD thermostat needs to generate an arbitrary number of random numbers per unique tuple, depending on the number of shape-based constraints in the system.

here the id of the particle representation of the constraint has to be used

@KaiSzuttor
Copy link
Member

Looking at the code, I would say it prevents correlation in the noise when you have more than one shape-based constraint. The shape-based constraint has a particle representation Particle ShapeBasedConstraint::part_rep which is default-constructed, i.e. its particle id is always -1, presumably to ensure real particles are numbered continuously.

Then this needs to change.

@KaiSzuttor
Copy link
Member

This cannot be used in DPD because the salt has to be known at compile time, while the DPD thermostat needs to generate an arbitrary number of random numbers per unique tuple, depending on the number of shape-based constraints in the system.

This would also not be the right approach. Actually, the interface for the random numbers should be such that it cannot be used the wrong way... Then we would see that we neither have the correct RNG interface for the NPT case nor have we solved the special case of constraint interaction via DPD (which should be handled just like any other particle particle interaction). The real issue here is that the particle can be default initialized which leads to the issue that you don't know which information of the particle representation in the constraint can actually be used and which not.

@KaiSzuttor
Copy link
Member

With a global counter, the LB RNG sequence will be incremented anyway, just like the other thermostats. Looking again at the code, this should not pose an issue.

isn't the correct behavior to not propagate the system's state at all in case of steepest descent since there is no notion of time here? not sure about that

@KaiSzuttor
Copy link
Member

I tried in 486612d but this means adding an extra argument to all classes in the constraint infrastructure just for DPD. Or adding a getter function to get the value of the global counter variable (to avoid exposing it via extern), in which case you need to include integrate.hpp.

What's wrong which the additional argument? Following your reasoning we should have more global variables.

@KaiSzuttor
Copy link
Member

On a side-note, adding a new salt means shifting all the following salts in the RNGSalt enum, which is a silent change: scripts from one espresso minor version will generate different random numbers with the next espresso minor version where salts are shifted.

Not an issue if you add the salt at the end?

@KaiSzuttor
Copy link
Member

offline discussion: the issue with the DPD interaction on ShapeBasedConstraint has it's origin in the fact that the particle representation in the Constraint is not a real particle and thus has not a meaningful particle id (which is in fact a part of the counter based RNG we are using). One possible solution could be to actually create the particle and only store the pointer to the particle in the constraint. The interaction of this solution with other parts of the code, however, is not clear.

*
* @return Vector of uniform random numbers.
*/
template <RNGSalt salt, size_t N = 3,
std::enable_if_t<(N > 1) and (N <= 4), int> = 0>
auto noise_uniform(uint64_t counter, int key1, int key2 = 0) {
auto noise_uniform(uint64_t counter, uint32_t seed, int key1, int key2 = 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think what is missing is an abstraction for retrieving the random numbers specifically for the case of particle ids

@jngrad
Copy link
Member Author

jngrad commented Sep 14, 2020

The DPD interaction currently prevents the use of a single counter, we'll need to find a long-term solution based on e.g. uuids (see #3892). Until then, the correlation bugs can be fixed by #3888, where thermostats still have their own counter.

@RudolfWeeber RudolfWeeber removed this from the Espresso 4.2 milestone Oct 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants