Skip to content

Commit cc236fd

Browse files
committed
perf(relax): Korthikanti per-segment recompute cache — launch fusion, 45.44 tok/s @8l (1.524x over §13), 28L 11.76 tok/s (2.267x)
§14 lands + measures the launch-fusion lever named in §13. The naive sqrt-N remat re-derived the forward prefix b..i independently per non-boundary region (O(N·√N) anti-pattern); the Korthikanti cache walks each segment ONCE, caching all its checkpoints (O(N) = N−#boundaries recompute). Numerically identical (loss 5.525e-06 bit-for-bit with §13). MEASURED gb10 CUDA 8L/4steps: 137.36s→90.14s/step, 29.82→45.44 tok/s (1.524×). Forward launches 20→13 (8L), 117→51 (28L). Per-call MR kernel unchanged (6.585→6.560 s — same kernel; pure launch-count win). 28L extrapolates 5.19→ 11.76 tok/s (2.267×). Gap vs Megatron 3399: 8L 114×→75×, 28L 654×→289×. Honest cost: segment checkpoint cache holds a whole segment's recomputed checkpoints concurrently → planned device-peak 4.682→6.400 GB (8L), 8.787→ 12.998 GB (28L), still 2× under Megatron's 26 GB. Next floor: the 6.56 s/call MR kernel itself (95.0% of step) — needs a faster/fused/FP8 kernel, not fewer launches.
1 parent ee543df commit cc236fd

4 files changed

Lines changed: 327 additions & 98 deletions

File tree

cppmega_mlx/runtime/path_c_relax_step_optim.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,42 +206,59 @@ def build_full_step_with_optim(numels: dict[str, int], n_layers: int,
206206
v0 = relax.Var("v0", sV)
207207
with bb.function("train_step", [act0, param, paramg0, actg0, m0, v0]):
208208
with bb.dataflow():
209-
# ---- FORWARD (sqrt-N remat: save boundary act + boundary checkpoints) ----
209+
# ---- FORWARD (sqrt-N remat) ----
210+
# SAVE at each boundary: the checkpoint (read directly by bwd) and the
211+
# EXITING activation (entering region i+1) = the recompute START point.
210212
act = act0
211-
saved_act: dict[int, relax.Var] = {}
212213
saved_ckpt: dict[int, relax.Var] = {}
214+
saved_exit: dict[int, relax.Var] = {}
213215
for i in range(n_layers):
214-
if i in bset:
215-
saved_act[i] = act
216216
out = bb.emit(relax.call_dps_packed(
217217
f"pathc.bank_fwd_{i}", [act, param, act], [sAct, sState]))
218218
act_next = bb.emit(relax.TupleGetItem(out, 0))
219219
ck = bb.emit(relax.TupleGetItem(out, 1))
220220
if i in bset:
221221
saved_ckpt[i] = ck
222+
saved_exit[i] = act_next
222223
act = act_next
223224

224-
# ---- BACKWARD (re-emit forward for non-boundary checkpoints) ----
225+
# ---- BACKWARD (Korthikanti per-segment recompute cache) ----
226+
# Walk each segment ONCE: the first backward region needing a recomputed
227+
# checkpoint re-emits the forward (b+1)..seg_end exactly once, caching every
228+
# checkpoint; later backward regions in the segment read the cache. O(N)
229+
# recompute (N - #boundaries) instead of the naive O(N*sqrt N) per-region
230+
# prefix re-derivation. Numerically identical; recomputes are emitted lazily
231+
# inside each segment's local backward window so they stay short-lived.
232+
seg_end_of: dict[int, int] = {}
233+
for k, b in enumerate(boundaries):
234+
seg_end_of[b] = (boundaries[k + 1] - 1) if k + 1 < len(boundaries) \
235+
else (n_layers - 1)
236+
recomputed_ckpt: dict[int, relax.Var] = {}
237+
238+
def _recompute_segment(b: int) -> None:
239+
rec_act = saved_exit[b]
240+
for j in range(b + 1, seg_end_of[b] + 1):
241+
rout = bb.emit(relax.call_dps_packed(
242+
f"pathc.bank_fwd_{j}", [rec_act, param, rec_act],
243+
[sAct, sState]))
244+
rec_act = bb.emit(relax.TupleGetItem(rout, 0))
245+
rec_ck = bb.emit(relax.TupleGetItem(rout, 1))
246+
recomputed_ckpt[j] = rec_ck
247+
225248
actg = actg0
226249
paramg = paramg0
227250
for i in reversed(range(n_layers)):
228251
if i in bset:
229252
ck_i = saved_ckpt[i]
230253
else:
231254
b = nearest_boundary(i, boundaries)
232-
rec_act = saved_act[b]
233-
ck_i = None
234-
for j in range(b, i + 1):
235-
rout = bb.emit(relax.call_dps_packed(
236-
f"pathc.bank_fwd_{j}", [rec_act, param, rec_act],
237-
[sAct, sState]))
238-
rec_act = bb.emit(relax.TupleGetItem(rout, 0))
239-
rec_ck = bb.emit(relax.TupleGetItem(rout, 1))
240-
ck_i = rec_ck
255+
if i not in recomputed_ckpt:
256+
_recompute_segment(b)
257+
ck_i = recomputed_ckpt.get(i)
241258
if ck_i is None:
242259
raise RuntimeError(
243260
"FAIL-LOUD: recompute produced no checkpoint for "
244-
f"region {i}")
261+
f"region {i} (segment boundary {b})")
245262
out = bb.emit(relax.call_dps_packed(
246263
f"pathc.bank_bwd_{i}", [actg, param, ck_i, paramg],
247264
[sActG, sParamG]))

cppmega_mlx/runtime/path_c_relax_step_remat.py

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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
220255
def 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

cppmega_mlx/runtime/path_c_relax_train_step.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -280,45 +280,63 @@ def build_train_step(numels: dict[str, int], n_layers: int) -> tvm.IRModule:
280280
with bb.function("train_step", [act0, param, paramg0, actg0, m0, v0]):
281281
with bb.dataflow():
282282
# ---- FORWARD (sqrt-N remat) ----
283+
# SAVE at each boundary: the checkpoint (read directly by bwd) and the
284+
# EXITING activation (entering region i+1) = the recompute START point.
283285
act = act0
284-
saved_act: dict[int, relax.Var] = {}
285286
saved_ckpt: dict[int, relax.Var] = {}
287+
saved_exit: dict[int, relax.Var] = {}
286288
for i in range(n_layers):
287-
if i in bset:
288-
saved_act[i] = act
289289
out = bb.emit(relax.call_dps_packed(
290290
f"pathc.bank_fwd_{i}", [act, param, act], [sAct, sState]))
291291
act_next = bb.emit(relax.TupleGetItem(out, 0))
292292
ck = bb.emit(relax.TupleGetItem(out, 1))
293293
if i in bset:
294294
saved_ckpt[i] = ck
295+
saved_exit[i] = act_next
295296
act = act_next
296297
act_final = act
297298

298299
# ---- LOSS on the final forward activation ----
299300
loss = bb.emit(relax.call_dps_packed("pathc.bank_loss", [act_final], [sLoss]))
300301

301-
# ---- BACKWARD (re-emit forward for non-boundary checkpoints) ----
302+
# ---- BACKWARD (Korthikanti per-segment recompute cache) ----
303+
# Walk each segment ONCE: the first backward region that needs a recomputed
304+
# checkpoint re-emits the forward (b+1)..seg_end exactly once, caching every
305+
# checkpoint; every later backward region in the segment reads the cache.
306+
# This is O(N) recompute (N - #boundaries) instead of the naive O(N*sqrt N)
307+
# per-region prefix re-derivation. Numerically identical; the recomputes are
308+
# emitted lazily inside each segment's local backward window so they stay
309+
# short-lived (O(sqrt N) checkpoint peak).
310+
seg_end_of: dict[int, int] = {}
311+
for k, b in enumerate(boundaries):
312+
seg_end_of[b] = (boundaries[k + 1] - 1) if k + 1 < len(boundaries) \
313+
else (n_layers - 1)
314+
recomputed_ckpt: dict[int, relax.Var] = {}
315+
316+
def _recompute_segment(b: int) -> None:
317+
rec_act = saved_exit[b]
318+
for j in range(b + 1, seg_end_of[b] + 1):
319+
rout = bb.emit(relax.call_dps_packed(
320+
f"pathc.bank_fwd_{j}", [rec_act, param, rec_act],
321+
[sAct, sState]))
322+
rec_act = bb.emit(relax.TupleGetItem(rout, 0))
323+
rec_ck = bb.emit(relax.TupleGetItem(rout, 1))
324+
recomputed_ckpt[j] = rec_ck
325+
302326
actg = actg0
303327
paramg = paramg0
304328
for i in reversed(range(n_layers)):
305329
if i in bset:
306330
ck_i = saved_ckpt[i]
307331
else:
308332
b = nearest_boundary(i, boundaries)
309-
rec_act = saved_act[b]
310-
ck_i = None
311-
for j in range(b, i + 1):
312-
rout = bb.emit(relax.call_dps_packed(
313-
f"pathc.bank_fwd_{j}", [rec_act, param, rec_act],
314-
[sAct, sState]))
315-
rec_act = bb.emit(relax.TupleGetItem(rout, 0))
316-
rec_ck = bb.emit(relax.TupleGetItem(rout, 1))
317-
ck_i = rec_ck
333+
if i not in recomputed_ckpt:
334+
_recompute_segment(b)
335+
ck_i = recomputed_ckpt.get(i)
318336
if ck_i is None:
319337
raise RuntimeError(
320338
"FAIL-LOUD: recompute produced no checkpoint for region "
321-
f"{i}")
339+
f"{i} (segment boundary {b})")
322340
out = bb.emit(relax.call_dps_packed(
323341
f"pathc.bank_bwd_{i}", [actg, param, ck_i, paramg],
324342
[sActG, sParamG]))

0 commit comments

Comments
 (0)