Skip to content

Commit

Permalink
Reimplement jacobians using the vectorized forward mode.
Browse files Browse the repository at this point in the history
 Previously, jacobians were based on the non-vectorized reverse mode, which was mostly incapable of capturing multiple outputs. The implementation worked in a few particular cases. For example, it was not possible to differentiate function calls or declare variables inside the original function body.
 This PR implements jacobians using the vectorized forward mode. At the very least, this will solve the issues described above and give a way forward to solve other ones. This also means introducing features to the vectorized fwd mode will introduce the same features to jacobians.
 Let's take a look at the new signature of jacobians. Since the function to be differentiated is expected to have multiple outputs, we should expect the output in the form of array/pointer/reference parameters (just like before). And for every output parameter, we should generate a corresponding adjoint parameter for the user to acquire the results. Since there is no way to specify which parameters are used as output and which are not, adjoints are generated for all array/pointer/reference parameters. For example:

```
void f(double a, double b, double* c)
 -->
void f_jac(double a, double b, double* c, <matrix<double>* _d_c)
```

or

```
void f(double a, double b, double* c, double[7] t)
 -->
void f_jac(double a, double b, double* c, double[7] t,
 array_ref<matrix<double>> _d_c, matrix<double>* _d_t)
```

This signature is also similar to the old one. e.g.
```
df.execute(a, b, c, result); // old behavior
df.execute(a, b, c, &result); // new behavior
```
However, the behavior differs for multiple output parameters, which the old jacobians did not support.

 Note: the same functionality can be achieved by using the vectorized reverse mode, which should probably be implemented at some point. However, the old code for jacobians is unlikely to be useful for that, and there is not much point in keeping it.

Fixes vgvassilev#472, Fixes vgvassilev#1057, Fixes vgvassilev#480, Fixes vgvassilev#527
  • Loading branch information
PetroZarytskyi committed Nov 19, 2024
1 parent a7de6af commit 6a18cb9
Show file tree
Hide file tree
Showing 27 changed files with 1,016 additions and 1,051 deletions.
60 changes: 35 additions & 25 deletions include/clad/Differentiator/BuiltinDerivatives.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,31 +199,31 @@ CUDA_HOST_DEVICE inline void __builtin_powf_pullback(float x, float exponent,
// FIXME: Add the rest of the __builtin_ routines for log, sqrt, abs, etc.

namespace std {
template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> abs_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> abs_pushforward(T x, dT d_x) {
if (x >= 0)
return {x, d_x};
else
return {-x, -d_x};
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> exp_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> exp_pushforward(T x, dT d_x) {
return {::std::exp(x), ::std::exp(x) * d_x};
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> sin_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> sin_pushforward(T x, dT d_x) {
return {::std::sin(x), ::std::cos(x) * d_x};
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> cos_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> cos_pushforward(T x, dT d_x) {
return {::std::cos(x), (-1) * ::std::sin(x) * d_x};
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> sqrt_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> sqrt_pushforward(T x, dT d_x) {
return {::std::sqrt(x), (((T)1) / (((T)2) * ::std::sqrt(x))) * d_x};
}

Expand All @@ -232,9 +232,9 @@ CUDA_HOST_DEVICE ValueAndPushforward<T, T> floor_pushforward(T x, T /*d_x*/) {
return {::std::floor(x), (T)0};
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> atan2_pushforward(T y, T x, T d_y,
T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> atan2_pushforward(T y, T x, dT d_y,
dT d_x) {
return {::std::atan2(y, x),
-(y / ((x * x) + (y * y))) * d_x + x / ((x * x) + (y * y)) * d_y};
}
Expand All @@ -246,8 +246,8 @@ CUDA_HOST_DEVICE void atan2_pullback(T y, T x, U d_z, T* d_y, T* d_x) {
*d_x += -(y / ((x * x) + (y * y))) * d_z;
}

template <typename T>
CUDA_HOST_DEVICE ValueAndPushforward<T, T> acos_pushforward(T x, T d_x) {
template <typename T, typename dT>
CUDA_HOST_DEVICE ValueAndPushforward<T, dT> acos_pushforward(T x, dT d_x) {
return {::std::acos(x), ((-1) / (::std::sqrt(1 - x * x))) * d_x};
}

Expand All @@ -263,17 +263,27 @@ ValueAndPushforward<float, float> sqrtf_pushforward(float x, float d_x) {

#endif

template <typename T1, typename T2>
CUDA_HOST_DEVICE ValueAndPushforward<decltype(::std::pow(T1(), T2())),
decltype(::std::pow(T1(), T2()))>
pow_pushforward(T1 x, T2 exponent, T1 d_x, T2 d_exponent) {
auto val = ::std::pow(x, exponent);
auto derivative = (exponent * ::std::pow(x, exponent - 1)) * d_x;
template <typename T, typename dT> struct AdjOutType {
using type = T;
};

template <typename T, typename dT> struct AdjOutType<T, clad::array<dT>> {
using type = clad::array<T>;
};

template <typename T1, typename T2, typename dT1, typename dT2,
typename T_out = decltype(::std::pow(T1(), T2())),
typename dT_out = typename AdjOutType<T_out, dT1>::type>
CUDA_HOST_DEVICE ValueAndPushforward<T_out, dT_out>
pow_pushforward(T1 x, T2 exponent, dT1 d_x, dT2 d_exponent) {
T_out val = ::std::pow(x, exponent);
dT_out derivative = (exponent * ::std::pow(x, exponent - 1)) * d_x;
// Only add directional derivative of base^exp w.r.t exp if the directional
// seed d_exponent is non-zero. This is required because if base is less than or
// equal to 0, then log(base) is undefined, and therefore if user only requested
// directional derivative of base^exp w.r.t base -- which is valid --, the result would
// be undefined because as per C++ valid number + NaN * 0 = NaN.
// seed d_exponent is non-zero. This is required because if base is less than
// or equal to 0, then log(base) is undefined, and therefore if user only
// requested directional derivative of base^exp w.r.t base -- which is valid
// --, the result would be undefined because as per C++ valid number + NaN * 0
// = NaN.
if (d_exponent)
derivative += (::std::pow(x, exponent) * ::std::log(x)) * d_exponent;
return {val, derivative};
Expand Down
14 changes: 8 additions & 6 deletions include/clad/Differentiator/Differentiator.h
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,13 @@ CUDA_HOST_DEVICE T push(tape<T>& to, ArgsT... val) {
typename F, typename DerivedFnType = JacobianDerivedFnTraits_t<F>,
typename = typename std::enable_if<
!std::is_class<remove_reference_and_pointer_t<F>>::value>::type>
constexpr CladFunction<
DerivedFnType, ExtractFunctorTraits_t<F>> __attribute__((annotate("J")))
constexpr CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>,
/*EnablePadding=*/true> __attribute__((annotate("J")))
jacobian(F f, ArgSpec args = "",
DerivedFnType derivedFn = static_cast<DerivedFnType>(nullptr),
const char* code = "") {
return CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>>(
return CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>,
/*EnablePadding=*/true>(
derivedFn /* will be replaced by Jacobian*/, code);
}

Expand All @@ -640,12 +641,13 @@ CUDA_HOST_DEVICE T push(tape<T>& to, ArgsT... val) {
typename F, typename DerivedFnType = JacobianDerivedFnTraits_t<F>,
typename = typename std::enable_if<
std::is_class<remove_reference_and_pointer_t<F>>::value>::type>
constexpr CladFunction<
DerivedFnType, ExtractFunctorTraits_t<F>> __attribute__((annotate("J")))
constexpr CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>,
/*EnablePadding=*/true> __attribute__((annotate("J")))
jacobian(F&& f, ArgSpec args = "",
DerivedFnType derivedFn = static_cast<DerivedFnType>(nullptr),
const char* code = "") {
return CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>>(
return CladFunction<DerivedFnType, ExtractFunctorTraits_t<F>,
/*EnablePadding=*/true>(
derivedFn /* will be replaced by Jacobian*/, code, f);
}

Expand Down
57 changes: 24 additions & 33 deletions include/clad/Differentiator/FunctionTraits.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define FUNCTION_TRAITS

#include "clad/Differentiator/ArrayRef.h"
#include "clad/Differentiator/Matrix.h"

#include <type_traits>

Expand Down Expand Up @@ -548,15 +549,25 @@ namespace clad {
using type = NoFunction*;
};

template <class... Args> struct SelectLast;
// OutputVecParamType is used to deduce the type of derivative arguments
// for vector forward mode.
template <class T, class R> struct OutputVecParamType {
using type = array_ref<typename std::remove_pointer<R>::type>;
};

template <class... Args>
using SelectLast_t = typename SelectLast<Args...>::type;
template <class T, class R>
using OutputVecParamType_t = typename OutputVecParamType<T, R>::type;

/// Specialization for vector forward mode type.
template <class F, class = void> struct ExtractDerivedFnTraitsVecForwMode {};

template <class T> struct SelectLast<T> { using type = T; };
template <class F>
using ExtractDerivedFnTraitsVecForwMode_t =
typename ExtractDerivedFnTraitsVecForwMode<F>::type;

template <class T, class... Args> struct SelectLast<T, Args...> {
using type = typename SelectLast<Args...>::type;
template <class ReturnType, class... Args>
struct ExtractDerivedFnTraitsVecForwMode<ReturnType (*)(Args...)> {
using type = void (*)(Args..., OutputVecParamType_t<Args, void>...);
};

template <class T, class = void> struct JacobianDerivedFnTraits {};
Expand All @@ -569,7 +580,7 @@ namespace clad {
// JacobianDerivedFnTraits specializations for pure function pointer types
template <class ReturnType, class... Args>
struct JacobianDerivedFnTraits<ReturnType (*)(Args...)> {
using type = void (*)(Args..., SelectLast_t<Args...>);
using type = void (*)(Args..., OutputParamType_t<Args, void>...);
};

/// These macro expansions are used to cover all possible cases of
Expand All @@ -581,11 +592,12 @@ namespace clad {
/// qualifier and reference respectively. The AddNOEX adds cases for noexcept
/// qualifier only if it is supported and finally AddSPECS declares the
/// function with all the cases
#define JacobianDerivedFnTraits_AddSPECS(var, cv, vol, ref, noex) \
template <typename R, typename C, typename... Args> \
struct JacobianDerivedFnTraits<R (C::*)(Args...) cv vol ref noex> { \
using type = void (C::*)(Args..., SelectLast_t<Args...>) cv vol ref noex; \
};
#define JacobianDerivedFnTraits_AddSPECS(var, cv, vol, ref, noex) \
template <typename R, typename C, typename... Args> \
struct JacobianDerivedFnTraits<R (C::*)(Args...) cv vol ref noex> { \
using type = void (C::*)( \
Args..., OutputParamType_t<Args, void>...) cv vol ref noex; \
};

#if __cpp_noexcept_function_type > 0
#define JacobianDerivedFnTraits_AddNOEX(var, con, vol, ref) \
Expand Down Expand Up @@ -739,27 +751,6 @@ namespace clad {
using ExtractDerivedFnTraitsForwMode_t =
typename ExtractDerivedFnTraitsForwMode<F>::type;

// OutputVecParamType is used to deduce the type of derivative arguments
// for vector forward mode.
template <class T, class R> struct OutputVecParamType {
using type = array_ref<typename std::remove_pointer<R>::type>;
};

template <class T, class R>
using OutputVecParamType_t = typename OutputVecParamType<T, R>::type;

/// Specialization for vector forward mode type.
template <class F, class = void> struct ExtractDerivedFnTraitsVecForwMode {};

template <class F>
using ExtractDerivedFnTraitsVecForwMode_t =
typename ExtractDerivedFnTraitsVecForwMode<F>::type;

template <class ReturnType, class... Args>
struct ExtractDerivedFnTraitsVecForwMode<ReturnType (*)(Args...)> {
using type = void (*)(Args..., OutputVecParamType_t<Args, void>...);
};

/// Specialization for free function pointer type
template <class F>
struct ExtractDerivedFnTraitsForwMode<
Expand Down
4 changes: 0 additions & 4 deletions include/clad/Differentiator/ReverseModeVisitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,6 @@ namespace clad {
"attempt to differentiate unsupported operator, ignored.",
args);
}
/// Builds an overload for the gradient function that has derived params for
/// all the arguments of the requested function and it calls the original
/// gradient function internally
clang::FunctionDecl* CreateGradientOverload();

/// Returns the type that should be used to represent the derivative of a
/// variable of type `yType` with respect to a parameter variable of type
Expand Down
2 changes: 1 addition & 1 deletion include/clad/Differentiator/VectorForwardModeVisitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace clad {
/// A visitor for processing the function code in vector forward mode.
/// Used to compute derivatives by clad::vector_forward_differentiate.
class VectorForwardModeVisitor : public BaseForwardModeVisitor {
private:
protected:
llvm::SmallVector<const clang::ValueDecl*, 16> m_IndependentVars;
/// Map used to keep track of parameter variables w.r.t which the
/// the derivative is being computed. This is separate from the
Expand Down
6 changes: 6 additions & 0 deletions include/clad/Differentiator/VisitorBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,12 @@ namespace clad {
/// Returns type clad::ConstructorReverseForwTag<T>
clang::QualType GetCladConstructorReverseForwTagOfType(clang::QualType T);

/// Builds an overload for the derivative function that has derived params
/// for all the arguments of the requested function and it calls the
/// original derivative function internally. Used in gradient and jacobian
/// modes.
clang::FunctionDecl* CreateDerivativeOverload();

public:
/// Rebuild a sequence of nested namespaces ending with DC.
clang::NamespaceDecl* RebuildEnclosingNamespaces(clang::DeclContext* DC);
Expand Down
15 changes: 13 additions & 2 deletions lib/Differentiator/BaseForwardModeVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,16 @@ StmtDiff BaseForwardModeVisitor::VisitCallExpr(const CallExpr* CE) {
callDiff = m_Builder.BuildCallToCustomDerivativeOrNumericalDiff(
customPushforward, customDerivativeArgs, getCurrentScope(),
const_cast<DeclContext*>(FD->getDeclContext()));

// Custom derivative templates can be written in a
// general way that works for both vectorized and non-vectorized
// modes. We have to also look for the pushforward with the regular name.
if (!callDiff && m_DiffReq.Mode != DiffMode::forward) {
customPushforward =
clad::utils::ComputeEffectiveFnName(FD) + "_pushforward";
callDiff = m_Builder.BuildCallToCustomDerivativeOrNumericalDiff(
customPushforward, customDerivativeArgs, getCurrentScope(),
const_cast<DeclContext*>(FD->getDeclContext()));
}
if (!isLambda) {
// Check if it is a recursive call.
if (!callDiff && (FD == m_DiffReq.Function) &&
Expand Down Expand Up @@ -1446,7 +1455,9 @@ BaseForwardModeVisitor::VisitBinaryOperator(const BinaryOperator* BinOp) {
derivedR = BuildParens(derivedR);
opDiff = BuildOp(opCode, derivedL, derivedR);
} else if (BinOp->isAssignmentOp()) {
if (Ldiff.getExpr_dx()->isModifiableLvalue(m_Context) != Expr::MLV_Valid) {
if ((Ldiff.getExpr_dx()->isModifiableLvalue(m_Context) !=
Expr::MLV_Valid) &&
!isCladArrayType(Ldiff.getExpr_dx()->getType())) {
diag(DiagnosticsEngine::Warning, BinOp->getEndLoc(),
"derivative of an assignment attempts to assign to unassignable "
"expr, assignment ignored");
Expand Down
1 change: 1 addition & 0 deletions lib/Differentiator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ llvm_add_library(cladDifferentiator
DiffPlanner.cpp
ErrorEstimator.cpp
EstimationModel.cpp
JacobianModeVisitor.cpp
HessianModeVisitor.cpp
MultiplexExternalRMVSource.cpp
PushForwardModeVisitor.cpp
Expand Down
5 changes: 3 additions & 2 deletions lib/Differentiator/DerivativeBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "clad/Differentiator/DerivativeBuilder.h"

#include "JacobianModeVisitor.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/TemplateBase.h"
#include "clang/Sema/Lookup.h"
Expand Down Expand Up @@ -432,8 +433,8 @@ static void registerDerivative(FunctionDecl* derivedFD, Sema& semaRef) {
HessianModeVisitor H(*this, request);
result = H.Derive();
} else if (request.Mode == DiffMode::jacobian) {
ReverseModeVisitor R(*this, request);
result = R.Derive();
JacobianModeVisitor J(*this, request);
result = J.DeriveJacobian();
} else if (request.Mode == DiffMode::error_estimation) {
ReverseModeVisitor R(*this, request);
InitErrorEstimation(m_ErrorEstHandler, m_EstModel, *this, request);
Expand Down
9 changes: 1 addition & 8 deletions lib/Differentiator/DiffPlanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -579,21 +579,14 @@ namespace clad {

// If the function has no parameters, then we cannot differentiate it."
// and if the DiffMode is Jacobian, we must have atleast 2 parameters.
if (params.empty() || (params.size()==1 && this->Mode == DiffMode::jacobian)) {
if (params.empty()) {
utils::EmitDiag(semaRef, DiagnosticsEngine::Error,
CallContext->getEndLoc(),
"Attempted to differentiate a function without "
"parameters");
return;
}

// If it is a Vector valued function, the last parameter is to store the
// output vector and hence is not a differentiable parameter, so we must
// pop it out
if (this->Mode == DiffMode::jacobian){
params.pop_back();
}

// insert an empty index for each parameter.
for (unsigned i=0; i<params.size(); ++i) {
DiffInputVarInfo dVarInfo(params[i], IndexInterval());
Expand Down
Loading

0 comments on commit 6a18cb9

Please sign in to comment.