Skip to content

Commit 7e22572

Browse files
Sébastien LoiselSébastien Loisel
authored andcommitted
Rewrite map_rows for GPU-friendly broadcasting
- Add StaticArrays dependency for SVector support - Convert matrices to Vector{SVector} via transpose+reinterpret - Use broadcasting f.(local_arrays...) instead of iteration - Avoids GPU->CPU->GPU round-trips when arrays are on GPU - Helper functions: _matrix_to_svectors, _svectors_to_matrix, etc.
1 parent f310ec1 commit 7e22572

2 files changed

Lines changed: 72 additions & 139 deletions

File tree

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ MUMPS = "55d2b088-9f4e-11e9-26c0-150b02ea6a46"
1313
Metal = "dde4c033-4e86-420c-a63e-0dd931031962"
1414
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
1515
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
16+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1617

1718
[extensions]
1819
LinearAlgebraMPIMetalExt = "Metal"
@@ -25,6 +26,7 @@ MPI = "0.20"
2526
MUMPS = "1.5"
2627
Metal = "1.9.1"
2728
PrecompileTools = "1"
29+
StaticArrays = "1"
2830
julia = "1.10"
2931

3032
[extras]

src/LinearAlgebraMPI.jl

Lines changed: 70 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using MPI
44
using Blake3Hash
55
using SparseArrays
66
using MUMPS
7+
using StaticArrays
78
import SparseArrays: nnz, issparse, dropzeros, spdiagm, blockdiag
89
import LinearAlgebra
910
import LinearAlgebra: tr, diag, triu, tril, Transpose, Adjoint, norm, opnorm, mul!, ldlt, BLAS, issymmetric, UniformScaling, dot, Symmetric
@@ -828,17 +829,6 @@ Get the row partition from a distributed type.
828829
_get_row_partition(A::VectorMPI) = A.partition
829830
_get_row_partition(A::MatrixMPI) = A.row_partition
830831

831-
"""
832-
_local_rows(A::VectorMPI)
833-
_local_rows(A::MatrixMPI)
834-
835-
Get an iterator over local rows of a distributed type.
836-
For VectorMPI, returns the local vector directly (iteration yields scalars).
837-
For MatrixMPI, each row is a row vector.
838-
"""
839-
_local_rows(A::VectorMPI) = A.v
840-
_local_rows(A::MatrixMPI) = eachrow(A.A)
841-
842832
"""
843833
_align_to_partition(A::VectorMPI{T}, p::Vector{Int}) where T
844834
_align_to_partition(A::MatrixMPI{T}, p::Vector{Int}) where T
@@ -848,6 +838,32 @@ Repartition a distributed type to match partition p.
848838
_align_to_partition(A::VectorMPI, p::Vector{Int}) = repartition(A, p)
849839
_align_to_partition(A::MatrixMPI, p::Vector{Int}) = repartition(A, p)
850840

841+
# Helper: Convert local matrix to Vector{SVector} by transposing then reinterpreting
842+
# For column-major storage, transpose gives us rows as contiguous columns
843+
function _matrix_to_svectors(::Val{K}, M::AbstractMatrix{T}) where {K, T}
844+
# M is (nrows, K) in column-major. transpose(M) is (K, nrows) column-major.
845+
# Each column of transpose(M) is a row of M, which can be reinterpreted as SVector{K,T}
846+
MT = copy(transpose(M)) # Materialize to contiguous memory (stays on GPU if M is GPU)
847+
vec(reinterpret(reshape, SVector{K, T}, MT))
848+
end
849+
_matrix_to_svectors(M::AbstractMatrix{T}) where {T} = _matrix_to_svectors(Val(size(M, 2)), M)
850+
851+
# Helper: Convert Vector{SVector} back to Matrix
852+
function _svectors_to_matrix(v::AbstractVector{SVector{K,T}}) where {K,T}
853+
# reinterpret as (K, nrows), then transpose to (nrows, K)
854+
Matrix(transpose(reinterpret(reshape, T, v)))
855+
end
856+
857+
# Helper: Convert to SVector representation for map_rows
858+
# Vectors pass through as-is (each element is a "row")
859+
_to_svectors(v::AbstractVector{T}) where {T<:Number} = v
860+
# Matrices get converted to Vector{SVector}
861+
_to_svectors(M::AbstractMatrix{T}) where {T} = _matrix_to_svectors(M)
862+
863+
# Helper: Convert result back based on type
864+
_from_result(v::AbstractVector{SVector{K,T}}) where {K,T} = _svectors_to_matrix(v)
865+
_from_result(v::AbstractVector{T}) where {T<:Number} = v
866+
851867
"""
852868
map_rows(f, A...)
853869
@@ -856,173 +872,88 @@ Apply function `f` to corresponding rows of distributed vectors/matrices.
856872
Each argument in `A...` must be either a `VectorMPI` or `MatrixMPI`. All inputs
857873
are repartitioned to match the partition of the first argument before applying `f`.
858874
859-
For each row index i, `f` is called with the i-th row from each input:
860-
- For `VectorMPI`, the i-th "row" is a length-1 view of element i
861-
- For `MatrixMPI`, the i-th row is a row vector (a view into the local matrix)
875+
This implementation uses GPU-friendly broadcasting: matrices are converted to
876+
Vector{SVector} via transpose+reinterpret, then f is broadcast over all arguments.
877+
This avoids GPU->CPU->GPU round-trips when the underlying arrays are on GPU.
862878
863-
## Result Type (vcat semantics)
879+
For each row index i, `f` is called with:
880+
- For `VectorMPI`: the scalar element at index i
881+
- For `MatrixMPI`: an SVector containing the i-th row
864882
865-
The result type depends on what `f` returns, matching the behavior of `vcat`:
883+
## Result Type
866884
867-
| `f` returns | Julia type | Result |
868-
|-------------|------------|--------|
869-
| scalar | `Number` | `VectorMPI` (one element per input row) |
870-
| column vector | `AbstractVector` | `VectorMPI` (vcat concatenates all vectors) |
871-
| row vector | `Transpose`, `Adjoint` | `MatrixMPI` (vcat stacks as rows) |
872-
| matrix | `AbstractMatrix` | `MatrixMPI` (vcat stacks rows) |
885+
The result type depends on what `f` returns:
873886
874-
## Lazy Wrappers
875-
876-
Julia's `transpose(v)` and `v'` (adjoint) return lazy wrappers that are subtypes
877-
of `AbstractMatrix`, so they produce `MatrixMPI` results:
878-
879-
```julia
880-
map_rows(r -> [1,2,3], A) # Vector → VectorMPI (length 3n)
881-
map_rows(r -> [1,2,3]', A) # Adjoint → MatrixMPI (n×3)
882-
map_rows(r -> transpose([1,2,3]), A) # Transpose → MatrixMPI (n×3)
883-
map_rows(r -> conj([1,2,3]), A) # Vector → VectorMPI (length 3n)
884-
map_rows(r -> [1 2 3], A) # Matrix literal → MatrixMPI (n×3)
885-
```
887+
| `f` returns | Result |
888+
|-------------|--------|
889+
| scalar (`Number`) | `VectorMPI` (one element per input row) |
890+
| `SVector{K,T}` | `MatrixMPI` (K columns, one row per input row) |
886891
887892
## Examples
888893
889894
```julia
890895
# Element-wise product of two vectors
891896
u = VectorMPI([1.0, 2.0, 3.0])
892897
v = VectorMPI([4.0, 5.0, 6.0])
893-
w = map_rows((a, b) -> a[1] * b[1], u, v) # VectorMPI([4.0, 10.0, 18.0])
898+
w = map_rows((a, b) -> a * b, u, v) # VectorMPI([4.0, 10.0, 18.0])
894899
895900
# Row norms of a matrix
896901
A = MatrixMPI(randn(5, 3))
897902
norms = map_rows(r -> norm(r), A) # VectorMPI of row norms
898903
899-
# Expand each row to multiple elements (vcat behavior)
900-
A = MatrixMPI(randn(3, 2))
901-
result = map_rows(r -> [1, 2, 3], A) # VectorMPI of length 9
902-
903-
# Return row vectors to build a matrix
904+
# Return SVector to build a matrix
904905
A = MatrixMPI(randn(3, 2))
905-
result = map_rows(r -> [1, 2, 3]', A) # 3×3 MatrixMPI
906-
907-
# Variable-length output per row
908-
v = VectorMPI([1.0, 2.0, 3.0])
909-
result = map_rows(r -> ones(Int(r[1])), v) # VectorMPI of length 6 (1+2+3)
906+
result = map_rows(r -> SVector(sum(r), prod(r)), A) # 3×2 MatrixMPI
910907
911-
# Mixed inputs: matrix rows weighted by vector elements
908+
# Mixed inputs: matrix rows combined with vector elements
912909
A = MatrixMPI(randn(4, 3))
913910
w = VectorMPI([1.0, 2.0, 3.0, 4.0])
914-
result = map_rows((row, wi) -> sum(row) * wi[1], A, w) # VectorMPI
915-
```
916-
917-
This is the MPI-distributed version of:
918-
```julia
919-
map_rows(f, A...) = vcat((f.((eachrow.(A))...))...)
911+
result = map_rows((row, wi) -> sum(row) * wi, A, w) # VectorMPI
920912
```
921913
"""
922914
function map_rows(f, A...)
923915
isempty(A) && error("map_rows requires at least one argument")
924916

925-
comm = MPI.COMM_WORLD
926-
rank = MPI.Comm_rank(comm)
927-
nranks = MPI.Comm_size(comm)
928-
929917
# Get target partition from first argument
930918
target_partition = _get_row_partition(A[1])
931919

932920
# Align all arguments to target partition
933921
aligned = map(a -> _align_to_partition(a, target_partition), A)
934922

935-
# Get iterators over local rows
936-
row_iters = map(_local_rows, aligned)
937-
938-
# Apply f to corresponding rows using map for performance
939-
results = collect(map(f, row_iters...))
940-
941-
# Determine result type based on what f returned (matching vcat semantics)
942-
# Need to handle empty results case by communicating type info across ranks
943-
944-
# Encode local result info: (has_results, result_kind, eltype_code, ncols_if_matrix)
945-
# result_kind: 0=unknown, 1=Number, 2=AbstractVector, 3=AbstractMatrix
946-
# eltype_code: 1=Float64, 2=ComplexF64, 3=Int64, 4=other
947-
local_info = if isempty(results)
948-
Int32[0, 0, 0, 0] # no results
949-
else
950-
first_result = results[1]
951-
kind = if first_result isa Number
952-
Int32(1)
953-
elseif first_result isa AbstractVector
954-
Int32(2)
955-
elseif first_result isa AbstractMatrix
956-
Int32(3)
957-
else
958-
Int32(0)
959-
end
960-
T = first_result isa Number ? typeof(first_result) : eltype(first_result)
961-
eltype_code = if T == Float64
962-
Int32(1)
963-
elseif T == ComplexF64
964-
Int32(2)
965-
elseif T <: Integer
966-
Int32(3)
923+
# Convert to SVector representation for broadcasting
924+
# VectorMPI.v passes through, MatrixMPI.A gets transposed and reinterpreted
925+
local_arrays = map(aligned) do a
926+
if a isa VectorMPI
927+
a.v # Vector{T} - each element is a "row"
967928
else
968-
Int32(4)
929+
_to_svectors(a.A) # Vector{SVector{K,T}} - each SVector is a row
969930
end
970-
ncols = first_result isa AbstractMatrix ? Int32(size(first_result, 2)) : Int32(0)
971-
Int32[1, kind, eltype_code, ncols]
972931
end
973932

974-
# Gather info from all ranks to determine global result type
975-
all_info = MPI.Allgather(local_info, comm)
976-
977-
# Find a rank that has results to determine the type
978-
result_kind = Int32(0)
979-
eltype_code = Int32(1)
980-
ncols = Int32(0)
981-
for r in 0:(nranks-1)
982-
idx = r * 4
983-
if all_info[idx + 1] == 1 # has_results
984-
result_kind = all_info[idx + 2]
985-
eltype_code = all_info[idx + 3]
986-
ncols = all_info[idx + 4]
987-
break
988-
end
989-
end
990-
991-
# Determine element type
992-
T = if eltype_code == 1
993-
Float64
994-
elseif eltype_code == 2
995-
ComplexF64
996-
elseif eltype_code == 3
997-
Int64
998-
else
999-
Float64 # fallback
1000-
end
933+
# Broadcast f over all local arrays (GPU-friendly)
934+
results = f.(local_arrays...)
1001935

1002-
# Build result based on kind
1003-
if result_kind == 1
1004-
# f returns a scalar -> VectorMPI (one element per row)
1005-
if isempty(results)
1006-
return VectorMPI_local(Vector{T}(undef, 0))
1007-
end
1008-
return VectorMPI_local(collect(T, results))
936+
# Convert results back to appropriate type
937+
local_result = _from_result(results)
1009938

1010-
elseif result_kind == 2
1011-
# f returns a column vector -> VectorMPI (vcat concatenates into longer vector)
1012-
if isempty(results)
1013-
return VectorMPI_local(Vector{T}(undef, 0))
1014-
end
1015-
return VectorMPI_local(Vector{T}(vcat(results...)))
1016-
1017-
elseif result_kind == 3
1018-
# f returns a row vector or matrix -> MatrixMPI (vcat stacks rows)
1019-
if isempty(results)
1020-
return MatrixMPI_local(Matrix{T}(undef, 0, ncols))
1021-
end
1022-
return MatrixMPI_local(Matrix{T}(vcat(results...)))
939+
# Wrap in MPI type using first argument's partition info
940+
first_arg = aligned[1]
941+
row_partition = first_arg isa VectorMPI ? first_arg.partition : first_arg.row_partition
942+
hash = compute_partition_hash(row_partition)
1023943

944+
if local_result isa Matrix
945+
return MatrixMPI(
946+
hash,
947+
row_partition,
948+
[1, size(local_result, 2) + 1], # Full columns on each rank
949+
local_result
950+
)
1024951
else
1025-
error("map_rows: f must return a Number, AbstractVector, or AbstractMatrix")
952+
return VectorMPI(
953+
hash,
954+
row_partition,
955+
local_result
956+
)
1026957
end
1027958
end
1028959

0 commit comments

Comments
 (0)