@@ -158,52 +158,87 @@ def build_remat_bank_chain(numels: dict[str, int], n_layers: int) -> tvm.IRModul
158158 with bb .function ("train_step" , [act0 , param , paramg0 , actg0 ]):
159159 with bb .dataflow ():
160160 # ---- FORWARD ----
161- # Thread act forward through every region. SAVE the entering activation
162- # act[i] at each boundary (kept live across backward for recompute), and
163- # SAVE checkpoint_i at each boundary (read directly by bwd i, no recompute).
164- # Non-boundary act/checkpoint outputs die at last use (forward-flowing).
161+ # Thread act forward through every region. At each boundary SAVE (a) the
162+ # checkpoint_i (read directly by bwd i, no recompute) and (b) the EXITING
163+ # activation act[i+1] (the activation ENTERING region i+1), which is the
164+ # recompute START point for that segment's backward pass. Non-boundary
165+ # act/checkpoint outputs die at last use (forward-flowing). Only O(sqrt N)
166+ # boundary checkpoints + O(sqrt N) boundary exit-activations span backward.
165167 act = act0
166- saved_act : dict [int , relax .Var ] = {} # boundary -> entering activation
167- saved_ckpt : dict [int , relax .Var ] = {} # boundary -> saved checkpoint
168+ saved_ckpt : dict [int , relax .Var ] = {} # boundary -> saved checkpoint
169+ saved_exit : dict [int , relax .Var ] = {} # boundary -> activation EXITING boundary
168170 for i in range (n_layers ):
169- if i in bset :
170- saved_act [i ] = act # the activation ENTERING fwd region i
171171 out = bb .emit (relax .call_dps_packed (
172172 f"pathc.bank_fwd_{ i } " , [act , param , act ],
173173 [sAct , sState ]))
174174 act_next = bb .emit (relax .TupleGetItem (out , 0 ))
175175 ck = bb .emit (relax .TupleGetItem (out , 1 ))
176176 if i in bset :
177177 saved_ckpt [i ] = ck # boundary checkpoint: live until bwd i
178+ saved_exit [i ] = act_next # activation ENTERING region i+1 (recompute start)
178179 act = act_next
179180
180- # ---- BACKWARD ----
181+ # ---- BACKWARD (Korthikanti per-segment recompute cache) ----
181182 # For each region i (reverse): obtain checkpoint_i. If i is a boundary,
182- # use the saved checkpoint. Else RE-EMIT the forward from the nearest
183- # boundary up to i to regenerate checkpoint_i LOCALLY (recompute), so the
184- # recomputed checkpoint is born here and killed at the bwd i call below.
183+ # use the saved checkpoint. Else the checkpoint must be RECOMPUTED from the
184+ # nearest saved boundary. The KEY anti-pattern fix vs the naive remat: do
185+ # NOT re-derive the prefix b..i independently for every non-boundary i
186+ # (that is the O(N*sqrt N) checkpointing anti-pattern -- region b+1
187+ # recomputes [b..b+1], b+2 recomputes [b..b+2] from scratch, etc., each
188+ # re-running the same prefix). Instead WALK EACH SEGMENT ONCE: the first
189+ # time the backward pass needs a recomputed checkpoint in a segment,
190+ # re-emit the forward b..segment_end exactly once and CACHE every
191+ # checkpoint b+1..segment_end. Every later backward region in that segment
192+ # reads its checkpoint straight from the cache -- zero extra forward calls.
193+ #
194+ # Recompute count drops from O(N*sqrt N) (sum 2+3+..+L per segment) to
195+ # O(N) (each non-boundary region recomputed exactly once: N - #boundaries
196+ # total). Numerically IDENTICAL: the cached ck_j is the same op on the same
197+ # boundary activation as before, just emitted once instead of redundantly.
198+ #
199+ # Liveness is preserved: a segment's recompute is emitted lazily, the FIRST
200+ # time (in reverse order) that segment is entered -- i.e. immediately before
201+ # that segment's highest backward region consumes its checkpoint -- so the
202+ # recomputed checkpoints are born inside the segment's local backward window
203+ # and the planner still sees them as short-lived (O(sqrt N) peak), exactly
204+ # as the naive remat. We only remove the REDUNDANT re-emissions.
205+ seg_end_of : dict [int , int ] = {} # boundary b -> last region index in its segment
206+ for k , b in enumerate (boundaries ):
207+ seg_end_of [b ] = (boundaries [k + 1 ] - 1 ) if k + 1 < len (boundaries ) \
208+ else (n_layers - 1 )
209+ recomputed_ckpt : dict [int , relax .Var ] = {} # region j -> its recomputed checkpoint
210+
211+ def _recompute_segment (b : int ) -> None :
212+ """Re-emit the forward (b+1)..seg_end ONCE, caching every checkpoint.
213+ Starts from saved_exit[b] (the activation entering region b+1), so the
214+ boundary region b's own forward is NOT re-run (its checkpoint is already
215+ saved). Lazy + idempotent: called the first time the backward pass needs
216+ a recomputed checkpoint in segment b, then never re-emitted. Emits
217+ exactly (seg_end - b) forward calls = one per non-boundary region."""
218+ rec_act = saved_exit [b ]
219+ for j in range (b + 1 , seg_end_of [b ] + 1 ):
220+ rout = bb .emit (relax .call_dps_packed (
221+ f"pathc.bank_fwd_{ j } " , [rec_act , param , rec_act ],
222+ [sAct , sState ]))
223+ rec_act = bb .emit (relax .TupleGetItem (rout , 0 ))
224+ rec_ck = bb .emit (relax .TupleGetItem (rout , 1 ))
225+ recomputed_ckpt [j ] = rec_ck
226+
185227 actg = actg0
186228 paramg = paramg0
187229 for i in reversed (range (n_layers )):
188230 if i in bset :
189231 ck_i = saved_ckpt [i ]
190232 else :
191233 b = nearest_boundary (i , boundaries )
192- # recompute forward b..i from the saved boundary activation,
193- # regenerating each intermediate checkpoint; keep only ck_i live.
194- rec_act = saved_act [b ]
195- ck_i = None
196- for j in range (b , i + 1 ):
197- rout = bb .emit (relax .call_dps_packed (
198- f"pathc.bank_fwd_{ j } " , [rec_act , param , rec_act ],
199- [sAct , sState ]))
200- rec_act = bb .emit (relax .TupleGetItem (rout , 0 ))
201- rec_ck = bb .emit (relax .TupleGetItem (rout , 1 ))
202- ck_i = rec_ck # the last segment step's checkpoint == ckpt_i
234+ if i not in recomputed_ckpt :
235+ # first backward region in this segment -> recompute it ONCE.
236+ _recompute_segment (b )
237+ ck_i = recomputed_ckpt .get (i )
203238 if ck_i is None :
204239 raise RuntimeError (
205240 "FAIL-LOUD: recompute produced no checkpoint for "
206- f"region { i } " )
241+ f"region { i } (segment boundary { b } ) " )
207242 out = bb .emit (relax .call_dps_packed (
208243 f"pathc.bank_bwd_{ i } " , [actg , param , ck_i , paramg ],
209244 [sActG , sParamG ]))
@@ -220,18 +255,15 @@ def build_remat_bank_chain(numels: dict[str, int], n_layers: int) -> tvm.IRModul
220255def recompute_overhead (n_layers : int ) -> tuple [int , int , float ]:
221256 """Returns (n_forward_calls_baseline, n_extra_recompute_calls, overhead_factor).
222257
223- Baseline forward calls = n_layers (one per region). Recompute calls = for each
224- NON-boundary backward region i, recompute from nearest boundary b..i = (i-b+1)
225- extra forward calls.
258+ Baseline forward calls = n_layers (one per region). With the Korthikanti
259+ per-segment recompute cache, each segment is walked exactly ONCE during backward
260+ (re-emitting (b+1)..seg_end), so each NON-boundary region is recomputed exactly
261+ once -> extra recompute calls = n_layers - #boundaries. This is the O(N) lower
262+ bound for sqrt-N checkpointing; the naive remat's redundant per-region prefix
263+ re-derivation (O(N*sqrt N), sum 2+3+..+L per segment) is eliminated.
226264 """
227265 boundaries = checkpoint_boundaries (n_layers )
228- bset = set (boundaries )
229- extra = 0
230- for i in range (n_layers ):
231- if i in bset :
232- continue
233- b = nearest_boundary (i , boundaries )
234- extra += (i - b + 1 )
266+ extra = n_layers - len (boundaries )
235267 return n_layers , extra , extra / max (1 , n_layers )
236268
237269
0 commit comments