1515 call run_dpa_with_cp(**kwargs) — the same work function the
1616 per-case subprocess design uses, with NVTE_CP_POOL_PG=1 so the
1717 function reuses our PG instead of re-initing it
18- torch.cuda.empty_cache() + gc.collect() per case
18+ torch.cuda.empty_cache() per case
1919 all ranks gather (ok, error_msg) to rank 0
2020 rank 0:
2121 write one JSON response line to stdout
2626 response: {"ok": true}
2727 {"ok": false, "error": "first failing rank's traceback"}
2828"""
29- import gc
3029import json
3130import os
3231import sys
4342from transformer_engine .pytorch .quantization import FP8GlobalStateManager
4443
4544
46- # Env vars run_dpa_with_cp re-sets at the top of every call. We pop them
47- # defensively between cases so a future caller that *doesn't* re-set them
48- # can't inherit a leftover value from a previous case in the same worker.
49- _TRANSIENT_ENV_KEYS = (
50- "NVTE_FP8_DPA_BWD" ,
51- "NVTE_DPA_FP8CS_O_in_F16" ,
52- "NVTE_FLASH_ATTN" ,
53- "NVTE_FUSED_ATTN" ,
54- "NVTE_ALLOW_NONDETERMINISTIC_ALGO" ,
55- )
56-
57-
5845def _recv_request (rank : int ) -> dict :
5946 box = [None ]
6047 if rank == 0 :
@@ -79,17 +66,17 @@ def _send_response(rank: int, payload: dict) -> None:
7966def _reset_between_cases () -> None :
8067 """Drop state that would otherwise cascade across cases.
8168
82- Matches the per-case startup of the single-shot worker (``_run_single_config``
83- on the per-case-subprocess branch): identical RNG seed at the start of every
84- case, FP8 state cleared, transient env vars cleared, allocator clean.
69+ Matches the per-case startup of the single-shot worker
70+ (``_run_single_config`` on the per-case-subprocess branch): identical RNG
71+ seed at the start of every case, FP8 state cleared, allocator clean.
72+ ``run_dpa_with_cp`` re-sets ``NVTE_FUSED_ATTN``/``NVTE_FLASH_ATTN``
73+ unconditionally and pops the other transient env vars itself, so no
74+ explicit pop is needed here.
8575 """
8676 torch .manual_seed (1234 )
8777 torch .cuda .manual_seed (1234 )
8878 FP8GlobalStateManager .reset ()
89- for env_key in _TRANSIENT_ENV_KEYS :
90- os .environ .pop (env_key , None )
9179 torch .cuda .empty_cache ()
92- gc .collect ()
9380
9481
9582_case_counter = 0
@@ -125,13 +112,49 @@ def _run_one(req: dict, rank: int) -> tuple[bool, str]:
125112 return ok , err
126113
127114
115+ def _create_cp_comm_groups (rank : int , world_size : int ) -> tuple :
116+ """Pre-create the CP collective groups for this pool.
117+
118+ world_size and the rank set are constant for the lifetime of one pool, so
119+ the world group and the a2a+p2p sub-groups are deterministic. Creating
120+ them once here and reusing them across every case eliminates ~50-100 ms
121+ of NCCL setup per case (cyanguwa's review feedback on PR #2993).
122+
123+ Returns ``(world_group, a2a_p2p_sub_groups)``. ``a2a_p2p_sub_groups`` is
124+ empty when world_size is too small to support a2a+p2p (needs an even
125+ world_size ≥ 4); cases with cp_comm_type='a2a+p2p' wouldn't be routed to
126+ such a pool anyway.
127+ """
128+ world_group = dist .new_group (range (world_size ), backend = "nccl" )
129+ sub_groups : list = []
130+ if world_size >= 4 and world_size % 2 == 0 :
131+ # Mirror the layout in run_attention_with_cp.py: cp_size/2 pairs along
132+ # axis 0, plus 2 stride-2 groups along axis 1.
133+ cp_comm_sub_ranks = [range (i * 2 , (i + 1 ) * 2 ) for i in range (world_size // 2 )]
134+ cp_comm_sub_ranks += [range (i , world_size , 2 ) for i in range (2 )]
135+ for sub_ranks in cp_comm_sub_ranks :
136+ sub_group = dist .new_group (sub_ranks , backend = "nccl" )
137+ if rank in sub_ranks :
138+ sub_groups .append (sub_group )
139+ return world_group , sub_groups
140+
141+
128142def main () -> None :
129143 rank = int (os .environ ["RANK" ])
130144 world_size = int (os .environ ["WORLD_SIZE" ])
131145 torch .cuda .set_device (rank % torch .cuda .device_count ())
132146 dist .init_process_group (backend = "nccl" , rank = rank , world_size = world_size )
133147 os .environ ["NVTE_CP_POOL_PG" ] = "1"
134148
149+ # Stash pool-shared CP groups on the run_attention_with_cp module so
150+ # run_dpa_with_cp can read them per case. Imported here (after the env var
151+ # is set) to keep import-time side effects minimal.
152+ import run_attention_with_cp as _rac
153+
154+ _rac ._pool_cp_comm_group , _rac ._pool_cp_comm_sub_groups = _create_cp_comm_groups (
155+ rank , world_size
156+ )
157+
135158 try :
136159 while True :
137160 req = _recv_request (rank )
@@ -141,12 +164,10 @@ def main() -> None:
141164 ok , msg = _run_one (req , rank )
142165
143166 gathered : list [tuple [bool , str ]] = [None ] * world_size # type: ignore[list-item]
167+ # gather_object is itself a collective synchronization point — if
168+ # every rank reached it, none is ahead. No extra barrier needed.
144169 dist .gather_object ((ok , msg ), gathered if rank == 0 else None , dst = 0 )
145170
146- # Surface a wedged communicator here, before the next case's
147- # collectives can inherit the corruption.
148- dist .barrier ()
149-
150171 if rank == 0 :
151172 all_ok = all (o for o , _ in gathered )
152173 if all_ok :
@@ -155,6 +176,21 @@ def main() -> None:
155176 first_err = next (m for o , m in gathered if not o )
156177 _send_response (rank , {"ok" : False , "error" : first_err })
157178 finally :
179+ # Tear down pool-shared CP groups before the main PG (NCCL requires
180+ # sub-groups to be destroyed first). Each destroy is independently
181+ # guarded so a wedged communicator on one group doesn't leak the rest.
182+ if _rac ._pool_cp_comm_group is not None :
183+ try :
184+ dist .destroy_process_group (_rac ._pool_cp_comm_group )
185+ except Exception :
186+ pass
187+ for g in _rac ._pool_cp_comm_sub_groups :
188+ try :
189+ dist .destroy_process_group (g )
190+ except Exception :
191+ pass
192+ _rac ._pool_cp_comm_group = None
193+ _rac ._pool_cp_comm_sub_groups = []
158194 dist .destroy_process_group ()
159195
160196
0 commit comments