|
| 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