Conversation
Kernels can now iterate over a region whose indices do not start at 1.
Each entry of `ndrange` may be a range instead of an extent, and the
whole `ndrange` may be a single range or a `CartesianIndices`, both
statically (`kernel(backend, workgroupsize, (-2:N+3, 0:M+1))`) and at
launch (`ndrange=(-2:N+3, 0:M+1)`). `@index(Global, Cartesian)` and
`@index(Global, NTuple)` return the shifted indices, `@index(Global,
Linear)` counts the region from 1 in column-major order, `@ndrange()`
returns the extents.
Downstream packages such as Oceananigans implement this today by
pirating `partition`, `expand`, `__ndrange` and `__groupsize` with a
custom `_Size` subtype smuggled into `NDRange`'s dynamic-workitems type
parameter, which is fragile and broke with the compiled CPU backend.
Implementation:
- `StaticSize` stores `UnitRange{Int}` axes next to `Int` extents;
`StaticSize(ranges)` and `StaticSize(::CartesianIndices)` normalise
their input, `Base.OneTo` axes become plain extents.
- `NDRange` gains a `mapping` field holding a `StaticOffset` or
`DynamicOffset` (or `nothing`), which `expand` adds to the blocked
index. Plain-size launches keep `mapping === nothing`, so their
`NDRange` types are unchanged.
- `partition` normalises `ndrange`/`workgroupsize` (integer, range,
tuple, `CartesianIndices`) and compares static and launch ndranges by
extents and offsets, so `(1:128,)` matches a static `(128,)`.
- `CompilerMetadata` keeps an offset `CartesianIndices` as `ndrange`
(`CartesianIndices(::CartesianIndices)` would drop the offsets), so
the existing `I in __ndrange(ctx)` bounds check is correct.
- The global linear index is computed by `linear_index`, since
`LinearIndices` only supports 1-based axes.
- The POCL autotune path uses `extents(ndrange)`.
Offsets need no backend changes as long as the workgroup size is static
or given at launch; autotuning a dynamic workgroup size from a range
`ndrange` requires backends to call `extents` where they use
`prod(ndrange)`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHciC8x39gm97sABrSvBkt
|
Looks generally okay, but I wouldn't backport it to 0.9 at this point. Does this have any impact on performance if you don't use offset ranges? |
`linear_index` built a `LinearIndices` from the extents of the `ndrange`. Each `Base.OneTo` axis constructed that way clamps its length at zero, which left two `llvm.smax` per work item in the device code of kernels using `@index(Global, Linear)` on a multi-dimensional `ndrange`, compared to the code generated before index ranges were supported. Folding the column-major index directly from the extents and the axis starts produces the same instruction sequence as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHciC8x39gm97sABrSvBkt
Benchmark script (pass output file as argument)# Launch-overhead and throughput benchmarks for plain size-tuple ndranges on the CPU backend.
# Run once per KernelAbstractions checkout; prints one line per case: name, min and median
# time, allocations per call.
using KernelAbstractions, BenchmarkTools, Statistics, Printf
@kernel function scale_linear!(A, @Const(B))
i = @index(Global, Linear)
@inbounds A[i] = 2 * B[i]
end
@kernel function scale_cartesian!(A, @Const(B))
i, j, k = @index(Global, NTuple)
@inbounds A[i, j, k] = 2 * B[i, j, k]
end
# 3-D ndrange, linear global index (exercises the linear-index computation)
@kernel function scale_linear3d!(A, @Const(B))
i = @index(Global, Linear)
@inbounds A[i] = 2 * B[i]
end
backend = CPU()
results = Dict{String, Tuple{Float64, Float64, Int}}()
function record!(name, trial)
results[name] = (minimum(trial).time, median(trial).time, trial.allocs)
return @printf("%-40s min %10.3f μs median %10.3f μs allocs %5d\n", name, minimum(trial).time / 1e3, median(trial).time / 1e3, trial.allocs)
end
BenchmarkTools.DEFAULT_PARAMETERS.seconds = 3
## Host-side partition cost (no launch)
let k_dd = scale_linear!(backend), k_sd = scale_linear!(backend, 16), k_ss = scale_linear!(backend, 16, (1024,))
nd, wg = Ref((1024,)), Ref((16,))
record!("partition dyn-wg dyn-nd", @benchmark KernelAbstractions.partition($k_dd, $nd[], $wg[]))
record!("partition static-wg dyn-nd", @benchmark KernelAbstractions.partition($k_sd, $nd[], nothing))
record!("partition static-wg static-nd", @benchmark KernelAbstractions.partition($k_ss, nothing, nothing))
end
## Launch overhead: tiny problem, one workgroup
let n = 16, A = zeros(n), B = rand(n)
k = scale_linear!(backend)
record!("launch dyn-wg dyn-nd (autotune)", @benchmark $k($A, $B; ndrange = ($n,)))
record!("launch dyn-wg dyn-nd (wg given)", @benchmark $k($A, $B; ndrange = ($n,), workgroupsize = ($n,)))
k = scale_linear!(backend, n)
record!("launch static-wg dyn-nd", @benchmark $k($A, $B; ndrange = ($n,)))
k = scale_linear!(backend, n, (n,))
record!("launch static-wg static-nd", @benchmark $k($A, $B))
end
let dims = (4, 4, 4), A = zeros(dims), B = rand(Float64, dims)
k = scale_cartesian!(backend, dims)
record!("launch 3d static-wg dyn-nd", @benchmark $k($A, $B; ndrange = $dims))
k = scale_cartesian!(backend, dims, dims)
record!("launch 3d static-wg static-nd", @benchmark $k($A, $B))
end
## Throughput: 2^23 Float64 elements (64 MiB per array)
let n = 2^23, A = zeros(n), B = rand(n)
k = scale_linear!(backend, 256)
record!("throughput 1d linear", @benchmark $k($A, $B; ndrange = ($n,)))
@assert A == 2B
end
let dims = (256, 256, 128), A = zeros(dims), B = rand(Float64, dims)
k = scale_cartesian!(backend, (16, 16, 1))
record!("throughput 3d cartesian", @benchmark $k($A, $B; ndrange = $dims))
@assert A == 2B
k = scale_linear3d!(backend, (16, 16, 1))
fill!(A, 0)
record!("throughput 3d linear-index", @benchmark $k($A, $B; ndrange = $dims))
@assert A == 2B
k = scale_cartesian!(backend, (16, 16, 1), dims)
fill!(A, 0)
record!("throughput 3d cartesian static-nd", @benchmark $k($A, $B))
@assert A == 2B
end
# Ragged ndrange: every workgroup row is partially filled, so the bounds check is live
let dims = (250, 250, 120), A = zeros(dims), B = rand(Float64, dims)
k = scale_cartesian!(backend, (16, 16, 1))
record!("throughput 3d cartesian ragged", @benchmark $k($A, $B; ndrange = $dims))
@assert A == 2B
k = scale_linear3d!(backend, (16, 16, 1))
fill!(A, 0)
record!("throughput 3d linear-index ragged", @benchmark $k($A, $B; ndrange = $dims))
@assert A == 2B
end
open(ARGS[1], "w") do io
for (name, (mn, md, al)) in sort!(collect(results))
println(io, name, "\t", mn, "\t", md, "\t", al)
end
endSummary of results:
Bot comments:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #764 +/- ##
==========================================
+ Coverage 63.72% 64.70% +0.98%
==========================================
Files 23 23
Lines 1935 1989 +54
==========================================
+ Hits 1233 1287 +54
Misses 702 702 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Can you add these as benchmarks to our current benchmark file? |
|
What's the difference that made you say #765 (comment) over there but not here? Is it that this allows not materializing the index array whereas in #765 the index array already exists? |
|
The history goes back to #399 and #403 back then I was hesitant since I was unsure if this is something that folks generally needed. I think my issue with using an array is that it overloads the semantics of ndrange too much. I think I would be happier with something like |
|
Closing in favour of #771. |
Part 1/2 of upstreaming Oceananigans piracy of KA (part 1 is #765). Ref: #757. I'm open to adapt the implementation if necessary. I also have ready a backport to the v0.9 branch, but will open later after the design here is finalised.
CC @simone-silvestri.