Loading...
Searching...
No Matches
stick_partition.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Assignment of FFT sticks to ranks.
5 *
6 * A stick is the set of grid points sharing the first two Miller indices and
7 * running the whole third dimension. Distributing whole sticks, rather than
8 * the whole planes FFTW-MPI hands out, buys three things the slab layout
9 * cannot give:
10 *
11 * - A stick and its mirror can be assigned together, which is what keeps the
12 * gamma-only reconstruction local. Under contiguous plane ranges they are
13 * almost never on the same rank: for a 108-plane grid over four ranks, 2
14 * planes of 108 have their mirror locally.
15 * - The split can be balanced. Sticks hold unequal numbers of G-vectors
16 * inside the cutoff sphere and the outer ones hold none, so dealing them
17 * longest-first evens out what each rank carries. The slab split was
18 * measured uneven by 3.8x across four ranks.
19 * - The rank count is capped by the number of sticks rather than the number
20 * of planes, which is larger by a factor of the second dimension.
21 *
22 * This class is only the assignment. It takes a weight per stick -- the caller
23 * counts G-vectors inside its own cutoff -- and says who owns what, so it can
24 * be reasoned about and tested without an FFT or an MPI communicator.
25 */
26
27#include <algorithm>
28#include <cstddef>
29#include <numeric>
30#include <vector>
31
32namespace q3 {
33
35 public:
36 /// Sticks with no G-vectors inside the cutoff belong to nobody.
37 static constexpr int unassigned = -1;
38
39 /**
40 * Deal sticks onto ranks, mirror pairs together, heaviest pair first.
41 *
42 * `stick_weights` is indexed by `wrapped_first * dimension_2 + wrapped_second`
43 * and holds the number of G-vectors on that stick; zero means the stick is
44 * outside the cutoff everywhere and is left unassigned.
45 */
46 void build(int dimension_1,
47 int dimension_2,
48 const std::vector<int>& stick_weights,
49 int number_of_ranks) {
50 dimension_1_ = dimension_1;
51 dimension_2_ = dimension_2;
52 number_of_ranks_ = std::max(number_of_ranks, 1);
53 std::size_t stick_count = static_cast<std::size_t>(dimension_1) * dimension_2;
54 owner_.assign(stick_count, unassigned);
55 weight_per_rank_.assign(static_cast<std::size_t>(number_of_ranks_), 0);
56
57 /**
58 * Group sticks into mirror pairs before weighing them. A pair is
59 * assigned as a unit, so that -G is always on the rank that holds G.
60 * Sticks that are their own mirror -- the origin row, and the Nyquist
61 * rows on even grids -- stand alone.
62 */
63 struct StickPair {
64 int first_index;
65 int second_index; // equal to first_index when self-mirrored
66 long long weight;
67 };
68 std::vector<StickPair> pairs;
69 pairs.reserve(stick_count);
70 std::vector<bool> taken(stick_count, false);
71 for (int first = 0; first < dimension_1; ++first) {
72 for (int second = 0; second < dimension_2; ++second) {
73 std::size_t index = stick_index(first, second);
74 if (taken[index] || stick_weights[index] == 0) {
75 continue;
76 }
77 int mirror_first = (dimension_1 - first) % dimension_1;
78 int mirror_second = (dimension_2 - second) % dimension_2;
79 std::size_t mirror_index = stick_index(mirror_first, mirror_second);
80 taken[index] = true;
81 long long weight = stick_weights[index];
82 if (mirror_index != index) {
83 taken[mirror_index] = true;
84 weight += stick_weights[mirror_index];
85 }
86 pairs.push_back({static_cast<int>(index), static_cast<int>(mirror_index), weight});
87 }
88 }
89
90 /**
91 * Longest-first onto the least-loaded rank. Ties break on the stick
92 * index so the assignment is identical on every rank and between runs
93 * -- ranks build this independently and must agree exactly.
94 */
95 std::sort(pairs.begin(), pairs.end(), [](const StickPair& left, const StickPair& right) {
96 if (left.weight != right.weight) {
97 return left.weight > right.weight;
98 }
99 return left.first_index < right.first_index;
100 });
101 for (const auto& pair : pairs) {
102 int target = 0;
103 for (int rank = 1; rank < number_of_ranks_; ++rank) {
104 if (weight_per_rank_[static_cast<std::size_t>(rank)] <
105 weight_per_rank_[static_cast<std::size_t>(target)]) {
106 target = rank;
107 }
108 }
109 owner_[static_cast<std::size_t>(pair.first_index)] = target;
110 owner_[static_cast<std::size_t>(pair.second_index)] = target;
111 weight_per_rank_[static_cast<std::size_t>(target)] += pair.weight;
112 }
113 }
114
115 /// Rank owning the stick at wrapped indices, or `unassigned` if empty.
116 [[nodiscard]] int owner_of(int wrapped_first, int wrapped_second) const {
117 return owner_[stick_index(wrapped_first, wrapped_second)];
118 }
119
120 /// Rank owning the stick carrying this G-vector, by signed Miller indices.
121 [[nodiscard]] int owner_of_miller(int miller_1, int miller_2) const {
122 return owner_of(wrap(miller_1, dimension_1_), wrap(miller_2, dimension_2_));
123 }
124
125 [[nodiscard]] const std::vector<int>& owner() const {
126 return owner_;
127 }
128
129 [[nodiscard]] const std::vector<long long>& weight_per_rank() const {
130 return weight_per_rank_;
131 }
132
133 /// Heaviest rank's share divided by the lightest; 1.0 is a perfect split.
134 [[nodiscard]] double imbalance() const {
135 if (weight_per_rank_.empty()) {
136 return 1.0;
137 }
138 auto bounds = std::minmax_element(weight_per_rank_.begin(), weight_per_rank_.end());
139 if (*bounds.first <= 0) {
140 return *bounds.second > 0 ? 0.0 : 1.0;
141 }
142 return static_cast<double>(*bounds.second) / static_cast<double>(*bounds.first);
143 }
144
145 [[nodiscard]] int number_of_ranks() const {
146 return number_of_ranks_;
147 }
148
149 static int wrap(int miller, int dimension) {
150 int wrapped = miller % dimension;
151 return wrapped < 0 ? wrapped + dimension : wrapped;
152 }
153
154 private:
155 [[nodiscard]] std::size_t stick_index(int wrapped_first, int wrapped_second) const {
156 return static_cast<std::size_t>(wrapped_first) * dimension_2_ + wrapped_second;
157 }
158
159 int dimension_1_ = 0;
160 int dimension_2_ = 0;
161 int number_of_ranks_ = 1;
162 std::vector<int> owner_;
163 std::vector<long long> weight_per_rank_;
164};
165
166} // namespace q3
void build(int dimension_1, int dimension_2, const std::vector< int > &stick_weights, int number_of_ranks)
static int wrap(int miller, int dimension)
static constexpr int unassigned
Sticks with no G-vectors inside the cutoff belong to nobody.
double imbalance() const
Heaviest rank's share divided by the lightest; 1.0 is a perfect split.
int owner_of(int wrapped_first, int wrapped_second) const
Rank owning the stick at wrapped indices, or unassigned if empty.
const std::vector< long long > & weight_per_rank() const
const std::vector< int > & owner() const
int owner_of_miller(int miller_1, int miller_2) const
Rank owning the stick carrying this G-vector, by signed Miller indices.