Skip to content

Commit f396c54

Browse files
authored
Merge pull request #79 from MagneticResonanceImaging/CartFFTNormalOp
Cartesian GPU code
2 parents aa36bf4 + 1d3f021 commit f396c54

8 files changed

Lines changed: 416 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
MRISubspaceRecon.jl is a Julia package that implements sub-space reconstructions as described by [Jon Tamir et al.](https://onlinelibrary.wiley.com/doi/abs/10.1002/mrm.26102) and by us in the paper [Low-rank alternating direction method of multipliers reconstruction for MR fingerprinting](https://doi.org/10.1002/mrm.26639) as *low rank inversion*.
1111

12-
Currently, the package supports Cartesian and non-Cartesian trajectories on CPU, and non-Cartesian ones for NVIDIA GPUs. The package is still work in progress. The ultimate goal of this package is to provide a comprehensive Julia implementation of MRI subspace reconstructions, similar to our [Matlab package](https://bitbucket.org/asslaender/nyu_mrf_recon).
12+
Currently, the package supports Cartesian and non-Cartesian trajectories on single- or multithreaded CPUs, as well as NVIDIA GPUs. The package is still under active development. Its ultimate goal is to provide a comprehensive Julia implementation for MRI subspace reconstructions, analagous to our [Matlab package](https://bitbucket.org/asslaender/nyu_mrf_recon).
1313

1414

1515
[docs-img]: https://img.shields.io/badge/docs-latest%20release-blue.svg

docs/src/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ CurrentModule = MRISubspaceRecon
44

55
# MRISubspaceRecon.jl
66

7-
[MRISubspaceRecon.jl](https://github.com/MagneticResonanceImaging/MRISubspaceRecon.jl) package package aims to enable rapid iterative reconstruction
7+
The [MRISubspaceRecon.jl](https://github.com/MagneticResonanceImaging/MRISubspaceRecon.jl) package aims to enable rapid iterative reconstruction
88
of Cartesian and non-Cartesian MRI data using subspace modeling [1,2]. Particular care is given to enable the reconstruction of large numbers of subspace coefficients along with large image grid sizes.
99

1010
For compatibility with other Julia packages, such as [IterativeSolvers.jl](https://github.com/JuliaLinearAlgebra/IterativeSolvers.jl) and [RegularizedLeastSquares.jl](https://github.com/JuliaImageRecon/RegularizedLeastSquares.jl), operations are defined in terms of linear operators and their effects on the data vectors. [MRISubspaceRecon.jl](https://github.com/MagneticResonanceImaging/MRISubspaceRecon.jl) is designed to compute these objects with multi-threaded CPUs and on NVIDIA GPUs. The package further contains functions to perform GRAPPA operator gridding (GROG) [3] or to generate radial trajectories. However, all methods that require an explicit k-space trajectory as input generalize to arbitrary trajectories.
@@ -31,7 +31,7 @@ Reconstructions of non-Cartesian MRI in [MRISubspaceRecon.jl](https://github.com
3131
using MRISubspaceRecon
3232
using CUDA
3333
```
34-
We recommend using the GPU code which can be faster by a factor of 10--20 than CPU multi-threading (for typical solvers like conjugate gradient or FISTA [7]). However, a specific GPU implementation for Cartesian MRI is still under development. In this case, one can use the CPU implementation or the non-Cartesian methods.
34+
We recommend using the GPU code which can be faster by a factor of 10--20 than CPU multi-threading (for typical solvers like conjugate gradient or FISTA [7]).
3535

3636
# References
3737
1. Assländer J, et al. “Low rank alternating direction method of multipliers reconstruction for MR fingerprinting”. Magn Reson Med 79.1 (2018), pp. 83–96. https://doi.org/10.1002/mrm.26639

ext/MRISubspaceReconCUDAExt/BackProjection.jl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,98 @@ function MRISubspaceRecon.calculate_backprojection(data::CuArray{Tc,4}, trj::CuA
8181
end
8282

8383

84+
## ##########################################################################
85+
# Cartesian backprojection (Integer trajectory) on GPU
86+
#############################################################################
87+
function MRISubspaceRecon.calculate_backprojection(data::CuArray{Tc}, trj::CuArray{<:Integer,3}, cmaps::AbstractVector{<:CuArray}; U=cu(I(size(trj)[end])), sample_mask=CUDA.ones(Bool, size(trj)[2:end])) where {Tc <: Complex}
88+
Ncoeff = size(U, 2)
89+
img_shape = size(cmaps[1])
90+
img_idx = CartesianIndices(img_shape)
91+
T = real(eltype(data))
92+
93+
dataU_real = CUDA.zeros(T, 2, img_shape...)
94+
dataU = CuArray{eltype(data)}(undef, img_shape)
95+
xbp = CUDA.zeros(eltype(data), img_shape..., Ncoeff)
96+
97+
ifftplan = plan_ifft!(dataU)
98+
99+
Nsamp = size(trj, 2)
100+
Nt = size(trj, 3)
101+
Nrep = size(U, 3)
102+
103+
# Configure kernel launch
104+
kernel = @cuda launch=false _cartesian_backprojection_kernel!(dataU_real, data, trj, U, sample_mask, img_shape, 1, 1, Nsamp, Nt, Nrep)
105+
config = launch_configuration(kernel.fun)
106+
threads = min(config.threads, Nsamp)
107+
blocks = cld(Nsamp, threads)
108+
109+
for icoef in axes(U, 2)
110+
for icoil in axes(data, 3)
111+
fill!(dataU_real, 0)
112+
@cuda threads=threads blocks=blocks _cartesian_backprojection_kernel!(dataU_real, data, trj, U, sample_mask, img_shape, icoef, icoil, Nsamp, Nt, Nrep)
113+
dataU .= reshape(reinterpret(eltype(data), vec(dataU_real)), img_shape)
114+
ifftplan * dataU
115+
@views xbp[img_idx, icoef] .+= conj.(cmaps[icoil]) .* fftshift(dataU)
116+
end
117+
end
118+
return xbp
119+
end
120+
121+
function MRISubspaceRecon.calculate_backprojection(data::CuArray{Tc}, trj::CuArray{<:Integer,3}, img_shape; U=cu(I(size(trj)[end])), sample_mask=CUDA.ones(Bool, size(trj)[2:end])) where {Tc <: Complex}
122+
Ncoeff = size(U, 2)
123+
Ncoil = size(data, 3)
124+
img_idx = CartesianIndices(img_shape)
125+
T = real(eltype(data))
126+
127+
dataU_real = CUDA.zeros(T, 2, img_shape...)
128+
dataU = CuArray{eltype(data)}(undef, img_shape)
129+
xbp = CUDA.zeros(eltype(data), img_shape..., Ncoeff, Ncoil)
130+
131+
ifftplan = plan_ifft!(dataU)
132+
133+
Nsamp = size(trj, 2)
134+
Nt = size(trj, 3)
135+
Nrep = size(U, 3)
136+
137+
# Configure kernel launch
138+
kernel = @cuda launch=false _cartesian_backprojection_kernel!(dataU_real, data, trj, U, sample_mask, img_shape, 1, 1, Nsamp, Nt, Nrep)
139+
config = launch_configuration(kernel.fun)
140+
threads = min(config.threads, Nsamp)
141+
blocks = cld(Nsamp, threads)
142+
143+
for icoef in axes(U, 2)
144+
for icoil in axes(data, 3)
145+
fill!(dataU_real, 0)
146+
@cuda threads=threads blocks=blocks _cartesian_backprojection_kernel!(dataU_real, data, trj, U, sample_mask, img_shape, icoef, icoil, Nsamp, Nt, Nrep)
147+
dataU .= reshape(reinterpret(eltype(data), vec(dataU_real)), img_shape)
148+
ifftplan * dataU
149+
@views xbp[img_idx, icoef, icoil] .= fftshift(dataU)
150+
end
151+
end
152+
return xbp
153+
end
154+
155+
function _cartesian_backprojection_kernel!(dataU_real, data, trj, U, sample_mask, img_shape::NTuple{N,Int}, icoef, icoil, Nsamp, Nt, Nrep) where N
156+
is = (blockIdx().x - 1) * blockDim().x + threadIdx().x
157+
158+
if is <= Nsamp
159+
for it in 1:Nt
160+
if sample_mask[is, it]
161+
k_idx = ntuple(Val(N)) do j
162+
mod1(Int(trj[j, is, it]) - img_shape[j] ÷ 2, img_shape[j])
163+
end
164+
165+
for irep in 1:Nrep
166+
val = data[is, it, icoil, irep] * conj(U[it, icoef, irep])
167+
CUDA.@atomic dataU_real[1, k_idx...] += real(val)
168+
CUDA.@atomic dataU_real[2, k_idx...] += imag(val)
169+
end
170+
end
171+
end
172+
end
173+
return
174+
end
175+
84176
## ##########################################################################
85177
# Internal helper functions
86178
#############################################################################

ext/MRISubspaceReconCUDAExt/CoilMaps.jl

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,72 @@ function MRISubspaceRecon.calculate_coil_maps(
4848
return calculate_coil_maps(data, trj, img_shape; sample_mask, kwargs...)
4949
end
5050

51+
## ##########################################################################
52+
# Cartesian coil maps (Integer trajectory) on GPU
53+
#############################################################################
54+
function MRISubspaceRecon.calculate_coil_maps(
55+
data::CuArray{Complex{T}},
56+
trj::CuArray{<:Integer},
57+
img_shape::NTuple{N,Int};
58+
U=CUDA.ones(Complex{T}, size(data, 2)),
59+
sample_mask=CUDA.ones(Bool, size(trj)[2:end]),
60+
kernel_size=ntuple(_ -> 6, N),
61+
calib_size=img_shape (img_shape[1] ÷ 32),
62+
eigThresh_1=0.01,
63+
eigThresh_2=0.9,
64+
nmaps=1,
65+
Niter_cg=5,
66+
verbose=false) where {N,T}
67+
68+
lower_bound = @. Int(ceil((img_shape - calib_size) / 2))
69+
upper_bound = @. lower_bound + calib_size + 1
70+
mask_calib = dropdims(all(Array(trj) .> lower_bound; dims=1) .& all(Array(trj) .< upper_bound; dims=1); dims=1)
71+
mask_calib .&= Array(sample_mask)
72+
mask_calib = cu(mask_calib)
73+
74+
x = MRISubspaceRecon.reconstruct_coilwise(data, trj, calib_size; U, sample_mask=mask_calib, Niter_cg)
75+
76+
imdims = ntuple(i -> i, length(img_shape))
77+
kbp = fftshift(x, imdims)
78+
fft!(kbp, imdims)
79+
kbp = fftshift(kbp, imdims)
80+
81+
t = @elapsed begin
82+
cmaps = espirit(Array(kbp), img_shape, kernel_size; eigThresh_1, eigThresh_2, nmaps)
83+
end
84+
verbose && println("espirit: $t s")
85+
86+
cmaps = [cu(cmaps[CartesianIndices(img_shape), ic, in]) for ic in axes(cmaps, length(img_shape) + 1), in = (nmaps == 1 ? 1 : 1:nmaps)]
87+
return cmaps
88+
end
89+
5190

5291
## ##########################################################################
5392
# Internal helper functions
5493
#############################################################################
94+
function MRISubspaceRecon.reconstruct_coilwise(
95+
data::CuArray{Tc},
96+
trj::CuArray{<:Integer,3},
97+
img_shape;
98+
U=CUDA.ones(Tc, size(data, 2)),
99+
sample_mask=CUDA.ones(Bool, size(trj)[2:end]),
100+
Niter_cg=5,
101+
verbose=false) where {Tc <: Complex}
102+
103+
AᴴA = MRISubspaceRecon.FFTNormalOp(img_shape, trj, U[:, 1]; sample_mask)
104+
xbp = MRISubspaceRecon.calculate_backprojection(data, trj, img_shape; U=U[:, 1], sample_mask)
105+
106+
Ncoil = size(data, 3)
107+
x = CUDA.zeros(Tc, img_shape..., Ncoil)
108+
109+
for icoil in axes(xbp, length(img_shape) + 2)
110+
bi = vec(@view xbp[CartesianIndices(img_shape), 1, icoil])
111+
xi = vec(@view x[CartesianIndices(img_shape), icoil])
112+
cg!(xi, AᴴA, bi; maxiter=Niter_cg, verbose)
113+
end
114+
return x
115+
end
116+
55117
function MRISubspaceRecon.reconstruct_coilwise(
56118
data::CuArray{Tc,3},
57119
trj::CuArray{T,3},
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
function MRISubspaceRecon.FFTNormalOp(img_shape, trj::CuArray{<:Integer,3}, U; cmaps=(1,), sample_mask=CUDA.ones(Bool, size(trj)[2:end]))
2+
Λ = calculate_kernel_cartesian(img_shape, trj, U; sample_mask)
3+
return MRISubspaceRecon.FFTNormalOp(Λ; cmaps)
4+
end
5+
6+
# Wrapper for 4D data arrays
7+
function MRISubspaceRecon.FFTNormalOp(img_shape, trj::CuArray{<:Integer,4}, U; sample_mask=CUDA.ones(Bool, size(trj)[2:end]), kwargs...)
8+
trj = reshape(trj, size(trj, 1), :, size(trj, 4))
9+
sample_mask = reshape(sample_mask, :, size(sample_mask, 3))
10+
return MRISubspaceRecon.FFTNormalOp(img_shape, trj, U; sample_mask, kwargs...)
11+
end
12+
13+
# GPU FFTNormalOp constructor from pre-computed Λ
14+
function MRISubspaceRecon.FFTNormalOp::CuArray{Tc}; cmaps=(1,), eltype_x=eltype(Λ)) where {Tc <: Complex}
15+
16+
Ncoeff = size(Λ, 1)
17+
img_shape = size(Λ)[3:end]
18+
19+
kL1 = CuArray{Tc}(undef, img_shape..., Ncoeff)
20+
kL2 = similar(kL1)
21+
22+
@views kmask = (Λ[1, 1, CartesianIndices(img_shape)] .!= 0)
23+
kmask_indcs = findall(vec(kmask))
24+
Λ = reshape(Λ, Ncoeff, Ncoeff, :)
25+
Λ = Λ[:, :, kmask_indcs]
26+
27+
fftplan = plan_fft!(kL1, 1:length(img_shape))
28+
ifftplan = plan_ifft!(kL2, 1:length(img_shape))
29+
30+
# Pre-compute kernel launch configuration
31+
Nk = size(Λ, 3)
32+
kL1_rs = reshape(kL1, :, Ncoeff)
33+
kL2_rs = reshape(kL2, :, Ncoeff)
34+
kernel = @cuda launch=false kernel_mul_cartesian!(kL2_rs, Λ, kL1_rs, kmask_indcs)
35+
config = launch_configuration(kernel.fun)
36+
37+
threads_x = min(config.threads ÷ Ncoeff, Nk)
38+
threads_y = min(config.threads ÷ threads_x, Ncoeff)
39+
threads = (threads_x, threads_y)
40+
blocks = cld.((Nk, Ncoeff), threads)
41+
42+
A = MRISubspaceRecon._FFTNormalOp(img_shape, Ncoeff, fftplan, ifftplan, Λ, kmask_indcs, kL1, kL2, cmaps, threads, blocks)
43+
44+
return LinearOperator(
45+
eltype_x,
46+
prod(A.shape) * A.Ncoeff,
47+
prod(A.shape) * A.Ncoeff,
48+
true,
49+
true,
50+
(res, x, α, β) -> mul!(res, A, x, α, β),
51+
nothing,
52+
(res, x, α, β) -> mul!(res, A, x, α, β);
53+
S = typeof(similar(Λ, eltype_x, 0))
54+
)
55+
end
56+
57+
## ##########################################################################
58+
# Internal use
59+
#############################################################################
60+
61+
function calculate_kernel_cartesian(img_shape, trj::CuArray{<:Integer,3}, U; sample_mask=CUDA.ones(Bool, size(trj)[2:end]), verbose=false)
62+
Ncoeff = size(U, 2)
63+
Nrep = size(U, 3) # number of repetitions (defaults to 1)
64+
65+
# For complex U, we need to accumulate real and imaginary parts separately via atomics
66+
Λ_real = CUDA.zeros(real(eltype(U)), 2, Ncoeff, Ncoeff, img_shape...) # Use a real-valued array with 2x the leading dimension to handle complex atomics
67+
68+
Nsamp = size(trj, 2)
69+
Nt = size(trj, 3)
70+
71+
verbose && println("calculating Cartesian kernel on GPU...")
72+
t = @elapsed CUDA.@sync begin
73+
# Configure kernel launch
74+
kernel = @cuda launch=false kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape, Ncoeff, Nsamp, Nt, Nrep)
75+
config = launch_configuration(kernel.fun)
76+
threads = min(config.threads, Nsamp)
77+
blocks = cld(Nsamp, threads)
78+
79+
@cuda threads=threads blocks=blocks kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape, Ncoeff, Nsamp, Nt, Nrep)
80+
end
81+
verbose && println("time to compute kernel: t = $t s")
82+
83+
# Reinterpret the 2-element real array as complex
84+
Λ = reinterpret(eltype(U), reshape(Λ_real, 2, Ncoeff * Ncoeff * prod(img_shape)))
85+
Λ = reshape(Λ, Ncoeff, Ncoeff, img_shape...)
86+
return Λ
87+
end
88+
89+
function kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape::NTuple{N,Int}, Ncoeff, Nsamp, Nt, Nrep) where N
90+
is = (blockIdx().x - 1) * blockDim().x + threadIdx().x
91+
92+
if is <= Nsamp
93+
for it in 1:Nt
94+
if sample_mask[is, it]
95+
# Compute k-space index with ifftshift incorporated
96+
k_idx = ntuple(Val(N)) do j
97+
mod1(Int(trj[j, is, it]) - img_shape[j] ÷ 2, img_shape[j])
98+
end
99+
100+
# Accumulate into kernel using atomic adds on real/imag parts
101+
for ic2 in 1:Ncoeff, ic1 in 1:Ncoeff
102+
val = zero(eltype(U))
103+
for irep in 1:Nrep
104+
val += conj(U[it, ic1, irep]) * U[it, ic2, irep]
105+
end
106+
# CUDA atomics does not support complex directly, so split into real/imag
107+
CUDA.@atomic Λ_real[1, ic1, ic2, k_idx...] += real(val)
108+
CUDA.@atomic Λ_real[2, ic1, ic2, k_idx...] += imag(val)
109+
end
110+
end
111+
end
112+
end
113+
return
114+
end
115+
116+
## ##########################################################################
117+
# mul! for GPU _FFTNormalOp
118+
#############################################################################
119+
120+
function LinearAlgebra.mul!(x::CuArray, S::MRISubspaceRecon._FFTNormalOp, b, α, β)
121+
idx = CartesianIndices(S.shape)
122+
123+
b = reshape(b, S.shape..., S.Ncoeff)
124+
if β == 0
125+
x .= 0
126+
else
127+
x .*= β
128+
end
129+
xr = reshape(x, S.shape..., S.Ncoeff)
130+
131+
for cmap S.cmaps
132+
# Forward FFT: multiply by coil map and transform
133+
S.kL1[idx, :] .= cmap .* b
134+
S.fftplan * S.kL1
135+
136+
# Multiply by Toeplitz kernel
137+
kL1_rs = reshape(S.kL1, :, S.Ncoeff)
138+
kL2_rs = reshape(S.kL2, :, S.Ncoeff)
139+
fill!(S.kL2, 0)
140+
141+
@cuda threads=S.threads blocks=S.blocks kernel_mul_cartesian!(kL2_rs, S.Λ, kL1_rs, S.kmask_indcs)
142+
143+
# Inverse FFT and accumulate
144+
S.ifftplan * S.kL2
145+
@views xr .+= α .* conj.(cmap) .* S.kL2[idx, :]
146+
end
147+
return x
148+
end
149+
150+
function kernel_mul_cartesian!(kL2_rs, Λ, kL1_rs, kmask_indcs)
151+
ik = (blockIdx().x - 1) * blockDim().x + threadIdx().x
152+
ic1 = (blockIdx().y - 1) * blockDim().y + threadIdx().y
153+
154+
if ik <= length(kmask_indcs) && ic1 <= size(kL2_rs, 2)
155+
ind = kmask_indcs[ik]
156+
acc = zero(eltype(kL2_rs))
157+
158+
@inbounds for ic2 in axes(Λ, 2)
159+
acc += Λ[ic1, ic2, ik] * kL1_rs[ind, ic2]
160+
end
161+
kL2_rs[ind, ic1] = acc
162+
end
163+
return
164+
end

ext/MRISubspaceReconCUDAExt/MRISubspaceReconCUDAExt.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ using CUDA
1313
include("NFFTNormalOp.jl")
1414
include("BackProjection.jl")
1515
include("CoilMaps.jl")
16+
include("FFTNormalOp.jl")
1617

1718
end # module

src/FFTNormalOp.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function FFTNormalOp(Λ; cmaps=(1,), num_fft_threads=round(Int, Threads.nthreads
5656
ktmp = @view kL1[CartesianIndices(img_shape), 1]
5757
fftplan = plan_fft!(ktmp; flags=FFTW.MEASURE, num_threads=num_fft_threads)
5858
ifftplan = plan_ifft!(ktmp; flags=FFTW.MEASURE, num_threads=num_fft_threads)
59-
A = _FFTNormalOp(img_shape, Ncoeff, fftplan, ifftplan, Λ, kmask_indcs, kL1, kL2, cmaps)
59+
A = _FFTNormalOp(img_shape, Ncoeff, fftplan, ifftplan, Λ, kmask_indcs, kL1, kL2, cmaps, nothing, nothing)
6060

6161
return LinearOperator(
6262
eltype_x,
@@ -73,7 +73,7 @@ end
7373
## ##########################################################################
7474
# Internal use
7575
#############################################################################
76-
struct _FFTNormalOp{S,E,F,G,H,I,J,K}
76+
struct _FFTNormalOp{S,E,F,G,H,I,J,K,L,M}
7777
shape::S
7878
Ncoeff::Int
7979
fftplan::E
@@ -83,6 +83,8 @@ struct _FFTNormalOp{S,E,F,G,H,I,J,K}
8383
kL1::I
8484
kL2::J
8585
cmaps::K
86+
threads::L
87+
blocks::M
8688
end
8789

8890
function calculate_kernel_cartesian(img_shape, trj::AbstractArray{<:Integer,3}, U; sample_mask=trues(size(trj)[2:end]), verbose=false)

0 commit comments

Comments
 (0)