@@ -87,6 +87,10 @@ class EliminateRecomputableAdStackPushes {
8787 stacks.push_back (s->as <AdStackAllocaStmt>());
8888 }
8989
90+ // SNodes written in the IB; gates `GlobalLoadStmt` chain leaves in `is_recomputable`. The pass body only erases
91+ // adstack stmts, so the write-stmt set is invariant across one outer iteration and we can recompute it per pass.
92+ auto mutated_snodes = RecomputableChainAnalyzer::collect_mutated_snodes (ib);
93+
9094 bool modified = false ;
9195 std::unordered_map<Stmt *, bool > recomputable_cache;
9296
@@ -158,7 +162,7 @@ class EliminateRecomputableAdStackPushes {
158162 }
159163
160164 Stmt *pushed_val = body_push->v ;
161- if (!RecomputableChainAnalyzer::is_recomputable (pushed_val, recomputable_cache)) {
165+ if (!RecomputableChainAnalyzer::is_recomputable (pushed_val, recomputable_cache, mutated_snodes )) {
162166 continue ;
163167 }
164168 // Loop-carried self-reference: when the pushed value's transitive operand DAG includes an AdStackLoadTopStmt of
@@ -238,80 +242,42 @@ class EliminateRecomputableAdStackPushes {
238242 continue ;
239243 }
240244
241- // Reverse-position correctness for chain leaves pushed in the same block as S.
242- //
243- // After elimination, every reverse stmt that consumed `load_top(S)` instead consumes the chain (cloned by
244- // `BackupSSA::generic_visit` at the consumer's reverse position). The cloned chain reads `load_top(T)` for each
245- // chain leaf T, where the read happens at the consumer's reverse cursor.
246- //
247- // `MakeAdjoint` visits forward stmts in REVERSE order, emitting at a cursor that advances per visit. For a
248- // forward stmt at position P, its reverse emissions land "later" in reverse block iff P is smaller. T's pop is
249- // emitted when `MakeAdjoint` visits T's body push at position P_T. The cloned chain at the consumer's reverse
250- // position reads `load_top(T)` POST-pop iff T's pop has fired by then, i.e. iff visit(P_T) precedes visit(C_pos),
251- // i.e. iff P_T > C_pos for every consumer C of S's load_tops.
245+ // Reverse-position correctness for chain leaves.
252246 //
253- // Forward chain evaluates at S's push position P_S, before any of T's same-block body pushes (since we already
254- // required P_T > P_S via the dominance check above for S's loads). So forward chain reads T's pre-iter-k-push
255- // value. Reverse clone post-pop also returns T's pre-iter-k-push value (= iter k INPUT for loop-carried T).
256- // Match.
247+ // Each chain leaf `AdStackLoadTopStmt(T)` is re-cloned by `BackupSSA::generic_visit` at the consumer's reverse
248+ // position; the cloned `load_top(T)` reads `T`'s top at that reverse cursor, which must equal what the original
249+ // load_top read in forward iter k. `MakeAdjoint` emits `T`'s pop for each forward push at the reverse cursor
250+ // mirroring that push's forward position, so by consumer-reverse only the pops for `T`-pushes with forward
251+ // position > P_cons have fired. The reverse top therefore equals `T`'s last forward push at-or-before P_cons,
252+ // while the forward chain at the load_top's own forward position P_lt reads `T`'s last forward push
253+ // at-or-before P_lt. They match iff no `T`-push falls strictly between P_lt and P_cons in document order.
257254 //
258- // Without this check, the canonical Fibonacci-style shape (`p, q = q, p + q; b[q] += a[q]`) where S stores `p +
259- // q` and chain leaves p_stack / q_stack are pushed AFTER S but BEFORE the GlobalPtr index consumer would silently
260- // corrupt gradients: the reverse clone at the GlobalPtr's adjoint emission reads p_stack and q_stack PRE-pop
261- // (iter k POST-push values), summing to a different value than the forward chain at P_S used.
262- //
263- // Chain leaves T whose stacks have NO body push in S's containing block are stable within the iter (their tops do
264- // not change during the iter), so neither forward nor reverse evaluation order shifts their values. Excluded from
265- // this check.
266- auto find_consumers_of_load_tops = [&](std::vector<int > &out_positions) {
267- std::function<void (Block *, int )> walk = [&](Block *b, int container_pos_in_push_block) {
268- for (size_t i = 0 ; i < b->statements .size (); i++) {
269- Stmt *s = b->statements [i].get ();
270- int effective_pos = (b == push_block) ? static_cast <int >(i) : container_pos_in_push_block;
271- for (auto *op : s->get_operands ()) {
272- if (op == nullptr )
273- continue ;
274- for (auto *lt : load_tops) {
275- if (op == lt) {
276- out_positions.push_back (effective_pos);
277- break ;
278- }
279- }
280- }
281- if (auto *if_s = s->cast <IfStmt>()) {
282- if (if_s->true_statements )
283- walk (if_s->true_statements .get (), effective_pos);
284- if (if_s->false_statements )
285- walk (if_s->false_statements .get (), effective_pos);
286- } else if (auto *rf = s->cast <RangeForStmt>()) {
287- if (rf->body )
288- walk (rf->body .get (), effective_pos);
289- } else if (auto *sf = s->cast <StructForStmt>()) {
290- if (sf->body )
291- walk (sf->body .get (), effective_pos);
292- }
293- }
294- };
295- walk (push_block, -1 );
296- };
297- std::vector<int > consumer_positions;
298- find_consumers_of_load_tops (consumer_positions);
299- int max_consumer_pos = -1 ;
300- for (int p : consumer_positions) {
301- if (p > max_consumer_pos)
302- max_consumer_pos = p;
255+ // A single preorder index over the IB lets the check compare positions across nested blocks in one integer
256+ // compare; pushes inside `IfStmt`, `RangeForStmt`, and `StructForStmt` bodies are reached via the visitor walk
257+ // and slot in between the container's preorder and the next sibling's preorder.
258+ std::unordered_map<Stmt *, int > preorder;
259+ {
260+ auto all = irpass::analysis::gather_statements (ib, [](Stmt *) { return true ; });
261+ preorder.reserve (all.size ());
262+ for (size_t i = 0 ; i < all.size (); i++)
263+ preorder[all[i]] = static_cast <int >(i);
303264 }
265+ auto preorder_of = [&](Stmt *s) -> int {
266+ auto it = preorder.find (s);
267+ return it == preorder.end () ? -1 : it->second ;
268+ };
304269
305- // Collect all distinct AdStackAllocaStmt referenced via AdStackLoadTopStmt leaves in pushed_val's chain.
306- std::unordered_set<AdStackAllocaStmt *> chain_leaf_stacks;
270+ // Collect every `AdStackLoadTopStmt` reachable from `pushed_val`'s recomputable chain. Each one has its own
271+ // forward position P_lt - the position at which the cloned chain in reverse will read its target stack's top
272+ // for the corresponding forward iter - so the check is per-load_top, not per-stack.
273+ std::vector<AdStackLoadTopStmt *> chain_load_tops;
307274 std::unordered_set<Stmt *> walked_for_leaves;
308275 std::function<void (Stmt *)> collect_leaves = [&](Stmt *s) {
309276 if (walked_for_leaves.count (s))
310277 return ;
311278 walked_for_leaves.insert (s);
312279 if (auto *lt = s->cast <AdStackLoadTopStmt>()) {
313- if (auto *as = lt->stack ->cast <AdStackAllocaStmt>())
314- chain_leaf_stacks.insert (as);
280+ chain_load_tops.push_back (lt);
315281 return ;
316282 }
317283 if (s->is <AdStackAllocaStmt>() || s->is <ArgLoadStmt>() || s->is <ConstStmt>())
@@ -323,20 +289,40 @@ class EliminateRecomputableAdStackPushes {
323289 };
324290 collect_leaves (pushed_val);
325291
292+ int max_consumer_preorder = -1 ;
293+ auto consumer_users = irpass::analysis::gather_statements (ib, [&](Stmt *s) {
294+ for (auto *op : s->get_operands ()) {
295+ for (auto *lt : load_tops) {
296+ if (op == lt)
297+ return true ;
298+ }
299+ }
300+ return false ;
301+ });
302+ for (auto *u : consumer_users) {
303+ int p = preorder_of (u);
304+ if (p > max_consumer_preorder)
305+ max_consumer_preorder = p;
306+ }
307+
326308 bool reverse_safe = true ;
327- for (auto *T : chain_leaf_stacks) {
328- // Find T's body pushes (non-init) in push_block. Only same-block pushes can interfere: pushes in outer blocks
329- // happen once per outer iteration, so their tops are stable across the inner iter.
309+ for (auto *lt : chain_load_tops) {
310+ auto *T = lt->stack ? lt->stack ->cast <AdStackAllocaStmt>() : nullptr ;
311+ if (T == nullptr ) {
312+ reverse_safe = false ;
313+ break ;
314+ }
315+ int lt_pre = preorder_of (lt);
330316 AdStackPushStmt *T_init = find_init_push (T);
331- for ( size_t i = 0 ; i < push_block-> statements . size (); i++ ) {
332- Stmt *s = push_block-> statements [i]. get ();
333- if ( auto *p = s-> cast <AdStackPushStmt>()) {
334- if (p-> stack == T && p != T_init) {
335- if ( static_cast < int >(i) <= max_consumer_pos ) {
336- reverse_safe = false ;
337- break ;
338- }
339- }
317+ auto T_pushes = irpass::analysis::gather_statements (ib, [&](Stmt *s ) {
318+ auto *p = s-> cast <AdStackPushStmt> ();
319+ return p != nullptr && p-> stack == T && p != T_init;
320+ });
321+ for ( auto *p : T_pushes ) {
322+ int p_pre = preorder_of (p) ;
323+ if (p_pre > lt_pre && p_pre <= max_consumer_preorder) {
324+ reverse_safe = false ;
325+ break ;
340326 }
341327 }
342328 if (!reverse_safe)
0 commit comments