You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hmm, this is a little disconcerting: Even a very simple Cartesian kernel inhibits a very significant slowdown.
Reduced from our broadcast implementation:
using CUDA, KernelAbstractions, Chairmarks
functionmain()
a =CuArray{Float32}(undef, 512, 1000)
bc = Broadcast.broadcasted(identity, 0f0)
bc = Broadcast.instantiate(Broadcast.Broadcasted(bc.f, bc.args, axes(a)))
print("Old: ")
display(@b CUDA.@synccopyto_old!(a, bc))
print("New: ")
display(@b CUDA.@synccopyto_new!(a, bc))
end@inlinefunctioncopyto_old!(dest::AbstractArray, bc)
axes(dest) ==axes(bc) || Broadcast.throwdm(axes(dest), axes(bc))
isempty(dest) &&return dest
bc = Broadcast.preprocess(dest, bc)
functionbroadcast_kernel(dest, bc)
i = (blockIdx().x-1) *blockDim().x +threadIdx().x
@inboundsif i <=length(dest)
I =CartesianIndices(dest)[i]
dest[I] = bc[I]
endreturnend
kernel =@cuda launch=falsebroadcast_kernel(dest, bc)
config =launch_configuration(kernel.fun)
threads =min(length(dest), config.threads)
blocks =cld(length(dest), threads)
kernel(dest, bc; threads, blocks)
return dest
end@inlinefunctioncopyto_new!(dest::AbstractArray, bc)
axes(dest) ==axes(bc) || Broadcast.throwdm(axes(dest), axes(bc))
isempty(dest) &&return dest
bc = Broadcast.preprocess(dest, bc)
@kernelfunctionbroadcast_kernel(dest, bc)
I =@index(Global, Cartesian)
@inbounds dest[I] = bc[I]
endbroadcast_kernel(get_backend(dest))(dest, bc; ndrange=size(dest))
return dest
end
Without CUDA.@sync, i.e., measuring launch overhead:
define ptx_kernel void @_Z20gpu_broadcast_kernel16CompilerMetadataI11DynamicSize12DynamicCheckv16CartesianIndicesILi2E5TupleI5OneToI5Int64ES6_EE7NDRangeILi2ES0_S0_S8_S8_EE13CuDeviceArrayI7Float32Li2ELi1EE11BroadcastedI17DefaultArrayStyleILi0EES7_8identityS3_ISD_EE({ i64, i32 } %state, { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, { i8 addrspace(1)*, i64, [2 x i64], i64 } %1, { [1 x float], [2 x [1 x i64]] } %2) local_unnamed_addr {
conversion:
%.fca.0.0.0.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 0, 0, 0, 0
%.fca.0.0.1.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 0, 0, 1, 0
%.fca.1.0.0.0.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 1, 0, 0, 0, 0
%.fca.1.0.0.1.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 1, 0, 0, 1, 0
%.fca.1.1.0.0.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 1, 1, 0, 0, 0
%.fca.1.1.0.1.0.extract = extractvalue { [1 x [2 x [1 x i64]]], [2 x [1 x [2 x [1 x i64]]]] } %0, 1, 1, 0, 1, 0
%3 = icmp sgt i64 %.fca.1.0.0.0.0.extract, 0
call void @llvm.assume(i1 %3)
%4 = icmp sgt i64 %.fca.1.0.0.1.0.extract, 0
call void @llvm.assume(i1 %4)
%5 = icmp sgt i64 %.fca.1.1.0.0.0.extract, 0
call void @llvm.assume(i1 %5)
%6 = icmp sgt i64 %.fca.1.1.0.1.0.extract, 0
call void @llvm.assume(i1 %6)
%7 = call i32 @llvm.nvvm.read.ptx.sreg.tid.x()
%8 = zext i32 %7 to i64
%9 = call i32 @llvm.nvvm.read.ptx.sreg.ctaid.x()
%10 = zext i32 %9 to i64
%11 = udiv i64 %10, %.fca.1.0.0.0.0.extract
%.neg22 = mul i64 %11, %.fca.1.0.0.0.0.extract
%12 = udiv i64 %8, %.fca.1.1.0.0.0.extract
%13 = add nuw nsw i64 %12, 1
%reass.add23 = add i64 %.neg22, %12
%reass.add = sub i64 %10, %reass.add23
%reass.mul = mul i64 %reass.add, %.fca.1.1.0.0.0.extract
%14 = add nuw nsw i64 %8, 1
%15 = add i64 %14, %reass.mul
%16 = mul i64 %11, %.fca.1.1.0.1.0.extract
%17 = add i64 %13, %16
%18 = icmp sgt i64 %15, 0
%19 = icmp sle i64 %15, %.fca.0.0.0.0.extract
%20 = and i1 %18, %19
%21 = icmp sgt i64 %17, 0
%22 = icmp sle i64 %17, %.fca.0.0.1.0.extract
%23 = and i1 %21, %22
%24 = and i1 %23, %20
br i1 %24, label %pass6, label %L585
L585: ; preds = %pass6, %conversion
ret void
pass6: ; preds = %conversion
%.fca.0.0.extract = extractvalue { [1 x float], [2 x [1 x i64]] } %2, 0, 0
%.fca.2.0.extract = extractvalue { i8 addrspace(1)*, i64, [2 x i64], i64 } %1, 2, 0
%.fca.0.extract = extractvalue { i8 addrspace(1)*, i64, [2 x i64], i64 } %1, 0
%25 = add i64 %16, %12
%26 = mul i64 %25, %.fca.2.0.extract
%27 = add i64 %26, %8
%28 = add i64 %27, %reass.mul
%29 = bitcast i8 addrspace(1)* %.fca.0.extract to float addrspace(1)*
%30 = getelementptr inbounds float, float addrspace(1)* %29, i64 %28
store float %.fca.0.0.extract, float addrspace(1)* %30, align 4
br label %L585
}
The added arithmetic instructions are very clear now, and the udivs are probably still the culprit.
It's curious that the old version doesn't have any integer divisions to convert the linear index into a global one. Presumably this is possible because the array style of the broadcast is 0D (we're broadcasting a simple scalar), and when using KA.jl that somehow gets lost.
Indeed, when broadcasting an actual array (where the old code would have to udiv too), the performance is much closer:
functionmain()
a =CuArray{Float32}(undef, 4000, 4000)
b =CuArray{Float32}(undef, 4000, 4000)
bc = Broadcast.broadcasted(identity, b)
bc = Broadcast.instantiate(Broadcast.Broadcasted(bc.f, bc.args, axes(a)))
print("Old: ")
display(@b CUDA.@synccopyto_old!(a, bc))
print("New: ")
display(@b CUDA.@synccopyto_new!(a, bc))
end
JuliaGPU/KernelAbstractions.jl#539 results in only a single sdiv, which is an improvement, but still not the 0 div case reported here (which comes from broadcasting a scalar).
To summarize, there seems to be (at least) three areas to improve / optimize:
scalar broadcast now having udiv: somehow the scalar nature of the broadcast is getting lost
more complex linear index calculations, relying on block size from arguments instead of using hardware indices: maybe this is solved by Try fast linear indexes for KA CUDA.jl#2612
moving towards using ND hardware indices to avoid the linear->cartesian computation where possible
Activity
maleadt commentedon Oct 18, 2024
Hmm, this is a little disconcerting: Even a very simple Cartesian kernel inhibits a very significant slowdown.
Reduced from our
broadcastimplementation:Without
CUDA.@sync, i.e., measuring launch overhead:With
CUDA.@sync, i.e., measuring execution time:The overhead scales, e.g., using 4k x 4k inputs instead:
Generated code looks pretty bad, with both extra exceptions, branches, and argument mangling:
This results in a much higher register usage, from 28 to 64 (at the PTX level).
cc @vchuravy
maleadt commentedon Oct 18, 2024
Looks like most of the added code comes from KA's nditeration handlng:
A couple of other things that stand out:
sdivsblockDim, while KA.jl somehow doesn't (I guess it never computes a global linear index?)maleadt commentedon Oct 18, 2024
Testing on JuliaGPU/KernelAbstractions.jl#518, a bit of performance is recovered, but it remains bad:
Updated MWE:
Old:
New:
The added arithmetic instructions are very clear now, and the
udivs are probably still the culprit.It's curious that the old version doesn't have any integer divisions to convert the linear index into a global one. Presumably this is possible because the array style of the broadcast is 0D (we're broadcasting a simple scalar), and when using KA.jl that somehow gets lost.
Indeed, when broadcasting an actual array (where the old code would have to
udivtoo), the performance is much closer:Old:
Notice the
udiv.KA.jl:
Still a lot more code, and and additional
udiv, but at least it makes the hypothesis more likely.maleadt commentedon Oct 18, 2024
JuliaGPU/KernelAbstractions.jl#539 results in only a single
sdiv, which is an improvement, but still not the 0divcase reported here (which comes from broadcasting a scalar).maleadt commentedon Oct 21, 2024
With JuliaGPU/KernelAbstractions.jl#539, performance is interestingly worse. For the scalar broadcast:
(as opposed to 61us on master, and 56us on JuliaGPU/KernelAbstractions.jl#518)
For a 2D broadcast:
... which is again a bit slower than before.
Adding some
assumecalls to get rid of all exceptions (both thedivrelated one, the newly addedDivError, and a remainingInexactError), I get:Remove old code and clean-up tests (#464)
maleadt commentedon Jan 13, 2025
To summarize, there seems to be (at least) three areas to improve / optimize:
scalar broadcast now having
udiv: somehow the scalar nature of the broadcast is getting lostmore complex linear index calculations, relying on block size from arguments instead of using hardware indices: maybe this is solved by Try fast linear indexes for KA CUDA.jl#2612
moving towards using ND hardware indices to avoid the linear->cartesian computation where possible
These are all visible in the MWE from #565 (comment)