From 47f15b41a6e466f18683b4de29a5ec66f1373130 Mon Sep 17 00:00:00 2001 From: Han Wang Date: Tue, 2 Jun 2026 10:54:16 +0800 Subject: [PATCH 1/7] fix(cc): handle nloc==0 in DeepSpinPTExpt with phantom-atom padding Multi-rank spin MD can leave a rank with zero real local atoms when all atoms migrate to other subdomains. The with-comm AOTI artifact hits an intermittent SIGFPE (integer divide by zero) at runtime in inductor-generated shape arithmetic that uses nloc as a divisor. The graph was traced with nloc_min=1 and inductor lowered an even stricter nloc>=2 runtime-check which is silently bypassed because AOTI_RUNTIME_CHECK_INPUTS is unset by default. Whether the offending divide is actually emitted depends on inductor's code-gen choices, which vary across compiles -- hence the random nature of the failure (reproduced on CI run 26667802665). Fix: prepend two phantom atoms with empty neighbour lists ahead of the real atoms when nloc_real==0. The AOTI graph then runs with nloc==2, satisfying the inductor specialisation. Phantoms have no neighbours so they contribute zero atomic energy / force / virial, preserving the physically-correct 'this rank has no real atoms' result. comm_dict's nlocal is set to 2 so border_op writes received ghost features past the phantom slots; outputs are stripped of the phantom prefix before being scattered back to LAMMPS via select_map. --- source/api_cc/src/DeepSpinPTExpt.cc | 79 ++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/source/api_cc/src/DeepSpinPTExpt.cc b/source/api_cc/src/DeepSpinPTExpt.cc index 75b445085f..601c337306 100644 --- a/source/api_cc/src/DeepSpinPTExpt.cc +++ b/source/api_cc/src/DeepSpinPTExpt.cc @@ -371,12 +371,43 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, int nloc = nall_real - nghost_real; int nframes = 1; - // Build spin tensor for real atoms using bkw_map - std::vector dspin(static_cast(nall_real) * 3); - for (int ii = 0; ii < nall_real; ++ii) { + // Phantom-atom padding for the empty-subdomain corner case + // (``nloc_real == 0``). Multi-rank spin MD can land a rank with zero + // real local atoms when atoms migrate to other subdomains. The + // with-comm AOTI artifact, traced with ``nloc_min=1`` and lowered by + // inductor with an even stricter ``nloc >= 2`` runtime-check + // (silently bypassed because ``AOTI_RUNTIME_CHECK_INPUTS`` is unset by + // default), then SIGFPEs at runtime with an "integer divide by zero" + // inside inductor-generated shape arithmetic that uses ``nloc`` as a + // divisor. The failure is intermittent because inductor re-codegens + // across runs and only some compiles emit the offending divide. + // + // Fix: prepend two phantom atoms with no neighbours so the AOTI graph + // runs with ``nloc == 2``. The phantoms have an empty nlist row and + // therefore contribute zero atomic energy / force / virial, preserving + // the physically-correct "this rank has no real atoms" semantics. + // ``nlocal`` in the comm tensors is set to ``2`` so border_op writes + // received ghost features past the phantom slots; outputs are stripped + // of the phantom prefix before being scattered back to LAMMPS atoms + // via ``select_map``. + const int phantom_n = (nloc_real == 0 && nall_real > 0) ? 2 : 0; + if (phantom_n > 0) { + dcoord.insert(dcoord.begin(), static_cast(phantom_n) * 3, + static_cast(0)); + datype.insert(datype.begin(), static_cast(phantom_n), 0); + nall_real += phantom_n; + nloc_real = phantom_n; + nloc = nall_real - nghost_real; + } + + // Build spin tensor for real atoms using bkw_map (skip phantom prefix + // which keeps zero spin). + std::vector dspin(static_cast(nall_real) * 3, + static_cast(0)); + for (int ii = phantom_n; ii < nall_real; ++ii) { for (int dd = 0; dd < 3; ++dd) { dspin[static_cast(ii) * 3 + dd] = - spin[static_cast(bkw_map[ii]) * 3 + dd]; + spin[static_cast(bkw_map[ii - phantom_n]) * 3 + dd]; } } @@ -445,11 +476,16 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, nlist_data.shuffle_exclude_empty(fwd_map); nlist_data.padding(); - // Rebuild mapping tensor + // Rebuild mapping tensor. Phantom slots (when phantom_n > 0) get + // identity entries — they index into their own row and never appear + // in any other atom's nlist (their nlist rows are all -1 below). if (lmp_list.mapping) { std::vector mapping(nall_real); - for (int ii = 0; ii < nall_real; ii++) { - mapping[ii] = fwd_map[lmp_list.mapping[bkw_map[ii]]]; + for (int ii = 0; ii < phantom_n; ii++) { + mapping[ii] = ii; + } + for (int ii = phantom_n; ii < nall_real; ii++) { + mapping[ii] = fwd_map[lmp_list.mapping[bkw_map[ii - phantom_n]]]; } mapping_tensor = torch::from_blob(mapping.data(), {1, nall_real}, int_option) @@ -472,8 +508,16 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, } // Flatten raw nlist — the .pt2 model sorts by distance on-device. + // Phantom rows (all -1) are prepended below so the AOTI graph sees + // nloc == phantom_n + nloc_real_orig instead of 0. firstneigh_tensor = createNlistTensor(nlist_data.jlist, nnei).to(torch::kInt64).to(device); + if (phantom_n > 0) { + auto phantom_rows = torch::full( + {1, phantom_n, nnei}, static_cast(-1), + torch::TensorOptions().dtype(torch::kInt64).device(device)); + firstneigh_tensor = torch::cat({phantom_rows, firstneigh_tensor}, 1); + } } // Build fparam/aparam tensors @@ -588,6 +632,17 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, virial.assign(cpu_virial_.data_ptr(), cpu_virial_.data_ptr() + cpu_virial_.numel()); + // Strip the phantom prefix (see phantom-atom padding comment near + // ``select_real_atoms_coord``) so the ``bkw_map`` lookup below sees + // only the real / ghost atoms it was built for. The phantom slots + // carry zero forces because their nlist rows were all -1 — they + // produce no neighbour contributions, so dropping them is exact. + if (phantom_n > 0) { + dforce.erase(dforce.begin(), dforce.begin() + phantom_n * 3); + dforce_mag.erase(dforce_mag.begin(), dforce_mag.begin() + phantom_n * 3); + nall_real -= phantom_n; + } + // bkw map: map force from real atoms back to full atom list force.resize(static_cast(nframes) * fwd_map.size() * 3); force_mag.resize(static_cast(nframes) * fwd_map.size() * 3); @@ -612,6 +667,16 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, cpu_atom_virial_.data_ptr(), cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); + // Strip the phantom prefix from atomic outputs as well (see force + // block above). Phantom slots carry zero atomic energy / virial + // because their nlist rows were all -1. + if (phantom_n > 0) { + datom_energy.erase(datom_energy.begin(), + datom_energy.begin() + phantom_n); + datom_virial.erase(datom_virial.begin(), + datom_virial.begin() + phantom_n * 9); + } + atom_energy.resize(static_cast(nframes) * fwd_map.size()); atom_virial.resize(static_cast(nframes) * fwd_map.size() * 9); select_map(atom_energy, datom_energy, bkw_map, 1, nframes, From c43468fb7477fe71a2db566e1c461454a658d011 Mon Sep 17 00:00:00 2001 From: Han Wang Date: Tue, 2 Jun 2026 11:38:08 +0800 Subject: [PATCH 2/7] fix(cc): zero reduced energy on empty rank in DeepSpinPTExpt Codex flagged on PR #5485: phantoms have constant atomic-energy outputs that flow into 'energy_redu'. On the spin path the SpinModel doubles atoms internally, so both real and spin phantom halves contribute -- and 'output_map["energy"]' only exposes the real half after the '[:, :nloc]' slice. Subtracting only that real half (a first attempt) left the spin half leaking into the MPI-reduced LAMMPS total: CI run 26796476553 showed mpi-2 = -2.45 vs mpi-1 ref = -1.49. Simpler exact fix: a rank with no real local atoms contributes zero to the total energy by definition. The phantoms are pure scaffolding to satisfy inductor's nloc>=2 specialisation; their fitting output is a numerical artifact, not physics. Zero 'ener' directly when phantom_n > 0. Forces / force_mag / virial are unaffected because phantom outputs are coord-independent (no neighbours) so their derivatives are zero -- no analogous correction is needed there. --- source/api_cc/src/DeepSpinPTExpt.cc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/api_cc/src/DeepSpinPTExpt.cc b/source/api_cc/src/DeepSpinPTExpt.cc index 601c337306..a82006be4c 100644 --- a/source/api_cc/src/DeepSpinPTExpt.cc +++ b/source/api_cc/src/DeepSpinPTExpt.cc @@ -610,6 +610,23 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, ener.assign(flat_energy_.data_ptr(), flat_energy_.data_ptr() + flat_energy_.numel()); + // Zero the reduced energy on an empty rank. Phantoms have constant + // atomic outputs (per-type bias + zero-neighbour MLP) that flow into + // ``energy_redu`` -- and on the spin path the SpinModel doubles atoms + // so the bias contribution appears for both real and spin phantom + // halves; subtracting only the real-half exposed by + // ``output_map["energy"]`` after the ``[:, :nloc]`` slice leaves the + // spin-half leaking into the MPI-reduced LAMMPS total. The physical + // contribution of a rank with no real local atoms is zero by + // definition, so just clear ``ener`` directly. + // + // Forces, force_mag, and virial are unaffected because phantom atomic + // outputs are coord-independent (no neighbours) so their derivatives + // are zero -- no analogous correction is needed. + if (phantom_n > 0) { + std::fill(ener.begin(), ener.end(), static_cast(0)); + } + // Extract force: energy_derv_r (nf, nall, 1, 3) -> (nf, nall, 3) torch::Tensor force_tensor = output_map["energy_derv_r"].squeeze(-2).view({-1}).to(floatType); From b025f9ac81d32d977ad916b1a45a54310d424a1d Mon Sep 17 00:00:00 2001 From: Han Wang Date: Thu, 4 Jun 2026 08:53:42 +0800 Subject: [PATCH 3/7] fix(cc): pad aparam for phantom atoms in DeepSpinPTExpt empty-rank path The nloc==0 phantom-atom prefix padded dcoord/datype/dspin/nlist/mapping but not aparam_, so with dim_aparam > 0 the aparam tensor (shape {1, nloc, daparam}) was built against the padded nloc while aparam_ still held the pre-padding layout -- a shape mismatch (or a silently-skipped aparam when nloc_real==0 left aparam_ empty). Prepend phantom_n * daparam zero rows to aparam_ when phantom_n > 0 and daparam > 0, so the two phantom local atoms carry zero atomic parameters and the aparam tensor stays aligned with the padded local atoms. aparam_nall is false on this path, so aparam_ is a per-local-atom buffer. --- source/api_cc/src/DeepSpinPTExpt.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/api_cc/src/DeepSpinPTExpt.cc b/source/api_cc/src/DeepSpinPTExpt.cc index a82006be4c..90982a6ab5 100644 --- a/source/api_cc/src/DeepSpinPTExpt.cc +++ b/source/api_cc/src/DeepSpinPTExpt.cc @@ -395,6 +395,14 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, dcoord.insert(dcoord.begin(), static_cast(phantom_n) * 3, static_cast(0)); datype.insert(datype.begin(), static_cast(phantom_n), 0); + // Keep aparam_ aligned with the padded local atoms: the phantom atoms + // get zero-valued atomic-parameter rows so the aparam tensor built below + // (shape {1, nloc, daparam}) stays consistent with the padded ``nloc``. + // (aparam_nall is false here, so aparam_ is a per-local-atom buffer.) + if (daparam > 0) { + aparam_.insert(aparam_.begin(), static_cast(phantom_n) * daparam, + static_cast(0)); + } nall_real += phantom_n; nloc_real = phantom_n; nloc = nall_real - nghost_real; From ff691f1fde9cd1ec7113c311c7243fa7fa631a93 Mon Sep 17 00:00:00 2001 From: Han Wang Date: Thu, 4 Jun 2026 09:21:43 +0800 Subject: [PATCH 4/7] test(cc): cover the empty-rank aparam padding via numb_aparam DPA3 spin fixture Give the DPA3 spin fixture (deeppot_dpa3_spin{,_mpi}.pt2) numb_aparam=1 so the empty-subdomain MPI test exercises the phantom-atom aparam padding added in DeepSpinPTExpt (rank with nloc_real==0 must prepend zero aparam rows). The spin LAMMPS runner now supplies a uniform `aparam`, and the C++ with-comm load-failure test passes a uniform aparam to its DeepSpin compute calls (the shared fixture now has dim_aparam=1 and there is no default_aparam). The spin LAMMPS tests are self-consistent (mpi-N vs mpi-1), so no reference values change. Reuses the existing fixture rather than adding a new model. --- .../test_with_comm_load_failure_ptexpt.cc | 9 +++++++-- .../tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py | 7 ++++++- source/lmp/tests/test_lammps_spin_dpa3_pt2.py | 4 ++++ source/tests/infer/gen_spin.py | 18 ++++++++++++++++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc b/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc index 10111a41b7..418373945e 100644 --- a/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc +++ b/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc @@ -177,8 +177,10 @@ TEST_F(TestDeepSpinPTExptWithCommLoadFailure, single_rank_compute_succeeds) { double ener; std::vector force_, force_mag, virial; + // The fixture is built with numb_aparam=1; supply a uniform per-atom aparam. + std::vector fparam, aparam(natoms, 1.0); EXPECT_NO_THROW(dp.compute(ener, force_, force_mag, virial, coord, spin, - atype, empty_box, 0, inlist, 0)); + atype, empty_box, 0, inlist, 0, fparam, aparam)); } TEST_F(TestDeepSpinPTExptWithCommLoadFailure, multi_rank_compute_throws) { @@ -196,7 +198,10 @@ TEST_F(TestDeepSpinPTExptWithCommLoadFailure, multi_rank_compute_throws) { double ener; std::vector force_, force_mag, virial; + // The fixture is built with numb_aparam=1; supply a uniform per-atom aparam + // so the throw comes from the multi-rank dispatch, not a missing aparam. + std::vector fparam, aparam(natoms, 1.0); EXPECT_THROW(dp.compute(ener, force_, force_mag, virial, coord, spin, atype, - empty_box, 0, inlist, 0), + empty_box, 0, inlist, 0, fparam, aparam), deepmd::deepmd_exception); } diff --git a/source/lmp/tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py b/source/lmp/tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py index a47b354f55..ef7ca6e4da 100644 --- a/source/lmp/tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py +++ b/source/lmp/tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py @@ -106,7 +106,12 @@ lammps.timestep(0.0005) lammps.fix("1 all nve") -lammps.pair_style(f"deepspin {args.PB_FILE}") +# The DPA3 spin fixture is built with numb_aparam=1, so supply a uniform +# atom parameter. This exercises the aparam path in DeepSpinPTExpt, including +# the empty-subdomain phantom-atom aparam padding; a uniform value keeps the +# per-rank results self-consistent (real atoms get the same aparam regardless +# of the processor grid). +lammps.pair_style(f"deepspin {args.PB_FILE} aparam 1.0") lammps.pair_coeff(args.pair_coeff) lammps.compute("virial all centroid/stress/atom NULL pair") # Per-atom magnetic force components. LAMMPS does not expose ``fm`` diff --git a/source/lmp/tests/test_lammps_spin_dpa3_pt2.py b/source/lmp/tests/test_lammps_spin_dpa3_pt2.py index 5429fbb516..d11d29ed34 100644 --- a/source/lmp/tests/test_lammps_spin_dpa3_pt2.py +++ b/source/lmp/tests/test_lammps_spin_dpa3_pt2.py @@ -263,6 +263,10 @@ def test_pair_deepmd_mpi_dpa3_spin_empty_subdomain() -> None: empty-rank guard for the spin path (the with-comm artifact still runs on rank 1 with nloc_real=0). Compares against same-archive mpi-1 reference. + + The DPA3 spin fixture has ``numb_aparam=1`` and the runner supplies a + uniform aparam, so the empty rank also exercises the phantom-atom aparam + padding in ``DeepSpinPTExpt`` (PR #5485 review). """ out_mpi = _run_mpi_subprocess(nprocs=2, data_path=data_file_empty_subdomain) out_ref = _run_mpi_subprocess(nprocs=1, data_path=data_file_empty_subdomain) diff --git a/source/tests/infer/gen_spin.py b/source/tests/infer/gen_spin.py index b08d45060d..56b2f2a6cd 100644 --- a/source/tests/infer/gen_spin.py +++ b/source/tests/infer/gen_spin.py @@ -125,7 +125,14 @@ def _build_dpa3_mpi_yaml(yaml_path: str) -> None: "precision": "float64", "seed": 1, }, - "fitting_net": {"neuron": [5, 5, 5], "resnet_dt": True, "seed": 1}, + # numb_aparam=1 exercises the aparam path of DeepSpinPTExpt, including + # the empty-subdomain phantom-atom aparam padding (PR #5485 review). + "fitting_net": { + "neuron": [5, 5, 5], + "resnet_dt": True, + "numb_aparam": 1, + "seed": 1, + }, "spin": {"use_spin": [True, False], "virtual_scale": [0.3140, 0.0]}, } @@ -185,7 +192,14 @@ def _build_dpa3_single_yaml(yaml_path: str) -> None: "precision": "float64", "seed": 1, }, - "fitting_net": {"neuron": [5, 5, 5], "resnet_dt": True, "seed": 1}, + # numb_aparam=1 exercises the aparam path of DeepSpinPTExpt, including + # the empty-subdomain phantom-atom aparam padding (PR #5485 review). + "fitting_net": { + "neuron": [5, 5, 5], + "resnet_dt": True, + "numb_aparam": 1, + "seed": 1, + }, "spin": {"use_spin": [True, False], "virtual_scale": [0.3140, 0.0]}, } From 65043789e1fa399bab24efe412908ecef3df7ede Mon Sep 17 00:00:00 2001 From: Han Wang Date: Thu, 4 Jun 2026 09:59:31 +0800 Subject: [PATCH 5/7] test(cc): fix stale multi-rank simulation in spin with-comm load-failure test multi_rank_compute_throws simulated multi-rank via inlist.nswap=1, but the DeepSpinPTExpt dispatch keys on lmp_list.nprocs>1 (nswap is unsound for atom_style spin). With nprocs unset, multi_rank was false and the use_with_comm/with-comm-loader-failed throw never fired -- the compute returned without throwing (also note nghost==0 skips the message-passing fail-fast block). Set inlist.nprocs=2 so the test exercises the real multi-rank dispatch throw. Verified locally: both tests in the suite pass against the regenerated numb_aparam=1 DPA3 spin fixture. --- source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc b/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc index 418373945e..768c1947bf 100644 --- a/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc +++ b/source/api_cc/tests/test_with_comm_load_failure_ptexpt.cc @@ -194,7 +194,10 @@ TEST_F(TestDeepSpinPTExptWithCommLoadFailure, multi_rank_compute_throws) { deepmd::InputNlist inlist(natoms, ilist.data(), numneigh.data(), firstneigh.data()); convert_nlist(inlist, nlist_data); - inlist.nswap = 1; // simulate multi-rank without populating send/recv + // Multi-rank is keyed on nprocs (DeepSpinPTExpt.cc), not nswap; with + // has_comm_artifact_ true but the with-comm loader failed to load, the + // dispatch must throw. + inlist.nprocs = 2; double ener; std::vector force_, force_mag, virial; From 0bc627a499290c2c4009e3897363036a7c86fb89 Mon Sep 17 00:00:00 2001 From: Han Wang Date: Thu, 4 Jun 2026 10:05:02 +0800 Subject: [PATCH 6/7] fix(cc): shift phantom-padded mapping into the post-padding index space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the nloc==0 phantom-padding path, the rebuilt mapping resolved real/ghost rows to fwd_map[...] — a pre-padding local index. Since the phantom prefix shifts every real/ghost row by phantom_n, the resolved target must be shifted by +phantom_n into the post-padding local index space (no-op when phantom_n == 0). This branch is reached by the empty-subdomain test (multi-rank + with-comm + atom_modify map yes populates lmp_list.mapping). --- source/api_cc/src/DeepSpinPTExpt.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/api_cc/src/DeepSpinPTExpt.cc b/source/api_cc/src/DeepSpinPTExpt.cc index 90982a6ab5..8a76d80427 100644 --- a/source/api_cc/src/DeepSpinPTExpt.cc +++ b/source/api_cc/src/DeepSpinPTExpt.cc @@ -493,7 +493,12 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, mapping[ii] = ii; } for (int ii = phantom_n; ii < nall_real; ii++) { - mapping[ii] = fwd_map[lmp_list.mapping[bkw_map[ii - phantom_n]]]; + // fwd_map resolves to a *pre-padding* local index; the phantom prefix + // shifted every real/ghost row by phantom_n, so shift the resolved + // target into the post-padding local index space (no-op when + // phantom_n == 0). + mapping[ii] = + fwd_map[lmp_list.mapping[bkw_map[ii - phantom_n]]] + phantom_n; } mapping_tensor = torch::from_blob(mapping.data(), {1, nall_real}, int_option) From 94600920cd84ae4ce067eb359c3ae392cd7f23e0 Mon Sep 17 00:00:00 2001 From: Han Wang Date: Thu, 4 Jun 2026 10:08:46 +0800 Subject: [PATCH 7/7] docs(cc): note phantom-padded mapping shift is a defensive guard Clarify that the lmp_list.mapping branch with phantom_n>0 is structurally unreachable (set_mapping is single-rank only; phantom_n>0 is multi-rank only), so the +phantom_n shift is a no-op on every reachable path and is kept only to keep the mapping correct if that invariant ever changes. Corrects the prior commit's overstated reachability claim. --- source/api_cc/src/DeepSpinPTExpt.cc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/api_cc/src/DeepSpinPTExpt.cc b/source/api_cc/src/DeepSpinPTExpt.cc index 8a76d80427..9edd51474b 100644 --- a/source/api_cc/src/DeepSpinPTExpt.cc +++ b/source/api_cc/src/DeepSpinPTExpt.cc @@ -493,10 +493,14 @@ void DeepSpinPTExpt::compute(ENERGYVTYPE& ener, mapping[ii] = ii; } for (int ii = phantom_n; ii < nall_real; ii++) { - // fwd_map resolves to a *pre-padding* local index; the phantom prefix - // shifted every real/ghost row by phantom_n, so shift the resolved - // target into the post-padding local index space (no-op when - // phantom_n == 0). + // Defensive: this branch (lmp_list.mapping != nullptr) is single-rank + // only (set_mapping is gated on comm->nprocs==1 in pair_deepspin / + // pair_deepmd), while phantom_n>0 only occurs on a multi-rank empty + // subdomain, so the two cannot currently co-occur and the +phantom_n + // term is a no-op (phantom_n==0) on every reachable path. It is kept + // so the mapping stays correct -- resolving fwd_map's pre-padding local + // index into the post-padding local index space -- if that invariant + // ever changes. mapping[ii] = fwd_map[lmp_list.mapping[bkw_map[ii - phantom_n]]] + phantom_n; }