Skip to content

Commit 2af67ca

Browse files
committed
add distributed
1 parent 223f71d commit 2af67ca

5 files changed

Lines changed: 334 additions & 28 deletions

File tree

vbcsr/atomic_data.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def parse_param(param, name, dtype):
104104
)
105105

106106
@classmethod
107-
def from_distributed(
107+
def from_graph_arrays(
108108
cls,
109109
n_atom,
110110
N_atom,
@@ -148,7 +148,7 @@ def from_distributed(
148148
if atomic_number_array.size != int(n_atom):
149149
raise ValueError("atomic_numbers size must equal n_atom")
150150

151-
return super().from_distributed(
151+
return super().from_graph_arrays(
152152
int(n_atom),
153153
int(N_atom),
154154
int(atom_offset),
@@ -165,6 +165,30 @@ def from_distributed(
165165
comm,
166166
)
167167

168+
@classmethod
169+
def from_distributed(cls, pos, z, input_index, cell, pbc, r_max, type_norb, comm=None):
170+
"""Build the distributed AtomicData from a CALLER-GIVEN partition (doc 42 §4).
171+
172+
Each rank passes ONLY its owned atoms: ``pos`` (n_owned, 3), ``z`` (n_owned,),
173+
and ``input_index`` (n_owned,) — the original input-order index of each owned
174+
atom (fills ``atom_index``/``indices``). ``r_max``/``type_norb`` are per-type
175+
(global, indexed by sorted-unique-Z). Global ids are assigned contiguously by
176+
rank and the edge graph + ghosts are built distributed (no rank-0 gather, no
177+
ParMETIS). The partition (which atom each rank owns) is the caller's choice.
178+
"""
179+
comm = _default_comm(comm)
180+
pos = np.ascontiguousarray(np.asarray(pos, dtype=np.float64).reshape(-1, 3))
181+
z = np.ascontiguousarray(np.asarray(z, dtype=np.int32).reshape(-1))
182+
input_index = np.ascontiguousarray(np.asarray(input_index, dtype=np.int32).reshape(-1))
183+
cell = np.ascontiguousarray(np.asarray(cell, dtype=np.float64).reshape(3, 3))
184+
r_max_vec = np.ascontiguousarray(np.asarray(r_max, dtype=np.float64).reshape(-1))
185+
type_norb_vec = np.ascontiguousarray(np.asarray(type_norb, dtype=np.int32).reshape(-1))
186+
if z.size != pos.shape[0] or input_index.size != pos.shape[0]:
187+
raise ValueError("from_distributed: pos/z/input_index must share n_owned")
188+
return super().from_distributed(
189+
pos, z, input_index, cell, _normalize_pbc(pbc), r_max_vec, type_norb_vec, comm
190+
)
191+
168192
@classmethod
169193
def from_ase(cls, atoms, r_max, type_norb=1, comm=None):
170194
pbc = atoms.pbc

vbcsr/core/atomic/atomic_data.hpp

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include "../dist_graph.hpp"
33
#include <vector>
44
#include <map>
5+
#include <set>
6+
#include <array>
57
#include <algorithm>
68
#include <mpi.h>
79
#include <iostream>
@@ -370,7 +372,80 @@ class AtomicData {
370372
r_edges,
371373
type_norb);
372374
}
373-
375+
376+
// Distributed construction from a CALLER-GIVEN partition (doc/design/42 §4).
377+
// Each rank passes ONLY its owned atoms — there is no rank-0 gather of the
378+
// geometry and no ParMETIS: the partition is whatever the caller chose (e.g.
379+
// a spatial slab/RCB aligned with the grid). Global ids are assigned
380+
// contiguously by rank (Allgather of counts -> Exscan), so rank r owns
381+
// [offset_r, offset_r + n_r). This form Allgathers positions/Z (O(N), transient)
382+
// so every rank builds the edges of ITS OWNED atoms locally; the persistent
383+
// storage is owned + ghost only. ``my_input_index[li]`` is the original
384+
// input-order index of owned atom ``li`` (fills AtomicData.atom_index).
385+
static AtomicData* from_distributed(
386+
const std::vector<double>& my_pos, const std::vector<int>& my_z,
387+
const std::vector<int>& my_input_index,
388+
const std::vector<double>& cell, const std::vector<bool>& pbc,
389+
const std::vector<double>& r_max_per_type, const std::vector<int>& type_norb_in,
390+
MPI_Comm comm) {
391+
int rank = 0, size = 1, initialized = 0;
392+
MPI_Initialized(&initialized);
393+
if (initialized) { MPI_Comm_rank(comm, &rank); MPI_Comm_size(comm, &size); }
394+
const int my_n = static_cast<int>(my_z.size());
395+
396+
// 1. Contiguous global-id layout: rank r owns [offset_r, offset_r + n_r).
397+
std::vector<int> counts(size, my_n), displs(size + 1, 0);
398+
if (initialized) MPI_Allgather(&my_n, 1, MPI_INT, counts.data(), 1, MPI_INT, comm);
399+
for (int i = 0; i < size; ++i) displs[i + 1] = displs[i] + counts[i];
400+
const int total = displs[size];
401+
const int my_offset = displs[rank];
402+
if (total == 0) return new AtomicData(comm);
403+
404+
// 2. Allgather all positions + atomic numbers (global-id == rank order).
405+
std::vector<int> pos_counts(size), pos_displs(size + 1, 0);
406+
for (int i = 0; i < size; ++i) { pos_counts[i] = counts[i] * 3; pos_displs[i + 1] = pos_displs[i] + pos_counts[i]; }
407+
std::vector<double> all_pos(static_cast<size_t>(total) * 3);
408+
std::vector<int> all_z(total);
409+
if (initialized) {
410+
MPI_Allgatherv(my_pos.data(), my_n * 3, MPI_DOUBLE, all_pos.data(), pos_counts.data(), pos_displs.data(), MPI_DOUBLE, comm);
411+
MPI_Allgatherv(my_z.data(), my_n, MPI_INT, all_z.data(), counts.data(), displs.data(), MPI_INT, comm);
412+
} else { all_pos = my_pos; all_z = my_z; }
413+
414+
// 3. Global z -> type (sorted-unique z; identical on every rank).
415+
std::vector<int> uz = all_z; std::sort(uz.begin(), uz.end());
416+
uz.erase(std::unique(uz.begin(), uz.end()), uz.end());
417+
std::map<int, int> z2t; for (size_t i = 0; i < uz.size(); ++i) z2t[uz[i]] = static_cast<int>(i);
418+
std::vector<int> all_types(total); for (int g = 0; g < total; ++g) all_types[g] = z2t[all_z[g]];
419+
std::vector<int> type_norb = type_norb_in;
420+
if (type_norb.empty()) type_norb.assign(uz.size(), 1);
421+
std::vector<int> my_types(my_n); for (int li = 0; li < my_n; ++li) my_types[li] = all_types[my_offset + li];
422+
423+
// 4. Edges of MY owned atoms (NeighborList over all positions; same cutoff
424+
// + distance test as process_input_rank0, so the graph is identical to
425+
// from_points for the same geometry). r_edges: 5 ints {gi, gj, rx, ry, rz}.
426+
double max_r = 0; for (double r : r_max_per_type) max_r = std::max(max_r, r);
427+
NeighborList nl; nl.build(all_pos, cell, pbc, max_r * 2.0);
428+
std::vector<int> r_edges;
429+
for (int li = 0; li < my_n; ++li) {
430+
const int gi = my_offset + li, ti = all_types[gi];
431+
for (const auto& nb : nl.get_neighbors(gi)) {
432+
const int gj = nb.index, tj = all_types[gj];
433+
const double rc = r_max_per_type[ti] + r_max_per_type[tj];
434+
const double dx = all_pos[3 * gj] - all_pos[3 * gi] + nb.rx * cell[0] + nb.ry * cell[3] + nb.rz * cell[6];
435+
const double dy = all_pos[3 * gj + 1] - all_pos[3 * gi + 1] + nb.rx * cell[1] + nb.ry * cell[4] + nb.rz * cell[7];
436+
const double dz = all_pos[3 * gj + 2] - all_pos[3 * gi + 2] + nb.rx * cell[2] + nb.ry * cell[5] + nb.rz * cell[8];
437+
if (std::sqrt(dx * dx + dy * dy + dz * dz) > rc + 1e-9) continue;
438+
r_edges.push_back(gi); r_edges.push_back(gj);
439+
r_edges.push_back(nb.rx); r_edges.push_back(nb.ry); r_edges.push_back(nb.rz);
440+
}
441+
}
442+
443+
// 5. Build the distributed object (owned atoms + owned-src edges; the ctor
444+
// derives ghosts from the edge dsts). Same final assembly as from_points.
445+
return construct_final_object(comm, rank, size, cell, pbc, my_n,
446+
my_input_index, my_z, my_types, my_pos, r_edges, type_norb);
447+
}
448+
374449
static AtomicData* from_file(const std::string& filename, const std::vector<double>& r_max_per_type, std::vector<int> type_norb, MPI_Comm comm, const std::string& format="") {
375450
int rank;
376451
int initialized = 0;
@@ -749,10 +824,144 @@ class AtomicData {
749824
}
750825

751826
new_graph->construct_distributed(owned_indices, my_block_sizes, matrix_adj);
752-
827+
753828
return new_graph;
754829
}
755830

831+
// Build a full ``AtomicData`` whose neighbour graph IS the 3-centre (graph3b) structure,
832+
// with the SAME atom distribution as ``this`` — the proper-AtomicData sibling of
833+
// ``get_graph3b`` (doc/design/41). Unlike ``get_graph3b`` (which returns a shift-less
834+
// ``DistGraph`` and so forces the union-graph ``ImageContainer`` ctor + a hand-built
835+
// explicit shift set), this tracks each 3-centre block's integer lattice shift, so the
836+
// normal ``ImageContainer(AtomicData*)`` ctor builds the per-R image graphs directly.
837+
//
838+
// For an owned centre i with reduced neighbours (j, R_j) and (k, R_k) (within the
839+
// ``r_max_left[itype] + r_max_right[jtype]`` cut, same as get_graph3b), the 3-centre block
840+
// (gid_j -> gid_k) lives at shift R_k - R_j and is owned by gid_j's row owner; the routing
841+
// is the same one-Alltoallv idiom as get_graph3b, carrying 5 ints (gj, gk, dRx, dRy, dRz)
842+
// per block. The self-onsite (j==k, R=0) is dropped — the ImageContainer ctor re-adds the
843+
// R=0 diagonal. The resulting (gi, gj, R) edge set equals the graph3b pair set the V_nl /
844+
// force / DM consumers enumerate, so an ImageContainer on this AtomicData has exactly their
845+
// blocks (and ``build_dmr_images`` can read its pairs straight from the graph).
846+
AtomicData* get_atomicdata3b(const std::vector<double>& r_max_left,
847+
const std::vector<double>& r_max_right) {
848+
int initialized = 0;
849+
MPI_Initialized(&initialized);
850+
851+
// 1. Reduced neighbour list WITH integer shift (do NOT collapse periodic images).
852+
std::vector<std::vector<std::pair<int, std::array<int, 3>>>> rnbr(n_atom);
853+
for (int i = 0; i < n_atom; ++i) {
854+
const int itype = atom_type[i];
855+
for (int edge_idx : iconn[i]) {
856+
const int j = edges[edge_idx].dst;
857+
double rx, ry, rz;
858+
get_edge_vec(edge_idx, &rx, &ry, &rz);
859+
const double r = std::sqrt(rx * rx + ry * ry + rz * rz);
860+
const int jtype = atom_type[j];
861+
if (r <= r_max_left[itype] + r_max_right[jtype] + 1e-9) {
862+
int sx, sy, sz;
863+
get_edge_shift_vec(edge_idx, &sx, &sy, &sz);
864+
rnbr[i].push_back({get_gid(j), {sx, sy, sz}});
865+
}
866+
}
867+
}
868+
869+
// 2. Each ordered neighbour pair (j,Rj),(k,Rk) of i -> block (gid_j -> gid_k) at Rk-Rj,
870+
// routed to gid_j's owner (5 ints each).
871+
std::map<int, std::vector<std::array<int, 5>>> send_map;
872+
for (int i = 0; i < n_atom; ++i) {
873+
const auto& nb = rnbr[i];
874+
for (const auto& a : nb) {
875+
const int owner_j = graph->find_owner(a.first);
876+
for (const auto& b : nb) {
877+
send_map[owner_j].push_back({a.first, b.first,
878+
b.second[0] - a.second[0], b.second[1] - a.second[1], b.second[2] - a.second[2]});
879+
}
880+
}
881+
}
882+
for (auto& kv : send_map) {
883+
std::sort(kv.second.begin(), kv.second.end());
884+
kv.second.erase(std::unique(kv.second.begin(), kv.second.end()), kv.second.end());
885+
}
886+
887+
std::vector<int> send_counts(size, 0);
888+
for (auto& kv : send_map) send_counts[kv.first] = static_cast<int>(kv.second.size()) * 5;
889+
std::vector<int> recv_counts(size);
890+
if (initialized) {
891+
MPI_Alltoall(send_counts.data(), 1, MPI_INT, recv_counts.data(), 1, MPI_INT, comm);
892+
} else {
893+
recv_counts = send_counts;
894+
}
895+
std::vector<int> sdispls(size + 1, 0), rdispls(size + 1, 0);
896+
for (int i = 0; i < size; ++i) {
897+
sdispls[i + 1] = sdispls[i] + send_counts[i];
898+
rdispls[i + 1] = rdispls[i] + recv_counts[i];
899+
}
900+
std::vector<int> send_buf(sdispls[size]);
901+
for (auto& kv : send_map) {
902+
int off = sdispls[kv.first];
903+
for (const auto& e : kv.second) for (int t = 0; t < 5; ++t) send_buf[off++] = e[t];
904+
}
905+
std::vector<int> recv_buf(rdispls[size]);
906+
if (initialized) {
907+
MPI_Alltoallv(send_buf.data(), send_counts.data(), sdispls.data(), MPI_INT,
908+
recv_buf.data(), recv_counts.data(), rdispls.data(), MPI_INT, comm);
909+
} else {
910+
recv_buf = send_buf;
911+
}
912+
913+
// 3. Dedup received 3-centre edges (gid_j owned here); drop the self-onsite.
914+
std::set<std::array<int, 5>> edge_set;
915+
for (size_t p = 0; p + 4 < recv_buf.size(); p += 5) {
916+
if (recv_buf[p] == recv_buf[p + 1] &&
917+
recv_buf[p + 2] == 0 && recv_buf[p + 3] == 0 && recv_buf[p + 4] == 0) continue;
918+
edge_set.insert({recv_buf[p], recv_buf[p + 1], recv_buf[p + 2], recv_buf[p + 3], recv_buf[p + 4]});
919+
}
920+
921+
// graph3b ⊇ the 2-body S/T graph: add this rank's owned 2-body edges (i -> j, R_j) WITH
922+
// shift (owned locally, no routing). The 3-centre enumeration alone covers only pairs
923+
// sharing a common centre, so the pure 2-body pairs must be added explicitly — mirrors
924+
// get_graph3b's "add two-body and onsite edges". The R=0 onsite is re-added by the
925+
// ImageContainer ctor, so it is skipped here.
926+
for (int i = 0; i < n_atom; ++i) {
927+
const int gi = get_gid(i);
928+
for (int edge_idx : iconn[i]) {
929+
const int j = edges[edge_idx].dst;
930+
int sx, sy, sz;
931+
get_edge_shift_vec(edge_idx, &sx, &sy, &sz);
932+
if (gi == get_gid(j) && sx == 0 && sy == 0 && sz == 0) continue;
933+
edge_set.insert({gi, get_gid(j), sx, sy, sz});
934+
}
935+
}
936+
937+
std::vector<int> edge_index, edge_shift;
938+
edge_index.reserve(edge_set.size() * 2);
939+
edge_shift.reserve(edge_set.size() * 3);
940+
for (const auto& e : edge_set) {
941+
edge_index.push_back(e[0]); edge_index.push_back(e[1]);
942+
edge_shift.push_back(e[2]); edge_shift.push_back(e[3]); edge_shift.push_back(e[4]);
943+
}
944+
int n_edge3 = static_cast<int>(edge_set.size());
945+
int N_edge3 = n_edge3;
946+
if (initialized) MPI_Allreduce(&n_edge3, &N_edge3, 1, MPI_INT, MPI_SUM, comm);
947+
948+
// 4. Owned per-atom arrays copied from this AtomicData.
949+
std::vector<int> at_index(n_atom), at_type(n_atom), at_num(n_atom);
950+
std::vector<double> pos(3 * n_atom);
951+
for (int i = 0; i < n_atom; ++i) {
952+
at_index[i] = atom_index[i];
953+
at_type[i] = atom_type[i];
954+
at_num[i] = atomic_numbers[i];
955+
pos[3 * i] = x[i]; pos[3 * i + 1] = y[i]; pos[3 * i + 2] = z[i];
956+
}
957+
958+
return new AtomicData(
959+
static_cast<size_t>(n_atom), static_cast<size_t>(N_atom), static_cast<size_t>(atom_offset),
960+
static_cast<size_t>(n_edge3), static_cast<size_t>(N_edge3),
961+
at_index.data(), at_type.data(), edge_index.data(), type_norb.data(), edge_shift.data(),
962+
cell.data(), pos.data(), at_num.data(), comm, pbc);
963+
}
964+
756965
private:
757966
static std::vector<bool> infer_pbc_from_edge_shifts(size_t n_edge, const int *edge_shift_vec_in, int initialized, MPI_Comm comm) {
758967
int local_flags[3] = {0, 0, 0};

vbcsr/core/atomic/image_container.hpp

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,20 @@ class ImageContainer {
4545
// True when this container owns ``base_graph`` (the union-graph ctor below);
4646
// the normal ctor borrows ``data->graph`` and leaves this false.
4747
bool owns_base_graph = false;
48+
// True when this container was built with the owning ``ImageContainer(AtomicData*, true)``
49+
// ctor and must delete ``atom_data`` (and, through it, its graph) in the destructor.
50+
bool owns_atom_data = false;
4851

4952
public:
50-
ImageContainer(AtomicData* data) : atom_data(data), base_graph(data->graph) {
53+
// ``own_atom_data`` lets the container take ownership of a freshly-built AtomicData (e.g.
54+
// ``get_atomicdata3b``, doc/design/41) and delete it in the destructor — so a graph3b
55+
// operator/weight container can be built with this NORMAL ctor (which reads the per-edge
56+
// shifts to build the per-R image graphs) instead of the union-graph ctor below.
57+
ImageContainer(AtomicData* data, bool own_atom_data = false)
58+
: atom_data(data), base_graph(data->graph), owns_atom_data(own_atom_data) {
5159
build_image_graphs();
5260
}
5361

54-
// Union-graph ctor: every image (one per shift in ``shifts``, which MUST be the
55-
// globally-consistent shift set across the comm) lives on the single provided
56-
// ``union_graph`` rather than per-R exact graphs derived from the 2-body
57-
// ``iconn``. This is for operators whose (i, j) sparsity is wider than the
58-
// AtomicData edges — notably V_nl, whose pairs share a common projector and so
59-
// span the 3-body ``get_graph3b`` graph. ``add_block`` routes each (i, j) block
60-
// to its row owner exactly like ``VBCSR(graph3b).add_block``; ``sample_k``
61-
// accumulates onto ``base_graph = union_graph``. If ``own_union`` the container
62-
// deletes ``union_graph`` in its destructor. The per-R ``BlockSpMat`` borrow the
63-
// graph (non-owning), so there is no double free.
64-
ImageContainer(AtomicData* data, DistGraph* union_graph,
65-
const std::vector<std::vector<int>>& shifts, bool own_union = false)
66-
: atom_data(data), base_graph(union_graph), owns_base_graph(own_union) {
67-
for (const auto& R : shifts) {
68-
image_blocks[R] = new BlockSpMat<T, Kernel>(union_graph);
69-
}
70-
}
71-
7262
~ImageContainer() {
7363
for (auto& kv : image_blocks) {
7464
delete kv.second;
@@ -79,6 +69,9 @@ class ImageContainer {
7969
if (owns_base_graph) {
8070
delete base_graph;
8171
}
72+
if (owns_atom_data) {
73+
delete atom_data; // deletes its own_graph too (AtomicData dtor)
74+
}
8275
}
8376

8477
void build_image_graphs() {
@@ -233,6 +226,25 @@ class ImageContainer {
233226
}
234227
}
235228

229+
// In-place per-image axpy: ``this += alpha * other``, matching images by their R shift. Used
230+
// to fuse the 2-body kinetic T into the graph3b V_nl to form the combined static Hamiltonian
231+
// H_static = T + V_nl (doc/design/41 §3), so only {S, H_static} is sent down (not 3 images)
232+
// and V_nl is never rebuilt on the pool graph. ``this`` MUST be a superset graph — every R in
233+
// ``other`` present here, and (per-R) every (row,col) of ``other`` present in this image — the
234+
// graph3b ⊇ 2-body case. Both sides must share the owned-row partition + block sizes; the
235+
// cross-graph ``BlockSpMat::axpby`` maps blocks by GLOBAL (row,col) index.
236+
void axpy_into(const ImageContainer& other, T alpha) {
237+
for (const auto& kv : other.image_blocks) {
238+
auto it = image_blocks.find(kv.first);
239+
if (it == image_blocks.end()) {
240+
throw std::runtime_error(
241+
"ImageContainer::axpy_into: a shift R of `other` is absent in this container "
242+
"(this must be a superset graph, e.g. graph3b absorbing the 2-body operator)");
243+
}
244+
it->second->axpy(alpha, *kv.second);
245+
}
246+
}
247+
236248
// Read one image block (R, global row, global col) on the owner of the row;
237249
// returns empty if this rank does not hold it. RowMajor by default.
238250
std::vector<T> get_block(const std::vector<int>& R, int g_row, int g_col,
@@ -361,6 +373,12 @@ class ImageContainer {
361373
}
362374
}
363375

376+
// NOTE: the former ``gather_into`` (request-list gather that re-``assemble``d into a target
377+
// DistGraph) was removed in doc/design/41 §2.3 — its ``assemble`` re-routes the just-fetched
378+
// blocks by the target partition (a no-op only for a serial target, and an un-gather for any
379+
// real partition). The operator-walk gather is now the keyed ``LocalBlockView`` produced by
380+
// ``rescupp::lcao_grid::GatherPlan::gather_local`` (alias local, cache remote, no re-route).
381+
364382
// Accumulate all image blocks onto a compatible reference graph using
365383
// a per-image weight and an optional per-block correction factor.
366384
template <typename ResultT, typename ResultKernel = DefaultKernel<ResultT>, typename ImageWeightFn, typename BlockWeightFn>

0 commit comments

Comments
 (0)