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..194c9d4353 100644 --- a/src/CUDA.jl +++ b/src/CUDA.jl @@ -58,6 +58,7 @@ include("device/utils.jl") include("device/pointer.jl") include("device/array.jl") include("device/intrinsics.jl") +include("device/hostcall.jl") include("device/runtime.jl") include("device/texture.jl") include("device/random.jl") @@ -75,8 +76,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 b00e330e0f..a2a31796e7 100644 --- a/src/compiler/exceptions.jl +++ b/src/compiler/exceptions.jl @@ -18,15 +18,15 @@ 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) exception_flag = get!(exception_flags, mod.ctx, - Mem.alloc(Mem.Host, sizeof(Int), Mem.HOSTALLOC_DEVICEMAP)) - return reinterpret(Ptr{Cvoid}, convert(CuPtr{Cvoid}, exception_flag)) + 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 220ddff2b6..078c1ac9a6 100644 --- a/src/compiler/execution.jl +++ b/src/compiler/execution.jl @@ -453,7 +453,10 @@ end # create the kernel state object exception_ptr = create_exceptions!(mod) - state = KernelState(exception_ptr) + 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, state) end 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..d2c74dfa60 --- /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(1024u32) + #@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(1024u32) + 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(1024u32) + 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/math.jl b/src/device/intrinsics/math.jl index 6fd9258a65..8530652caa 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(0u32, 32u32), + 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(0u32, 64u32), + 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(0u32, 32u32), + 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(0u32, 64u32), + 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(0u32, 32u32), + 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(0u32, 64u32), + 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/warp_shuffle.jl b/src/device/intrinsics/warp_shuffle.jl index e988fbd788..bc898a6de3 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 = 32u32 # 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-(1u32)))) 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/runtime.jl b/src/device/runtime.jl index 81e8419c7b..1e4d32517f 100644 --- a/src/device/runtime.jl +++ b/src/device/runtime.jl @@ -25,24 +25,21 @@ function precompile_runtime(caps=CUDA.llvm_compat(LLVM.version()).cap) end struct KernelState - exception_flag::Ptr{Cvoid} + exception_flag::LLVMPtr{Int8, AS.Global} + + hostcall_pointers::LLVMPtr{UInt32, AS.Global} + hostcalls::LLVMPtr{Hostcall, AS.Global} end @inline @generated kernel_state() = GPUCompiler.kernel_state_value(KernelState) +# 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 @@ -72,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/src/device/utils.jl b/src/device/utils.jl index 680a610df4..2195dfe5d1 100644 --- a/src/device/utils.jl +++ b/src/device/utils.jl @@ -3,8 +3,9 @@ # helper type for writing Int32 literals # TODO: upstream this struct Literal{T} end -Base.:(*)(x, ::Type{Literal{T}}) where {T} = T(x) +Base.:(*)(x, ::Type{Literal{T}}) where {T} = x%T const i32 = Literal{Int32} +const u32 = Literal{UInt32} # local method table for device functions @static if isdefined(Base.Experimental, Symbol("@overlay")) 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/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()