diff --git a/Project.toml b/Project.toml index 94b018ff58..76566933f5 100644 --- a/Project.toml +++ b/Project.toml @@ -34,7 +34,7 @@ CEnum = "0.2, 0.3, 0.4" ExprTools = "0.1" GPUArrays = "8" GPUCompiler = "0.13.0" -LLVM = "4.1.1" +LLVM = "4.5.1" Random123 = "1.2" RandomNumbers = "1.5.3" Reexport = "0.2, 1.0" diff --git a/lib/cudadrv/execution.jl b/lib/cudadrv/execution.jl index a858fe5a83..2123e2bd0c 100644 --- a/lib/cudadrv/execution.jl +++ b/lib/cudadrv/execution.jl @@ -76,9 +76,13 @@ end # convert the argument values to match the kernel's signature (specified by the user) # (this mimics `lower-ccall` in julia-syntax.scm) -@inline @generated function convert_arguments(f::Function, ::Type{tt}, args...) where {tt} - types = tt.parameters +@inline @generated function convert_arguments(f::F, ::Type{T}, args...) where {F,T} + types = T.parameters + convert_argument_exprs(types, args, :f) +end +# version without a closure for use in generated functions +function convert_argument_exprs(types, args, f, extra_args...) ex = quote end converted_args = Vector{Symbol}(undef, length(args)) @@ -92,11 +96,11 @@ end append!(ex.args, (quote GC.@preserve $(converted_args...) begin - f($(arg_ptrs...)) + $f($(extra_args...), $(arg_ptrs...)) end end).args) - return ex + ex end """ diff --git a/src/CUDA.jl b/src/CUDA.jl index c5cebd71d6..a98b681fb4 100644 --- a/src/CUDA.jl +++ b/src/CUDA.jl @@ -58,6 +58,8 @@ include("device/utils.jl") include("device/pointer.jl") include("device/array.jl") include("device/intrinsics.jl") +include("device/hostcall.jl") +include("device/output.jl") include("device/runtime.jl") include("device/texture.jl") include("device/random.jl") @@ -75,8 +77,9 @@ export CUPTI, NVTX # compiler implementation include("compiler/gpucompiler.jl") -include("compiler/execution.jl") include("compiler/exceptions.jl") +include("compiler/hostcall.jl") +include("compiler/execution.jl") include("compiler/reflection.jl") # array implementation diff --git a/src/compiler/exceptions.jl b/src/compiler/exceptions.jl index 4596431eb8..a2a31796e7 100644 --- a/src/compiler/exceptions.jl +++ b/src/compiler/exceptions.jl @@ -17,19 +17,16 @@ const exception_flags = Dict{CuContext, Mem.HostBuffer}() # create a CPU/GPU exception flag for error signalling, and put it in the module function create_exceptions!(mod::CuModule) - flag_ptr = CuGlobal{Ptr{Cvoid}}(mod, "exception_flag") exception_flag = get!(exception_flags, mod.ctx, - Mem.alloc(Mem.Host, sizeof(Int), Mem.HOSTALLOC_DEVICEMAP)) - flag_ptr[async=true] = reinterpret(Ptr{Cvoid}, convert(CuPtr{Cvoid}, exception_flag)) - - return + Mem.alloc(Mem.Host, sizeof(Int8), Mem.HOSTALLOC_DEVICEMAP)) + return reinterpret(LLVMPtr{Int8, AS.Global}, convert(CuPtr{Int8}, exception_flag)) end # check the exception flags on every API call, similarly to how CUDA handles errors function check_exceptions() for (ctx,buf) in exception_flags if isvalid(ctx) - ptr = convert(Ptr{Int}, buf) + ptr = convert(Ptr{Int8}, buf) flag = unsafe_load(ptr) if flag != 0 unsafe_store!(ptr, 0) diff --git a/src/compiler/execution.jl b/src/compiler/execution.jl index dab5fbe40c..078c1ac9a6 100644 --- a/src/compiler/execution.jl +++ b/src/compiler/execution.jl @@ -193,6 +193,10 @@ AbstractKernel end end + # add the kernel state + pushfirst!(call_t, KernelState) + pushfirst!(call_args, :(kernel.state)) + # finalize types call_tt = Base.to_tuple_type(call_t) @@ -209,6 +213,7 @@ struct HostKernel{F,TT} <: AbstractKernel{F,TT} ctx::CuContext mod::CuModule fun::CuFunction + state::KernelState end @doc (@doc AbstractKernel) HostKernel @@ -446,13 +451,14 @@ end mod = @timeit_ci "CuModule" CuModule(compiled.image) fun = CuFunction(mod, compiled.entry) - # initialize and register the exception flag, if any - if "exception_flag" in compiled.external_gvars - create_exceptions!(mod) - filter!(!isequal("exception_flag"), compiled.external_gvars) - end + # create the kernel state object + exception_ptr = create_exceptions!(mod) + pool = hostcall_pool(ctx) + state = KernelState(exception_ptr, + reinterpret(LLVMPtr{UInt32, AS.Global}, pointer(pool.pointers)), + reinterpret(LLVMPtr{Hostcall, AS.Global}, pointer(pool.calls))) - return HostKernel{typeof(job.source.f),job.source.tt}(job.source.f, ctx, mod, fun) + return HostKernel{typeof(job.source.f),job.source.tt}(job.source.f, ctx, mod, fun, state) end function (kernel::HostKernel)(args...; threads::CuDim=1, blocks::CuDim=1, kwargs...) @@ -465,6 +471,7 @@ end struct DeviceKernel{F,TT} <: AbstractKernel{F,TT} f::F fun::CuDeviceFunction + state::KernelState end @doc (@doc AbstractKernel) DeviceKernel @@ -483,7 +490,7 @@ No keyword arguments are supported. @inline function dynamic_cufunction(f::F, tt::Type=Tuple{}) where {F <: Function} fptr = GPUCompiler.deferred_codegen(Val(f), Val(tt)) fun = CuDeviceFunction(fptr) - DeviceKernel{F,tt}(f, fun) + DeviceKernel{F,tt}(f, fun, kernel_state()) end (kernel::DeviceKernel)(args...; kwargs...) = call(kernel, args...; kwargs...) diff --git a/src/compiler/gpucompiler.jl b/src/compiler/gpucompiler.jl index f852c9dac9..d08ee79beb 100644 --- a/src/compiler/gpucompiler.jl +++ b/src/compiler/gpucompiler.jl @@ -74,3 +74,5 @@ end GPUCompiler.ci_cache(@nospecialize(job::CUDACompilerJob)) = ci_cache GPUCompiler.method_table(@nospecialize(job::CUDACompilerJob)) = method_table + +GPUCompiler.kernel_state_type(job::CUDACompilerJob) = KernelState diff --git a/src/compiler/hostcall.jl b/src/compiler/hostcall.jl new file mode 100644 index 0000000000..4bb8537340 --- /dev/null +++ b/src/compiler/hostcall.jl @@ -0,0 +1,148 @@ +# host-side functionality for receiving method calls from the GPU + +const HOSTCALL_POOL_SIZE = UInt32(1024*16) # ~64MB +# ring buffer helpers assume pow2 +@assert ispow2(HOSTCALL_POOL_SIZE) +# we should be able to request slots for a full warp, or we would deadlock +@assert HOSTCALL_POOL_SIZE >= 32 +# head and tail pointers can exceed HOSTCALL_POOL_SIZE, so overflow behaviour should match +@assert (typemax(UInt32)+1)%HOSTCALL_POOL_SIZE == 0 + +struct HostcallPool + context::CuContext + + # mapped host storage for ring buffer pointers + # + # we can't perform operations that are atomic wrt. both the CPU and GPU, only wrt. to + # a single device, but that's okay as the tail pointer is only moved by the CPU, while + # the head pointer is only moved by the GPU. stale reads from either device will only + # result in under-estimated capacities. + pointer_buf::Mem.HostBuffer + pointers::Vector{UInt32} # [head, tail], 0-indexed for simplified modulo arithmetic + + # mapped host storage for actual hostcall objects + call_buf::Mem.HostBuffer + calls::Vector{Hostcall} +end + +# small helpers for pow2 ring buffer management. +# - the head is where the producer inserts, the tail is where the consumer reads +# - tail == head indicates an empty buffer +# - head and tail pointers can be 0 or 1 indexed, and do not need to fall within size bounds +ring_count(head, tail, size) = (head - tail) & (size-1) +ring_space(head, tail, size) = ring_count(tail, head+1, size) +# NOTE: one item is left unused, as a full buffer means head==tail which also means empty + +# create and return the hostcall pool for each context +const hostcall_pools = Dict{CuContext, HostcallPool}() +hostcall_pool(ctx::CuContext) = get!(hostcall_pools, ctx) do + @context! ctx begin + # NOTE: we allocate the host memory manually, instead of just registering an array, + # to avoid accidentally re-registering a memory range. + pointer_buf = Mem.alloc(Mem.Host, 2*sizeof(UInt32), Mem.HOSTALLOC_DEVICEMAP) + pointer_ptr = convert(Ptr{UInt32}, pointer_buf) + pointers = unsafe_wrap(Array, pointer_ptr, 2) + fill!(pointers, 0) + + call_buf = Mem.alloc(Mem.Host, HOSTCALL_POOL_SIZE*sizeof(Hostcall), Mem.HOSTALLOC_DEVICEMAP) + call_ptr = convert(Ptr{Hostcall}, call_buf) + calls = unsafe_wrap(Array, call_ptr, HOSTCALL_POOL_SIZE) + + pool = HostcallPool(ctx, pointer_buf, pointers, call_buf, calls) + marker = Threads.Atomic{Int}(0) + + watcher = @async begin + while isvalid(ctx) + Base.invokelatest(check_hostcalls, pool) + marker[] = 1 + sleep(0.1) + end + end + VERSION >= v"1.7-" && errormonitor(watcher) + + hostcall_markers[ctx] = marker + return pool + end +end + +# wait for all hostcalls to complete. +# XXX: add to `synchronize()`? +const hostcall_markers = Dict{CuContext, Threads.Atomic{Int}}() +function hostcall_synchronize(ctx::CuContext=context()) + haskey(hostcall_pools, ctx) || return + marker = hostcall_markers[ctx] + marker[] = 0 + while marker[] == 0 + sleep(0.1) + end + return +end + +# check whether a pool has any outstanding hostcalls, and execute them +function check_hostcalls(pool::HostcallPool) + head0, tail0 = pool.pointers + while ring_count(head0, tail0, HOSTCALL_POOL_SIZE) >= 1 + slot = tail0 & (HOSTCALL_POOL_SIZE - 0x1) + 0x1 + hostcall = pool.calls[slot] + hostcall_ptr = pointer(pool.calls, slot) + + if hostcall.state == HOSTCALL_SUBMITTED + # Setfield.jl chokes on the 4k tuple, so we manually create pointers to fields. + state_ptr = reinterpret(Ptr{HostcallState}, hostcall_ptr) + fieldoffset(Hostcall, 1) + buffer_ptr = hostcall_ptr + fieldoffset(Hostcall, fieldcount(Hostcall)) + + try + sig, rettyp = hostcall_targets[hostcall.target] + # function barrier for specialization + state = process_hostcall(sig, rettyp, buffer_ptr) + unsafe_store!(state_ptr, state) + catch ex + Base.display_error(ex, catch_backtrace()) + unsafe_store!(state_ptr, HOSTCALL_READY) + end + end + + tail0 += 0x1 + pool.pointers[2] = tail0 + end +end + +@inline @generated function read_hostcall_arguments(ptr, sig) + args = [] + last_offset = 0 + for typ in sig.parameters + sz = sizeof(typ) + arg = if sz > 0 + align = Base.datatype_alignment(typ) + offset = Base.cld(last_offset, align) * align + last_offset = offset + sz + if last_offset > HOSTCALL_BUFFER_SIZE + return :(error("hostcall arguments exceed maximum buffer size")) + end + :(unsafe_load(reinterpret(Ptr{$typ}, ptr+$offset))) + else + :($(typ.instance)) + end + push!(args, arg) + end + + quote + ($(args...)) + end +end + +@noinline function process_hostcall(sig::Type{T}, rettyp::Type{U}, buffer_ptr) where {T,U} + f, args... = read_hostcall_arguments(buffer_ptr, sig) + rv = Base.invokelatest(f, args...)::rettyp + + if rettyp === Nothing + HOSTCALL_READY + else + # store the return type + if sizeof(rettyp) > HOSTCALL_BUFFER_SIZE + error("hostcall return value exceeds maximum buffer size") + end + unsafe_store!(reinterpret(Ptr{rettyp}, buffer_ptr), rv) + HOSTCALL_RETURNED + end +end diff --git a/src/device/hostcall.jl b/src/device/hostcall.jl new file mode 100644 index 0000000000..5099748bdb --- /dev/null +++ b/src/device/hostcall.jl @@ -0,0 +1,236 @@ +# device-side functionality for calling host methods +# +# MAJOR TODOs: +# - avoid deadlocks: the watcher thread isn't guaranteed to be scheduled when a kernel is +# waiting for a response from the host when performing a block API call. +# threads don't help here, since the Julia scheduler doesn't migrate work +# (i.e. a run of the watcher) to an available thread (and we also want to support -t1): +# https://github.com/JuliaGPU/CUDA.jl/pull/1140#issuecomment-916118046 +# +# MINOR TODOs: +# - improve performance: currently takes around 2us per non-blocking uncontended hostcall. +# all time spend in the atomic CAS, probably due to the PCI-E latency. +# try using unified memory? ideally, avoiding atomics entirely is even better, +# but that would require per-kernel and per-SM pools. +# - contended hostcalls are MUCH slower (try `@hostcall identity(nothing)` with more threads +# that fit in the hostcall buffer). +# - 4K arg buffer per hostcall is wasteful, we could derive the size from the actual call. + +export hostcall, @hostcall + +@enum HostcallState::Int8 begin + HOSTCALL_READY # ready to receive a hostcall + HOSTCALL_SUBMITTED # params submitted, ready to process + HOSTCALL_RETURNED # host has stored return values (if any, else HOSTCALL_READY) +end + +const HOSTCALL_BUFFER_SIZE = 4096 + +# GPU-compatible representation of a hostcall invocation +struct Hostcall + state::HostcallState + target::Int + buffer::NTuple{HOSTCALL_BUFFER_SIZE, UInt8} # for parameters, and returned values + + # NOTE: the state and buffer fields should always be the first and last one respectively + + Hostcall(state, thread, block, target, buffer) = + new(state, thread, block, target, buffer) + Hostcall() = new(HOSTCALL_READY) +end + +# list of called functions, represented in the Hostcall struct as an index into this list. +const hostcall_targets = [] + +""" + hostcall(fun, rettyp, Tuple{argtyps...}, args...) + +Call a function `fun` on the host, passing arguments `args` of types `argtyps`. The host +function returns `rettyp`, which is then returned by the hostcall. If `rettyp` is `Nothing`, +nothing is returned, and the hostcall will not have to wait on the CPU to finish the call. + +!!! warning + This interface is experimental, and might change without warning. +""" +@generated function hostcall(f::F, rettyp::Type{T}, ::Type{U}, args...) where {F,T,U} + # register the target + sig = Tuple{F, U.parameters...} + push!(hostcall_targets, (; sig, rettyp=T)) + index = length(hostcall_targets) + + # perform ccall-like argument conversion (cconvert |> unsafe_convert) + argtypes = Type[U.parameters...] + convert_argument_exprs(argtypes, args, :perform_hostcall, :f, :rettyp, index) +end + +@inline function perform_hostcall(f, rettyp::Type{T}, index::Int, args...) where {T} + # NOTE: this function has been carefully implemented to avoid throwing any exception, + # even trivial ones that are optimized away (e.g. by calling `UInt32(0)`). + # this is because hostcall is used to implement throw_* functions, + # and we otherwise run into recursion during inference. + # debug by enabling inference remarks and looking for: + # "compilation of Core.throw_*(...): Bounded recursion detected" + + # XXX: timeouts to prevent deadlocks? + mask = active_mask() + slots = popc(mask) + leader = ffs(mask) + + # reserve the amount of hostcall slots this warp needs + head0 = 0x00000000 + if laneid() == leader + pointers = hostcall_pointers() + pointers_ptr = pointer(pointers) + pointers_align = 4 + #@inbounds head0, tail0 = pointers[1], pointers[2] + head0 = unsafe_load(pointers_ptr, 1, Val(pointers_align)) + tail0 = unsafe_load(pointers_ptr, 2, Val(pointers_align)) + while true + if ring_space(head0, tail0, HOSTCALL_POOL_SIZE) >= slots + cmp = head0 + new_head0 = head0 + slots # clamped to valid range below + head0 = atomic_cas!(pointers_ptr, cmp, new_head0) + (head0 == cmp) && break + else + # wait for the CPU to process items + compute_capability() >= sv"7.0" && nanosleep(1024%UInt32) + #@inbounds tail0 = pointers[2] + tail0 = unsafe_load(pointers_ptr, 2, Val(pointers_align)) + end + end + end + + sync_warp(mask) + + # get our own slot + base0 = shfl_sync(mask, head0, leader) + idx0 = popc(mask & ((0x00000001 << (laneid() - 0x1)) - 0x1)) + slot = (base0 + idx0) & (HOSTCALL_POOL_SIZE - 0x1) + 0x1 + + # wait for the slot to be available (another thread may still be processing returned values) + while hostcall_state(slot) != HOSTCALL_READY + compute_capability() >= sv"7.0" && nanosleep(1024%UInt32) + end + + # submit the hostcall + hostcall_target!(index, slot) + write_hostcall_arguments(hostcall_buffer_ptr(slot), f, args...) + hostcall_state!(HOSTCALL_SUBMITTED, slot) + + if rettyp === Nothing + # non-blocking hostcall; let's just continue + rv = nothing + else + # wait for the last returned value (implying all preceding ones are ready too) + if idx0 + 0x1 == slots + while hostcall_state(slot) == HOSTCALL_SUBMITTED + compute_capability() >= sv"7.0" && nanosleep(1024%UInt32) + end + end + + sync_warp(mask) + + if hostcall_state(slot) == HOSTCALL_READY + # something went wrong... let's bail out + trap() + end + + rv = unsafe_load(reinterpret(LLVMPtr{T,AS.Global}, hostcall_buffer_ptr(slot)), 1, + Val(Base.datatype_alignment(T))) + + # release the parameters + hostcall_state!(HOSTCALL_READY, slot) + end + + # NOTE: the flag _needs_ to be set to READY here, either by the CPU or the GPU, because + # otherwise the CPU could try to access the hostcall object before it has been + # fully initialized (but after the tail pointer has been bumped). + + return rv::T +end + +# generated helper to efficiently write hostcall argument, without iterating at run time. +@inline @generated function write_hostcall_arguments(ptr, args...) + ex = quote end + + # NOTE: we use the same storage convention as dynamic parallelism + last_offset = 0 + for i in 1:length(args) + T = args[i] + sz = sizeof(T) + if sz > 0 + align = Base.datatype_alignment(T) + offset = Base.cld(last_offset, align) * align + last_offset = offset + sz + if last_offset > HOSTCALL_BUFFER_SIZE + # buffer overrun; bail out, the CPU will warn about this + break + end + push!(ex.args, :( + unsafe_store!(reinterpret(LLVMPtr{$T,AS.Global}, ptr+$offset), + args[$i], 1, Val($align)) + )) + end + end + + ex +end + + +## convenience macro + +""" + @hostcall fun([args...]) + @hostcall fun([args...])::T + +Call the function `fun` on the host, passing `args`. The return typeof the function is +inferred. If this fails, the return type may be specified explicitly using the `::T` syntax. + +See also: [`hostcall`](@ref) + +!!! warning + This interface is experimental, and might change without warning. +""" +macro hostcall(ex) + # check if the return type is specified + if Meta.isexpr(ex, :(::)) + ex, rettyp = ex.args + else + rettyp = nothing + end + + # decode the call + @assert Meta.isexpr(ex, :call) + f, args... = ex.args + + # forward to a generated function to figure out the argument types + esc(quote + $emit_hostcall($f, $rettyp, $(args...)) + end) +end + +@generated function emit_hostcall(f::F, retspec::T, args...) where {F, T} + argtyps = Tuple{args...} + + # determine the return type + if retspec <: Type + # the user has provided the type + rettyp = retspec.parameters[1] + elseif isdefined(F, :instance) + # check with inference + rettyp = Core.Compiler.return_type(F.instance, argtyps) + if rettyp === Union{} + tn = F.name::Core.TypeName + fn = isdefined(tn, :mt) ? tn.mt.name : string(F) + Core.println("WARNING: @hostcall could not deduce return type of '$fn($(args...))'; try annotating the call instead") + rettyp = Nothing + end + else + Core.println("WARNING: @hostcall cannot deduce return type closures; annotating the call instead") + rettyp = Nothing + end + + quote + hostcall(f, $rettyp, $argtyps, args...) + end +end diff --git a/src/device/intrinsics/atomics.jl b/src/device/intrinsics/atomics.jl index a27f97ff66..5d5829c5ad 100644 --- a/src/device/intrinsics/atomics.jl +++ b/src/device/intrinsics/atomics.jl @@ -406,7 +406,7 @@ array element should be used in the left and right hand side of the assignment, in-place application of a known operator. In both cases, the array reference should be pure and not induce any side-effects. -!!! warn +!!! warning 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. """ diff --git a/src/device/intrinsics/dynamic_parallelism.jl b/src/device/intrinsics/dynamic_parallelism.jl index e3bc5ff74c..182deab042 100644 --- a/src/device/intrinsics/dynamic_parallelism.jl +++ b/src/device/intrinsics/dynamic_parallelism.jl @@ -16,7 +16,8 @@ description(err::CuDeviceError) = cudaGetErrorString(err) @noinline function throw_device_cuerror(err::CuDeviceError) # the exception won't be rendered on the host, so print some details here already - @cuprintln("ERROR: a CUDA error was thrown during kernel execution: $(description(err)) (code $(Int(err.code)), $(name(err)))") + @cuprintf("ERROR: a CUDA error was thrown during kernel execution: %s (code %d), %s)", + description(err), Int32(err.code), name(err)) throw(err) end diff --git a/src/device/intrinsics/indexing.jl b/src/device/intrinsics/indexing.jl index e61745acac..6049af60f0 100644 --- a/src/device/intrinsics/indexing.jl +++ b/src/device/intrinsics/indexing.jl @@ -103,7 +103,7 @@ Returns the warp size (in threads). Returns the thread's lane within the warp. """ -@inline laneid() = Int(ccall("llvm.nvvm.read.ptx.sreg.laneid", llvmcall, UInt32, ()))+UInt32(1) +@inline laneid() = Int(ccall("llvm.nvvm.read.ptx.sreg.laneid", llvmcall, UInt32, ())) + 1 """ active_mask() diff --git a/src/device/intrinsics/math.jl b/src/device/intrinsics/math.jl index 6fd9258a65..3f9f9d89e6 100644 --- a/src/device/intrinsics/math.jl +++ b/src/device/intrinsics/math.jl @@ -150,25 +150,25 @@ end @device_function clz(x::Union{Int32, UInt32}) = - assume(within(UInt32(0), UInt32(32)), - ccall("extern __nv_clz", llvmcall, Int32, (UInt32,), x)) + assume(within(0%UInt32, 32%UInt32), + ccall("extern __nv_clz", llvmcall, UInt32, (UInt32,), x)) @device_function clz(x::Union{Int64, UInt64}) = - assume(within(UInt64(0), UInt64(64)), - ccall("extern __nv_clzll", llvmcall, Int32, (UInt64,), x)) + assume(within(0%UInt32, 64%UInt32), + ccall("extern __nv_clzll", llvmcall, UInt32, (UInt64,), x)) @device_function ffs(x::Union{Int32, UInt32}) = - assume(within(UInt32(0), UInt32(32)), - ccall("extern __nv_ffs", llvmcall, Int32, (UInt32,), x)) + assume(within(0%UInt32, 32%UInt32), + ccall("extern __nv_ffs", llvmcall, UInt32, (UInt32,), x)) @device_function ffs(x::Union{Int64, UInt64}) = - assume(within(UInt64(0), UInt64(64)), - ccall("extern __nv_ffsll", llvmcall, Int32, (UInt64,), x)) + assume(within(0%UInt32, 64%UInt32), + ccall("extern __nv_ffsll", llvmcall, UInt32, (UInt64,), x)) @device_function popc(x::Union{Int32, UInt32}) = - assume(within(UInt32(0), UInt32(32)), - ccall("extern __nv_popc", llvmcall, Int32, (UInt32,), x)) + assume(within(0%UInt32, 32%UInt32), + ccall("extern __nv_popc", llvmcall, UInt32, (UInt32,), x)) @device_function popc(x::Union{Int64, UInt64}) = - assume(within(UInt64(0), UInt64(64)), - ccall("extern __nv_popcll", llvmcall, Int32, (UInt64,), x)) + assume(within(0%UInt32, 64%UInt32), + ccall("extern __nv_popcll", llvmcall, UInt32, (UInt64,), x)) @device_function byte_perm(x::Union{Int32, UInt32}, y::Union{Int32, UInt32}, z::Union{Int32, UInt32}) = ccall("extern __nv_byte_perm", llvmcall, Int32, (UInt32, UInt32, UInt32), x, y, z) diff --git a/src/device/intrinsics/misc.jl b/src/device/intrinsics/misc.jl index 397a1eb5a8..c1f28be73c 100644 --- a/src/device/intrinsics/misc.jl +++ b/src/device/intrinsics/misc.jl @@ -23,7 +23,7 @@ Puts a thread for a given amount `t`(in nanoseconds). !!! note Requires CUDA >= 10.0 and sm_6.2 """ -@inline function nanosleep(t::Unsigned) +@inline function nanosleep(t::Integer) @asmcall("nanosleep.u32 \$0;", "r", true, Cvoid, Tuple{UInt32}, convert(UInt32, t)) end diff --git a/src/device/intrinsics/output.jl b/src/device/intrinsics/output.jl index 77b304fb26..666780644a 100644 --- a/src/device/intrinsics/output.jl +++ b/src/device/intrinsics/output.jl @@ -83,170 +83,3 @@ end call_function(llvm_f, Int32, Tuple{arg_types...}, arg_exprs...) end end - - -## print-like functionality - -export @cuprint, @cuprintln - -# simple conversions, defining an expression and the resulting argument type. nothing fancy, -# `@cuprint` pretty directly maps to `@cuprintf`; we should just support `write(::IO)`. -const cuprint_conversions = Dict( - Float32 => (x->:(Float64($x)), Float64), - Ptr{<:Any} => (x->:(convert(Ptr{Cvoid}, $x)), Ptr{Cvoid}), - LLVMPtr{<:Any} => (x->:(reinterpret(Ptr{Cvoid}, $x)), Ptr{Cvoid}), - Bool => (x->:(Int32($x)), Int32), -) - -# format specifiers -const cuprint_specifiers = Dict( - # integers - Int16 => "%hd", - Int32 => "%d", - Int64 => Sys.iswindows() ? "%lld" : "%ld", - UInt16 => "%hu", - UInt32 => "%u", - UInt64 => Sys.iswindows() ? "%llu" : "%lu", - - # floating-point - Float64 => "%f", - - # other - Cchar => "%c", - Ptr{Cvoid} => "%p", - Cstring => "%s", -) - -@inline @generated function _cuprint(parts...) - fmt = "" - args = Expr[] - - for i in 1:length(parts) - part = :(parts[$i]) - T = parts[i] - - # put literals directly in the format string - if T <: Val - fmt *= string(T.parameters[1]) - continue - end - - # try to convert arguments if they are not supported directly - if !haskey(cuprint_specifiers, T) - for Tmatch in keys(cuprint_conversions) - if T <: Tmatch - conv, T = cuprint_conversions[Tmatch] - part = conv(part) - break - end - end - end - - # render the argument - if haskey(cuprint_specifiers, T) - fmt *= cuprint_specifiers[T] - push!(args, part) - elseif T <: Tuple - fmt *= "(" - for (j, U) in enumerate(T.parameters) - if haskey(cuprint_specifiers, U) - fmt *= cuprint_specifiers[U] - push!(args, :($part[$j])) - if j < length(T.parameters) - fmt *= ", " - elseif length(T.parameters) == 1 - fmt *= "," - end - else - @error("@cuprint does not support values of type $U") - end - end - fmt *= ")" - elseif T <: String - @error("@cuprint does not support non-literal strings") - else - @error("@cuprint does not support values of type $T") - end - end - - quote - @cuprintf($fmt, $(args...)) - end -end - -""" - @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: - -```julia - @cuprint("Hello, World ", 42, "\\n") - @cuprint "Hello, World \$(42)\\n" -``` -""" -macro cuprint(parts...) - args = Union{Val,Expr,Symbol}[] - - parts = [parts...] - while true - isempty(parts) && break - - part = popfirst!(parts) - - # handle string interpolation - if isa(part, Expr) && part.head == :string - parts = vcat(part.args, parts) - continue - end - - # expose literals to the generator by using Val types - if isbits(part) # literal numbers, etc - push!(args, Val(part)) - elseif isa(part, QuoteNode) # literal symbols - push!(args, Val(part.value)) - elseif isa(part, String) # literal strings need to be interned - push!(args, Val(Symbol(part))) - else # actual values that will be passed to printf - push!(args, part) - end - end - - quote - _cuprint($(map(esc, args)...)) - end -end - -@doc (@doc @cuprint) -> -macro cuprintln(parts...) - esc(quote - CUDA.@cuprint($(parts...), "\n") - end) -end - -export @cushow - -""" - @cushow(ex) - -GPU analog of `Base.@show`. It comes with the same type restrictions as [`@cuprintf`](@ref). - -```julia -@cushow threadIdx().x -``` -""" -macro cushow(exs...) - blk = Expr(:block) - for ex in exs - push!(blk.args, :(CUDA.@cuprintln($(sprint(Base.show_unquoted,ex)*" = "), - begin local value = $(esc(ex)) end))) - end - isempty(exs) || push!(blk.args, :value) - blk -end diff --git a/src/device/intrinsics/warp_shuffle.jl b/src/device/intrinsics/warp_shuffle.jl index e988fbd788..a3d6a98ee4 100644 --- a/src/device/intrinsics/warp_shuffle.jl +++ b/src/device/intrinsics/warp_shuffle.jl @@ -5,7 +5,7 @@ # TODO: does not work on sub-word (ie. Int16) or non-word divisible sized types # TODO: these functions should dispatch based on the actual warp size -const ws = Int32(32) +const ws = UInt32(32) # core intrinsics @@ -18,7 +18,7 @@ const ws = Int32(32) for (name, mode, mask, offset) in (("_up", :up, UInt32(0x00), src->src), ("_down", :down, UInt32(0x1f), src->src), ("_xor", :bfly, UInt32(0x1f), src->src), - ("", :idx, UInt32(0x1f), src->:($src-1))) + ("", :idx, UInt32(0x1f), src->:($src-(1%UInt32)))) fname = Symbol("shfl$(name)_sync") @eval export $fname @@ -28,8 +28,8 @@ for (name, mode, mask, offset) in (("_up", :up, UInt32(0x00), src->src), @eval begin @inline $fname(mask, val::$T, src, width=$ws) = ccall($intrinsic, llvmcall, $T, - (UInt32, $T, UInt32, UInt32), - mask, val, $(offset(:src)), pack(width, $mask)) + (UInt32, $T, UInt32, UInt32), + mask, val, $(offset(:src)), pack(width, $mask)) end end end diff --git a/src/device/output.jl b/src/device/output.jl new file mode 100644 index 0000000000..87ff480c2e --- /dev/null +++ b/src/device/output.jl @@ -0,0 +1,88 @@ +# print-like functionality + +export @cuprint, @cuprintln + +const kernel_strings = Union{String,Symbol}[] +struct KernelString + id::Int +end + +function _cuprint(args...) + actual_args = map(arg -> isa(arg, KernelString) ? kernel_strings[arg.id] : arg, args) + print(actual_args...) + return +end + +""" + @cuprint(xs...) + @cuprintln(xs...) + +Print a textual representation of values `xs` to standard output from the GPU. + +Limited string interpolation is also possible: + +```julia + @cuprint("Hello, World ", 42, "\\n") + @cuprint "Hello, World \$(42)\\n" +``` +""" +macro cuprint(parts...) + args = [] + + parts = [parts...] + while true + isempty(parts) && break + + part = popfirst!(parts) + + # handle string interpolation + if isa(part, Expr) && part.head == :string + parts = vcat(part.args, parts) + continue + end + + if isa(part, String) + push!(kernel_strings, part) + id = length(kernel_strings) + push!(args, :(CUDA.KernelString($id))) + elseif isa(part, QuoteNode) + push!(kernel_strings, part.value) + id = length(kernel_strings) + push!(args, :(CUDA.KernelString($id))) + else + push!(args, part) + end + end + + esc(quote + CUDA.@hostcall CUDA._cuprint($(args...))::Nothing + end) +end + +@doc (@doc @cuprint) -> +macro cuprintln(parts...) + esc(quote + CUDA.@cuprint($(parts...), "\n") + end) +end + +export @cushow + +""" + @cushow(ex) + +GPU analog of `Base.@show`. It comes with the same type restrictions as [`@cuprintf`](@ref). + +```julia +@cushow threadIdx().x +``` +""" +macro cushow(exs...) + blk = Expr(:block) + for ex in exs + push!(blk.args, :(CUDA.@cuprintln($(sprint(Base.show_unquoted,ex)*" = "), + begin local value = $(esc(ex)) end))) + end + isempty(exs) || push!(blk.args, :value) + blk +end diff --git a/src/device/runtime.jl b/src/device/runtime.jl index 280d7511a6..954fb077a9 100644 --- a/src/device/runtime.jl +++ b/src/device/runtime.jl @@ -24,27 +24,22 @@ function precompile_runtime(caps=CUDA.llvm_compat(LLVM.version()).cap) return end -@eval @inline exception_flag() = - Base.llvmcall( - $("""@exception_flag = weak externally_initialized global i$(WORD_SIZE) 0 - define i64 @entry() #0 { - %ptr = load i$(WORD_SIZE), i$(WORD_SIZE)* @exception_flag, align 8 - ret i$(WORD_SIZE) %ptr - } - attributes #0 = { alwaysinline } - """, "entry"), Ptr{Cvoid}, Tuple{}) +struct KernelState + exception_flag::LLVMPtr{Int8, AS.Global} + + hostcall_pointers::LLVMPtr{UInt32, AS.Global} + hostcalls::LLVMPtr{Hostcall, AS.Global} +end + +kernel_state() = unsafe_load(convert(Ptr{KernelState}, GPUCompiler.kernel_state_pointer())) + +# exception handling + +exception_flag() = kernel_state().exception_flag function signal_exception() - ptr = exception_flag() - if ptr !== C_NULL - unsafe_store!(convert(Ptr{Int}, ptr), 1) - threadfence_system() - else - @cuprintf(""" - WARNING: could not signal exception status to the host, execution will continue. - Please file a bug. - """) - end + unsafe_store!(exception_flag(), 1) + threadfence_system() return end @@ -74,6 +69,31 @@ function report_exception_frame(idx, func, file, line) return end +# hostcall + +hostcall_pointers() = CuDeviceArray(2, kernel_state().hostcall_pointers) + +hostcalls() = CuDeviceArray(HOSTCALL_POOL_SIZE, kernel_state().hostcalls) + +# generate accessors for individual fields +for i in 1:fieldcount(Hostcall) + local typ = fieldtype(Hostcall, i) + local name = fieldname(Hostcall, i) + local offset = fieldoffset(Hostcall, i) + + local align = Base.datatype_alignment(typ) + + ptr = Symbol("hostcall_$(name)_ptr") + getter = Symbol("hostcall_$(name)") + setter = Symbol("hostcall_$(name)!") + @eval begin + $(ptr)(i=1) = + reinterpret(LLVMPtr{$typ,AS.Global}, pointer(hostcalls(), i)) + $offset + $(getter)(i=1) = unsafe_load($(ptr)(i), 1, Val($align)) + $(setter)(x::$typ, i=1) = unsafe_store!($(ptr)(i), x, 1, Val($align)) + end +end + ## CUDA device library diff --git a/test/device/hostcall.jl b/test/device/hostcall.jl new file mode 100644 index 0000000000..d327ae14b7 --- /dev/null +++ b/test/device/hostcall.jl @@ -0,0 +1,50 @@ +@testset "essentials" begin + @on_device hostcall(identity, Nothing, Tuple{Nothing}, nothing) + @on_device @hostcall identity(nothing) + @on_device @hostcall identity(nothing)::Nothing +end + +saved = [] +function save(args...) + push!(saved, args...) + return +end + +@testset "argument passing" begin + # no args + @on_device @hostcall identity(nothing) + CUDA.hostcall_synchronize() + @test isempty(saved) + + # 1 primitive arg + @on_device @hostcall save(threadIdx().x)::Nothing + CUDA.hostcall_synchronize() + @test saved == [1] + empty!(saved) + + # multiple primitive args + @on_device @hostcall save(threadIdx().x, blockIdx().x)::Nothing + CUDA.hostcall_synchronize() + @test saved == [1, 1] + empty!(saved) + + # isbits args + @on_device @hostcall save((threadIdx().x, blockIdx().x))::Nothing + CUDA.hostcall_synchronize() + @test saved == [(1, 1)] + empty!(saved) +end + +@testset "return values" begin + # primitive + @on_device @hostcall save(@hostcall +(threadIdx().x, 1))::Nothing + CUDA.hostcall_synchronize() + @test saved == [2] + empty!(saved) + + # isbits + @on_device @hostcall save(@hostcall tuple(threadIdx().x, blockIdx().x))::Nothing + CUDA.hostcall_synchronize() + @test saved == [(1, 1)] + empty!(saved) +end diff --git a/test/device/intrinsics/output.jl b/test/device/intrinsics/output.jl index 512c74a99f..a79607250a 100644 --- a/test/device/intrinsics/output.jl +++ b/test/device/intrinsics/output.jl @@ -36,118 +36,3 @@ endline = Sys.iswindows() ? "\r\n" : "\n" end @test out == "1.000000 1.000000$endline" end - -@testset "@cuprint" begin - # basic @cuprint/@cuprintln - - _, out = @grab_output @on_device @cuprint("Hello, World\n") - @test out == "Hello, World$endline" - - _, out = @grab_output @on_device @cuprintln("Hello, World") - @test out == "Hello, World$endline" - - - # argument interpolation (by the macro, so can use literals) - - _, out = @grab_output @on_device @cuprint("foobar") - @test out == "foobar" - - _, out = @grab_output @on_device @cuprint(:foobar) - @test out == "foobar" - - _, out = @grab_output @on_device @cuprint("foo", "bar") - @test out == "foobar" - - _, out = @grab_output @on_device @cuprint("foobar ", 42) - @test out == "foobar 42" - - _, out = @grab_output @on_device @cuprint("foobar $(42)") - @test out == "foobar 42" - - _, out = @grab_output @on_device @cuprint("foobar $(4)", 2) - @test out == "foobar 42" - - _, out = @grab_output @on_device @cuprint("foobar ", 4, "$(2)") - @test out == "foobar 42" - - _, out = @grab_output @on_device @cuprint(42) - @test out == "42" - - _, out = @grab_output @on_device @cuprint(4, 2) - @test out == "42" - - # bug: @cuprintln failed to invokce @cuprint with endline in the case of interpolation - _, out = @grab_output @on_device @cuprintln("foobar $(42)") - @test out == "foobar 42$endline" - - - # argument types - - # we're testing the generated functions now, so can't use literals - function test_output(val, str) - canary = rand(Int32) # if we mess up the main arg, this one will print wrong - _, out = @grab_output @on_device @cuprint(val, " (", canary, ")") - @test out == "$(str) ($(Int(canary)))" - end - - for typ in (Int16, Int32, Int64, UInt16, UInt32, UInt64) - test_output(typ(42), "42") - end - - for typ in (Float32, Float64) - test_output(typ(42), "42.000000") - end - - test_output(Cchar('c'), "c") - - for typ in (Ptr{Cvoid}, Ptr{Int}) - ptr = convert(typ, Int(0x12345)) - test_output(ptr, Sys.iswindows() ? "0000000000012345" : "0x12345") - end - - test_output(true, "1") - test_output(false, "0") - - test_output((1,), "(1,)") - test_output((1,2), "(1, 2)") - test_output((1,2,3.), "(1, 2, 3.000000)") - - - # escaping - - kernel1(val) = (@cuprint(val); nothing) - _, out = @grab_output @on_device kernel1(42) - @test out == "42" - - kernel2(val) = (@cuprintln(val); nothing) - _, out = @grab_output @on_device kernel2(42) - @test out == "42$endline" -end - -@testset "@cushow" begin - function kernel() - seven_i32 = Int32(7) - three_f64 = Float64(3) - @cushow seven_i32 - @cushow three_f64 - @cushow 1f0 + 4f0 - return - end - - _, out = @grab_output @on_device kernel() - @test out == "seven_i32 = 7$(endline)three_f64 = 3.000000$(endline)1.0f0 + 4.0f0 = 5.000000$(endline)" -end - -@testset "@cushow array pointers" begin - function kernel() - a = CuStaticSharedArray(Float32, 1) - b = CuStaticSharedArray(Float32, 2) - @cushow pointer(a) pointer(b) - return - end - - _, out = @grab_output @on_device kernel() - @test occursin("pointer(a) = ", out) - @test occursin("pointer(b) = ", out) - @test occursin("= 0", out) # 0x... on Linux, 0... on Windows -end diff --git a/test/device/output.jl b/test/device/output.jl new file mode 100644 index 0000000000..1249a27e11 --- /dev/null +++ b/test/device/output.jl @@ -0,0 +1,117 @@ + +endline = Sys.iswindows() ? "\r\n" : "\n" + +@testset "@cuprint" begin + # basic @cuprint/@cuprintln + + _, out = @grab_output @on_device @cuprint("Hello, World\n") + @test out == "Hello, World$endline" + + _, out = @grab_output @on_device @cuprintln("Hello, World") + @test out == "Hello, World$endline" + + + # argument interpolation (by the macro, so can use literals) + + _, out = @grab_output @on_device @cuprint("foobar") + @test out == "foobar" + + _, out = @grab_output @on_device @cuprint(:foobar) + @test out == "foobar" + + _, out = @grab_output @on_device @cuprint("foo", "bar") + @test out == "foobar" + + _, out = @grab_output @on_device @cuprint("foobar ", 42) + @test out == "foobar 42" + + _, out = @grab_output @on_device @cuprint("foobar $(42)") + @test out == "foobar 42" + + _, out = @grab_output @on_device @cuprint("foobar $(4)", 2) + @test out == "foobar 42" + + _, out = @grab_output @on_device @cuprint("foobar ", 4, "$(2)") + @test out == "foobar 42" + + _, out = @grab_output @on_device @cuprint(42) + @test out == "42" + + _, out = @grab_output @on_device @cuprint(4, 2) + @test out == "42" + + # bug: @cuprintln failed to invokce @cuprint with endline in the case of interpolation + _, out = @grab_output @on_device @cuprintln("foobar $(42)") + @test out == "foobar 42$endline" + + + # argument types + + # we're testing the generated functions now, so can't use literals + function test_output(val) + str = sprint(io->print(io, val)) + canary = rand(Int32) # if we mess up the main arg, this one will print wrong + _, out = @grab_output @on_device @cuprint(val, " (", canary, ")") + @test out == "$(str) ($(Int(canary)))" + end + + for typ in (Int16, Int32, Int64, UInt16, UInt32, UInt64) + test_output(typ(42)) + end + + for typ in (Float32, Float64) + test_output(typ(42)) + end + + test_output(Cchar('c')) + + for typ in (Ptr{Cvoid}, Ptr{Int}) + ptr = convert(typ, Int(0x12345)) + test_output(ptr) + end + + test_output(true) + test_output(false) + + test_output((1,)) + test_output((1,2)) + test_output((1,2,3.)) + + + # escaping + + kernel1(val) = (@cuprint(val); nothing) + _, out = @grab_output @on_device kernel1(42) + @test out == "42" + + kernel2(val) = (@cuprintln(val); nothing) + _, out = @grab_output @on_device kernel2(42) + @test out == "42$endline" +end + +@testset "@cushow" begin + function kernel() + seven_i32 = Int32(7) + three_f64 = Float64(3) + @cushow seven_i32 + @cushow three_f64 + @cushow 1f0 + 4f0 + return + end + + _, out = @grab_output @on_device kernel() + @test out == "seven_i32 = 7$(endline)three_f64 = 3.0$(endline)1.0f0 + 4.0f0 = 5.0$(endline)" +end + +@testset "@cushow array pointers" begin + function kernel() + a = CuStaticSharedArray(Float32, 1) + b = CuStaticSharedArray(Float32, 2) + @cushow pointer(a) pointer(b) + return + end + + _, out = @grab_output @on_device kernel() + @test occursin("pointer(a) = Core.LLVMPtr{Float32, 3}(0x0000000000000000)", out) + @test occursin("pointer(b) = Core.LLVMPtr{Float32, 3}(0x0000000000000020)", out) +end diff --git a/test/execution.jl b/test/execution.jl index 90cb9eb028..21f2d7f75a 100644 --- a/test/execution.jl +++ b/test/execution.jl @@ -624,7 +624,7 @@ end @testset "unreachable" begin function unreachable() - @cuprintln("go home ptxas you're drunk") + @cuprintf("go home ptxas you're drunk") Base.llvmcall("unreachable", Cvoid, Tuple{}) end diff --git a/test/setup.jl b/test/setup.jl index ac5e10c998..4db17383bd 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -133,6 +133,10 @@ macro grab_output(ex) # NOTE: CUDA requires a 'proper' sync to flush its printf buffer device_synchronize() + + # we also want to give our hostcall watcher the time to execute + # XXX: ensure the watcher executes to avoid spurious CI failures + sleep(0.1) end end ret, read(fname, String) diff --git a/wip.jl b/wip.jl new file mode 100644 index 0000000000..a931bf0fd6 --- /dev/null +++ b/wip.jl @@ -0,0 +1,20 @@ +using CUDA + +function test(x) + println("This is a hostcall from thread $x") + x+1 +end + +function kernel() + rv = hostcall(test, Int, Tuple{Int}, threadIdx().x) + @cuprintln("Hostcall returned $rv") + return +end + +function main() + @cuda threads=16 kernel() + synchronize() + return +end + +isinteractive() || main()