Skip to content

Commit f87f67d

Browse files
sfc-gh-hkaraucursoragentholdenk
committed
[PYTHON][MINOR] Use a unique per-run rendezvous id for distributed torch training
Distributed mode passed a fixed rendezvous id to torchrun, so concurrent training runs sharing a rendezvous endpoint could cross-join each other's rendezvous. Generate a unique id per run on the rank-0 barrier task, share it with the other tasks via allGather, and require it when building the torchrun command instead of falling back to a constant. Co-authored-by: Cursor <cursoragent@cursor.com> Co-Authored-By: Holden Karau <holden@pigscanfly.ca>
1 parent 0da2811 commit f87f67d

3 files changed

Lines changed: 49 additions & 9 deletions

File tree

python/pyspark/ml/deepspeed/tests/test_deepspeed_distributor.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ def _get_env_var(self, var_name: str, default_value: Any) -> Any:
4141
os.environ[var_name] = str(default_value)
4242
return default_value
4343

44-
def _get_env_variables_distributed(self) -> Tuple[Any, Any, Any]:
44+
def _get_env_variables_distributed(self) -> Tuple[Any, Any, Any, Any]:
4545
master_addr = self._get_env_var("MASTER_ADDR", "127.0.0.1")
4646
master_port = self._get_env_var("MASTER_PORT", 2000)
4747
rank = self._get_env_var("RANK", 0)
48-
return master_addr, master_port, rank
48+
rdzv_id = self._get_env_var("PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID", "test-rdzv-id")
49+
return master_addr, master_port, rank, rdzv_id
4950

5051
def test_get_torchrun_args_local(self) -> None:
5152
number_of_processes = 5
@@ -60,12 +61,12 @@ def test_get_torchrun_args_local(self) -> None:
6061

6162
def test_get_torchrun_args_distributed(self) -> None:
6263
number_of_processes = 5
63-
master_addr, master_port, rank = self._get_env_variables_distributed()
64+
master_addr, master_port, rank, rdzv_id = self._get_env_variables_distributed()
6465
expected_torchrun_args_distributed = [
6566
f"--nnodes={number_of_processes}",
6667
f"--node_rank={rank}",
6768
f"--rdzv_endpoint={master_addr}:{master_port}",
68-
"--rdzv_id=0",
69+
f"--rdzv_id={rdzv_id}",
6970
]
7071
torchrun_args_distributed, process_per_node = DeepspeedTorchDistributor._get_torchrun_args(
7172
False, number_of_processes
@@ -131,12 +132,13 @@ def test_create_torchrun_command_distributed(self) -> None:
131132
distributed_master_address,
132133
distributed_master_port,
133134
distributed_rank,
135+
distributed_rdzv_id,
134136
) = self._get_env_variables_distributed()
135137
distributed_torchrun_args = [
136138
f"--nnodes={num_procs}",
137139
f"--node_rank={distributed_rank}",
138140
f"--rdzv_endpoint={distributed_master_address}:{distributed_master_port}",
139-
"--rdzv_id=0",
141+
f"--rdzv_id={distributed_rdzv_id}",
140142
]
141143
with self.subTest(msg="Distributed training command verification with no extra args"):
142144
distributed_cmd_no_args_expected = [

python/pyspark/ml/torch/distributor.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,12 +431,20 @@ def _get_torchrun_args(local_mode: bool, num_processes: int) -> Tuple[List[Any],
431431
else:
432432
processes_per_node = 1
433433
node_rank = os.environ["RANK"]
434+
# Set by set_torch_config; no constant fallback, since concurrent runs sharing a
435+
# rendezvous endpoint would collide on a fixed id.
436+
rdzv_id = os.environ.get("PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID")
437+
if not rdzv_id:
438+
raise RuntimeError(
439+
"Missing PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID environment variable: a unique "
440+
"per-run rendezvous id is required for distributed training."
441+
)
434442

435443
torchrun_args = [
436444
f"--nnodes={num_processes // processes_per_node}",
437445
f"--node_rank={node_rank}",
438446
f"--rdzv_endpoint={master_addr}:{master_port}",
439-
"--rdzv_id=0", # TODO: setup random ID that is gleaned from env variables
447+
f"--rdzv_id={rdzv_id}",
440448
]
441449
return torchrun_args, processes_per_node
442450

@@ -660,6 +668,7 @@ def _get_spark_task_function(
660668
# Spark task program
661669
def wrapped_train_fn(iterator): # type: ignore[no-untyped-def]
662670
import os
671+
import secrets
663672
import pandas as pd
664673
import pyarrow
665674
from pyspark import BarrierTaskContext
@@ -687,6 +696,15 @@ def set_torch_config(context: "BarrierTaskContext") -> None:
687696

688697
os.environ["MASTER_ADDR"] = str(addrs[0])
689698
os.environ["MASTER_PORT"] = str(get_free_port(addrs[0], context))
699+
# Unique per run so that concurrent runs sharing a rendezvous endpoint
700+
# do not collide.
701+
rdzv_id = secrets.token_hex(16) if context.partitionId() == 0 else ""
702+
rdzv_id = context.allGather(str(rdzv_id))[0]
703+
if not rdzv_id:
704+
raise RuntimeError(
705+
"Failed to generate a shared rendezvous id for distributed training."
706+
)
707+
os.environ["PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID"] = rdzv_id
690708
os.environ["WORLD_SIZE"] = str(num_processes)
691709
os.environ["NODE_RANK"] = str(context.partitionId())
692710
os.environ["RANK"] = str(context.partitionId())

python/pyspark/ml/torch/tests/test_distributor.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,21 @@ def test_create_torchrun_command(self) -> None:
281281
)
282282

283283
distributed_mode_input_params = {"num_processes": 4, "local_mode": False}
284-
input_env_vars = {"MASTER_ADDR": "localhost", "MASTER_PORT": "9350", "RANK": "3"}
284+
285+
# Without the per-run rendezvous id in the environment, command construction must
286+
# fail instead of falling back to a fixed rendezvous id.
287+
missing_rdzv_env_vars = {"MASTER_ADDR": "localhost", "MASTER_PORT": "9350", "RANK": "3"}
288+
self.setup_env_vars(missing_rdzv_env_vars)
289+
with self.assertRaisesRegex(RuntimeError, "PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID"):
290+
TorchDistributor._create_torchrun_command(distributed_mode_input_params, train_path)
291+
self.delete_env_vars(missing_rdzv_env_vars)
292+
293+
input_env_vars = {
294+
"MASTER_ADDR": "localhost",
295+
"MASTER_PORT": "9350",
296+
"RANK": "3",
297+
"PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID": "0123456789abcdef",
298+
}
285299

286300
args_number = [1, 3] # testing conversion to strings
287301
self.setup_env_vars(input_env_vars)
@@ -292,7 +306,7 @@ def test_create_torchrun_command(self) -> None:
292306
"--nnodes=4",
293307
"--node_rank=3",
294308
"--rdzv_endpoint=localhost:9350",
295-
"--rdzv_id=0",
309+
"--rdzv_id=0123456789abcdef",
296310
"--nproc_per_node=1",
297311
"train.py",
298312
"1",
@@ -313,13 +327,19 @@ def test_create_torchrun_command(self) -> None:
313327
"MASTER_ADDR": "11.22.33.44",
314328
"MASTER_PORT": "6677",
315329
"RANK": "1",
330+
"PYSPARK_TORCH_DISTRIBUTOR_RDZV_ID": "0123456789abcdef",
316331
},
317332
)
318333
def test_multi_gpu_node_get_torchrun_args(self):
319334
torchrun_args, processes_per_node = TorchDistributor._get_torchrun_args(False, 8)
320335
self.assertEqual(
321336
torchrun_args,
322-
["--nnodes=2", "--node_rank=1", "--rdzv_endpoint=11.22.33.44:6677", "--rdzv_id=0"],
337+
[
338+
"--nnodes=2",
339+
"--node_rank=1",
340+
"--rdzv_endpoint=11.22.33.44:6677",
341+
"--rdzv_id=0123456789abcdef",
342+
],
323343
)
324344
self.assertEqual(processes_per_node, 4)
325345

0 commit comments

Comments
 (0)