Skip to content

Commit 699ed6e

Browse files
Fix distributed Newton-Schulz correctness (#3541)
* Fix distributed Newton-Schulz correctness - Explicitly enable cuSOLVERMp input normalization and compute-type Gram reduction. - Validate distributed dimensions and reject unsupported tall low-level inputs. - Reduce device workspace requirements to the grid-wide maximum. - Synchronize workspace cleanup and retain host workspace for asynchronous operations. - Normalize the test reference and evaluate Gram and reference errors in FP32. - Use rank-independent aligned test shapes and strict absolute tolerances. Tests: - pytest -q tests/pytorch/distributed/test_newton_schulz.py - torchrun workers with 1, 2, 4, and 8 processes Signed-off-by: Vladimir Cherepanov <vcherepanov@nvidia.com> * Cache distributed Newton-Schulz workspaces Cache cuSOLVERMp workspace configurations and retain the NCCL reduction scalar so repeated optimizer steps avoid redundant workspace queries, allocations, collectives, and stream synchronization. Restore tall tensor-parallel coverage for supported layouts and reject distributed partitions along the smaller matrix dimension with a clear error. Tests: - ninja -C build/cmake transformer_engine - pytest -q tests/pytorch/distributed/test_newton_schulz.py (2 passed) Signed-off-by: Vladimir Cherepanov <vcherepanov@nvidia.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Vladimir Cherepanov <vcherepanov@nvidia.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9233c73 commit 699ed6e

4 files changed

Lines changed: 218 additions & 56 deletions

File tree

‎tests/pytorch/distributed/run_newton_schulz.py‎

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
def newton_schulz_reference(
3232
in_x: torch.Tensor, coefficients: list[tuple[float, float, float]]
3333
) -> torch.Tensor:
34-
"""Local Newton-Schulz reference mirroring the provided Octave update."""
35-
x = in_x.clone()
34+
"""Local Newton-Schulz reference matching cuSOLVERMp input normalization."""
35+
x = in_x.float().clone()
36+
x /= torch.linalg.vector_norm(x)
3637
for a, b, c in coefficients:
3738
xxt = x @ x.mT
3839
x = a * x + b * xxt @ x + c * xxt @ xxt @ x
@@ -47,30 +48,32 @@ def _dtype_from_name(dtype: str) -> torch.dtype:
4748
raise ValueError(f"Unsupported dtype: {dtype}")
4849

4950

50-
def _test_tolerances(dtype: str, check: str, world_size: int) -> tuple[float, float]:
51-
if dtype == "bfloat16":
52-
return (5e-2, 5e-2)
53-
if check == "orthogonality" and world_size == 1:
54-
return (2e-2, 2e-2)
55-
return (1e-2, 1e-2)
51+
def _test_tolerances(dtype: str, check: str) -> tuple[float, float]:
52+
if check == "reference":
53+
return (1e-2 if dtype == "bfloat16" else 1e-3, 0.0)
54+
return (1.5e-2 if dtype == "bfloat16" else 1e-2, 0.0)
5655

5756

58-
def _shape_scale(world_size: int) -> int:
59-
return 4 if world_size == 1 else world_size
57+
def _aligned_size(size: int, world_size: int) -> int:
58+
"""Round a global distributed dimension up to a whole number of shards."""
59+
return (size + world_size - 1) // world_size * world_size
6060

6161

6262
def _orthogonality_shapes(world_size: int) -> list[tuple[int, int]]:
63-
scale = _shape_scale(world_size)
63+
rows = _aligned_size(512, world_size)
6464
return [
65-
(scale * 64, scale * 64),
66-
(scale * 64, scale * 96),
67-
(scale * 96, scale * 64),
65+
(rows, rows),
66+
(rows, _aligned_size(768, world_size)),
6867
]
6968

7069

7170
def _reference_shapes(world_size: int) -> list[tuple[int, int]]:
72-
scale = _shape_scale(world_size)
73-
return [(scale * 64, scale * 64)]
71+
size = _aligned_size(512, world_size)
72+
return [(size, size)]
73+
74+
75+
def _tall_reference_shape(world_size: int) -> tuple[int, int]:
76+
return (_aligned_size(768, world_size), _aligned_size(512, world_size))
7477

7578

7679
def _make_matrix(
@@ -117,7 +120,7 @@ def _run_case(
117120
dtype = _dtype_from_name(dtype_name)
118121
m, n = matrix_shape
119122
coefficients = get_coefficients(num_iterations, coeff_type)
120-
atol, rtol = _test_tolerances(dtype_name, check, world_size)
123+
atol, rtol = _test_tolerances(dtype_name, check)
121124

122125
if api == "tp" and partition_dim is None:
123126
# Replicated inputs are sharded along the larger dimension for cuSolverMp.
@@ -169,21 +172,23 @@ def _run_case(
169172

170173
# Check: the resulting matrix should be orthogonal, or match a local reference.
171174
if check == "orthogonality":
175+
X_float = X.float()
172176
if m <= n:
173-
gram = X @ X.t()
177+
gram = X_float @ X_float.t()
174178
expected = torch.eye(m, device=gram.device, dtype=gram.dtype)
175179
label = "X @ X.t() - I"
176180
else:
177-
gram = X.t() @ X
181+
gram = X_float.t() @ X_float
178182
expected = torch.eye(n, device=gram.device, dtype=gram.dtype)
179183
label = "X.t() @ X - I"
180184
max_diff = (gram - expected).abs().max().item()
181185
passed = torch.allclose(gram, expected, atol=atol, rtol=rtol)
182186
elif check == "reference":
183-
reference = newton_schulz_reference(A.float(), coefficients).to(dtype)
184-
max_diff = (X - reference).abs().max().item()
187+
reference = newton_schulz_reference(A, coefficients)
188+
X_float = X.float()
189+
max_diff = (X_float - reference).abs().max().item()
185190
label = "distributed - reference"
186-
passed = torch.allclose(X, reference, atol=atol, rtol=rtol)
191+
passed = torch.allclose(X_float, reference, atol=atol, rtol=rtol)
187192
else:
188193
raise ValueError(f"Unsupported check: {check}")
189194

@@ -254,6 +259,47 @@ def run_all_tests(ctx: CusolverMpCtx) -> None:
254259
tp_mode=tp_mode,
255260
)
256261

262+
tall_shape = _tall_reference_shape(world_size)
263+
tall_tp_configs = (
264+
(0, "distributed"),
265+
(0, "duplicated"),
266+
(1, "duplicated"),
267+
(None, "duplicated"),
268+
)
269+
for partition_dim, tp_mode in tall_tp_configs:
270+
config = (tall_shape, partition_dim, tp_mode)
271+
if rank == 0:
272+
print(f"Running tall TP API reference check with {config=}", flush=True)
273+
_run_case(
274+
ctx=ctx,
275+
check="reference",
276+
dtype_name="float32",
277+
matrix_shape=tall_shape,
278+
num_iterations=5,
279+
coeff_type="quintic",
280+
api="tp",
281+
partition_dim=partition_dim,
282+
tp_mode=tp_mode,
283+
)
284+
285+
# A directly column-sharded tall matrix cannot be transposed into the column distribution
286+
# required by the low-level API without first redistributing it.
287+
m, n = tall_shape
288+
x_local = torch.empty(m, n // world_size, device="cuda", dtype=torch.float32)
289+
try:
290+
newton_schulz_tp(
291+
x_local,
292+
ctx,
293+
num_iterations=5,
294+
partition_dim=1,
295+
tp_mode="distributed",
296+
)
297+
except ValueError as exc:
298+
if "must be partitioned along their larger dimension" not in str(exc):
299+
raise
300+
else:
301+
raise AssertionError("Expected a directly column-sharded tall matrix to be rejected")
302+
257303
if rank == 0:
258304
print("Running TP API reference check with replicated input", flush=True)
259305
_run_case(

‎transformer_engine/common/include/transformer_engine/newton_schulz.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void nvte_cusolvermp_ctx_destroy(NVTECusolverMpCtx* ctx);
4545
/*! \brief Compute Newton-Schulz matrix orthogonalization in-place.
4646
*
4747
* \param[in] ctx cuSolverMp context.
48-
* \param[in] m Global number of rows.
48+
* \param[in] m Global number of rows. Must be no greater than n.
4949
* \param[in] n Global number of columns.
5050
* \param[in,out] x Local part of the matrix (modified in-place).
5151
* \param[in] num_iterations Number of Newton-Schulz iterations.

0 commit comments

Comments
 (0)