Skip to content

KOKKOS: fix host/device coherence bugs and include-group binning - #50

Merged
stanmoore1 merged 72 commits into
developfrom
kk_bugfixes
Sep 2, 2026
Merged

stanmoore1 merged 72 commits into
developfrom
kk_bugfixes

Conversation

@stanmoore1

Copy link
Copy Markdown
Owner

Summary

A collection of KOKKOS bug fixes, most of them host/device data coherence
defects: an array written on one side and never claimed with modified_*(),
or read on the side that is not current. These are invisible on a host-only
Kokkos build, where the two sides share an allocation, and show up as wrong
forces or energies on a real device.

Also included are two fixes to the KOKKOS neighbor binning, which ignored
neigh_modify include entirely, and a crash fix for fix qtpie/reaxff.

The changes fall into these groups:

  • Unclaimed writes and stale reads: pair dpd/fdt/energy, min_style fire,
    fix deform, fix shake, fix nh, comm_tiled, grid3d, bond quartic,
    the improper styles, Atom::set_mass(), and the set command.
  • force_clear(): the host side of the forces was cleared only in the
    branch without an include group, and only for the forces; the torques and
    the SPIN forces were never cleared on the host in any branch.
  • neigh_modify include: NBinKokkos and NBinSSAKokkos binned every
    owned atom and every ghost, where the plain NBin classes bin only the
    atoms an include group's pairs are built from. Atoms outside the group
    therefore appeared in the neighbor lists of atoms inside it.
  • Topology lists: ~NeighBondKokkos() ran on the functor copy Kokkos hands
    each loop and released lists that belong to Neighbor, so a bond, angle,
    dihedral or improper style without KOKKOS support crashed in setup.
  • DomainKokkos::remap_all() and image_flip() were shadowing the base
    class methods rather than overriding them, so their kernels had never run.
  • fix qtpie/reaxff read the pair style's device neighbor list and
    segfaulted; it now uses its own host-built list.
  • run_style respa is refused with the KOKKOS package (see Backward
    Compatibility).

Related Issue(s)

Several of the divergences reported in lammps#5037 are of the shapes fixed
here.

Author(s)

Stan Moore, Sandia National Laboratories

Licensing

By submitting this pull request, I agree, that my contribution will be
included in LAMMPS and redistributed under either the GNU General Public
License version 2 (GPL v2) or the GNU Lesser General Public License version
2.1 (LGPL v2.1).

Artificial Intelligence (AI) Tools Usage

AI tools were used for parts of this pull request and this section replaces
the default statement accordingly.

Most of the code changes here were generated by Claude Code, which was also
used to find the underlying defects: it ran a KOKKOS host/device
sync-debugging build over the examples/ tree, compared the thermodynamic
output against a stock KOKKOS build, and root-caused each divergence. The
AI-generated changes are the ones touching min_fire_kokkos,
fix_deform_kokkos and fix_deform, fix_shake_kokkos,
pair_dpd_fdt_energy_kokkos, comm_tiled_kokkos, domain_kokkos and
domain.h, modify_kokkos, atom_kokkos and atom.h, set.cpp,
respa.cpp, fix_qtpie_reaxff.cpp, fix_numdiff*, compute_born_matrix,
nbin_kokkos, nbin_ssa_kokkos, and the force_clear() and
init_topology_kk() changes in verlet_kokkos and neigh_bond_kokkos.

The remaining changes -- the improper style syncing, bond_quartic_kokkos,
grid3d_kokkos, fix_nh_kokkos::remap(), the NeighBondKokkos destructor,
and the first host-side clear in force_clear() -- were written by a human.

Every change, AI-generated or not, was reviewed and verified as described
below.

Backward Compatibility

One intentional break: run_style respa now errors out when the KOKKOS
package is active, where it previously ran and silently produced wrong
forces. rRESPA keeps per-level force copies and clears and sums them
through the plain LAMMPS arrays, without the host/device transfers that
run_style verlet performs, so on a GPU the results are wrong with no
indication that anything is amiss. An input combining the two will now stop
with an error instead of producing bad numbers. doc/src/run_style.rst is
updated with a .. versionchanged:: TBD note.

No other input syntax changes.

Implementation Notes

How correctness was verified:

  • Sweep: all 751 inputs under examples/ were run on a stock KOKKOS build
    and on a KOKKOS_DEBUG_SYNC build and their thermodynamic output compared
    column by column. Before these fixes 47 inputs diverged; after them none
    do.
  • Split-host A/B: the force_clear() fixes were checked on a build where
    OpenMP stands in for the device and Serial for the host, so the two sides
    are genuinely distinct on a CPU. With the host clear removed the total
    energy of a fourmol run climbs 371.81 -> 372.61 over four steps where the
    non-KOKKOS run holds 371.68; with it in place the two agree to the last
    digit. The same A/B confirms the include-group branch specifically.
  • Include group: on the peptide example with atom_modify first and
    neigh_modify include, E_pair was -31960.18 against -31864.22 without
    KOKKOS, from the first step and roughly constant thereafter -- the shape of
    a different set of pairs rather than of forces accumulating. The two runs
    now agree to the last digit.
  • Regression: ilves, reaxff qtpie, SPIN iron, melt, peptide, bench/in.lj,
    and the dpde/dpdrx shardlow examples are unchanged against their
    pre-change output and match their non-KOKKOS runs.
  • make check-whitespace check-permissions check-fmtlib check-homepage check-errordocs all pass.

Two notes for reviewers:

  • Domain::remap_all() and Domain::image_flip() becoming virtual activates
    a DomainKokkos kernel that had never run. That kernel omits the
    deform_vremap velocity correction, so a host fallback is used for that
    case; finishing the kernel instead may be preferable.
  • Atom::sync_host_arrays() / Atom::modified_host_arrays() are new virtual
    hooks that let a style declare a host write of the per-atom arrays without
    depending on KOKKOS. They are currently undocumented in the developer
    guide.

Testing has been on CPU only, with a single MPI rank; the fixes have not been
exercised on a GPU.

Post Submission Checklist

  • The feature or features in this pull request is complete
  • Licensing information is complete
  • Corresponding author information is complete
  • The source code follows the LAMMPS formatting guidelines
  • Suitable new documentation files and/or updates to the existing docs are included
  • The added/updated documentation is integrated and tested with the documentation build system
  • The feature has been verified to work with the CMake based build system

Generated by Claude Code

stanmoore1 and others added 30 commits August 24, 2026 15:28
pair_style dpd/fdt/energy/kk finishes its compute by copying duCond and
duMech to the host and running the reverse communication, which adds the
ghost contributions into the plain host arrays.  That write was never
declared, so the two copies were left apart with counters that said they
agreed: the sync in fix dpd/energy/kk then had nothing to copy and the
half step integrated the pre-communication values instead.

Found with the sync-debugging build, which reports

  [watch] pair:duCond: the host side was written without a claim and this
          sync_device has nothing to copy -- the device keeps stale data
  [stale] pair:duCond: device side read while host side is newer,
          from FixDPDenergyKokkos<Kokkos::Serial>::take_half_step()

and shows up as diverging thermo output from the tenth step on in
examples/PACKAGES/dpd-react (dpde-vv, dpde-shardlow, dpdh-shardlow).

(cherry picked from commit 65b2d13)
MinFireKokkos::run_iterate() synced the per-atom arrays to the device
once, took the device views and nlocal, and then ran the whole iteration
loop on them.  energy_force() inside that loop runs the communication,
the neighbor build and the fixes, so it can leave the newest data on the
host and can grow or reorder the arrays.  The integration kernels then
worked from a device copy the host had overtaken, and the modified()
after them found the host side claimed as well; nlocal could also be out
of date after a migration.

The sync-debugging build stops the run at that point:

  LAMMPS::DualView::modify_device ERROR: concurrent modification of host
  and device views in DualView "atom:x"
    LAMMPS_NS::MinFireKokkos::run_iterate<0, false>(int)

reproduced by examples/PACKAGES/pafi, where fix pafi has no KOKKOS
support and does its work on the host.

(cherry picked from commit f8d10ee)
force_clear() overwrites atom->fm and atom->fm_long on the device and
claims them there, the same way it does for f and torque, but only f and
torque had their host side released first.  A host side left claimed by
the setup was therefore still claimed when the device claim came, giving
two claimed sides with nothing to say which one is current.

Every input under examples/SPIN stops in the sync-debugging build with

  LAMMPS::DualView::modify_device ERROR: concurrent modification of host
  and device views in DualView "atom:fm"
    LAMMPS_NS::VerletKokkos::force_clear()

(cherry picked from commit d50b393)
comm_kokkos.cpp drops the claim that resizing a send or scratch buffer
leaves behind, because those buffers are filled through raw pointers on
whichever side packs them and the claim otherwise stands until something
claims the other side and the two collide.  comm_tiled_kokkos.cpp has the
same two functions and did not get the same treatment.

examples/balance/in.balance.neigh.rcb, which is what selects the tiled
communication, stops in the sync-debugging build with

  LAMMPS::DualView::modify_host ERROR: concurrent modification of host
  and device views
    LAMMPS_NS::CommTiledKokkos::grow_send_kokkos(int, int, ExecutionSpace)

(cherry picked from commit 0edfa53)
pair_style dpd/fdt/energy/kk claimed the force array on its execution
space at the top of compute(), before the kernels that fill it.  Between
the two it copies the energy changes to the host and runs their reverse
communication, and that copies the forces to the host as well and takes
the claim with it.  The forces the kernels then wrote were left
unclaimed, and because this style declares an empty datamask there is no
second claim from the integrator to cover them: the reverse
communication of the forces afterwards found nothing to copy and summed
the ghost contributions into the previous step's values.

  [watch] atom:f: the device side was written without a claim and this
          sync_host has nothing to copy -- the host keeps stale data
    LAMMPS_NS::CommKokkos::reverse_comm()
    LAMMPS_NS::VerletKokkos::setup(int)

Claim them after the kernels instead, as pair_style dpd/kk does.  With
this and the reverse-communication claim of the energy changes, the three
examples/PACKAGES/dpd-react inputs reproduce the reference build exactly;
before, they parted from it at the tenth step.

(cherry picked from commit a55c9cd)
Atom::set_mass() writes the per-type masses through the plain host array,
which leaves the device copy holding whatever it was allocated with and
nothing in the state to say so.  Two styles worked around it by claiming
and copying the masses in their own init() -- fix nve/kk and fix nh/kk --
and the other fourteen KOKKOS styles that read the masses on the device
relied on one of those two being in the input.

examples/SPIN has neither: fix nve/spin has no KOKKOS version, so nobody
claimed the write and compute temp/kk read masses that were never copied.
The sync-debugging build reports

  [watch] atom::mass: the host side was written without a claim and this
          sync_device has nothing to copy -- the device keeps stale data
  [stale] atom::mass: device side read while host side is newer,
          from ComputeTempKokkos<Kokkos::Serial>::compute_scalar()

and all twelve examples/SPIN inputs come out different from the reference
build; running them with the two copies of the masses forced into one
reproduces it exactly, which is what identifies the array.

Claim the write where it happens instead, by making the four set_mass()
overloads virtual and overriding them in AtomKokkos.  The two init()
workarounds are left as they are; they are harmless now.

(cherry picked from commit 0dab7fa)
FixDeformKokkos::pre_exchange() wrapped the whole base class call in a
host sync and a host claim.  Everything that call does to the per-atom
data goes through DomainKokkos -- image_flip(), remap_all(), x2lamda()
and lamda2x() -- which runs on the device and declares itself, so the
wrapper claimed the host side of arrays the device work had just claimed,
and the run stopped:

  LAMMPS::DualView::modify_host ERROR: concurrent modification of host
  and device views in DualView "atom:x"
    LAMMPS_NS::ModifyKokkos::pre_exchange()

reproduced by examples/VISCOSITY/in.nemd.2d at the first box flip.  The
host sync at the front was also too early to be of use to the one step
that needs it, the atom migration, which runs after two device kernels
have moved the coordinates on.

Give the migration its own virtual hook in FixDeform and bracket that
instead, leaving the device work to declare itself as it already does.

(cherry picked from commit 7303197)
Four of the kernels in MinFireKokkos::run_iterate() wrote the per-atom
coordinates or velocities on the device and never declared it.  The one
that matters most is the inertia reset, which steps the coordinates back
by half a step and is followed immediately by energy_force(): the forward
communication there syncs the host, finds nothing to copy and builds the
ghost positions from coordinates the device had already moved on from.

  [watch] atom:x: the device side was written without a claim and this
          sync_host has nothing to copy -- the host keeps stale data
    LAMMPS_NS::CommKokkos::forward_comm(int)
    LAMMPS_NS::MinKokkos::energy_force(int)
    LAMMPS_NS::MinFireKokkos::run_iterate<0, false>(int)

With this and the refresh around energy_force, examples/fire (in.fire,
in.fire_mod, in.meam.fire) and examples/PACKAGES/pafi reproduce the
reference build exactly.

(cherry picked from commit 7660543)
FixShakeKokkos::grow_arrays() synced the cluster tables to the device
before growing them.  grow_kokkos() grows the host side and hands the
plain shake_flag, shake_atom and shake_type pointers back to the base
class, which reads and writes the clusters through them in copy_arrays(),
the exchange packers and the destructor, so it is the host side that has
to be current there.  Syncing the other way left the grown host arrays
holding what they had before the last device update.

  [stale] shake:shake_flag: host side read while device side is newer,
          from FixShakeKokkos<Kokkos::Serial>::grow_arrays(int)

examples/rdf-adf/in.spce and in.spce.hbond stopped at the first step with
'Out of range atoms - cannot compute PPPM' and a pressure of -7e+300;
both now reproduce the reference build exactly, and examples/peptide and
examples/micelle, which also use fix shake, are unchanged.

(cherry picked from commit a3a56dc)
rRESPA keeps its own copy of the forces of each level and clears and sums
them through the plain LAMMPS arrays, and it calls the force computations
directly, without the transfers between the host and the device that
run_style verlet does for KOKKOS.  There is no rRESPA version in the
package to supply them.  Where the two sides have separate memory the
forces are simply wrong, with nothing to say so:

  [watch] atom:f: the device side was written without a claim and this
          sync_host has nothing to copy -- the host keeps stale data
    LAMMPS_NS::CommKokkos::reverse_comm()
    LAMMPS_NS::Respa::setup(int)

examples/relres/in.22DMH.respa ran to completion with the temperature at
2930 K instead of 292 K by the fiftieth step.  Refuse the combination.

Along the way, fix nvt/kk claimed the coordinates before the kernel that
writes them rather than after, so any copy running in between took the
claim and left the new coordinates unclaimed -- the same shape as the
dpd/fdt/energy claim.  Its two siblings, nve_v() and nh_v_press(), claim
after their kernels already.

(cherry picked from commit 52185ff)
fix numdiff, fix numdiff/virial and compute born/matrix numdiff all take
a finite difference by displacing the atoms through the plain coordinate
array and then calling the force computations directly.  With the KOKKOS
package those computations work from their own copies, and the copy that
brings the host side up to date runs after the displacement was written,
so it overwrites it: every difference was taken between two evaluations
of the undisplaced configuration.  The same happens to the forces, which
these styles clear, save and restore through the plain array.

  [watch] atom:x: the host side was written, never claimed, and is now
          lost; the write is between modify_device and sync_host, which
          discards it
          element 0 of 49152 changed from 0 to 0.0001

examples/numdiff/in.numdiff reported a relative force error of 2.5e-03
where the reference build gives 5.6e-09, and the restored coordinates
were off by the displacement, so the trajectory drifted and the total
energy stopped being conserved.

Add Atom::sync_host_arrays() and Atom::modified_host_arrays(), which do
nothing without the package and which AtomKokkos maps onto its existing
host transfers, and bracket the six places where these three styles read
or write the arrays directly.  This avoids a KOKKOS version of each of
them, and the same two calls are available to any other style that
reaches for the plain arrays.  The input now reproduces the reference
build exactly.

(cherry picked from commit cb16ae1)
Domain::remap_all() and Domain::image_flip() were not virtual, so the
versions in DomainKokkos hid them instead of overriding them.  Everything
reaches these through a Domain pointer -- fix deform, fix nh, fix bocs,
fix npt/cauchy -- so the host implementations ran and read and wrote the
plain per-atom arrays while the current copy was on the device, and the
KOKKOS versions were never called at all.  AddressSanitizer names the
read at the faulting line:

  use-after-poison
    LAMMPS_NS::Domain::x2lamda(double*, double*) domain.cpp:2433
    LAMMPS_NS::Domain::remap_all() domain.cpp:1713
    LAMMPS_NS::FixDeform::pre_exchange() fix_deform.cpp:828

Make both virtual.  The image flip kernel is a faithful copy of the host
loop, but remap_all's is not: it leaves out the velocity correction that
fix deform's 'remap v' applies to a wrapped atom.  Hand that case back to
the host implementation, with the arrays brought over and the write
declared, rather than silently drop the correction.

examples/VISCOSITY/in.nemd.2d now reproduces the reference build exactly,
and the reference build itself is unchanged, which is what says the host
path kept its meaning.

ModifyKokkos::setup() also ran the computes with no transfer around them,
unlike every other loop in that file; compute chunk/atom read the
coordinates on the host there.  Give them the same treatment as the
fixes.

(cherry picked from commit 26c0392)
The set command reads and writes the per-atom arrays through the plain
pointers.  With the KOKKOS package the current copy can be on the device,
so the values it read could be stale and the values it wrote were dropped
by the next transfer, with nothing to say so.

examples/MC-LOOP/in.mc moves one atom per Monte Carlo step with 'set atom
$i x' and takes the energy of the result, and one move in two thousand
came out at the energy of the unmoved configuration.

Bracket the action loop rather than each of the thirty keywords: the
command already walks the arrays it was given, and the two calls do
nothing without the package.

(cherry picked from commit fcbdc04)
fix qtpie/reaxff borrows pair reaxff's neighbor list when there is one, to
avoid building a second one, and reads it through the plain ilist,
numneigh and firstneigh.  The KOKKOS version of the pair style builds its
list on the device, where those are not filled in, so the fix indexed on
uninitialized values:

  AddressSanitizer: SEGV on unknown address
    LAMMPS_NS::FixQtpieReaxFF::init_storage() fix_qtpie_reaxff.cpp:665
    LAMMPS_NS::FixQtpieReaxFF::setup_pre_force(int)
    LAMMPS_NS::ModifyKokkos::setup_pre_force(int)

examples/reaxff/water/in.water.qtpie and in.water.qtpie.field both crashed
this way.  This fix has no KOKKOS version -- fix qeq/reaxff does, which is
why the same code path is never reached there.

Fall back to the list this fix requests for itself, which is the list it
already uses when there is no reaxff pair style at all, whenever the pair
style is a KOKKOS one.  Both inputs now run and reproduce the non-KOKKOS
results exactly.

(cherry picked from commit 3299c05)
Neighbor::init_topology() chooses between the all and the partial variant
of the topology list builders.  The partial one skips a bond whose type
has been made negative; the all one copies the type into the list without
looking at its sign.  A fix that turns bonds off does so after this choice
is taken, so it cannot be found by the scan of the types that follows and
has to be named instead.  The list in the host version names shake,
rattle and ilves; the KOKKOS version was never given ilves.

fix ilves therefore got the all variant, its negative bond types reached
the list, and the bond style indexed its coefficient arrays with them:

  AddressSanitizer: heap-buffer-overflow
    BondHarmonicKokkos<Kokkos::Serial>::operator()(
      TagBondHarmonicCompute<1, 1>, int const&, s_EV_FLOAT&) const
    bond_harmonic_kokkos.cpp:170     -- d_r0[type]

examples/PACKAGES/ilves/in.rhodo-ilves reported E_bond = 1.6e9 where the
run without the package gives 2537.99, every other term agreeing, and
later stopped with 'Non-numeric box dimensions'.  It now reproduces the
non-KOKKOS energies, and AddressSanitizer is quiet.

(cherry picked from commit c98911b)
The style synced and claimed the per-atom arrays itself at the top of
compute().  When host executing and device executing styles overlap, the
integrator deliberately keeps the force mask out of its own sync and
modify calls: the host force buffer is zeroed, the host styles accumulate
into it alone, and it is merged into the device buffer afterwards.  A
style that syncs for itself does not know that, and pulls the force array
back from the device in the middle of that accumulation, so the pair
force lands in the host buffer as well and the merge adds it twice.  In
rhodopsin with the bonded styles on the host, that dies a few steps later
with atoms out of range in PPPM.

The bond, angle and dihedral styles already leave this to the integrator;
the impropers were missed.
Same as improper/harmonic: each of these synced and claimed the per-atom
arrays itself at the top of compute(), which puts the force array back in
play in the middle of the overlap path, where the host force buffer is
zeroed, filled by the host styles alone and merged into the device buffer
afterwards.  The pair force then lands in the host buffer as well and the
merge counts it twice.

The integrator's own calls are the masked ones, which reduce to the plain
masks when nothing is excluded, so this loses nothing on the ordinary
path.  For the pair style it was issuing both the plain and the masked
form, and the plain one re-armed exactly what the masked one leaves out;
keep the masked one alone.

bond/quartic keeps its calls: it copies to the legacy host arrays in the
middle of compute() for the bond breaking that follows, which is a
different pattern and is being looked at separately (lammps#5037).

With rhodopsin and the bonded styles on the host, the run no longer dies
with atoms out of range in PPPM, and its trajectory now matches the same
run with every style on the device.
bond/quartic runs its kernel on the execution space, then copies the
positions, forces and bond topology to the legacy host arrays, breaks
bonds and corrects the 1-4 pair interaction there, and copies the forces
back.  That round trip starts from whatever the force array holds on this
side, and compute() never made sure it was current: on any path that
reaches it without the integrator's sync, the host pass reads one side
and writes the other.  Claiming the device side afterwards then collides
with the still pending host modification, which is the concurrent
modification abort reported in lammps#5037; suppressing that abort alone
leaves the wrong forces behind.

With the fourmol bond/quartic case, four steps under -sf kk came out with
a force error of rms 8e3 against the same run without KOKKOS; with the
sync they agree exactly.

Also turn off the host/device overlap while this style is in use, as
bond/hybrid does for a host executing sub-style: the overlap keeps a
separate host force buffer that is zeroed, filled by the host executing
styles alone and merged into the device buffer afterwards, and this
style's round trip would land in the middle of it.
The tiled forward and reverse comm post every receive into one host
buffer, each into its own stretch of it, and then walk the completions
with MPI_Waitany.  Inside that loop they copy the whole buffer to the
device, which is started while the receives that have not completed are
still writing into it: the copy reads those stretches early and puts them
on the device, and the later iterations copy them again.  The brick path
does not do this -- it waits for its one receive before copying.

Wait for all of them, copy once, then unpack.  Found by reading the two
paths against each other while looking at the tiled pppm failures in
lammps#5037; on a CPU the copy is synchronous and the window is narrow,
so this needs checking on a device before the tiled skips come off.
Remapping with a dilate group walks the atoms on the host, and tested
each one against "mask", which is the view for the execution space.  On
a device that is device memory read from host code, which is the
inaccessible memory space error for atom:mask reported against fix
npt/kk in lammps#5037.  Take the host view instead, and sync it once for
the loop rather than syncing the positions again for every atom.

On a CPU the device view is host readable, so this reads the right values
either way and the forces are unchanged (2.9e-15 relative on an npt run
with a dilate group, i.e. summation order); the fault it removes only
shows on a real device.
force_clear() zeroed the device force view and left the host side alone.
Anything that adds its forces there instead -- a style running on the
host, or a style without KOKKOS support adding into the plain LAMMPS
array -- therefore started each step from the forces it added last step,
and when the two sides were brought together those were counted again.
The total energy climbs step by step: with the fourmol improper case and
pair_style zero, which is what the force-style tests use, four steps came
out with a relative force error of 4 against the same run without KOKKOS,
and the total energy drifted from 95.617 to 96.108 where the reference
holds it exactly.  With the host side cleared as well the two agree to
the last digit and the energy is flat.

A run whose styles are all on the device never reads that buffer, which
is why this only shows when something runs on the host: the ewald and
pppm divergences in lammps#5037 are of that shape, since kspace_style
ewald has no KOKKOS variant at all and runs as a plain style inside the
KOKKOS run.
Kokkos passes the loop body a copy of NeighBondKokkos and destroys that
copy when the loop finishes, so ~NeighBondKokkos() ran on every bond,
angle, dihedral and improper build.  It released the topology lists that
belong to Neighbor, leaving neighbor->bondlist and its three siblings as
null pointers from the first neighbor loop onward.  A bond, angle,
dihedral or improper style without KOKKOS support reads those pointers
directly and crashed in setup.

Skip the release when the destructor runs on such a copy, the way the
other KOKKOS styles already do.  Also move the transfer of the topology
lists to the host out of the device branch, so the lists reach a style
without KOKKOS support on a host run as well.
The same change that moved this claim also refuses run_style respa with
the KOKKOS package, so the case the comment named as the one that hits it
can no longer happen.  Claiming after the kernel is right either way: a
claim taken up front is taken by whatever copy runs in between.
The host side of the forces was cleared only in the branch that runs
without an include group, and there only for the forces themselves.  A run
with neigh_modify include takes the other branch, where the loops zero the
device copy and leave the host side untouched, so the stale host forces
that branch was meant to remove survive after all.  The torques and the
SPIN forces are overwritten the same way and were never cleared on the
host in any branch.

Clear the host side next to every device loop that goes with it, for all
four arrays.  Each clear takes the same range as its device loop rather
than the whole view, so the atoms outside the include group are left alone
exactly as Verlet::force_clear() leaves them; that also makes the range in
the first branch agree with the loop above it, which reaches nall and not
the end of the array.

Both host views have to be cleared: the Kokkos host view that a style
running on the host adds into, and the plain LAMMPS array behind it that a
style without KOKKOS support adds into.

On a host-only Kokkos build the two sides share an allocation, so this is
redundant there and the results are unchanged; the runs it matters for are
the ones with a real device.
NBinKokkos::bin_atoms() put every owned atom and every ghost into the bins.
NBinStandard::bin_atoms() bins only the atoms an include group's pairs are
built from: the owned atoms of the group, which sorting has put first, and
the ghosts that are in the group.  The atoms outside the group therefore
turned up in the neighbor lists of the atoms inside it, and a run with
neigh_modify include computed pairs that the same run without KOKKOS does
not.

On the peptide example with atom_modify first and neigh_modify include the
difference is there from the first step, with E_pair at -31960.18 against
-31864.22 without KOKKOS, and it stays about that size for the whole run
rather than growing, which is the shape of a different set of pairs rather
than of forces accumulating.  With the group honored the two runs agree to
the last digit.

atom2bin is still set for every atom.  Nothing reads it for an atom outside
the group, and leaving it alone would leave a stale bin behind instead.
The same omission as in NBinKokkos, in the twin that the SSA neighbor build
uses: every owned atom and every ghost went into the bins, where
NBinSSA::bin_atoms() takes only the owned atoms of the include group, which
are the first nfirst, and the ghosts that are in the group.  The ghosts
still start at the number of owned atoms rather than at nfirst.

Without an include group the group bit is zero and the ghost range is the
number of owned atoms, so nothing changes there; the dpde/shardlow and
dpdrx/shardlow examples come out the same as without KOKKOS either way.
…hake

The host pack/unpack pair for the SHAKE unconstrained coordinates brings
xshake to the host before reading it and claims the host side after writing
it.  The device pair did neither: the pack read the device copy without
bringing it up to date, and the unpack wrote the ghost coordinates there
without claiming them.

Today the sequence in post_force() hides this, because
unconstrained_update() claims the device side immediately before the
communication, so the device copy happens to be the current one and the
sync that follows the communication has nothing to do.  Nothing in either
routine relies on that, though, and if the host side is the current one on
entry the pack sends whatever the device copy last held and the sync after
the communication then discards the ghosts the unpack just wrote.

Results are unchanged on a host-only build, where the two sides share an
allocation.
Brings in the fixes for the orphaned base-class eatom/vatom arrays in
the KOKKOS fixes and pair styles, the per-atom energy and virial of fix
shake/kk, the energy accounting of fix addforce/kk and fix efield/kk,
and the shared region contact list.

Four conflicts, all in code both sides had touched:

- region_block_kokkos.h, region_sphere_kokkos.h: this branch's openflag
  handling is kept, since running surface_exterior() first and
  surface_interior() only if it found nothing is the correct fix for a
  coordinate sitting exactly on a periodic boundary, where the other
  side's sum would have counted two contacts while both wrote index 0.
  It now takes the caller-owned contact list of the other side.  The
  shared d_contact view is gone with that change, the coordinate remap
  member added here stays.

- fix_addforce_kokkos.cpp, fix_efield_kokkos.cpp: same hunk, the
  comment explaining the host-to-device copy of atom-style variables
  from this branch, the condition from the other side, which also
  covers an atom-style energy or potential variable writing into the
  fourth column of the same array.

The three files that merged without conflict were checked by hand: the
sync fixes in fix shake/kk touch init(), grow_arrays() and the forward
comm and do not overlap the per-atom energy and virial work, and
k_remap.setup() in the two regions is untouched by the removal of their
now empty destructors.
NBinKokkos::bin_atoms() grew atoms_per_bin by a fixed increment of 16 and
re-binned every atom once per increment whenever a bin overflowed. For
skewed atom distributions - such as the large-cutoff bins the KOKKOS
package uses on GPUs, especially with pair_style hybrid - the busiest bin
can hold thousands of atoms, so this loop performs O(max bin occupancy)
reallocations and full re-bins. That makes the first neighbor build during
run setup take minutes and grow GPU memory monotonically (see issue lammps#4988).

Because the atomic increment in binatomsItem() runs for every atom even
after a bin is full, bincount holds the true occupancy of every bin after a
pass. Use a parallel reduction over bincount to size atoms_per_bin from the
actual maximum (plus a small margin) in a single step, so the loop
converges in at most two passes regardless of how skewed the distribution
is. Results are unchanged; only the setup cost is fixed.
The ComputeNeigh kernel in pair pace/kk and pace/extrapolation/kk caches
the short neighbor list in level-0 (on-chip shared) team scratch memory,
sized team_size*maxneigh*sizeof(int).  On GPUs shared memory is a very
limited resource, so runs with many neighbors and/or many atomic species
could abort with "Requested too much scratch memory on level 0" (CUDA) or
"could not find a valid team size" (HIP).  See lammps#5063.

Query the maximum available level-0 scratch from Kokkos via
TeamPolicy::scratch_size_max(0) and transparently fall back to level-1
(global memory) scratch when the request does not fit, printing a warning
the first time.  The limit is queried rather than hard-coded (e.g. 48 KiB),
so larger shared-memory limits such as the opt-in >48 KiB shared memory in
newer Kokkos are picked up automatically.

Add a "neigh" pair_style keyword (auto|shared|global) so the user can
override the automatic choice, and document it.

Checked on a Kokkos Serial build of the fcc-Cu deck: all three keyword
values run and give forces identical to the non-accelerated evaluator, an
unknown value is rejected by the KOKKOS styles, and the non-accelerated
styles stop with "Unknown pair_style pace keyword: neigh" as the docs now
state.  The scratch-level selection itself only takes effect on a GPU
backend and needs a CUDA or HIP build to exercise.
stanmoore1 added a commit that referenced this pull request Aug 31, 2026
The bugfix branch behind PR #50 and this one arrived at the same fixes
independently, so the two would have collided once that PR reaches
develop.  Adopt its text verbatim for every hunk both carry -- the
MinFireKokkos view refresh, and the include-group binning in NBinKokkos
and NBinSSAKokkos, down to the member names and the local-variable
structure -- so the merge is clean.

Only two hunks stay: the guards in binatomsItem() and binIDAtomsItem()
that stop a non-numeric coordinate or an out-of-range bin index before
the atomic update writes past the end of the bin arrays.
stanmoore1 added a commit that referenced this pull request Aug 31, 2026
The bugfix branch behind PR #50 and this one arrived at the same fixes
independently, so the two would have collided once that PR reaches
develop.  Adopt its text verbatim for every hunk both carry -- the
MinFireKokkos view refresh, and the include-group binning in NBinKokkos
and NBinSSAKokkos, down to the member names and the local-variable
structure -- so the merge is clean.

Only two hunks stay: the guards in binatomsItem() and binIDAtomsItem()
that stop a non-numeric coordinate or an out-of-range bin index before
the atomic update writes past the end of the bin arrays.
stanmoore1 and others added 9 commits September 1, 2026 01:57
MinFireKokkos takes k_mass.view_device() and divides by it in every one of its
integration kernels, but never syncs it.  The masses are written through the
plain host array, so on a GPU backend, and in the mixed and single precision
builds where the device copy is a separate allocation, that copy stays zero.
dtfm = dtf/mass is then inf and the coordinates turn into NaN on the first
iteration.  In a double precision host build the device view aliases the host
array, which is why this only shows up elsewhere.

The per-type masses are not covered by the sync()/modified() masks, so styles
that read them on the device have to sync them by hand.  Do that in init(),
the same way fix nve/kk and fix nh/kk do.
…tion

dtv was declared KK_FLOAT but is the receive buffer of

  MPI_Allreduce(&dtvone, &dtv, 1, MPI_DOUBLE, MPI_MIN, world);

so in the mixed and single precision builds MPI_Allreduce() wrote eight bytes
into a four byte object.  dtvone and the Kokkos::Min reducer feeding it are
already doubles, so make dtv one as well.

The integration kernels use dtv, so add a KK_FLOAT alias for them, following
the dtf_kk / dtf_init_kk / l_dmax_kk pattern already used in this file, rather
than promoting the kernel arithmetic to double.

Together with the preceding commit this makes min_style fire usable again in
mixed and single precision.  Verified with lj/cut on an fcc lattice for all
four integrators, with and without abcfire: double and mixed precision now
both reproduce the non-accelerated reference.  Before, mixed precision ran to
completion but never moved the atoms, reporting the initial energy and force
norm at every step.
Dump::write() temporarily replaces atom->x, atom->v, and atom->image by
plain host copies before calling domain->x2lamda(), domain->pbc(), and
domain->lamda2x() for "dump_modify pbc yes".  The Kokkos versions of these
three functions operate on the Kokkos views (atomKK->k_x etc.) rather than
on atom->x, so they silently worked on the real atom data instead of on the
copies.  With a triclinic box this produced several wrong results:

- the copy handed to the dump was never converted to lamda coordinates, so
  Domain::pbc() wrapped real space coordinates against the lamda bounds
  [0,1).  Coordinates came out shifted by up to one box length with image
  flags off by one.
- a second dump at the same timestep saw the stale host view left behind by
  the device/host sync sequence and wrote lamda coordinates instead of real
  space ones.
- the image flags of the real atoms were modified as a side effect.

Detect that atom->x no longer aliases the Kokkos managed array and fall back
to the base class implementations on the host in that case.  Nothing changes
for all other callers, where atom->x is the Kokkos array.

Fixes lammps#4923, fixes lammps#4940

(cherry picked from commit 3daee37)
All six fix-timestep-wall_region tests defined the region as
"block EDGE EDGE EDGE EDGE EDGE EDGE" (and a sphere of radius 10 for the
sphere test), which puts every atom farther from the wall than the cutoff.
The stored reference data was all zeros: zero forces, zero stress and a zero
global scalar, so the tests passed no matter what fix wall/region computed.

Size the regions so the atoms are within range of the walls.

The reference data was regenerated on the current develop base rather than
taken from the original commit, so that the skip_tests and input_coeffs
entries added since are kept.  The regenerated files carry the "generated"
tag, which marks reference data that has not been reviewed and validated yet.

(cherry picked from commit 9bd5a7c)
…fter

The test as written passed both with and without the fix in
DomainKokkos::pbc()/x2lamda()/lamda2x(), so it did not guard anything.  Its
single atom sat inside the box, where the remapping is a no-op, and it
compared the dump against a coordinate transform computed in the test rather
than against what the non-accelerated styles write.

Run the same input twice in one process, once without and once with the
KOKKOS package, and compare the two dump files atom by atom.  Let the atoms
drift out of the box with a short run first, which is the situation in which
the two disagree.  Without the fix every atom now comes out one full box
length away from the reference.

Also drop the std::set_terminate()/_Exit() workaround, which skipped
MPI_Finalize() and would have swallowed any real abort, and the Kokkos target
guessing in CMakeLists.txt: the test uses only the public LAMMPS interface, so
linking against lammps is enough.

Co-authored-by: Mitch Murphy <alphataubio@gmail.com>
Say in the comment that the caller owns the x, v and image buffers it has
substituted, so syncing those masks would overwrite the copies with the Kokkos
data they were made from.  Without that note the missing masks read like an
oversight and invite a "fix" that reintroduces the bug.

Also make detached_atom_x() const, it only inspects.
stanmoore1 and others added 13 commits September 1, 2026 18:48
Conflicts and how they were resolved:

src/KOKKOS/region_remap_kokkos.h, region_block_kokkos.h, region_sphere_kokkos.h
  develop grew its own device-side remap of the tested coordinate into the
  periodic box (commit "remap positions into the periodic box in region
  block/kk and sphere/kk"), which is the same fix as this branch carried, with
  different names: RegionRemapKokkos::capture() instead of setup(), the member
  called boxremap instead of k_remap.  develop's version is kept, since the
  five new region styles it also added use it.  This branch's data race fix on
  the contact list was re-applied on top of it: the list is passed in as a
  Contact* by the caller, MAXCONTACT gives its length, and the surface
  functions became const.  The destructors went with the device-side contact
  view, and point_on_line_segment(), inside_face() and find_closest_point()
  are const because the now-const surface path calls them.

src/KOKKOS/domain_kokkos.cpp
  both sides changed the group-bit overloads of x2lamda() and lamda2x():
  develop added the missing MASK_MASK to the device sync, this branch added
  the fall back to the host implementations when atom->x has been detached.
  Both are kept.

src/KOKKOS/nbin_kokkos.cpp
  both sides taught the binning to honor an include group.  develop's version
  is kept whole, since the merge had already taken its includegroup_* members
  in nbin_kokkos.h; dropping the conflicting statement would have left them
  set but never read.  The bin sizing from actual occupancy is untouched.

src/KOKKOS/Install.sh
  develop's new region styles were added.

The region_cone/cylinder/ellipsoid/plane/prism styles develop adds implement
only match_kokkos(), not the surface and contact path, and fix wall/region/kk
still accepts block and sphere only, so the contact list fix does not have to
be extended to them.

Verified after the merge: KOKKOS double, mixed and single all build and
reproduce the non-accelerated reference for min_style fire with every
integrator, with and without abcfire; dump_modify pbc yes on a triclinic box
matches the plain styles; fix wall/region/kk gives the same energy on 1, 2 and
4 threads; fix nve/kk with lj/cut/kk reproduces the plain thermo output; the
six fix wall/region tests and the dump_modify pbc test pass.
…ackage

The "neigh" keyword was parsed in the KOKKOS pair styles, which stripped
it from the argument list before forwarding the rest to the base class.
An input written for pace/kk therefore stopped with "Unknown pair_style
pace keyword: neigh" when run in a build without the KOKKOS package.

Parse the keyword in the non-accelerated styles instead and store the
request in a base class member the KOKKOS styles read, which is how
pair style snap handles its accelerator-only chunksize and parallelthresh
keywords.  The keyword is validated everywhere, so a typo is still caught
without the KOKKOS package, but it has no effect there.

This removes the settings() overrides in both KOKKOS styles along with
the argument copying they needed, which also leaked the copied array on
the error paths since error->all() throws when exceptions are enabled.
bond_harmonic/kk had its read_restart() fixed to assign the class members
instead of declaring locals with the same names.  angle_charmm/kk,
bond_class2/kk, dihedral_charmm/kk and dihedral_charmmfsw/kk were cloned from
the same template and still declare the locals, so after reading a restart
file the k_* members are left default constructed while coeff() would have
filled them.

Nothing reads those members again on the current code paths, and the d_* device
views keep the data alive through their reference count, so no result changes:
a restart round trip with angle charmm and dihedral charmm gives the same
energies before and after this commit, with and without the KOKKOS package.
This removes the inconsistent state rather than a visible failure, and brings
the four styles in line with bond_harmonic/kk.

Co-authored-by: Mitch Murphy <alphataubio@gmail.com>
The regression test for issues lammps#4923 and lammps#4940 was never executed.  The
workflows that run the full test suite build with the "most" preset, which
does not include the KOKKOS package, so the test was not even compiled
there, and unittest-kokkos.yml selects tests by name with a regular
expression the test did not match.  Add it to that expression.

The test also could not have passed in four of the six configurations that
workflow builds.  It asked for two threads unconditionally, which the serial
backend refuses to start; since the LAMMPS constructor ran inside a captured
stdout block, the error message was swallowed and the test binary died
without reporting anything.  Ask for two threads only when a threading
backend is available.

Finally it compared a KOKKOS run against a run without the package, which
only agree to the compared tolerance when both use double precision.  The
mixed and single precision builds store positions as float and drift apart
during the preceding run.  Compare two dumps of the same timestep inside a
single run instead, one with "pbc yes" and one without: wrapping an atom
into the box must not move it, so both have to write the same unwrapped
coordinates.  That is the property the two issues are about, it needs no
second run to compare against, and the remapping is done on host copies in
double precision, so it holds for every backend and precision.

Verified that the test still fails without the fix in domain_kokkos.cpp for
double/OpenMP, double/serial and single/OpenMP, with unwrapped coordinates
off by several box lengths, and passes in all three with it.
Conflicts in pair_pace_kokkos.cpp and pair_pace.rst between the neigh
scratch fallback on this branch and the CPU-backend kernels for pace/kk
on develop: keep both.  develop turned host_flag into a compile-time
constant, so the constructor no longer assigns it and only initializes
host_fallback and the two neigh_scratch counters.
- nbin/kk: the max-occupancy reduction ran in the default execution
  space while d_bincount lives in DeviceType, so the host binning of a
  GPU build read a HostSpace view from a device kernel as soon as a bin
  overflowed.  Give it a RangePolicy<DeviceType>.

- fix shake/kk: the angle statistics counted the central atom as well,
  where FixShake::bond_force() counts only the two outer atoms, so the
  printed angle counts disagreed with the plain style.

- verlet/kk: force_clear() zeroed both host copies of the force arrays
  on every step, on a GPU two full host passes that nothing reads in a
  run whose force styles are all on the device.  The host copies are
  only read without a preceding sync on the host/device overlap path,
  where F_MASK is excluded from the sync and the host styles accumulate
  into them, so zero them only when a force style runs on the host side.
  The scan of the force styles is factored into host_force_styles(),
  which run() now uses as well for the same decision.  The fused path
  skips force_clear() entirely and is unchanged.

- run_style respa: refuse only a pair, bonded or kspace style that runs
  on a device, instead of every run with the KOKKOS package.  The KOKKOS
  styles of a build without a device backend share their memory with the
  plain arrays and worked with rRESPA before, as do the plain styles.

- min sd/kk: nvcc refuses an extended host/device lambda in a member
  function with private access, which set_search_direction() had.
The batched CPU derivative kernel that pace/kk gained on develop gathers
its per-lane buffers from KK_FLOAT views and hands them to
pace_batched_derivative(), which takes KK_FLOAT pointers, but declared
the buffers as double.  That only compiles when KK_FLOAT is double, so
the single and mixed precision builds failed with

  error: cannot convert 'double*' to 'const KK_FLOAT*' {aka 'const float*'}

Declare the buffers as KK_FLOAT.

While there, fix the copies of the per-atom core repulsion factor and
extrapolation grade from the host mirrors into the base class arrays.
They used memcpy with sizeof(double)*chunk_size, but the mirrors are
KK_FLOAT, so in a single or mixed precision build that read past the end
of a float buffer and filled the double arrays with garbage.  Copy
element by element, which converts.
- min quickmin/kk: sync the per-type masses to the device in init()
- fix wall/harmonic/outside/kk: size the per-atom virial dual view in
  v_setup_peratom() with alloc = 0, after v_init() set vflag_atom
- compute reaxff/atom/kk: TAG_MASK/EMPTY_MASK datamasks and a host sync
  of the tags before compute_local(), instead of the ALL_MASK defaults
- compute temp/sphere/kk: sync and claim around the bias compute's
  remove_bias_all()/restore_bias_all(), as temp/deform/kk does
- pair mliap/kk: launch the comm packing kernels in RangePolicy<DeviceType>
  so the host instantiation does not run them on the device
- fix nvt/nph/npt/sphere/kk: remove leftover debug output
- comm_style tiled (plain and KOKKOS): reverse_comm(Pair*) honored the
  size argument backwards; match CommBrick
- fix shake/kk: pack_forward_comm() no longer claims a host write it does
  not make; drop the now unused i0
- pair uf3/kk: size the centroid virial with maxcvatom and free it in the
  destructor
- fix propel/self/kk: v_init(vflag,0), the per-atom virial is a dual view
- pair eam/kk: size the HIP team scratch with sizeof(KK_FLOAT)
- fix addtorque/atom/kk, settorque/atom/kk: release the base class array
  before the dual view takes it over
- fix qeq/reaxff/kk: struct/class tag mismatch on a friend declaration
- fix cmap/kk: tally the global virial and the per-atom energy and virial
  the way FixCMAP::post_force() does through Fix::ev_tally(): each
  crossterm split over its five atoms, only the owned ones counted.  The
  per-atom arrays are dual views sized after ev_init(eflag,vflag,0).
  Before this the fix's virial was zero and compute pe/atom and
  stress/atom silently omitted the CMAP term.
- pair hybrid/scaled under KOKKOS: a pair style that runs on the device
  but does not implement the KOKKOS packing communicated through a null
  KokkosBase pointer; both comm classes now take the host path for it
- remap/kk: wait for all receives before copying the shared scratch
  buffer to the device (the grid3d fix), fence the pack kernel before MPI
  reads the device buffer, copy only the message's own stretch of the host
  send buffer, and MPI_STATUSES_IGNORE for the Waitall
- pair dpd/kk: cache update->ntimestep on the host instead of
  dereferencing the Update object inside the team kernel
- pair pod/kk: drop the commented-out debugging dumpers, whose fwrite of
  a KK_FLOAT mirror with sizeof(double) was wrong in single precision
- fix spring/kk, fix wall/flow/kk, compute ave/sphere/atom/kk: sync the
  per-type masses to the device before reading them, instead of relying
  on the integrator having done it
- compute temp/profile/kk: sync the legacy host side of the masses, which
  is what the host loop reads; sync<LMPHostType>() refreshes the other side
- atom map: map_one() claims its host write; map_clear() releases both
  sides before it overwrites the whole array
- compute inertia/kk: the host sync before inertia_extended() now covers
  the radius, the ellipsoid indices and the bonus data it reads
- fix nh/sphere/kk, compute temp/sphere/kk: sync the host copies before
  the radius/mask loops of init() and dof_compute()
- fix rigid/small/kk and its Nose-Hoover variants: v_init(vflag,0), the
  per-atom virial is a dual view the fix zeroes itself
- pair gran/hooke/kk, gran/hooke/history/kk, gran/hertz/history/kk: stop
  claiming and copying a per-atom energy that is never written
- comm_style tiled/kk: MPI_STATUSES_IGNORE for Waitall, and take the send
  pointer after the pack that may resize the buffer
- NBin gets a copymode guard, set by nbin/kk and nbin/ssa/kk while a copy
  of the object lives in a device functor; likewise MLIAP_SO3Kokkos
- region ellipsoid/kk: rotate() locals no longer shadow the semi-axes
- pair lj/gromacs/kk: drop the cut_inner dual views nothing reads
pair_style hybrid/scaled/kk kept its accumulators as plain host arrays,
so every sub-style needed the forces (and torques) copied back and forth
between host and device on each timestep.  Do the work in Kokkos kernels
instead:

- fsum/tsum become dual views and the save/clear/scale-add/restore steps
  run as parallel_for kernels
- init_style() picks the memory space for the sum: the device when every
  sub-style runs there, so the forces stay resident on the device, and
  the Kokkos host space otherwise
- virial_fdotr is only synced to the device when it is actually needed

Per-atom scale factors from an atom-style variable are now communicated
through KokkosBase::pack/unpack_forward_comm_kokkos.  Previously the
forward communication of a Device pair style dispatched to the device
path and dereferenced a null KokkosBase, which crashed in compute() and
in single().  The variable itself is still evaluated on the host, so the
local values are uploaded and the ghost values filled in by the
exchange; single() and born_matrix() read them back from the dual view.

Verified against pair_style lj/cut with sub-style scale factors summing
to one (constant, equal-style, and spatially varying atom-style), with
atom_style sphere for the torque path, with a mix of /kk and plain
sub-styles, and against the CPU hybrid/scaled for compute group/group.
The force-style unit tests pass in double/legacy-layout and in
mixed-precision/default-layout builds.
Respa::init() called into the styles' KOKKOS execution spaces itself and
refused any force style on the device.  Move the decision into
KokkosLMP::respa_check(), so that respa.cpp keeps a single guarded call
and the dummy interface a no-op, and make the criterion what rRESPA
actually needs: every participant on the host side of the atom data.
That admits a build without a device backend as before, and now also a
device build whose styles all use the /kk/host suffix with comm, sort and
atom/map on the host; it refuses a style, fix or compute in the device
execution space and device-side communication, sorting or atom map, and
names what to change.

Drop the versionchanged directive from the run_style documentation: the
KOKKOS package never supported rRESPA, so nothing changed for users.
@stanmoore1
stanmoore1 merged commit d25b4df into develop Sep 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants