Differentiation of call expressions that takes array arguments give incorrect derivatives if the said array is modified between the
primal call expression and the derived call expression.
A reproducible example:
double some_fn(double i, double *arr, int n) {
return arr[0]*i;
}
double fn(double i, double *arr, int n) {
double res = 0;
arr[0] = 1;
res = some_fn(i, arr, n);
arr[0] = 5;
return res;
}
int main() {
double arr[5] = {1, 2, 3, 4, 5};
double d_arr[5] = {};
auto d_fn = clad::gradient(fn);
clad::array_ref<double> d_arr_ref(d_arr, 5);
double d_i = 0, d_n = 0;
d_fn.execute(3, arr, 5, &d_i, d_arr_ref, &d_n);
std::cout<<"d_i: "<<d_i<<"\n";
}
Expected result:
d_i: 1
Actual result
d_i: 5
Root cause of the error and solution
Derived call expression needs the same values of arguments that were passed to primal call expression. We do clone
all the arguments in the forward pass and use the cloned values in the reverse pass. This works well for ordinary non-pointer variables. But in C++, cloning pointers leads to shallow cloning -- the value pointed by the pointer isn't cloned. Therefore, if the array changes at any point between the primal call expression and the derived call expression, then even though derived call expression is using the cloned pointer, it will still use the modified array values. Therefore, we should deep clone pointers/arrays in the forward pass and use the cloned value in the derived call expression.
Deep cloning an array can be expensive and might be unnecessary if the array is not being modified anywhere between the primal call expression and derived call expression. Data flow and active variable analysis can help us to avoid creating unnecessary clone in those cases and generate optimal derived functions.
Differentiation of call expressions that takes array arguments give incorrect derivatives if the said array is modified between the
primal call expression and the derived call expression.
A reproducible example:
Expected result:
d_i: 1
Actual result
d_i: 5
Root cause of the error and solution
Derived call expression needs the same values of arguments that were passed to primal call expression. We do clone
all the arguments in the forward pass and use the cloned values in the reverse pass. This works well for ordinary non-pointer variables. But in C++, cloning pointers leads to shallow cloning -- the value pointed by the pointer isn't cloned. Therefore, if the array changes at any point between the primal call expression and the derived call expression, then even though derived call expression is using the cloned pointer, it will still use the modified array values. Therefore, we should deep clone pointers/arrays in the forward pass and use the cloned value in the derived call expression.
Deep cloning an array can be expensive and might be unnecessary if the array is not being modified anywhere between the primal call expression and derived call expression. Data flow and active variable analysis can help us to avoid creating unnecessary clone in those cases and generate optimal derived functions.