Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions tests/pytorch/distributed/run_newton_schulz.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,17 @@ def main():
else:
A = torch.empty(m, n, device="cuda", dtype=dtype)

# Broadcast the full matrix to all ranks
dist.broadcast(A, src=0)

# Scatter columns to each rank
local_cols = n // world_size
x_local = A[:, rank * local_cols : (rank + 1) * local_cols].contiguous()

# Context creation is the first collective, exercising lazy NCCL
# communicator materialization in CusolverMpCtx.
ctx = CusolverMpCtx(dist.group.WORLD)
try:
# Broadcast the full matrix to all ranks
dist.broadcast(A, src=0)

# Scatter columns to each rank
local_cols = n // world_size
x_local = A[:, rank * local_cols : (rank + 1) * local_cols].contiguous()

newton_schulz(x_local, ctx, args.num_iterations, coefficients=coefficients)
finally:
ctx.destroy()
Expand Down
13 changes: 13 additions & 0 deletions transformer_engine/common/newton_schulz/newton_schulz.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ void FreeWorkspace(NVTECusolverMpCtx* ctx) {

NVTECusolverMpCtx* nvte_cusolvermp_ctx_create(ncclComm_t comm, int nranks, int rank) {
NVTE_API_CALL(nvte_cusolvermp_ctx_create);
NVTE_CHECK(comm != nullptr, "NCCL communicator must be non-null");
NVTE_CHECK(nranks > 0, "Number of ranks must be positive, got ", nranks);
NVTE_CHECK(rank >= 0 && rank < nranks, "Rank ", rank, " is outside [0, ", nranks, ")");

int comm_nranks{};
int comm_rank{};
NVTE_CHECK_NCCL(ncclCommCount(comm, &comm_nranks));
NVTE_CHECK_NCCL(ncclCommUserRank(comm, &comm_rank));
NVTE_CHECK(comm_nranks == nranks, "NCCL communicator has ", comm_nranks,
" ranks, but the process group reports ", nranks);
NVTE_CHECK(comm_rank == rank, "NCCL communicator rank is ", comm_rank,
", but the process group reports ", rank);

int device_id{};
NVTE_CHECK_CUDA(cudaGetDevice(&device_id));

Expand Down
42 changes: 34 additions & 8 deletions transformer_engine/pytorch/newton_schulz.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,33 +127,59 @@ class CusolverMpCtx:
"""cuSolverMp context for Newton-Schulz matrix orthogonalization.

Context creation is expensive; create once and reuse across multiple
:func:`newton_schulz` calls. Call :meth:`destroy` when done.
:func:`newton_schulz` calls. Creation is collective over ``group`` and
must be called by every group member on its intended CUDA device. Call
:meth:`destroy` before destroying ``group``.
"""

def __init__(self, group: dist.ProcessGroup) -> None:
# The cuSolverMp grid borrows the ProcessGroupNCCL communicator. Keep
# the group alive until the native context has released the grid.
self._ptr: Optional[int] = None
self._group: Optional[dist.ProcessGroup] = group

if not dist.is_initialized():
raise RuntimeError(
"torch.distributed must be initialized before creating CusolverMpCtx"
)

self.nranks = dist.get_world_size(group)
self._ptr = tex.cusolvermp_ctx_create(
_get_nccl_comm_ptr(group), dist.get_world_size(group), dist.get_rank(group)
)
self.rank = dist.get_rank(group)
if self.rank < 0:
raise RuntimeError("The current process is not a member of the supplied process group")

comm_ptr = _get_nccl_comm_ptr(group)
self._ptr = tex.cusolvermp_ctx_create(comm_ptr, self.nranks, self.rank)

def destroy(self) -> None:
"""Destroy the underlying cuSolverMp context."""
if self._ptr is not None:
tex.cusolvermp_ctx_destroy(self._ptr)
try:
if self._ptr is not None:
tex.cusolvermp_ctx_destroy(self._ptr)
finally:
self._ptr = None
self._group = None

def __del__(self) -> None:
# Called when the context is manually destroyed or during Python teardown
self.destroy()
Comment on lines 163 to 165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 __del__ may race with CUDA context teardown

__del__ calls destroy() which in turn calls tex.cusolvermp_ctx_destroy. During interpreter shutdown, CUDA driver state is torn down in an unspecified order relative to Python object finalization. If the CUDA context has already been destroyed when __del__ fires, cusolvermp_ctx_destroy (and the cusolverMpDestroyGrid / cusolverMpDestroy calls inside it) may trigger CUDA errors or a segfault. Guarding with try/except Exception: pass inside __del__, or checking torch.cuda.is_initialized() before calling into the native layer, would prevent noisy teardown-time crashes in programs that forget to call destroy() explicitly.



def _get_nccl_comm_ptr(group: dist.ProcessGroup) -> int:
"""Extract the raw NCCL communicator pointer from a PyTorch process group."""
"""Materialize and borrow a raw NCCL communicator from a process group."""
backend = dist.get_backend(group)
if backend != "nccl":
raise RuntimeError(f"Newton-Schulz requires NCCL backend, got '{backend}'")

# ProcessGroupNCCL creates communicators lazily. This device-specific
# collective ensures that the communicator returned by _comm_ptr() exists
# and is ready on every group rank before cuSolverMp borrows it.
dist.barrier(group=group, device_ids=[torch.cuda.current_device()])
nccl_backend = group._get_backend(torch.device("cuda"))
return nccl_backend._comm_ptr()
comm_ptr = nccl_backend._comm_ptr()
Comment on lines +177 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Private PyTorch API dependency

_get_backend() and _comm_ptr() are private ProcessGroupNCCL methods and can be renamed, replaced, or removed across PyTorch minor releases without a deprecation cycle. If the API changes, the failure mode is a silent AttributeError at context-creation time rather than a compilation error, which may only surface in distributed runs. It would be worth tracking the PyTorch version that introduced these methods and adding an assertion or a version guard, or at minimum filing a PyTorch issue to request a stable public surface for borrowing the raw ncclComm_t.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

if not isinstance(comm_ptr, int) or comm_ptr == 0:
raise RuntimeError("ProcessGroupNCCL returned an invalid communicator pointer")
return comm_ptr


def newton_schulz(
Expand Down
Loading