Global Variables

Most programmers are familiar with the concept of a "global variable": a variable which is globally accessible to any function in the user's program. In Julia, programmers are told to avoid using global variables (also known as "globals") because of their tendency to introduce type instabilities. However, they're often useful for sharing data between functions in distinct areas of the user's program.

In the JuliaGPU ecosystem, globals in the Julia sense are not available unless their value is constant and inlinable into the function referencing them, as all GPU kernels must be statically compileable. However, a different sort of global variable is available which serves a very similar purpose. This variant of global variable is statically typed and sized, and is accessible from: all kernels with the same function signature (e.g. mykernel(a::Int32, b::Float64)), the CPU host, and other devices and kernels when accessed by pointer.

Global variables can be created within kernels with the AMDGPU.get_global_pointer function, which both declares the global variable, and returns a pointer to it (specifically a AMDGPU.DevicePtr). Once a kernel which declares a global is compiled for GPU execution (either by @roc or rocfunction), the global is allocated memory and made available to the kernel (during the linking stage). Globals are unique by name, and so you shouldn't attempt to call get_global_pointer with the same name but a different type; if you do, undefined behavior will result. Like regular pointers in Julia, you can use functions like Base.unsafe_load and Base.unsafe_store! to read from and write to the global variable, respectively.

As a concrete example of global variable usage, let's define a kernel which creates a global and uses its value to increment the indices of an array:

function my_kernel(A)
    idx = workitemIdx().x
    ptr = AMDGPU.get_global_pointer(Val(:myglobal), Float32)
    A[idx] += Base.unsafe_load(ptr)
    nothing
end

In order to access and modify this global before the kernel is launched, we can specify a hook function to @roc which will be passed the global pointer as an argument:

function myglobal_hook(gbl, mod, dev)
    gbl_ptr = Base.unsafe_convert(Ptr{Float32}, gbl.ptr)
    Base.unsafe_store!(gbl_ptr, 42f0)
end
RA = ROCArray(ones(Float32, 4))
wait(@roc groupsize=4 global_hooks=(myglobal=myglobal_hook,) my_kernel(RA))

In the above function, gbl_ptr is a pointer (specifically a Ptr{Float32}) to the memory that represents the global variable myglobal. We can't guarantee the initial value of an uninitialized global variable, so we need to write a value to that global variable (in this case 42::Float32).

We can then read the values of HA and see that it's what we expect:

julia> A = Array(HA)
4-element HSAArray{Float32,1}:
 43.0
 43.0
 43.0
 43.0