JuliaGPU / JuliaGPU/GPUCompiler.jl

PTX: byval results in local memory usage

Open
#92 9 comments 0 reactions 0 assignees View on GitHub
ptx
Dominant language
Julia
Stars
187
Forks
68
Avg merge
1d 12h
Merged PRs (30d)
28

Description

Trying to migrate to CUDA.jl from CUDAdrv.jl (and Julia 1.3.1) but can't still due to performance issues. Here is an (artificial) example I managed to boil down to. I have this test code` add_kernel.jl`, where I have an array of 2d arrays and I want add it to another array of 2d arrays :

```
const threads = 256

#simple add matrixes kernel
function kernel_add_mat(n, x1, x2, y)
i = (blockIdx().x-1) * blockDim().x + threadIdx().x
if i <= n
@inbounds y[i] = x1[i] + x2[i]
end
return
end

@inline get_inputs3(indx_y, a, b, c) = (a, b, c)
@inline get_inputs3(indx_y, a1, a2, b1, b2, c1, c2) = indx_y == 1 ? (a1, b1, c1) : (a2, b2, c2)
@inline get_inputs3(indx_y, a1, a2, a3, b1, b2, b3, c1, c2, c3) = indx_y == 1 ? (a1, b1, c1) : indx_y == 2 ? (a2, b2, c2) : (a3, b3, c3)

#add arrays of matrixes kernel
function kernel_add_mat_z_slices(n, vararg...)
x1, x2, y = get_inputs3(blockIdx().y, vararg...)
i = (blockIdx().x-1) * blockDim().x + threadIdx().x
if i <= n
@inbounds y[i] = x1[i] + x2[i]
end
return
end

function add_z_slices!(y, x1, x2)
m1, n1 = size(x1[1]) #get size of first slice
blocks = (m1 * n1 + threads - 1) ÷ threads
#get length(x1) more blocks than needed to process 1 slice
@cuda blocks = blocks, length(x1) threads = threads kernel_add_mat_z_slices(m1 * n1, x1..., x2..., y...)
end

function add!(y, x1, x2)
m1, n1 = size(x1)
blocks = (m1 * n1 + threads - 1) ÷ threads
@cuda blocks = blocks, 1 threads = threads kernel_add_mat(m1 * n1, x1, x2, y)
end

num_z_slices = 3
Random.seed!(1)

#m, n = 7, 5 # tiny to measure overhead
#m, n = 521, 111
#m, n = 1521, 1111
#m, n = 3001, 1511 # prime numbers to test memory access correctness
m, n = 3072, 1536 # 256 multiplier
#m, n = 6007, 3001 # prime numbers to test memory access correctness

x1 = [cu(randn(Float32, (m, n)) .+ Float32(0.5)) for i = 1:num_z_slices]
x2 = [cu(randn(Float32, (m, n)) .+ Float32(0.5)) for i = 1:num_z_slices]
y1 = [similar(x1[1]) for i = 1:num_z_slices]

#reference down to bones add on GPU
add!(y1[1], x1[1], x2[1])
print("add! ");
@btime begin add!($y1[1], $x1[1], $x2[1]); synchronize() end

#adding arrays in an array
for slices = 1:num_z_slices
add_z_slices!(y1[1:slices], x1[1:slices], x2[1:slices])
print("add_z_slices!, slices = $slices ");
@btime begin add_z_slices!($y1[1:$slices], $x1[1:$slices], $x2[1:$slices]); synchronize() end
end
```
in julia 1.3.1 with CUDADrv (Win 10, GTX 2070, I have only 1 card.) I get the following numbers
```
add! 167.600 μs (35 allocations: 1.11 KiB)
add_z_slices!, slices = 1 171.101 μs (75 allocations: 2.97 KiB)
add_z_slices!, slices = 2 313.199 μs (93 allocations: 4.09 KiB)
add_z_slices!, slices = 3 456.600 μs (111 allocations: 5.41 KiB)
```
there is a slight overhead for adding z slicing code in case of just one slice. and then time grows reasonably linearly with the number of z slices increasing. however with julia 1.5.2 and CUDA.jl I get the following numbers:
```
add! 167.100 μs (14 allocations: 400 bytes)
add_z_slices!, slices = 1 169.800 μs (56 allocations: 2.33 KiB)
add_z_slices!, slices = 2 4.536 ms (68 allocations: 3.02 KiB)
add_z_slices!, slices = 3 2.435 ms (80 allocations: 3.83 KiB)
```
i.e. totally unreasonable growth with slices 2 and 3 (yet same performance with simple add kernel and even just one slice). Looks like an issue in CUDA.jl

here is how I setup julia 1.3.1 env
```
using Pkg
Pkg.activate("CUDAdrv")
pkg_ref = Dict("BenchmarkTools" => v"0.5.0",
"CUDAdrv" => v"5.0.1",
"CUDAnative" => v"2.7.0",
"CuArrays" => v"1.6.0",
)

for pkg in keys(pkg_ref)
println("install $pkg => ", pkg_ref[pkg])
Pkg.add(PackageSpec(name=pkg, version=pkg_ref[pkg]))
end
```
and this is how I setup julia 1.5.2 env
```
using Pkg
Pkg.activate("Cuda")

pkg_ref = Dict("BenchmarkTools" => v"0.5.0",
"CUDA" => v"2.0.0"
)

for pkg in keys(pkg_ref)
println("install $pkg => ", pkg_ref[pkg])
Pkg.add(PackageSpec(name=pkg, version=pkg_ref[pkg]))
end
```

and this is code to call the kernels in 1.3.1 `add_kernel_1.3.1.jl`:
```
using Pkg
Pkg.activate("CUDAdrv")

using BenchmarkTools, Printf, Random
using CUDAdrv: CuDevice, CuContext, DeviceSet, CuDefaultStream, synchronize, unsafe_destroy!, CUctx_flags, devices

use_dev = 0
dev = CuDevice(use_dev)
ctx = CuContext(dev)
using CUDAnative
CUDAnative.device!(use_dev)
using CuArrays
include("add_kernel.jl")
```
this is code to call the kernels in 1.5.2 `add_kernel_1.5.2.jl`:
```
using Pkg
Pkg.activate("Cuda")
using BenchmarkTools, Printf, Random, CUDA
include("add_kernel.jl")
```

and I call it like this
`C:\Bin\Julia-1.3.1\bin\julia.exe add_kernel_1.3.1.jl`
`C:\Bin\Julia 1.5.2\bin\julia.exe add_kernel_1.5.2.jl`

aj

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by running add_kernel.jl with add_kernel_1.3.1.jl and add_kernel_1.5.2.jl, comparing the single-slice and multi-slice kernels and their reported timings. Inspect the generated PTX or local-memory behavior for kernel_add_mat_z_slices. Done means the multi-slice performance regression is explained and corrected without changing the intended results.

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
compilers, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.