Files
JuliaFEM.jl/src/assemblers/partitioned_matvec.jl
T
Jukka Aho 4edf8301b1 feat(src): add partitioned_matvec.jl
src/assemblers/partitioned_matvec.jl | 276 +++++++++++++++++++++++++++++++++++  1 file changed, 276 insertions(+)
2026-05-09 16:30:36 +03:00

277 lines
10 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
# Single-partition orchestration for matrix-free `K*x` on owned rows using a
# packed patch + matvec halo exchange (MPI-ready).
"""
allocate_halo_recv_buffers(exchange::RankHaloExchange) -> Vector{Vector{Float64}}
Preallocate receive buffers aligned with `exchange.recv_dof` (setup only).
After MPI non-blocking receives complete, or [`simulate_halo_recv_from_global!`](@ref)
in serial, pass the result
to [`unpack_halo_recv_to_packed!`](@ref).
"""
function allocate_halo_recv_buffers(exchange::RankHaloExchange)::Vector{Vector{Float64}}
return [zeros(length(rd)) for rd in exchange.recv_dof]
end
"""
allocate_halo_send_buffers(exchange::RankHaloExchange) -> Vector{Vector{Float64}}
Preallocate send buffers aligned with `exchange.send_dof` (setup only).
Used by [`pack_halo_send_from_packed!`](@ref) before [`exchange_matvec_halos_mpi!`](@ref)
(with both `JuliaFEM` and `MPI` loaded).
"""
function allocate_halo_send_buffers(exchange::RankHaloExchange)::Vector{Vector{Float64}}
return [zeros(length(sd)) for sd in exchange.send_dof]
end
"""
partitioned_matvec_workspace(layout::PartitionPackedLayout, exchange::RankHaloExchange)
Named tuple `(packed, work, recv_vals)` with correct lengths for
[`partitioned_owned_matvec!`](@ref):
- `packed` — length `layout.n_packed`
- `work` — length `layout.ndofs_global` (expanded stencil workspace)
- `recv_vals` — from [`allocate_halo_recv_buffers`](@ref)`(exchange)`
`exchange.part` must match `layout.part`.
"""
function partitioned_matvec_workspace(
layout::PartitionPackedLayout,
exchange::RankHaloExchange,
)
exchange.part == layout.part ||
throw(ArgumentError("exchange.part $(exchange.part) != layout.part $(layout.part)"))
packed = zeros(layout.n_packed)
work = zeros(layout.ndofs_global)
recv_vals = allocate_halo_recv_buffers(exchange)
return (; packed, work, recv_vals)
end
"""
partitioned_mpi_owned_matvec_workspace(layout::PartitionPackedLayout, exchange::RankHaloExchange)
Like [`partitioned_matvec_workspace`](@ref), but omits the `ndofs_global`-length `work` buffer used
by [`expand_packed_to_global!`](@ref). Intended for the lean MPI path
[`mpi_partitioned_operator_matvec_owned!`](@ref), which applies the stiffness rows directly from
[`packed`](@ref).
Returns `(; packed, recv_vals, send_vals)` with `exchange.part == layout.part`.
"""
function partitioned_mpi_owned_matvec_workspace(
layout::PartitionPackedLayout,
exchange::RankHaloExchange,
)
exchange.part == layout.part ||
throw(ArgumentError("exchange.part $(exchange.part) != layout.part $(layout.part)"))
packed = zeros(layout.n_packed)
recv_vals = allocate_halo_recv_buffers(exchange)
send_vals = allocate_halo_send_buffers(exchange)
return (; packed, recv_vals, send_vals)
end
"""
simulate_halo_recv_from_global!(recv_vals, x_global, exchange) -> recv_vals
Serial stand-in for completed MPI receives:
`recv_vals[k][j] = x_global[exchange.recv_dof[k][j]]`.
Buffers must be preallocated (`recv_vals[k]` length matches `recv_dof[k]`).
Allocation-free in the inner loops.
"""
function simulate_halo_recv_from_global!(
recv_vals::Vector{Vector{Float64}},
x_global::AbstractVector{Float64},
exchange::RankHaloExchange,
)
length(recv_vals) == length(exchange.recv_dof) ==
length(exchange.recv_neighbor) ||
throw(DimensionMismatch("recv_vals length mismatch"))
@inbounds for k in eachindex(exchange.recv_dof)
rd = exchange.recv_dof[k]
rv = recv_vals[k]
length(rv) == length(rd) ||
throw(DimensionMismatch("recv_vals[$k] length $(length(rv)) != recv_dof length $(length(rd))"))
for j in eachindex(rd)
g = rd[j]
rv[j] = x_global[g]
end
end
return recv_vals
end
"""
partitioned_owned_matvec!(
y_contrib, x_global, packed, work, recv_vals,
layout, exchange, cache, assembler, kernel, mesh;
fill_recv_from_global = true,
) -> y_contrib
One partitions additive contribution to `K*x` on **owned rows** (zeros elsewhere
in `y_contrib`), using the matvec packed layout and [`build_matvec_halo_exchanges`](@ref)
metadata:
1. [`gather_owned_from_global_to_packed!`](@ref)`(packed, x_global, layout)`
2. If `fill_recv_from_global` — [`simulate_halo_recv_from_global!`](@ref)`(recv_vals, x_global, exchange)` (skip when MPI fills `recv_vals`)
3. [`unpack_halo_recv_to_packed!`](@ref)`(packed, recv_vals, exchange, layout)`
4. [`fill!`](@ref)`(work, 0)` then [`expand_packed_to_global!`](@ref)`(work, packed, layout)`
5. [`apply_K_owned_rows!`](@ref)`(y_contrib, layout.owned_rows, ...)`
Requires `exchange.part == layout.part`, `length(y_contrib) == cache.ndofs`, and
matching `work` / `packed` sizes from [`partitioned_matvec_workspace`](@ref).
Summing `y_contrib` over disjoint owned partitions recovers global [`apply_K!`](@ref)
when `x_global` is the full vector (serial replica).
Allocation-free after workspace setup when `fill_recv_from_global` follows the same pattern.
"""
function partitioned_owned_matvec!(
y_contrib::AbstractVector{Float64},
x_global::AbstractVector{Float64},
packed::AbstractVector{Float64},
work::AbstractVector{Float64},
recv_vals::Vector{Vector{Float64}},
layout::PartitionPackedLayout,
exchange::RankHaloExchange,
cache::DOFBasedCOOCache,
assembler::DOFBasedCOOAssembler,
kernel::AbstractKernel,
mesh::AbstractMesh;
fill_recv_from_global::Bool = true,
)
exchange.part == layout.part ||
throw(ArgumentError("exchange.part $(exchange.part) != layout.part $(layout.part)"))
nd = cache.ndofs
length(y_contrib) == nd ||
throw(DimensionMismatch("y_contrib length $(length(y_contrib)), ndofs $nd"))
length(x_global) == nd ||
throw(DimensionMismatch("x_global length $(length(x_global)), ndofs $nd"))
length(work) == nd ||
throw(DimensionMismatch("work length $(length(work)), ndofs $nd"))
layout.ndofs_global == nd ||
throw(DimensionMismatch("layout.ndofs_global $(layout.ndofs_global), cache.ndofs $nd"))
gather_owned_from_global_to_packed!(packed, x_global, layout)
if fill_recv_from_global
simulate_halo_recv_from_global!(recv_vals, x_global, exchange)
end
unpack_halo_recv_to_packed!(packed, recv_vals, exchange, layout)
fill!(work, 0.0)
expand_packed_to_global!(work, packed, layout)
apply_K_owned_rows!(y_contrib, layout.owned_rows, cache, assembler, kernel, mesh, work)
return y_contrib
end
"""
apply_K_owned_rows_from_packed!(
Ap_owned, packed, layout, cache, assembler, kernel, mesh,
) -> Ap_owned
Like [`apply_K_owned_rows!`](@ref)`(y, layout.owned_rows, …, x)` with trial vector `x` implied by
`packed`: for each global column index `j`, use `packed[layout.global_to_packed[j]]`. Only owned
rows `packed_to_global[1:n_owned]` are computed; results go to `Ap_owned[k]` for row
`packed_to_global[k]`.
Requires every stencil neighbor `j` of those rows to satisfy `layout.global_to_packed[j] ≠ 0`
(as ensured by [`build_partition_packed_layout_for_matvec`](@ref)).
`length(Ap_owned) == layout.n_owned`, `length(packed) ≥ layout.n_packed`, `layout.ndofs_global == cache.ndofs`.
Allocation-free after warmup.
"""
function apply_K_owned_rows_from_packed!(
Ap_owned::AbstractVector{Float64},
packed::AbstractVector{Float64},
layout::PartitionPackedLayout,
cache::DOFBasedCOOCache{T,B,IPS,E,GC,Buf,FieldType,StateType},
assembler::DOFBasedCOOAssembler,
kernel::AbstractKernel,
mesh::AbstractMesh,
) where {T,B,IPS,E<:AbstractElement,GC,Buf,FieldType,StateType}
ndofs = cache.ndofs
layout.ndofs_global == ndofs ||
throw(DimensionMismatch(
"layout.ndofs_global $(layout.ndofs_global) != cache.ndofs $ndofs",
))
no = layout.n_owned
n_packed = layout.n_packed
length(Ap_owned) == no ||
throw(DimensionMismatch("Ap_owned length $(length(Ap_owned)), n_owned $no"))
length(packed) n_packed ||
throw(DimensionMismatch("packed length $(length(packed)) < n_packed $n_packed"))
owned_rows = layout.owned_rows
length(owned_rows) == ndofs ||
throw(DimensionMismatch("owned_rows length $(length(owned_rows)); expected $ndofs"))
g2p = layout.global_to_packed
p2g = layout.packed_to_global
_prepare_caches!(cache, kernel, mesh)
elements = cache.elements
element_caches = cache.element_caches
geometry_caches = cache.geometry_caches
qp_buffers = cache.qp_buffers
dof_connectivity = cache.dof_connectivity
dof_to_elements = dof_connectivity.dof_to_elements
loc_layout = local_dof_layout(E)
ndofs_elem = length(loc_layout)
@inbounds for k in 1:no
dof_i = p2g[k]
owned_rows[dof_i] ||
throw(ArgumentError("packed owned slot $k → global $dof_i not marked owned in layout"))
yi = 0.0
touching_elements = dof_to_elements[dof_i]
n_conns = length(touching_elements)
@inbounds for conn_idx in 1:n_conns
conn = touching_elements[conn_idx]
elem_id_val = elem_id(conn)
local_i = local_dof_idx(conn)
element = elements[elem_id_val]::E
element_cache = element_caches[elem_id_val]
geometry_cache = geometry_caches[elem_id_val]
qp_buffer = view(qp_buffers, :, elem_id_val)
entry_i = loc_layout[local_i]
dofs_elem = element_cache.dofs
@inbounds for local_j in 1:ndofs_elem
dof_j_global = Int(dofs_elem[local_j])
entry_j = loc_layout[local_j]
K_ij = evaluate_entry(
kernel,
geometry_cache,
qp_buffer,
entry_i,
entry_j,
Int(elem_id_val),
)
pk_j = g2p[dof_j_global]
pk_j == 0 &&
throw(ArgumentError(
"global trial dof $dof_j_global not in packed patch (row $dof_i)",
))
yi += K_ij * packed[pk_j]
end
end
Ap_owned[k] = yi
end
return Ap_owned
end