Multi-threading
Multi-threading in Tesserae parallelizes CPU work over particles, grid nodes, and particle-grid transfers. The @threaded macro adds this parallelism while keeping transfer expressions close to their sequential form. It behaves similarly to Julia's built-in Threads.@threads, but is designed to work with particle-grid transfer macros such as @G2P, @P2G, @G2P2G, and @P2G_Matrix.
Usage guidelines
Particle-grid transfers have two directions: gathering and scattering. In a gathering transfer, each particle reads values from nearby grid nodes, so the operation can be threaded directly. In a scattering transfer, particles write contributions to grid nodes. If multiple threads update the same grid node at the same time, this is a data race; see Julia's discussion of data races between threads. Threaded scattering therefore uses a ThreadPartition.
Gathering (@G2P)
To parallelize @G2P, simply prefix it with @threaded.
@threaded @G2P grid=>i particles=>p weights=>ip begin
# your code here
endScattering (@P2G, @G2P2G and @P2G_Matrix)
For scattering operations, prefix @P2G with @threaded and use ThreadPartition to avoid data races on the grid.
partition = ThreadPartition(mesh)
update!(partition, particles.x) # CartesianMesh only
@threaded @P2G grid=>i particles=>p weights=>ip partition begin
# your code here
endFor FEMesh and IGAMesh, the partition is built from the fixed cell connectivity, so it does not need an update! call. The same partitioning applies to @G2P2G and @P2G_Matrix.
Updating basis weights
To update basis weights, either use the update! function, or simply:
@threaded for p in eachindex(particles)
update!(weights[p], particles.x[p], mesh)
endReordering particles
For @P2G and related scattering operations, using reorder_particles! together with ThreadPartition can significantly improve cache efficiency and thread scaling:
partition = ThreadPartition(mesh)
update!(partition, particles.x)
reorder_particles!(particles, partition)Reordering ensures that particles within the same grid block are stored contiguously in memory, reducing random memory access during parallel execution. When reordering is checked every step, use an adaptive threshold such as threshold=0.85:
update!(partition, particles.x)
reorder_particles!(particles, partition; threshold=0.85)For 0 ≤ threshold ≤ 1, larger values reorder more often. Particles are reordered when Tesserae.block_ordered_particle_contiguity is below threshold.
At the endpoints, threshold=0 never reorders and threshold=1 always reorders.
reorder_particles! can be expensive for large systems. It is usually sufficient to reorder particles only when their spatial distribution has changed significantly. Avoid forcing it on every step unless the P2G speedup is worth the reorder cost.
Multi-threading API
Tesserae.@threaded — Macro
@threaded [scheduler] for ...
@threaded [scheduler] @P2G ...
@threaded @P2G ...A macro similar to Threads.@threads, but also works with @P2G, @G2P, @G2P2G, and @P2G_Matrix macros for particle-grid transfers.
The optional scheduler can be :static, :dynamic, :greedy, or :nothing (sequential execution). The default is :dynamic.
See also ThreadPartition.
If multi-threading is disabled or only one thread is available, this macro falls back to sequential execution.
Examples
# Parallel loop
@threaded for i in 1:100
println(i)
end
# Grid-to-particle transfer
@threaded @G2P grid=>i particles=>p weights=>ip begin
v[p] = @∑ w[ip] * v[i]
endTesserae.ThreadPartition — Type
ThreadPartition(::CartesianMesh)
ThreadPartition(::FEMesh)
ThreadPartition(::IGAMesh)ThreadPartition stores partitioning information used by the @P2G, @G2P2G and @P2G_Matrix macros to avoid write conflicts during threaded particle-to-grid transfers.
The @threaded macro must be placed before @P2G, @G2P2G and @P2G_Matrix to enable parallel transfer.
Examples
# Construct ThreadPartition
partition = ThreadPartition(mesh)
# Update partition using current particle positions
update!(partition, particles.x) # Required only for `CartesianMesh`.
# P2G transfer
@threaded @P2G grid=>i particles=>p weights=>ip partition begin
m[i] = @∑ w[ip] * m[p]
mv[i] = @∑ w[ip] * m[p] * v[p]
endTesserae.reorder_particles! — Function
reorder_particles!(particles, partition; threshold=1)Reorder particles by the current block partition.
For 0 ≤ threshold ≤ 1, larger values reorder more often. Particles are reordered when Tesserae.block_ordered_particle_contiguity is below threshold.
At the endpoints, threshold=0 never reorders and threshold=1 always reorders.
A practical value for adaptive reordering is threshold=0.85.
Returns true when particles were reordered.
Tesserae.block_ordered_particle_contiguity — Function
Tesserae.block_ordered_particle_contiguity(partition)Return how contiguous the block-ordered particle list is in memory order. The score is 1 just after reorder_particles! and decreases as particles move across blocks.
The score is the fraction of neighboring entries in the current block-ordered particle index array that are also consecutive in memory. For example, a block-ordered list [1, 2, 3, 8] has two consecutive pairs out of three.