Skip to content

Fix JLArray buffer-backed temporaries against JLArrays 0.3.2 - #296

Open
lkdvos wants to merge 2 commits into
mainfrom
fix-jlarrays-offset-semantics
Open

Fix JLArray buffer-backed temporaries against JLArrays 0.3.2#296
lkdvos wants to merge 2 commits into
mainfrom
fix-jlarrays-offset-semantics

Conversation

@lkdvos

@lkdvos lkdvos commented Aug 7, 2026

Copy link
Copy Markdown
Member

Fixes CI on main.

What broke

JLArrays 0.3.2 changed JLArray's offset field from a count of elements to a count of bytes:

0.3.1 0.3.2
field doc offset … in number of elements offset … in bytes
pointer pointer(x.data) + x.offset * Base.elsize(x) pointer(x.data) + x.offset

unsafe_buffer_wrap constructed the temporary with the JLArray constructor directly, passing offset = Int(start) ÷ sizeof(T). Under 0.3.1 that came back out as base + start; under 0.3.2 it lands at base + start ÷ sizeof(T). So every buffer-backed temporary was misaligned and overlapped both its neighbours and earlier temporaries, silently corrupting results — e.g. 382943.97 where -199.47 was expected. 48 failures in test/allocator.jl, reproduced locally.

ROCArray already passed a byte offset directly and was unaffected.

Why CI was green on #295

JLArrays 0.3.2 was released between #295's PR run (15:45 UTC, resolved 0.3.1, green) and its merge run (18:09 UTC, resolved 0.3.2) — and that merge run was cancelled by the next push, so the failure was never surfaced. JLArrays = "0.3" allowed the upgrade silently.

The fix

Use GPUArrays.derive, the documented backend hook for producing an array of a different type and size backed by the same data (what reshape and contiguous views go through). Its additional_offset is expressed in elements on both 0.3.1 and 0.3.2 — the backend absorbs the representation change — so this is insensitive to how the offset happens to be stored. It also drops the direct use of GPUArrays.storage plus the private constructor.

Verified green on both JLArrays 0.3.1 and 0.3.2 (test/allocator.jl 134/134, test/gpu.jl including the CuArray sets). Since derive removes the version sensitivity, compat gets a 0.3.1 lower bound — matching exactly what was verified — rather than a hard pin to 0.3.2, which would needlessly exclude 0.3.1.

The existing tests already catch the regression, so no new ones were added.

Worth reporting the semantics change upstream as well — it is breaking, and shipped in a patch release.

🤖 Generated with Claude Code

lkdvos and others added 2 commits August 7, 2026 15:06
JLArrays 0.3.2 changed `JLArray`'s `offset` field from a count of elements
to a count of bytes, and `pointer` from `pointer(x.data) + x.offset*elsize(x)`
to `pointer(x.data) + x.offset`. `unsafe_buffer_wrap` built the temporary with
the constructor directly, so under 0.3.2 every buffer-backed temporary landed
at byte `start ÷ sizeof(T)` instead of `start`: misaligned, and overlapping
both each other and earlier temporaries, which silently corrupted results.

Go through `GPUArrays.derive` instead, the documented backend hook for
producing an array of a different type and size backed by the same data. Its
offset is expressed in elements on both versions, so this is insensitive to
how the backend stores it. `ROCArray` already used a byte offset directly and
was unaffected.

Compat gets a `0.3.1` lower bound, matching the versions this was verified on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`unsafe_buffer_wrap` was written three times, once per backend, and each
reached for a different private detail: `JLArray`'s constructor with an
element offset, `ROCArray`'s with a byte offset, and `unsafe_wrap` on a raw
`CuPtr`. The first of those is what broke against JLArrays 0.3.2.

Route all three through `GPUArrays.derive` in a new `TensorOperationsGPUArraysExt`
instead. It is the documented backend hook for producing an array of a different
type and size backed by the same data, its offset is in elements on every
backend and version, and sharing the storage's refcounted handle keeps the
buffer alive for as long as a temporary derived from it -- which the `CuArray`
path, wrapping a bare pointer, did not do.

`buffer_arraytype` likewise collapses into one method, via a new
`buffer_similartype` that asks the buffer's own storage what it produces for the
requested element type and rank. This resolves at compile time and reproduces
each backend's answer exactly, memory space and buffer type included. It stays
out of the core default because a storage is not always the kind of array it
hands out: `similar(::Memory{UInt8}, T, Dims{1})` is a `Memory`, not a `Vector`.

Since `derive` offsets by whole elements, the element-addressability restriction
that only `JLArray` applied now covers all three, as `buffer_iselementaddressable`.
Element types it rejects fall back on a regular allocation, so this costs buffer
backing for exotic types, never correctness.

Verified against JLArrays 0.3.1 and 0.3.2, so the compat bound stays at "0.3".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
ext/TensorOperationsAMDGPUExt.jl 62.85% <ø> (+62.85%) ⬆️
ext/TensorOperationsCUDACoreExt.jl 90.24% <ø> (+90.24%) ⬆️
ext/TensorOperationsGPUArraysExt.jl 100.00% <100.00%> (ø)
ext/TensorOperationsJLArraysExt.jl 100.00% <ø> (ø)
src/implementation/allocator.jl 95.96% <100.00%> (+0.31%) ⬆️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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