Repository navigation
fix(cuda): harden driver error handling, access tracking and host copies - #10871
Conversation
|
@claude review this pr in depth |
|
Claude finished @0ax1's task in 3m 0s —— View job Review of PR #10871: CUDA driver error handling, access tracking and host copies
I didn't find any blocking problems. The error-handling and tracking fixes look right, and each commit does what its message says. The main thing I'd change is that the new check for page-locked memory fails open. Details below, most important first. 1.
|
Merging this PR will improve performance by 13.48%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
`CudaContext::bind_to_thread` returns and clears any error recorded with `record_err` before making the context current. Frees, access tracking and drops all bound through it, so one error recorded anywhere in the context made them bail out early and record the error again: - `CudaSlice::drop` skipped `cuMemFreeAsync`, leaking every allocation dropped while the error was pending. - `Access::after` skipped recording a finished access, so a later free could skip waiting for a read still running on another stream. - `PinnedByteBuffer::drop` failed `CudaEvent::synchronize` and leaked its page-locked allocation. Cleanup and tracking now bind with `make_current`, which leaves recorded errors for the public entry points that can report them, and `CudaEvent::synchronize` waits for its own event regardless of them. A free also used to run even when ordering it after other streams' accesses failed; it now leaks the allocation instead, since those accesses may still be in flight. Signed-off-by: Alexander Droste <[email protected]>
`CudaContext::check_err` documents that it returns the first error recorded where none could be returned, but `record_err` overwrote any pending error, so the original cause was replaced by its consequences, such as failed frees after a failed launch. Only store an error when none is pending. Signed-off-by: Alexander Droste <[email protected]>
`DevicePtr::device_ptr` and `DevicePtrMut::device_ptr_mut` return a pointer and an armed guard even when `Access::before` failed to make the stream wait for earlier conflicting accesses. The caller then enqueues work that may overlap another stream's write, and for writes the guard also records the write as following every earlier read: the next free waits only for this write and can release memory still being read elsewhere. The pointer methods cannot report the failure, so fail closed instead: synchronize the context, after which every earlier access is complete and the access needs no waits. If that fails too, the context is broken and rejects the access's work as well. Signed-off-by: Alexander Droste <[email protected]>
Once a kernel or copy is enqueued, recording its access is the only thing that orders later frees and writes after it. Three paths lost that ordering when recording failed: - `LaunchArgs::launch` returned on the first failed `Access::after`, skipping the remaining arguments, so freeing any of them could overlap the kernel still reading it. - `Access::after` took the previous write event out of the state before recording the new write, and pushed a new read event before recording it. A failed record dropped the write's ordering, or left an unrecorded read event that later waits treat as complete. - `SyncOnDrop` only stored the error. Recording now leaves the state unchanged on failure and returns new events to the pool. Launches and guards record every access, and fall back to synchronizing the stream for any access they could not record, so the access is complete before anything needs to follow it. Signed-off-by: Alexander Droste <[email protected]>
Access tracking takes events from the accessing stream's context and returns them to the pool of the context that later releases them, such as the allocation's. Accessing memory from a stream of another context, for example one made with `CudaContext::new_non_primary`, therefore moved that context's events into the allocation context's pool, where recording them fails with `CUDA_ERROR_INVALID_HANDLE`, and where they outlive the context that owns them. Cross-context access is a caller bug, and the pointer methods cannot return an error, so assert that both streams share the CUDA context before changing any tracking state. Signed-off-by: Alexander Droste <[email protected]>
The copy methods are safe, but bounded their transfers by lengths that safe code outside the driver could misreport: - `DeviceSlice`, `DevicePtr` and `DevicePtrMut` were implementable anywhere, so a wrapper could forward a real allocation's pointer while reporting a `len` and `num_bytes` that disagree, making `clone_dtoh` copy past its vector's capacity. - `memcpy_htod` and `memcpy_dtoh` checked `HostSlice::len`, then copied the slice `stream_synced_slice` returned, which may be longer. Seal the device memory traits, which nothing outside the driver implements, and check host copies against the slices they copy. Signed-off-by: Alexander Droste <[email protected]>
`transmute` and `transmute_mut` checked `len * size_of::<S>()` against the memory's size without checking the multiplication, so in release builds a large `len` wrapped around, passed the check and produced a view far larger than its allocation. Check the multiplication. Their safety contracts also required valid values of `S` but not alignment, although a view sliced at an odd offset yields a pointer misaligned for `S`, and a misaligned access faults the kernel and breaks the context; require that as well. Signed-off-by: Alexander Droste <[email protected]>
Empty uploads, such as `copy_to_device` of an empty slice, allocate zero bytes. `cuMemAllocAsync` returns a null pointer for them, but `cuMemAlloc`, which devices without memory pools use, rejects them with `CUDA_ERROR_INVALID_VALUE`, so empty buffers failed only on those devices. Return a null pointer for empty allocations on both paths, as the pool allocator already did, and skip freeing it. Signed-off-by: Alexander Droste <[email protected]>
`CudaContext::drop` stored the errors of its final driver calls with `record_err`, which writes them into the context being dropped, where nothing can read them. Ignore them explicitly instead. Signed-off-by: Alexander Droste <[email protected]>
`PooledPinnedBuffer::copy_to_device` copied from a view whose `SyncOnDrop` guard recorded the buffer's completion event when the copy was enqueued, then recorded the same event on the same stream again, because only the explicit record's error is returned and decides whether the buffer takes the recovery path. Copy from a plain slice and keep only the checked record, which removes `PinnedByteBufferView` and its `HostSlice` implementation and one driver call per transfer. Signed-off-by: Alexander Droste <[email protected]>
`CudaStream::memcpy_htod` is safe and borrows its source only for the call, but for slices, vectors and arrays it enqueued the copy without waiting. That is sound for pageable memory, which the driver stages before the copy reads it, but the device reads page-locked memory asynchronously. A pooled pinned buffer passed as a plain slice, for example, could be modified or reused while the copy still read it. The guards of borrowed slices now wait for the copy when the slice is page-locked memory. Callers that keep their source alive until the copy completes, such as the pinned buffer pool, use the new unsafe `memcpy_htod_async` to keep their copies asynchronous. Uploads no longer need their future to keep the source alive, so dropping the future before the copy completes no longer frees memory the copy reads. Signed-off-by: Alexander Droste <[email protected]>
Signed-off-by: Alexander Droste <[email protected]>
Disarm only same-stream wait guards after a successful D2H synchronization, preserving error-path waits and event recording. Add guard and pinned-target coverage, and remove the oversized shared-pool leak test. Signed-off-by: Alexander Droste <[email protected]>
Signed-off-by: Alexander Droste <[email protected]>
f7eeeaf to
51ab33f
Compare
Use cuPointerGetAttributes to classify borrowed host slices without the expected INVALID_VALUE error that Compute Sanitizer reports for pageable pointers. Preserve pinned-memory waits and record every actual query failure conservatively. Add focused pageable and pinned guard coverage. Signed-off-by: Alexander Droste <[email protected]>
Consolidate borrowed-pointer error handling, simplify ordered frees, and collect access-tracking errors without short-circuiting. Remove the unused pointer attribute enum alias, explain the D2H wait disarm at its call site, and shrink the pinned-copy test allocation to 1 MiB. Signed-off-by: Alexander Droste <[email protected]>
Signed-off-by: Alexander Droste <[email protected]>
## Summary Follow-up to #11110. Keep the remaining noisy cases out of automatic CodSpeed runs while preserving them for local benchmarks. The Python scalar exclusions in #11110 never took effect because the runner exports simulation as `instrumentation`, while the fixture checked for `simulation`. Use the plugin's resolved mode so these cases are actually skipped. ## Changes - Allow a trailing comma in `#[divan::bench(...)]` when the CPU-feature macro appends its options. The multiline bitpacked take definition covers this case in CI builds. - Share the resolved simulation check between the scalar and filter fixtures. Also skip the 2 integer one-shot filtered scans that still report system calls under simulation. - Keep AVX2 bitpacked take with 80 selections local after batching still produced a regression alert. Retain the 64-selection case and both AVX-512 cases in CI. - Keep AVX-512 nullable integer comparison local because its minimum changes by more than 10% while its median stays stable. - Keep the small random power-law boolean filter and the large correlated power-law boolean filter local under simulation. The former has repeated unexplained changes, and the latter repeatedly makes system calls that simulation cannot measure. <details> <summary>Evidence from recent PR reports</summary> Reviewed 40 CodSpeed report comments updated between November 2 at 19:28 UTC and November 3 at 19:28 UTC, including their comparison revisions and the affected measurement modes. | Case | Evidence | | --- | --- | | Python integer scalar scans | [#11154](#11154 (comment)) still reports the repeated scan's 10 system calls. [A develop run after #11110](https://github.com/vortex-data/vortex/actions/runs/41677647530/job/126062463160) executes both integer scalar cases instead of skipping them. The [runner exports the legacy `instrumentation` value](https://github.com/CodSpeedHQ/codspeed/blob/main/crates/runner-shared/src/measurement_mode.rs), which pytest-codspeed 5.0.3 normalizes to simulation. | | Python integer filtered scans | [#11130](#11130 (comment)) reports 30 system calls totaling 14.3 ms in `test_scan_filter[int]`. [#11138](#11138 (comment)) reports 30 calls totaling 16.3 ms in `test_scan_filter_to_arrow_array[int]`. | | AVX2 `threshold[80]` | [#11004](#11004 (comment)) compares `f7b5b1d864` with `e59b236d7b`: 2.183 to 2.531 µs, reported as a 13.75% regression. Both use 1000 samples of 16 iterations. The head median is 2.556 µs, so this is not just a single outlying minimum. The PR does not change the fastlanes code or benchmark. | | AVX-512 `lt_i64_nullable` | [#11168](#11168 (comment)) reports 7.342 to 8.217 µs. Several unrelated CUDA PRs report the opposite direction, including [#10872](#10872 (comment)), at 8.208 to 7.348 µs. Their head medians remain around 8.3 to 8.5 µs. | | Boolean `filter_powerlaw_by_random[10000]` | [#11167](#11167 (comment)) reports a 16.2% regression, [#11168](#11168 (comment)) a 23.56% improvement, and [#11169](#11169 (comment)) a 13.06% regression. The cause is unknown. | | Boolean `filter_powerlaw_by_correlated_runs[100000]` | [#10871](#10871 (comment)) and [#11165](#11165 (comment)) each report 2 system calls totaling about 44 µs. | The NEON chunked-take alerts on #11090 are on a PR that changes execution substantially, so those cases remain enabled. Other dictionary simulation alerts need more attribution before excluding them. </details> CI passes formatting, Python lint and script tests, Rust lint, default-feature and all-feature Rust builds, all 3 CPU walltime benchmark jobs, and Python benchmarks (36 passed, 4 intentionally skipped). The previously failed simulation shard also passes. Some remaining CI jobs and CodSpeed performance analysis are still pending. No local checks were run. The pre-commit and pre-push hooks were bypassed as requested. --------- Signed-off-by: Connor Tsui <[email protected]>
Summary
Fixes in the CUDA driver wrapper for deferred errors, stream-ordered access
tracking, and host-copy bounds and synchronization. They ship together because
most build on CudaContext::make_current, added by the first commit, and share the
same cleanup and access-tracking paths. Each commit message explains its change.
Borrowed host-pointer classification now binds the context before querying CUDA,
treats only CUDA_ERROR_INVALID_VALUE as pageable memory, and records other errors
while conservatively waiting. D2H copies avoid a redundant synchronization after
a successful explicit wait. Cross-context device access remains rejected, with
the restriction documented on the device-pointer APIs.
Changes