Repository navigation
[fix](mooncake): init per-thread accelerator context for buffer registe… - #179
Conversation
CLA Signature Passys2025-AI, thanks for your pull request. All authors of the commits have signed the CLA. 👍 |
0oshowero0
left a comment
There was a problem hiding this comment.
[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:
- 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 onset_device. - The once-per-process
torch.zeroswarmup cannot be doing what its comment claims, and it hides the fact thataclrtSetDevicealone 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.
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>
Thanks for the review — all points addressed in ebd2b5c:
|
|
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. |
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).
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-callThreadPoolExecutorinput()/get().Why
MooncakeStoreClient.put()/get()create a freshThreadPoolExecutorper call and register/unregister transient buffers inside those worker threads. ACL contexts are thread-local, so the workers had no context:register_buffersilently fell back to host memory;unregister_bufferfailed withINVALID_PARAMS, leaking the registration and flooding logs withaclrtGetCurrentContext failed.Changes
transfer_queue/storage/clients/mooncake_client.py_ensure_accelerator_context(): no-op whentorch.npuis unavailable (CUDA/CPU/generic unaffected), otherwisenpu.set_device(...)for the calling thread with a one-time context warmup;store.setup(...)(the client thread needs a context too);initializer=_ensure_accelerator_contextto bothThreadPoolExecutors (put()andget())._register_all_buffers/_unregister_all_buffersrun only inside those pools, so no other call sites are needed.Tests
tests/test_mooncake_client_thread_context.py(hardware-free):_ensure_accelerator_contextsets the device and warms up only once;put()andget()build their executor with a non-Noneinitializerthat binds the context (executor faked, store faked).Verification on hardware (Ascend 910B + mooncake-transfer-engine 0.3.11.post1)
Before:
Unregister buffer failed/aclrtGetCurrentContext failed1500+ times per short run.After: 0 occurrences; register/unregister succeed; no leak.