Array programming
The CUDA array type, CuArray
, generally implements the Base array interface and all of its expected methods.
diff --git a/previews/PR2495/.documenter-siteinfo.json b/previews/PR2495/.documenter-siteinfo.json new file mode 100644 index 0000000000..1a466b91a4 --- /dev/null +++ b/previews/PR2495/.documenter-siteinfo.json @@ -0,0 +1 @@ +{"documenter":{"julia_version":"1.10.5","generation_timestamp":"2024-09-16T15:13:22","documenter_version":"1.4.0"}} \ No newline at end of file diff --git a/previews/PR2495/api/array/index.html b/previews/PR2495/api/array/index.html new file mode 100644 index 0000000000..6cc980b3e2 --- /dev/null +++ b/previews/PR2495/api/array/index.html @@ -0,0 +1,6 @@ + +
The CUDA array type, CuArray
, generally implements the Base array interface and all of its expected methods.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
The main entry-point to the compiler is the @cuda
macro:
CUDA.@cuda
— Macro@cuda [kwargs...] func(args...)
High-level interface for executing code on a GPU. The @cuda
macro should prefix a call, with func
a callable function or object that should return nothing. It will be compiled to a CUDA function upon first use, and to a certain extent arguments will be converted and managed automatically using cudaconvert
. Finally, a call to cudacall
is performed, scheduling a kernel launch on the current CUDA context.
Several keyword arguments are supported that influence the behavior of @cuda
.
launch
: whether to launch this kernel, defaults to true
. If false
the returned kernel object should be launched by calling it and passing arguments again.dynamic
: use dynamic parallelism to launch device-side kernels, defaults to false
.cufunction
and dynamic_cufunction
CUDA.HostKernel
and CUDA.DeviceKernel
If needed, you can use a lower-level API that lets you inspect the compiler kernel:
CUDA.cudaconvert
— Functioncudaconvert(x)
This function is called for every argument to be passed to a kernel, allowing it to be converted to a GPU-friendly format. By default, the function does nothing and returns the input object x
as-is.
Do not add methods to this function, but instead extend the underlying Adapt.jl package and register methods for the the CUDA.KernelAdaptor
type.
CUDA.cufunction
— Functioncufunction(f, tt=Tuple{}; kwargs...)
Low-level interface to compile a function invocation for the currently-active GPU, returning a callable kernel object. For a higher-level interface, use @cuda
.
The following keyword arguments are supported:
minthreads
: the required number of threads in a thread blockmaxthreads
: the maximum number of threads in a thread blockblocks_per_sm
: a minimum number of thread blocks to be scheduled on a single multiprocessormaxregs
: the maximum number of registers to be allocated to a single thread (only supported on LLVM 4.0+)name
: override the name that the kernel will have in the generated codealways_inline
: inline all function calls in the kernelfastmath
: use less precise square roots and flush denormalscap
and ptx
: to override the compute capability and PTX version to compile forThe output of this function is automatically cached, i.e. you can simply call cufunction
in a hot path without degrading performance. New code will be generated automatically, when when function changes, or when different types or keyword arguments are provided.
CUDA.HostKernel
— Type(::HostKernel)(args...; kwargs...)
+(::DeviceKernel)(args...; kwargs...)
Low-level interface to call a compiled kernel, passing GPU-compatible arguments in args
. For a higher-level interface, use @cuda
.
A HostKernel
is callable on the host, and a DeviceKernel
is callable on the device (created by @cuda
with dynamic=true
).
The following keyword arguments are supported:
threads
(default: 1
): Number of threads per block, or a 1-, 2- or 3-tuple of dimensions (e.g. threads=(32, 32)
for a 2D block of 32×32 threads). Use threadIdx()
and blockDim()
to query from within the kernel.blocks
(default: 1
): Number of thread blocks to launch, or a 1-, 2- or 3-tuple of dimensions (e.g. blocks=(2, 4, 2)
for a 3D grid of blocks). Use blockIdx()
and gridDim()
to query from within the kernel.shmem
(default: 0
): Amount of dynamic shared memory in bytes to allocate per thread block; used by CuDynamicSharedArray
.stream
(default: stream()
): CuStream
to launch the kernel on.cooperative
(default: false
): whether to launch a cooperative kernel that supports grid synchronization (see CG.this_grid
and CG.sync
). Note that this requires care wrt. the number of blocks launched.CUDA.version
— Functionversion(k::HostKernel)
Queries the PTX and SM versions a kernel was compiled for. Returns a named tuple.
CUDA.maxthreads
— Functionmaxthreads(k::HostKernel)
Queries the maximum amount of threads a kernel can use in a single block.
CUDA.registers
— Functionregisters(k::HostKernel)
Queries the register usage of a kernel.
CUDA.memory
— Functionmemory(k::HostKernel)
Queries the local, shared and constant memory usage of a compiled kernel in bytes. Returns a named tuple.
If you want to inspect generated code, you can use macros that resemble functionality from the InteractiveUtils standard library:
@device_code_lowered
+@device_code_typed
+@device_code_warntype
+@device_code_llvm
+@device_code_ptx
+@device_code_sass
+@device_code
These macros are also available in function-form:
CUDA.code_typed
+CUDA.code_warntype
+CUDA.code_llvm
+CUDA.code_ptx
+CUDA.code_sass
For more information, please consult the GPUCompiler.jl documentation. Only the code_sass
functionality is actually defined in CUDA.jl:
CUDA.@device_code_sass
— Macro@device_code_sass [io::IO=stdout, ...] ex
Evaluates the expression ex
and prints the result of CUDA.code_sass
to io
for every executed CUDA kernel. For other supported keywords, see CUDA.code_sass
.
CUDA.code_sass
— Functioncode_sass([io], f, types; raw=false)
+code_sass(f, [io]; raw=false)
Prints the SASS code corresponding to one or more CUDA modules to io
, which defaults to stdout
.
If providing both f
and types
, it is assumed that this uniquely identifies a kernel function, for which SASS code will be generated, and printed to io
.
If only providing a callable function f
, typically specified using the do
syntax, the SASS code for all modules executed during evaluation of f
will be printed. This can be convenient to display the SASS code for functions whose source code is not available.
raw
: dump the assembly like nvdisasm
reports it, without post-processing;f
and types
: all keyword arguments from cufunction
See also: @device_code_sass
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
CUDA.functional
— Methodfunctional(show_reason=false)
Check if the package has been configured successfully and is ready to use.
This call is intended for packages that support conditionally using an available GPU. If you fail to check whether CUDA is functional, actual use of functionality might warn and error.
CUDA.has_cuda
— Functionhas_cuda()::Bool
Check whether the local system provides an installation of the CUDA driver and runtime. Use this function if your code loads packages that require CUDA.jl. ```
CUDA.has_cuda_gpu
— Functionhas_cuda_gpu()::Bool
Check whether the local system provides an installation of the CUDA driver and runtime, and if it contains a CUDA-capable GPU. See has_cuda
for more details.
Note that this function initializes the CUDA API in order to check for the number of GPUs.
CUDA.context
— Functioncontext(ptr)
Identify the context memory was allocated in.
context()::CuContext
Get or create a CUDA context for the current thread (as opposed to current_context
which may return nothing
if there is no context bound to the current thread).
CUDA.context!
— Functioncontext!(ctx::CuContext)
+context!(ctx::CuContext) do ... end
Bind the current host thread to the context ctx
. Returns the previously-bound context. If used with do-block syntax, the change is only temporary.
Note that the contexts used with this call should be previously acquired by calling context
, and not arbitrary contexts created by calling the CuContext
constructor.
CUDA.device
— Functiondevice(::CuContext)
Returns the device for a context.
device(ptr)
Identify the device memory was allocated on.
device()::CuDevice
Get the CUDA device for the current thread, similar to how context()
works compared to current_context()
.
CUDA.device!
— Functiondevice!(dev::Integer)
+device!(dev::CuDevice)
+device!(dev) do ... end
Sets dev
as the current active device for the calling host thread. Devices can be specified by integer id, or as a CuDevice
(slightly faster). Both functions can be used with do-block syntax, in which case the device is only changed temporarily, without changing the default device used to initialize new threads or tasks.
Calling this function at the start of a session will make sure CUDA is initialized (i.e., a primary context will be created and activated).
CUDA.device_reset!
— Functiondevice_reset!(dev::CuDevice=device())
Reset the CUDA state associated with a device. This call with release the underlying context, at which point any objects allocated in that context will be invalidated.
Note that this does not guarantee to free up all memory allocations, as many are not bound to a context, so it is generally not useful to call this function to free up memory.
This function is only reliable on CUDA driver >= v12.0, and may lead to crashes if used on older drivers.
CUDA.stream
— Functionstream()
Get the CUDA stream that should be used as the default one for the currently executing task.
CUDA.stream!
— Functionstream!(::CuStream)
+stream!(::CuStream) do ... end
Change the default CUDA stream for the currently executing task, temporarily if using the do-block version of this function.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This section lists the package's public functionality that corresponds to special CUDA functions for use in device code. It is loosely organized according to the C language extensions appendix from the CUDA C programming guide. For more information about certain intrinsics, refer to the aforementioned NVIDIA documentation.
CUDA.gridDim
— FunctiongridDim()::NamedTuple
Returns the dimensions of the grid.
CUDA.blockIdx
— FunctionblockIdx()::NamedTuple
Returns the block index within the grid.
CUDA.blockDim
— FunctionblockDim()::NamedTuple
Returns the dimensions of the block.
CUDA.threadIdx
— FunctionthreadIdx()::NamedTuple
Returns the thread index within the block.
CUDA.warpsize
— Functionwarpsize(dev::CuDevice)
Returns the warp size (in threads) of the device.
warpsize()::Int32
Returns the warp size (in threads).
CUDA.laneid
— Functionlaneid()::Int32
Returns the thread's lane within the warp.
CUDA.active_mask
— Functionactive_mask()
Returns a 32-bit mask indicating which threads in a warp are active with the current executing thread.
CUDA.jl provides a primitive, lightweight array type to manage GPU data organized in an plain, dense fashion. This is the device-counterpart to the CuArray
, and implements (part of) the array interface as well as other functionality for use on the GPU:
CUDA.CuDeviceArray
— TypeCuDeviceArray{T,N,A}(ptr, dims, [maxsize])
Construct an N
-dimensional dense CUDA device array with element type T
wrapping a pointer, where N
is determined from the length of dims
and T
is determined from the type of ptr
. dims
may be a single scalar, or a tuple of integers corresponding to the lengths in each dimension). If the rank N
is supplied explicitly as in Array{T,N}(dims)
, then it must match the length of dims
. The same applies to the element type T
, which should match the type of the pointer ptr
.
CUDA.Const
— TypeConst(A::CuDeviceArray)
Mark a CuDeviceArray as constant/read-only. The invariant guaranteed is that you will not modify an CuDeviceArray for the duration of the current kernel.
This API can only be used on devices with compute capability 3.5 or higher.
Experimental API. Subject to change without deprecation.
CUDA.CuStaticSharedArray
— FunctionCuStaticSharedArray(T::Type, dims) -> CuDeviceArray{T,N,AS.Shared}
Get an array of type T
and dimensions dims
(either an integer length or tuple shape) pointing to a statically-allocated piece of shared memory. The type should be statically inferable and the dimensions should be constant, or an error will be thrown and the generator function will be called dynamically.
CUDA.CuDynamicSharedArray
— FunctionCuDynamicSharedArray(T::Type, dims, offset::Integer=0) -> CuDeviceArray{T,N,AS.Shared}
Get an array of type T
and dimensions dims
(either an integer length or tuple shape) pointing to a dynamically-allocated piece of shared memory. The type should be statically inferable or an error will be thrown and the generator function will be called dynamically.
Note that the amount of dynamic shared memory needs to specified when launching the kernel.
Optionally, an offset parameter indicating how many bytes to add to the base shared memory pointer can be specified. This is useful when dealing with a heterogeneous buffer of dynamic shared memory; in the case of a homogeneous multi-part buffer it is preferred to use view
.
CUDA.CuDeviceTexture
— TypeCuDeviceTexture{T,N,M,NC,I}
N
-dimensional device texture with elements of type T
. This type is the device-side counterpart of CuTexture{T,N,P}
, and can be used to access textures using regular indexing notation. If NC
is true, indices used by these accesses should be normalized, i.e., fall into the [0,1)
domain. The I
type parameter indicates the kind of interpolation that happens when indexing into this texture. The source memory of the texture is specified by the M
parameter, either linear memory or a texture array.
Device-side texture objects cannot be created directly, but should be created host-side using CuTexture{T,N,P}
and passed to the kernel as an argument.
Experimental API. Subject to change without deprecation.
CUDA.sync_threads
— Functionsync_threads()
Waits until all threads in the thread block have reached this point and all global and shared memory accesses made by these threads prior to sync_threads()
are visible to all threads in the block.
CUDA.sync_threads_count
— Functionsync_threads_count(predicate)
Identical to sync_threads()
with the additional feature that it evaluates predicate for all threads of the block and returns the number of threads for which predicate
evaluates to true.
CUDA.sync_threads_and
— Functionsync_threads_and(predicate)
Identical to sync_threads()
with the additional feature that it evaluates predicate for all threads of the block and returns true
if and only if predicate
evaluates to true
for all of them.
CUDA.sync_threads_or
— Functionsync_threads_or(predicate)
Identical to sync_threads()
with the additional feature that it evaluates predicate for all threads of the block and returns true
if and only if predicate
evaluates to true
for any of them.
CUDA.sync_warp
— Functionsync_warp(mask::Integer=FULL_MASK)
Waits threads in the warp, selected by means of the bitmask mask
, have reached this point and all global and shared memory accesses made by these threads prior to sync_warp()
are visible to those threads in the warp. The default value for mask
selects all threads in the warp.
Requires CUDA >= 9.0 and sm_6.2
CUDA.threadfence_block
— Functionthreadfence_block()
A memory fence that ensures that:
threadfence_block()
are observed by all threads in the block of the calling thread as occurring before all writes to all memory made by the calling thread after the call to threadfence_block()
threadfence_block()
are ordered before all reads from all memory made by the calling thread after the call to threadfence_block()
.CUDA.threadfence
— Functionthreadfence()
A memory fence that acts as threadfence_block
for all threads in the block of the calling thread and also ensures that no writes to all memory made by the calling thread after the call to threadfence()
are observed by any thread in the device as occurring before any write to all memory made by the calling thread before the call to threadfence()
.
Note that for this ordering guarantee to be true, the observing threads must truly observe the memory and not cached versions of it; this is requires the use of volatile loads and stores, which is not available from Julia right now.
CUDA.threadfence_system
— Functionthreadfence_system()
A memory fence that acts as threadfence_block
for all threads in the block of the calling thread and also ensures that all writes to all memory made by the calling thread before the call to threadfence_system()
are observed by all threads in the device, host threads, and all threads in peer devices as occurring before all writes to all memory made by the calling thread after the call to threadfence_system()
.
CUDA.clock
— Functionclock(UInt32)
Returns the value of a per-multiprocessor counter that is incremented every clock cycle.
clock(UInt64)
Returns the value of a per-multiprocessor counter that is incremented every clock cycle.
CUDA.nanosleep
— Functionnanosleep(t)
Puts a thread for a given amount t
(in nanoseconds).
Requires CUDA >= 10.0 and sm_6.2
The warp vote functions allow the threads of a given warp to perform a reduction-and-broadcast operation. These functions take as input a boolean predicate from each thread in the warp and evaluate it. The results of that evaluation are combined (reduced) across the active threads of the warp in one different ways, broadcasting a single return value to each participating thread.
CUDA.vote_all_sync
— Functionvote_all_sync(mask::UInt32, predicate::Bool)
Evaluate predicate
for all active threads of the warp and return whether predicate
is true for all of them.
CUDA.vote_any_sync
— Functionvote_any_sync(mask::UInt32, predicate::Bool)
Evaluate predicate
for all active threads of the warp and return whether predicate
is true for any of them.
CUDA.vote_uni_sync
— Functionvote_uni_sync(mask::UInt32, predicate::Bool)
Evaluate predicate
for all active threads of the warp and return whether predicate
is the same for any of them.
CUDA.vote_ballot_sync
— Functionvote_ballot_sync(mask::UInt32, predicate::Bool)
Evaluate predicate
for all active threads of the warp and return an integer whose Nth bit is set if and only if predicate
is true for the Nth thread of the warp and the Nth thread is active.
CUDA.shfl_sync
— Functionshfl_sync(threadmask::UInt32, val, lane::Integer, width::Integer=32)
Shuffle a value from a directly indexed lane lane
, and synchronize threads according to threadmask
.
CUDA.shfl_up_sync
— Functionshfl_up_sync(threadmask::UInt32, val, delta::Integer, width::Integer=32)
Shuffle a value from a lane with lower ID relative to caller, and synchronize threads according to threadmask
.
CUDA.shfl_down_sync
— Functionshfl_down_sync(threadmask::UInt32, val, delta::Integer, width::Integer=32)
Shuffle a value from a lane with higher ID relative to caller, and synchronize threads according to threadmask
.
CUDA.shfl_xor_sync
— Functionshfl_xor_sync(threadmask::UInt32, val, mask::Integer, width::Integer=32)
Shuffle a value from a lane based on bitwise XOR of own lane ID with mask
, and synchronize threads according to threadmask
.
CUDA.@cushow
— Macro@cushow(ex)
GPU analog of Base.@show
. It comes with the same type restrictions as @cuprintf
.
@cushow threadIdx().x
CUDA.@cuprint
— Macro@cuprint(xs...)
+@cuprintln(xs...)
Print a textual representation of values xs
to standard output from the GPU. The functionality builds on @cuprintf
, and is intended as a more use friendly alternative of that API. However, that also means there's only limited support for argument types, handling 16/32/64 signed and unsigned integers, 32 and 64-bit floating point numbers, Cchar
s and pointers. For more complex output, use @cuprintf
directly.
Limited string interpolation is also possible:
@cuprint("Hello, World ", 42, "\n")
+ @cuprint "Hello, World $(42)\n"
CUDA.@cuprintln
— Macro@cuprint(xs...)
+@cuprintln(xs...)
Print a textual representation of values xs
to standard output from the GPU. The functionality builds on @cuprintf
, and is intended as a more use friendly alternative of that API. However, that also means there's only limited support for argument types, handling 16/32/64 signed and unsigned integers, 32 and 64-bit floating point numbers, Cchar
s and pointers. For more complex output, use @cuprintf
directly.
Limited string interpolation is also possible:
@cuprint("Hello, World ", 42, "\n")
+ @cuprint "Hello, World $(42)\n"
CUDA.@cuprintf
— Macro@cuprintf("%Fmt", args...)
Print a formatted string in device context on the host standard output.
Note that this is not a fully C-compliant printf
implementation; see the CUDA documentation for supported options and inputs.
Also beware that it is an untyped, and unforgiving printf
implementation. Type widths need to match, eg. printing a 64-bit Julia integer requires the %ld
formatting string.
CUDA.@cuassert
— Macro@assert cond [text]
Signal assertion failure to the CUDA driver if cond
is false
. Preferred syntax for writing assertions, mimicking Base.@assert
. Message text
is optionally displayed upon assertion failure.
A failed assertion will crash the GPU, so use sparingly as a debugging tool. Furthermore, the assertion might be disabled at various optimization levels, and thus should not cause any side-effects.
A high-level macro is available to annotate expressions with:
CUDA.@atomic
— Macro@atomic a[I] = op(a[I], val)
+@atomic a[I] ...= val
Atomically perform a sequence of operations that loads an array element a[I]
, performs the operation op
on that value and a second value val
, and writes the result back to the array. This sequence can be written out as a regular assignment, in which case the same array element should be used in the left and right hand side of the assignment, or as an in-place application of a known operator. In both cases, the array reference should be pure and not induce any side-effects.
This interface is experimental, and might change without warning. Use the lower-level atomic_...!
functions for a stable API, albeit one limited to natively-supported ops.
If your expression is not recognized, or you need more control, use the underlying functions:
CUDA.atomic_cas!
— Functionatomic_cas!(ptr::LLVMPtr{T}, cmp::T, val::T)
Reads the value old
located at address ptr
and compare with cmp
. If old
equals to cmp
, stores val
at the same address. Otherwise, doesn't change the value old
. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64. Additionally, on GPU hardware with compute capability 7.0+, values of type UInt16 are supported.
CUDA.atomic_xchg!
— Functionatomic_xchg!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
and stores val
at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_add!
— Functionatomic_add!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes old + val
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32, UInt64, and Float32. Additionally, on GPU hardware with compute capability 6.0+, values of type Float64 are supported.
CUDA.atomic_sub!
— Functionatomic_sub!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes old - val
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_and!
— Functionatomic_and!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes old & val
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_or!
— Functionatomic_or!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes old | val
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_xor!
— Functionatomic_xor!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes old ⊻ val
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_min!
— Functionatomic_min!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes min(old, val)
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_max!
— Functionatomic_max!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes max(old, val)
, and stores the result back to memory at the same address. These operations are performed in one atomic transaction. The function returns old
.
This operation is supported for values of type Int32, Int64, UInt32 and UInt64.
CUDA.atomic_inc!
— Functionatomic_inc!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes ((old >= val) ? 0 : (old+1))
, and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old
.
This operation is only supported for values of type Int32.
CUDA.atomic_dec!
— Functionatomic_dec!(ptr::LLVMPtr{T}, val::T)
Reads the value old
located at address ptr
, computes (((old == 0) | (old > val)) ? val : (old-1) )
, and stores the result back to memory at the same address. These three operations are performed in one atomic transaction. The function returns old
.
This operation is only supported for values of type Int32.
Similarly to launching kernels from the host, you can use @cuda
while passing dynamic=true
for launching kernels from the device. A lower-level API is available as well:
CUDA.dynamic_cufunction
— Functiondynamic_cufunction(f, tt=Tuple{})
Low-level interface to compile a function invocation for the currently-active GPU, returning a callable kernel object. Device-side equivalent of CUDA.cufunction
.
No keyword arguments are supported.
CUDA.DeviceKernel
— Type(::HostKernel)(args...; kwargs...)
+(::DeviceKernel)(args...; kwargs...)
Low-level interface to call a compiled kernel, passing GPU-compatible arguments in args
. For a higher-level interface, use @cuda
.
A HostKernel
is callable on the host, and a DeviceKernel
is callable on the device (created by @cuda
with dynamic=true
).
The following keyword arguments are supported:
threads
(default: 1
): Number of threads per block, or a 1-, 2- or 3-tuple of dimensions (e.g. threads=(32, 32)
for a 2D block of 32×32 threads). Use threadIdx()
and blockDim()
to query from within the kernel.blocks
(default: 1
): Number of thread blocks to launch, or a 1-, 2- or 3-tuple of dimensions (e.g. blocks=(2, 4, 2)
for a 3D grid of blocks). Use blockIdx()
and gridDim()
to query from within the kernel.shmem
(default: 0
): Amount of dynamic shared memory in bytes to allocate per thread block; used by CuDynamicSharedArray
.stream
(default: stream()
): CuStream
to launch the kernel on.cooperative
(default: false
): whether to launch a cooperative kernel that supports grid synchronization (see CG.this_grid
and CG.sync
). Note that this requires care wrt. the number of blocks launched.CUDA.CG
— ModuleCUDA.jl's cooperative groups implementation.
Cooperative groups in CUDA offer a structured approach to synchronize and communicate among threads. They allow developers to define specific groups of threads, providing a means to fine-tune inter-thread communication granularity. By offering a more nuanced alternative to traditional CUDA synchronization methods, cooperative groups enable a more controlled and efficient parallel decomposition in kernel design.
The following functionality is available in CUDA.jl:
sync
, barrier_arrive
, barrier_wait
memcpy_async
, wait
and wait_prior
Noteworthy missing functionality:
CUDA.CG.thread_rank
— Functionthread_rank(group)
Returns the linearized rank of the calling thread along the interval [1, num_threads()]
.
CUDA.CG.num_threads
— Functionnum_threads(group)
Returns the total number of threads in the group.
CUDA.CG.thread_block
— Typethread_block <: thread_group
Every GPU kernel is executed by a grid of thread blocks, and threads within each block are guaranteed to reside on the same streaming multiprocessor. A thread_block
represents a thread block whose dimensions are not known until runtime.
Constructed via this_thread_block
CUDA.CG.this_thread_block
— Functionthis_thread_block()
Constructs a thread_block
group
CUDA.CG.group_index
— Functiongroup_index(tb::thread_block)
3-Dimensional index of the block within the launched grid.
CUDA.CG.thread_index
— Functionthread_index(tb::thread_block)
3-Dimensional index of the thread within the launched block.
CUDA.CG.dim_threads
— Functiondim_threads(tb::thread_block)
Dimensions of the launched block in units of threads.
CUDA.CG.grid_group
— Typegrid_group <: thread_group
Threads within this this group are guaranteed to be co-resident on the same device within the same launched kernel. To use this group, the kernel must have been launched with @cuda cooperative=true
, and the device must support it (queryable device attribute).
Constructed via this_grid
.
CUDA.CG.this_grid
— Functionthis_grid()
Constructs a grid_group
.
CUDA.CG.is_valid
— Functionis_valid(gg::grid_group)
Returns whether the grid_group can synchronize
CUDA.CG.block_rank
— Functionblock_rank(gg::grid_group)
Rank of the calling block within [0, num_blocks)
CUDA.CG.num_blocks
— Functionnum_blocks(gg::grid_group)
Total number of blocks in the group.
CUDA.CG.dim_blocks
— Functiondim_blocks(gg::grid_group)
Dimensions of the launched grid in units of blocks.
CUDA.CG.block_index
— Functionblock_index(gg::grid_group)
3-Dimensional index of the block within the launched grid.
CUDA.CG.coalesced_group
— Typecoalesced_group <: thread_group
A group representing the current set of converged threads in a warp. The size of the group is not guaranteed and it may return a group of only one thread (itself).
This group exposes warp-synchronous builtins. Constructed via coalesced_threads
.
CUDA.CG.coalesced_threads
— Functioncoalesced_threads()
Constructs a coalesced_group
.
CUDA.CG.meta_group_rank
— Functionmeta_group_rank(cg::coalesced_group)
Rank of this group in the upper level of the hierarchy.
CUDA.CG.meta_group_size
— Functionmeta_group_size(cg::coalesced_group)
Total number of partitions created out of all CTAs when the group was created.
CUDA.CG.sync
— Functionsync(group)
Synchronize the threads named in the group, equivalent to calling barrier_wait
and barrier_arrive
in sequence.
CUDA.CG.barrier_arrive
— Functionbarrier_arrive(group)
Arrive on the barrier, returns a token that needs to be passed into barrier_wait
.
CUDA.CG.barrier_wait
— Functionbarrier_wait(group, token)
Wait on the barrier, takes arrival token returned from barrier_arrive
.
CUDA.CG.wait
— Functionwait(group)
Make all threads in this group wait for all previously submitted memcpy_async
operations to complete.
CUDA.CG.wait_prior
— Functionwait_prior(group, stage)
Make all threads in this group wait for all but stage
previously submitted memcpy_async
operations to complete.
CUDA.CG.memcpy_async
— Functionmemcpy_async(group, dst, src, bytes)
Perform a group-wide collective memory copy from src
to dst
of bytes
bytes. This operation may be performed asynchronously, so you should wait
or wait_prior
before using the data. It is only supported by thread blocks and coalesced groups.
For this operation to be performed asynchronously, the following conditions must be met:
CUDA.align
.Many mathematical functions are provided by the libdevice
library, and are wrapped by CUDA.jl. These functions are used to implement well-known functions from the Julia standard library and packages like SpecialFunctions.jl, e.g., calling the cos
function will automatically use __nv_cos
from libdevice
if possible.
Some functions do not have a counterpart in the Julia ecosystem, those have to be called directly. For example, to call __nv_logb
or __nv_logbf
you use CUDA.logb
in a kernel.
For a list of available functions, look at src/device/intrinsics/math.jl
.
Warp matrix multiply-accumulate (WMMA) is a CUDA API to access Tensor Cores, a new hardware feature in Volta GPUs to perform mixed precision matrix multiply-accumulate operations. The interface is split in two levels, both available in the WMMA submodule: low level wrappers around the LLVM intrinsics, and a higher-level API similar to that of CUDA C.
CUDA.WMMA.llvm_wmma_load
— FunctionWMMA.llvm_wmma_load_{matrix}_{layout}_{shape}_{addr_space}_stride_{elem_type}(src_addr, stride)
Wrapper around the LLVM intrinsic @llvm.nvvm.wmma.load.{matrix}.sync.{layout}.{shape}.{addr_space}.stride.{elem_type}
.
Arguments
src_addr
: The memory address to load from.stride
: The leading dimension of the matrix, in numbers of elements.Placeholders
{matrix}
: The matrix to load. Can be a
, b
or c
.{layout}
: The storage layout for the matrix. Can be row
or col
, for row major (C style) or column major (Julia style), respectively.{shape}
: The overall shape of the MAC operation. Valid values are m16n16k16
, m32n8k16
, and m8n32k16
.{addr_space}
: The address space of src_addr
. Can be empty (generic addressing), shared
or global
.{elem_type}
: The type of each element in the matrix. For a
and b
matrices, valid values are u8
(byte unsigned integer), s8
(byte signed integer), and f16
(half precision floating point). For c
and d
matrices, valid values are s32
(32-bit signed integer), f16
(half precision floating point), and f32
(full precision floating point).CUDA.WMMA.llvm_wmma_mma
— FunctionWMMA.llvm_wmma_mma_{a_layout}_{b_layout}_{shape}_{d_elem_type}_{c_elem_type}(a, b, c) or
+WMMA.llvm_wmma_mma_{a_layout}_{b_layout}_{shape}_{a_elem_type}(a, b, c)
For floating point operations: wrapper around the LLVM intrinsic @llvm.nvvm.wmma.mma.sync.{a_layout}.{b_layout}.{shape}.{d_elem_type}.{c_elem_type}
For all other operations: wrapper around the LLVM intrinsic @llvm.nvvm.wmma.mma.sync.{a_layout}.{b_layout}.{shape}.{a_elem_type}
Arguments
a
: The WMMA fragment corresponding to the matrix $A$.b
: The WMMA fragment corresponding to the matrix $B$.c
: The WMMA fragment corresponding to the matrix $C$.Placeholders
{a_layout}
: The storage layout for matrix $A$. Can be row
or col
, for row major (C style) or column major (Julia style), respectively. Note that this must match the layout used in the load operation.{b_layout}
: The storage layout for matrix $B$. Can be row
or col
, for row major (C style) or column major (Julia style), respectively. Note that this must match the layout used in the load operation.{shape}
: The overall shape of the MAC operation. Valid values are m16n16k16
, m32n8k16
, and m8n32k16
.{a_elem_type}
: The type of each element in the $A$ matrix. Valid values are u8
(byte unsigned integer), s8
(byte signed integer), and f16
(half precision floating point).{d_elem_type}
: The type of each element in the resultant $D$ matrix. Valid values are s32
(32-bit signed integer), f16
(half precision floating point), and f32
(full precision floating point).{c_elem_type}
: The type of each element in the $C$ matrix. Valid values are s32
(32-bit signed integer), f16
(half precision floating point), and f32
(full precision floating point).Remember that the shape, type and layout of all operations (be it MMA, load or store) MUST match. Otherwise, the behaviour is undefined!
CUDA.WMMA.llvm_wmma_store
— FunctionWMMA.llvm_wmma_store_d_{layout}_{shape}_{addr_space}_stride_{elem_type}(dst_addr, data, stride)
Wrapper around the LLVM intrinsic @llvm.nvvm.wmma.store.d.sync.{layout}.{shape}.{addr_space}.stride.{elem_type}
.
Arguments
dst_addr
: The memory address to store to.data
: The $D$ fragment to store.stride
: The leading dimension of the matrix, in numbers of elements.Placeholders
{layout}
: The storage layout for the matrix. Can be row
or col
, for row major (C style) or column major (Julia style), respectively.{shape}
: The overall shape of the MAC operation. Valid values are m16n16k16
, m32n8k16
, and m8n32k16
.{addr_space}
: The address space of src_addr
. Can be empty (generic addressing), shared
or global
.{elem_type}
: The type of each element in the matrix. For a
and b
matrices, valid values are u8
(byte unsigned integer), s8
(byte signed integer), and f16
(half precision floating point). For c
and d
matrices, valid values are s32
(32-bit signed integer), f16
(half precision floating point), and f32
(full precision floating point).CUDA.WMMA.RowMajor
— TypeWMMA.RowMajor
Type that represents a matrix stored in row major (C style) order.
CUDA.WMMA.ColMajor
— TypeWMMA.ColMajor
Type that represents a matrix stored in column major (Julia style) order.
CUDA.WMMA.Unspecified
— TypeWMMA.Unspecified
Type that represents a matrix stored in an unspecified order.
This storage format is not valid for all WMMA operations!
CUDA.WMMA.FragmentLayout
— TypeWMMA.FragmentLayout
Abstract type that specifies the storage layout of a matrix.
Possible values are WMMA.RowMajor
, WMMA.ColMajor
and WMMA.Unspecified
.
CUDA.WMMA.Fragment
— TypeWMMA.Fragment
Type that represents per-thread intermediate results of WMMA operations.
You can access individual elements using the x
member or []
operator, but beware that the exact ordering of elements is unspecified.
CUDA.WMMA.Config
— TypeWMMA.Config{M, N, K, d_type}
Type that contains all information for WMMA operations that cannot be inferred from the argument's types.
WMMA instructions calculate the matrix multiply-accumulate operation $D = A \cdot B + C$, where $A$ is a $M \times K$ matrix, $B$ a $K \times N$ matrix, and $C$ and $D$ are $M \times N$ matrices.
d_type
refers to the type of the elements of matrix $D$, and can be either Float16
or Float32
.
All WMMA operations take a Config
as their final argument.
Examples
julia> config = WMMA.Config{16, 16, 16, Float32}
+CUDA.WMMA.Config{16, 16, 16, Float32}
CUDA.WMMA.load_a
— FunctionWMMA.load_a(addr, stride, layout, config)
+WMMA.load_b(addr, stride, layout, config)
+WMMA.load_c(addr, stride, layout, config)
Load the matrix a
, b
or c
from the memory location indicated by addr
, and return the resulting WMMA.Fragment
.
Arguments
addr
: The address to load the matrix from.stride
: The leading dimension of the matrix pointed to by addr
, specified in number of elements.layout
: The storage layout of the matrix. Possible values are WMMA.RowMajor
and WMMA.ColMajor
.config
: The WMMA configuration that should be used for loading this matrix. See WMMA.Config
.See also: WMMA.Fragment
, WMMA.FragmentLayout
, WMMA.Config
All threads in a warp MUST execute the load operation in lockstep, and have to use exactly the same arguments. Failure to do so will result in undefined behaviour.
WMMA.load_b
and WMMA.load_c
have the same signature.
CUDA.WMMA.mma
— FunctionWMMA.mma(a, b, c, conf)
Perform the matrix multiply-accumulate operation $D = A \cdot B + C$.
Arguments
a
: The WMMA.Fragment
corresponding to the matrix $A$.b
: The WMMA.Fragment
corresponding to the matrix $B$.c
: The WMMA.Fragment
corresponding to the matrix $C$.conf
: The WMMA.Config
that should be used in this WMMA operation.All threads in a warp MUST execute the mma
operation in lockstep, and have to use exactly the same arguments. Failure to do so will result in undefined behaviour.
CUDA.WMMA.store_d
— FunctionWMMA.store_d(addr, d, stride, layout, config)
Store the result matrix d
to the memory location indicated by addr
.
Arguments
addr
: The address to store the matrix to.d
: The WMMA.Fragment
corresponding to the d
matrix.stride
: The leading dimension of the matrix pointed to by addr
, specified in number of elements.layout
: The storage layout of the matrix. Possible values are WMMA.RowMajor
and WMMA.ColMajor
.config
: The WMMA configuration that should be used for storing this matrix. See WMMA.Config
.See also: WMMA.Fragment
, WMMA.FragmentLayout
, WMMA.Config
All threads in a warp MUST execute the store
operation in lockstep, and have to use exactly the same arguments. Failure to do so will result in undefined behaviour.
CUDA.WMMA.fill_c
— FunctionWMMA.fill_c(value, config)
Return a WMMA.Fragment
filled with the value value
.
This operation is useful if you want to implement a matrix multiplication (and thus want to set $C = O$).
Arguments
value
: The value used to fill the fragment. Can be a Float16
or Float32
.config
: The WMMA configuration that should be used for this WMMA operation. See WMMA.Config
.CUDA.align
— TypeCUDA.align{N}(obj)
Construct an aligned object, providing alignment information to APIs that require it.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
+ ${display_result} +
+Even if your kernel executes, it may be computing the wrong values, or even error at run time. To debug these issues, both CUDA.jl and the CUDA toolkit provide several utilities. These are generally low-level, since we generally cannot use the full extend of the Julia programming language and its tools within GPU kernels.
The easiest, and often reasonably effective way to debug GPU code is to visualize intermediary computations using output functions. CUDA.jl provides several macros that facilitate this style of debugging:
@cushow
(like @show
): to visualize an expression, its result, and return that value. This makes it easy to wrap expressions without disturbing their execution.@cuprintln
(like println
): to print text and values. This macro does support string interpolation, but the types it can print are restricted to C primitives.The @cuassert
macro (like @assert
) can also be useful to find issues and abort execution.
If you run into run-time exceptions, stack trace information will by default be very limited. For example, given the following out-of-bounds access:
julia> function kernel(a)
+ a[threadIdx().x] = 0
+ return
+ end
+kernel (generic function with 1 method)
+
+julia> @cuda threads=2 kernel(CuArray([1]))
If we execute this code, we'll get a very short error message:
ERROR: a exception was thrown during kernel execution.
+Run Julia on debug level 2 for device stack traces.
As the message suggests, we can have CUDA.jl emit more rich stack trace information by setting Julia's debug level to 2 or higher by passing -g2
to the julia
invocation:
ERROR: a exception was thrown during kernel execution.
+Stacktrace:
+ [1] throw_boundserror at abstractarray.jl:541
+ [2] checkbounds at abstractarray.jl:506
+ [3] arrayset at /home/tim/Julia/pkg/CUDA/src/device/array.jl:84
+ [4] setindex! at /home/tim/Julia/pkg/CUDA/src/device/array.jl:101
+ [5] kernel at REPL[4]:2
Note that these messages are embedded in the module (CUDA does not support stack unwinding), and thus bloat its size. To avoid any overhead, you can disable these messages by setting the debug level to 0 (passing -g0
to julia
). This disabled any device-side message, but retains the host-side detection:
julia> @cuda threads=2 kernel(CuArray([1]))
+# no device-side error message!
+
+julia> synchronize()
+ERROR: KernelException: exception thrown during kernel execution
Setting the debug level does not only enrich stack traces, it also changes the debug info emitted in the CUDA module. On debug level 1, which is the default setting if unspecified, CUDA.jl emits line number information corresponding to nvcc -lineinfo
. This information does not hurt performance, and is used by a variety of tools to improve the debugging experience.
To emit actual debug info as nvcc -G
does, you need to start Julia on debug level 2 by passing the flag -g2
. Support for emitting PTX-compatible debug info is a recent addition to the NVPTX LLVM back-end, so it's possible this information is incorrect or otherwise affects compilation.
Due to bugs in ptxas
, you need CUDA 11.5 or higher for debug info support.
To disable all debug info emission, start Julia with the flag -g0
.
compute-sanitizer
To debug kernel issues like memory errors or race conditions, you can use CUDA's compute-sanitizer
tool. Refer to the manual for more information.
To use compute-sanitizer
, you need to install the CUDA_SDK_jll
package in your environment first.
To spawn a new Julia session under compute-sanitizer
:
julia> using CUDA_SDK_jll
+
+# Get location of compute_sanitizer executable
+julia> compute_sanitizer = joinpath(CUDA_SDK_jll.artifact_dir, "cuda/compute-sanitizer/compute-sanitizer")
+.julia/artifacts/feb6b469b6047f344fec54df2619d65f6b704bdb/cuda/compute-sanitizer/compute-sanitizer
+
+# Recommended options for use with Julia and CUDA.jl
+julia> options = ["--launch-timeout=0", "--target-processes=all", "--report-api-errors=no"]
+3-element Vector{String}:
+ "--launch-timeout=0"
+ "--target-processes=all"
+ "--report-api-errors=no"
+
+# Run the executable with Julia
+julia> run(`$compute_sanitizer $options $(Base.julia_cmd())`)
+========= COMPUTE-SANITIZER
+julia> using CUDA
+
+julia> CuArray([1]) .+ 1
+1-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 2
+
+julia> exit()
+========= ERROR SUMMARY: 0 errors
+Process(`.julia/artifacts/feb6b469b6047f344fec54df2619d65f6b704bdb/cuda/compute-sanitizer/compute-sanitizer --launch-timeout=0 --target-processes=all --report-api-errors=no julia`, ProcessExited(0))
By default, compute-sanitizer
launches the memcheck
tool, which is great for dealing with memory issues. Other tools can be selected with the --tool
argument, e.g., to find thread synchronization hazards use --tool synccheck
, racecheck
can be used to find shared memory data races, and initcheck
is useful for spotting uses of uninitialized device memory.
cuda-gdb
To debug Julia code, you can use the CUDA debugger cuda-gdb
. When using this tool, it is recommended to enable Julia debug mode 2 so that debug information is emitted. Do note that the DWARF info emitted by Julia is currently insufficient to e.g. inspect variables, so the debug experience will not be pleasant.
If you encounter the CUDBG_ERROR_UNINITIALIZED
error, ensure all your devices are supported by cuda-gdb
(e.g., Kepler-era devices aren't). If some aren't, re-start Julia with CUDA_VISIBLE_DEVICES
set to ignore that device.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
When arrays operations are not flexible enough, you can write your own GPU kernels in Julia. CUDA.jl aims to expose the full power of the CUDA programming model, i.e., at the same level of abstraction as CUDA C/C++, albeit with some Julia-specific improvements.
As a result, writing kernels in Julia is very similar to writing kernels in CUDA C/C++. It should be possible to learn CUDA programming from existing CUDA C/C++ resources, and apply that knowledge to programming in Julia using CUDA.jl. Nontheless, this section will give a brief overview of the most important concepts and their syntax.
Kernels are written as ordinary Julia functions, returning nothing
:
function my_kernel()
+ return
+end
To launch this kernel, use the @cuda
macro:
julia> @cuda my_kernel()
This automatically (re)compiles the my_kernel
function and launches it on the current GPU (selected by calling device!
).
By passing the launch=false
keyword argument to @cuda
, it is possible to obtain a callable object representing the compiled kernel. This can be useful for reflection and introspection purposes:
julia> k = @cuda launch=false my_kernel()
+CUDA.HostKernel for my_kernel()
+
+julia> CUDA.registers(k)
+4
+
+julia> k()
GPU kernels cannot return values, and should always return
or return nothing
on all code paths. To communicate values from a kernel, you can use a CuArray
:
function my_kernel(a)
+ a[1] = 42
+ return
+end
julia> a = CuArray{Int}(undef, 1);
+
+julia> @cuda my_kernel(a);
+
+julia> a
+1-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 42
Simply using @cuda
only launches a single thread, which is not very useful. To launch more threads, use the threads
and blocks
keyword arguments to @cuda
, while using indexing intrinsics in the kernel to differentiate the computation for each thread:
julia> function my_kernel(a)
+ i = threadIdx().x
+ a[i] = 42
+ return
+ end
+
+julia> a = CuArray{Int}(undef, 5);
+
+julia> @cuda threads=length(a) my_kernel(a);
+
+julia> a
+5-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 42
+ 42
+ 42
+ 42
+ 42
As shown above, the threadIdx
etc. values from CUDA C are available as functions returning a NamedTuple
with x
, y
, and z
fields. The intrinsics return 1-based indices.
To synchronize threads in a block, use the sync_threads()
function. More advanced variants that take a predicate are also available:
sync_threads_count(pred)
: returns the number of threads for which pred
was truesync_threads_and(pred)
: returns true
if pred
was true for all threadssync_threads_or(pred)
: returns true
if pred
was true for any threadTo maintain multiple thread synchronization barriers, use the barrier_sync
function, which takes an integer argument to identify the barrier.
To synchronize lanes in a warp, use the sync_warp()
function. This function takes a mask to select which lanes to participate (this defaults to FULL_MASK
).
If only a memory barrier is required, and not an execution barrier, use fence intrinsics:
threadfence_block
: ensure memory ordering for all threads in the blockthreadfence
: the same, but for all threads on the devicethreadfence_system
: the same, but including host threads and threads on peer devicesAlthough the CuArray
type is the main array type used in CUDA.jl to represent GPU arrays and invoke operations on the device, it is a type that's only meant to be used from the host. For example, certain operations will call into the CUBLAS library, which is a library whose entrypoints are meant to be invoked from the CPU.
When passing a CuArray
to a kernel, it will be converted to a CuDeviceArray
object instead, representing the same memory but implemented with GPU-compatible operations. The API surface of this type is very limited, i.e., it only supports indexing and assignment, and some basic operations like view
, reinterpret
, reshape
, etc. Implementing higher level operations like map
would be a performance trap, as they would not make use of the GPU's parallelism, but execute slowly on a single GPU thread.
To communicate between threads, device arrays that are backed by shared memory can be allocated using the CuStaticSharedArray
function:
julia> function reverse_kernel(a::CuDeviceArray{T}) where T
+ i = threadIdx().x
+ b = CuStaticSharedArray(T, 2)
+ b[2-i+1] = a[i]
+ sync_threads()
+ a[i] = b[i]
+ return
+ end
+
+julia> a = cu([1,2])
+2-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+
+julia> @cuda threads=2 reverse_kernel(a)
+
+julia> a
+2-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 2
+ 1
When the amount of shared memory isn't known beforehand, and you don't want to recompile the kernel for each size, you can use the CuDynamicSharedArray
type instead. This requires you to pass the size of the shared memory (in bytes) as an argument to the kernel:
julia> function reverse_kernel(a::CuDeviceArray{T}) where T
+ i = threadIdx().x
+ b = CuDynamicSharedArray(T, length(a))
+ b[length(a)-i+1] = a[i]
+ sync_threads()
+ a[i] = b[i]
+ return
+ end
+
+julia> a = cu([1,2,3])
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+
+julia> @cuda threads=length(a) shmem=sizeof(a) reverse_kernel(a)
+
+julia> a
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 3
+ 2
+ 1
When needing multiple arrays of dynamic shared memory, pass an offset
parameter to the subsequent CuDynamicSharedArray
constructors indicating the offset in bytes from the start of the shared memory. The shmem
keyword to @cuda
should be the total amount of shared memory used by all arrays.
By default, indexing a CuDeviceArray
will perform bounds checking, and throw an error when the index is out of bounds. This can be a costly operation, so make sure to use @inbounds
when you know the index is in bounds.
CUDA.jl kernels do not yet integrate with Julia's standard input/output, but we provide some basic functions to print to the standard output from a kernel:
@cuprintf
: print a formatted string to standard output@cuprint
and @cuprintln
: print a string and any values to standard output@cushow
: print the name and value of an objectThe @cuprintf
macro does not support all formatting options; refer to the NVIDIA documentation on printf
for more details. It is often more convenient to use @cuprintln
and rely on CUDA.jl to convert any values to their appropriate string representation:
julia> @cuda threads=2 (()->(@cuprintln("Hello, I'm thread $(threadIdx().x)!"); return))()
+Hello, I'm thread 1!
+Hello, I'm thread 2!
To simply show a value, which can be useful during debugging, use @cushow
:
julia> @cuda threads=2 (()->(@cushow threadIdx().x; return))()
+(threadIdx()).x = 1
+(threadIdx()).x = 2
Note that these aren't full-blown implementations, and only support a very limited number of types. As such, they should only be used for debugging purposes.
The rand
and randn
functions are available for use in kernels, and will return a random number sampled from a special GPU-compatible random number generator:
julia> @cuda (()->(@cushow rand(); return))()
+rand() = 0.191897
Although the API is very similar to the random number generators used on the CPU, there are a few differences and considerations that stem from the design of a parallel RNG:
Random.seed!
, however, the RNG uses warp-shared state, so at least one thread per warp should seed, and all seeds within a warp should be identicalRandom.seed!
; refer to CUDA.jl's host-side RNG for an exampleCUDA.jl provides atomic operations at two levels of abstraction:
atomic_
functions mapping directly on hardware instructionsCUDA.@atomic
expressions for convenient element-wise operationsThe former is the safest way to use atomic operations, as it is stable and will not change behavior in the future. The interface is restrictive though, only supporting what the hardware provides, and requiring matching input types. The CUDA.@atomic
API is much more user friendly, but will disappear at some point when it integrates with the @atomic
macro in Julia Base.
The low-level atomic in trinsics take pointer inputs, which can be obtained from calling the pointer
function on a CuArray
:
julia> function atomic_kernel(a)
+ CUDA.atomic_add!(pointer(a), Int32(1))
+ return
+ end
+
+julia> a = cu(Int32[1])
+1-element CuArray{Int32, 1, CUDA.DeviceMemory}:
+ 1
+
+julia> @cuda atomic_kernel(a)
+
+julia> a
+1-element CuArray{Int32, 1, CUDA.DeviceMemory}:
+ 2
Supported atomic operations are:
add
, sub
, and
, or
, xor
, min
, max
, xchg
inc
, dec
cas
Refer to the documentation of these intrinsics for more information on the type support, and hardware requirements.
For more convenient atomic operations on arrays, CUDA.jl provides the CUDA.@atomic
macro which can be used with expressions that assign array elements:
julia> function atomic_kernel(a)
+ CUDA.@atomic a[1] += 1
+ return
+ end
+
+julia> a = cu(Int32[1])
+1-element CuArray{Int32, 1, CUDA.DeviceMemory}:
+ 1
+
+julia> @cuda atomic_kernel(a)
+
+julia> a
+1-element CuArray{Int32, 1, CUDA.DeviceMemory}:
+ 2
This macro is much more lenient, automatically converting inputs to the appropriate type, and falling back to an atomic compare-and-swap loop for unsupported operations. It however may disappear once CUDA.jl integrates with the @atomic
macro in Julia Base.
Most of CUDA's warp intrinsics are available in CUDA.jl, under similar names. Their behavior is mostly identical as well, with the exception that they are 1-indexed, and that they support more types by automatically converting and splitting (to some extent) inputs:
laneid
, lanemask
, active_mask
, warpsize
shfl_sync
, shfl_up_sync
, shfl_down_sync
, shfl_xor_sync
vote_all_sync
, vote_any_sync
, vote_unisync
, vote_ballot_sync
Many of these intrinsics require a mask
argument, which is a bit mask indicating which lanes should participate in the operation. To default to all lanes, use the FULL_MASK
constant.
Where kernels are normally launched from the host, using dynamic parallelism it is also possible to launch kernels from within a kernel. This is useful for recursive algorithms, or for algorithms that otherwise need to dynamically spawn new work.
Device-side launches are also done using the @cuda
macro, but require setting the dynamic
keyword argument to true
:
julia> function outer()
+ @cuprint("Hello ")
+ @cuda dynamic=true inner()
+ return
+ end
+
+julia> function inner()
+ @cuprintln("World!")
+ return
+ end
+
+julia> @cuda outer()
+Hello World!
Within a kernel, only a very limited subset of the CUDA API is available:
device_synchronize
CuDeviceStream
constructor, unsafe_destroy!
destuctor; these streams can be passed to @cuda
using the stream
keyword argumentWith cooperative groups, it is possible to write parallel kernels that are not tied to a specific thread configuration, instead making it possible to more dynamically partition threads and communicate between groups of threads. This functionality is relative new in CUDA.jl, and does not yet support all aspects of the cooperative groups programming model.
Essentially, instead of manually computing a thread index and using that to differentiate computation, kernel functionality now queries a group it is part of, and can query the size, rank, etc of that group:
julia> function reverse_kernel(d::CuDeviceArray{T}) where {T}
+ block = CG.this_thread_block()
+
+ n = length(d)
+ t = CG.thread_rank(block)
+ tr = n-t+1
+
+ s = @inbounds CuDynamicSharedArray(T, n)
+ @inbounds s[t] = d[t]
+ CG.sync(block)
+ @inbounds d[t] = s[tr]
+
+ return
+ end
+
+julia> a = cu([1,2,3])
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+
+julia> @cuda threads=length(a) shmem=sizeof(a) reverse_kernel(a)
+
+julia> a
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 3
+ 2
+ 1
The following implicit groups are supported:
CG.this_thread_block()
CG.this_grid()
CG.coalesced_threads()
Support is currently lacking for the cluster and multi-grid implicit groups, as well as all explicit (tiled, partitioned) groups.
Thread blocks are supported by all devices, in all kernels. Grid groups (CG.this_grid()
) can be used to synchronize the entire grid, which is normally not possible, but requires additional care: kernels need to be launched cooperatively, using @cuda cooperative=true
, which is only supported on devices with compute capability 6.0 or higher. Also, cooperative kernels can only launch as many blocks as there are SMs on the device.
Every kind of thread group supports the following indexing operations:
thread_rank
: returns the rank of the current thread within the groupnum_threads
: returns the number of threads in the groupIn addition, some group kinds support additional indexing operations:
group_index
, thread_index
, dim_threads
block_rank
, num_blocks
, dim_blocks
, block_index
meta_group_rank
, meta_group_size
Refer to the docstrings of these functions for more details.
Group objects support the CG.sync
operation to synchronize threads within a group.
In addition, thread and grid groups support more fine-grained synchronization using barriers: CG.barrier_arrive
and CG.barrier_wait
: Calling barrier_arrive
returns a token that needs to be passed to barrier_wait
to synchronize.
Certain collective operations (i.e. operations that need to be performed by multiple threads) provide a more convenient API when using cooperative groups. For example, shuffle intrinsics normally require a thread mask, but this can be replaced by a group object:
function reverse_kernel(d)
+ cta = CG.this_thread_block()
+ I = CG.thread_rank(cta)
+
+ warp = CG.coalesced_threads()
+ i = CG.thread_rank(warp)
+ j = CG.num_threads(warp) - i + 1
+
+ d[I] = CG.shfl(warp, d[I], j)
+
+ return
+end
The following collective operations are supported:
shfl
, shfl_down
, shfl_up
vote_any
, vote_all
, vote_ballot
With thread blocks and coalesced groups, the CG.memcpy_async
function is available to perform asynchronous memory copies. Currently, only copies from device to shared memory are accelerated, and only on devices with compute capability 8.0 or higher. However, the implementation degrades gracefully and will fall back to a synchronizing copy:
julia> function memcpy_kernel(input::AbstractArray{T}, output::AbstractArray{T},
+ elements_per_copy) where {T}
+ tb = CG.this_thread_block()
+
+ local_smem = CuDynamicSharedArray(T, elements_per_copy)
+ bytes_per_copy = sizeof(local_smem)
+
+ i = 1
+ while i <= length(input)
+ # this copy can sometimes be accelerated
+ CG.memcpy_async(tb, pointer(local_smem), pointer(input, i), bytes_per_copy)
+ CG.wait(tb)
+
+ # do something with the data here
+
+ # this copy is always a simple element-wise operation
+ CG.memcpy_async(tb, pointer(output, i), pointer(local_smem), bytes_per_copy)
+ CG.wait(tb)
+
+ i += elements_per_copy
+ end
+ end
+
+julia> a = cu([1, 2, 3, 4]);
+julia> b = similar(a);
+julia> nb = 2;
+
+julia> @cuda shmem=sizeof(eltype(a))*nb memcpy_kernel(a, b, nb)
+
+julia> b
+4-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+ 4
The above example waits for the copy to complete before continuing, but it is also possible to have multiple copies in flight using the CG.wait_prior
function, which waits for all but the last N copies to complete.
Warp matrix multiply-accumulate (WMMA) is a cooperative operation to perform mixed precision matrix multiply-accumulate on the tensor core hardware of recent GPUs. The CUDA.jl interface is split in two levels, both available in the WMMA submodule: low level wrappers around the LLVM intrinsics, and a higher-level API similar to that of CUDA C.
The WMMA operations perform a matrix multiply-accumulate. More concretely, it calculates $D = A \cdot B + C$, where $A$ is a $M \times K$ matrix, $B$ is a $K \times N$ matrix, and $C$ and $D$ are $M \times N$ matrices.
However, not all values of $M$, $N$ and $K$ are allowed. The tuple $(M, N, K)$ is often called the "shape" of the multiply accumulate operation.
The multiply-accumulate consists of the following steps:
Note that WMMA is a warp-wide operation, which means that all threads in a warp must cooperate, and execute the WMMA operations in lockstep. Failure to do so will result in undefined behaviour.
Each thread in a warp will hold a part of the matrix in its registers. In WMMA parlance, this part is referred to as a "fragment". Note that the exact mapping between matrix elements and fragment is unspecified, and subject to change in future versions.
Finally, it is important to note that the resultant $D$ matrix can be used as a $C$ matrix for a subsequent multiply-accumulate. This is useful if one needs to calculate a sum of the form $\sum_{i=0}^{n} A_i B_i$, where $A_i$ and $B_i$ are matrices of the correct dimension.
The LLVM intrinsics are accessible by using the one-to-one Julia wrappers. The return type of each wrapper is the Julia type that corresponds closest to the return type of the LLVM intrinsic. For example, LLVM's [8 x <2 x half>]
becomes NTuple{8, NTuple{2, VecElement{Float16}}}
in Julia. In essence, these wrappers return the SSA values returned by the LLVM intrinsic. Currently, all intrinsics that are available in LLVM 6, PTX 6.0 and SM 70 are implemented.
These LLVM intrinsics are then lowered to the correct PTX instructions by the LLVM NVPTX backend. For more information about the PTX instructions, please refer to the PTX Instruction Set Architecture Manual.
The LLVM intrinsics are subdivided in three categories:
WMMA.llvm_wmma_load
WMMA.llvm_wmma_mma
WMMA.llvm_wmma_store
The main difference between the CUDA C-like API and the lower level wrappers, is that the former enforces several constraints when working with WMMA. For example, it ensures that the $A$ fragment argument to the MMA instruction was obtained by a load_a
call, and not by a load_b
or load_c
. Additionally, it makes sure that the data type and storage layout of the load/store operations and the MMA operation match.
The CUDA C-like API heavily uses Julia's dispatch mechanism. As such, the method names are much shorter than the LLVM intrinsic wrappers, as most information is baked into the type of the arguments rather than the method name.
Note that, in CUDA C++, the fragment is responsible for both the storage of intermediate results and the WMMA configuration. All CUDA C++ WMMA calls are function templates that take the resultant fragment as a by-reference argument. As a result, the type of this argument can be used during overload resolution to select the correct WMMA instruction to call.
In contrast, the API in Julia separates the WMMA storage (WMMA.Fragment
) and configuration (WMMA.Config
). Instead of taking the resultant fragment by reference, the Julia functions just return it. This makes the dataflow clearer, but it also means that the type of that fragment cannot be used for selection of the correct WMMA instruction. Thus, there is still a limited amount of information that cannot be inferred from the argument types, but must nonetheless match for all WMMA operations, such as the overall shape of the MMA. This is accomplished by a separate "WMMA configuration" (see WMMA.Config
) that you create once, and then give as an argument to all intrinsics.
WMMA.Fragment
WMMA.Config
WMMA.load_a
, WMMA.load_b
, WMMA.load_c
WMMA.fill_c
WMMA.mma
WMMA.store_d
Similar to the CUDA C++ WMMA API, WMMA.Fragment
s have an x
member that can be used to access individual elements. Note that, in contrast to the values returned by the LLVM intrinsics, the x
member is flattened. For example, while the Float16
variants of the load_a
instrinsics return NTuple{8, NTuple{2, VecElement{Float16}}}
, the x
member has type NTuple{16, Float16}
.
Typically, you will only need to access the x
member to perform elementwise operations. This can be more succinctly expressed using Julia's broadcast mechanism. For example, to double each element in a fragment, you can simply use:
frag = 2.0f0 .* frag
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
Benchmarking and profiling a GPU program is harder than doing the same for a program executing on the CPU. For one, GPU operations typically execute asynchronously, and thus require appropriate synchronization when measuring their execution time. Furthermore, because the program executes on a different processor, it is much harder to know what is currently executing. CUDA, and the Julia CUDA packages, provide several tools and APIs to remedy this.
To accurately measure execution time in the presence of asynchronously-executing GPU operations, CUDA.jl provides an @elapsed
macro that, much like Base.@elapsed
, measures the total execution time of a block of code on the GPU:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> Base.@elapsed sin.(a) # WRONG!
+0.008714211
+
+julia> CUDA.@elapsed sin.(a)
+0.051607586f0
This is a low-level utility, and measures time by submitting events to the GPU and measuring the time between them. As such, if the GPU was not idle in the first place, you may not get the expected result. The macro is mainly useful if your application needs to know about the time it took to complete certain GPU operations.
For more convenient time reporting, you can use the CUDA.@time
macro which mimics Base.@time
by printing execution times as well as memory allocation stats, while making sure the GPU is idle before starting the measurement, as well as waiting for all asynchronous operations to complete:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> CUDA.@time sin.(a);
+ 0.046063 seconds (96 CPU allocations: 3.750 KiB) (1 GPU allocation: 4.000 GiB, 14.33% gc time of which 99.89% spent allocating)
The CUDA.@time
macro is more user-friendly and is a generally more useful tool when measuring the end-to-end performance characteristics of a GPU application.
For robust measurements however, it is advised to use the BenchmarkTools.jl package which goes to great lengths to perform accurate measurements. Due to the asynchronous nature of GPUs, you need to ensure the GPU is synchronized at the end of every sample, e.g. by calling synchronize()
or, even better, wrapping your code in CUDA.@sync
:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> @benchmark CUDA.@sync sin.($a)
+BenchmarkTools.Trial:
+ memory estimate: 3.73 KiB
+ allocs estimate: 95
+ --------------
+ minimum time: 46.341 ms (0.00% GC)
+ median time: 133.302 ms (0.50% GC)
+ mean time: 130.087 ms (0.49% GC)
+ maximum time: 153.465 ms (0.43% GC)
+ --------------
+ samples: 39
+ evals/sample: 1
Note that the allocations as reported by BenchmarkTools are CPU allocations. For the GPU allocation behavior you need to consult CUDA.@time
.
For profiling large applications, simple timings are insufficient. Instead, we want a overview of how and when the GPU was active, to avoid times where the device was idle and/or find which kernels needs optimization.
Once again, we cannot use CPU utilities to profile GPU programs, as they will only paint a partial picture. Instead, CUDA.jl provides a CUDA.@profile
macro that separately reports the time spent on the CPU, and the time spent on the GPU:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> CUDA.@profile sin.(a)
+Profiler ran for 11.93 ms, capturing 8 events.
+
+Host-side activity: calling CUDA APIs took 437.26 µs (3.67% of the trace)
+┌──────────┬───────────┬───────┬───────────┬───────────┬───────────┬─────────────────┐
+│ Time (%) │ Time │ Calls │ Avg time │ Min time │ Max time │ Name │
+├──────────┼───────────┼───────┼───────────┼───────────┼───────────┼─────────────────┤
+│ 3.56% │ 424.15 µs │ 1 │ 424.15 µs │ 424.15 µs │ 424.15 µs │ cuLaunchKernel │
+│ 0.10% │ 11.92 µs │ 1 │ 11.92 µs │ 11.92 µs │ 11.92 µs │ cuMemAllocAsync │
+└──────────┴───────────┴───────┴───────────┴───────────┴───────────┴─────────────────┘
+
+Device-side activity: GPU was busy for 11.48 ms (96.20% of the trace)
+┌──────────┬──────────┬───────┬──────────┬──────────┬──────────┬───────────────────────
+│ Time (%) │ Time │ Calls │ Avg time │ Min time │ Max time │ Name ⋯
+├──────────┼──────────┼───────┼──────────┼──────────┼──────────┼───────────────────────
+│ 96.20% │ 11.48 ms │ 1 │ 11.48 ms │ 11.48 ms │ 11.48 ms │ _Z16broadcast_kernel ⋯
+└──────────┴──────────┴───────┴──────────┴──────────┴──────────┴───────────────────────
By default, CUDA.@profile
will provide a summary of host and device activities. If you prefer a chronological view of the events, you can set the trace
keyword argument:
julia> CUDA.@profile trace=true sin.(a)
+Profiler ran for 11.71 ms, capturing 8 events.
+
+Host-side activity: calling CUDA APIs took 217.68 µs (1.86% of the trace)
+┌────┬──────────┬───────────┬─────────────────┬──────────────────────────┐
+│ ID │ Start │ Time │ Name │ Details │
+├────┼──────────┼───────────┼─────────────────┼──────────────────────────┤
+│ 2 │ 7.39 µs │ 14.07 µs │ cuMemAllocAsync │ 4.000 GiB, device memory │
+│ 6 │ 29.56 µs │ 202.42 µs │ cuLaunchKernel │ - │
+└────┴──────────┴───────────┴─────────────────┴──────────────────────────┘
+
+Device-side activity: GPU was busy for 11.48 ms (98.01% of the trace)
+┌────┬──────────┬──────────┬─────────┬────────┬──────┬─────────────────────────────────
+│ ID │ Start │ Time │ Threads │ Blocks │ Regs │ Name ⋯
+├────┼──────────┼──────────┼─────────┼────────┼──────┼─────────────────────────────────
+│ 6 │ 229.6 µs │ 11.48 ms │ 768 │ 284 │ 34 │ _Z16broadcast_kernel15CuKernel ⋯
+└────┴──────────┴──────────┴─────────┴────────┴──────┴─────────────────────────────────
Here, every call is prefixed with an ID, which can be used to correlate host and device events. For example, here we can see that the host-side cuLaunchKernel
call with ID 6 corresponds to the device-side broadcast
kernel.
If you want more details, or a graphical representation, we recommend using external profilers. To inform those external tools which code needs to be profiled (e.g., to exclude warm-up iterations or other noninteresting elements) you can also use CUDA.@profile
to surround interesting code with:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> sin.(a); # warmup
+
+julia> CUDA.@profile sin.(a);
+[ Info: This Julia session is already being profiled; defaulting to the external profiler.
+
+julia>
Note that the external profiler is automatically detected, and makes CUDA.@profile
switch to a mode where it merely activates an external profiler and does not do perform any profiling itself. In case the detection does not work, this mode can be forcibly activated by passing external=true
to CUDA.@profile
.
NVIDIA provides two tools for profiling CUDA applications: NSight Systems and NSight Compute for respectively timeline profiling and more detailed kernel analysis. Both tools are well-integrated with the Julia GPU packages, and make it possible to iteratively profile without having to restart Julia.
Generally speaking, the first external profiler you should use is NSight Systems, as it will give you a high-level overview of your application's performance characteristics. After downloading and installing the tool (a version might have been installed alongside with the CUDA toolkit, but it is recommended to download and install the latest version from the NVIDIA website), you need to launch Julia from the command-line, wrapped by the nsys
utility from NSight Systems:
$ nsys launch julia
You can then execute whatever code you want in the REPL, including e.g. loading Revise so that you can modify your application as you go. When you call into code that is wrapped by CUDA.@profile
, the profiler will become active and generate a profile output file in the current folder:
julia> using CUDA
+
+julia> a = CUDA.rand(1024,1024,1024);
+
+julia> sin.(a);
+
+julia> CUDA.@profile sin.(a);
+start executed
+Processing events...
+Capturing symbol files...
+Saving intermediate "report.qdstrm" file to disk...
+
+Importing [===============================================================100%]
+Saved report file to "report.qdrep"
+stop executed
Even with a warm-up iteration, the first kernel or API call might seem to take significantly longer in the profiler. If you are analyzing short executions, instead of whole applications, repeat the operation twice (optionally separated by a call to synchronize()
or wrapping in CUDA.@sync
)
You can open the resulting .qdrep
file with nsight-sys
:
If NSight Systems does not capture any kernel launch, even though you have used CUDA.@profile
, try starting nsys
with --trace cuda
.
If you want details on the execution properties of a single kernel, or inspect API interactions in detail, Nsight Compute is the tool for you. It is again possible to use this profiler with an interactive session of Julia, and debug or profile only those sections of your application that are marked with CUDA.@profile
.
First, ensure that all (CUDA) packages that are involved in your application have been precompiled. Otherwise, you'll end up profiling the precompilation process, instead of the process where the actual work happens.
Then, launch Julia under the Nsight Compute CLI tool as follows:
$ ncu --mode=launch julia
You will get an interactive REPL, where you can execute whatever code you want:
julia> using CUDA
+# Julia hangs!
As soon as you use CUDA.jl, your Julia process will hang. This is expected, as the tool breaks upon the very first call to the CUDA API, at which point you are expected to launch the Nsight Compute GUI utility, select Interactive Profile
under Activity
, and attach to the running session by selecting it in the list in the Attach
pane:
Note that this even works with remote systems, i.e., you can have NSight Compute connect over ssh to a remote system where you run Julia under ncu
.
Once you've successfully attached to a Julia process, you will see that the tool has stopped execution on the call to cuInit
. Now check Profile > Auto Profile
to make Nsight Compute gather statistics on our kernels, uncheck Debug > Break On API Error
to avoid halting the process when innocuous errors happen, and click Debug > Resume
to resume your application.
After doing so, our CLI session comes to life again, and we can execute the rest of our script:
julia> a = CUDA.rand(1024,1024,1024);
+
+julia> sin.(a);
+
+julia> CUDA.@profile sin.(a);
Once that's finished, the Nsight Compute GUI window will have plenty details on our kernel:
By default, this only collects a basic set of metrics. If you need more information on a specific kernel, select detailed
or full
in the Metric Selection
pane and re-run your kernels. Note that collecting more metrics is also more expensive, sometimes even requiring multiple executions of your kernel. As such, it is recommended to only collect basic metrics by default, and only detailed or full metrics for kernels of interest.
At any point in time, you can also pause your application from the debug menu, and inspect the API calls that have been made:
If you're running into issues, make sure you're using the same version of NSight Compute on the host and the device, and make sure it's the latest version available. You do not need administrative permissions to install NSight Compute, the runfile
downloaded from the NVIDIA home page can be executed as a regular user.
File not found
When profiling a remote application, NSight Compute will not be able to find the sources of kernels, and instead show File not found
errors in the Source view. Although it is possible to point NSight Compute to a local version of the remote file, it is recommended to enable "Auto-Resolve Remote Source File" in the global Profile preferences (Tools menu
Preferences). With that option set to "Yes", clicking the "Resolve" button will
automatically download and use the remote version of the requested source file.
Could not load library "libpcre2-8
This is caused by an incompatibility between Julia and NSight Compute, and should be fixed in the latest versions of NSight Compute. If it's not possible to upgrade, the following workaround may help:
LD_LIBRARY_PATH=$(/path/to/julia -e 'println(joinpath(Sys.BINDIR, Base.LIBDIR, "julia"))') ncu --mode=launch /path/to/julia
Make sure that the port that is used by NSight Compute (49152 by default) is accessible via ssh. To verify this, you can also try forwarding the port manually:
ssh user@host.com -L 49152:localhost:49152
Then, in the "Connect to process" window of NSight Compute, add a connection to localhost
instead of the remote host.
If SSH complains with Address already in use
, that means the port is already in use. If you're using VSCode, try closing all instances as VSCode might automatically forward the port when launching NSight Compute in a terminal within VSCode.
In some versions of NSight Compute, you might have to start Julia without the --project
option and switch the environment from inside Julia.
Make sure that everything is precompiled before starting Julia with NSight Compute, otherwise you end up profiling the precompilation process instead of your actual application.
Alternatively, disable auto profiling, resume, wait until the precompilation is finished, and then enable auto profiling again.
Scroll down in the "API Stream" tab and look for errors in the "Details" column. If it says "The user does not have permission to access NVIDIA GPU Performance Counters on the target device", add this config:
# cat /etc/modprobe.d/nvprof.conf
+options nvidia NVreg_RestrictProfilingToAdminUsers=0
The nvidia.ko
kernel module needs to be reloaded after changing this configuration, and your system may require regenerating the initramfs or even a reboot. Refer to your distribution's documentation for details.
Make sure Break On API Error
is disabled in the Debug
menu, as CUDA.jl purposefully triggers some API errors as part of its normal operation.
If you want to put additional information in the profile, e.g. phases of your application, or expensive CPU operations, you can use the NVTX library via the NVTX.jl package:
using CUDA, NVTX
+
+NVTX.@mark "reached Y"
+
+NVTX.@range "doing X" begin
+ ...
+end
+
+NVTX.@annotate function foo()
+ ...
+end
For more details, refer to the documentation of the NVTX.jl package.
Some tools, like NSight Systems Compute, also make it possible to do source-level profiling. CUDA.jl will by default emit the necessary source line information, which you can disable by launching Julia with -g0
. Conversely, launching with -g2
will emit additional debug information, which can be useful in combination with tools like cuda-gdb
, but might hurt performance or code size.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This section deals with common errors you might run into while writing GPU code, preventing the code to compile.
Not all of Julia is supported by CUDA.jl. Several commonly-used features, like strings or exceptions, will not compile to GPU code, because of their interactions with the CPU-only runtime library.
For example, say we define and try to execute the following kernel:
julia> function kernel(a)
+ @inbounds a[threadId().x] = 0
+ return
+ end
+
+julia> @cuda kernel(CuArray([1]))
+ERROR: InvalidIRError: compiling kernel kernel(CuDeviceArray{Int64,1,1}) resulted in invalid LLVM IR
+Reason: unsupported dynamic function invocation (call to setindex!)
+Stacktrace:
+ [1] kernel at REPL[2]:2
+Reason: unsupported dynamic function invocation (call to getproperty)
+Stacktrace:
+ [1] kernel at REPL[2]:2
+Reason: unsupported use of an undefined name (use of 'threadId')
+Stacktrace:
+ [1] kernel at REPL[2]:2
CUDA.jl does its best to decode the unsupported IR and figure out where it came from. In this case, there's two so-called dynamic invocations, which happen when a function call cannot be statically resolved (often because the compiler could not fully infer the call, e.g., due to inaccurate or instable type information). These are a red herring, and the real cause is listed last: a typo in the use of the threadIdx
function! If we fix this, the IR error disappears and our kernel successfully compiles and executes.
Union{}
Where the previous section clearly pointed to the source of invalid IR, in other cases your function will return an error. This is encoded by the Julia compiler as a return value of type Union{}
:
julia> function kernel(a)
+ @inbounds a[threadIdx().x] = CUDA.sin(a[threadIdx().x])
+ return
+ end
+
+julia> @cuda kernel(CuArray([1]))
+ERROR: GPU compilation of kernel kernel(CuDeviceArray{Int64,1,1}) failed
+KernelError: kernel returns a value of type `Union{}`
Now we don't know where this error came from, and we will have to take a look ourselves at the generated code. This is easily done using the @device_code
introspection macros, which mimic their Base counterparts (e.g. @device_code_llvm
instead of @code_llvm
, etc).
To debug an error returned by a kernel, we should use @device_code_warntype
to inspect the Julia IR. Furthermore, this macro has an interactive
mode, which further facilitates inspecting this IR using Cthulhu.jl. First, install and import this package, and then try to execute the kernel again prefixed by @device_code_warntype interactive=true
:
julia> using Cthulhu
+
+julia> @device_code_warntype interactive=true @cuda kernel(CuArray([1]))
+Variables
+ #self#::Core.Compiler.Const(kernel, false)
+ a::CuDeviceArray{Int64,1,1}
+ val::Union{}
+
+Body::Union{}
+1 ─ %1 = CUDA.sin::Core.Compiler.Const(CUDA.sin, false)
+│ ...
+│ %14 = (...)::Int64
+└── goto #2
+2 ─ (%1)(%14)
+└── $(Expr(:unreachable))
+
+Select a call to descend into or ↩ to ascend.
+ • %17 = call CUDA.sin(::Int64)::Union{}
Both from the IR and the list of calls Cthulhu offers to inspect further, we can see that the call to CUDA.sin(::Int64)
results in an error: in the IR it is immediately followed by an unreachable
, while in the list of calls it is inferred to return Union{}
. Now we know where to look, it's easy to figure out what's wrong:
help?> CUDA.sin
+ # 2 methods for generic function "sin":
+ [1] sin(x::Float32) in CUDA at /home/tim/Julia/pkg/CUDA/src/device/intrinsics/math.jl:13
+ [2] sin(x::Float64) in CUDA at /home/tim/Julia/pkg/CUDA/src/device/intrinsics/math.jl:12
There's no method of CUDA.sin
that accepts an Int64, and thus the function was determined to unconditionally throw a method error. For now, we disallow these situations and refuse to compile, but in the spirit of dynamic languages we might change this behavior to just throw an error at run time.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This page is a compilation of frequently asked questions and answers.
Sometimes it happens that a breaking version of CUDA.jl or one of its dependencies is released. If any package you use isn't yet compatible with this release, this will block automatic upgrade of CUDA.jl. For example, with Flux.jl v0.11.1 we get CUDA.jl v1.3.3 despite there being a v2.x release:
pkg> add Flux
+ [587475ba] + Flux v0.11.1
+pkg> add CUDA
+ [052768ef] + CUDA v1.3.3
To examine which package is holding back CUDA.jl, you can "force" an upgrade by specifically requesting a newer version. The resolver will then complain, and explain why this upgrade isn't possible:
pkg> add CUDA.jl@2
+ Resolving package versions...
+ERROR: Unsatisfiable requirements detected for package Adapt [79e6a3ab]:
+ Adapt [79e6a3ab] log:
+ ├─possible versions are: [0.3.0-0.3.1, 0.4.0-0.4.2, 1.0.0-1.0.1, 1.1.0, 2.0.0-2.0.2, 2.1.0, 2.2.0, 2.3.0] or uninstalled
+ ├─restricted by compatibility requirements with CUDA [052768ef] to versions: [2.2.0, 2.3.0]
+ │ └─CUDA [052768ef] log:
+ │ ├─possible versions are: [0.1.0, 1.0.0-1.0.2, 1.1.0, 1.2.0-1.2.1, 1.3.0-1.3.3, 2.0.0-2.0.2] or uninstalled
+ │ └─restricted to versions 2 by an explicit requirement, leaving only versions 2.0.0-2.0.2
+ └─restricted by compatibility requirements with Flux [587475ba] to versions: [0.3.0-0.3.1, 0.4.0-0.4.2, 1.0.0-1.0.1, 1.1.0] — no versions left
+ └─Flux [587475ba] log:
+ ├─possible versions are: [0.4.1, 0.5.0-0.5.4, 0.6.0-0.6.10, 0.7.0-0.7.3, 0.8.0-0.8.3, 0.9.0, 0.10.0-0.10.4, 0.11.0-0.11.1] or uninstalled
+ ├─restricted to versions * by an explicit requirement, leaving only versions [0.4.1, 0.5.0-0.5.4, 0.6.0-0.6.10, 0.7.0-0.7.3, 0.8.0-0.8.3, 0.9.0, 0.10.0-0.10.4, 0.11.0-0.11.1]
+ └─restricted by compatibility requirements with CUDA [052768ef] to versions: [0.4.1, 0.5.0-0.5.4, 0.6.0-0.6.10, 0.7.0-0.7.3, 0.8.0-0.8.3, 0.9.0, 0.10.0-0.10.4] or uninstalled, leaving only versions: [0.4.1, 0.5.0-0.5.4, 0.6.0-0.6.10, 0.7.0-0.7.3, 0.8.0-0.8.3, 0.9.0, 0.10.0-0.10.4]
+ └─CUDA [052768ef] log: see above
A common source of these incompatibilities is having both CUDA.jl and the older CUDAnative.jl/CuArrays.jl/CUDAdrv.jl stack installed: These are incompatible, and cannot coexist. You can inspect in the Pkg REPL which exact packages you have installed using the status --manifest
option.
If a certain API isn't wrapped with some high-level functionality, you can always use the underlying C APIs which are always available as unexported methods. For example, you can access the CUDA driver library as cu
prefixed, unexported functions like CUDA.cuDriverGetVersion
. Similarly, vendor libraries like CUBLAS are available through their exported submodule handles, e.g., CUBLAS.cublasGetVersion_v2
.
Any help on designing or implementing high-level wrappers for this low-level functionality is greatly appreciated, so please consider contributing your uses of these APIs on the respective repositories.
If you're working on a cluster, precompilation may stall if you have not requested sufficient memory. You may also wish to make sure you have enough disk space prior to installing CUDA.jl.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
The CUDA.jl package is the main entrypoint for programming NVIDIA GPUs in Julia. The package makes it possible to do so at various abstraction levels, from easy-to-use arrays down to hand-written kernels using low-level CUDA APIs.
If you have any questions, please feel free to use the #gpu
channel on the Julia slack, or the GPU domain of the Julia Discourse.
For information on recent or upcoming changes, consult the NEWS.md
document in the CUDA.jl repository.
The Julia CUDA stack only requires a working NVIDIA driver; you don't need to install the entire CUDA toolkit, as it will automatically be downloaded when you first use the package:
# install the package
+using Pkg
+Pkg.add("CUDA")
+
+# smoke test (this will download the CUDA toolkit)
+using CUDA
+CUDA.versioninfo()
If you want to ensure everything works as expected, you can execute the test suite. Note that this test suite is fairly exhaustive, taking around an hour to complete when using a single thread (multiple processes are used automatically based on the number of threads Julia is started with), and requiring significant amounts of CPU and GPU memory.
using Pkg
+Pkg.test("CUDA")
+
+# the test suite takes command-line options that allow customization; pass --help for details:
+#Pkg.test("CUDA"; test_args=`--help`)
For more details on the installation process, consult the Installation section. To understand the toolchain in more detail, have a look at the tutorials in this manual. It is highly recommended that new users start with the Introduction tutorial. For an overview of the available functionality, read the Usage section. The following resources may also be of interest:
The Julia CUDA stack has been a collaborative effort by many individuals. Significant contributions have been made by the following individuals:
Much of the software in this ecosystem was developed as part of academic research. If you would like to help support it, please star the repository as such metrics may help us secure funding in the future. If you use our software as part of your research, teaching, or other activities, we would be grateful if you could cite our work. The CITATION.bib file in the root of this repository lists the relevant papers.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
CUDA.jl is special in that developers may want to depend on the GPU toolchain even though users might not have a GPU. In this section, we describe two different usage scenarios and how to implement them. Key to remember is that CUDA.jl will always load, which means you need to manually check if the package is functional.
Because CUDA.jl always loads, even if the user doesn't have a GPU or CUDA, you should just depend on it like any other package (and not use, e.g., Requires.jl). This ensures that breaking changes to the GPU stack will be taken into account by the package resolver when installing your package.
If you unconditionally use the functionality from CUDA.jl, you will get a run-time error in the case the package failed to initialize. For example, on a system without CUDA:
julia> using CUDA
+julia> CUDA.driver_version()
+ERROR: UndefVarError: libcuda not defined
To avoid this, you should call CUDA.functional()
to inspect whether the package is functional and condition your use of GPU functionality on that. Let's illustrate with two scenarios, one where having a GPU is required, and one where it's optional.
If your application requires a GPU, and its functionality is not designed to work without CUDA, you should just import the necessary packages and inspect if they are functional:
using CUDA
+@assert CUDA.functional(true)
Passing true
as an argument makes CUDA.jl display why initialization might have failed.
If you are developing a package, you should take care only to perform this check at run time. This ensures that your module can always be precompiled, even on a system without a GPU:
module MyApplication
+
+using CUDA
+
+__init__() = @assert CUDA.functional(true)
+
+end
This of course also implies that you should avoid any calls to the GPU stack from global scope, since the package might not be functional.
If your application does not require a GPU, and can work without the CUDA packages, there is a tradeoff. As an example, let's define a function that uploads an array to the GPU if available:
module MyApplication
+
+using CUDA
+
+if CUDA.functional()
+ to_gpu_or_not_to_gpu(x::AbstractArray) = CuArray(x)
+else
+ to_gpu_or_not_to_gpu(x::AbstractArray) = x
+end
+
+end
This works, but cannot be simply adapted to a scenario with precompilation on a system without CUDA. One option is to evaluate code at run time:
function __init__()
+ if CUDA.functional()
+ @eval to_gpu_or_not_to_gpu(x::AbstractArray) = CuArray(x)
+ else
+ @eval to_gpu_or_not_to_gpu(x::AbstractArray) = x
+ end
+end
However, this causes compilation at run-time, and might negate much of the advantages that precompilation has to offer. Instead, you can use a global flag:
const use_gpu = Ref(false)
+to_gpu_or_not_to_gpu(x::AbstractArray) = use_gpu[] ? CuArray(x) : x
+
+function __init__()
+ use_gpu[] = CUDA.functional()
+end
The disadvantage of this approach is the introduction of a type instability.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
The Julia CUDA stack only requires users to have a functional NVIDIA driver. It is not necessary to install the CUDA toolkit. On Windows, also make sure you have the Visual C++ redistributable installed.
For most users, installing the latest tagged version of CUDA.jl will be sufficient. You can easily do that using the package manager:
pkg> add CUDA
Or, equivalently, via the Pkg
API:
julia> import Pkg; Pkg.add("CUDA")
In some cases, you might need to use the master
version of this package, e.g., because it includes a specific fix you need. Often, however, the development version of this package itself relies on unreleased versions of other packages. This information is recorded in the manifest at the root of the repository, which you can use by starting Julia from the CUDA.jl directory with the --project
flag:
$ cd .julia/dev/CUDA.jl # or wherever you have CUDA.jl checked out
+$ julia --project
+pkg> instantiate # to install correct dependencies
+julia> using CUDA
In the case you want to use the development version of CUDA.jl with other packages, you cannot use the manifest and you need to manually install those dependencies from the master branch. Again, the exact requirements are recorded in CUDA.jl's manifest, but often the following instructions will work:
pkg> add GPUCompiler#master
+pkg> add GPUArrays#master
+pkg> add LLVM#master
We support the same operation systems that NVIDIA supports: Linux, and Windows. Similarly, we support x86, ARM, PPC, ... as long as Julia is supported on it and there exists an NVIDIA driver and CUDA toolkit for your platform. The main development platform (and the only CI system) however is x86_64 on Linux, so if you are using a more exotic combination there might be bugs.
To use the Julia GPU stack, you need to install the NVIDIA driver for your system and GPU. You can find detailed instructions on the NVIDIA home page.
If you're using Linux you should always consider installing the driver through the package manager of your distribution. In the case that driver is out of date or does not support your GPU, and you need to download a driver from the NVIDIA home page, similarly prefer a distribution-specific package (e.g., deb, rpm) instead of the generic runfile option.
If you are using a shared system, ask your system administrator on how to install or load the NVIDIA driver. Generally, you should be able to find and use the CUDA driver library, called libcuda.so
on Linux, libcuda.dylib
on macOS and nvcuda64.dll
on Windows. You should also be able to execute the nvidia-smi
command, which lists all available GPUs you have access to.
On some enterprise systems, CUDA.jl will be able to upgrade the driver for the duration of the session (using CUDA's Forward Compatibility mechanism). This will be mentioned in the CUDA.versioninfo()
output, so be sure to verify that before asking your system administrator to upgrade:
julia> CUDA.versioninfo()
+CUDA runtime 10.2
+CUDA driver 11.8
+NVIDIA driver 520.56.6, originally for CUDA 11.7
Finally, to be able to use all of the Julia GPU stack you need to have permission to profile GPU code. On Linux, that means loading the nvidia
kernel module with the NVreg_RestrictProfilingToAdminUsers=0
option configured (e.g., in /etc/modprobe.d
). Refer to the following document for more information.
The recommended way to use CUDA.jl is to let it automatically download an appropriate CUDA toolkit. CUDA.jl will check your driver's capabilities, which versions of CUDA are available for your platform, and automatically download an appropriate artifact containing all the libraries that CUDA.jl supports.
If you really need to use a different CUDA toolkit, it's possible (but not recommended) to load a different version of the CUDA runtime, or even an installation from your local system. Both are configured by setting the version
preference (using Preferences.jl) on the CUDARuntimejll.jl package, but there is also a user-friendly API available in CUDA.jl.
You can choose which version to (try to) download and use by calling CUDA.set_runtime_version!
:
julia> using CUDA
+
+julia> CUDA.set_runtime_version!(v"11.8")
+[ Info: Set CUDA.jl toolkit preference to use CUDA 11.8.0 from artifact sources, please re-start Julia for this to take effect.
This generates the following LocalPreferences.toml
file in your active environment:
[CUDA_Runtime_jll]
+version = "11.8"
This preference is compatible with other CUDA JLLs, e.g., if you load CUDNN_jll
it will only select artifacts that are compatible with the configured CUDA runtime.
To use a local installation, you set the local_toolkit
keyword argument to CUDA.set_runtime_version!
:
julia> using CUDA
+
+julia> CUDA.versioninfo()
+CUDA runtime 11.8, artifact installation
+...
+
+julia> CUDA.set_runtime_version!(local_toolkit=true)
+[ Info: Set CUDA.jl toolkit preference to use CUDA from the local system, please re-start Julia for this to take effect.
After re-launching Julia:
julia> using CUDA
+
+julia> CUDA.versioninfo()
+CUDA runtime 11.8, local installation
+...
Calling the above helper function generates the following LocalPreferences.toml
file in your active environment:
[CUDA_Runtime_jll]
+local = "true"
This preference not only configures CUDA.jl to use a local toolkit, it also prevents downloading any artifact, so it may be interesting to set this preference before ever importing CUDA.jl (e.g., by putting this preference file in a system-wide depot).
If CUDA.jl doesn't properly detect your local toolkit, it may be that certain libraries or binaries aren't on a globally-discoverable path. For more information, run Julia with the JULIA_DEBUG
environment variable set to CUDA_Runtime_Discovery
.
Note that using a local toolkit instead of artifacts any CUDA-related JLL, not just of CUDA_Runtime_jll
. Any package that depends on such a JLL needs to inspect CUDA.local_toolkit
, and if set use CUDA_Runtime_Discovery
to detect libraries and binaries instead.
CUDA.jl can be precompiled and imported on systems without a GPU or CUDA installation. This simplifies the situation where an application optionally uses CUDA. However, when CUDA.jl is precompiled in such an environment, it cannot be used to run GPU code. This is a result of artifacts being selected at precompile time.
In some cases, e.g. with containers or HPC log-in nodes, you may want to precompile CUDA.jl on a system without CUDA, yet still want to have it download the necessary artifacts and/or produce a precompilation image that can be used on a system with CUDA. This can be achieved by informing CUDA.jl which CUDA toolkit to run time by calling CUDA.set_runtime_version!
.
When using artifacts, that's as simple as e.g. calling CUDA.set_runtime_version!(v"11.8")
, and afterwards re-starting Julia and re-importing CUDA.jl in order to trigger precompilation again and download the necessary artifacts. If you want to use a local CUDA installation, you also need to set the local_toolkit
keyword argument, e.g., by calling CUDA.set_runtime_version!(v"11.8"; local_toolkit=true)
. Note that the version specified here needs to match what will be available at run time. In both cases, i.e. when using artifacts or a local toolkit, the chosen version needs to be compatible with the available driver.
Finally, in such a scenario you may also want to call CUDA.precompile_runtime()
to ensure that the GPUCompiler runtime library is precompiled as well. This and all of the above is demonstrated in the Dockerfile that's part of the CUDA.jl repository.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This means that CUDA.jl could not find a suitable CUDA driver. For more information, re-run with the JULIA_DEBUG
environment variable set to CUDA_Driver_jll
.
If you encounter this error, there are several known issues that may be causing it:
dmesg
Generally though, it's impossible to say what's the reason for the error, but Julia is likely not to blame. Make sure your set-up works (e.g., try executing nvidia-smi
, a CUDA C binary, etc), and if everything looks good file an issue.
Check and make sure the NVSMI
folder is in your PATH
. By default it may not be. Look in C:\Program Files\NVIDIA Corporation
for the NVSMI
folder - you should see nvml.dll
within it. You can add this folder to your PATH
and check that nvidia-smi
runs properly.
Ensure the Visual C++ Redistributable is installed.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This section lists the package's public functionality that directly corresponds to functionality of the CUDA driver API. In general, the abstractions stay close to those of the CUDA driver API, so for more information on certain library calls you can consult the CUDA driver API reference.
The documentation is grouped according to the modules of the driver API.
CUDA.CuError
— TypeCuError(code)
Create a CUDA error object with error code code
.
CUDA.name
— Methodname(err::CuError)
Gets the string representation of an error code.
julia> err = CuError(CUDA.cudaError_enum(1))
+CuError(CUDA_ERROR_INVALID_VALUE)
+
+julia> name(err)
+"ERROR_INVALID_VALUE"
CUDA.description
— Methoddescription(err::CuError)
Gets the string description of an error code.
CUDA.driver_version
— Methoddriver_version()
Returns the latest version of CUDA supported by the loaded driver.
CUDA.runtime_version
— Methodruntime_version()
Returns the CUDA Runtime version.
CUDA.set_runtime_version!
— FunctionCUDA.set_runtime_version!([version::VersionNumber]; [local_toolkit::Bool])
Configures the active project to use a specific CUDA toolkit version from a specific source.
If local_toolkit
is set, the CUDA toolkit will be used from the local system, otherwise it will be downloaded from an artifact source. In the case of a local toolkit, version
informs CUDA.jl which version that is (this may be useful if auto-detection fails). In the case of artifact sources, version
controls which version will be downloaded and used.
When not specifying either the version
or the local_toolkit
argument, the default behavior will be used, which is to use the most recent compatible runtime available from an artifact source. Note that this will override any Preferences that may be configured in a higher-up depot; to clear preferences nondestructively, use CUDA.reset_runtime_version!
instead.
CUDA.reset_runtime_version!
— FunctionCUDA.reset_runtime_version!()
Resets the CUDA version preferences in the active project to the default, which is to use the most recent compatible runtime available from an artifact source, unless a higher-up depot has configured a different preference. To force use of the default behavior for the local project, use CUDA.set_runtime_version!
with no arguments.
CUDA.CuDevice
— TypeCuDevice(ordinal::Integer)
Get a handle to a compute device.
CUDA.devices
— Functiondevices()
Get an iterator for the compute devices.
CUDA.current_device
— Functioncurrent_device()
Returns the current device.
This is a low-level API, returning the current device as known to the CUDA driver. For most users, it is recommended to use the device
method instead.
CUDA.name
— Methodname(dev::CuDevice)
Returns an identifier string for the device.
CUDA.totalmem
— Methodtotalmem(dev::CuDevice)
Returns the total amount of memory (in bytes) on the device.
CUDA.attribute
— Functionattribute(dev::CuDevice, code)
Returns information about the device.
attribute(X, pool::CuMemoryPool, attr)
Returns attribute attr
about pool
. The type of the returned value depends on the attribute, and as such must be passed as the X
parameter.
attribute(X, ptr::Union{Ptr,CuPtr}, attr)
Returns attribute attr
about pointer ptr
. The type of the returned value depends on the attribute, and as such must be passed as the X
parameter.
Certain common attributes are exposed by additional convenience functions:
CUDA.capability
— Methodcapability(dev::CuDevice)
Returns the compute capability of the device.
CUDA.warpsize
— Methodwarpsize(dev::CuDevice)
Returns the warp size (in threads) of the device.
CUDA.CuContext
— TypeCuContext(dev::CuDevice, flags=CTX_SCHED_AUTO)
+CuContext(f::Function, ...)
Create a CUDA context for device. A context on the GPU is analogous to a process on the CPU, with its own distinct address space and allocated resources. When a context is destroyed, the system cleans up the resources allocated to it.
When you are done using the context, call CUDA.unsafe_destroy!
to mark it for deletion, or use do-block syntax with this constructor.
CUDA.unsafe_destroy!
— Methodunsafe_destroy!(ctx::CuContext)
Immediately destroy a context, freeing up all resources associated with it. This does not respect any users of the context, and might make other objects unusable.
CUDA.current_context
— Functioncurrent_context()
Returns the current context. Throws an undefined reference error if the current thread has no context bound to it, or if the bound context has been destroyed.
This is a low-level API, returning the current context as known to the CUDA driver. For most users, it is recommended to use the context
method instead.
CUDA.activate
— Methodactivate(ctx::CuContext)
Binds the specified CUDA context to the calling CPU thread.
CUDA.synchronize
— Methodsynchronize(ctx::Context)
Block for the all operations on ctx
to complete. This is a heavyweight operation, typically you only need to call synchronize
which only synchronizes the stream associated with the current task.
CUDA.device_synchronize
— Functiondevice_synchronize()
Block for the all operations on ctx
to complete. This is a heavyweight operation, typically you only need to call synchronize
which only synchronizes the stream associated with the current task.
On the device, device_synchronize
acts as a synchronization point for child grids in the context of dynamic parallelism.
CUDA.CuPrimaryContext
— TypeCuPrimaryContext(dev::CuDevice)
Create a primary CUDA context for a given device.
Each primary context is unique per device and is shared with CUDA runtime API. It is meant for interoperability with (applications using) the runtime API.
CUDA.CuContext
— MethodCuContext(pctx::CuPrimaryContext)
Derive a context from a primary context.
Calling this function increases the reference count of the primary context. The returned context should not be free with the unsafe_destroy!
function that's used with ordinary contexts. Instead, the refcount of the primary context should be decreased by calling unsafe_release!
, or set to zero by calling unsafe_reset!
. The easiest way to do this is by using the do
-block syntax.
CUDA.isactive
— Methodisactive(pctx::CuPrimaryContext)
Query whether a primary context is active.
CUDA.flags
— Methodflags(pctx::CuPrimaryContext)
Query the flags of a primary context.
CUDA.setflags!
— Methodsetflags!(pctx::CuPrimaryContext)
Set the flags of a primary context.
CUDA.unsafe_reset!
— Methodunsafe_reset!(pctx::CuPrimaryContext)
Explicitly destroys and cleans up all resources associated with a device's primary context in the current process. Note that this forcibly invalidates all contexts derived from this primary context, and as a result outstanding resources might become invalid.
CUDA.unsafe_release!
— MethodCUDA.unsafe_release!(pctx::CuPrimaryContext)
Lower the refcount of a context, possibly freeing up all resources associated with it. This does not respect any users of the context, and might make other objects unusable.
CUDA.CuModule
— TypeCuModule(data, options::Dict{CUjit_option,Any})
+CuModuleFile(path, options::Dict{CUjit_option,Any})
Create a CUDA module from a data, or a file containing data. The data may be PTX code, a CUBIN, or a FATBIN.
The options
is an optional dictionary of JIT options and their respective value.
CUDA.CuFunction
— TypeCuFunction(mod::CuModule, name::String)
Acquires a function handle from a named function in a module.
CUDA.CuGlobal
— TypeCuGlobal{T}(mod::CuModule, name::String)
Acquires a typed global variable handle from a named global in a module.
Base.eltype
— Methodeltype(var::CuGlobal)
Return the element type of a global variable object.
Base.getindex
— MethodBase.getindex(var::CuGlobal)
Return the current value of a global variable.
Base.setindex!
— MethodBase.setindex(var::CuGlobal{T}, val::T)
Set the value of a global variable to val
CUDA.CuLink
— TypeCuLink()
Creates a pending JIT linker invocation.
CUDA.add_data!
— Functionadd_data!(link::CuLink, name::String, code::String)
Add PTX code to a pending link operation.
add_data!(link::CuLink, name::String, data::Vector{UInt8})
Add object code to a pending link operation.
CUDA.add_file!
— Functionadd_file!(link::CuLink, path::String, typ::CUjitInputType)
Add data from a file to a link operation. The argument typ
indicates the type of the contained data.
CUDA.CuLinkImage
— TypeThe result of a linking operation.
This object keeps its parent linker object alive, as destroying a linker destroys linked images too.
CUDA.complete
— Functioncomplete(link::CuLink)
Complete a pending linker invocation, returning an output image.
CUDA.CuModule
— MethodCuModule(img::CuLinkImage, ...)
Create a CUDA module from a completed linking operation. Options from CuModule
apply.
Different kinds of memory objects can be created, representing different kinds of memory that the CUDA toolkit supports. Each of these memory objects can be allocated by calling alloc
with the type of memory as first argument, and freed by calling free
. Certain kinds of memory have specific methods defined.
This memory is accessible only by the GPU, and is the most common kind of memory used in CUDA programming.
CUDA.DeviceMemory
— TypeDeviceMemory
Device memory residing on the GPU.
CUDA.alloc
— Methodalloc(DeviceMemory, bytesize::Integer;
+ [async=false], [stream::CuStream], [pool::CuMemoryPool])
Allocate bytesize
bytes of memory on the device. This memory is only accessible on the GPU, and requires explicit calls to unsafe_copyto!
, which wraps cuMemcpy
, for access on the CPU.
Unified memory is accessible by both the CPU and the GPU, and is managed by the CUDA runtime. It is automatically migrated between the CPU and the GPU as needed, which simplifies programming but can lead to performance issues if not used carefully.
CUDA.UnifiedMemory
— TypeUnifiedMemory
Unified memory that is accessible on both the CPU and GPU.
CUDA.alloc
— Methodalloc(UnifiedMemory, bytesize::Integer, [flags::CUmemAttach_flags])
Allocate bytesize
bytes of unified memory. This memory is accessible from both the CPU and GPU, with the CUDA driver automatically copying upon first access.
CUDA.prefetch
— Methodprefetch(::UnifiedMemory, [bytes::Integer]; [device::CuDevice], [stream::CuStream])
Prefetches memory to the specified destination device.
CUDA.advise
— Methodadvise(::UnifiedMemory, advice::CUDA.CUmem_advise, [bytes::Integer]; [device::CuDevice])
Advise about the usage of a given memory range.
Host memory resides on the CPU, but is accessible by the GPU via the PCI bus. This is the slowest kind of memory, but is useful for communicating between running kernels and the host (e.g., to update counters or flags).
CUDA.HostMemory
— TypeHostMemory
Pinned memory residing on the CPU, possibly accessible on the GPU.
CUDA.alloc
— Methodalloc(HostMemory, bytesize::Integer, [flags])
Allocate bytesize
bytes of page-locked memory on the host. This memory is accessible from the CPU, and makes it possible to perform faster memory copies to the GPU. Furthermore, if flags
is set to MEMHOSTALLOC_DEVICEMAP
the memory is also accessible from the GPU. These accesses are direct, and go through the PCI bus. If flags
is set to MEMHOSTALLOC_PORTABLE
, the memory is considered mapped by all CUDA contexts, not just the one that created the memory, which is useful if the memory needs to be accessed from multiple devices. Multiple flags
can be set at one time using a bytewise OR
:
flags = MEMHOSTALLOC_PORTABLE | MEMHOSTALLOC_DEVICEMAP
CUDA.register
— Methodregister(HostMemory, ptr::Ptr, bytesize::Integer, [flags])
Page-lock the host memory pointed to by ptr
. Subsequent transfers to and from devices will be faster, and can be executed asynchronously. If the MEMHOSTREGISTER_DEVICEMAP
flag is specified, the buffer will also be accessible directly from the GPU. These accesses are direct, and go through the PCI bus. If the MEMHOSTREGISTER_PORTABLE
flag is specified, any CUDA context can access the memory.
CUDA.unregister
— Methodunregister(::HostMemory)
Unregisters a memory range that was registered with register
.
Array memory is a special kind of memory that is optimized for 2D and 3D access patterns. The memory is opaquely managed by the CUDA runtime, and is typically only used on combination with texture intrinsics.
CUDA.ArrayMemory
— TypeArrayMemory
Array memory residing on the GPU, possibly in a specially-formatted way.
CUDA.alloc
— Methodalloc(ArrayMemory, dims::Dims)
Allocate array memory with dimensions dims
. The memory is accessible on the GPU, but can only be used in conjunction with special intrinsics (e.g., texture intrinsics).
To work with these buffers, you need to convert
them to a Ptr
, CuPtr
, or in the case of ArrayMemory
an CuArrayPtr
. You can then use common Julia methods on these pointers, such as unsafe_copyto!
. CUDA.jl also provides some specialized functionality that does not match standard Julia functionality:
CUDA.unsafe_copy2d!
— Functionunsafe_copy2d!(dst, dstTyp, src, srcTyp, width, height=1;
+ dstPos=(1,1), dstPitch=0,
+ srcPos=(1,1), srcPitch=0,
+ async=false, stream=nothing)
Perform a 2D memory copy between pointers src
and dst
, at respectively position srcPos
and dstPos
(1-indexed). Pitch can be specified for both the source and destination; consult the CUDA documentation for more details. This call is executed asynchronously if async
is set, otherwise stream
is synchronized.
CUDA.unsafe_copy3d!
— Functionunsafe_copy3d!(dst, dstTyp, src, srcTyp, width, height=1, depth=1;
+ dstPos=(1,1,1), dstPitch=0, dstHeight=0,
+ srcPos=(1,1,1), srcPitch=0, srcHeight=0,
+ async=false, stream=nothing)
Perform a 3D memory copy between pointers src
and dst
, at respectively position srcPos
and dstPos
(1-indexed). Both pitch and height can be specified for both the source and destination; consult the CUDA documentation for more details. This call is executed asynchronously if async
is set, otherwise stream
is synchronized.
CUDA.memset
— Functionmemset(mem::CuPtr, value::Union{UInt8,UInt16,UInt32}, len::Integer; [stream::CuStream])
Initialize device memory by copying val
for len
times.
CUDA.free_memory
— Functionfree_memory()
Returns the free amount of memory (in bytes), available for allocation by the CUDA context.
CUDA.total_memory
— Functiontotal_memory()
Returns the total amount of memory (in bytes), available for allocation by the CUDA context.
CUDA.CuStream
— TypeCuStream(; flags=STREAM_DEFAULT, priority=nothing)
Create a CUDA stream.
CUDA.isdone
— Methodisdone(s::CuStream)
Return false
if a stream is busy (has task running or queued) and true
if that stream is free.
CUDA.priority_range
— Functionpriority_range()
Return the valid range of stream priorities as a StepRange
(with step size 1). The lower bound of the range denotes the least priority (typically 0), with the upper bound representing the greatest possible priority (typically -1).
CUDA.priority
— Functionpriority_range(s::CuStream)
Return the priority of a stream s
.
CUDA.synchronize
— Methodsynchronize([stream::CuStream])
Wait until stream
has finished executing, with stream
defaulting to the stream associated with the current Julia task.
See also: device_synchronize
CUDA.@sync
— Macro@sync [blocking=false] ex
Run expression ex
and synchronize the GPU afterwards.
The blocking
keyword argument determines how synchronization is performed. By default, non-blocking synchronization will be used, which gives other Julia tasks a chance to run while waiting for the GPU to finish. This may increase latency, so for short operations, or when benchmaring code that does not use multiple tasks, it may be beneficial to use blocking synchronization instead by setting blocking=true
. Blocking synchronization can also be enabled globally by changing the nonblocking_synchronization
preference.
See also: synchronize
.
For specific use cases, special streams are available:
CUDA.default_stream
— Functiondefault_stream()
Return the default stream.
It is generally better to use stream()
to get a stream object that's local to the current task. That way, operations scheduled in other tasks can overlap.
CUDA.legacy_stream
— Functionlegacy_stream()
Return a special object to use use an implicit stream with legacy synchronization behavior.
You can use this stream to perform operations that should block on all streams (with the exception of streams created with STREAM_NON_BLOCKING
). This matches the old pre-CUDA 7 global stream behavior.
CUDA.per_thread_stream
— Functionper_thread_stream()
Return a special object to use an implicit stream with per-thread synchronization behavior. This stream object is normally meant to be used with APIs that do not have per-thread versions of their APIs (i.e. without a ptsz
or ptds
suffix).
It is generally not needed to use this type of stream. With CUDA.jl, each task already gets its own non-blocking stream, and multithreading in Julia is typically accomplished using tasks.
CUDA.CuEvent
— TypeCuEvent()
Create a new CUDA event.
CUDA.record
— Functionrecord(e::CuEvent, [stream::CuStream])
Record an event on a stream.
CUDA.synchronize
— Methodsynchronize(e::CuEvent)
Waits for an event to complete.
CUDA.isdone
— Methodisdone(e::CuEvent)
Return false
if there is outstanding work preceding the most recent call to record(e)
and true
if all captured work has been completed.
CUDA.wait
— Methodwait(e::CuEvent, [stream::CuStream])
Make a stream wait on a event. This only makes the stream wait, and not the host; use synchronize(::CuEvent)
for that.
CUDA.elapsed
— Functionelapsed(start::CuEvent, stop::CuEvent)
Computes the elapsed time between two events (in seconds).
CUDA.@elapsed
— Macro@elapsed [blocking=false] ex
A macro to evaluate an expression, discarding the resulting value, instead returning the number of seconds it took to execute on the GPU, as a floating-point number.
See also: @sync
.
CUDA.CuDim3
— TypeCuDim3(x)
+
+CuDim3((x,))
+CuDim3((x, y))
+CuDim3((x, y, x))
A type used to specify dimensions, consisting of 3 integers for respectively the x
, y
and z
dimension. Unspecified dimensions default to 1
.
Often accepted as argument through the CuDim
type alias, eg. in the case of cudacall
or CUDA.launch
, allowing to pass dimensions as a plain integer or a tuple without having to construct an explicit CuDim3
object.
CUDA.cudacall
— Functioncudacall(f, types, values...; blocks::CuDim, threads::CuDim,
+ cooperative=false, shmem=0, stream=stream())
ccall
-like interface for launching a CUDA function f
on a GPU.
For example:
vadd = CuFunction(md, "vadd")
+a = rand(Float32, 10)
+b = rand(Float32, 10)
+ad = alloc(CUDA.DeviceMemory, 10*sizeof(Float32))
+unsafe_copyto!(ad, convert(Ptr{Cvoid}, a), 10*sizeof(Float32)))
+bd = alloc(CUDA.DeviceMemory, 10*sizeof(Float32))
+unsafe_copyto!(bd, convert(Ptr{Cvoid}, b), 10*sizeof(Float32)))
+c = zeros(Float32, 10)
+cd = alloc(CUDA.DeviceMemory, 10*sizeof(Float32))
+
+cudacall(vadd, (CuPtr{Cfloat},CuPtr{Cfloat},CuPtr{Cfloat}), ad, bd, cd; threads=10)
+unsafe_copyto!(convert(Ptr{Cvoid}, c), cd, 10*sizeof(Float32)))
The blocks
and threads
arguments control the launch configuration, and should both consist of either an integer, or a tuple of 1 to 3 integers (omitted dimensions default to 1). The types
argument can contain both a tuple of types, and a tuple type, the latter being slightly faster.
CUDA.launch
— Functionlaunch(f::CuFunction; args...; blocks::CuDim=1, threads::CuDim=1,
+ cooperative=false, shmem=0, stream=stream())
Low-level call to launch a CUDA function f
on the GPU, using blocks
and threads
as respectively the grid and block configuration. Dynamic shared memory is allocated according to shmem
, and the kernel is launched on stream stream
.
Arguments to a kernel should either be bitstype, in which case they will be copied to the internal kernel parameter buffer, or a pointer to device memory.
This is a low-level call, prefer to use cudacall
instead.
launch(exec::CuGraphExec, [stream::CuStream])
Launches an executable graph, by default in the currently-active stream.
CUDA.@profile
— Macro@profile [trace=false] [raw=false] code...
+@profile external=true code...
Profile the GPU execution of code
.
There are two modes of operation, depending on whether external
is true
or false
. The default value depends on whether Julia is being run under an external profiler.
Integrated profiler (external=false
, the default)
In this mode, CUDA.jl will profile the execution of code
and display the result. By default, a summary of host and device-side execution will be show, including any NVTX events. To display a chronological trace of the captured activity instead, trace
can be set to true
. Trace output will include an ID column that can be used to match host-side and device-side activity. If raw
is true
, all data will always be included, even if it may not be relevant. The output will be written to io
, which defaults to stdout
.
Slow operations will be highlighted in the output: Entries colored in yellow are among the slowest 25%, while entries colored in red are among the slowest 5% of all operations.
!!! compat "Julia 1.9" This functionality is only available on Julia 1.9 and later.
!!! compat "CUDA 11.2" Older versions of CUDA, before 11.2, contain bugs that may prevent the CUDA.@profile
macro to work. It is recommended to use a newer runtime.
External profilers (external=true
, when an external profiler is detected)
For more advanced profiling, it is possible to use an external profiling tool, such as NSight Systems or NSight Compute. When doing so, it is often advisable to only enable the profiler for the specific code region of interest. This can be done by wrapping the code with CUDA.@profile external=true
, which used to be the only way to use this macro.
CUDA.Profile.start
— Functionstart()
Enables profile collection by the active profiling tool for the current context. If profiling is already enabled, then this call has no effect.
CUDA.Profile.stop
— Functionstop()
Disables profile collection by the active profiling tool for the current context. If profiling is already disabled, then this call has no effect.
Textures are represented by objects of type CuTexture
which are bound to some underlying memory, either CuArray
s or CuTextureArray
s:
CUDA.CuTexture
— TypeCuTexture{T,N,P}
N
-dimensional texture object with elements of type T
. These objects do not store data themselves, but are bounds to another source of device memory. Texture objects can be passed to CUDA kernels, where they will be accessible through the CuDeviceTexture
type.
Experimental API. Subject to change without deprecation.
CUDA.CuTexture
— MethodCuTexture{T,N,P}(parent::P; address_mode, filter_mode, normalized_coordinates)
Construct a N
-dimensional texture object with elements of type T
as stored in parent
.
Several keyword arguments alter the behavior of texture objects:
address_mode
(wrap, clamp, mirror): how out-of-bounds values are accessed. Can be specified as a value for all dimensions, or as a tuple of N
entries.interpolation
(nearest neighbour, linear, bilinear): how non-integral indices are fetched. Nearest-neighbour fetches a single value, others interpolate between multiple.normalized_coordinates
(true, false): whether indices are expected to fall in the normalized [0:1)
range.!!! warning Experimental API. Subject to change without deprecation.
CuTexture(x::CuTextureArray{T,N})
Create a N
-dimensional texture object withelements of type T
that will be read from x
.
Experimental API. Subject to change without deprecation.
CuTexture(x::CuArray{T,N})
Create a N
-dimensional texture object that reads from a CuArray
.
Note that it is necessary the their memory is well aligned and strided (good pitch). Currently, that is not being enforced.
Experimental API. Subject to change without deprecation.
You can create CuTextureArray
objects from both host and device memory:
CUDA.CuTextureArray
— TypeCuTextureArray{T,N}(undef, dims)
N
-dimensional dense texture array with elements of type T
. These arrays are optimized for texture fetching, and are only meant to be used as a source for CuTexture{T,N,P}
objects.
Experimental API. Subject to change without deprecation.
CUDA.CuTextureArray
— MethodCuTextureArray(A::AbstractArray)
Allocate and initialize a texture array from host memory in A
.
Experimental API. Subject to change without deprecation.
CuTextureArray(A::CuArray)
Allocate and initialize a texture array from device memory in A
.
Experimental API. Subject to change without deprecation.
The occupancy API can be used to figure out an appropriate launch configuration for a compiled kernel (represented as a CuFunction
) on the current device:
CUDA.launch_configuration
— Functionlaunch_configuration(fun::CuFunction; shmem=0, max_threads=0)
Calculate a suggested launch configuration for kernel fun
requiring shmem
bytes of dynamic shared memory. Returns a tuple with a suggested amount of threads, and the minimal amount of blocks to reach maximal occupancy. Optionally, the maximum amount of threads can be constrained using max_threads
.
In the case of a variable amount of shared memory, pass a callable object for shmem
instead, taking a single integer representing the block size and returning the amount of dynamic shared memory for that configuration.
CUDA.active_blocks
— Functionactive_blocks(fun::CuFunction, threads; shmem=0)
Calculate the maximum number of active blocks per multiprocessor when running threads
threads of a kernel fun
requiring shmem
bytes of dynamic shared memory.
CUDA.occupancy
— Functionoccupancy(fun::CuFunction, threads; shmem=0)
Calculate the theoretical occupancy of launching threads
threads of a kernel fun
requiring shmem
bytes of dynamic shared memory.
CUDA graphs can be easily recorded and executed using the high-level @captured
macro:
CUDA.@captured
— Macrofor ...
+ @captured begin
+ # code that executes several kernels or CUDA operations
+ end
+end
A convenience macro for recording a graph of CUDA operations and automatically cache and update the execution. This can improve performance when executing kernels in a loop, where the launch overhead might dominate the execution.
For this to be effective, the kernels and operations executed inside of the captured region should not signficantly change across iterations of the loop. It is allowed to, e.g., change kernel arguments or inputs to operations, as this will be processed by updating the cached executable graph. However, significant changes will result in an instantiation of the graph from scratch, which is an expensive operation.
See also: capture
.
Low-level operations are available too:
CUDA.CuGraph
— TypeCuGraph([flags])
Create an empty graph for use with low-level graph operations. If you want to create a graph while directly recording operations, use capture
. For a high-level interface that also automatically executes the graph, use the @captured
macro.
CUDA.capture
— Functioncapture([flags], [throw_error::Bool=true]) do
+ ...
+end
Capture a graph of CUDA operations. The returned graph can then be instantiated and executed repeatedly for improved performance.
Note that many operations, like initial kernel compilation or memory allocations, cannot be captured. To work around this, you can set the throw_error
keyword to false, which will cause this function to return nothing
if such a failure happens. You can then try to evaluate the function in a regular way, and re-record afterwards.
See also: instantiate
.
CUDA.instantiate
— Functioninstantiate(graph::CuGraph)
Creates an executable graph from a graph. This graph can then be launched, or updated with an other graph.
CUDA.launch
— Methodlaunch(f::CuFunction; args...; blocks::CuDim=1, threads::CuDim=1,
+ cooperative=false, shmem=0, stream=stream())
Low-level call to launch a CUDA function f
on the GPU, using blocks
and threads
as respectively the grid and block configuration. Dynamic shared memory is allocated according to shmem
, and the kernel is launched on stream stream
.
Arguments to a kernel should either be bitstype, in which case they will be copied to the internal kernel parameter buffer, or a pointer to device memory.
This is a low-level call, prefer to use cudacall
instead.
launch(exec::CuGraphExec, [stream::CuStream])
Launches an executable graph, by default in the currently-active stream.
CUDA.update
— Functionupdate(exec::CuGraphExec, graph::CuGraph; [throw_error::Bool=true])
Check whether an executable graph can be updated with a graph and perform the update if possible. Returns a boolean indicating whether the update was successful. Unless throw_error
is set to false, also throws an error if the update failed.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
This tutorial shows how to use custom structs on the GPU. Our example will be a one dimensional interpolation. Lets start with the CPU version:
using CUDA
+
+struct Interpolate{A}
+ xs::A
+ ys::A
+end
+
+function (itp::Interpolate)(x)
+ i = searchsortedfirst(itp.xs, x)
+ i = clamp(i, firstindex(itp.ys), lastindex(itp.ys))
+ @inbounds itp.ys[i]
+end
+
+xs_cpu = [1.0, 2.0, 3.0]
+ys_cpu = [10.0,20.0,30.0]
+itp_cpu = Interpolate(xs_cpu, ys_cpu)
+pts_cpu = [1.1,2.3]
+result_cpu = itp_cpu.(pts_cpu)
2-element Vector{Float64}:
+ 20.0
+ 30.0
Ok the CPU code works, let's move our data to the GPU:
itp = Interpolate(CuArray(xs_cpu), CuArray(ys_cpu))
+pts = CuArray(pts_cpu);
If we try to call our interpolate itp.(pts)
, we get an error however:
...
+KernelError: passing and using non-bitstype argument
+...
Why does it throw an error? Our calculation involves a custom type Interpolate{CuArray{Float64, 1}}
. At the end of the day all arguments of a CUDA kernel need to be bitstypes. However we have
isbitstype(typeof(itp))
false
How to fix this? The answer is, that there is a conversion mechanism, which adapts objects into CUDA compatible bitstypes. It is based on the Adapt.jl package and basic types like CuArray
already participate in this mechanism. For custom types, we just need to add a conversion rule like so:
import Adapt
+function Adapt.adapt_structure(to, itp::Interpolate)
+ xs = Adapt.adapt_structure(to, itp.xs)
+ ys = Adapt.adapt_structure(to, itp.ys)
+ Interpolate(xs, ys)
+end
Now our struct plays nicely with CUDA.jl:
result = itp.(pts)
2-element CuArray{Float64, 1, CUDA.DeviceMemory}:
+ 20.0
+ 30.0
It works, we get the same result as on the CPU.
@assert CuArray(result_cpu) == result
Alternatively instead of defining Adapt.adapt_structure
explictly, we could have done
Adapt.@adapt_structure Interpolate
which expands to the same code that we wrote manually.
This page was generated using Literate.jl.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
A gentle introduction to parallelization and GPU programming in Julia
Julia has first-class support for GPU programming: you can use high-level abstractions or obtain fine-grained control, all without ever leaving your favorite programming language. The purpose of this tutorial is to help Julia users take their first step into GPU computing. In this tutorial, you'll compare CPU and GPU implementations of a simple calculation, and learn about a few of the factors that influence the performance you obtain.
This tutorial is inspired partly by a blog post by Mark Harris, An Even Easier Introduction to CUDA, which introduced CUDA using the C++ programming language. You do not need to read that tutorial, as this one starts from the beginning.
We'll consider the following demo, a simple calculation on the CPU.
N = 2^20
+x = fill(1.0f0, N) # a vector filled with 1.0 (Float32)
+y = fill(2.0f0, N) # a vector filled with 2.0
+
+y .+= x # increment each element of y with the corresponding element of x
1048576-element Vector{Float32}:
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ ⋮
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
+ 3.0
check that we got the right answer
using Test
+@test all(y .== 3.0f0)
Test Passed
From the Test Passed
line we know everything is in order. We used Float32
numbers in preparation for the switch to GPU computations: GPUs are faster (sometimes, much faster) when working with Float32
than with Float64
.
A distinguishing feature of this calculation is that every element of y
is being updated using the same operation. This suggests that we might be able to parallelize this.
First let's do the parallelization on the CPU. We'll create a "kernel function" (the computational core of the algorithm) in two implementations, first a sequential version:
function sequential_add!(y, x)
+ for i in eachindex(y, x)
+ @inbounds y[i] += x[i]
+ end
+ return nothing
+end
+
+fill!(y, 2)
+sequential_add!(y, x)
+@test all(y .== 3.0f0)
Test Passed
And now a parallel implementation:
function parallel_add!(y, x)
+ Threads.@threads for i in eachindex(y, x)
+ @inbounds y[i] += x[i]
+ end
+ return nothing
+end
+
+fill!(y, 2)
+parallel_add!(y, x)
+@test all(y .== 3.0f0)
Test Passed
Now if I've started Julia with JULIA_NUM_THREADS=4
on a machine with at least 4 cores, I get the following:
using BenchmarkTools
+@btime sequential_add!($y, $x)
487.303 μs (0 allocations: 0 bytes)
versus
@btime parallel_add!($y, $x)
259.587 μs (13 allocations: 1.48 KiB)
You can see there's a performance benefit to parallelization, though not by a factor of 4 due to the overhead for starting threads. With larger arrays, the overhead would be "diluted" by a larger amount of "real work"; these would demonstrate scaling that is closer to linear in the number of cores. Conversely, with small arrays, the parallel version might be slower than the serial version.
For most of this tutorial you need to have a computer with a compatible GPU and have installed CUDA. You should also install the following packages using Julia's package manager:
pkg> add CUDA
If this is your first time, it's not a bad idea to test whether your GPU is working by testing the CUDA.jl package:
pkg> add CUDA
+pkg> test CUDA
We'll first demonstrate GPU computations at a high level using the CuArray
type, without explicitly writing a kernel function:
using CUDA
+
+x_d = CUDA.fill(1.0f0, N) # a vector stored on the GPU filled with 1.0 (Float32)
+y_d = CUDA.fill(2.0f0, N) # a vector stored on the GPU filled with 2.0
1048576-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ ⋮
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+ 2.0
Here the d
means "device," in contrast with "host". Now let's do the increment:
y_d .+= x_d
+@test all(Array(y_d) .== 3.0f0)
Test Passed
The statement Array(y_d)
moves the data in y_d
back to the host for testing. If we want to benchmark this, let's put it in a function:
function add_broadcast!(y, x)
+ CUDA.@sync y .+= x
+ return
+end
add_broadcast! (generic function with 1 method)
@btime add_broadcast!($y_d, $x_d)
67.047 μs (84 allocations: 2.66 KiB)
The most interesting part of this is the call to CUDA.@sync
. The CPU can assign jobs to the GPU and then go do other stuff (such as assigning more jobs to the GPU) while the GPU completes its tasks. Wrapping the execution in a CUDA.@sync
block will make the CPU block until the queued GPU tasks are done, similar to how Base.@sync
waits for distributed CPU tasks. Without such synchronization, you'd be measuring the time takes to launch the computation, not the time to perform the computation. But most of the time you don't need to synchronize explicitly: many operations, like copying memory from the GPU to the CPU, implicitly synchronize execution.
For this particular computer and GPU, you can see the GPU computation was significantly faster than the single-threaded CPU computation, and that the use of multiple CPU threads makes the CPU implementation competitive. Depending on your hardware you may get different results.
Using the high-level GPU array functionality made it easy to perform this computation on the GPU. However, we didn't learn about what's going on under the hood, and that's the main goal of this tutorial. So let's implement the same functionality with a GPU kernel:
function gpu_add1!(y, x)
+ for i = 1:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return nothing
+end
+
+fill!(y_d, 2)
+@cuda gpu_add1!(y_d, x_d)
+@test all(Array(y_d) .== 3.0f0)
Test Passed
Aside from using the CuArray
s x_d
and y_d
, the only GPU-specific part of this is the kernel launch via @cuda
. The first time you issue this @cuda
statement, it will compile the kernel (gpu_add1!
) for execution on the GPU. Once compiled, future invocations are fast. You can see what @cuda
expands to using ?@cuda
from the Julia prompt.
Let's benchmark this:
function bench_gpu1!(y, x)
+ CUDA.@sync begin
+ @cuda gpu_add1!(y, x)
+ end
+end
bench_gpu1! (generic function with 1 method)
@btime bench_gpu1!($y_d, $x_d)
119.783 ms (47 allocations: 1.23 KiB)
That's a lot slower than the version above based on broadcasting. What happened?
When you don't get the performance you expect, usually your first step should be to profile the code and see where it's spending its time:
bench_gpu1!(y_d, x_d) # run it once to force compilation
+CUDA.@profile bench_gpu1!(y_d, x_d)
Profiler ran for 98.38 ms, capturing 804 events.
+
+Host-side activity: calling CUDA APIs took 97.5 ms (99.11% of the trace)
+┌──────────┬────────────┬───────┬─────────────────────┐
+│ Time (%) │ Total time │ Calls │ Name │
+├──────────┼────────────┼───────┼─────────────────────┤
+│ 99.10% │ 97.5 ms │ 1 │ cuStreamSynchronize │
+│ 0.04% │ 41.48 µs │ 1 │ cuLaunchKernel │
+│ 0.00% │ 1.91 µs │ 1 │ cuCtxSetCurrent │
+│ 0.00% │ 715.26 ns │ 1 │ cuCtxGetDevice │
+│ 0.00% │ 476.84 ns │ 1 │ cuDeviceGetCount │
+└──────────┴────────────┴───────┴─────────────────────┘
+
+Device-side activity: GPU was busy for 98.28 ms (99.89% of the trace)
+┌──────────┬────────────┬───────┬───────────────────────────────────────────────
+│ Time (%) │ Total time │ Calls │ Name ⋯
+├──────────┼────────────┼───────┼───────────────────────────────────────────────
+│ 99.89% │ 98.28 ms │ 1 │ _Z9gpu_add1_13CuDeviceArrayI7Float32Ll1ELl1E ⋯
+└──────────┴────────────┴───────┴───────────────────────────────────────────────
+ 1 column omitted
+
You can see that almost all of the time was spent in ptxcall_gpu_add1__1
, the name of the kernel that CUDA.jl assigned when compiling gpu_add1!
for these inputs. (Had you created arrays of multiple data types, e.g., xu_d = CUDA.fill(0x01, N)
, you might have also seen ptxcall_gpu_add1__2
and so on. Like the rest of Julia, you can define a single method and it will be specialized at compile time for the particular data types you're using.)
For further insight, run the profiling with the option trace=true
CUDA.@profile trace=true bench_gpu1!(y_d, x_d)
Profiler ran for 108.59 ms, capturing 804 events.
+
+Host-side activity: calling CUDA APIs took 107.7 ms (99.18% of the trace)
+┌─────┬───────────┬───────────┬────────┬─────────────────────┐
+│ ID │ Start │ Time │ Thread │ Name │
+├─────┼───────────┼───────────┼────────┼─────────────────────┤
+│ 21 │ 40.05 µs │ 34.81 µs │ 1 │ cuLaunchKernel │
+│ 795 │ 839.95 µs │ 2.15 µs │ 2 │ cuCtxSetCurrent │
+│ 796 │ 845.67 µs │ 953.67 ns │ 2 │ cuCtxGetDevice │
+│ 797 │ 854.49 µs │ 238.42 ns │ 2 │ cuDeviceGetCount │
+│ 800 │ 867.61 µs │ 107.7 ms │ 2 │ cuStreamSynchronize │
+└─────┴───────────┴───────────┴────────┴─────────────────────┘
+
+Device-side activity: GPU was busy for 108.48 ms (99.90% of the trace)
+┌────┬──────────┬───────────┬─────────┬────────┬──────┬─────────────────────────
+│ ID │ Start │ Time │ Threads │ Blocks │ Regs │ Name ⋯
+├────┼──────────┼───────────┼─────────┼────────┼──────┼─────────────────────────
+│ 21 │ 75.82 µs │ 108.48 ms │ 1 │ 1 │ 19 │ _Z9gpu_add1_13CuDevice ⋯
+└────┴──────────┴───────────┴─────────┴────────┴──────┴─────────────────────────
+ 1 column omitted
+
The key thing to note here is that we are only using a single block with a single thread. These terms will be explained shortly, but for now, suffice it to say that this is an indication that this computation ran sequentially. Of note, sequential processing with GPUs is much slower than with CPUs; where GPUs shine is with large-scale parallelism.
To speed up the kernel, we want to parallelize it, which means assigning different tasks to different threads. To facilitate the assignment of work, each CUDA thread gets access to variables that indicate its own unique identity, much as Threads.threadid()
does for CPU threads. The CUDA analogs of threadid
and nthreads
are called threadIdx
and blockDim
, respectively; one difference is that these return a 3-dimensional structure with fields x
, y
, and z
to simplify cartesian indexing for up to 3-dimensional arrays. Consequently we can assign unique work in the following way:
function gpu_add2!(y, x)
+ index = threadIdx().x # this example only requires linear indexing, so just use `x`
+ stride = blockDim().x
+ for i = index:stride:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return nothing
+end
+
+fill!(y_d, 2)
+@cuda threads=256 gpu_add2!(y_d, x_d)
+@test all(Array(y_d) .== 3.0f0)
Test Passed
Note the threads=256
here, which divides the work among 256 threads numbered in a linear pattern. (For a two-dimensional array, we might have used threads=(16, 16)
and then both x
and y
would be relevant.)
Now let's try benchmarking it:
function bench_gpu2!(y, x)
+ CUDA.@sync begin
+ @cuda threads=256 gpu_add2!(y, x)
+ end
+end
bench_gpu2! (generic function with 1 method)
@btime bench_gpu2!($y_d, $x_d)
1.873 ms (47 allocations: 1.23 KiB)
Much better!
But obviously we still have a ways to go to match the initial broadcasting result. To do even better, we need to parallelize more. GPUs have a limited number of threads they can run on a single streaming multiprocessor (SM), but they also have multiple SMs. To take advantage of them all, we need to run a kernel with multiple blocks. We'll divide up the work like this:
This diagram was borrowed from a description of the C/C++ library; in Julia, threads and blocks begin numbering with 1 instead of 0. In this diagram, the 4096 blocks of 256 threads (making 1048576 = 2^20 threads) ensures that each thread increments just a single entry; however, to ensure that arrays of arbitrary size can be handled, let's still use a loop:
function gpu_add3!(y, x)
+ index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
+ stride = gridDim().x * blockDim().x
+ for i = index:stride:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return
+end
+
+numblocks = ceil(Int, N/256)
+
+fill!(y_d, 2)
+@cuda threads=256 blocks=numblocks gpu_add3!(y_d, x_d)
+@test all(Array(y_d) .== 3.0f0)
Test Passed
The benchmark:
function bench_gpu3!(y, x)
+ numblocks = ceil(Int, length(y)/256)
+ CUDA.@sync begin
+ @cuda threads=256 blocks=numblocks gpu_add3!(y, x)
+ end
+end
bench_gpu3! (generic function with 1 method)
@btime bench_gpu3!($y_d, $x_d)
67.268 μs (52 allocations: 1.31 KiB)
Finally, we've achieved the similar performance to what we got with the broadcasted version. Let's profile again to confirm this launch configuration:
CUDA.@profile trace=true bench_gpu3!(y_d, x_d)
Profiler ran for 13.24 ms, capturing 296 events.
+
+Host-side activity: calling CUDA APIs took 97.04 µs (0.73% of the trace)
+┌─────┬──────────┬──────────┬─────────────────────┐
+│ ID │ Start │ Time │ Name │
+├─────┼──────────┼──────────┼─────────────────────┤
+│ 21 │ 13.03 ms │ 46.97 µs │ cuLaunchKernel │
+│ 292 │ 13.22 ms │ 5.72 µs │ cuStreamSynchronize │
+└─────┴──────────┴──────────┴─────────────────────┘
+
+Device-side activity: GPU was busy for 130.89 µs (0.99% of the trace)
+┌────┬──────────┬───────────┬─────────┬────────┬──────┬─────────────────────────
+│ ID │ Start │ Time │ Threads │ Blocks │ Regs │ Name ⋯
+├────┼──────────┼───────────┼─────────┼────────┼──────┼─────────────────────────
+│ 21 │ 13.08 ms │ 130.89 µs │ 256 │ 4096 │ 40 │ _Z9gpu_add3_13CuDevice ⋯
+└────┴──────────┴───────────┴─────────┴────────┴──────┴─────────────────────────
+ 1 column omitted
+
In the previous example, the number of threads was hard-coded to 256. This is not ideal, as using more threads generally improves performance, but the maximum number of allowed threads to launch depends on your GPU as well as on the kernel. To automatically select an appropriate number of threads, it is recommended to use the launch configuration API. This API takes a compiled (but not launched) kernel, returns a tuple with an upper bound on the number of threads, and the minimum number of blocks that are required to fully saturate the GPU:
kernel = @cuda launch=false gpu_add3!(y_d, x_d)
+config = launch_configuration(kernel.fun)
+threads = min(N, config.threads)
+blocks = cld(N, threads)
1366
The compiled kernel is callable, and we can pass the computed launch configuration as keyword arguments:
fill!(y_d, 2)
+kernel(y_d, x_d; threads, blocks)
+@test all(Array(y_d) .== 3.0f0)
Test Passed
Now let's benchmark this:
function bench_gpu4!(y, x)
+ kernel = @cuda launch=false gpu_add3!(y, x)
+ config = launch_configuration(kernel.fun)
+ threads = min(length(y), config.threads)
+ blocks = cld(length(y), threads)
+
+ CUDA.@sync begin
+ kernel(y, x; threads, blocks)
+ end
+end
bench_gpu4! (generic function with 1 method)
@btime bench_gpu4!($y_d, $x_d)
70.826 μs (99 allocations: 3.44 KiB)
A comparable performance; slightly slower due to the use of the occupancy API, but that will not matter with more complex kernels.
When debugging, it's not uncommon to want to print some values. This is achieved with @cuprint
:
function gpu_add2_print!(y, x)
+ index = threadIdx().x # this example only requires linear indexing, so just use `x`
+ stride = blockDim().x
+ @cuprintln("thread $index, block $stride")
+ for i = index:stride:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return nothing
+end
+
+@cuda threads=16 gpu_add2_print!(y_d, x_d)
+synchronize()
thread 1, block 16
+thread 2, block 16
+thread 3, block 16
+thread 4, block 16
+thread 5, block 16
+thread 6, block 16
+thread 7, block 16
+thread 8, block 16
+thread 9, block 16
+thread 10, block 16
+thread 11, block 16
+thread 12, block 16
+thread 13, block 16
+thread 14, block 16
+thread 15, block 16
+thread 16, block 16
Note that the printed output is only generated when synchronizing the entire GPU with synchronize()
. This is similar to CUDA.@sync
, and is the counterpart of cudaDeviceSynchronize
in CUDA C++.
The final topic of this intro concerns the handling of errors. Note that the kernels above used @inbounds
, but did not check whether y
and x
have the same length. If your kernel does not respect these bounds, you will run into nasty errors:
ERROR: CUDA error: an illegal memory access was encountered (code #700, ERROR_ILLEGAL_ADDRESS)
+Stacktrace:
+ [1] ...
If you remove the @inbounds
annotation, instead you get
ERROR: a exception was thrown during kernel execution.
+ Run Julia on debug level 2 for device stack traces.
As the error message mentions, a higher level of debug information will result in a more detailed report. Let's run the same code with with -g2
:
ERROR: a exception was thrown during kernel execution.
+Stacktrace:
+ [1] throw_boundserror at abstractarray.jl:484
+ [2] checkbounds at abstractarray.jl:449
+ [3] setindex! at /home/tbesard/Julia/CUDA/src/device/array.jl:79
+ [4] some_kernel at /tmp/tmpIMYANH:6
On older GPUs (with a compute capability below sm_70
) these errors are fatal, and effectively kill the CUDA environment. On such GPUs, it's often a good idea to perform your "sanity checks" using code that runs on the CPU and only turn over the computation to the GPU once you've deemed it to be safe.
Keep in mind that the high-level functionality of CUDA often means that you don't need to worry about writing kernels at such a low level. However, there are many cases where computations can be optimized using clever low-level manipulations. Hopefully, you now feel comfortable taking the plunge.
This page was generated using Literate.jl.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
Always start by profiling your code (see the Profiling page for more details). You first want to analyze your application as a whole, using CUDA.@profile
or NSight Systems, identifying hotspots and bottlenecks. Focusing on these you will want to:
If that isn't sufficient, and you identified a kernel that executes slowly, you can try using NSight Compute to analyze that kernel in detail. Some things to try in order of importance:
Float32
and Int32
instead of 64 bit types like Float64
and Int
/Int64
;while
or for
loops behave identically across the entire warp, and replace if
s that diverge within a warp with ifelse
s;Inlining can reduce register usage and thus speed up kernels. To force inlining of all functions use @cuda always_inline=true
.
The number of threads that can be launched is partly determined by the number of registers a kernel uses. This is due to registers being shared between all threads on a multiprocessor. Setting the maximum number of registers per thread will force less registers to be used which can increase thread count at the expense of having to spill registers into local memory, this may improve performance. To set the max registers to 32 use @cuda maxregs=32
.
Use @fastmath
to use faster versions of common mathematical functions and use @cuda fastmath=true
for even faster square roots.
For further information you can check out these resources.
NVidia's technical blog has a lot of good tips: Pro-Tips, Optimization.
The CUDA C++ Best Practices Guide is relevant for Julia.
The following notebooks also have some good tips: JuliaCon 2021 GPU Workshop, Advanced Julia GPU Training.
Also see the perf folder for some optimised code examples.
Many common operations can throw errors at runtime in Julia, they often do this by branching and calling a function in that branch both of which are slow on GPUs. Using @inbounds
when indexing into arrays will eliminate exceptions due to bounds checking. Note that running code with --check-bounds=yes
(the default for Pkg.test
) will always emit bounds checking. You can also use assume
from the package LLVM.jl to get rid of exceptions, e.g.
using LLVM.Interop
+
+function test(x, y)
+ assume(x > 0)
+ div(y, x)
+end
The assume(x > 0)
tells the compiler that there cannot be a divide by 0 error.
For more information and examples check out Kernel analysis and optimization.
Use 32-bit integers where possible. A common source of register pressure is the use of 64-bit integers when only 32-bits are required. For example, the hardware's indices are 32-bit integers, but Julia's literals are Int64's which results in expressions like blockIdx().x-1
to be promoted to 64-bit integers. To use 32-bit integers we can instead replace the 1
with Int32(1)
or more succintly 1i32
if you run using CUDA: i32
.
To see how much of a difference this makes let's use a kernel introduced in the introductory tutorial for inplace addition.
using CUDA, BenchmarkTools
+
+function gpu_add3!(y, x)
+ index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
+ stride = gridDim().x * blockDim().x
+ for i = index:stride:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return
+end
gpu_add3! (generic function with 1 method)
Now let's see how many registers are used:
x_d = CUDA.fill(1.0f0, 2^28)
+y_d = CUDA.fill(2.0f0, 2^28)
+
+CUDA.registers(@cuda gpu_add3!(y_d, x_d))
29
Our kernel using 32-bit integers is below:
function gpu_add4!(y, x)
+ index = (blockIdx().x - Int32(1)) * blockDim().x + threadIdx().x
+ stride = gridDim().x * blockDim().x
+ for i = index:stride:length(y)
+ @inbounds y[i] += x[i]
+ end
+ return
+end
gpu_add4! (generic function with 1 method)
CUDA.registers(@cuda gpu_add4!(y_d, x_d))
28
So we use one less register by switching to 32 bit integers, for kernels using even more 64 bit integers we would expect to see larger falls in register count.
StepRange
In the previous kernel in the for loop we iterated over index:stride:length(y)
, this is a StepRange
. Unfortunately, constructing a StepRange
is slow as they can throw errors and they contain unnecessary computation when we just want to loop over them. Instead it is faster to use a while loop like so:
function gpu_add5!(y, x)
+ index = (blockIdx().x - Int32(1)) * blockDim().x + threadIdx().x
+ stride = gridDim().x * blockDim().x
+
+ i = index
+ while i <= length(y)
+ @inbounds y[i] += x[i]
+ i += stride
+ end
+ return
+end
gpu_add5! (generic function with 1 method)
The benchmark[1]:
function bench_gpu4!(y, x)
+ kernel = @cuda launch=false gpu_add4!(y, x)
+ config = launch_configuration(kernel.fun)
+ threads = min(length(y), config.threads)
+ blocks = cld(length(y), threads)
+
+ CUDA.@sync kernel(y, x; threads, blocks)
+end
+
+function bench_gpu5!(y, x)
+ kernel = @cuda launch=false gpu_add5!(y, x)
+ config = launch_configuration(kernel.fun)
+ threads = min(length(y), config.threads)
+ blocks = cld(length(y), threads)
+
+ CUDA.@sync kernel(y, x; threads, blocks)
+end
bench_gpu5! (generic function with 1 method)
@btime bench_gpu4!($y_d, $x_d)
76.149 ms (57 allocations: 3.70 KiB)
@btime bench_gpu5!($y_d, $x_d)
75.732 ms (58 allocations: 3.73 KiB)
This benchmark shows there is a only a small performance benefit for this kernel however we can see a big difference in the amount of registers used, recalling that 28 registers were used when using a StepRange
:
CUDA.registers(@cuda gpu_add5!(y_d, x_d))
12
This page was generated using Literate.jl.
always_inline=true
on the @cuda
macro, e.g. @cuda always_inline=true launch=false gpu_add4!(y, x)
.Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
The easiest way to use the GPU's massive parallelism, is by expressing operations in terms of arrays: CUDA.jl provides an array type, CuArray
, and many specialized array operations that execute efficiently on the GPU hardware. In this section, we will briefly demonstrate use of the CuArray
type. Since we expose CUDA's functionality by implementing existing Julia interfaces on the CuArray
type, you should refer to the upstream Julia documentation for more information on these operations.
If you encounter missing functionality, or are running into operations that trigger so-called "scalar iteration", have a look at the issue tracker and file a new issue if there's none. Do note that you can always access the underlying CUDA APIs by calling into the relevant submodule. For example, if parts of the Random interface isn't properly implemented by CUDA.jl, you can look at the CURAND documentation and possibly call methods from the CURAND
submodule directly. These submodules are available after importing the CUDA package.
The CuArray
type aims to implement the AbstractArray
interface, and provide implementations of methods that are commonly used when working with arrays. That means you can construct CuArray
s in the same way as regular Array
objects:
julia> CuArray{Int}(undef, 2)
+2-element CuArray{Int64, 1}:
+ 0
+ 0
+
+julia> CuArray{Int}(undef, (1,2))
+1×2 CuArray{Int64, 2}:
+ 0 0
+
+julia> similar(ans)
+1×2 CuArray{Int64, 2}:
+ 0 0
Copying memory to or from the GPU can be expressed using constructors as well, or by calling copyto!
:
julia> a = CuArray([1,2])
+2-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+
+julia> b = Array(a)
+2-element Vector{Int64}:
+ 1
+ 2
+
+julia> copyto!(b, a)
+2-element Vector{Int64}:
+ 1
+ 2
The real power of programming GPUs with arrays comes from Julia's higher-order array abstractions: Operations that take user code as an argument, and specialize execution on it. With these functions, you can often avoid having to write custom kernels. For example, to perform simple element-wise operations you can use map
or broadcast
:
julia> a = CuArray{Float32}(undef, (1,2));
+
+julia> a .= 5
+1×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 5.0 5.0
+
+julia> map(sin, a)
+1×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ -0.958924 -0.958924
To reduce the dimensionality of arrays, CUDA.jl implements the various flavours of (map)reduce(dim)
:
julia> a = CUDA.ones(2,3)
+2×3 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 1.0 1.0 1.0
+ 1.0 1.0 1.0
+
+julia> reduce(+, a)
+6.0f0
+
+julia> mapreduce(sin, *, a; dims=2)
+2×1 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.59582335
+ 0.59582335
+
+julia> b = CUDA.zeros(1)
+1-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.0
+
+julia> Base.mapreducedim!(identity, +, b, a)
+1×1 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 6.0
To retain intermediate values, you can use accumulate
:
julia> a = CUDA.ones(2,3)
+2×3 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 1.0 1.0 1.0
+ 1.0 1.0 1.0
+
+julia> accumulate(+, a; dims=2)
+2×3 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 1.0 2.0 3.0
+ 1.0 2.0 3.0
Be wary that the operator f
of accumulate
, accumulate!
, scan
and scan!
must be associative since the operation is performed in parallel. That is f(f(a,b)c)
must be equivalent to f(a,f(b,c))
. Accumulating with a non-associative operator on a CuArray
will not produce the same result as on an Array
.
CuArray
s can also be indexed with arrays of boolean values to select items:
julia> a = CuArray([1,2,3])
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+
+julia> a[[false,true,false]]
+1-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 2
Built on top of this, are several functions with higher-level semantics:
julia> a = CuArray([11,12,13])
+3-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 11
+ 12
+ 13
+
+julia> findall(isodd, a)
+2-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 3
+
+julia> findfirst(isodd, a)
+1
+
+julia> b = CuArray([11 12 13; 21 22 23])
+2×3 CuArray{Int64, 2, CUDA.DeviceMemory}:
+ 11 12 13
+ 21 22 23
+
+julia> findmin(b)
+(11, CartesianIndex(1, 1))
+
+julia> findmax(b; dims=2)
+([13; 23;;], CartesianIndex{2}[CartesianIndex(1, 3); CartesianIndex(2, 3);;])
To some extent, CUDA.jl also supports well-known array wrappers from the standard library:
julia> a = CuArray(collect(1:10))
+10-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+
+julia> a = CuArray(collect(1:6))
+6-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+
+julia> b = reshape(a, (2,3))
+2×3 CuArray{Int64, 2, CUDA.DeviceMemory}:
+ 1 3 5
+ 2 4 6
+
+julia> c = view(a, 2:5)
+4-element CuArray{Int64, 1, CUDA.DeviceMemory}:
+ 2
+ 3
+ 4
+ 5
The above contiguous view
and reshape
have been specialized to return new objects of type CuArray
. Other wrappers, such as non-contiguous views or the LinearAlgebra wrappers that will be discussed below, are implemented using their own type (e.g. SubArray
or Transpose
). This can cause problems, as calling methods with these wrapped objects will not dispatch to specialized CuArray
methods anymore. That may result in a call to fallback functionality that performs scalar iteration.
Certain common operations, like broadcast or matrix multiplication, do know how to deal with array wrappers by using the Adapt.jl package. This is still not a complete solution though, e.g. new array wrappers are not covered, and only one level of wrapping is supported. Sometimes the only solution is to materialize the wrapper to a CuArray
again.
Base's convenience functions for generating random numbers are available in the CUDA module as well:
julia> CUDA.rand(2)
+2-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.74021935
+ 0.9209938
+
+julia> CUDA.randn(Float64, 2, 1)
+2×1 CuArray{Float64, 2, CUDA.DeviceMemory}:
+ -0.3893830994647195
+ 1.618410515635752
Behind the scenes, these random numbers come from two different generators: one backed by CURAND, another by kernels defined in CUDA.jl. Operations on these generators are implemented using methods from the Random standard library:
julia> using Random
+
+julia> a = Random.rand(CURAND.default_rng(), Float32, 1)
+1-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.74021935
+
+julia> a = Random.rand!(CUDA.default_rng(), a)
+1-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.46691537
CURAND also supports generating lognormal and Poisson-distributed numbers:
julia> CUDA.rand_logn(Float32, 1, 5; mean=2, stddev=20)
+1×5 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 2567.61 4.256f-6 54.5948 0.00283999 9.81175f22
+
+julia> CUDA.rand_poisson(UInt32, 1, 10; lambda=100)
+1×10 CuArray{UInt32, 2, CUDA.DeviceMemory}:
+ 0x00000058 0x00000066 0x00000061 … 0x0000006b 0x0000005f 0x00000069
Note that these custom operations are only supported on a subset of types.
CUDA's linear algebra functionality from the CUBLAS library is exposed by implementing methods in the LinearAlgebra standard library:
julia> # enable logging to demonstrate a CUBLAS kernel is used
+ CUBLAS.cublasLoggerConfigure(1, 0, 1, C_NULL)
+
+julia> CUDA.rand(2,2) * CUDA.rand(2,2)
+I! cuBLAS (v10.2) function cublasStatus_t cublasSgemm_v2(cublasContext*, cublasOperation_t, cublasOperation_t, int, int, int, const float*, const float*, int, const float*, int, const float*, float*, int) called
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.295727 0.479395
+ 0.624576 0.557361
Certain operations, like the above matrix-matrix multiplication, also have a native fallback written in Julia for the purpose of working with types that are not supported by CUBLAS:
julia> # enable logging to demonstrate no CUBLAS kernel is used
+ CUBLAS.cublasLoggerConfigure(1, 0, 1, C_NULL)
+
+julia> CUDA.rand(Int128, 2, 2) * CUDA.rand(Int128, 2, 2)
+2×2 CuArray{Int128, 2, CUDA.DeviceMemory}:
+ -147256259324085278916026657445395486093 -62954140705285875940311066889684981211
+ -154405209690443624360811355271386638733 -77891631198498491666867579047988353207
Operations that exist in CUBLAS, but are not (yet) covered by high-level constructs in the LinearAlgebra standard library, can be accessed directly from the CUBLAS submodule. Note that you do not need to call the C wrappers directly (e.g. cublasDdot
), as many operations have more high-level wrappers available as well (e.g. dot
):
julia> x = CUDA.rand(2)
+2-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.74021935
+ 0.9209938
+
+julia> y = CUDA.rand(2)
+2-element CuArray{Float32, 1, CUDA.DeviceMemory}:
+ 0.03902049
+ 0.9689629
+
+julia> CUBLAS.dot(2, x, y)
+0.92129254f0
+
+julia> using LinearAlgebra
+
+julia> dot(Array(x), Array(y))
+0.92129254f0
LAPACK-like functionality as found in the CUSOLVER library can be accessed through methods in the LinearAlgebra standard library too:
julia> using LinearAlgebra
+
+julia> a = CUDA.rand(2,2)
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.740219 0.0390205
+ 0.920994 0.968963
+
+julia> a = a * a'
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.549447 0.719547
+ 0.719547 1.78712
+
+julia> cholesky(a)
+Cholesky{Float32, CuArray{Float32, 2, CUDA.DeviceMemory}}
+U factor:
+2×2 UpperTriangular{Float32, CuArray{Float32, 2, CUDA.DeviceMemory}}:
+ 0.741247 0.970725
+ ⋅ 0.919137
Other operations are bound to the left-division operator:
julia> a = CUDA.rand(2,2)
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.740219 0.0390205
+ 0.920994 0.968963
+
+julia> b = CUDA.rand(2,2)
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.925141 0.667319
+ 0.44635 0.109931
+
+julia> a \ b
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 1.29018 0.942773
+ -0.765663 -0.782648
+
+julia> Array(a) \ Array(b)
+2×2 Matrix{Float32}:
+ 1.29018 0.942773
+ -0.765663 -0.782648
Sparse array functionality from the CUSPARSE library is mainly available through functionality from the SparseArrays package applied to CuSparseArray
objects:
julia> using SparseArrays
+
+julia> x = sprand(10,0.2)
+10-element SparseVector{Float64, Int64} with 5 stored entries:
+ [2 ] = 0.538639
+ [4 ] = 0.89699
+ [6 ] = 0.258478
+ [7 ] = 0.338949
+ [10] = 0.424742
+
+julia> using CUDA.CUSPARSE
+
+julia> d_x = CuSparseVector(x)
+10-element CuSparseVector{Float64, Int32} with 5 stored entries:
+ [2 ] = 0.538639
+ [4 ] = 0.89699
+ [6 ] = 0.258478
+ [7 ] = 0.338949
+ [10] = 0.424742
+
+julia> nonzeros(d_x)
+5-element CuArray{Float64, 1, CUDA.DeviceMemory}:
+ 0.538639413965653
+ 0.8969897902567084
+ 0.25847781536337067
+ 0.3389490517221738
+ 0.4247416640213063
+
+julia> nnz(d_x)
+5
For 2-D arrays the CuSparseMatrixCSC
and CuSparseMatrixCSR
can be used.
Non-integrated functionality can be access directly in the CUSPARSE submodule again.
Functionality from CUFFT is integrated with the interfaces from the AbstractFFTs.jl package:
julia> a = CUDA.rand(2,2)
+2×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
+ 0.740219 0.0390205
+ 0.920994 0.968963
+
+julia> using CUDA.CUFFT
+
+julia> fft(a)
+2×2 CuArray{ComplexF32, 2, CUDA.DeviceMemory}:
+ 2.6692+0.0im 0.65323+0.0im
+ -1.11072+0.0im 0.749168+0.0im
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
A crucial aspect of working with a GPU is managing the data on it. The CuArray
type is the primary interface for doing so: Creating a CuArray
will allocate data on the GPU, copying elements to it will upload, and converting back to an Array
will download values to the CPU:
# generate some data on the CPU
+cpu = rand(Float32, 1024)
+
+# allocate on the GPU
+gpu = CuArray{Float32}(undef, 1024)
+
+# copy from the CPU to the GPU
+copyto!(gpu, cpu)
+
+# download and verify
+@test cpu == Array(gpu)
A shorter way to accomplish these operations is to call the copy constructor, i.e. CuArray(cpu)
.
In many cases, you might not want to convert your input data to a dense CuArray
. For example, with array wrappers you will want to preserve that wrapper type on the GPU and only upload the contained data. The Adapt.jl package does exactly that, and contains a list of rules on how to unpack and reconstruct types like array wrappers so that we can preserve the type when, e.g., uploading data to the GPU:
julia> cpu = Diagonal([1,2]) # wrapped data on the CPU
+2×2 Diagonal{Int64,Array{Int64,1}}:
+ 1 ⋅
+ ⋅ 2
+
+julia> using Adapt
+
+julia> gpu = adapt(CuArray, cpu) # upload to the GPU, keeping the wrapper intact
+2×2 Diagonal{Int64,CuArray{Int64,1,Nothing}}:
+ 1 ⋅
+ ⋅ 2
Since this is a very common operation, the cu
function conveniently does this for you:
julia> cu(cpu)
+2×2 Diagonal{Float32,CuArray{Float32,1,Nothing}}:
+ 1.0 ⋅
+ ⋅ 2.0
The cu
function is opinionated and converts input most floating-point scalars to Float32
. This is often a good call, as Float64
and many other scalar types perform badly on the GPU. If this is unwanted, use adapt
directly.
The CuArray
constructor and the cu
function default to allocating device memory, which can be accessed only from the GPU. It is also possible to allocate unified memory, which is accessible from both the CPU and GPU with the driver taking care of data movement:
julia> cpu = [1,2]
+2-element Vector{Int64}:
+ 1
+ 2
+
+julia> gpu = CuVector{Int,CUDA.UnifiedMemory}(cpu)
+2-element CuArray{Int64, 1, CUDA.UnifiedMemory}:
+ 1
+ 2
+
+julia> gpu = cu(cpu; unified=true)
+2-element CuArray{Int64, 1, CUDA.UnifiedMemory}:
+ 1
+ 2
Using unified memory has several advantages: it is possible to allocate more memory than the GPU has available, and the memory can be accessed efficiently from the CPU, either directly or by wrapping the CuArray
using an Array
:
julia> gpu[1] # no scalar indexing error!
+1
+
+julia> cpu_again = unsafe_wrap(Array, gpu)
+2-element Vector{Int64}:
+ 1
+ 2
This may make it significantly easier to port code to the GPU, as you can incrementally port parts of your application without having to worry about executing CPU code, or triggering an AbstractArray
fallback. It may come at a cost however, as unified memory needs to be paged in and out of the GPU memory, and cannot be allocated asynchronously. To alleviate this cost, CUDA.jl automatically prefetches unified memory when passing it to a kernel.
On recent systems (CUDA 12.2 with the open-source NVIDIA driver) it is also possible to do the reverse, and access CPU memory from the GPU without having to explicitly allocate unified memory using the CuArray
constructor or cu
function:
julia> cpu = [1,2];
+
+julia> gpu = unsafe_wrap(CuArray, cpu)
+2-element CuArray{Int64, 1, CUDA.UnifiedMemory}:
+ 1
+ 2
+
+julia> gpu .+= 1;
+
+julia> cpu
+2-element Vector{Int64}:
+ 2
+ 3
Right now, CUDA.jl still defaults to allocating device memory, but this may change in the future. If you want to change the default behavior, you can set the default_memory
preference to unified
or host
instead of device
.
Instances of the CuArray
type are managed by the Julia garbage collector. This means that they will be collected once they are unreachable, and the memory hold by it will be repurposed or freed. There is no need for manual memory management, just make sure your objects are not reachable (i.e., there are no instances or references).
Behind the scenes, a memory pool will hold on to your objects and cache the underlying memory to speed up future allocations. As a result, your GPU might seem to be running out of memory while it isn't. When memory pressure is high, the pool will automatically free cached objects:
julia> CUDA.pool_status() # initial state
+Effective GPU memory usage: 16.12% (2.537 GiB/15.744 GiB)
+Memory pool usage: 0 bytes (0 bytes reserved)
+
+julia> a = CuArray{Int}(undef, 1024); # allocate 8KB
+
+julia> CUDA.pool_status()
+Effective GPU memory usage: 16.35% (2.575 GiB/15.744 GiB)
+Memory pool usage: 8.000 KiB (32.000 MiB reserved)
+
+julia> a = nothing; GC.gc(true)
+
+julia> CUDA.pool_status() # 8KB is now cached
+Effective GPU memory usage: 16.34% (2.573 GiB/15.744 GiB)
+Memory pool usage: 0 bytes (32.000 MiB reserved)
+
If for some reason you need all cached memory to be reclaimed, call CUDA.reclaim()
:
julia> CUDA.reclaim()
+
+julia> CUDA.pool_status()
+Effective GPU memory usage: 16.17% (2.546 GiB/15.744 GiB)
+Memory pool usage: 0 bytes (0 bytes reserved)
It should never be required to manually reclaim memory before performing any high-level GPU array operation: Functionality that allocates should itself call into the memory pool and free any cached memory if necessary. It is a bug if that operation runs into an out-of-memory situation only if not manually reclaiming memory beforehand.
If you need to disable the memory pool, e.g. because of incompatibility with certain CUDA APIs, set the environment variable JULIA_CUDA_MEMORY_POOL
to none
before importing CUDA.jl.
If you're sharing a GPU with other users or applications, you might want to limit how much memory is used. By default, CUDA.jl will configure the memory pool to use all available device memory. You can change this using two environment variables:
JULIA_CUDA_SOFT_MEMORY_LIMIT
: This is an advisory limit, used to configure the memory pool. If you set this to a nonzero value, the memory pool will attempt to release cached memory until memory use falls below this limit. Note that this only happens at specific synchronization points, so memory use may temporarily exceed this limit. In addition, this limit is incompatible with JULIA_CUDA_MEMORY_POOL=none
.JULIA_CUDA_HARD_MEMORY_LIMIT
: This is a hard limit, checked before every allocation. On older versions of CUDA, before v12.2, this is a relatively expensive limit, so it is recommended to first try to use the soft limit.The value of these variables can be formatted as a numer of bytes, optionally followed by a unit, or as a percentage of the total device memory. Examples: 100M
, 50%
, 1.5GiB
, 10000
.
When your application performs a lot of memory operations, the time spent during GC might increase significantly. This happens more often than it does on the CPU because GPUs tend to have smaller memories and more frequently run out of it. When that happens, CUDA invokes the Julia garbage collector, which then needs to scan objects to see if they can be freed to get back some GPU memory.
To avoid having to depend on the Julia GC to free up memory, you can directly inform CUDA.jl when an allocation can be freed (or reused) by calling the unsafe_free!
method. Once you've done so, you cannot use that array anymore:
julia> a = CuArray([1])
+1-element CuArray{Int64,1,Nothing}:
+ 1
+
+julia> CUDA.unsafe_free!(a)
+
+julia> a
+1-element CuArray{Int64,1,Nothing}:
+Error showing value of type CuArray{Int64,1,Nothing}:
+ERROR: AssertionError: Use of freed memory
If you are dealing with data sets that are too large to fit on the GPU all at once, you can use CuIterator
to batch operations:
julia> batches = [([1], [2]), ([3], [4])]
+
+julia> for (batch, (a,b)) in enumerate(CuIterator(batches))
+ println("Batch $batch: ", a .+ b)
+ end
+Batch 1: [3]
+Batch 2: [7]
For each batch, every argument (assumed to be an array-like) is uploaded to the GPU using the adapt
mechanism from above. Afterwards, the memory is eagerly put back in the CUDA memory pool using unsafe_free!
to lower GC pressure.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
There are different ways of working with multiple GPUs: using one or more tasks, processes, or systems. Although all of these are compatible with the Julia CUDA toolchain, the support is a work in progress and the usability of some combinations can be significantly improved.
The easiest solution that maps well onto Julia's existing facilities for distributed programming, is to use one GPU per process
# spawn one worker per device
+using Distributed, CUDA
+addprocs(length(devices()))
+@everywhere using CUDA
+
+# assign devices
+asyncmap((zip(workers(), devices()))) do (p, d)
+ remotecall_wait(p) do
+ @info "Worker $p uses $d"
+ device!(d)
+ end
+end
Communication between nodes should happen via the CPU (the CUDA IPC APIs are available as CUDA.cuIpcOpenMemHandle
and friends, but not available through high-level wrappers).
Alternatively, one can use MPI.jl together with an CUDA-aware MPI implementation. In that case, CuArray
objects can be passed as send and receive buffers to point-to-point and collective operations to avoid going through the CPU.
In a similar vein to the multi-process solution, one can work with multiple devices from within a single process by calling CUDA.device!
to switch to a specific device. Furthermore, as the active device is a task-local property you can easily work with multiple devices using one task per device. For more details, refer to the section on Tasks and threads.
You currently need to re-set the device at the start of every task, i.e., call device!
as one of the first statement in your @async
or @spawn
block:
@sync begin
+ @async begin
+ device!(0)
+ # do work on GPU 0 here
+ end
+ @async begin
+ device!(1)
+ # do work on GPU 1 here
+ end
+end
Without this, the newly-created task would use the same device as the previously-executing task, and not the parent task as could be expected. This is expected to be improved in the future using context variables.
When working with multiple devices, you need to be careful with allocated memory: Allocations are tied to the device that was active when requesting the memory, and cannot be used with another device. That means you cannot allocate a CuArray
, switch devices, and use that object. Similar restrictions apply to library objects, like CUFFT plans.
To avoid this difficulty, you can use unified memory that is accessible from all devices:
using CUDA
+
+gpus = Int(length(devices()))
+
+# generate CPU data
+dims = (3,4,gpus)
+a = round.(rand(Float32, dims) * 100)
+b = round.(rand(Float32, dims) * 100)
+
+# allocate and initialize GPU data
+d_a = cu(a; unified=true)
+d_b = cu(b; unified=true)
+d_c = similar(d_a)
The data allocated here uses the GPU id as a the outermost dimension, which can be used to extract views of contiguous memory that represent the slice to be processed by a single GPU:
for (gpu, dev) in enumerate(devices())
+ device!(dev)
+ @views d_c[:, :, gpu] .= d_a[:, :, gpu] .+ d_b[:, :, gpu]
+end
Before downloading the data, make sure to synchronize the devices:
for dev in devices()
+ # NOTE: normally you'd use events and wait for them
+ device!(dev)
+ synchronize()
+end
+
+using Test
+c = Array(d_c)
+@test a+b ≈ c
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
CUDA.jl can be used with Julia tasks and threads, offering a convenient way to work with multiple devices, or to perform independent computations that may execute concurrently on the GPU.
Each Julia task gets its own local CUDA execution environment, with its own stream, library handles, and active device selection. That makes it easy to use one task per device, or to use tasks for independent operations that can be overlapped. At the same time, it's important to take care when sharing data between tasks.
For example, let's take some dummy expensive computation and execute it from two tasks:
# an expensive computation
+function compute(a, b)
+ c = a * b # library call
+ broadcast!(sin, c, c) # Julia kernel
+ c
+end
+
+function run(a, b)
+ results = Vector{Any}(undef, 2)
+
+ # computation
+ @sync begin
+ @async begin
+ results[1] = Array(compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ @async begin
+ results[2] = Array(compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ end
+
+ # comparison
+ results[1] == results[2]
+end
We use familiar Julia constructs to create two tasks and re-synchronize afterwards (@async
and @sync
), while the dummy compute
function demonstrates both the use of a library (matrix multiplication uses CUBLAS) and a native Julia kernel. The function is passed three GPU arrays filled with random numbers:
function main(N=1024)
+ a = CUDA.rand(N,N)
+ b = CUDA.rand(N,N)
+
+ # make sure this data can be used by other tasks!
+ synchronize()
+
+ run(a, b)
+end
The main
function illustrates how we need to take care when sharing data between tasks: GPU operations typically execute asynchronously, queued on an execution stream, so if we switch tasks and thus switch execution streams we need to synchronize()
to ensure the data is actually available.
Using Nsight Systems, we can visualize the execution of this example:
You can see how the two invocations of compute
resulted in overlapping execution. The memory copies, however, were executed in serial. This is expected: Regular CPU arrays cannot be used for asynchronous operations, because their memory is not page-locked. For most applications, this does not matter as the time to compute will typically be much larger than the time to copy memory.
If your application needs to perform many copies between the CPU and GPU, it might be beneficial to "pin" the CPU memory so that asynchronous memory copies are possible. This operation is expensive though, and should only be used if you can pre-allocate and re-use your CPU buffers. Applied to the previous example:
function run(a, b)
+ results = Vector{Any}(undef, 2)
+
+ # pre-allocate and pin destination CPU memory
+ results[1] = CUDA.pin(Array{eltype(a)}(undef, size(a)))
+ results[2] = CUDA.pin(Array{eltype(a)}(undef, size(a)))
+
+ # computation
+ @sync begin
+ @async begin
+ copyto!(results[1], compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ @async begin
+ copyto!(results[2], compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ end
+
+ # comparison
+ results[1] == results[2]
+end
The profile reveals that the memory copies themselves could not be overlapped, but the first copy was executed while the GPU was still active with the second round of computations. Furthermore, the copies executed much quicker – if the memory were unpinned, it would first have to be staged to a pinned CPU buffer anyway.
Use of tasks can be easily extended to multiple threads with functionality from the Threads standard library:
function run(a, b)
+ results = Vector{Any}(undef, 2)
+
+ # computation
+ @sync begin
+ Threads.@spawn begin
+ results[1] = Array(compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ Threads.@spawn begin
+ results[2] = Array(compute(a,b))
+ nothing # JuliaLang/julia#40626
+ end
+ end
+
+ # comparison
+ results[1] == results[2]
+end
By using the Threads.@spawn
macro, the tasks will be scheduled to be run on different CPU threads. This can be useful when you are calling a lot of operations that "block" in CUDA, e.g., memory copies to or from unpinned memory. The same result will occur when using a Threads.@threads for ... end
block. Generally, though, operations that synchronize GPU execution (including the call to synchronize
itself) are implemented in a way that they yield back to the Julia scheduler, to enable concurrent execution without requiring the use of different CPU threads.
Use of multiple threads with CUDA.jl is a recent addition, and there may still be bugs or performance issues.
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
The CUDA.jl package provides three distinct, but related, interfaces for CUDA programming:
CuArray
type: for programming with arrays;Much of the Julia CUDA programming stack can be used by just relying on the CuArray
type, and using platform-agnostic programming patterns like broadcast
and other array abstractions. Only once you hit a performance bottleneck, or some missing functionality, you might need to write a custom kernel or use the underlying CUDA APIs.
CuArray
typeThe CuArray
type is an essential part of the toolchain. Primarily, it is used to manage GPU memory, and copy data from and back to the CPU:
a = CuArray{Int}(undef, 1024)
+
+# essential memory operations, like copying, filling, reshaping, ...
+b = copy(a)
+fill!(b, 0)
+@test b == CUDA.zeros(Int, 1024)
+
+# automatic memory management
+a = nothing
Beyond memory management, there are a whole range of array operations to process your data. This includes several higher-order operations that take other code as arguments, such as map
, reduce
or broadcast
. With these, it is possible to perform kernel-like operations without actually writing your own GPU kernels:
a = CUDA.zeros(1024)
+b = CUDA.ones(1024)
+a.^2 .+ sin.(b)
When possible, these operations integrate with existing vendor libraries such as CUBLAS and CURAND. For example, multiplying matrices or generating random numbers will automatically dispatch to these high-quality libraries, if types are supported, and fall back to generic implementations otherwise.
For more details, refer to the section on Array programming.
@cuda
If an operation cannot be expressed with existing functionality for CuArray
, or you need to squeeze every last drop of performance out of your GPU, you can always write a custom kernel. Kernels are functions that are executed in a massively parallel fashion, and are launched by using the @cuda
macro:
a = CUDA.zeros(1024)
+
+function kernel(a)
+ i = threadIdx().x
+ a[i] += 1
+ return
+end
+
+@cuda threads=length(a) kernel(a)
These kernels give you all the flexibility and performance a GPU has to offer, within a familiar language. However, not all of Julia is supported: you (generally) cannot allocate memory, I/O is disallowed, and badly-typed code will not compile. As a general rule of thumb, keep kernels simple, and only incrementally port code while continuously verifying that it still compiles and executes as expected.
For more details, refer to the section on Kernel programming.
For advanced use of the CUDA, you can use the driver API wrappers in CUDA.jl. Common operations include synchronizing the GPU, inspecting its properties, using events, etc. These operations are low-level, but for your convenience wrapped using high-level constructs. For example:
CUDA.@elapsed begin
+ # code that will be timed using CUDA events
+end
+
+# or
+
+for device in CUDA.devices()
+ @show capability(device)
+end
If such high-level wrappers are missing, you can always access the underling C API (functions and structures prefixed with cu
) without having to ever exit Julia:
version = Ref{Cint}()
+CUDA.cuDriverGetVersion(version)
+@show version[]
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.
A typical approach for porting or developing an application for the GPU is as follows:
Array
typeCuArray
typeMany array operations in Julia are implemented using loops, processing one element at a time. Doing so with GPU arrays is very ineffective, as the loop won't actually execute on the GPU, but transfer one element at a time and process it on the CPU. As this wrecks performance, you will be warned when performing this kind of iteration:
julia> a = CuArray([1])
+1-element CuArray{Int64,1,Nothing}:
+ 1
+
+julia> a[1] += 1
+┌ Warning: Performing scalar indexing.
+│ ...
+└ @ GPUArrays ~/Julia/pkg/GPUArrays/src/host/indexing.jl:57
+2
Scalar indexing is only allowed in an interactive session, e.g. the REPL, because it is convenient when porting CPU code to the GPU. If you want to disallow scalar indexing, e.g. to verify that your application executes correctly on the GPU, call the allowscalar
function:
julia> CUDA.allowscalar(false)
+
+julia> a[1] .+ 1
+ERROR: scalar getindex is disallowed
+Stacktrace:
+ [1] error(::String) at ./error.jl:33
+ [2] assertscalar(::String) at GPUArrays/src/indexing.jl:14
+ [3] getindex(::CuArray{Int64,1,Nothing}, ::Int64) at GPUArrays/src/indexing.jl:54
+ [4] top-level scope at REPL[5]:1
+
+julia> a .+ 1
+1-element CuArray{Int64,1,Nothing}:
+ 2
In a non-interactive session, e.g. when running code from a script or application, scalar indexing is disallowed by default. There is no global toggle to allow scalar indexing; if you really need it, you can mark expressions using allowscalar
with do-block syntax or @allowscalar
macro:
julia> a = CuArray([1])
+1-element CuArray{Int64, 1}:
+ 1
+
+julia> CUDA.allowscalar(false)
+
+julia> CUDA.allowscalar() do
+ a[1] += 1
+ end
+2
+
+julia> CUDA.@allowscalar a[1] += 1
+3
Settings
This document was generated with Documenter.jl version 1.4.0 on Monday 16 September 2024. Using Julia version 1.10.5.