@@ -4,6 +4,7 @@ using MPI
44using Blake3Hash
55using SparseArrays
66using MUMPS
7+ using StaticArrays
78import SparseArrays: nnz, issparse, dropzeros, spdiagm, blockdiag
89import LinearAlgebra
910import 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.
856872Each argument in `A...` must be either a `VectorMPI` or `MatrixMPI`. All inputs
857873are 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
891896u = VectorMPI([1.0, 2.0, 3.0])
892897v = 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
896901A = MatrixMPI(randn(5, 3))
897902norms = 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
904905A = 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
912909A = MatrixMPI(randn(4, 3))
913910w = 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"""
922914function 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
1027958end
1028959
0 commit comments