Skip to content

Commit 9d26110

Browse files
author
Ivo Maatman
committed
Add Cartesian GPU code
1 parent aa36bf4 commit 9d26110

4 files changed

Lines changed: 325 additions & 0 deletions

File tree

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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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+
A = MRISubspaceRecon._FFTNormalOp(img_shape, Ncoeff, fftplan, ifftplan, Λ, kmask_indcs, kL1, kL2, cmaps)
31+
32+
return LinearOperator(
33+
eltype_x,
34+
prod(A.shape) * A.Ncoeff,
35+
prod(A.shape) * A.Ncoeff,
36+
true,
37+
true,
38+
(res, x, α, β) -> mul!(res, A, x, α, β),
39+
nothing,
40+
(res, x, α, β) -> mul!(res, A, x, α, β);
41+
S = typeof(similar(Λ, eltype_x, 0))
42+
)
43+
end
44+
45+
## ##########################################################################
46+
# Kernel Calculation
47+
#############################################################################
48+
49+
function calculate_kernel_cartesian(img_shape, trj::CuArray{<:Integer,3}, U; sample_mask=CUDA.ones(Bool, size(trj)[2:end]), verbose=false)
50+
Ncoeff = size(U, 2)
51+
Nrep = size(U, 3) # number of repetitions (defaults to 1)
52+
53+
# For complex U, we need to accumulate real and imaginary parts separately via atomics
54+
Λ_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
55+
56+
Nsamp = size(trj, 2)
57+
Nt = size(trj, 3)
58+
59+
verbose && println("calculating Cartesian kernel on GPU...")
60+
t = @elapsed CUDA.@sync begin
61+
# Configure kernel launch
62+
kernel = @cuda launch=false _kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape, Ncoeff, Nsamp, Nt, Nrep)
63+
config = launch_configuration(kernel.fun)
64+
threads = min(config.threads, Nsamp)
65+
blocks = cld(Nsamp, threads)
66+
67+
@cuda threads=threads blocks=blocks _kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape, Ncoeff, Nsamp, Nt, Nrep)
68+
end
69+
verbose && println("time to compute kernel: t = $t s")
70+
71+
# Reinterpret the 2-element real array as complex
72+
Λ = reinterpret(eltype(U), reshape(Λ_real, 2, Ncoeff * Ncoeff * prod(img_shape)))
73+
Λ = reshape(Λ, Ncoeff, Ncoeff, img_shape...)
74+
return Λ
75+
end
76+
77+
function _kernel_cartesian_complex!(Λ_real, trj, U, sample_mask, img_shape::NTuple{N,Int}, Ncoeff, Nsamp, Nt, Nrep) where N
78+
is = (blockIdx().x - 1) * blockDim().x + threadIdx().x
79+
80+
if is <= Nsamp
81+
for it in 1:Nt
82+
if sample_mask[is, it]
83+
# Compute k-space index with ifftshift incorporated
84+
k_idx = ntuple(Val(N)) do j
85+
mod1(Int(trj[j, is, it]) - img_shape[j] ÷ 2, img_shape[j])
86+
end
87+
88+
# Accumulate into kernel using atomic adds on real/imag parts
89+
for ic2 in 1:Ncoeff, ic1 in 1:Ncoeff
90+
val = zero(eltype(U))
91+
for irep in 1:Nrep
92+
val += conj(U[it, ic1, irep]) * U[it, ic2, irep]
93+
end
94+
# CUDA atomics don't support complex directly, so split into real/imag
95+
CUDA.@atomic Λ_real[1, ic1, ic2, k_idx...] += real(val)
96+
CUDA.@atomic Λ_real[2, ic1, ic2, k_idx...] += imag(val)
97+
end
98+
end
99+
end
100+
end
101+
return
102+
end
103+
104+
## ##########################################################################
105+
# mul! for GPU _FFTNormalOp
106+
#############################################################################
107+
108+
function LinearAlgebra.mul!(x::CuArray, S::MRISubspaceRecon._FFTNormalOp, b, α, β)
109+
idx = CartesianIndices(S.shape)
110+
111+
b = reshape(b, S.shape..., S.Ncoeff)
112+
if β == 0
113+
x .= 0
114+
else
115+
x .*= β
116+
end
117+
xr = reshape(x, S.shape..., S.Ncoeff)
118+
119+
for cmap S.cmaps
120+
# Forward FFT: multiply by coil map and transform
121+
S.kL1[idx, :] .= cmap .* b
122+
S.fftplan * S.kL1
123+
124+
# Multiply by Toeplitz kernel
125+
kL1_rs = reshape(S.kL1, :, S.Ncoeff)
126+
kL2_rs = reshape(S.kL2, :, S.Ncoeff)
127+
fill!(S.kL2, 0)
128+
129+
# Batched matrix multiplication at each masked k-space location
130+
# Each kmask_indcs[i] indexes a row in kL1_rs/kL2_rs, and Λ[:,:,i] is the kernel matrix
131+
_batched_kernel_mul!(kL2_rs, S.Λ, kL1_rs, S.kmask_indcs)
132+
133+
# Inverse FFT and accumulate
134+
S.ifftplan * S.kL2
135+
@views xr .+= α .* conj.(cmap) .* S.kL2[idx, :]
136+
end
137+
return x
138+
end
139+
140+
# Batched kernel multiplication on GPU
141+
function _batched_kernel_mul!(kL2_rs::CuArray, Λ::CuArray, kL1_rs::CuArray, kmask_indcs)
142+
Ncoeff = size(Λ, 1)
143+
Nk = size(Λ, 3)
144+
145+
kernel = @cuda launch=false _kernel_mul_cartesian!(kL2_rs, Λ, kL1_rs, kmask_indcs)
146+
config = launch_configuration(kernel.fun)
147+
148+
threads_x = min(config.threads ÷ Ncoeff, Nk)
149+
threads_y = min(config.threads ÷ threads_x, Ncoeff)
150+
threads = (threads_x, threads_y)
151+
blocks = cld.((Nk, Ncoeff), threads)
152+
153+
@cuda threads=threads blocks=blocks _kernel_mul_cartesian!(kL2_rs, Λ, kL1_rs, kmask_indcs)
154+
end
155+
156+
function _kernel_mul_cartesian!(kL2_rs, Λ, kL1_rs, kmask_indcs)
157+
ik = (blockIdx().x - 1) * blockDim().x + threadIdx().x
158+
ic1 = (blockIdx().y - 1) * blockDim().y + threadIdx().y
159+
160+
if ik <= length(kmask_indcs) && ic1 <= size(kL2_rs, 2)
161+
ind = kmask_indcs[ik]
162+
acc = zero(eltype(kL2_rs))
163+
164+
@inbounds for ic2 in axes(Λ, 2)
165+
acc += Λ[ic1, ic2, ik] * kL1_rs[ind, ic2]
166+
end
167+
kL2_rs[ind, ic1] = acc
168+
end
169+
return
170+
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

0 commit comments

Comments
 (0)