Skip to content

[fix](mooncake): init per-thread accelerator context for buffer registe… - #179

Merged
0oshowero0 merged 1 commit into
Ascend:mainfrom
ys2025-AI:main
Oct 8, 2026
Merged

0oshowero0 merged 1 commit into
Ascend:mainfrom
ys2025-AI:main

Conversation

@ys2025-AI

Copy link
Copy Markdown
Contributor

What

Bind the Ascend ACL device context on every thread that calls Mooncake register_buffer/unregister_buffer, i.e. the worker threads of the per-call ThreadPoolExecutor in put()/get().

Why

MooncakeStoreClient.put()/get() create a fresh ThreadPoolExecutor per call and register/unregister transient buffers inside those worker threads. ACL contexts are thread-local, so the workers had no context:

  • register_buffer silently fell back to host memory;
  • unregister_buffer failed with INVALID_PARAMS, leaking the registration and flooding logs with aclrtGetCurrentContext failed.

Changes

transfer_queue/storage/clients/mooncake_client.py

  • add _ensure_accelerator_context(): no-op when torch.npu is unavailable (CUDA/CPU/generic unaffected), otherwise npu.set_device(...) for the calling thread with a one-time context warmup;
  • call it once before store.setup(...) (the client thread needs a context too);
  • pass it as initializer=_ensure_accelerator_context to both ThreadPoolExecutors (put() and get()).

_register_all_buffers/_unregister_all_buffers run only inside those pools, so no other call sites are needed.

Tests

tests/test_mooncake_client_thread_context.py (hardware-free):

  • _ensure_accelerator_context sets the device and warms up only once;
  • it is a no-op when no NPU is available;
  • put() and get() build their executor with a non-None initializer that binds the context (executor faked, store faked).
python -m pytest -q tests/test_mooncake_client_thread_context.py   # 4 passed
python -m compileall -q transfer_queue tutorial tests
ruff check / ruff format --check                                   # clean

Verification on hardware (Ascend 910B + mooncake-transfer-engine 0.3.11.post1)

Before: Unregister buffer failed / aclrtGetCurrentContext failed 1500+ times per short run.
After: 0 occurrences; register/unregister succeed; no leak.

@ascend-robot

Copy link
Copy Markdown

CLA Signature Pass

ys2025-AI, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@0oshowero0 0oshowero0 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.

[AI Review] Duplicate submission (tooling retry) — superseded by the review below, which carries the same findings as inline comments.

@0oshowero0 0oshowero0 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.

[AI Review]

The root-cause analysis is right: ACL contexts are thread-local, put()/get() build a fresh ThreadPoolExecutor per call, and binding the device in the pool initializer is the cheapest correct hook. The claim that _register_all_buffers/_unregister_all_buffers only run inside those two pools also checks out — the GDR path does register on the caller thread, but it is gated by torch.cuda.is_initialized() and is unreachable on Ascend.

Two things I think should be fixed before merge:

  1. The device index is resolved inside the context-less worker thread, so npu.current_device() always reports 0 there and the workers never bind the device the client actually uses. Details in the inline comment on set_device.
  2. The once-per-process torch.zeros warmup cannot be doing what its comment claims, and it hides the fact that aclrtSetDevice alone is already sufficient.

The rest is reducible. The torch_npu probe can be resolved once at module import (MOONCAKE_STORE_IMPORTED already shows the pattern in this file), the module-level mutable flag can disappear with the warmup, and roughly two thirds of the new test file either asserts that a line of code was written or is never exercised at all.

A shape that keeps the fix while dropping the global state, the per-call import, the swallowed failure and the wrong device (~10 lines instead of 33):

try:
    import torch_npu  # noqa: F401

    NPU_IMPORTED = True
except ImportError:
    NPU_IMPORTED = False
        # ACL contexts are thread-local: the per-call pool workers must bind the
        # same device as the client, otherwise unregister_buffer fails with
        # INVALID_PARAMS and leaks the registration.
        self._npu_device = (
            torch.npu.current_device() if NPU_IMPORTED and torch.npu.is_initialized() else None
        )
    def _bind_npu_device(self) -> None:
        if self._npu_device is None:
            return
        try:
            torch.npu.set_device(self._npu_device)
        except Exception:
            logger.warning(f"Failed to bind NPU device {self._npu_device} on a worker thread.", exc_info=True)

with initializer=self._bind_npu_device on both executors, plus one test asserting that the worker binds the device captured at construction time rather than 0.

Comment thread transfer_queue/storage/clients/mooncake_client.py Outdated
Comment thread transfer_queue/storage/clients/mooncake_client.py Outdated
Comment thread transfer_queue/storage/clients/mooncake_client.py Outdated
Comment thread transfer_queue/storage/clients/mooncake_client.py Outdated
Comment thread transfer_queue/storage/clients/mooncake_client.py Outdated
Comment thread tests/test_mooncake_client_thread_context.py Outdated
Comment thread tests/test_mooncake_client_thread_context.py Outdated
Comment thread tests/test_mooncake_client_thread_context.py Outdated
Mooncake's Ascend transport is bound to a thread-local ACL context. put()/get()
run register_buffer/unregister_buffer on per-call ThreadPoolExecutor workers that
never called npu.set_device, so unregister_buffer failed with INVALID_PARAMS
(aclrtGetCurrentContext failed) and the registered host buffers were leaked.

Capture the device once on the owning thread in __init__ and bind it in each pool
worker through the executor initializer. Gate on is_initialized() so a process
that does not use the NPU is left untouched, and add hardware-free unit tests for
the capture/bind behavior.

Signed-off-by: ys2025-AI <yuanshuaien@163.com>
@ys2025-AI

Copy link
Copy Markdown
Contributor Author

[AI Review]

The root-cause analysis is right: ACL contexts are thread-local, put()/get() build a fresh ThreadPoolExecutor per call, and binding the device in the pool initializer is the cheapest correct hook. The claim that _register_all_buffers/_unregister_all_buffers only run inside those two pools also checks out — the GDR path does register on the caller thread, but it is gated by torch.cuda.is_initialized() and is unreachable on Ascend.

Two things I think should be fixed before merge:

  1. The device index is resolved inside the context-less worker thread, so npu.current_device() always reports 0 there and the workers never bind the device the client actually uses. Details in the inline comment on set_device.
  2. The once-per-process torch.zeros warmup cannot be doing what its comment claims, and it hides the fact that aclrtSetDevice alone is already sufficient.

The rest is reducible. The torch_npu probe can be resolved once at module import (MOONCAKE_STORE_IMPORTED already shows the pattern in this file), the module-level mutable flag can disappear with the warmup, and roughly two thirds of the new test file either asserts that a line of code was written or is never exercised at all.

A shape that keeps the fix while dropping the global state, the per-call import, the swallowed failure and the wrong device (~10 lines instead of 33):

try:
    import torch_npu  # noqa: F401

    NPU_IMPORTED = True
except ImportError:
    NPU_IMPORTED = False
        # ACL contexts are thread-local: the per-call pool workers must bind the
        # same device as the client, otherwise unregister_buffer fails with
        # INVALID_PARAMS and leaks the registration.
        self._npu_device = (
            torch.npu.current_device() if NPU_IMPORTED and torch.npu.is_initialized() else None
        )
    def _bind_npu_device(self) -> None:
        if self._npu_device is None:
            return
        try:
            torch.npu.set_device(self._npu_device)
        except Exception:
            logger.warning(f"Failed to bind NPU device {self._npu_device} on a worker thread.", exc_info=True)

with initializer=self._bind_npu_device on both executors, plus one test asserting that the worker binds the device captured at construction time rather than 0.

Thanks for the review — all points addressed in ebd2b5c:

  • Wrong device on workers: device is now captured once on the owning thread in init (_resolve_npu_device); workers bind that value via _bind_npu_device, never calling current_device() on a context-less thread.
  • Misleading warmup: removed torch.zeros and the module-level flag entirely; set_device alone is relied upon.
  • Swallowed failure: bind/resolve failures now logger.warning(..., exc_info=True).
  • Import probe: torch_npu is resolved once at import (NPU_IMPORTED); is_available() replaced with is_initialized() so non-NPU processes are untouched.
  • Tests: dropped _FakeStore and the two tautological executor tests; new tests cover device capture, the not-initialized no-op, binding the captured device (not 0), and logged-and-swallowed bind failure.

@Chase-Rong

Chase-Rong commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

LGTM. Thanks for your fix. Validated this PR on Ascend A3 hardware with mooncake-transfer-engine-npu 0.3.11.post1.

Results:

• Worker device binding: With the owning thread on npu:1, the base version’s workers used device 0 and produced 24 registration failures and 24 unregistration failures (-600). With this PR, all 24 workers had a valid ACL context on device 1; all 159 registrations and 159 unregistrations returned 0.

• Real data transfers: CPU tensor/object batches completed three put/get/clear rounds with 802 keys per round. NPU tensor/object transfers also completed three rounds using the ascend protocol. All data-integrity and key-removal checks passed.

• Without diagnostic instrumentation: The base version logged 22 occurrences each of aclrtGetCurrentContext failed and Unregister buffer failed; both counts dropped to zero with this PR.

@0oshowero0 0oshowero0 changed the title fix(mooncake): init per-thread accelerator context for buffer registe… [fix](mooncake): init per-thread accelerator context for buffer registe… Oct 8, 2026
@0oshowero0
0oshowero0 merged commit c01270f into Ascend:main Oct 8, 2026
8 checks passed
Ginray added a commit to Ginray/TransferQueue that referenced this pull request Oct 9, 2026
Sync 11 main PRs (Ascend#162, Ascend#167, Ascend#168, Ascend#170-Ascend#172, Ascend#174, Ascend#175, Ascend#177-Ascend#179)
into the payload-transfer branch. Main's PR history is preserved; the
payload transfer abstraction stays on top.

Conflict resolutions:
- SimpleStorageUnit: keep main's config-dict constructor, hybrid
  memory/SSD storage, proxy identity filter, arrival counters, accept
  probe, and checkpoint delegation; the worker dispatches PUT/GET
  families to the configured PayloadTransfer as before.
- AsyncSimpleStorageManager: keep main's socket pools, retry with
  diagnosis, and heavy-request logging; per-unit put/get delegate to
  the configured PayloadTransfer.
- Bootstrap: keep main's config-dict actor construction plus payload
  transfer validation and NIXL endpoint collection.

Semantic adaptations (not textual conflicts):
- Add PayloadTransferTimeout (base.py) carrying wire_bytes; both
  backends raise it on an unanswered control message, and the manager
  translates it to StorageUnitTimeout so Ascend#171 retry/diagnosis covers
  the ZMQ and NIXL-UCX paths uniformly.
- PayloadTransfer.put returns payload bytes moved, so Ascend#171's manager
  side heavy-put logging keeps true wire sizes per backend.

Verified: python -m compileall -q transfer_queue tutorial tests clean;
ruff check clean; pytest 622 passed, 8 skipped, 8 errors (the 8 errors
are the pre-existing optional-Yuanrong-backend baseline).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants