Skip to content

fix: check cudaHostRegister return code and clear CUDA sticky error#65

Merged
jooho-XCENA merged 4 commits into
mainfrom
fix/pin-rc-check
Jul 23, 2026
Merged

fix: check cudaHostRegister return code and clear CUDA sticky error#65
jooho-XCENA merged 4 commits into
mainfrom
fix/pin-rc-check

Conversation

@jooho-XCENA

@jooho-XCENA jooho-XCENA commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

DaxMapper.map_region() ignores the return code of torch.cuda.cudart().cudaHostRegister(...) — torch's cudart binding reports errors via cudaError_t, it does not raise. Two consequences:

  1. False success: a failed pin still logs CUDA pinned region ..., so the region silently runs unpinned (pageable-copy GPU transfers).
  2. Misattributed crash: the failed call latches CUDA's per-thread sticky error, and the next error-checked CUDA call on that thread — typically an unrelated kernel launch — raises torch.AcceleratorError: CUDA error: out of memory. Reproduced with pool_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 cudaHostRegister of >= 2^39 bytes (512 GiB) fails with cudaErrorMemoryAllocation because the driver's per-page bookkeeping table (16 B / 4 KiB page, one flat kvzalloc) hits the kernel's INT_MAX cap — NVRM: failed to allocate page table in 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)

  • Check the cudaHostRegister return code. Success: same log as before, and the region is marked pinned. Failure: consume the sticky error via the process's own libcudart (cudaGetLastError) and log a warning — the region stays usable, GPU transfers fall back to pageable copies, and nothing crashes later.
  • unmap_region()/close() only call cudaHostUnregister for regions that actually pinned, and check that return code too.

Verification

  • 2 new unit tests (rc failure -> warning + no unregister; rc success -> pinned + single unregister); mapper suite 30 passed.
  • Diagnosis aid: with this branch, an oversized pool logs cudaHostRegister failed for region N (...): rc=2 — region stays unpinned instead 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.

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.
jooho-XCENA added a commit that referenced this pull request Jul 15, 2026
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.
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
__init__.py60100% 
__main__.py330%5, 7–8
allocation_manager.py1291191%30–31, 44–45, 52, 237–238, 268–271
client.py187896%88, 130, 147–148, 311–313, 319
config.py46198%72
constants.py90100% 
device_scanner.py943167%25, 100, 102–104, 106, 114–123, 125, 127, 129, 134–143, 145, 147
handler.py5578685%153, 168, 179–186, 194–196, 211, 217–219, 227, 238–243, 248, 273, 300–301, 305, 345, 349–350, 355, 377, 384–385, 389–395, 398, 402–404, 420, 422–423, 432, 486, 507, 517–518, 624–626, 633, 755–758, 761, 772–775, 781, 867, 971, 1134–1138, 1144, 1155–1159, 1165, 1244, 1249, 1291
ipc.py275299%365, 441
kv_manager.py1080100% 
logging_setup.py190100% 
plugin.py59395%65–67
protocol.py2330100% 
resource_manager_installer.py1031387%80–86, 167, 169–172, 187
rpc_async_client.py1900100% 
rpc_async_server.py1110100% 
rpc_client.py710100% 
rpc_client_base.py1061091%185, 221–222, 233–234, 306–307, 311–312, 375
rpc_handler_mixin.py1061982%154–156, 159–161, 219–222, 227, 231–235, 256–257, 263
rpc_server.py640100% 
serializer.py810100% 
server.py1701392%64–65, 73, 168, 172, 243, 247, 265–266, 357–359, 444
stats_manager.py950100% 
types.py60198%145
uds_helpers.py130100% 
memory
   __init__.py50100% 
   allocator.py550100% 
   mapper.py1641790%65–74, 77–80, 293, 342, 376
   owned_region_manager.py101199%212
   types.py630100% 
TOTAL330421993% 

Tests Skipped Failures Errors Time
713 4 💤 0 ❌ 0 🔥 8.204s ⏱️

seohui-XCENA

This comment was marked as duplicate.

@seohui-XCENA seohui-XCENA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread maru_handler/memory/mapper.py Outdated
path = None
with open("/proc/self/maps") as f:
for line in f:
if "libcudart.so" in line:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread maru_handler/memory/mapper.py Outdated
lib.cudaGetLastError.restype = ctypes.c_int
lib.cudaGetLastError()
except (OSError, IndexError, AttributeError):
logger.debug("could not clear CUDA sticky error", exc_info=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the clear fails, a misattributed crash can still happen later — that deserves warning, not debug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed — it warns now whenever nothing could be cleared.

Comment thread maru_handler/memory/mapper.py Outdated
region_id,
handle.length,
)
rc = int(rc[0]) if isinstance(rc, tuple) else int(rc)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

jooho-XCENA and others added 2 commits July 23, 2026 16:41
…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 seohui-XCENA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a565bc1 addresses everything — the parser tests are a nice touch. Thanks for the quick turnaround!

@jooho-XCENA
jooho-XCENA merged commit 42f5ae6 into main Jul 23, 2026
3 checks passed
@jooho-XCENA
jooho-XCENA deleted the fix/pin-rc-check branch July 23, 2026 08:06
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.

2 participants