Skip to content

Commit 7a09a65

Browse files
authored
Fix ref-return pullback lookup fallback (#1832)
1 parent 37b021a commit 7a09a65

4 files changed

Lines changed: 145 additions & 13 deletions

File tree

lib/Differentiator/DiffPlanner.cpp

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,7 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
11391139
return true;
11401140

11411141
bool nonDiff = false;
1142+
bool hasNoMemoryInputForPointerOrRefReturn = false;
11421143
// FIXME: We might want to support nested calls to differentiate/gradient
11431144
// inside differentiated functions.
11441145
if (!m_TopMostReq) {
@@ -1221,10 +1222,13 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
12211222
if (!(MD && MD->isInstance()) && !hasPointerOrRefReturn &&
12221223
allArgumentsAreLiterals(E->arguments(), m_ParentReq))
12231224
nonDiff = true;
1224-
// In the reverse mode, such functions don't have dfdx()
1225-
if (!utils::hasMemoryTypeParams(FD) && hasPointerOrRefReturn &&
1226-
m_TopMostReq->Mode == DiffMode::reverse)
1227-
nonDiff = true;
1225+
// In reverse mode, calls without memory parameters normally have no
1226+
// adjoint destination for a pointer or reference return. Defer the final
1227+
// decision for instance calls until custom derivatives are known because
1228+
// their implicit object can carry the adjoint.
1229+
hasNoMemoryInputForPointerOrRefReturn =
1230+
!utils::hasMemoryTypeParams(FD) && hasPointerOrRefReturn &&
1231+
m_TopMostReq->Mode == DiffMode::reverse;
12281232
// Skip reverse-mode scheduling for integral-return helper calls that
12291233
// cannot accumulate through memory arguments. Keep this narrow to avoid
12301234
// suppressing diagnostics on variadic/non-helper calls.
@@ -1371,7 +1375,36 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
13711375
E->getDirectCallee();
13721376
bool shouldUseRestoreTracker =
13731377
utils::shouldUseRestoreTracker(request.Function);
1374-
if (!(LookupCustomDerivativeDecl(request) || nonDiff) || requestTBR) {
1378+
bool hasCustomPullback = LookupCustomDerivativeDecl(request);
1379+
// Share one request between early classification and final scheduling.
1380+
DiffRequest forwPassRequest;
1381+
if (request.Mode == DiffMode::pullback) {
1382+
forwPassRequest.Function = request.Function;
1383+
forwPassRequest.BaseFunctionName = request.BaseFunctionName;
1384+
forwPassRequest.Mode = DiffMode::reverse_mode_forward_pass;
1385+
forwPassRequest.CallContext = request.CallContext;
1386+
forwPassRequest.UseRestoreTracker = shouldUseRestoreTracker;
1387+
}
1388+
1389+
if (hasNoMemoryInputForPointerOrRefReturn) {
1390+
const auto* calledMethod = dyn_cast<CXXMethodDecl>(FD);
1391+
bool isRefReturningInstanceCall =
1392+
request.Mode == DiffMode::pullback && calledMethod &&
1393+
calledMethod->isInstance() &&
1394+
utils::isNonConstReferenceType(request->getReturnType());
1395+
bool hasCustomReverseForw = false;
1396+
if (isRefReturningInstanceCall)
1397+
hasCustomReverseForw = LookupCustomDerivativeDecl(forwPassRequest);
1398+
1399+
// A reverse_forw still needs a matching pullback. Preserve the existing
1400+
// forward-only contract only when a custom reverse_forw has no custom
1401+
// pullback, as with smart-pointer and reference-wrapper helpers.
1402+
if (!isRefReturningInstanceCall ||
1403+
(hasCustomReverseForw && !hasCustomPullback))
1404+
nonDiff = true;
1405+
}
1406+
1407+
if (!(hasCustomPullback || nonDiff) || requestTBR) {
13751408
clang::CFG::BuildOptions Options;
13761409
std::unique_ptr<AnalysisDeclContext> AnalysisDC =
13771410
std::make_unique<AnalysisDeclContext>(
@@ -1445,14 +1478,10 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
14451478
}
14461479

14471480
if (request.Mode == DiffMode::pullback) {
1448-
DiffRequest forwPassRequest;
1449-
forwPassRequest.Function = request.Function;
1450-
forwPassRequest.BaseFunctionName = request.BaseFunctionName;
1451-
forwPassRequest.Mode = DiffMode::reverse_mode_forward_pass;
1452-
forwPassRequest.CallContext = request.CallContext;
1481+
// TBR can prove that no state needs restoring, so refresh this before
1482+
// scheduling the request.
14531483
forwPassRequest.UseRestoreTracker = shouldUseRestoreTracker;
14541484
QualType returnType = request->getReturnType();
1455-
bool hasCustomPullback = request.CustomDerivative != nullptr;
14561485
bool hasCustomReverseForw = LookupCustomDerivativeDecl(forwPassRequest);
14571486

14581487
if (hasCustomReverseForw ||

lib/Differentiator/ReverseModeVisitor.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,7 +2129,17 @@ Expr* ReverseModeVisitor::getStdInitListSizeExpr(const Expr* E) {
21292129
// derivatives even if there is no `dfdx()` and thus we should call the
21302130
// derived function. In the case of member functions, `implicit`
21312131
// this object is always passed by reference.
2132-
if (!nonDiff && !dfdx() && !utils::hasMemoryTypeParams(FD))
2132+
bool isRefReturningInstanceCall =
2133+
MD && MD->isInstance() &&
2134+
utils::isNonConstReferenceType(FD->getReturnType());
2135+
bool needsPullbackForRefReturningInstance =
2136+
isRefReturningInstanceCall && !elideReverseForw;
2137+
// A non-elidable reverse_forw for a reference-returning member function
2138+
// leaves the adjoint on the returned reference. Keep the call
2139+
// differentiable so its statically scheduled pullback can propagate it
2140+
// through the implicit object.
2141+
if (!nonDiff && !dfdx() && !utils::hasMemoryTypeParams(FD) &&
2142+
!needsPullbackForRefReturningInstance)
21332143
nonDiff = true;
21342144

21352145
// If all arguments are constant literals, then this does not contribute to
@@ -2188,7 +2198,12 @@ Expr* ReverseModeVisitor::getStdInitListSizeExpr(const Expr* E) {
21882198
}
21892199
}
21902200
pullbackFD = FindDerivedFunction(pullbackRequest);
2191-
if (pullbackFD && utils::hasEmptyBody(pullbackFD))
2201+
if (pullbackFD && utils::hasEmptyBody(pullbackFD) &&
2202+
!needsPullbackForRefReturningInstance)
2203+
nonDiff = true;
2204+
// Custom elidable reverse_forw helpers propagate their adjoint directly
2205+
// and intentionally have no pullback, for example smart pointers.
2206+
if (!pullbackFD && isRefReturningInstanceCall && elideReverseForw)
21922207
nonDiff = true;
21932208
}
21942209

test/Gradient/UserDefinedTypes.C

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,10 @@ double fn26(double x, double y) {
961961
// CHECK-NEXT: ptrClass p(&x);
962962
// CHECK-NEXT: ptrClass _d_p(_d_x);
963963
// CHECK-NEXT: clad::ValueAndAdjoint<double &, double &> _t0 = p.operator_star_reverse_forw(&_d_p);
964+
// CHECK-NEXT: {
964965
// CHECK-NEXT: _t0.adjoint += 1;
966+
// CHECK-NEXT: p.operator_star_pullback(&_d_p);
967+
// CHECK-NEXT: }
965968
// CHECK-NEXT: }
966969

967970
struct MyStructWrapper {
@@ -1264,6 +1267,7 @@ double fn34(double x, double y) {
12641267
// CHECK-NEXT: clad::ValueAndAdjoint<double &, double &> _t1 = obj_x.conversion_operator_reverse_forw(clad::Tag<double &>(), &_d_obj_x);
12651268
// CHECK-NEXT: {
12661269
// CHECK-NEXT: _t1.adjoint += 1;
1270+
// CHECK-NEXT: obj_x.conversion_operator_pullback(&_d_obj_x);
12671271
// CHECK-NEXT: _d_conv.val += 1;
12681272
// CHECK-NEXT: }
12691273
// CHECK-NEXT: obj_y.conversion_operator_pullback(_d_conv, &_d_obj_y);

test/Regressions/issue-1827.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// RUN: %cladclang %s -I%S/../../include -o %t 2>&1 | %filecheck %s
2+
// RUN: %t | %filecheck_exec %s
3+
4+
#include "clad/Differentiator/Differentiator.h"
5+
6+
#include <cstdio>
7+
8+
struct Ref {
9+
double* p;
10+
Ref(double& x) : p(&x) {}
11+
operator double&() const { return *p; }
12+
};
13+
14+
struct IndexedRef {
15+
double* p;
16+
double& at(int i) const { return p[i]; }
17+
};
18+
19+
int customPullbackCalls = 0;
20+
21+
namespace clad {
22+
namespace custom_derivatives {
23+
namespace class_functions {
24+
clad::ValueAndAdjoint<double&, double&>
25+
at_reverse_forw(const ::IndexedRef* x, int i, const ::IndexedRef* d_x,
26+
int /*d_i*/) {
27+
return {x->at(i), d_x->at(i)};
28+
}
29+
30+
void at_pullback(const ::IndexedRef* /*x*/, int /*i*/, ::IndexedRef* /*d_x*/,
31+
int* /*d_i*/) {
32+
++customPullbackCalls;
33+
}
34+
} // namespace class_functions
35+
} // namespace custom_derivatives
36+
} // namespace clad
37+
38+
template <class T> double product(const T* x) {
39+
return x[0] * x[1] * x[2] * x[3];
40+
}
41+
42+
double custom_ref_case(double* params) {
43+
Ref x[]{params[3], params[2], params[1], params[0]};
44+
return product(x);
45+
}
46+
47+
double custom_derivative_case(double* params) {
48+
IndexedRef x{params};
49+
return x.at(1);
50+
}
51+
52+
int main() {
53+
double params[]{1, 2, 3, 4};
54+
double dParams[4]{};
55+
auto grad = clad::gradient(custom_ref_case);
56+
grad.execute(params, dParams);
57+
std::printf("{%.0f, %.0f, %.0f, %.0f}\n", dParams[0], dParams[1],
58+
dParams[2], dParams[3]);
59+
60+
double dCustomParams[4]{};
61+
auto customGrad = clad::gradient(custom_derivative_case);
62+
customGrad.execute(params, dCustomParams);
63+
std::printf("{%.0f, %.0f, %.0f, %.0f}\n", dCustomParams[0],
64+
dCustomParams[1], dCustomParams[2], dCustomParams[3]);
65+
std::printf("%d\n", customPullbackCalls);
66+
}
67+
68+
// CHECK: void product_pullback(const Ref *x, double _d_y, Ref *_d_x) {
69+
// CHECK: clad::ValueAndAdjoint<double &, double &> _t3 = x[0].conversion_operator_reverse_forw(clad::Tag<double &>(), &_d_x[0]);
70+
// CHECK: clad::ValueAndAdjoint<double &, double &> _t4 = x[1].conversion_operator_reverse_forw(clad::Tag<double &>(), &_d_x[1]);
71+
// CHECK: clad::ValueAndAdjoint<double &, double &> _t5 = x[2].conversion_operator_reverse_forw(clad::Tag<double &>(), &_d_x[2]);
72+
// CHECK: clad::ValueAndAdjoint<double &, double &> _t6 = x[3].conversion_operator_reverse_forw(clad::Tag<double &>(), &_d_x[3]);
73+
// CHECK: x[0].conversion_operator_pullback(&_d_x[0]);
74+
// CHECK: x[1].conversion_operator_pullback(&_d_x[1]);
75+
// CHECK: x[2].conversion_operator_pullback(&_d_x[2]);
76+
// CHECK: x[3].conversion_operator_pullback(&_d_x[3]);
77+
78+
// CHECK: void custom_derivative_case_grad(double *params, double *_d_params) {
79+
// CHECK: clad::ValueAndAdjoint<double &, double &> _t0 = clad::custom_derivatives::class_functions::at_reverse_forw(&x, 1, &_d_x, 0);
80+
// CHECK: clad::custom_derivatives::class_functions::at_pullback(&x, 1, &_d_x, &_r0);
81+
82+
// CHECK-EXEC: {24, 12, 8, 6}
83+
// CHECK-EXEC-NEXT: {0, 1, 0, 0}
84+
// CHECK-EXEC-NEXT: 1

0 commit comments

Comments
 (0)