3737
3838#include < iterator>
3939#include < span>
40+ #include < type_traits>
4041#include < utility>
42+ #include < vector>
4143
4244template <class KokkosRangePolicy = Kokkos::RangePolicy<>>
4345ESPRESSO_ATTR_ALWAYS_INLINE inline void
@@ -101,6 +103,32 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
101103 // but ghost particles from other ranks may have larger particle ids;
102104 // -1 is used as a sentinel value for particle ids from other threads
103105
106+ // Component-major layout only: gather the position column into an
107+ // INTERLEAVED scratch buffer once per Verlet build. The build examines
108+ // O(N * candidates) positions by row; under LayoutLeft each such read
109+ // touches three far-apart component streams (three cache lines per
110+ // candidate), which perf measured at ~2x the build cost at 4000
111+ // particles/core. The O(N) sequential gather below is prefetch-friendly
112+ // and runs at rebuild cadence only. The kernels consume base + runtime
113+ // strides, so they read the scratch ({3, 1}) and the native column
114+ // (view strides) through the same code path. Under particle-major the
115+ // column is already interleaved and the scratch is skipped entirely.
116+ std::vector<double > interleaved_positions;
117+ if constexpr (std::is_same_v<ParticleStore::StateVectorLayout,
118+ Kokkos::LayoutLeft>) {
119+ if (not cells.empty ()) {
120+ auto &store = cells.front ()->store ();
121+ auto const &position_view = store.position_view ();
122+ auto const total = store.number_of_particles ();
123+ interleaved_positions.resize (3u * total);
124+ for (std::size_t row = 0u ; row < total; ++row) {
125+ interleaved_positions[3u * row + 0u ] = position_view (row, 0 );
126+ interleaved_positions[3u * row + 1u ] = position_view (row, 1 );
127+ interleaved_positions[3u * row + 2u ] = position_view (row, 2 );
128+ }
129+ }
130+ }
131+
104132 // Phase 7a perf fix: iterate the cells' store-ROW bags directly and REBIND
105133 // two cached views (p1 + partner) per work item via
106134 // Particle::attach_to_store, instead of driving RowParticleRange iterators
@@ -109,7 +137,8 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
109137 // work item (one cell per Kokkos work item) is thread-safe. Iteration ORDER
110138 // is unchanged. Carriers stay default and are never read while attached.
111139 auto intra_kernel = [&cells, &box_geo, &verlet_criterion, &id_to_index,
112- &intra_operator, max_id](const int i) {
140+ &intra_operator, &interleaved_positions,
141+ max_id](const int i) {
113142 auto &store = cells[i]->store ();
114143 // Contiguous store-row range (phase 7c); clean store, so index directly.
115144 auto const offset = cells[i]->offset ();
@@ -122,7 +151,23 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
122151 // Particle views stay attached for the verlet_criterion (it may read
123152 // types/flags); iteration order and arithmetic are unchanged.
124153 auto const *const id_column = store.id_view ().data ();
125- auto const *const position_column = store.position_view ().data ();
154+ // Layout-agnostic hoisted position access: base pointer plus the view's
155+ // run-time strides (row stride, component stride). Particle-major
156+ // (LayoutRight) gives {3, 1}; component-major (LayoutLeft) gives
157+ // {1, padded extent}. See the StateVectorLayout toggle in ParticleStore.
158+ auto const &position_view = store.position_view ();
159+ // Prefer the interleaved rebuild-cadence scratch when populated
160+ // (component-major layout); otherwise read the native column. Same
161+ // base-plus-strides consumption either way.
162+ bool const use_scratch = not interleaved_positions.empty ();
163+ auto const *const position_column =
164+ use_scratch ? interleaved_positions.data () : position_view.data ();
165+ auto const pos_row_stride =
166+ use_scratch ? std::size_t {3u }
167+ : static_cast <std::size_t >(position_view.stride (0 ));
168+ auto const pos_comp_stride =
169+ use_scratch ? std::size_t {1u }
170+ : static_cast <std::size_t >(position_view.stride (1 ));
126171 Particle p1, p2;
127172 for (std::size_t a = 0u ; a < n; ++a) {
128173 auto const row_a = offset + a;
@@ -132,18 +177,20 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
132177 if (ii >= 0 ) {
133178 // Hoist p1's position out of the inner loop (one read per outer
134179 // particle instead of per pair candidate).
135- auto const p1_pos = Utils::Vector3d{position_column[3u * row_a + 0u ],
136- position_column[3u * row_a + 1u ],
137- position_column[3u * row_a + 2u ]};
180+ auto const *const p1_base = position_column + row_a * pos_row_stride;
181+ auto const p1_pos =
182+ Utils::Vector3d{p1_base[0u ], p1_base[pos_comp_stride],
183+ p1_base[2u * pos_comp_stride]};
138184 // pairs in this cell (j > i), same order as before
139185 for (std::size_t b = a + 1u ; b < n; ++b) {
140186 auto const row_b = offset + b;
141187 if (id_column[row_b] <= max_id) {
142188 p2.attach_to_store (store, static_cast <int >(row_b));
189+ auto const *const p2_base =
190+ position_column + row_b * pos_row_stride;
143191 auto const p2_pos =
144- Utils::Vector3d{position_column[3u * row_b + 0u ],
145- position_column[3u * row_b + 1u ],
146- position_column[3u * row_b + 2u ]};
192+ Utils::Vector3d{p2_base[0u ], p2_base[pos_comp_stride],
193+ p2_base[2u * pos_comp_stride]};
147194 if (verlet_criterion (p1, p2,
148195 box_geo.get_mi_dist2 (p1_pos, p2_pos))) {
149196 auto const jj = id_to_index (id_column[row_b]);
@@ -159,15 +206,29 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
159206 };
160207
161208 auto inter_kernel = [&cells, &box_geo, &verlet_criterion, &id_to_index,
162- &inter_operator, max_id](const int i) {
209+ &inter_operator, &interleaved_positions,
210+ max_id](const int i) {
163211 auto &store = cells[i]->store ();
164212 // Contiguous store-row range (phase 7c); clean store, so index directly.
165213 auto const offset = cells[i]->offset ();
166214 auto const n = cells[i]->count ();
167- // Hoisted raw column pointers: see intra_kernel. All cells share the one
168- // active ParticleStore, so the pointers are valid for neighbor cells too.
215+ // Hoisted raw column pointers: see intra_kernel (incl. the layout-agnostic
216+ // stride note). All cells share the one active ParticleStore, so the
217+ // pointers are valid for neighbor cells too.
169218 auto const *const id_column = store.id_view ().data ();
170- auto const *const position_column = store.position_view ().data ();
219+ auto const &position_view = store.position_view ();
220+ // Prefer the interleaved rebuild-cadence scratch when populated
221+ // (component-major layout); otherwise read the native column. Same
222+ // base-plus-strides consumption either way.
223+ bool const use_scratch = not interleaved_positions.empty ();
224+ auto const *const position_column =
225+ use_scratch ? interleaved_positions.data () : position_view.data ();
226+ auto const pos_row_stride =
227+ use_scratch ? std::size_t {3u }
228+ : static_cast <std::size_t >(position_view.stride (0 ));
229+ auto const pos_comp_stride =
230+ use_scratch ? std::size_t {1u }
231+ : static_cast <std::size_t >(position_view.stride (1 ));
171232 Particle p1, p2;
172233 for (std::size_t a = 0u ; a < n; ++a) {
173234 auto const row_a = offset + a;
@@ -176,9 +237,10 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
176237 auto const ii = id_to_index (id_column[row_a]);
177238 if (ii >= 0 ) {
178239 // Hoist p1's position out of the inner loops (see intra_kernel).
179- auto const p1_pos = Utils::Vector3d{position_column[3u * row_a + 0u ],
180- position_column[3u * row_a + 1u ],
181- position_column[3u * row_a + 2u ]};
240+ auto const *const p1_base = position_column + row_a * pos_row_stride;
241+ auto const p1_pos =
242+ Utils::Vector3d{p1_base[0u ], p1_base[pos_comp_stride],
243+ p1_base[2u * pos_comp_stride]};
182244 // pairs with neighboring cells, same order as before
183245 for (auto *neighbor : cells[i]->neighbors ().red ()) {
184246 auto &nb_store = neighbor->store ();
@@ -188,10 +250,11 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
188250 auto const row_k = nb_offset + k;
189251 if (id_column[row_k] <= max_id) {
190252 p2.attach_to_store (nb_store, static_cast <int >(row_k));
253+ auto const *const p2_base =
254+ position_column + row_k * pos_row_stride;
191255 auto const p2_pos =
192- Utils::Vector3d{position_column[3u * row_k + 0u ],
193- position_column[3u * row_k + 1u ],
194- position_column[3u * row_k + 2u ]};
256+ Utils::Vector3d{p2_base[0u ], p2_base[pos_comp_stride],
257+ p2_base[2u * pos_comp_stride]};
195258 if (verlet_criterion (p1, p2,
196259 box_geo.get_mi_dist2 (p1_pos, p2_pos))) {
197260 auto const jj = id_to_index (id_column[row_k]);
0 commit comments