Vectors
SafePETSc provides distributed vectors through the Vec{T,Prefix} type, which wraps PETSc's distributed vector functionality with automatic memory management.
Creating Vectors
Uniform Distribution
Use Vec_uniform when all ranks have the same data:
# Create a vector from uniform data
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
# With custom partition
partition = [1, 3, 5] # rank 0: rows 1-2, rank 1: rows 3-4
v = Vec_uniform([1.0, 2.0, 3.0, 4.0]; row_partition=partition)
# With custom prefix type (advanced, see PETSc Options below)
v = Vec_uniform([1.0, 2.0]; Prefix=MPIDENSE)Sum Distribution
Use Vec_sum when ranks contribute sparse entries:
using SparseArrays
# Each rank contributes different entries
# All contributions are summed
rank = MPI.Comm_rank(MPI.COMM_WORLD)
indices = [rank * 2 + 1, rank * 2 + 2]
values = [1.0, 2.0]
v = Vec_sum(sparsevec(indices, values, 10))Helper Constructors
Create vectors similar to existing ones:
# Zero vector with same size/partition as x
y = zeros_like(x)
# Ones vector
y = ones_like(x)
# Filled with specific value
y = fill_like(x, 3.14)
# With different element type
y = zeros_like(x; T2=Float32)Vector Operations
Broadcasting
Vectors support Julia's broadcasting syntax:
# Element-wise operations
y = x .+ 1.0
y = 2.0 .* x
y = x .^ 2
# Vector-vector operations
z = x .+ y
z = x .* y
# In-place operations
y .= x .+ 1.0
y .= 2.0 .* x .+ yArithmetic
# Addition and subtraction
z = x + y
z = x - y
# Mixed with scalars
z = x + 1.0
z = 2.0 - x
# Unary operations
z = -x
z = +xLinear Algebra
# Adjoint (transpose)
x_adj = x'
# Adjoint-matrix multiplication
result = x' * A # Returns adjoint vectorConcatenation
Vectors can be concatenated to form new vectors or matrices:
x = Vec_uniform([1.0, 2.0, 3.0])
y = Vec_uniform([4.0, 5.0, 6.0])
# Vertical concatenation (stacking) - creates a longer vector
v = vcat(x, y) # Vec{Float64,MPIAIJ} with 6 elements
# Horizontal concatenation - creates a matrix (auto-upgrades to MPIDENSE)
M = hcat(x, y) # 3×2 Mat{Float64,MPIDENSE}vcatof vectors returns aVec{T,Prefix}(preserves vector type)hcatof vectors returns aMat{T,Prefix}(creates a multi-column matrix)vcatof matrices returns aMat{T,Prefix}
The Prefix is automatically upgraded to MPIDENSE for horizontal concatenation since vectors are inherently dense.
Partitioning
Vectors are partitioned across ranks to distribute work and memory.
Default Partitioning
# Equal distribution
n = 100
nranks = MPI.Comm_size(MPI.COMM_WORLD)
partition = default_row_partition(n, nranks)
# For n=100, nranks=4:
# partition = [1, 26, 51, 76, 101]
# rank 0: rows 1-25 (25 elements)
# rank 1: rows 26-50 (25 elements)
# rank 2: rows 51-75 (25 elements)
# rank 3: rows 76-100 (25 elements)Custom Partitioning
# Unequal distribution
partition = [1, 10, 30, 101] # Different sizes per rank
v = Vec_uniform(data; row_partition=partition)Partition Requirements
- Length
nranks + 1 - First element is
1 - Last element is
n + 1(wherenis vector length) - Strictly increasing
Properties
# Element type
T = eltype(v) # e.g., Float64
# Size
n = length(v)
n = size(v, 1)
# Partition information
partition = v.obj.row_partitionRow Ownership and Indexing
Determining Owned Rows
Use own_row() to find which indices are owned by the current rank:
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
# Get ownership range for this rank
owned = own_row(v) # e.g., 1:2 on rank 0, 3:4 on rank 1
println(io0(), "Rank $(MPI.Comm_rank(MPI.COMM_WORLD)) owns indices: $owned")Indexing Vectors
Important: You can only index elements that are owned by the current rank. Attempting to access non-owned indices will result in an error.
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
owned = own_row(v)
# ✓ CORRECT - Access owned elements
if 2 in owned
val = v[2] # Returns 2.0 on the rank that owns index 2
end
# ✓ CORRECT - Access range of owned elements
if owned == 1:2
vals = v[1:2] # Returns [1.0, 2.0] on the rank that owns these indices
end
# ❌ WRONG - Accessing non-owned indices causes an error
val = v[3] # ERROR if rank doesn't own index 3!Indexing is non-collective - each rank can independently access its owned data without coordination.
Use Cases for Indexing
Indexing is useful when you need to:
- Extract specific local values for computation
- Implement custom local operations
- Interface with non-PETSc code on owned data
# Extract owned portion for local processing
v = Vec_uniform(randn(100))
owned = own_row(v)
# Get local values
local_vals = v[owned]
# Process locally
local_sum = sum(local_vals)
local_max = maximum(local_vals)
# Aggregate across ranks if needed
global_sum = MPI.Allreduce(local_sum, +, MPI.COMM_WORLD)Row-wise Operations with map_rows
The map_rows() function applies a function to each row of distributed vectors or matrices, similar to Julia's map but for distributed PETSc objects.
Basic Usage
# Apply function to each element of a vector
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
squared = map_rows(x -> x[1]^2, v) # Returns Vec([1.0, 4.0, 9.0, 16.0])
# Transform vector to matrix (using adjoint for row output)
powers = map_rows(x -> [x[1], x[1]^2, x[1]^3]', v) # Returns 4×3 MatNote: For vectors, the function receives a 1-element view, so use x[1] to access the scalar value.
Output Types
The return type depends on what your function returns:
- Scalar → Returns a
Vecwith same number of rows - Vector → Returns a
Vecwith expanded rows (m*n rows if each returns n-element vector) - Adjoint Vector (row vector) → Returns a
Mat{T,MPIDENSE}with m rows (always dense)
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
# Scalar output: Vec with same size
doubled = map_rows(x -> 2 * x[1], v)
# Vector output: Vec with expanded size (4 * 2 = 8 elements)
expanded = map_rows(x -> [x[1], x[1]^2], v)
# Adjoint vector output: Mat with 4 rows, 3 columns
matrix_form = map_rows(x -> [x[1], x[1]^2, x[1]^3]', v)Combining Multiple Inputs
Process multiple vectors or matrices together:
v1 = Vec_uniform([1.0, 2.0, 3.0])
v2 = Vec_uniform([4.0, 5.0, 6.0])
# Combine two vectors element-wise
combined = map_rows((x, y) -> [x[1] + y[1], x[1] * y[1]]', v1, v2)
# Returns 3×2 matrix: [sum, product] for each pairImportant: All inputs must have the same row partition.
Performance Notes
map_rows()is a collective operation - all ranks must call it- The function is applied only to locally owned rows on each rank
- Results are automatically assembled into a new distributed object
- Works efficiently with both vectors and matrices (see Matrices guide)
PETSc Options and the Prefix Type Parameter
SafePETSc vectors have a Prefix type parameter (e.g., Vec{Float64,MPIAIJ}) that controls PETSc configuration through option prefixes. SafePETSc provides two built-in prefix types:
Built-in Prefix Types
MPIAIJ(default): General-purpose prefix- String prefix:
"MPIAIJ_" - Use for: Most vector operations
- String prefix:
MPIDENSE: Dense matrix operations prefix- String prefix:
"MPIDENSE_" - Use for: Vectors associated with dense matrices
- String prefix:
Important: All PETSc vectors are inherently dense (they store all elements), regardless of the Prefix parameter. The prefix only affects which PETSc options are applied, not the internal storage format. Unlike matrices, there is no "sparse vector" format in PETSc.
Setting PETSc Options
You can configure PETSc behavior for vectors with a specific prefix:
# Configure CUDA vectors for dense operations
petsc_options_insert_string("-MPIDENSE_vec_type cuda")
# Create vector with MPIDENSE prefix
v = Vec_uniform(data; Prefix=MPIDENSE)
# Now PETSc will use CUDA for this vector
# Configure standard MPI vectors differently
petsc_options_insert_string("-MPIAIJ_vec_type standard")
w = Vec_uniform(data; Prefix=MPIAIJ)
# This vector uses the standard MPI typeThe string prefix (e.g., "MPIDENSE_", "MPIAIJ_") is automatically prepended to option names when PETSc processes options for objects with that prefix type.
Advanced users can define custom prefix types (not documented here).
Examples
Parallel Computation
using SafePETSc
using MPI
SafePETSc.Init()
rank = MPI.Comm_rank(MPI.COMM_WORLD)
nranks = MPI.Comm_size(MPI.COMM_WORLD)
# Each rank creates local data
n_global = 1000
partition = default_row_partition(n_global, nranks)
lo = partition[rank + 1]
hi = partition[rank + 2] - 1
# Generate data based on rank
data = collect(range(1.0, length=n_global))
# Create distributed vector
v = Vec_uniform(data)
# Compute: y = 2x + 1
y = 2.0 .* v .+ 1.0
println(io0(), "Computation complete")Sparse Contributions
using SafePETSc
using SparseArrays
using MPI
SafePETSc.Init()
rank = MPI.Comm_rank(MPI.COMM_WORLD)
n = 100
# Each rank contributes to different parts
# Use own_rank_only=true to assert local contributions
lo = rank * 25 + 1
hi = (rank + 1) * 25
indices = collect(lo:hi)
values = ones(length(indices)) * (rank + 1)
v = Vec_sum(sparsevec(indices, values, n); own_rank_only=true)Performance Tips
- Use Broadcasting: In-place broadcasting (
y .= ...) avoids allocations - Batch Operations: Combine multiple operations in one broadcast
- Avoid Extraction: Keep data in distributed vectors; don't extract to Julia arrays
- GPU-Aware: Set PETSc options for GPU execution
# Good: in-place, batched
y .= 2.0 .* x .+ 3.0 .* z .+ 1.0
# Less good: multiple allocations
y = 2.0 * x
y = y + 3.0 * z
y = y + 1.0Converting to Julia Arrays
You can convert a distributed Vec to a native Julia Vector using the Vector() constructor. This is useful for interoperability with other Julia packages, data export, or analysis.
# Create a distributed vector
v = Vec_uniform([1.0, 2.0, 3.0, 4.0])
# Convert to Julia Vector
v_julia = Vector(v) # Returns Vector{Float64}Important: Collective Operation
The conversion is a collective operation - all ranks must call it:
# ✓ CORRECT - All ranks participate
v_julia = Vector(v) # All ranks get the complete vector
# ❌ WRONG - Will hang MPI!
if rank == 0
v_julia = Vector(v) # Only rank 0 calls, others wait forever
endAfter conversion, all ranks receive the complete vector. The data is gathered from all ranks using MPI collective operations.
When to Use Conversions
Good use cases:
- Interoperability: Pass data to packages that don't support PETSc
- Small-scale analysis: Compute properties on small vectors
- Data export: Save results to files
- Visualization: Convert for plotting libraries
using Plots
# Solve distributed system
A = Mat_uniform([2.0 1.0; 1.0 3.0])
b = Vec_uniform([1.0, 2.0])
x = A \ b
# Convert for plotting (small vector, so conversion is cheap)
x_julia = Vector(x)
plot(x_julia, label="Solution")Avoid conversions for:
- Large datasets: Gathers all data to all ranks (expensive!)
- Intermediate computations: Keep data in PETSc format
- Repeated access: Don't convert in loops
# ❌ BAD - Expensive conversion in loop
for i in 1:1000
v_julia = Vector(v) # Wasteful! Gathers data every iteration
process(v_julia[1])
end
# ✓ BETTER - Convert once if needed
v_julia = Vector(v)
for i in 1:1000
process(v_julia[1])
endWorking with Converted Vectors
After conversion, you have a standard Julia array:
using Statistics
using LinearAlgebra
x = Vec_uniform([1.0, 2.0, 3.0, 4.0])
x_julia = Vector(x)
# Use with any Julia package
μ = mean(x_julia) # Statistics
σ = std(x_julia)
n = norm(x_julia) # LinearAlgebra
println(io0(), "Mean: $μ, Std: $σ, Norm: $n")See Converting to Native Julia Arrays for more details and examples.
See Also
Vec_uniformVec_sumzeros_likeones_likefill_like- Input/Output and Display - Display and conversion operations
Pooling and Cleanup
By default, released PETSc vectors are returned to an internal pool for reuse instead of being destroyed immediately. This reduces allocation overhead in vector-heavy workflows.
- Disable pooling:
ENABLE_VEC_POOL[] = false - Manually free pooled vectors:
clear_vec_pool!() - Cleanup points:
SafeMPI.check_and_destroy!()performs partial GC and collective release processing; vectors in use remain valid, pooled vectors remain available for reuse