You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Kudos to @kovdan01 for initial analysis of this issue.
It turns out that adjoints for active values in loops are just plain wrong. Consider the reproducer. As one case see, the gradient for repeat_while_loop is wrong, while gradient for while_loop is correct. Even more, if we'd replace the code in loop by result *= x then repeat_while_loop case will start working.
Why it is so?
The loop body for repeat_while_loop looks like as follows (removed loop condition calculation for brevity):
The key thing here is active %13 (which is essentially a result value), so we need to generate adjoint for it. AutoDiff code uses notion of "adjoint for value X in basic block Y. This is fine for code without loops. And is just plain wrong for values inside loops as it should be "adjoint for value X in basic block Y on loop iteration Z. The values are different at different loop iterations. Thus their adjoints should be distinct as well. Without this we're ending with artificial adjoint accumulations (since single adjoint value is shared between loop iterations) and wrong results.
So, when generating pullback for this loop body we need to ensure that initial value for adjoint of %13 is zero on each iteration. And then perform the usual pullback cloning that involves adjoint value generation and accumulation. We don't do this, so essentially we're accumulating into adjoint from the previous loop iteration.
Sure, if things are so broken, why we have not noticed this before? I would say: coincidence.
For the code like result *= x we do not have these extra active values, Float.*= takes adjoint buffer as an inout argument and perform proper adjoint generation there.
while / for case is more interesting. Here the code looks like as follows:
So, we're having loop header (bb1) first, then loop body and finally the code after loop bb3. Now, we're having a code that propagates adjoints of active values into predecessor BBs while doing function traverse in reverse post-order. Here, we first visit bb3, then bb1. Inside bb1 we're realizing that there are active values (%25 and %28) in predecessor bb2, so we are taking their adjoints in bb1 and propagating them into bb2. Since no adjoints were defined before, they will be zero initialized and further propagated. And since coincidentally it is a loop header, we're ending into zero-initializing them in each loop iteration in a pullback as pullback to loop header will be executed after loop body. Everything magically works.
The situation with repeat loop is in reverse, there is no "loop header" BB in the common sense, there is a "loop footer" instead fused into loop body. So, the adjoints for %13 and %16 will be first zero-initialized in pullback block corresponding to bb3 and then further propagated to bb1. So, no zero-initialization on each loop iteration, adjoint values will be reused from previous loop iteration, and wrong results will be provided.
It seems to me that we need to perform explicit adjoint zeroing inside loop headers in pullback cloner
Reproduction
import _Differentiation
func repeat_while_loop(_ x:Float)->Float{varresult= x
vari=0
repeat {
result = result * x
i +=1} while i <2return result
}func while_loop(_ x:Float)->Float{varresult= x
vari=0
while i <2{
result = result * x
i +=1}return result
}print(valueWithGradient(at:2, of: repeat_while_loop))print(valueWithGradient(at:2, of: while_loop))
Expected behavior
Correct gradient calculation for both cases
Environment
Swift version 6.2-dev (LLVM e404f8897f17aff, Swift 5a68861)
Target: arm64-apple-macosx15.0
Additional information
No response
The text was updated successfully, but these errors were encountered:
asl
added
bug
A deviation from expected or documented behavior. Also: expected but undesirable behavior.
AutoDiff
labels
Dec 18, 2024
For each basic block identify corresponding innermost loop header
Then for each active value inside this loop (but not in nested loops), explicitly zero corresponding adjoint in the loop header
Given that in the pullback loop header will be executed after corresponding loop body, we will effectively zero out adjoints of active values after each iteration. Values "reused" from previous iterations are propagated via phi-nodes and therefore will be unaffected, only those defined in loop body will be affected.
kovdan01
added a commit
to kovdan01/swift
that referenced
this issue
Dec 18, 2024
Description
Kudos to @kovdan01 for initial analysis of this issue.
It turns out that adjoints for active values in loops are just plain wrong. Consider the reproducer. As one case see, the gradient for
repeat_while_loop
is wrong, while gradient forwhile_loop
is correct. Even more, if we'd replace the code in loop byresult *= x
thenrepeat_while_loop
case will start working.Why it is so?
The loop body for
repeat_while_loop
looks like as follows (removed loop condition calculation for brevity):The key thing here is active
%13
(which is essentially aresult
value), so we need to generate adjoint for it. AutoDiff code uses notion of "adjoint for value X in basic block Y. This is fine for code without loops. And is just plain wrong for values inside loops as it should be "adjoint for value X in basic block Y on loop iteration Z
. The values are different at different loop iterations. Thus their adjoints should be distinct as well. Without this we're ending with artificial adjoint accumulations (since single adjoint value is shared between loop iterations) and wrong results.So, when generating pullback for this loop body we need to ensure that initial value for adjoint of
%13
is zero on each iteration. And then perform the usual pullback cloning that involves adjoint value generation and accumulation. We don't do this, so essentially we're accumulating into adjoint from the previous loop iteration.Sure, if things are so broken, why we have not noticed this before? I would say: coincidence.
For the code like
result *= x
we do not have these extra active values,Float.*=
takes adjoint buffer as an inout argument and perform proper adjoint generation there.while / for
case is more interesting. Here the code looks like as follows:So, we're having loop header (
bb1
) first, then loop body and finally the code after loopbb3
. Now, we're having a code that propagates adjoints of active values into predecessor BBs while doing function traverse in reverse post-order. Here, we first visitbb3
, thenbb1
. Insidebb1
we're realizing that there are active values (%25
and%28
) in predecessorbb2
, so we are taking their adjoints inbb1
and propagating them intobb2
. Since no adjoints were defined before, they will be zero initialized and further propagated. And since coincidentally it is a loop header, we're ending into zero-initializing them in each loop iteration in a pullback as pullback to loop header will be executed after loop body. Everything magically works.The situation with repeat loop is in reverse, there is no "loop header" BB in the common sense, there is a "loop footer" instead fused into loop body. So, the adjoints for
%13
and%16
will be first zero-initialized in pullback block corresponding tobb3
and then further propagated tobb1
. So, no zero-initialization on each loop iteration, adjoint values will be reused from previous loop iteration, and wrong results will be provided.It seems to me that we need to perform explicit adjoint zeroing inside loop headers in pullback cloner
Reproduction
Expected behavior
Correct gradient calculation for both cases
Environment
Swift version 6.2-dev (LLVM e404f8897f17aff, Swift 5a68861)
Target: arm64-apple-macosx15.0
Additional information
No response
The text was updated successfully, but these errors were encountered: