Skip to content

Commit 99a587f

Browse files
authored
Use HostMesh.spawn's bootstrap_command parameter to setup env vars (#3851)
In the past, the users generally use the `bootstrap` parameter to set up the env vars. In that approach, under the hood, monarch will spawn [a `SetupActor`](https://fburl.com/code/4yhl9t6g) on proc mesh, and run user's `bootstrap` callable inside that actor. In order to spawn the `SetupActor`, monarch's modules will be imported first. In rare cases, the monarch import could race with user's callable. For example, On CUDA 12.8, we observed that `import monarch._rust_bindings` lead to unexpected CDUA initialization, and make user's `CUDA_VISIBLE_DEVICES` setting too late. To solve this problem, Monarch added a `bootstrap_command` parameter to `HostMesh.spawn`, which offers a more reliable way to set up the env vars. More details can be found in the [monarch doc](https://github.com/meta-pytorch/monarch/blob/main/docs/source/actors.md?fbclid=IwY2xjawSwpk5wZG9mBGV4dG4DYWVtAjExAGJyaWQRMWRnbDJXbXNxcE1PQ1JEZDNzcnRjBmFwcF9pZAEwAAEeCXQ3kpG94UCcFM00_napubs8BqgLbYoxsHFbaomJr91TOIKjCNFBpmnVA3s_aem_P7ZE0RvYHb-YeFNctT9x-w#bootstrap-command-customization). Briefly, in this approach, user's env vars are set as part of the process spawning. Therefore, it is immune to the import race. Note that the old `bootstrap` parameter is still useful for non-env-var related setup. For example, in this PR, i still use it for the `import torch` warm up, a workaround which is necessary for the upstream's `concurrent import` bug.
1 parent 6164a75 commit 99a587f

1 file changed

Lines changed: 31 additions & 23 deletions

File tree

torchtitan/experiments/rl/train.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import asyncio
2626
import logging
2727
import os
28-
from collections.abc import Callable
2928
from dataclasses import dataclass
3029

3130
# must run before torch import. Set it as early as possible to avoid other
3231
# imports transitively importing torch.
3332
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3433

34+
# TODO: Remove `_src` after monarch cuts a new release. This is already a public
35+
# API in monarch nightly. https://github.com/meta-pytorch/monarch/pull/4327
36+
from monarch._src.actor.host_mesh import default_bootstrap_cmd
3537
from monarch.actor import HostMesh, ProcMesh, this_host
3638

3739
from torchtitan.config import ConfigManager, ParallelismConfig
@@ -50,7 +52,7 @@ def breakable_cudagraph_env(generator_cfg) -> dict[str, str]:
5052
makes prefill attention a cudagraph break (run eager at replay). The import happens before the
5153
generator actor's ``__init__`` and in the vLLM EngineCore worker subprocesses -- which do NOT
5254
inherit a runtime-set ``os.environ`` -- so setting it at runtime is too late. It must go in the
53-
proc's LAUNCH env (the spawn bootstrap, or the MAST role.env). Without it the decorator no-ops
55+
proc's LAUNCH env (the spawn ``bootstrap_command``, or the MAST role.env). Without it the decorator no-ops
5456
and prefill attention is captured as ``output.fill_(0)`` (zeroed) -> the model never reads the
5557
prompt -> coherent-but-unrelated output. FULL_DECODE_ONLY never captures prefill so it needs
5658
nothing. Shared so the OSS spawn path and the fbcode MAST launcher use one source of truth.
@@ -65,15 +67,21 @@ def breakable_cudagraph_env(generator_cfg) -> dict[str, str]:
6567
return {}
6668

6769

70+
def _preimport_torch() -> None:
71+
"""``bootstrap`` setup callable: pre-import torch on the spawned proc.
72+
"""
73+
# TODO: Remove once Monarch/PyTorch fixes concurrent import during unpickling.
74+
import torch # noqa: F401
75+
76+
6877
class PerHostProvisioner:
6978
"""Allocates non-overlapping GPU ranges within a single host.
7079
7180
On the same host, the trainer and generator run on separate GPU
7281
meshes (e.g. GPUs 0-3 for training, GPUs 4-7 for generation). Each
73-
call to `allocate(n)` reserves the next *n* GPUs and returns a
74-
bootstrap callable that sets `CUDA_VISIBLE_DEVICES` before CUDA
75-
initializes in the spawned process, ensuring each mesh only sees its
76-
own devices.
82+
call to `allocate(n)` reserves the next *n* GPUs and returns the launch
83+
env (`CUDA_VISIBLE_DEVICES` plus any `extra_env`) for those GPUs. The
84+
returned env vars are applied to the spawned process via ``bootstrap_command``.
7785
"""
7886

7987
def __init__(self, total_gpus: int = 8):
@@ -86,7 +94,7 @@ def available(self) -> int:
8694

8795
def allocate(
8896
self, num_gpus: int, *, extra_env: dict[str, str] | None = None
89-
) -> Callable[[], None]:
97+
) -> dict[str, str]:
9098
if num_gpus > self.available:
9199
raise RuntimeError(
92100
f"Requested {num_gpus} GPUs but only {self.available} "
@@ -95,18 +103,10 @@ def allocate(
95103
gpu_ids = list(range(self.next_gpu, self.next_gpu + num_gpus))
96104
self.next_gpu += num_gpus
97105

98-
def _bootstrap():
99-
os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in gpu_ids)
100-
# Per-proc LAUNCH env. Set here (in the bootstrap) -- not at runtime in the actor's
101-
# __init__ -- because rl/models/attention.py's @eager_break_during_capture reads
102-
# VLLM_USE_BREAKABLE_CUDAGRAPH at MODULE IMPORT, which happens before __init__ and
103-
# in the vLLM EngineCore worker subprocs (which do not inherit a runtime-set env).
104-
if extra_env:
105-
os.environ.update(extra_env)
106-
# TODO: Remove once Monarch/PyTorch fixes concurrent import during unpickling.
107-
import torch # noqa: F401
108-
109-
return _bootstrap
106+
env = {"CUDA_VISIBLE_DEVICES": ",".join(str(g) for g in gpu_ids)}
107+
if extra_env:
108+
env.update(extra_env)
109+
return env
110110

111111

112112
@dataclass
@@ -151,9 +151,11 @@ def _spawn_proc_mesh(
151151
)
152152
role_gpus_per_node = role_world_size // nodes
153153
provisioner = PerHostProvisioner(total_gpus=gpus_per_node)
154+
env = provisioner.allocate(role_gpus_per_node, extra_env=extra_env)
154155
return host_mesh.spawn_procs(
155156
per_host={"gpus": role_gpus_per_node},
156-
bootstrap=provisioner.allocate(role_gpus_per_node, extra_env=extra_env),
157+
bootstrap=_preimport_torch,
158+
bootstrap_command=default_bootstrap_cmd().with_env(env),
157159
)
158160

159161

@@ -216,13 +218,19 @@ def spawn_proc_mesh(
216218
provisioner = PerHostProvisioner(total_gpus=total_gpus)
217219
trainer_mesh = host_mesh.spawn_procs(
218220
per_host={"gpus": trainer_world_size},
219-
bootstrap=provisioner.allocate(trainer_world_size),
221+
bootstrap=_preimport_torch,
222+
bootstrap_command=default_bootstrap_cmd().with_env(
223+
provisioner.allocate(trainer_world_size)
224+
),
220225
)
221226
generator_meshes = [
222227
host_mesh.spawn_procs(
223228
per_host={"gpus": per_generator_world_size},
224-
bootstrap=provisioner.allocate(
225-
per_generator_world_size, extra_env=generator_env
229+
bootstrap=_preimport_torch,
230+
bootstrap_command=default_bootstrap_cmd().with_env(
231+
provisioner.allocate(
232+
per_generator_world_size, extra_env=generator_env
233+
)
226234
),
227235
)
228236
for _ in range(num_generators)

0 commit comments

Comments
 (0)