Repository navigation
Add foreach_index
#779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Add foreach_index
#779
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # One work item per index of a collection. | ||
| # | ||
| # The index space is carried by the `ndrange`, so the kernels take no argument besides the | ||
| # function: `@index(Global, Cartesian)` already returns the index of the `ndrange`, including | ||
| # the offset of an index space that does not start at 1. | ||
| # | ||
| # `f` is inlined on purpose: an out-of-line call to a closure can spill its captures to local | ||
| # memory on some backends. | ||
|
|
||
| @kernel function foreach_index_linear_kernel(f) | ||
| I = @index(Global, Cartesian) | ||
| @inline f(I.I[1]) | ||
| end | ||
|
|
||
| @kernel function foreach_index_cartesian_kernel(f) | ||
| I = @index(Global, Cartesian) | ||
| @inline f(I) | ||
| end | ||
|
|
||
| # `eachindex` of an `AbstractArray` is either a range of linear indices or a `CartesianIndices`; | ||
| # hand `f` the index type that `itr` is indexed with in either case. | ||
| foreach_index_kernel(::AbstractUnitRange, backend) = foreach_index_linear_kernel(backend) | ||
| foreach_index_kernel(::CartesianIndices, backend) = foreach_index_cartesian_kernel(backend) | ||
| foreach_index_kernel(indices, backend) = throw( | ||
| ArgumentError( | ||
| "`foreach_index` needs an index space that is a range of linear indices or a `CartesianIndices`, got `$(typeof(indices))`" | ||
| ) | ||
| ) | ||
|
|
||
| """ | ||
| foreach_index(f, itr, backend = get_backend(itr); workgroupsize = nothing) | ||
|
|
||
| Call `f(i)` once for every `i` in `eachindex(itr)`, with one work item per index. | ||
|
|
||
| This is the kernel-free spelling of a `for` loop over the indices of a collection: the body is | ||
| an ordinary Julia function, so the same code runs on every backend. | ||
|
|
||
| ```julia | ||
| function scale!(y, x) | ||
| foreach_index(x) do i | ||
| @inbounds y[i] = 2 * x[i] + 1 | ||
| end | ||
| return y | ||
| end | ||
| ``` | ||
|
|
||
| `f` receives the same index that a `for i in eachindex(itr)` loop would: a linear index for an | ||
| array with `IndexLinear` style, a `CartesianIndex` otherwise. | ||
|
|
||
| Like any other kernel launch this is asynchronous; call [`synchronize`](@ref) before reading the | ||
| result on the host. Bounds checks are not elided, so write `@inbounds` in the body where it is | ||
| warranted, as in a hand-written kernel. `workgroupsize` is passed on to the launch, and by | ||
| default the backend chooses it. | ||
|
|
||
| # Extended help | ||
|
|
||
| `f` becomes the body of a kernel, so it is subject to the same restrictions: every value it | ||
| captures must be of a known type. Closing over a variable of the enclosing *global* scope leaves | ||
| its type unknown and fails to compile (typically with `unsupported dynamic function invocation`), | ||
| which is why the example above wraps the loop in a function. | ||
|
|
||
| For the same reason `f` must not assign to a captured variable, as that makes Julia box the | ||
| capture; accumulate into a one-element array, with an atomic update if the indices race. | ||
|
|
||
| See also [`@kernel`](@ref) to write the kernel out, which is what to reach for when the body | ||
| needs more of the kernel language than an index (workgroup-level indices, local memory, or | ||
| synchronization). | ||
| """ | ||
| function foreach_index(f::F, itr, backend::Backend = get_backend(itr); workgroupsize = nothing) where {F} | ||
| indices = eachindex(itr) | ||
| isempty(indices) && return nothing | ||
| kernel = foreach_index_kernel(indices, backend) | ||
| kernel(f; ndrange = indices, workgroupsize) | ||
| return nothing | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| using KernelAbstractions | ||
| using KernelAbstractions: @atomic | ||
| using Adapt | ||
| using Test | ||
|
|
||
| # An `IndexLinear` array whose `axes1` does not start at 1, so that `eachindex` returns a | ||
| # range of linear indices with an offset. | ||
| struct OffsetVec{T, A <: AbstractVector{T}} <: AbstractVector{T} | ||
| data::A | ||
| r::Base.IdentityUnitRange{UnitRange{Int}} | ||
| end | ||
| OffsetVec(data::AbstractVector, off::Int) = OffsetVec(data, Base.IdentityUnitRange((1 + off):(length(data) + off))) | ||
| Base.size(v::OffsetVec) = size(v.data) | ||
| Base.axes(v::OffsetVec) = (v.r,) | ||
| Base.IndexStyle(::Type{<:OffsetVec}) = IndexLinear() | ||
| Base.@propagate_inbounds Base.getindex(v::OffsetVec, i::Int) = v.data[i - first(v.r) + 1] | ||
| Base.@propagate_inbounds Base.setindex!(v::OffsetVec, x, i::Int) = (v.data[i - first(v.r) + 1] = x) | ||
| KernelAbstractions.get_backend(v::OffsetVec) = KernelAbstractions.get_backend(v.data) | ||
| Adapt.adapt_structure(to, v::OffsetVec) = OffsetVec(adapt(to, v.data), v.r) | ||
|
|
||
| # The loop bodies live in functions so that their captures have known types, as the | ||
| # `foreach_index` docstring requires. | ||
|
|
||
| function foreach_index_copy!(dst, src) | ||
| foreach_index(src) do i | ||
| @inbounds dst[i] = src[i] | ||
| end | ||
| return dst | ||
| end | ||
|
|
||
| function foreach_index_copy_backend!(dst, src, backend) | ||
| foreach_index(src, backend) do i | ||
| @inbounds dst[i] = src[i] | ||
| end | ||
| return dst | ||
| end | ||
|
|
||
| function foreach_index_copy_workgroupsize!(dst, src, workgroupsize) | ||
| foreach_index(src; workgroupsize) do i | ||
| @inbounds dst[i] = src[i] | ||
| end | ||
| return dst | ||
| end | ||
|
|
||
| function foreach_index_fill_index!(v) | ||
| foreach_index(v) do i | ||
| @inbounds v[i] = i | ||
| end | ||
| return v | ||
| end | ||
|
|
||
| function foreach_index_mark!(out, itr) | ||
| foreach_index(itr) do I | ||
| @inbounds out[I] += 1 | ||
| end | ||
| return out | ||
| end | ||
|
|
||
| function foreach_index_sum_range!(out, range, backend) | ||
| foreach_index(range, backend) do i | ||
| @inbounds @atomic out[1] += i | ||
| end | ||
| return out | ||
| end | ||
|
|
||
| function foreach_index_testsuite(Backend, AT) | ||
| backend = Backend() | ||
|
|
||
| @testset "linear index space" begin | ||
| @testset "$(length(dims))-D" for dims in ((1024,), (32, 33), (4, 5, 6)) | ||
| src = AT(reshape(collect(1:prod(dims)), dims)) | ||
| dst = AT(zeros(Int, dims)) | ||
| @test foreach_index_copy!(dst, src) === dst | ||
| synchronize(backend) | ||
| @test Array(dst) == Array(src) | ||
|
|
||
| # every index is visited exactly once | ||
| out = AT(zeros(Int, dims)) | ||
| foreach_index_mark!(out, src) | ||
| synchronize(backend) | ||
| @test all(isone, Array(out)) | ||
| end | ||
| end | ||
|
|
||
| @testset "cartesian index space" begin | ||
| A = AT(zeros(Int, 8, 10)) | ||
| v = view(A, 1:2:8, 2:2:10) | ||
| @test eachindex(v) isa CartesianIndices | ||
| foreach_index_mark!(v, v) | ||
| synchronize(backend) | ||
| ref = zeros(Int, 8, 10) | ||
| ref[1:2:8, 2:2:10] .= 1 | ||
| @test Array(A) == ref | ||
| end | ||
|
|
||
| @testset "offset linear index space" begin | ||
| v = OffsetVec(AT(zeros(Int, 10)), -3) | ||
| @test eachindex(v) == -2:7 | ||
| foreach_index_fill_index!(v) | ||
| synchronize(backend) | ||
| @test Array(v.data) == collect(-2:7) | ||
| end | ||
|
|
||
| @testset "index space without device memory" begin | ||
| # a range carries no backend of its own, so it has to be given | ||
| out = AT(zeros(Int, 1)) | ||
| foreach_index_sum_range!(out, 1:100, backend) | ||
| synchronize(backend) | ||
| @test Array(out)[1] == sum(1:100) | ||
| end | ||
|
|
||
| @testset "workgroupsize" begin | ||
| src = AT(collect(1:1000)) | ||
| dst = AT(zeros(Int, 1000)) | ||
| foreach_index_copy_workgroupsize!(dst, src, 8) | ||
| synchronize(backend) | ||
| @test Array(dst) == Array(src) | ||
| end | ||
|
|
||
| @testset "explicit backend" begin | ||
| src = AT(collect(1:100)) | ||
| dst = AT(zeros(Int, 100)) | ||
| foreach_index_copy_backend!(dst, src, backend) | ||
| synchronize(backend) | ||
| @test Array(dst) == Array(src) | ||
| end | ||
|
|
||
| @testset "empty index space" begin | ||
| src = AT(Int[]) | ||
| dst = AT(Int[]) | ||
| @test foreach_index_copy!(dst, src) === dst | ||
| synchronize(backend) | ||
| @test isempty(Array(dst)) | ||
| end | ||
|
|
||
| @testset "errors" begin | ||
| # an index space that is neither linear nor cartesian | ||
| @test_throws ArgumentError foreach_index(identity, Dict(1 => 2), backend) | ||
| end | ||
| return | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.