55import torch .distributed as dist
66
77
8- def _continuation_mask (tensor , packed_seq_params , cp_group , dims ):
8+ def _continuation_mask (tensor , packed_seq_params , cp_group , dims , sp_group = None ):
99 """Return the local rows whose next token belongs to the same real document."""
10- size = cp_group .size () if cp_group is not None else 1
1110 rank = cp_group .rank () if cp_group is not None else 0
11+ if sp_group is not None :
12+ rank = rank * sp_group .size () + sp_group .rank ()
1213 length = tensor .shape [dims ]
13- rows = torch .arange (rank * length , (rank + 1 ) * length , device = tensor .device )
14- if packed_seq_params is None :
15- ends = torch .full_like (rows , length * size )
16- return rows + 1 < ends
1714 real = packed_seq_params .cu_seqlens_q
1815 physical = packed_seq_params .cu_seqlens_q_padded
1916 physical = real if physical is None else physical
17+ rows = torch .arange (rank * length , (rank + 1 ) * length , device = tensor .device )
2018 seq = torch .bucketize (rows , physical [1 :], right = True )
2119 in_pack = seq < physical .numel () - 1
2220 seq = seq .clamp_max (physical .numel () - 2 )
@@ -25,75 +23,106 @@ def _continuation_mask(tensor, packed_seq_params, cp_group, dims):
2523 return valid
2624
2725
26+ def exchange_neighbor_tokens (tokens , group , directions , * , wrap = False ):
27+ """Batch boundary exchanges, preserving each token's shape and dtype.
28+
29+ Each direction is -1 (send left, receive right) or +1 (send right, receive left).
30+ Missing neighbors return None unless wrap is enabled. Callers own boundary fills
31+ and autograd; this helper only performs communication on the supplied group.
32+ """
33+ rank = group .rank () if group is not None else 0
34+ size = group .size () if group is not None else 1
35+ received , ops = [], []
36+ for token , direction in zip (tokens , directions ):
37+ send_rank , recv_rank = rank + direction , rank - direction
38+ if wrap :
39+ send_rank , recv_rank = send_rank % size , recv_rank % size
40+ if 0 <= send_rank < size :
41+ ops .append (
42+ dist .P2POp (
43+ dist .isend , token .contiguous (), dist .get_global_rank (group , send_rank ), group
44+ )
45+ )
46+ recv = None
47+ if 0 <= recv_rank < size :
48+ recv = torch .empty_like (token , memory_format = torch .contiguous_format )
49+ ops .append (dist .P2POp (dist .irecv , recv , dist .get_global_rank (group , recv_rank ), group ))
50+ received .append (recv )
51+ # P2POp retains each contiguous send buffer until the batch completes.
52+ for work in dist .batch_isend_irecv (ops ) if ops else []:
53+ work .wait ()
54+ return received
55+
56+
2857class _RollLeft (torch .autograd .Function ):
2958 """One differentiable left roll, including the reverse neighbor exchange."""
3059
3160 @staticmethod
32- def forward (ctx , tensor , group , dims ):
61+ def forward (ctx , tensor , group , dims , sp_group ):
3362 """Shift local values and receive the next rank's first row."""
34- ctx .group , ctx .dims = group , dims
63+ ctx .group , ctx .dims , ctx . sp_group = group , dims , sp_group
3564 result = torch .roll (tensor , - 1 , dims )
36- result .select (dims , - 1 ).zero_ ()
37- if group is not None and group .size () > 1 :
38- rank , size = group .rank (), group .size ()
39- send = tensor .select (dims , 0 ).contiguous ()
40- recv = torch .empty_like (send )
41- ops = []
42- if rank > 0 :
43- ops .append (
44- dist .P2POp (dist .isend , send , dist .get_global_rank (group , rank - 1 ), group )
45- )
46- if rank + 1 < size :
47- ops .append (
48- dist .P2POp (dist .irecv , recv , dist .get_global_rank (group , rank + 1 ), group )
49- )
50- for work in dist .batch_isend_irecv (ops ):
51- work .wait ()
52- if rank + 1 < size :
53- result .select (dims , - 1 ).copy_ (recv )
65+ boundary = tensor .select (dims , 0 )
66+ if sp_group is None or sp_group .rank () == 0 :
67+ (boundary ,) = exchange_neighbor_tokens ((boundary ,), group , (- 1 ,))
68+ if boundary is None :
69+ boundary = torch .zeros_like (tensor .select (dims , 0 ))
70+ # Owners are ordered CP rank first, then SP rank. Relay the next CP
71+ # shard's first token from SP rank zero to the last SP rank.
72+ if sp_group is not None and sp_group .size () > 1 :
73+ (boundary ,) = exchange_neighbor_tokens ((boundary ,), sp_group , (- 1 ,), wrap = True )
74+ result .select (dims , - 1 ).copy_ (boundary )
5475 return result
5576
5677 @staticmethod
5778 def backward (ctx , grad ):
5879 """Return the boundary gradient to its original token owner."""
59- group , dims = ctx .group , ctx .dims
80+ group , dims , sp_group = ctx .group , ctx .dims , ctx . sp_group
6081 result = torch .roll (grad , 1 , dims )
61- result .select (dims , 0 ).zero_ ()
62- if group is not None and group .size () > 1 :
63- rank , size = group .rank (), group .size ()
64- send = grad .select (dims , - 1 ).contiguous ()
65- recv = torch .empty_like (send )
66- ops = []
67- if rank + 1 < size :
68- ops .append (
69- dist .P2POp (dist .isend , send , dist .get_global_rank (group , rank + 1 ), group )
70- )
71- if rank > 0 :
72- ops .append (
73- dist .P2POp (dist .irecv , recv , dist .get_global_rank (group , rank - 1 ), group )
74- )
75- for work in dist .batch_isend_irecv (ops ):
76- work .wait ()
77- if rank > 0 :
78- result .select (dims , 0 ).copy_ (recv )
79- return result , None , None
82+ boundary = grad .select (dims , - 1 )
83+ if sp_group is not None and sp_group .size () > 1 :
84+ (boundary ,) = exchange_neighbor_tokens ((boundary ,), sp_group , (1 ,), wrap = True )
85+ if sp_group is None or sp_group .rank () == 0 :
86+ (boundary ,) = exchange_neighbor_tokens ((boundary ,), group , (1 ,))
87+ if boundary is None :
88+ boundary = torch .zeros_like (grad .select (dims , - 1 ))
89+ result .select (dims , 0 ).copy_ (boundary )
90+ return result , None , None , None
8091
8192
82- def roll_contiguous (tensor , dims , cp_group , packed_seq_params = None , fill_value = 0 ):
83- """Roll a dense or packed contiguous shard by one, preserving real document boundaries."""
93+ def roll_contiguous (tensor , dims , cp_group , packed_seq_params = None , fill_value = 0 , * , sp_group = None ):
94+ """Roll a contiguous shard by one, preserving real document boundaries.
95+
96+ With sequence parallelism, each CP shard is further split in SP rank order.
97+ Both boundary exchanges are differentiable and use the supplied groups.
98+ """
8499 if tensor .shape [dims ] == 0 :
85100 return tensor .clone ()
86- valid = _continuation_mask (tensor , packed_seq_params , cp_group , dims )
101+ cp_size = cp_group .size () if cp_group is not None else 1
102+ sp_size = sp_group .size () if sp_group is not None else 1
103+ if cp_size == sp_size == 1 :
104+ result = torch .roll (tensor , - 1 , dims )
105+ result .select (dims , - 1 ).fill_ (fill_value )
106+ else :
107+ result = _RollLeft .apply (tensor , cp_group , dims , sp_group )
108+ if fill_value != 0 :
109+ last_cp = cp_group is None or cp_group .rank () == cp_size - 1
110+ last_sp = sp_group is None or sp_group .rank () == sp_size - 1
111+ if last_cp and last_sp :
112+ result .select (dims , - 1 ).fill_ (fill_value )
113+ if packed_seq_params is None :
114+ return result
115+ valid = _continuation_mask (tensor , packed_seq_params , cp_group , dims , sp_group )
87116 shape = [1 ] * tensor .ndim
88117 shape [dims ] = tensor .shape [dims ]
89- return _RollLeft . apply ( tensor , cp_group , dims ). masked_fill (~ valid .view (shape ), fill_value )
118+ return result . masked_fill_ (~ valid .view (shape ), fill_value )
90119
91120
92121def roll_contiguous_fields (tensors , cp_group , packed_seq_params = None , fill_values = None ):
93122 """Roll non-differentiable token fields together, preserving each dtype and fill value.
94123
95124 This batches one depth's IDs/positions/masks; activations continue to use the
96- differentiable roll above. No cross-layer cache or model-wide state is introduced.
125+ differentiable roll above.
97126 """
98127 present = [tensor for tensor in tensors if tensor is not None ]
99128 if not present :
@@ -110,34 +139,27 @@ def roll_contiguous_fields(tensors, cp_group, packed_seq_params=None, fill_value
110139 raise ValueError ("Each token field needs one fill value." )
111140 if length == 0 :
112141 return tuple (None if t is None else t .clone () for t in tensors )
113- valid = _continuation_mask (reference , packed_seq_params , cp_group , - 1 )
114- rank = cp_group .rank () if cp_group is not None else 0
115- size = cp_group .size () if cp_group is not None else 1
116- outputs , received , sends , ops = [], [], [], []
142+ invalid = (
143+ ~ _continuation_mask (reference , packed_seq_params , cp_group , - 1 )
144+ if packed_seq_params is not None
145+ else None
146+ )
147+ boundaries = exchange_neighbor_tokens (
148+ [tensor [..., 0 ] for tensor in present ], cp_group , [- 1 ] * len (present )
149+ )
150+ received = iter (boundaries )
151+ outputs = []
117152 for tensor , fill in zip (tensors , fill_values ):
118153 if tensor is None :
119154 outputs .append (None )
120155 continue
121156 result = torch .roll (tensor , - 1 , - 1 )
122- result [..., - 1 ].fill_ (fill )
157+ boundary = next (received )
158+ if boundary is not None :
159+ result [..., - 1 ].copy_ (boundary )
160+ else :
161+ result [..., - 1 ].fill_ (fill )
162+ if invalid is not None :
163+ result .masked_fill_ (invalid , fill )
123164 outputs .append (result )
124- if rank > 0 :
125- send = tensor [..., 0 ].contiguous ()
126- sends .append (send ) # Retain temporary send storage until all work completes.
127- ops .append (
128- dist .P2POp (dist .isend , send , dist .get_global_rank (cp_group , rank - 1 ), cp_group )
129- )
130- if rank + 1 < size :
131- recv = torch .empty_like (tensor [..., 0 ])
132- received .append ((result , recv ))
133- ops .append (
134- dist .P2POp (dist .irecv , recv , dist .get_global_rank (cp_group , rank + 1 ), cp_group )
135- )
136- for work in dist .batch_isend_irecv (ops ) if ops else []:
137- work .wait ()
138- for result , recv in received :
139- result [..., - 1 ].copy_ (recv )
140- for result , fill in zip (outputs , fill_values ):
141- if result is not None :
142- result .masked_fill_ (~ valid , fill )
143165 return tuple (outputs )
0 commit comments