Skip to content

Support arrays with offset axes, and fix other indexing bugs - #177

Merged
maleadt merged 9 commits into
tb/strided-reduce-shapesfrom
tb/cleanup-fixes
Oct 25, 2026
Merged

maleadt merged 9 commits into
tb/strided-reduce-shapesfrom
tb/cleanup-fixes

Conversation

@maleadt

@maleadt maleadt commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Bugs found while auditing the package for a cleanup. The main one: most operations index arrays from 1, so arrays whose axes start elsewhere (e.g. an OffsetArray wrapping a CuArray, as Oceananigans uses) gave wrong results or were read and written out of bounds:

  • on the host, reductions (whole-array and along dims, and mapreducedim! from such a source) reduced the wrong elements, because Auto takes the threaded path, which reduces src[irange] with 1-based ranges, even for small inputs;
  • scans threw a BoundsError on the host and wrote the wrong elements on GPUs;
  • sort!/sortperm! threw on the host and indexed out of bounds in GPU kernels; along dims they were rejected;
  • a one-element source of a GPU reduction, any/all, reverse! along dims, and searchsorted*! with offset queries or results were read or written out of bounds.

Rather than teaching every kernel about offsets, the operations now hand their kernels and threaded loops 1-based versions of the arrays at the host entry: a reshape for arrays of several dimensions (whose linear indexing already starts at 1, and which OffsetArrays turns into the parent array, so an OffsetArray{CuArray} reaches the kernels as a plain CuArray), a view for vectors, and likewise for the arrays in a Broadcasted source. The checks (shapes, aliasing, workspaces) still see the caller's arrays. sortperm! returns indices of the source, as Base.sortperm does. Copies and fills into such views run as kernels on GPUs, since Base's would index element by element. Arrays whose axes start at 1 take the same paths as before.

Smaller fixes:

  • task_partition(f, tp) and itask_partition(f, tp) threw a BoundsError when the TaskPartitioner had a single task (e.g. on one thread), as such a partitioner stores no tasks.
  • reverse!(dst, src) threw a MethodError for Cartesian-indexed sources such as view(A, 1:2, 1:3).
  • The threaded any/all set their result from several tasks through a plain Ref; it is now atomic.
  • Before Julia 1.12, reductions of several arrays or of a Broadcasted object materialize it first, with copyto!(similar(bc), bc). For arrays without a GPU broadcast style (lazy wrappers, or offset arrays around GPU arrays) that indexed the GPU arrays element by element, which GPU backends disallow. A kernel materializes it now.

Each fix comes with tests that fail without it, using a minimal offset-axes array type in the test setup. Tested on CPU threads, PoCL (also with AK's kernels on the host backend) and CUDA.

A TaskPartitioner that ends up with one task, e.g. on a single thread
or with fewer than twice `min_elems` elements, stores no tasks, so
passing it to task_partition or itask_partition threw a BoundsError.
Call `f` on the whole range instead.
reverse!(dst, src) offset the destination index by the source's
`eachindex`, which throws for sources with Cartesian indices such as a
view of a matrix.
The ConcurrentWrite kernel and the threaded algorithm read `v[i]` for
`i` in `1:length(v)`, which reads out of bounds for arrays whose axes
do not start at one. The threaded algorithm also set its result flag
from several tasks without synchronization; make it atomic.
The threaded reductions index their source from 1, so arrays whose axes
start elsewhere (an `OffsetArray`, for instance) reduced the wrong
elements on the host, whole and along `dims`, also as the source of
`mapreducedim!`. On GPUs, a one-element source was read out of bounds,
also by `any`/`all` with `ViaReduce`.

Reductions now pass the kernels and threaded loops the source and
destination with axes starting at 1: a reshape of arrays of several
dimensions, whose linear indexing already starts at 1 (and which
`OffsetArrays` makes the parent array), and a view of vectors. Fills of
such views on GPUs run as a kernel, as Base's `fill!` would index them
element by element.

The tests get a minimal array type with offset axes, which replaces the
one of the reduction tests.
The threaded whole-array scan indexes its array from 1, so it read out
of bounds for arrays whose axes start elsewhere, and the GPU scans wrote
the wrong elements. `accumulate` could not scan such arrays at all, as
the result it allocates has axes starting at 1, which `accumulate!`
rejected for an offset source.

As for reductions, the scans now run on views of the destination and
source with axes starting at 1. Copies into such views from the scratch
array, and fills of them, run as kernels on GPUs.
The sorts index their arrays from 1, so arrays whose axes start
elsewhere threw a `BoundsError` on the host and were read and written
out of bounds by the GPU kernels; along `dims` they were rejected.

The sorts now run on views of their arrays with axes starting at 1, as
reductions and scans do. `sortperm!` and `sortperm` shift the
permutation into the source's own linear indices, as Base's do, and
`sortperm` along `dims` no longer rejects its own result, whose axes
start at 1. The GPU merge sorts initialize the permutation with linear
indices also for Cartesian-indexed index arrays.
The mirror index of an element along `dims` was computed from 1-based
coordinates, which indexed arrays whose axes start elsewhere out of
bounds. Reverse 1-based views of them instead, as the other operations
do.
The batched searches only require `ix` and `xs` to have the same
length, but wrote `ix[i]` for `i in eachindex(xs)`, out of bounds when
either has offset axes.
@maleadt
maleadt added this pull request to stack #178 October 24, 2026 14:57
Before Julia 1.12, reductions of several arrays or of a `Broadcasted`
object first materialize it, as it cannot be indexed linearly. That
used `copyto!(similar(bc), bc)`, whose broadcast style comes from the
arrays: for array types without a GPU broadcast style, such as lazy
wrappers or arrays with offset axes around GPU arrays, it allocated a
host array and indexed the GPU arrays element by element, which GPU
backends disallow. Evaluate it with a kernel on the reduction's backend
instead, through Cartesian indices.
@maleadt
maleadt merged commit 10f4659 into main Oct 25, 2026
34 of 35 checks passed
@maleadt
maleadt deleted the tb/cleanup-fixes branch October 25, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant