-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[https://nvbugs/6581063][fix] Release MegaMoE symm buffers on executor teardown #18182
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
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 |
|---|---|---|
|
|
@@ -63,7 +63,122 @@ | |
| # on the current TRT-LLM execution contract that MegaMoE layers run | ||
| # serially within a forward pass; concurrent MegaMoE forwards sharing | ||
| # a key would race on the same scratch buffers. | ||
| _MEGA_MOE_SYMM_BUFFER_CACHE: Dict[tuple, object] = {} | ||
| # | ||
| # Keyed on buffer geometry only, never on the EP group's identity: a | ||
| # worker process outlives the LLM built in it, so keying on the group | ||
| # would mint a fresh key per LLM and stack a second allocation on top | ||
| # of the first (and ``id()`` is recycled, so it can also alias a dead | ||
| # group). Since the key no longer distinguishes groups, each entry | ||
| # records the ProcessGroup it was allocated over and a hit is | ||
| # re-validated against the caller's live group in | ||
| # ``_take_cached_symm_buffer``. The group is recorded on this side of | ||
| # the DeepGEMM boundary on purpose: relying on ``SymmBuffer.group`` | ||
| # would make correctness depend on DeepGEMM storing the passed group | ||
| # verbatim, and a version bump that normalizes it would turn every hit | ||
| # into a false stale -- freeing a buffer another layer already holds. | ||
| # ``release_symm_buffer_cache`` bounds the lifetime. | ||
| _MEGA_MOE_SYMM_BUFFER_CACHE: Dict[tuple, Tuple[object, object]] = {} | ||
|
|
||
|
|
||
| def _free_symm_buffer(buffered: object) -> int: | ||
| """Release one DG SymmBuffer's symmetric memory. Returns its size in bytes. | ||
|
|
||
| ``SymmBuffer.destroy`` nulls only ``handle``/``buffer``/``group``/``x``/ | ||
| ``x_sf``, leaving the other tensor views sliced out of the allocation | ||
| in place -- and any surviving view pins the whole thing. So sweep every | ||
| remaining tensor attribute too, otherwise this frees nothing whenever the | ||
| object is held by a reference cycle rather than dropped outright. | ||
|
|
||
| Tolerates an already-destroyed buffer (``buffer`` is ``None``) so a | ||
| double free degrades to a no-op instead of taking down the teardown path. | ||
| """ | ||
| buffer = getattr(buffered, "buffer", None) | ||
| nbytes = buffer.nbytes if buffer is not None else 0 | ||
| buffered.destroy() | ||
| for name, value in list(vars(buffered).items()): | ||
| if isinstance(value, torch.Tensor): | ||
| setattr(buffered, name, None) | ||
| return nbytes | ||
|
|
||
|
|
||
| def _take_cached_symm_buffer(key: tuple, ep_pg: object) -> Optional[object]: | ||
| """Return the cached SymmBuffer for ``key`` if it was allocated over | ||
| ``ep_pg``; free and drop a geometry-equal entry from another group. | ||
|
|
||
| A geometry-equal entry recorded against a different (already destroyed) | ||
| EP group means an LLM was torn down without ``release_symm_buffer_cache``, | ||
| and its buffer's peer mappings are dead. Free it here so the driver can | ||
| reuse those pages for the caller's allocation -- symmetric memory is | ||
| outside the caching allocator, so this is the only way to get it back | ||
| within the process. Evict before freeing so a raising free cannot leave | ||
| a half-destroyed buffer behind for the next lookup. | ||
| """ | ||
| entry = _MEGA_MOE_SYMM_BUFFER_CACHE.get(key) | ||
| if entry is None: | ||
| return None | ||
| cached, cached_pg = entry | ||
| if cached_pg is ep_pg: | ||
| return cached | ||
| del _MEGA_MOE_SYMM_BUFFER_CACHE[key] | ||
| # This runs on the next LLM's init path and the stale buffer was | ||
| # rendezvoused over an already-destroyed group, so a raising free must | ||
| # not escape and fail the new LLM -- fall through to a fresh allocation | ||
| # instead (no worse than the pre-fix behavior, which leaked and | ||
| # allocated). | ||
| try: | ||
| freed = _free_symm_buffer(cached) | ||
| except Exception as e: | ||
| logger.error( | ||
| f"[MegaMoE] failed to release a stale DG SymmBuffer from a " | ||
| f"previous EP group, allocating fresh: {e}" | ||
| ) | ||
| return None | ||
|
Comment on lines
+128
to
+135
Contributor
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. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
ast-grep outline tensorrt_llm/_torch/modules/fused_moe/mega_moe/mega_moe_deepgemm.py \
--items all --type function
rg -n -C 5 'def destroy\s*\(' --glob '*.py' .
rg -n -C 5 'class .*SymmBuffer|SymmBuffer|destroyed process group' --glob '*.py' .Repository: NVIDIA/TensorRT-LLM Length of output: 50375 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
sed -n '25,180p' tensorrt_llm/_torch/modules/fused_moe/mega_moe/mega_moe_deepgemm.py
printf '\n--- MoE developer guide references ---\n'
rg -n -C 2 'SymmBuffer|destroy|DeepGEMM|deep_gemm' \
tensorrt_llm/_torch/modules/fused_moe/MOE_DEVELOPER_GUIDE.md \
tensorrt_llm/_torch/modules/fused_moe/mega_moe 2>/dev/null || true
printf '\n--- DeepGEMM dependency declarations ---\n'
rg -n -C 2 'deep.?gemm|deep_gemm' \
requirements*.txt pyproject.toml setup.py setup.cfg \
tensorrt_llm 2>/dev/null | head -200 || trueRepository: NVIDIA/TensorRT-LLM Length of output: 50375 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- DeepGEMM files ---'
git ls-files | grep -E '(^|/)(deep_gemm|deepgemm)(/|\.|$)' | head -200
printf '%s\n' '--- SymmBuffer definitions and destroy calls in tracked DeepGEMM sources ---'
files=$(git ls-files | grep -E '(^|/)(deep_gemm|deepgemm)(/|\.|$)' || true)
if [ -n "$files" ]; then
rg -n -C 8 'class SymmBuffer|def destroy|SymmBuffer\(' $files
fi
printf '%s\n' '--- Import helper binding ---'
rg -n -C 12 'def _import_deep_gemm|_import_deep_gemm' \
tensorrt_llm/_torch/modules/fused_moe/quantization.py \
tensorrt_llm/_torch/modules/fused_moe 2>/dev/null | head -160Repository: NVIDIA/TensorRT-LLM Length of output: 892 🌐 Web query:
💡 Result: The exception related to SymmBuffer.destroy and a destroyed process group in DeepGEMM typically arises during the teardown phase of a distributed model, where the underlying PyTorch distributed process group is closed while the DeepGEMM SymmBuffer object still holds a reference to it [1]. In DeepGEMM, the SymmBuffer class is designed to manage symmetric memory buffers for Mega-MoE operations, which requires a valid torch.distributed.ProcessGroup for the rendezvous mechanism [1][2]. When the SymmBuffer.destroy method is called, it explicitly nullifies its references to the group and internal buffers [1]. However, if this cleanup occurs after the global PyTorch distributed process group has been destroyed (e.g., via dist.destroy_process_group), accessing or interacting with the process group object within the buffer can trigger errors [3]. To resolve or mitigate this issue: 1. Ensure Proper Teardown Order: Ensure that all DeepGEMM model components or buffers are explicitly destroyed before calling dist.destroy_process_group. If your model structure allows, call the.destroy method on your SymmBuffer instances during the model shutdown phase before the distributed backend is torn down. 2. Avoid Premature Cleanup: If you are seeing this error during inference or training loops, verify that your distributed process group is not being re-initialized or closed prematurely. For instance, in frameworks like SGLang, crashes during initialization or warmup are often related to misconfigured distributed settings (e.g., PD disaggregation or NCCL timeouts) [4][5]. 3. Monitor for Underlying Hangs: The error can sometimes be a secondary effect of a hang in the distributed watchdog thread, often caused by CUDA API operations (like event destruction) being blocked [5]. If you suspect a deadlock or timeout, increasing the timeout settings (e.g., --watchdog-timeout) or checking environment variables like TORCH_NCCL_ENABLE_MONITORING=0 may help confirm if the issue is a performance-related stall rather than a pure code logic error [5]. 4. Check for Version Compatibility: DeepGEMM and its dependencies (like vLLM, which also utilizes symmetric memory) have seen significant improvements regarding rendezvous reliability in newer PyTorch versions [6]. Ensure your environment is using up-to-date versions of PyTorch and the DeepGEMM kernel library to benefit from fixes related to symmetric memory allocation and CUDA stream synchronization [7][6]. Citations:
🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 15 'def _import_deep_gemm|_import_deep_gemm' \
tensorrt_llm/_torch/modules/fused_moe/quantization.py \
tensorrt_llm/_torch/modules/fused_moe 2>/dev/null | head -160 || true
printf '%s\n' '--- Generated or vendored SymmBuffer references ---'
rg -n -C 5 'SymmBuffer|symm_mem\.rendezvous|\.destroy\(\)' \
tensorrt_llm cpp tests 2>/dev/null | head -240 || trueRepository: NVIDIA/TensorRT-LLM Length of output: 36532 🌐 Web query:
💡 Result: The class SymmBuffer is a component found in the DeepGEMM library by deepseek-ai, specifically within the mega module [1]. The destroy method in this class is used to manually release references to the objects held by the SymmBuffer instance, which assists in garbage collection and memory management [1]. Its implementation is as follows: def destroy(self): self.handle = None self.buffer = None self.group = None self.x = None self.x_sf = None By setting these attributes to None, the method clears the references to the CUDA buffer, rendezvous handle, process group, and various tensor views (like self.x and self.x_sf), allowing the associated memory to be freed by Python's garbage collector [1]. Citations: Do not catch every exception from stale-buffer cleanup.
🧰 Tools🪛 Ruff (0.16.2)[warning] 122-122: Do not catch blind exception: (BLE001) 🤖 Prompt for AI AgentsSources: Coding guidelines, Linters/SAST tools |
||
| logger.info( | ||
| f"[MegaMoE] released stale DG SymmBuffer from a previous EP group: {freed / 2**30:.2f} GiB" | ||
| ) | ||
| return None | ||
|
|
||
|
|
||
| def release_symm_buffer_cache() -> None: | ||
| """Free every cached DG SymmBuffer. Call once per executor teardown. | ||
|
|
||
| The EP group these buffers were rendezvoused over is destroyed on | ||
| executor shutdown, so nothing may reuse them afterwards. They must be | ||
| dropped explicitly: the backing symmetric memory sits outside PyTorch's | ||
| caching allocator, so neither a GC nor ``torch.cuda.empty_cache()`` | ||
| reclaims it, and a reused worker process would otherwise carry a full | ||
| activation workspace into the next LLM's memory budget. | ||
|
|
||
| Only the MPI worker path (``GenerationExecutorWorker.shutdown``) calls | ||
| this today. The Ray and RPC worker paths do not: their worker process | ||
| exits with the LLM, so the driver reclaims the memory at process exit. | ||
| Any path that starts reusing a worker process across LLMs must call this | ||
| on its teardown too. | ||
| """ | ||
| if not _MEGA_MOE_SYMM_BUFFER_CACHE: | ||
| return | ||
| # Detach before freeing so a raising free cannot leave a half-destroyed | ||
| # buffer in the cache for a later lookup to trip over. | ||
| entries = list(_MEGA_MOE_SYMM_BUFFER_CACHE.values()) | ||
| _MEGA_MOE_SYMM_BUFFER_CACHE.clear() | ||
| # Free one at a time and keep going on failure: the entries are already | ||
| # out of the cache, so any buffer skipped by a raising predecessor would | ||
| # be unreclaimable for the rest of the process lifetime. | ||
| released = 0 | ||
| total_bytes = 0 | ||
| for buffered, _ in entries: | ||
| try: | ||
| total_bytes += _free_symm_buffer(buffered) | ||
| released += 1 | ||
| except Exception as e: | ||
| logger.error( | ||
| f"[MegaMoE] failed to release a DG SymmBuffer during executor " | ||
| f"teardown, continuing with the remaining buffers: {e}" | ||
| ) | ||
| logger.info( | ||
| f"[MegaMoE] released {released}/{len(entries)} DG SymmBuffer(s): " | ||
| f"{total_bytes / 2**30:.2f} GiB" | ||
| ) | ||
|
|
||
|
|
||
| # ---- Fused MXFP8 per-token quant backends -------------------------------- | ||
|
|
@@ -564,7 +679,8 @@ def _alloc_symm_buffer(self) -> None: | |
| capture would fail on the host-side IPC handle exchange. | ||
|
|
||
| Buffers are shared across layers via ``_MEGA_MOE_SYMM_BUFFER_CACHE`` | ||
| keyed on the (EP-PG, slot/expert/topk/shape/activation) tuple. | ||
| keyed on the (EP-size, slot/expert/topk/shape/activation) tuple; see | ||
| that cache's definition for why the key carries no group identity. | ||
| Sharing is safe only while MegaMoE layer forwards are issued | ||
| serially within a forward pass; concurrent MegaMoE forwards | ||
| sharing a key would race on the same scratch buffers. | ||
|
|
@@ -584,7 +700,7 @@ def _alloc_symm_buffer(self) -> None: | |
| if self._symm_buffer is not None: | ||
| return | ||
| key = ( | ||
| id(self._ep_pg), | ||
| self.ep_size, | ||
| self.num_experts, | ||
| self.num_slots, | ||
| self.max_num_tokens, | ||
|
|
@@ -593,7 +709,7 @@ def _alloc_symm_buffer(self) -> None: | |
| self.intermediate_size, | ||
| self.dg_activation, | ||
| ) | ||
| cached = _MEGA_MOE_SYMM_BUFFER_CACHE.get(key) | ||
| cached = _take_cached_symm_buffer(key, self._ep_pg) | ||
| if cached is None: | ||
| cached = self._dg.get_symm_buffer_for_mega_moe( | ||
| self._ep_pg, | ||
|
|
@@ -606,7 +722,7 @@ def _alloc_symm_buffer(self) -> None: | |
| mma_type="fp8xfp4", | ||
| activation=self.dg_activation, | ||
| ) | ||
| _MEGA_MOE_SYMM_BUFFER_CACHE[key] = cached | ||
| _MEGA_MOE_SYMM_BUFFER_CACHE[key] = (cached, self._ep_pg) | ||
| # Log only on the first layer; deeper layers reuse the cache | ||
| # and would otherwise spam N copies of an identical line. | ||
| log_fn = logger.info if self.layer_idx == 0 else logger.debug | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.