fix: check cudaHostRegister return code and clear CUDA sticky error#65
Conversation
Minimal diagnostic fix (no chunking): DaxMapper ignored the return code
of torch's cudaHostRegister binding, which reports errors via
cudaError_t instead of raising. A failed pin — e.g. the NVIDIA r580+
per-registration 512GiB limit ("NVRM: failed to allocate page table",
per-page table kvzalloc > INT_MAX) — was logged as "CUDA pinned region"
and left CUDA's per-thread sticky error latched, crashing an unrelated
later kernel launch with a misattributed "CUDA error: out of memory".
- Check rc: success sets MappedRegion._cuda_pinned and keeps the old
log; failure clears the sticky error via the process's own libcudart
and warns that the region stays unpinned (pageable-copy fallback).
- unmap/close only cudaHostUnregister regions that actually pinned,
and check that rc too.
The chunked-registration fix that makes >=512GiB pools actually pin is
feat/chunked-cuda-pin (PR #64); this branch is the rc-check subset for
minimal-risk deployment/diagnosis.
Builds on the rc-check fix (#65) to make >= 512GiB pools actually pin. A single cudaHostRegister call of >= 2^39 bytes (512GiB) fails on NVIDIA r580+ drivers (per-registration page-table kvzalloc > INT_MAX, "NVRM: failed to allocate page table"); the cap is per call, not cumulative, so large regions are registered in chunks. - Register regions in MARU_PIN_CHUNK_GB chunks (default 128GiB, clamped to 256GiB; 0 = single call), recording (addr, size) per pinned chunk on MappedRegion and unregistering per chunk on unmap/close (replaces the single _cuda_pinned flag from #65). - MARU_PIN_MODE=eager (default) pins synchronously inside map_region(), preserving the fully-pinned-on-return contract; MARU_PIN_MODE=lazy pins from a background thread. A per-region pin lock + stop event serialize the pin loop against unmap-time unpinning, bounding teardown to one in-flight chunk. Verified: unit suite 766 passed; E2E on gaia (/dev/dax0.0) with pool_size=520 — previously crashed at startup, now pins 5 chunks in 4.45s and the benchmark passes 20/20 with cache-hit TTFT 1.8s.
seohui-XCENA
left a comment
There was a problem hiding this comment.
LGTM overall — clean, minimal fix. Inline comments below.
Summary: please take a look at the libcudart-matching comment before merge — it can defeat the fix on some torch builds. The log-level and DRY ones are optional cleanups. The multi-host one doesn't block this PR; let's discuss it separately (happy to file an issue).
| path = None | ||
| with open("/proc/self/maps") as f: | ||
| for line in f: | ||
| if "libcudart.so" in line: |
There was a problem hiding this comment.
torch builds that bundle cudart name it libcudart-<hash>.so.X, which this substring doesn't match — path stays None and the sticky error stays latched. Matching "libcudart" is safer.
Also, multiple cudart instances can be loaded (pip nvidia package + system CUDA), and the last-error state is per instance — clear every match, not just the first.
Nit: the maps path can carry an " (deleted)" suffix, which breaks CDLL.
There was a problem hiding this comment.
Good catch — stock torch wheels do bundle it as libcudart-.so.X, so the old match would have missed it entirely. Fixed in a565bc1: match on the basename containing "libcudart", strip the " (deleted)" suffix, and clear every distinct mapped copy (added parser tests for the hashed name, deleted suffix, and dedupe cases).
| lib.cudaGetLastError.restype = ctypes.c_int | ||
| lib.cudaGetLastError() | ||
| except (OSError, IndexError, AttributeError): | ||
| logger.debug("could not clear CUDA sticky error", exc_info=True) |
There was a problem hiding this comment.
If the clear fails, a misattributed crash can still happen later — that deserves warning, not debug.
There was a problem hiding this comment.
Changed — it warns now whenever nothing could be cleared.
| region_id, | ||
| handle.length, | ||
| ) | ||
| rc = int(rc[0]) if isinstance(rc, tuple) else int(rc) |
There was a problem hiding this comment.
This normalization appears 3×, and the unregister block in unmap_region()/close() is nearly identical — worth extracting helpers. A test for the unregister-failure path could come with it.
There was a problem hiding this comment.
Extracted _cuda_rc() and _cuda_unpin_region(); unmap_region()/close() share the unregister path now, and the unregister-failure rc test is in.
| logger.warning( | ||
| "cudaHostRegister failed for region %d " | ||
| "(%d bytes): rc=%d — region stays unpinned, " | ||
| "GPU transfers fall back to pageable copies", |
There was a problem hiding this comment.
The pageable fallback routes transfers through the CPU, so the CXL region is no longer touched by GPU DMA only. This could be a problem for multi-host sharing — probably worth a follow-up discussion.
There was a problem hiding this comment.
Agreed, and out of scope here — worth its own thread. Note the driver-side fix is pending upstream (NVIDIA/open-gpu-kernel-modules#1234), which removes the >=512GiB trigger, but partial pin failure in general still leaves the pageable path so the multi-host question stands.
…in path - _find_libcudart_paths(): match on basename containing "libcudart" so torch wheels' bundled libcudart-<hash>.so.X is found, strip the " (deleted)" maps suffix, and return every distinct mapped copy — the sticky-error state is per instance, so clear all of them. - Warn (not debug) when no libcudart could be cleared. - Extract _cuda_rc() and _cuda_unpin_region(); unmap_region()/close() now share the unregister path. - Tests: maps-parser cases (hashed name, deleted suffix, dedupe, anonymous rows) and the unregister-failure rc path.
seohui-XCENA
left a comment
There was a problem hiding this comment.
a565bc1 addresses everything — the parser tests are a nice touch. Thanks for the quick turnaround!
Problem
DaxMapper.map_region()ignores the return code oftorch.cuda.cudart().cudaHostRegister(...)— torch's cudart binding reports errors viacudaError_t, it does not raise. Two consequences:CUDA pinned region ..., so the region silently runs unpinned (pageable-copy GPU transfers).torch.AcceleratorError: CUDA error: out of memory. Reproduced withpool_size: "520": vLLM EngineCore dies at startup in flashinfer autotune (sm.fill_), pointing at an innocent op.The concrete trigger today is the NVIDIA r580+ driver's per-registration limit: a single
cudaHostRegisterof >= 2^39 bytes (512 GiB) fails withcudaErrorMemoryAllocationbecause the driver's per-page bookkeeping table (16 B / 4 KiB page, one flatkvzalloc) hits the kernel'sINT_MAXcap —NVRM: failed to allocate page tablein dmesg. Confirmed on two hosts (580.126.20, 580.159.03); drivers <= 575.x are unaffected.Change (minimal, no behavior change on the success path)
cudaHostRegisterreturn code. Success: same log as before, and the region is marked pinned. Failure: consume the sticky error via the process's ownlibcudart(cudaGetLastError) and log a warning — the region stays usable, GPU transfers fall back to pageable copies, and nothing crashes later.unmap_region()/close()only callcudaHostUnregisterfor regions that actually pinned, and check that return code too.Verification
cudaHostRegister failed for region N (...): rc=2 — region stays unpinnedinstead of crashing with a fake OOM.Upstream driver fix
The underlying >= 512 GiB failure is an r580 driver regression, and the fix (vzalloc fallback for the oversized page table) is already pending upstream: NVIDIA/open-gpu-kernel-modules#1234 (we reproduced the bug on two hosts and confirmed the same fix compiles against 580.126.20; see the supporting comment there). Once that lands and a fixed driver ships, large single-call registrations simply work again.
So the plan is: merge this rc-check fix (worth having regardless of the driver — a failed pin should warn instead of lying in the logs and crashing an unrelated kernel launch later), and wait for the driver fix for the >= 512 GiB case itself. Until a fixed driver ships, oversized pools run unpinned with a clear warning (pageable-copy performance). The chunked-registration workaround was #64, now closed; the branch is kept in case the upstream fix stalls.