Repository navigation
Make unsafe_wrap of host memory safe, and support wrapping Arrays - #1116
Merged
Merged
Conversation
This was referenced Sep 29, 2026
gbaraldi
reviewed
Oct 1, 2026
gbaraldi
left a comment
Member
There was a problem hiding this comment.
Tested on MI300A + MI250 (ROCm 7.2.4):
- CI failure is the test. Inside the
@testset,wrap_tracked'sa = …andxd = …assign the testset'sa/xd, so neither ever gets collected.local a, xdfixes it (1.12 and 1.13). - Freeing during a capture.
unsafe_free!(xd)insidecapture() do … end, wherexdwas last used on another stream:HIP.isdonecallshipStreamQuery, which returns 900 during a global-mode capture and invalidates it. The wrapper leaks and the stream stays broken (next sync 900, next launch 901). main is fine. Use relaxed capture mode for the release calls, or defer them while capturing. - Register/unregister race.
Mem.unregistercallshipHostUnregisterafter dropping__pin_lock, so a concurrentregisterof the same pointer sees Host memory and doesn't track it. 2 threads × 3000 wrap/free of one Array:hipErrorInvalidValueinget_device_ptrin 5 of 6 runs. Pre-existing forown=true, but default wraps with async releases now hit it. Make the HIP calls under the lock. - Larger re-wrap. Re-wrapping a pointer at a larger size before its pending release has run reuses the old registration (the
count > 0path ignores the size) →hipErrorIllegalAddress. Same on main, but an explicitunsafe_free!could wait for the release (cf. CUDA.jl#3308). - With #1120,
managed.streammay belong to another task by release time; release immediately if it was recycled.
maleadt
force-pushed
the
tb/unsafe_wrap_lifetime
branch
from
October 2, 2026 12:20
a1c1de3 to
7506dbd
Compare
maleadt
added this pull request to stack #1130
October 2, 2026 12:20
Member
Author
|
Addressed review comments, and put in a stack to handle the stream recycling. |
maleadt
force-pushed
the
tb/unsafe_wrap_lifetime
branch
from
October 6, 2026 07:34
7506dbd to
243bc12
Compare
Wrapping unregistered host memory with `unsafe_wrap(ROCArray, ptr)` page-locks it with a refcounted `hipHostRegister`, but with the default `own=false` the wrapper never undid that registration: its finalizer did nothing, and `free(::HostBuffer)` returns early for unowned buffers. The memory stayed pinned forever, and because the refcount is keyed by address, wrapping new memory that happens to reuse that address skipped registering it. The wrapper now drops its reference to the registration when freed, without freeing memory it doesn't own. Releasing wrapped host memory, owned or not, first waits for the device to stop using it, polling the stream so that the thread stays available to service hostcalls. Finalizers cannot yield, so when a wrapper is finalized the release is queued for a background task, started the first time host memory is wrapped. Explicit `unsafe_free!` still releases immediately. If waiting fails, the memory stays registered and rooted rather than risking a use after free. Also add `unsafe_wrap(ROCArray, ::Array)`, which keeps the array alive for as long as the wrapper exists, and `unsafe_wrap(Array, ::ROCArray)` for host-backed arrays, documenting that the latter does not keep the ROCArray alive. Copies now preserve their operands until the copy has been submitted, so that a wrapper can't be finalized (unregistering its memory) in between.
Replace the global release queue, its service task and the polling loop with a host function launched on the stream that last used the memory. It signals an async condition once the device is done with the memory, and the task waiting for that condition releases it. That task keeps the wrapped Array alive, so failing to signal it leaks the memory without needing a global list, and it isn't affected by task cancellation. Explicitly freeing a wrapper that the device is done with still releases it right away.
- Query and launch on the stream with a relaxed capture mode, so that freeing a wrapper while another stream is being captured doesn't invalidate the capture. - Release immediately if the stream was recycled to another task, since the work has finished and the stream may be captured by its new owner. - Unregister host memory while holding the pin lock, so a concurrent register doesn't see it as externally pinned. Querying the memory type also takes the lock, as HIP can crash when it races with hipHostUnregister. - Wait for the release when a wrapper is freed explicitly, so the memory can be wrapped again right away, and refuse to extend a registration still in use. - Fix the test that checks the wrapped array is released.
maleadt
force-pushed
the
tb/unsafe_wrap_lifetime
branch
from
October 6, 2026 20:04
243bc12 to
c5843a1
Compare
Contributor
AMDGPU.jl BenchmarksDetails
This comment was automatically generated by workflow using github-action-benchmark. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
unsafe_wrap(ROCArray, ...)lets the GPU work directly on host memory, without copying it. This PR makes that safe to use, and adds theArrayconvenience method that CUDA.jl, Metal.jl and OpenCL.jl already have:The wrapper keeps
aalive for as long as it's used, so wrapping a temporary array is fine. You can still wrap a raw pointer, in which case keeping the memory alive is up to you. The other direction,unsafe_wrap(Array, ::ROCArray), now works for arrays backed by host memory. Its docstring warns that the returnedArraydoesn't keep theROCArrayalive.Why
Wrapping host memory page-locks it with
hipHostRegister, but with the defaultown=falseit was never unregistered:Besides leaking pinned memory, this meant a later allocation at the same address silently reused the stale registration. Wrapping the same pointer again at a larger size then crashed:
How
The registration is undone when the wrapper is freed, but only once outstanding GPU work on it has finished. Finalizers can't wait for the GPU: they can't switch tasks, and blocking the thread could deadlock with kernels doing host calls. So freeing a wrapper launches a host function on the stream that last used it. When the GPU reaches it, that host function wakes a Julia task, which unregisters the memory and lets go of the wrapped
Array. If anything prevents the GPU from getting there, the memory stays registered and theArraystays alive, rather than being released while it may still be in use.When you free a wrapper explicitly with
unsafe_free!, the call waits for the release, so the memory can be wrapped again right away. Some cases need special handling:hipPointerGetAttributesandhipHostUnregisterrun concurrently on the same pointer, so those calls are now serialized.Testing
@gbaraldi tested an earlier version on MI300A and MI250 (thanks!), and the problems found there are fixed and covered by tests. The new tests in
test/core/rocarray_base.jlfail on that version and pass now. The full test suite passes on a gfx1036 iGPU (ROCm 7.2.4). The only failure is anInt128 axpby!test marked broken that unexpectedly passes, which also happens on the commit this stack is based on.