using WEANJ, Enzyme, CUDA, KernelAbstractions, Printf
include("common.jl") # Setup, wrap, minimg, taper, inputs
d = ensure_inputs()
x0, v0, target0 = d["x0"], d["v"], d["target"]
s0 = Setup()Setup{Float64}(50, 0.01, 0.005, 1024)
In [1]:
using WEANJ, Enzyme, CUDA, KernelAbstractions, Printf
include("common.jl") # Setup, wrap, minimg, taper, inputs
d = ensure_inputs()
x0, v0, target0 = d["x0"], d["v"], d["target"]
s0 = Setup()Setup{Float64}(50, 0.01, 0.005, 1024)
In [1]:
function deposit_loss(x0::Vector{T}, v, target, s::Setup) where {T}
N, nb = length(x0), s.nb
H = zeros(T, nb)
sel = Int[] # grows and shrinks with the data
for t in 1:s.T
c = T(t) / s.T # window centre
empty!(sel)
for i in 1:N # compaction: indices of x[mask]
d = minimg(x0[i] + v[i] * (t * s.dt) - c)
abs(d) < s.halfwidth && push!(sel, i)
end
for i in sel # work only for the selected particles
x = wrap(x0[i] + v[i] * (t * s.dt))
w = taper(minimg(x - c), s.halfwidth)
g = x * nb - T(0.5) # cloud-in-cell deposit
j = floor(Int, g)
f = g - j
H[mod(j, nb) + 1] += w * (1 - f) # scatter-add at a data-dependent index
H[mod(j + 1, nb) + 1] += w * f
end
end
return sum(abs2, H .- target)
enddeposit_loss (generic function with 1 method)
In [1]:
function deposit_gradient(x0::Vector, v, target, s)
dx = zero(x0)
Enzyme.autodiff(Reverse, deposit_loss, Active, Duplicated(x0, dx), Const(v), Const(target), Const(s))
return dx
end
g_cpu = deposit_gradient(x0, v0, target0, s0)
@printf "loss = %.10f ‖∇loss‖ = %.6f nonzero gradient entries: %d of %d\n" deposit_loss(x0, v0, target0, s0) sqrt(sum(abs2, g_cpu)) count(!iszero, g_cpu) length(x0)loss = 559.5605712700 ‖∇loss‖ = 52854.055962 nonzero gradient entries: 983 of 2000
In [1]:
# On the GPU the natural form is one thread per particle and step: threads outside the
# window return immediately, the rest scatter into H with atomic adds.
Enzyme.EnzymeRules.inactive_type(::Type{<:Setup}) = true # constants, never differentiated
@kernel function deposit_step!(H, @Const(x0), @Const(v), s, t)
i = @index(Global)
T = eltype(H)
@inbounds begin
x = wrap(x0[i] + v[i] * (t * s.dt))
d = minimg(x - T(t) / s.T)
if abs(d) < s.halfwidth
w = taper(d, s.halfwidth)
g = x * s.nb - T(0.5)
j = unsafe_trunc(Int32, floor(g))
f = g - j
KernelAbstractions.@atomic H[mod(j, Int32(s.nb)) + 1] += w * (1 - f)
KernelAbstractions.@atomic H[mod(j + Int32(1), Int32(s.nb)) + 1] += w * f
end
end
end
function deposit!(H, x0, v, s)
fill!(H, 0)
for t in 1:s.T
deposit_step!(get_backend(H))(H, x0, v, s, t; ndrange=length(x0))
end
return nothing
end
function deposit_loss(x0::CuVector, v, target, s)
H = similar(x0, s.nb)
deposit!(H, x0, v, s)
return sum(abs2, H .- target)
enddeposit_loss (generic function with 2 methods)
In [1]:
"Reverse through deposit!, seeded with ∂loss/∂H = 2(H − target)."
function deposit_gradient(x0::CuVector{T}, v, target, s) where {T}
H = similar(x0, s.nb)
deposit!(H, x0, v, s)
dH = 2 .* (H .- target)
dx = CUDA.zeros(T, length(x0))
Enzyme.autodiff(Reverse, deposit!, Const, Duplicated(H, dH), Duplicated(x0, dx), Const(v), Const(s))
return dx
end
g_gpu = Array(deposit_gradient(CuArray(x0), CuArray(v0), CuArray(target0), s0))
@printf "max |∇_gpu − ∇_cpu| / max |∇_cpu| = %.1e\n" maximum(abs, g_gpu - g_cpu) / maximum(abs, g_cpu)max |∇_gpu − ∇_cpu| / max |∇_cpu| = 4.0e-16
In [1]:
err = fd_check(y -> deposit_loss(y, v0, target0, s0), x0, g_cpu; n=20, h=1e-7)
@assert maximum(abs, g_gpu - g_cpu) <= 1e-10 * maximum(abs, g_cpu) # atomics reorder sums
save_gradient(EXAMPLE, "enzyme", g_cpu)
@printf "finite differences: max rel. error %.1e on 20 components\n" errfinite differences: max rel. error 7.2e-06 on 20 components
In [1]:
Ns = [10^4, 10^5, 10^6, 10^7]
gpu_sync = _ -> CUDA.synchronize()
records = Dict("cpu" => [], "gpu" => [])
for (device, prep, sync) in (("cpu", identity, identity), ("gpu", CuArray, gpu_sync))
for (k, N) in enumerate(Ns)
x, v, tg = prep.(inputs(N))
compile_s = k == 1 ? @elapsed(sync(deposit_gradient(x, v, tg, s0))) : nothing
samples = N >= 10^6 ? 3 : 10
push!(records[device], (; dtype="Float64", N,
forward_s = time_median(() -> deposit_loss(x, v, tg, s0); sync, samples),
grad_s = time_median(() -> deposit_gradient(x, v, tg, s0); sync, samples),
compile_s,
# host bytes allocated by one gradient (CPU only): Enzyme's tape stores the
# selected particles of every step, not all N
peak_mem_bytes = device == "cpu" ? @allocated(deposit_gradient(x, v, tg, s0)) : nothing))
end
write_results(EXAMPLE, "enzyme", device, records[device])
end
for d in ("cpu", "gpu"), r in records[d]
@printf "%s N=%-9d forward %9.3f ms gradient %9.3f ms %s\n" d r.N 1e3r.forward_s 1e3r.grad_s (r.peak_mem_bytes === nothing ? "" : @sprintf("allocated %.1f MiB", r.peak_mem_bytes / 2^20))
endcpu N=10000 forward 0.884 ms gradient 0.988 ms allocated 0.1 MiB
cpu N=100000 forward 8.875 ms gradient 23.716 ms allocated 0.8 MiB
cpu N=1000000 forward 94.128 ms gradient 275.931 ms allocated 7.9 MiB
cpu N=10000000 forward 1253.366 ms gradient 3180.572 ms allocated 78.0 MiB
gpu N=10000 forward 0.404 ms gradient 3.407 ms
gpu N=100000 forward 0.788 ms gradient 3.832 ms
gpu N=1000000 forward 4.978 ms gradient 16.430 ms
gpu N=10000000 forward 41.608 ms gradient 146.902 ms