CoDiPack  3.1.0
A Code Differentiation Package
SciComp TU Kaiserslautern
Loading...
Searching...
No Matches
Tutorial 7 - Aggregated type implementation

Goal: Learn how to add your own aggregated types to CoDiPack.

Prerequisite: Tutorial 2 - Reverse mode AD

Function: Norm of two scaled vector.

template<typename VectorType>
ActiveNumber func(ActiveNumber const& s1, VectorType const& v1, ActiveNumber const& s2, VectorType const& v2) {
return (s1 * v1 + s2 * v2).norm();
}

Full code:

// Declare all types without the CoDiPack active type.
using Number = double;
using Vector4 = Eigen::Matrix<Number, 4, 1>;
using Vector4Transpose = Eigen::Matrix<Number, 1, 4>;
// Declare all types with the CoDiPack active type.
using ActiveNumber = codi::RealReverse;
// Start with the implementation of aggregated types.
// 1. Specialize the necessary traits.
template<>
: public codi::RealTraits::ArrayAggregatedTypeTraitsBase<Vector4, Number, Vector4, 4> {};
namespace codi {
CODI_CREATE_TRANSPOSE(Vector4, Vector4Transpose, jacobian.transpose());
}
// 2. Implement CoDiPack expression operations for the vector type.
template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct ScalarVectorMultiplicationOperation : public codi::BinaryJacobianOperation<Vector4, ScalarVectorMultiplicationOperation<T>> {
static CODI_INLINE Vector4 primal(Number const& s, Vector4 const& v) {
return s * v;
}
static CODI_INLINE Vector4 gradientA(Number const& s, Vector4 const& v, Vector4 const& result) {
return v;
}
static CODI_INLINE Number gradientB(Number const& s, Vector4 const& v, Vector4 const& result) {
return s;
}
};
// Define scalar * vector overload.
template<typename ArgS, typename ArgV>
}
template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct VectorAdditionOperation : public codi::BinaryJacobianOperation<Vector4, VectorAdditionOperation<T>> {
static CODI_INLINE Vector4 primal(Vector4 const& v1, Vector4 const& v2) {
return v1 + v2;
}
static CODI_INLINE Number gradientA(Vector4 const& v1, Vector4 const& v2, Vector4 const& result) {
return 1.0;
}
static CODI_INLINE Number gradientB(Vector4 const& v1, Vector4 const& v2, Vector4 const& result) {
return 1.0;
}
};
// Define vector + vector overload.
template<typename ArgV1, typename ArgV2>
}
// 3. Add member CoDiPack expression operations on the vector type.
// Operation for vector.norm().
template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct VectorNormOperation : public codi::UnaryJacobianOperation<Number, VectorNormOperation<T>> {
static CODI_INLINE Number primal(Vector4 const& v) {
return v.norm();
}
static CODI_INLINE Vector4 gradient(Vector4 const& v, Number const& result) {
return v / result;
}
};
template<typename Impl>
public:
using Real = Vector4;
auto norm() const {
return codi::ComputeExpression<Number, VectorNormOperation, Impl>(this->cast());
}
private:
CODI_INLINE Impl const& cast() const {
return static_cast<Impl const&>(*this);
}
};
// 4. Implement the ActiveVector4 with the help of the aggregated type class in CoDiPack.
struct ActiveVector4 : public codi::AggregatedActiveType<Vector4, ActiveNumber, ActiveVector4> {
using Base = codi::AggregatedActiveType<Vector4, ActiveNumber, ActiveVector4>;
using Base::Base; // Include constructors from the base class.
template<typename Arg1, typename Arg2, typename Arg3>
codi::ExpressionInterface<Number, Arg1> const& arg1,
codi::ExpressionInterface<Number, Arg2> const& arg2,
codi::ExpressionInterface<Number, Arg3> const& arg3,
codi::ExpressionInterface<Number, Arg3> const& arg4) : Base() {
Base::values[0] = arg1;
Base::values[1] = arg2;
Base::values[2] = arg3;
Base::values[3] = arg4;
}
ActiveNumber& operator[](size_t index) {
return Base::values[index];
}
};
template<typename VectorType>
ActiveNumber func(ActiveNumber const& s1, VectorType const& v1, ActiveNumber const& s2, VectorType const& v2) {
return (s1 * v1 + s2 * v2).norm();
}
// Test the implementation with either ActiveVector4 or Vector4WithActiveType.
template<typename VectorType>
void test() {
VectorType v1(ActiveNumber(1.0), ActiveNumber(2.0), ActiveNumber(4.0), ActiveNumber(8.0));
VectorType v2(ActiveNumber(0.1), ActiveNumber(0.2), ActiveNumber(0.4), ActiveNumber(0.8));
ActiveNumber s1(5.0);
ActiveNumber s2(0.5);
tape.setActive();
tape.registerInput(s1);
tape.registerInput(s2);
for(int i = 0; i < 4; i += 1) {
tape.registerInput(v1[i]);
tape.registerInput(v2[i]);
}
ActiveNumber res = func(s1, v1, s2, v2);
codi::TapeValues diff = tape.getTapeValues().subtract(before);
tape.registerOutput(res);
tape.setPassive();
res.gradient() = 1.0;
tape.evaluate();
std::cout << "d f/d s1 = " << s1.getGradient() << std::endl;
std::cout << "d f/d s2 = " << s2.getGradient() << std::endl;
for(int i = 0; i < 4; i += 1) {
std::cout << "d f/d v1[" << i << "] = " << v1[i].getGradient() << std::endl;
}
for(int i = 0; i < 4; i += 1) {
std::cout << "d f/d v2[" << i << "] = " << v2[i].getGradient() << std::endl;
}
diff.formatDefault();
tape.reset();
}
int main() {
using Vector4WithActiveType = Eigen::Matrix<ActiveNumber, 4, 1>;
std::cout << "Running example with 'Vector4WithActiveType' vector type. No specialization are used for Eigen vector." << std::endl;
test<Vector4WithActiveType>();
std::cout << std::endl;
std::cout << "Running example with 'ActiveVector4' vector type. The specializations are used for Eigen vector." << std::endl;
test<ActiveVector4>();
return 0;
}

Introduction

An aggregated type, in the sense of CoDiPack, is a structure that can be expressed by a set of double values. E.g. std::complex<double> can be represented by two double values. If aggregated types are added to the CoDiPack expression tree, then memory and runtime of differentiated programs can be reduced.

Defining the basic types and the active vector type

In this tutorial we want to add specialized expressions for the Eigen Vector4 type. Therefore, we decalare first the basic types as:

// Declare all types without the CoDiPack active type.
using Number = double;
using Vector4 = Eigen::Matrix<Number, 4, 1>;
using Vector4Transpose = Eigen::Matrix<Number, 1, 4>;
// Declare all types with the CoDiPack active type.
using ActiveNumber = codi::RealReverse;

It is now possible to declare the vector type that integrates into the CoDiPack expressions. The simplest way is to extend from codi::AggregatedActiveType. This base class implements all the necessary interfaces and functions for the compatibility with the CoDiPack expressions. In order to do this, codi::AggregatedActiveType needs some information about the used type, which is in our case Vector4. This is done by the specialization of codi::RealTraits::AggregatedTypeTraits. Here, the array constructor, the array access methods, and the number of elements for the type are defined. If the type can be described as an array of floating point values, then the default implementation codi::RealTraits::ArrayAggregatedTypeTraitsBase can be used. In addition we need to specify a transposed operation for the type. All together this is done with:

template<>
: public codi::RealTraits::ArrayAggregatedTypeTraitsBase<Vector4, Number, Vector4, 4> {};
namespace codi {
CODI_CREATE_TRANSPOSE(Vector4, Vector4Transpose, jacobian.transpose());
}
struct ActiveVector4 : public codi::AggregatedActiveType<Vector4, ActiveNumber, ActiveVector4> {
using Base = codi::AggregatedActiveType<Vector4, ActiveNumber, ActiveVector4>;
using Base::Base; // Include constructors from the base class.
template<typename Arg1, typename Arg2, typename Arg3>
codi::ExpressionInterface<Number, Arg1> const& arg1,
codi::ExpressionInterface<Number, Arg2> const& arg2,
codi::ExpressionInterface<Number, Arg3> const& arg3,
codi::ExpressionInterface<Number, Arg3> const& arg4) : Base() {
Base::values[0] = arg1;
Base::values[1] = arg2;
Base::values[2] = arg3;
Base::values[3] = arg4;
}
ActiveNumber& operator[](size_t index) {
return Base::values[index];
}
};

Currently we only define a constructor and the array access for the ActiveVector4.

Definition of operations

For our function example, we need two operations the scalar multiplication and the vector addition. Both can be defined by implementing the codi::BinaryOperation interface. This interface defines the primal operation and the gradient computations. For the real valued case, the operation implementations can always assume that the Jacobian has the same type as the primal result. For the vector operations, this is no longer the case. Therfore, we also need to declare the Jacobian types in the operation implementations:

template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct ScalarVectorMultiplicationOperation : public codi::BinaryJacobianOperation<Vector4, ScalarVectorMultiplicationOperation<T>> {
static CODI_INLINE Vector4 primal(Number const& s, Vector4 const& v) {
return s * v;
}
static CODI_INLINE Vector4 gradientA(Number const& s, Vector4 const& v, Vector4 const& result) {
return v;
}
static CODI_INLINE Number gradientB(Number const& s, Vector4 const& v, Vector4 const& result) {
return s;
}
};
// Define scalar * vector overload.
template<typename ArgS, typename ArgV>
}
template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct VectorAdditionOperation : public codi::BinaryJacobianOperation<Vector4, VectorAdditionOperation<T>> {
static CODI_INLINE Vector4 primal(Vector4 const& v1, Vector4 const& v2) {
return v1 + v2;
}
static CODI_INLINE Number gradientA(Vector4 const& v1, Vector4 const& v2, Vector4 const& result) {
return 1.0;
}
static CODI_INLINE Number gradientB(Vector4 const& v1, Vector4 const& v2, Vector4 const& result) {
return 1.0;
}
};
// Define vector + vector overload.
template<typename ArgV1, typename ArgV2>
}

Definition of member operations

If the base type contains member operations, then these need to be specified to. These operations could be specified in the implementation of ActiveVector4, but then they would not be availalbe on expressions. For example, it would not be possible to write (v1 + v2).norm(), since (v1 + v2) has the type codi::ComputeExpression. It is therefore necessary to inject the member operations into all expressions. This can be done by a specialization of codi::ExpressionMemberOperations for Vector4:

// Operation for vector.norm().
template<typename T> // Template argument not used, we use the direct type specification of Vector4.
struct VectorNormOperation : public codi::UnaryJacobianOperation<Number, VectorNormOperation<T>> {
static CODI_INLINE Number primal(Vector4 const& v) {
return v.norm();
}
static CODI_INLINE Vector4 gradient(Vector4 const& v, Number const& result) {
return v / result;
}
};
template<typename Impl>
public:
using Real = Vector4;
auto norm() const {
return codi::ComputeExpression<Number, VectorNormOperation, Impl>(this->cast());
}
private:
CODI_INLINE Impl const& cast() const {
return static_cast<Impl const&>(*this);
}
};

Advantages and further hints

With the finished implementation we can now call the function defined above. We will do this once with an unspecialized type (using Vector4WithActiveType = Eigen::Matrix<ActiveNumber, 4, 1>) and once with the ActiveVector4. In the test we measure the tape memory that is recorded during the function evaluation. The result is:

Running example with 'Vector4WithActiveType' vector type. No specialization are used for Eigen vector.
-------------------------------------
CoDi Tape Statistics ( JacobianLinearTape )
-------------------------------------
Total memory used : 648.00 B
-------------------------------------
Statement entries
-------------------------------------
Total number : 20
-------------------------------------
Jacobian entries
-------------------------------------
Total number : 39
-------------------------------------
Running example with 'ActiveVector4' vector type. The specializations are used for Eigen vector.
-------------------------------------
CoDi Tape Statistics ( JacobianLinearTape )
-------------------------------------
Total memory used : 129.00 B
-------------------------------------
-------------------------------------
Statement entries
-------------------------------------
Total number : 1
-------------------------------------
Jacobian entries
-------------------------------------
Total number : 10
-------------------------------------

With the specialization only 129 bytes are used which is a reduction of about 80%. Since we integrated the ActiveVector4 into the CoDiPack expressions, the tape no longer records all intermediate steps in the function evaluation. The statement in the function is seen by CoDiPack as one big expression which reduces the number of statment and Jacobian entries.

Currently, all uses of Vector4 in an application would need to be exchanged with ActiveVector4. A declaration like Eigen::Matrix<ActiveNumber, 4, 1> would not automatically use the specialization. An automatic use can be achieved by a direct specialization of Eigen::Matrix for a CoDiPack type. E.g:

template<>
struct Eigen::Matrix<ActiveNumber, 4, 1, Eigen::AutoAlign, 4, 1> :
public codi::AggregatedActiveType<Vector4, ActiveNumber, ActiveVector4> {
... // Same as ActiveVector4.
};
Represents a concrete aggregated lvalue int the CoDiPack expression tree.
Definition aggregatedActiveType.hpp:164

With this specialization both runs of the test function have the same reduced memory consumption.

Notes on implementation

The current implementation of aggregated types is tailored to the implementation of std::complex. The current expression framework of CoDiPack has problems in handling operations where the operators have not the commutativity property. It is also problematic to handle operations where the AD reverse mode can not be expressed as a Jacobian times a bar value. These cases can be handled, but their implementation is quite cumbersome. We will extend the expression framework in the near future such that these cases can be handled in an improved way.