Skip to content

Commit d947518

Browse files
committed
Add multi-threading documentation
1 parent 80f56e2 commit d947518

File tree

3 files changed

+76
-1
lines changed

3 files changed

+76
-1
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ makedocs(
3939
"Serialization" => "generated/howto/serialization.md",
4040
"Caching" => "generated/howto/caching.md",
4141
"Observables" => "generated/howto/observables.md",
42+
"Multi-Threading" => "generated/howto/multi_threading.md",
4243
],
4344
"API Reference" => "API/api.md",
4445
#"Regularization Terms" => "API/regularization.md"],

docs/src/literate/example/4_iterative.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, para
6464
end
6565

6666
# Note that initially the operator is `nothing` and the processing step would fail as it stands. To "fix" this we define a `process` method for the algorithm instance which creates the operator and stores it in the algorithm:
67-
function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::IterativeRadonReconstructionParameters, ::Nothing, data::AbstractArray{T, 4}) where {T}
67+
function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::AbstractIterativeRadonReconstructionParameters, ::Nothing, data::AbstractArray{T, 4}) where {T}
6868
op = RadonOp(T; shape = params.shape, angles = params.angles)
6969
algo.op = op
7070
return process(AbstractIterativeRadonAlgorithm, params, op, data)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
include("../../literate/example/example_include_all.jl") #hide
2+
3+
# # Multi-Threading
4+
# `AbstractImageReconstruction` assumes that algorithms are stateful. This is reflected in the FIFO behaviour and the locking interface of algorithms.
5+
# The motivation behind this choice is that the nature of computations within an algorithms heavily impact if multi-threading is beneficial or not.
6+
# For example, consider a GPU-accelerated reconstruction. There it might be faster to sequentially process all images on the GPU instead of processing them in parallel. Or consider, the preprocessing step of the Radon example where we average our data. If we were to extend our algorithm to read sinograms from a file, it might be inefficient to partially read and average frames from the file in parallel.
7+
# Instead it would be more efficient to read the required file in one go and then average the frames in parallel.
8+
# Therefore, the actual runtime behaviour is intended to be an implementation detail of an algorithm which is to be abstracted behind `reconstruct`.
9+
10+
# In the following we will explore the results of this design decision. If we consider a n algorithm such as:
11+
plan = RecoPlan(IterativeRadonAlgorithm, parameter = RecoPlan(IterativeRadonParameters,
12+
pre = RecoPlan(RadonPreprocessingParameters, frames = collect(1:5)),
13+
reco = RecoPlan(IterativeRadonReconstructionParameters, shape = size(images)[1:3], angles = angles,
14+
iterations = 20, reg = [L2Regularization(0.001), PositiveRegularization()], solver = CGNR)
15+
))
16+
algo = build(plan)
17+
18+
# which acts on one frame at a time, we could in theory do:
19+
# ```julia
20+
# Threads.@threads for i = 1:size(sinograms, 4)
21+
# res = reconstruct(algo, sinograms[:, :, :, i:i])
22+
# # Store res
23+
# end
24+
# ```
25+
# Due to the locking interface of the algorithm, this will not actually run in parallel. Instead the algorithm will be locked for each iteration and tasks will wait for the previous reconstruction to finish.
26+
27+
# If a user wants to explicitly use multi-threading, we could the plan to create a new algorithm for each task:
28+
# ```julia
29+
# Threads.@threads for i = 1:size(sinograms, 4)
30+
# algo = build(plan)
31+
# res = reconstruct(algo, sinograms[:, :, :, i:i])
32+
# # Store res
33+
# end
34+
# ```
35+
# This way each task has its own algorithm and can run in parallel. As mentioned before, this parallelization might not be the most efficient parallel reconstruction, both in terms of memory and runtime.
36+
37+
# To further improve the performance of the reconstruction, we could implement specific multi-threading `process`-ing steps for our algorithms. As an example, we will implement parallel processing for the iterative solver:
38+
Base.@kwdef struct ThreadedIterativeReconstructionParameters{S <: AbstractLinearSolver, R <: AbstractRegularization, N} <: AbstractIterativeRadonReconstructionParameters
39+
solver::Type{S}
40+
iterations::Int64
41+
reg::Vector{R}
42+
shape::NTuple{N, Int64}
43+
angles::Vector{Float64}
44+
end
45+
# Our parameters are identical to the iterative reconstruction parameters from the iterative example. We only differ in the type of the parameters. This allows us to dispatch on the type of the parameters and implement a different `process` method for the threaded version:
46+
function AbstractImageReconstruction.process(::Type{<:AbstractIterativeRadonAlgorithm}, params::ThreadedIterativeReconstructionParameters, op, data::AbstractArray{T, 4}) where {T}
47+
48+
result = similar(data, params.shape..., size(data, 4))
49+
50+
Threads.@threads for i = 1:size(data, 4)
51+
solver = createLinearSolver(params.solver, op; iterations = params.iterations, reg = params.reg)
52+
result[:, :, :, i] = solve!(solver, vec(data[:, :, :, i]))
53+
end
54+
55+
return result
56+
end
57+
58+
# While the Radon operator is thread-safe, the normal operator constructed by the solver is not. Therefore, we can reuse the Radon operator but still have to construct new solvers for each task.
59+
60+
# Our multi-threaded reconstruction can be constructed and used just like the single-threaded one::
61+
plan.parameter.pre.frames = collect(1:size(sinograms, 4))
62+
plan.parameter.reco = RecoPlan(ThreadedIterativeReconstructionParameters, shape = size(images)[1:3], angles = angles,
63+
iterations = 20, reg = [L2Regularization(0.001), PositiveRegularization()], solver = CGNR)
64+
65+
algo = build(plan)
66+
imag_iter = reconstruct(algo, sinograms)
67+
fig = Figure()
68+
for i = 1:5
69+
plot_image(fig[i,1], reverse(images[:, :, 24, i]))
70+
plot_image(fig[i,2], sinograms[:, :, 24, i])
71+
plot_image(fig[i,3], reverse(imag_iter[:, :, 24, i]))
72+
end
73+
resize_to_layout!(fig)
74+
fig

0 commit comments

Comments
 (0)