Repository navigation
[None][feat] Use pooled NCCL windows for expert-parallel dispatch and combine #20631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fafda83
cd2dd70
8c83351
2bb7bc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,8 @@ | |
|
|
||
| #include "tensorrt_llm/thop/outputTensor.h" | ||
|
|
||
| #include <c10/cuda/CUDAGuard.h> | ||
| #include <torch/custom_class.h> | ||
| #include <torch/extension.h> | ||
|
|
||
| #include <vector> | ||
|
|
@@ -37,6 +39,102 @@ TRTLLM_NAMESPACE_BEGIN | |
| namespace torch_ext | ||
| { | ||
|
|
||
| //! A context-owned checkout, independent of operation-scoped tensor leases. | ||
| //! Construction and release must occur in identical rank order outside capture. | ||
| //! Before use, callers must drain prior users on every rank. Before release, | ||
| //! callers must drain all users on every rank and retire graphs using this tensor. | ||
| class NcclWindowReservation : public torch::CustomClassHolder | ||
| { | ||
| public: | ||
| NcclWindowReservation(at::Tensor like, std::vector<int64_t> group) | ||
| : mTensor(like) | ||
| { | ||
| TORCH_CHECK(like.is_cuda(), "NCCL window reservations require a CUDA tensor"); | ||
| c10::cuda::CUDAGuard const guard(like.device()); | ||
| cudaStreamCaptureStatus status{}; | ||
| TLLM_CUDA_CHECK(cudaStreamIsCapturing(at::cuda::getCurrentCUDAStream().stream(), &status)); | ||
| TORCH_CHECK(status == cudaStreamCaptureStatusNone, "Reserve NCCL windows before CUDA graph capture"); | ||
| #if ENABLE_MULTI_DEVICE | ||
| std::set<int> const ranks(group.begin(), group.end()); | ||
| mComm = getComm(ranks); | ||
| if (!mComm || !*mComm) | ||
| { | ||
| return; | ||
| } | ||
| mBuffer = common::nccl_util::NCCLWindowAllocator::getInstance().requestBuffer( | ||
| *mComm, like.nbytes(), at::cuda::getCurrentCUDAStream().stream()); | ||
| if (mBuffer.isValid()) | ||
| { | ||
| // Deliberately do not register a temporary tensor lease. The reservation | ||
| // owns the checkout; tensor views cannot transfer it to a module scope. | ||
| mTensor = torch::from_blob(mBuffer.ptr, like.sizes(), [comm = mComm](void*){}, like.options()); | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| ~NcclWindowReservation() override | ||
| { | ||
| #if ENABLE_MULTI_DEVICE | ||
| if (mBuffer.isValid()) | ||
| { | ||
| // Missing explicit release must never make storage eligible via GC timing. | ||
| common::nccl_util::NCCLWindowAllocator::getInstance().releaseBufferFromDestructor( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Blocker] If a reservation is destroyed without
These used to exit cleanly; now they end in SIGABRT / core dumps on top of the real error. Could the destructor quarantine with a warning only (no lifecycle assert) for reservations, and
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for flagging the construction failure path. The destructor assertion is intentional: silently abandoning there would hide missing lifecycle cleanup from CI. We added explicit constructor rollback instead. After reservation setup, ranks exchange initialization status through MPI or the EP process group. If any rank fails, all ranks abandon their reservations; the failing rank preserves its original exception, and peers report the construction failure. Validated with two-GPU tests injecting allocation and descriptor failures on one rank, for both communication paths, with lifecycle assertions enabled. This addresses constructor failures after reservation setup; callers still need to explicitly destroy successfully constructed contexts.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The constructor-failure portion is covered by the ExitStack rollback guard and _construction_rendezvous(): failures after reservation setup abandon the reservation, exchange initialization status, and preserve the local exception. The failure-injection contract tests cover local/peer failure and status-exchange errors. Leaving this open because that does not cover every case in the original comment. The reservation destructor still uses releaseBufferFromDestructor(), so a successfully constructed context discarded without explicit release/abandon can still hit the lifecycle assertion. There is also no general finally-based abandonment around synchronization/native teardown. The earlier constructor-rollback reply should not be read as resolving those remaining cases. |
||
| *mComm, mBuffer.ptr, mBuffer.leaseId); | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| //! Quarantine without CUDA or collective calls; retain registration until communicator teardown. | ||
| void abandon() | ||
| { | ||
| #if ENABLE_MULTI_DEVICE | ||
| if (mBuffer.isValid()) | ||
| { | ||
| common::nccl_util::NCCLWindowAllocator::getInstance().abandonReservation( | ||
| *mComm, mBuffer.ptr, mBuffer.leaseId); | ||
| mBuffer = {}; | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| at::Tensor getTensor() const | ||
| { | ||
| return mTensor; | ||
| } | ||
|
|
||
| //! Return the raw ncclWindow_t encoded as an integer, or 0 on a reservation miss. | ||
| int64_t getWindow() const | ||
| { | ||
| #if ENABLE_MULTI_DEVICE | ||
| return static_cast<int64_t>(reinterpret_cast<uintptr_t>(mBuffer.window)); | ||
| #else | ||
| return 0; | ||
| #endif | ||
| } | ||
|
|
||
| //! Release after the caller's collective quiescence boundary; idempotent. | ||
| void release() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Nice to have] The class comment requires release outside capture (allocateOutput.cpp:43), but only the constructor checks it (:54-56). A |
||
| { | ||
| #if ENABLE_MULTI_DEVICE | ||
| if (mBuffer.isValid()) | ||
| { | ||
| c10::cuda::CUDAGuard const guard(mTensor.device()); | ||
| common::nccl_util::NCCLWindowAllocator::getInstance().releaseBuffer( | ||
| *mComm, mBuffer.ptr, at::cuda::getCurrentCUDAStream().stream()); | ||
| mBuffer = {}; | ||
| } | ||
| #endif | ||
| mTensor = at::Tensor(); | ||
| } | ||
|
|
||
| private: | ||
| at::Tensor mTensor; | ||
| #if ENABLE_MULTI_DEVICE | ||
| std::shared_ptr<ncclComm_t> mComm; | ||
| common::nccl_util::NCCLWindowBuffer mBuffer{}; | ||
| #endif | ||
| }; | ||
|
|
||
| std::tuple<at::Tensor, int64_t> allocateOutputOp(at::Tensor const& like, int64_t output_buffer_kind, | ||
| c10::optional<torch::List<int64_t>> group, c10::optional<at::IntArrayRef> shape, | ||
| c10::optional<at::ScalarType> out_dtype) | ||
|
|
@@ -93,6 +191,12 @@ TRTLLM_NAMESPACE_END | |
|
|
||
| TORCH_LIBRARY_FRAGMENT(trtllm, m) | ||
| { | ||
| m.class_<tensorrt_llm::torch_ext::NcclWindowReservation>("NCCLWindowReservation") | ||
| .def(torch::init<at::Tensor, std::vector<int64_t>>()) | ||
| .def("tensor", &tensorrt_llm::torch_ext::NcclWindowReservation::getTensor) | ||
| .def("window", &tensorrt_llm::torch_ext::NcclWindowReservation::getWindow) | ||
| .def("abandon", &tensorrt_llm::torch_ext::NcclWindowReservation::abandon) | ||
| .def("release", &tensorrt_llm::torch_ext::NcclWindowReservation::release); | ||
| m.def( | ||
| "allocate_output(Tensor like, int output_buffer_kind, int[]? group, " | ||
| "int[]? shape=None, ScalarType? out_dtype=None) -> (Tensor, int)"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Nice to have] Question: the reservation registers its window on TRT-LLM's
getComm(moe_ep_group)communicator, but under MPI the NCCL-EP group is created on its own nccl4py communicator from a fresh unique id (nccl_ep_utils.py:225-234, 257). Window registration is per communicator, so why is this handle valid when passed to that group for dispatch, and now for combine, CUDA graphs and FP8/NVFP4? Could you state the reason in the NCCL-EP section of MOE_DEVELOPER_GUIDE.md, or register on the group's own communicator if it does not hold?