DOLFINx 0.12.0.0
DOLFINx C++
Loading...
Searching...
No Matches
mattrans.h
1// Copyright (C) 2026 Chris Richardson
2// Copyright (C) 2026 Garth N. Wells
3//
4// This file is part of DOLFINx (https://www.fenicsproject.org)
5//
6// SPDX-License-Identifier: LGPL-3.0-or-later
7
8#pragma once
9
10#include <dolfinx/common/IndexMap.h>
11#include <dolfinx/common/log.h>
12#include <dolfinx/la/MatrixCSR.h>
13#include <dolfinx/la/matmul.h>
14#include <mpi.h>
15#include <stdexcept>
16
17#include <algorithm>
18#include <cassert>
19#include <numeric>
20#include <span>
21#include <utility>
22#include <vector>
23
24namespace dolfinx::la
25{
26namespace impl
27{
43template <typename T, int BS0 = -1, int BS1 = -1>
44std::tuple<std::vector<std::int32_t>, std::vector<std::int64_t>, std::vector<T>>
46{
47 logger().debug("local transpose");
48 std::array<int, 2> bs = A.block_size();
49 auto A_row_map = A.index_map(0);
50 auto A_col_map = A.index_map(1);
51 const auto A_cols = A.cols();
52 const auto A_row_start = A.row_ptr();
53 const auto A_row_end = A.off_diag_offset(); // end of owned-col block
54 const auto A_vals = A.values();
55
56 const std::int32_t n_rows = A_row_map->size_local();
57 const std::int32_t n_cols = A_col_map->size_local(); // rows in A^T
58
59 // Count entries per output row.
60 std::vector<std::int64_t> row_count(n_cols, 0);
61 for (std::int32_t i = 0; i < n_rows; ++i)
62 for (std::int32_t k = A_row_start[i]; k < A_row_end[i]; ++k)
63 ++row_count[A_cols[k]];
64
65 // Exclusive prefix-sum → row pointer (used as write cursor below).
66 std::vector<std::int64_t> row_offsetT(n_cols + 1, 0);
67 std::partial_sum(row_count.begin(), row_count.end(),
68 std::next(row_offsetT.begin()));
69
70 // Fill: column index in A^T is the original row index i.
71 const std::int64_t total_nnz = row_offsetT.back();
72 std::vector<std::int32_t> colsT(total_nnz);
73 std::vector<T> valsT(total_nnz * bs[0] * bs[1]);
74 std::vector<std::int64_t> cursor = row_offsetT;
75 for (std::int32_t i = 0; i < n_rows; ++i)
76 {
77 for (std::int32_t k = A_row_start[i]; k < A_row_end[i]; ++k)
78 {
79 const std::int32_t col = A_cols[k];
80 const std::int32_t pos = cursor[col]++;
81 colsT[pos] = i;
82 // A block (BS0 x BS1) stored row-major: A_vals[k*BS0*BS1 + k0*BS1 + k1]
83 // AT block (BS1 x BS0) stored row-major: valsT[pos*BS0*BS1 + k1*BS0 + k0]
84 if constexpr (BS0 < 0 or BS1 < 0)
85 {
86 for (int k0 = 0; k0 < bs[0]; ++k0)
87 for (int k1 = 0; k1 < bs[1]; ++k1)
88 {
89 valsT[pos * bs[0] * bs[1] + k1 * bs[0] + k0]
90 = A_vals[k * bs[0] * bs[1] + k0 * bs[1] + k1];
91 }
92 }
93 else
94 {
95 for (int k0 = 0; k0 < BS0; ++k0)
96 for (int k1 = 0; k1 < BS1; ++k1)
97 {
98 valsT[pos * BS0 * BS1 + k1 * BS0 + k0]
99 = A_vals[k * BS0 * BS1 + k0 * BS1 + k1];
100 }
101 }
102 }
103 }
104
105 return {std::move(colsT), std::move(row_offsetT), std::move(valsT)};
106}
107} // namespace impl
108
133template <typename T, int BS0 = -1, int BS1 = -1>
135{
136 dolfinx::common::Timer tt("Transpose MatrixCSR");
137
138 std::array<int, 2> bs = A.block_size();
139 if constexpr (BS0 != -1 or BS1 != -1)
140 {
141 if (bs[0] != BS0 or bs[1] != BS1)
142 throw std::invalid_argument("Invalid template parameters/block size");
143 }
144
145 auto row_map_A = A.index_map(0); // row map
146 auto col_map_A = A.index_map(1); // column map
147
148 const std::int32_t n_row = row_map_A->size_local();
149 const std::int32_t n_col = col_map_A->size_local();
150
151 // col_map_A communication topology:
152 // src = ranks that own our ghost columns → we SEND to them
153 // dest = ranks that ghost our owned columns → we RECEIVE from them
154 std::span<const int> src = col_map_A->src();
155 std::span<const int> dest = col_map_A->dest();
156
157 // Only serial execution may return early. Parallel ranks with no
158 // neighbours must still participate in communicator-wide collectives.
159 MPI_Comm comm = row_map_A->comm();
160 int comm_size = dolfinx::MPI::size(comm);
161 if (comm_size == 1)
162 {
163 // Pure local transpose: all columns are owned, so Aᵀ has no ghost columns.
164 auto at_col_map = std::make_shared<dolfinx::common::IndexMap>(comm, n_row);
165 auto at_row_map = std::make_shared<dolfinx::common::IndexMap>(comm, n_col);
166
167 auto [at_cols, at_rp, at_vals] = impl::local_transpose<T, BS0, BS1>(A);
168
169 // All columns are owned (< n_row) → off_diag_offset equals row width.
170 std::vector<std::int32_t> off_diag_offsets(n_col);
171 for (int j = 0; j < n_col; ++j)
172 off_diag_offsets[j] = at_rp[j + 1] - at_rp[j];
173
174 la::impl::Sparsity sp{at_row_map, at_col_map, at_cols,
175 at_rp, off_diag_offsets, {bs[1], bs[0]}};
177 std::ranges::copy(at_vals, At.values().begin());
178 return At;
179 }
180
181 auto row_ptr_A = A.row_ptr();
182 auto offset_row_A = A.off_diag_offset();
183 auto cols_A = A.cols();
184 auto vals_A = A.values();
185
186 std::span<const std::int64_t> ghost_col_globals = col_map_A->ghosts();
187
188 // Index into src[] giving the neighbor owning rank of a ghost column.
189 // src is guaranteed sorted (Scatterer asserts this).
190 auto rank_to_nbr = [&src](int r) -> int
191 {
192 auto it = std::lower_bound(src.begin(), src.end(), r);
193 assert(it != src.end() && *it == r);
194 return static_cast<int>(std::ranges::distance(src.begin(), it));
195 };
196 std::vector<int> ghost_col_owners(col_map_A->owners().size());
197 std::transform(col_map_A->owners().begin(), col_map_A->owners().end(),
198 ghost_col_owners.begin(), rank_to_nbr);
199
200 // Neighbourhood communicator: this rank sends to src[], receives from
201 // dest[].
202 MPI_Comm neigh_comm;
203 MPI_Dist_graph_create_adjacent(
204 comm, static_cast<int>(dest.size()), dest.data(), MPI_UNWEIGHTED,
205 static_cast<int>(src.size()), src.data(), MPI_UNWEIGHTED, MPI_INFO_NULL,
206 false, &neigh_comm);
207
208 // -----------------------------------------------------------------------
209 // Count off-process entries: A[i, j_ghost] grouped by owning rank.
210 // -----------------------------------------------------------------------
211 std::vector<int> send_count(src.size(), 0);
212 send_count.reserve(1); // ensure data is not a nullptr
213 for (std::int32_t i = 0; i < n_row; ++i)
214 {
215 for (std::int64_t k = offset_row_A[i]; k < row_ptr_A[i + 1]; ++k)
216 {
217 const std::int32_t j = cols_A[k] - n_col;
218 ++send_count[ghost_col_owners[j]];
219 }
220 }
221
222 std::vector<int> recv_count(dest.size(), 0);
223 recv_count.reserve(1); // ensure data is not a nullptr
224 MPI_Neighbor_alltoall(send_count.data(), 1, MPI_INT, recv_count.data(), 1,
225 MPI_INT, neigh_comm);
226
227 std::vector<int> send_disp(src.size() + 1, 0);
228 std::partial_sum(send_count.begin(), send_count.end(),
229 std::next(send_disp.begin()));
230 std::vector<int> recv_disp(dest.size() + 1, 0);
231 std::partial_sum(recv_count.begin(), recv_count.end(),
232 std::next(recv_disp.begin()));
233 const int total_send = send_disp.back();
234 const int total_recv = recv_disp.back();
235
236 // -----------------------------------------------------------------------
237 // For each A[i, j] with j < n_col (owned column), store the pair
238 // (global row index, value) in local_cols[j].
239 // -----------------------------------------------------------------------
240 auto [row_start, row_end] = row_map_A->local_range();
241
242 // Each send entry carries three values:
243 // send_row_gidx : global row index of A (= column of Aᵀ on receiver)
244 // send_col_gidx : global column index of A (= row of Aᵀ on receiver)
245 // send_vals : matrix entry value
246 std::vector<std::int64_t> send_row_gidx(total_send);
247 std::vector<std::int64_t> send_col_gidx(total_send);
248 std::vector<T> send_vals(total_send * bs[0] * bs[1]);
249 {
250 int nbs = bs[0] * bs[1];
251 std::vector<int> pos = send_disp; // per-rank write cursor
252 for (std::int32_t i = 0; i < n_row; ++i)
253 {
254 const std::int64_t global_i = row_start + i;
255 for (std::int64_t k = offset_row_A[i]; k < row_ptr_A[i + 1]; ++k)
256 {
257 const std::int32_t j = cols_A[k] - n_col;
258 const int sidx = ghost_col_owners[j];
259 const int p = pos[sidx]++;
260 send_row_gidx[p] = global_i;
261 send_col_gidx[p] = ghost_col_globals[j];
262 std::copy_n(std::next(vals_A.begin(), k * nbs), nbs,
263 std::next(send_vals.begin(), p * nbs));
264 }
265 }
266 }
267
268 // -----------------------------------------------------------------------
269 // POST non-blocking data exchange (three messages simultaneously).
270 // -----------------------------------------------------------------------
271 std::vector<std::int64_t> recv_row_gidx(total_recv);
272 std::vector<std::int64_t> recv_col_gidx(total_recv);
273 std::vector<T> recv_vals(total_recv * bs[0] * bs[1]);
274
275 MPI_Request data_reqs[3];
276 const dolfinx::MPI::Datatype<T> block(bs[0] * bs[1]);
277 MPI_Datatype mpi_T = block.type();
278
279 MPI_Ineighbor_alltoallv(send_row_gidx.data(), send_count.data(),
280 send_disp.data(), MPI_INT64_T, recv_row_gidx.data(),
281 recv_count.data(), recv_disp.data(), MPI_INT64_T,
282 neigh_comm, &data_reqs[0]);
283 MPI_Ineighbor_alltoallv(send_col_gidx.data(), send_count.data(),
284 send_disp.data(), MPI_INT64_T, recv_col_gidx.data(),
285 recv_count.data(), recv_disp.data(), MPI_INT64_T,
286 neigh_comm, &data_reqs[1]);
287 MPI_Ineighbor_alltoallv(send_vals.data(), send_count.data(), send_disp.data(),
288 mpi_T, recv_vals.data(), recv_count.data(),
289 recv_disp.data(), mpi_T, neigh_comm, &data_reqs[2]);
290
291 // Do local part
292 auto [at0_cols, at0_rp, at0_vals] = impl::local_transpose<T, BS0, BS1>(A);
293
294 // -----------------------------------------------------------------------
295 // Wait for all data exchanges to complete.
296 // -----------------------------------------------------------------------
297 MPI_Waitall(3, data_reqs, MPI_STATUSES_IGNORE);
298 MPI_Comm_free(&neigh_comm);
299
300 // -----------------------------------------------------------------------
301 // Merge received entries into local_cols and collect ghost row info.
302 // -----------------------------------------------------------------------
303 auto [col_start, col_end] = col_map_A->local_range();
304
305 // Count number of entries on each row of Aᵀ and build offset.
306 std::vector<std::int64_t> new_row_count(n_col);
307 std::adjacent_difference(std::next(at0_rp.begin()), at0_rp.end(),
308 new_row_count.begin());
309 for (std::int64_t c : recv_col_gidx)
310 {
311 const std::int32_t local_col = static_cast<std::int32_t>(c - col_start);
312 assert(local_col >= 0 && local_col < n_col);
313 ++new_row_count[local_col];
314 }
315 std::vector<std::int64_t> at_row_ptr(new_row_count.size() + 1, 0);
316 std::partial_sum(new_row_count.begin(), new_row_count.end(),
317 std::next(at_row_ptr.begin()));
318
319 // Compute new IndexMap for columns
320 std::vector<std::int64_t> ghost_row_globals;
321 std::vector<int> ghost_row_owners;
322 {
323 // ghost_row_pairs: (global row index, owner rank) for Aᵀ column map.
324 std::vector<std::pair<std::int64_t, int>> ghost_row_pairs;
325 ghost_row_pairs.reserve(recv_disp.back());
326 for (std::size_t p = 0; p < dest.size(); ++p)
327 {
328 for (int k = recv_disp[p]; k < recv_disp[p + 1]; ++k)
329 {
330 const std::int64_t gidx = recv_row_gidx[k];
331 assert(gidx < row_start || gidx >= row_end);
332 ghost_row_pairs.emplace_back(gidx, static_cast<int>(dest[p]));
333 }
334 }
335
336 // -----------------------------------------------------------------------
337 // Build Aᵀ column map (n_row owned + received ghosts).
338 // -----------------------------------------------------------------------
339 std::sort(ghost_row_pairs.begin(), ghost_row_pairs.end());
340 ghost_row_pairs.erase(
341 std::unique(ghost_row_pairs.begin(), ghost_row_pairs.end()),
342 ghost_row_pairs.end());
343
344 ghost_row_globals.reserve(ghost_row_pairs.size());
345 ghost_row_owners.reserve(ghost_row_pairs.size());
346 for (auto& [g, o] : ghost_row_pairs)
347 {
348 ghost_row_globals.push_back(g);
349 ghost_row_owners.push_back(o);
350 }
351 }
352
353 auto at_col_map = std::make_shared<dolfinx::common::IndexMap>(
354 comm, n_row, ghost_row_globals, ghost_row_owners);
355
356 // Batch convert received rows (cols of Aᵀ) into local indexing
357 std::vector<std::int32_t> new_col_local(recv_disp.back());
358 at_col_map->global_to_local(recv_row_gidx, new_col_local);
359
360 // Copy over local rows into new data structure
361 std::vector<std::int32_t> at_cols(at_row_ptr.back());
362 std::vector<T> at_vals(at_row_ptr.back() * bs[0] * bs[1]);
363 for (std::int32_t i = 0; i < n_col; ++i)
364 {
365 std::copy(std::next(at0_cols.begin(), at0_rp[i]),
366 std::next(at0_cols.begin(), at0_rp[i + 1]),
367 std::next(at_cols.begin(), at_row_ptr[i]));
368 std::copy(std::next(at0_vals.begin(), at0_rp[i] * bs[0] * bs[1]),
369 std::next(at0_vals.begin(), at0_rp[i + 1] * bs[0] * bs[1]),
370 std::next(at_vals.begin(), at_row_ptr[i] * bs[0] * bs[1]));
371 }
372
373 // Append received values.
374 // cursor[j] = next free write position in at_cols/at_vals for row j,
375 // initialised to just after the local entries already copied in.
376 std::vector<std::int32_t> cursor(n_col);
377 std::vector<std::int32_t> off_diag_offsets(n_col);
378 for (int j = 0; j < n_col; ++j)
379 {
380 off_diag_offsets[j] = at0_rp[j + 1] - at0_rp[j];
381 cursor[j] = static_cast<std::int32_t>(at_row_ptr[j]) + off_diag_offsets[j];
382 }
383
384 for (std::size_t p = 0; p < dest.size(); ++p)
385 {
386 for (int k = recv_disp[p]; k < recv_disp[p + 1]; ++k)
387 {
388 const std::int32_t local_col
389 = static_cast<std::int32_t>(recv_col_gidx[k] - col_start);
390 assert(local_col >= 0 && local_col < n_col);
391 const int pos = cursor[local_col]++;
392 at_cols[pos] = new_col_local[k];
393 // recv block (BS0 x BS1) row-major; at_vals block (BS1 x BS0) row-major.
394 if constexpr (BS0 < 0 or BS1 < 0)
395 {
396 for (int k0 = 0; k0 < bs[0]; ++k0)
397 for (int k1 = 0; k1 < bs[1]; ++k1)
398 {
399 at_vals[pos * bs[0] * bs[1] + k1 * bs[0] + k0]
400 = recv_vals[k * bs[0] * bs[1] + k0 * bs[1] + k1];
401 }
402 }
403 else
404 {
405 for (int k0 = 0; k0 < BS0; ++k0)
406 for (int k1 = 0; k1 < BS1; ++k1)
407 {
408 at_vals[pos * BS0 * BS1 + k1 * BS0 + k0]
409 = recv_vals[k * BS0 * BS1 + k0 * BS1 + k1];
410 }
411 }
412 }
413 }
414
415 // -----------------------------------------------------------------------
416 // Construct Aᵀ as MatrixCSR.
417 // Row map: ghost-free (n_col owned rows, no ghost rows needed).
418 // -----------------------------------------------------------------------
419 auto at_row_map = std::make_shared<dolfinx::common::IndexMap>(comm, n_col);
420
421 la::impl::Sparsity sp{at_row_map, at_col_map, at_cols,
422 at_row_ptr, off_diag_offsets, {bs[1], bs[0]}};
423
425 std::ranges::copy(at_vals, At.values().begin());
426
427 if (dolfinx::MPI::rank(comm) == 0)
428 {
429 logger().info("Transpose: ({} x {}) -> ({} x {}), nnz {}",
430 row_map_A->size_global(), col_map_A->size_global(),
431 col_map_A->size_global(), row_map_A->size_global(),
432 at_row_ptr.back());
433 }
434
435 return At;
436}
437
438} // namespace dolfinx::la
An MPI datatype for count contiguous values of type T, and manage its lifetime.
Definition MPI.h:346
MPI_Datatype type() const noexcept
The datatype to pass to MPI.
Definition MPI.h:395
Timer for measuring and logging elapsed time durations.
Definition Timer.h:41
Distributed sparse matrix using compressed sparse row storage.
Definition MatrixCSR.h:74
std::shared_ptr< const common::IndexMap > index_map(int dim) const
Index map for the row or column space.
Definition MatrixCSR.h:574
const rowptr_container_type & off_diag_offset() const
Get the start of off-diagonal (unowned columns) on each row, allowing the matrix to be split (virtual...
Definition MatrixCSR.h:604
container_type & values()
Get local data values.
Definition MatrixCSR.h:581
const column_container_type & cols() const
Definition MatrixCSR.h:593
std::array< int, 2 > block_size() const
Get block sizes.
Definition MatrixCSR.h:611
const rowptr_container_type & row_ptr() const
Get local row pointers.
Definition MatrixCSR.h:589
int size(MPI_Comm comm)
Definition MPI.cpp:81
int rank(MPI_Comm comm)
Return process rank for the communicator.
Definition MPI.cpp:73
Fetch the rows of B that correspond to the ghost columns of A.
Definition matmul.h:39
std::tuple< std::vector< std::int32_t >, std::vector< std::int64_t >, std::vector< T > > local_transpose(const dolfinx::la::MatrixCSR< T > &A)
Compute the local (diagonal-block) transpose of A.
Definition mattrans.h:45
Linear algebra interface.
Definition dolfinx_la.h:7
dolfinx::la::MatrixCSR< T > transpose(const dolfinx::la::MatrixCSR< T > &A)
Compute the distributed transpose of a MatrixCSR.
Definition mattrans.h:134
auto logger() -> spdlog::logger &
Dolfinx's logger.
Definition log.cpp:24
Lightweight sparsity descriptor satisfying the SparsityImplementation concept required by the MatrixC...
Definition matmul.h:53