In this note, we comment on an implementation detail that accounts for a 10x speed-up to a naive KMC implementation: memoization of rates. Memoization is the act of storing rates computed in the course of the simulated so that they do not have to be re-computed again if needed. We summarize why doing this makes sense in the KMC context and give two consequences of its implementation which improves both performance and memory usage.

## Motivation

The rate of any event in our KMC simulations are a function of a local neighborhood about the atom performing the event. In the GaAs simulations, for example, the small neighborhood is a 5×5 neighborhood centered at an atom and all rates for events of the atom depend solely on the species of all 25 atoms in the neighborhood. *A priori* there are such neighborhoods, and hence it would be impossible to precompute the rates for all neighborhoods. Indeed, assuming 12 events per atom and 8 bytes to store the rate of each event, we would need a 96 PB table. We do not have a system with this much memory.

Such a look-up table would be nice to avoid the floating-point computations needed to compute the rates. Instead, we use a table that stores recently computed rates for neighborhoods encountered during the normal course of the simulation. That way, if we ever encounter a neighborhood previously seen, we have the rates at hand, saving a lot of work. This is called memoization of rates.

KMC simulations are particularly amenable to memoization because the set of “active” local neighborhoods — that is all the neighborhoods about all atoms at a particular time in the simulation — is small (relative to # atoms) and slowly changing. Consider the diffusion of an Ga adatom:

After the diffusion event, we see that the adatom maintains the same local neighborhood as before. In fact, the set of local neighborhoods remains the same after the diffusion — any local neighborhood in the right configuration can be found in the left.

In fact, we can measure the number of active neighborhoods during an actual simulation. We consider the Ga droplet growth and crystallization experiments previously discussed in the notes. The experiment proceeds in three phases.

- Ga atoms are deposited on an As terminated surface at a rate of 0.1 ML/sec until ML of Ga has been deposited. This forms liquid droplets.
- The system is allowed to anneal in the absence of any deposition for 30 seconds.
- An As flux is introduced at a deposition rate of 0.1 ML/sec until the droplets are fully crystallized.

The simulation was performed on a domain size of 256×512 = 131072 atom positions. At periodic steps, we measure the number of 5×5 neighborhoods in the atomic configuration at those steps, allowing us to plot # active neighborhoods vs. # MC steps:

The dashed red lines indicate the start of a different experimental phase ( the beginning of annealing, then crystallization). We see an initial jump from the small number of neighborhoods found in the initially flat, As-terminated substrate. As the configuration fills in to a Ga-terminated substrate, the number of neighborhoods increase at first, then relax before nucleation of liquid droplets start introducing neighborhoods not seen before. This leads to a slow increase in neighborhoods which also relaxes during the annealing stage as the system equilibriates. When crystallization begins, many new neighborhoods show up, as indicated by the big jump in the above plot. However this behavior also levels off as the droplet becomes fully crystallized.

From this plot, we see that the number of neighborhoods active during the simulation is relatively small compared to the *a priori* upper bound of neighborhoods. We also see that their growth is slow vs. # mc steps and there are periods of no effective growth. Both of these observations imply that there is a lot of redundancy in space and time of the neighborhoods and hence the memoization of rates will probably save us a significant amount of work.

## Hash table Implementation

As indicated above, using an array indexed by local neighborhood is impossible to do because we require a 50-bit index to represent an atom’s local neighborhood (5×5 atoms and 2 bits per atom to represent 3 species). Instead, we implement the memoization by hash table, which makes sense given the small number of active neighborhoods relative to the size of the space of all neighborhoods.

Here we briefly recall the basics of hash tables. Let be the set of all neighborhoods (so that for our example ). The first ingredient in a hash table scheme is a *hash function:*

,

where is some integer. We assume . Then by pigeon hole principle, there exists such that . We call such pairs *collisions*.

Instead of having a lookup table of size , we use an array of size . Every entry of the array stores a pair where is the set of rates associated with neighborhood . We would like to use as a look-up table, so that entry is located at index . But the presence of collisions makes this impossible. So we must perform some work to deal with collisions. The way we do this is by a scheme called *linear open addressing*, which means entry is stored hash table at index of the first unoccupied entry after index .

### Example of hashing

Throughout the simulations, we continuously query our hash table for rates . To see how the hash table is used, we consider a toy example where we desire the rates associated with three neighborhoods where form a collision: .

We first encounter a neighborhood and we desire the rates . We query the hash table for . We search the table starting at until we either encounter the proper entry for or an empty spot, which tells us is not in the table.

Hence we need to calculate the rates :

A similar thing occurs when we encounter neighborhood , which maps to .

No collision occurred and was not in the table, so rates must be computed:

When the simulation encounters neighborhood , it queries the hash table for $r(z)$. In this case, there is a collision because :

Again, we traverse the table until either is found, or a free space is encountered. In this case, a free space is found, meaning was not in the table so once again we must calculate :

Finally, suppose we encounter the neighborhood again. We query the hash table for :

We again traverse down the list until either a free space is encountered (meaning was not in the table) or we find . In this case, is in the table, so we do not need to calculate .

Queries to the hash table proceed as in the above manner, adding entries into the table for all neighborhoods not encountered before. As illustrated above, if the hash function maps neighborhoods close together, long chaining results. This is bad for performance, as longer chains means longer times to find neighborhoods within chains. In the extreme case that is constant, we get one long chain, and look-ups take time # neighborhoods. This is undesirable, so some care must be taken to define a hash function that avoids long chaining.

### Searching for good hash functions

We search for a good hash function by a simulated annealing algorithm. We assume the function is of the form:

,

where we think of as a 50-bit number and means a (non-cyclic) right shift by bits and is the XOR operation. In other words, is a linear combination of contiguous portions of . While very simple, such a form suffices for the simulations we consider (as we see in the sequel).

We generate a set of neighborhoods observed in the liquid Ga droplet formation/crystallization simulations. For the tuple , we define the number of collisions in .

Starting with uniform random , we perform a random mutation

,

where , chosen uniformly and independently. We accept the mutation according to a Metropolis selection rule:

where is some notion of inverse temperature. Iterating over long time and several starting points yields an optimal and hence a hash function that attempts to minimize collisions in the training set.

Here is a plot of number of collisions vs. , number of mutations for three independently selected . We see that our simulated annealing yields efficient hash functions after 2000 mutations:

We use the values throughout our simulations.

## Memory Gains

Using a hash table, rates of events are now indexed by neighborhood, instead of indexed by atoms. Several atoms have the same neighborhood, so by collecting rates by neighborhood instead of associating them to individual atoms, we end up saving memory.

Consider the implementation of the CDF we had used previously. We had a binary tree in which the leaf nodes represented the rates of events for each specific atom. If we have atoms and potential events per atom, then there are leafs in the tree and a total of nodes total:

If we instead store rate information by neighborhood, within the hash table, we can remove redundancy from the last layers of the CDF by eliminating repeated leaf nodes for all atoms that have the same neighborhood, and hence the same rates:

Then the CDF tree (terminated at the atom level) will have a total of nodes, while each active neighborhood is associated with an event CDF tree, each with a total of nodes, a total of nodes if there are neighborhoods in the hash table. If we assume , then we have eliminated nodes — a significantly large portion of the nodes!

## Simulation Results

We perform the Ga droplet formation and crystallization simulations, varying Ga thickness in order to establish a relationship between number of MC steps (which increases with ) and wall clock time. This gives us a rough measure of performance. We do so with and without memoization. When memoization is turned off, we compute explicitly the rates of all affected atoms at each MC step. Here is a plot of wall clock time vs. MC steps:

Here the blue triangles correspond to simulations with memoization, while red circles are simulations without memoization. We see the expected linear relationship between time and MC steps, and by examining the slope of the fitted lines, we observe a 10x speed-up when memoization is implemented.

Moreover, we counted the number of times the hash table was queried in a specific simulation. When we deposit monolayers of Ga and fully crystallize as described above, the hash table was queried 557821194 times after 18532977 MC steps (resulting in an average of 30 neighborhoods queried per step). Among those queries, 557750012 neighborhoods were already in the table with precomputed rates. That is, a neighborhood’s rates were found in the hash table 99.987% of the time.

## Leave a Reply