Skip to content

Commit 8b38d31

Browse files
authored
[fix] MD-TRT: rebind NCCL communicator after execution context invalidation (#4464)
1 parent 6d09777 commit 8b38d31

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

core/runtime/TRTEngine.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ void TRTEngine::disable_profiling() {
376376
// Drop the profiler-attached context; next execute lazily creates a fresh
377377
// one with no profiler.
378378
invalidate_exec_ctx();
379+
#ifdef ENABLE_TRT_NCCL_COLLECTIVES
380+
// The communicator was bound onto the IExecutionContext we just dropped, so
381+
// the next ``execute_engine`` must re-bind via ``bind_nccl_comm()``.
382+
nccl_initialized = false;
383+
#endif
379384
}
380385

381386
void TRTEngine::dump_engine_layer_info_to_file(const std::string& path) {
@@ -814,6 +819,11 @@ void TRTEngine::set_resource_allocation_strategy(TRTEngine::ResourceAllocationSt
814819
<< (this->resource_allocation_strategy == TRTEngine::ResourceAllocationStrategy::kDynamic ? "dynamic"
815820
: "static"));
816821
invalidate_exec_ctx();
822+
#ifdef ENABLE_TRT_NCCL_COLLECTIVES
823+
// The communicator was bound onto the IExecutionContext we just dropped, so
824+
// the next ``execute_engine`` must re-bind via ``bind_nccl_comm()``.
825+
nccl_initialized = false;
826+
#endif
817827
}
818828
}
819829

tests/py/dynamo/distributed/test_native_nccl.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2198,6 +2198,125 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
21982198
print(f"[Rank {rank}] PASS _multirank_pg_migration", flush=True)
21992199

22002200

2201+
def _find_trt_module(mod: nn.Module) -> Any:
2202+
"""Return the first ``TorchTensorRTModule`` in a compiled module, or None."""
2203+
from torch_tensorrt.dynamo.runtime._TorchTensorRTModule import TorchTensorRTModule
2204+
2205+
if isinstance(mod, TorchTensorRTModule):
2206+
return mod
2207+
for child in mod.children() if isinstance(mod, nn.Module) else []:
2208+
found = _find_trt_module(child)
2209+
if found is not None:
2210+
return found
2211+
return None
2212+
2213+
2214+
def _multirank_comm_survives_invalidation(
2215+
rank: int, world_size: int, device: torch.device, trigger: str
2216+
) -> None:
2217+
"""The NCCL communicator must survive an IExecutionContext invalidation.
2218+
2219+
``bind_nccl_comm()`` attaches the communicator to the *IExecutionContext*.
2220+
Several C++ entry points drop that context and rely on the lazy re-bind in
2221+
``execute_engine.cpp`` -- which only fires when ``nccl_initialized`` is
2222+
false. ``TRTEngine::runtime_settings()`` and ``set_device_memory_budget()``
2223+
clear/re-bind correctly; ``disable_profiling()`` (TRTEngine.cpp:378) and
2224+
``set_resource_allocation_strategy()`` (TRTEngine.cpp:791) do not, so the
2225+
replacement context runs collectives with no communicator attached.
2226+
"""
2227+
import torch_tensorrt
2228+
from torch_tensorrt.distributed._nccl_utils import setup_nccl_for_torch_tensorrt
2229+
2230+
setup_nccl_for_torch_tensorrt()
2231+
group_name = dist.distributed_c10d._get_default_group().group_name
2232+
2233+
class _RowParallelLinear(nn.Module):
2234+
"""Row-parallel Linear: local matmul followed by an all-reduce."""
2235+
2236+
def __init__(self, lin: nn.Linear, group_name: str) -> None:
2237+
super().__init__()
2238+
self.lin = lin
2239+
self.group_name = group_name
2240+
2241+
def forward(self, x: torch.Tensor) -> torch.Tensor:
2242+
out = self.lin(x)
2243+
out = torch.ops._c10d_functional.all_reduce.default(
2244+
out, "sum", self.group_name
2245+
)
2246+
return torch.ops._c10d_functional.wait_tensor.default(out)
2247+
2248+
class TinyMLP(nn.Module):
2249+
def __init__(self) -> None:
2250+
super().__init__()
2251+
self.fc1 = nn.Linear(16, 64)
2252+
self.relu = nn.ReLU()
2253+
self.fc2 = nn.Linear(64, 16, bias=False)
2254+
2255+
def forward(self, x: torch.Tensor) -> torch.Tensor:
2256+
return self.fc2(self.relu(self.fc1(x)))
2257+
2258+
# Manually shard: fc1 column-parallel, fc2 row-parallel + all-reduce. Mirrors
2259+
# build_exportable_model() in test_export_save_load.py -- DTensor-based
2260+
# parallelize_module does not survive torch.export cleanly.
2261+
torch.manual_seed(42)
2262+
model = TinyMLP().to(device)
2263+
w = model.fc1.weight.data
2264+
chunk = w.shape[0] // world_size
2265+
model.fc1.weight = nn.Parameter(w[rank * chunk : (rank + 1) * chunk].contiguous())
2266+
b = model.fc1.bias.data
2267+
model.fc1.bias = nn.Parameter(b[rank * chunk : (rank + 1) * chunk].contiguous())
2268+
w2 = model.fc2.weight.data
2269+
chunk2 = w2.shape[1] // world_size
2270+
model.fc2.weight = nn.Parameter(
2271+
w2[:, rank * chunk2 : (rank + 1) * chunk2].contiguous()
2272+
)
2273+
model.fc2 = _RowParallelLinear(model.fc2, group_name)
2274+
2275+
torch.manual_seed(0)
2276+
inp = torch.randn(4, 16, device=device)
2277+
2278+
# Export, not torch.compile: torch.compile returns an OptimizedModule that
2279+
# wraps the ORIGINAL module, so the TRT submodules are not reachable via
2280+
# children()/named_modules(). dynamo.compile returns a real GraphModule.
2281+
ep = torch.export.export(model, args=(inp,), strict=False)
2282+
trt_model = torch_tensorrt.dynamo.compile(
2283+
ep,
2284+
inputs=[inp],
2285+
device=device,
2286+
disable_tf32=True,
2287+
use_python_runtime=False,
2288+
min_block_size=1,
2289+
use_distributed_mode_trace=True,
2290+
)
2291+
2292+
with torch.no_grad():
2293+
expected = trt_model(inp)
2294+
2295+
trt_mod = _find_trt_module(trt_model)
2296+
if trt_mod is None:
2297+
raise AssertionError("Could not locate a TorchTensorRTModule")
2298+
2299+
# Drop the IExecutionContext the communicator was bound to.
2300+
if trigger == "resource_allocation":
2301+
trt_mod.use_dynamically_allocated_resources(True)
2302+
elif trigger == "disable_profiling":
2303+
trt_mod.disable_profiling()
2304+
else:
2305+
raise ValueError(f"unknown trigger {trigger!r}")
2306+
2307+
dist.barrier() # keep ranks in step before the next collective
2308+
2309+
with torch.no_grad():
2310+
out = trt_model(inp)
2311+
2312+
_check_close(out, expected, f"output after {trigger} invalidation rank={rank}")
2313+
2314+
print(
2315+
f"[Rank {rank}] PASS _multirank_comm_survives_invalidation[{trigger}]",
2316+
flush=True,
2317+
)
2318+
2319+
22012320
# ============================================================================
22022321
# Section 8 — Multi-rank pytest tests (MultiProcessTestCase, requires 2 GPUs)
22032322
# ============================================================================
@@ -2329,6 +2448,26 @@ def test_pg_migration(self) -> None:
23292448
device = self._init_dist()
23302449
_multirank_pg_migration(self.rank, self.world_size, device)
23312450

2451+
@unittest.skipIf(not has_nccl_collectives(), "No NCCL collective support available")
2452+
@requires_nccl()
2453+
@skip_if_lt_x_gpu(2)
2454+
def test_comm_survives_resource_allocation_change(self) -> None:
2455+
"""set_resource_allocation_strategy() invalidates the context; comm must survive."""
2456+
device = self._init_dist()
2457+
_multirank_comm_survives_invalidation(
2458+
self.rank, self.world_size, device, "resource_allocation"
2459+
)
2460+
2461+
@unittest.skipIf(not has_nccl_collectives(), "No NCCL collective support available")
2462+
@requires_nccl()
2463+
@skip_if_lt_x_gpu(2)
2464+
def test_comm_survives_disable_profiling(self) -> None:
2465+
"""disable_profiling() invalidates the context; comm must survive."""
2466+
device = self._init_dist()
2467+
_multirank_comm_survives_invalidation(
2468+
self.rank, self.world_size, device, "disable_profiling"
2469+
)
2470+
23322471

23332472
# ============================================================================
23342473
# Section 9 — torchrun / mpirun entry point (legacy multi-rank runner)

0 commit comments

Comments
 (0)