Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions src/KernelAbstractions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,51 @@ synchronize(dev)
```
"""
macro kernel(expr)
return __kernel(expr, __source__, #=force_inbounds=# false, #=unsafe_indices=# false)
return __kernel(expr, __source__, __module__, #=force_inbounds=# false, #=unsafe_indices=# false, #=generated=# false)
end

"""
@kernel config function f(args) end

This allows for two different configurations:
This allows for the following configurations:

1. `cpu={true, false}`: Disables code-generation of the CPU function. This relaxes semantics such that KernelAbstractions primitives can be used in non-kernel functions.
1. `cpu={true, false}`: **Deprecated** in KernelAbstractions 0.11; this option is ignored.
2. `inbounds={false, true}`: Enables a forced `@inbounds` macro around the function definition in the case the user is using too many `@inbounds` already in their kernel. Note that this can lead to incorrect results, crashes, etc and is fundamentally unsafe. Be careful!
3. `unsafe_indices={false, true}`: Disables the implicit validation of indices, users must avoid `@index(Global)`.
4. `generated={false, true}`: Turns the kernel into a [generated function](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions), see *Generated* below.

- [`@context`](@ref)

!!! warning
This is an experimental feature.

!!! note
`cpu={true, false}` is deprecated for KernelAbstractions 1.0
### Generated

With `generated=true` the kernel body is treated as a quoted expression, so `\$` interpolation is
available and `where`-parameters are bound to their values, e.g. to unroll a loop `\$N` times:

```julia
@kernel generated = true function kernel_unroll!(a, ::Val{N}) where {N}
@unroll \$N for i in 1:5
@inbounds a[i] = i * \$N
end
end
```

This is meant for macros that need a literal, such as `@unroll \$N`, `Base.Cartesian.@nexprs \$N`
or `@ntuple \$N`; plain `where`-parameters are compile-time constants in every kernel already.
Configuration parameters must therefore be passed as types (`::Val{N}`) to be usable inside `\$`.
Inside `\$(...)` the argument names refer to the *types* of the arguments, not their values,
as in any generated function, and the body cannot contain closures, comprehensions or
generators (`x -> ...`, `do` blocks, `[f(i) for i in ...]`); use the Cartesian macros above instead.
"""
macro kernel(ex...)
if length(ex) == 1
return __kernel(ex[1], __source__, false, false)
return __kernel(ex[1], __source__, __module__, false, false, false)
else
unsafe_indices = false
force_inbounds = false
generated = false
for i in 1:(length(ex) - 1)
if ex[i] isa Expr && ex[i].head == :(=) &&
ex[i].args[1] == :cpu && ex[i].args[2] isa Bool
Expand All @@ -110,17 +129,21 @@ macro kernel(ex...)
elseif ex[i] isa Expr && ex[i].head == :(=) &&
ex[i].args[1] == :unsafe_indices && ex[i].args[2] isa Bool
unsafe_indices = ex[i].args[2]
elseif ex[i] isa Expr && ex[i].head == :(=) &&
ex[i].args[1] == :generated && ex[i].args[2] isa Bool
generated = ex[i].args[2]
else
error(
"Configuration should be of form:\n" *
"* `cpu=false`\n" *
"* `inbounds=true`\n" *
"* `unsafe_indices=true`\n" *
"* `generated=true`\n" *
"got `", ex[i], "`",
)
end
end
return __kernel(ex[end], __source__, force_inbounds, unsafe_indices)
return __kernel(ex[end], __source__, __module__, force_inbounds, unsafe_indices, generated)
end
end

Expand Down Expand Up @@ -295,18 +318,16 @@ end

Access the hidden context object used by KernelAbstractions.

!!! warning
Only valid to be used from a kernel with `cpu=false`.
!!! compat "KernelAbstractions 0.10"
`@context` is supported on all backends since KernelAbstractions 0.10.

!!! note
`@context` will be supported on all backends in KernelAbstractions 1.0
```
function f(@context, a)
I = @index(Global, Linear)
a[I]
end

@kernel cpu=false function my_kernel(a)
@kernel function my_kernel(a)
f(@context, a)
end
```
Expand Down
100 changes: 99 additions & 1 deletion src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function unblock_lines(ex)
end

# XXX: Proper errors
function __kernel(expr, __source__::LineNumberNode, force_inbounds = false, unsafe_indices = false)
function __kernel(expr, __source__::LineNumberNode, __module__::Module, force_inbounds = false, unsafe_indices = false, generated = false)
def = splitdef(expr)
name = def[:name]
args = def[:args]
Expand All @@ -46,6 +46,28 @@ function __kernel(expr, __source__::LineNumberNode, force_inbounds = false, unsa
def_gpu = deepcopy(def)
def_gpu[:name] = gpu_name = Symbol(:gpu_, name)
transform_gpu!(def_gpu, constargs, force_inbounds, unsafe_indices)
if generated
# Turn the kernel into a generated function: the transformed body is
# quoted so that it is returned as an expression. Passing the quote
# through `macroexpand` (one level only, we do not want to expand the
# macros *inside* the quoted body here) lowers the `$` interpolations
# into the code that builds the expression at generation time.
body = macroexpand(__module__, Expr(:quote, def_gpu[:body]), recursive = false)
# Inference swallows any error thrown while generating (the kernel then
# merely infers to `Any`, which GPUCompiler reports as "kernel returns a
# value of type `Any`" without ever showing the cause), and there is no
# common launch path across backends where we could rethrow it. So
# catch the error here and hand it back through the return type
# instead, where GPUCompiler's validation prints it on every backend.
body = quote
try
$(check_generated)($(__module__), $body)
catch err
$(generated_error_body)(err)
end
end
def_gpu[:body] = Expr(:if, Expr(:generated), body, Expr(:meta, :generated_only))
end
gpu_function = combinedef(def_gpu)

# create constructor functions
Expand All @@ -66,6 +88,82 @@ function __kernel(expr, __source__::LineNumberNode, force_inbounds = false, unsa
return Expr(:block, esc(gpu_function), esc(constructors))
end

"""
GeneratedKernelError{Msg}

Marker type a `generated=true` kernel returns when its generator failed, carrying the
error message in its type parameter. A kernel that returns this shows up in the
`KernelError` GPUCompiler raises for kernels that return a value, which is the only
channel through which a failure at generation time can be reported.
"""
struct GeneratedKernelError{Msg} end
GeneratedKernelError(msg::AbstractString) = GeneratedKernelError{Symbol(msg)}()

function Base.show(io::IO, ::Type{GeneratedKernelError{Msg}}) where {Msg}
return print(io, "KernelAbstractions.GeneratedKernelError(", repr(String(Msg)), ")")
end

# The left-hand side of a short-form method definition, `f(x) = ...`, possibly
# wrapped in `where` clauses and a return type annotation: `f(x)::T where {T} = ...`.
function is_short_def_lhs(lhs)
while isexpr(lhs, :where) || isexpr(lhs, :(::))
isempty(lhs.args) && return false
lhs = lhs.args[1]
end
return isexpr(lhs, :call)
end

# Runs inside the generator: makes sure the generated body is something Julia
# accepts as the result of a generated function. Julia itself only rejects a
# closure, comprehension or generator when lowering the body, which happens
# outside the generator's `try` and thus can't be turned into a useful error
# there, so check for them up front.
function check_generated(mod::Module, body)
ex = macroexpand(mod, body)
MacroTools.postwalk(ex) do node
if isexpr(node, :->) || isexpr(node, :function) || isexpr(node, :do) ||
isexpr(node, :comprehension) || isexpr(node, :generator) ||
isexpr(node, :flatten) || (isexpr(node, :(=)) && is_short_def_lhs(node.args[1]))
found = replace(string(MacroTools.striplines(node)), r"\s+" => " ")
error(
"the body of a `generated=true` kernel cannot contain a closure, " *
"comprehension or generator (found `", found, "`). " *
"Use `Base.Cartesian.@nexprs \$N` or `@ntuple \$N` instead.",
)
end
return node
end
return body
end

# Runs inside the generator, so it must not use code reflection: `showerror` for a
# `MethodError` looks up candidate methods, which is forbidden there, so format
# that one by hand and fall back to the bare exception type for anything else
# that can't be shown.
function generated_error_message(err)
if err isa MethodError
sig = join((a isa Type ? "::Type{$a}" : "::$(typeof(a))" for a in err.args), ", ")
return string("MethodError: no method matching ", err.f, "(", sig, ")")
end
msg = try
sprint(showerror, err)
catch
string(typeof(err))
end
return first(Base.split(msg, '\n'))
end

function generated_error_body(err)
msg = try
generated_error_message(err)
catch
string(typeof(err))
end
# A `Symbol` cannot contain NUL characters.
msg = replace(msg, '\0' => "\\0")
return :(return $(GeneratedKernelError(msg)))
end

# The easy case, transform the function for GPU execution
# - mark constant arguments by applying `constify`.
function transform_gpu!(def, constargs, force_inbounds, unsafe_indices)
Expand Down
67 changes: 67 additions & 0 deletions test/unroll.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,39 @@ end
end
end

# `generated=true` makes the kernel a generated function, so that the `where`
# parameter `N` can be interpolated into `@unroll $N`, which requires a literal.
@kernel generated = true function kernel_unroll_generated!(a, ::Val{N}) where {N}
@unroll $N for i in 1:5
@inbounds a[i] = i * $N
end
end

# `generated=true` composes with the other body transformations: `@Const`,
# `@localmem`, `@synchronize` and `inbounds=true` all round-trip through the quote.
@kernel generated = true inbounds = true function kernel_generated_transforms!(a, @Const(b), ::Val{N}) where {N}
tile = @localmem Float32 (N,)
I = @index(Global, Linear)
i = @index(Local, Linear)
@unroll $N for k in 1:N
tile[k] = b[k] * $N
end
@synchronize
a[I] = tile[i]
end

# Errors while generating are reported through the return type rather than being
# swallowed as `Any`; the message must carry the original error.
@kernel generated = true function kernel_generated_closure!(a)
I = @index(Global)
@inbounds a[I] = sum(x -> x, 1:$(2))
end

@kernel generated = true function kernel_generated_badinterp!(a)
I = @index(Global)
@inbounds a[I] = $(length(a))
end

function unroll_testsuite(backend, ArrayT)
a = ArrayT(zeros(Float32, 5))
kernel! = kernel_unroll!(backend(), 1, 1)
Expand All @@ -44,5 +77,39 @@ function unroll_testsuite(backend, ArrayT)
kernel2! = kernel_unroll2!(backend(), 1, 1)
kernel2!(a)
synchronize(backend())

a = ArrayT(zeros(Float32, 5))
kernel3! = kernel_unroll_generated!(backend(), 1, 1)
kernel3!(a, Val(2))
synchronize(backend())
@test Array(a) == Float32[2, 4, 6, 8, 10]

a = ArrayT(zeros(Float32, 4))
b = ArrayT(Float32[1, 2, 3, 4])
kernel4! = kernel_generated_transforms!(backend(), 4, 4)
kernel4!(a, b, Val(4))
synchronize(backend())
@test Array(a) == Float32[4, 8, 12, 16]

a = ArrayT(zeros(Float32, 2))
err = try
kernel_generated_closure!(backend(), 2)(a; ndrange = 2)
synchronize(backend())
nothing
catch e
sprint(showerror, e)
end
@test occursin("GeneratedKernelError", err)
@test occursin("cannot contain a closure", err)

err = try
kernel_generated_badinterp!(backend(), 2)(a; ndrange = 2)
synchronize(backend())
nothing
catch e
sprint(showerror, e)
end
@test occursin("GeneratedKernelError", err)
@test occursin("MethodError: no method matching length(::Type{", err)
return
end
Loading