Tape

Overview

template <typename T>
class
 Tape

Tape data type to record operations for adjoint computations, using the underlying scalar type T (which may in turn be an active type for higher-order derivative calculations).

Typical usage:

Tape<double> tape;
// initialize independent variables
AReal<double> x1 = 1.2, x2 = 12.1;
// register independents with the tape
tape.registerInput(x1);
tape.registerInput(x2);
// start recording derivatives on tape
tape.newRecording();
AReal<double> y = sin(x1) + x1*x2;
// register output and set adjoint values
tape.registerOutput(y);
derivative(y) = 1.0;
// compute the adjoints of the independent variables
tape.computeAdjoints();
// output/use results
std::cout << "y = " << value(y) << "\n"
          << "dy/dx1 = " << derivative(x1) << "\n"
          << "dy/dx2 = " << derivative(x2) << "\n";

For usability, it is recommended to use the type definitions in AD Mode Interface instead of declaring this tape type directly.

Members

Types

type size_type

Type for sizes

type slot_type

Type used to represent a slot of a specific active variable

type position_type

Type to represent a position in the tape (same as slot_type)

typedef AReal<T> active_type

Active data type that records on this type of tape

typedef T value_type

The value type of the tape, i.e. T

typedef Tape<T> tape_type

The tape’s type itself - for generic code

typedef CheckpointCallback<tape_type> *callback_type

The callback type used for checkpoints

Construct, Destruct, and Assign

A tape can be created and moved, but it is not copyable.

explicit Tape(bool activate = true)

Constructs a new tape, and activates it if needed.

If active is true, a global thread-local pointer is set to this constructed instance, resulting all operations and instantiations of active data types that follow to get automatically associated with this tape instance.

Throws:

TapeAlreadyActive – if activate is true and another tape is already active for the current thread

~Tape()

Destructor.

Tape(Tape &&)

Move-constructor.

Tape &operator=(Tape &&)

Move-assign.

Recording Control

void activate()

Sets a global thread-local pointer to this tape instance, resulting all registerInput calls and operations of active data types depending on such inputs to get associated with this tape instance.

Throws:

TapeAlreadyActive – if another tape is already active for the current thread

void deactivate()

Resets the global thread-local pointer to NULL, hence deactivating this tape.

bool isActive() const

Check if the current instance is the currently active tape.

Returns:

true if the this instance is active

static Tape *getActive()

Get a pointer to the currently active tape.

Note that this is a thread-local pointer - calling this function in different threads gives different results.

Returns:

Pointer to the currently-active thread-local tape - or nullptr

void registerInput(active_type &inp)

Register the given variable with the tape and start recording dependents of it. A call to this function or its overloads is required in order to calculate adjoints.

template <typename Inner>
void
 registerInputs(std::vector<Inner> &v)

Convenience function to register all variables in a vector as an input.

template <typename It>
void
 registerInputs(It first, It last)

Convenience iterator interface to register variables in a range with the tape.

void registerInput(std::complex<active_type> &inp)

Register a complex-valued input.

void registerOutput(active_type &inp)

Register the given variable as an output with the tape. A call to this function or its overloads is required in order to allow seeding derivatives (adjoints).

template <typename Inner>
void
 registerOutputs(std::vector<Inner> &v)

Convenience function to register all variables in a vector as an input.

template <typename It>
void
 registerOutputs(It first, It last)

Convenience iterator interface to register variables in a range with the tape.

void registerOutput(std::complex<active_type> &inp)

Register a complex-valued output.

void newRecording()

Start recording derivatives.

This function should be called after the independent variables are initialized and registered, as the computeAdjoints() method will roll back the adjoints until the point where newRecording() was called.

void computeAdjoints()

Propagates adjoints by interpreting the operations on the tape.

This function should be called after the output derivatives (adjoints) have been initialized to a non-zero value.

After this call, the derivatives of the independent variables are set and can be obtained.

Throws:

DerivativesNotInitialized – If called without setting any derivative first. Gives strong exception safety guarantee - tape state unchanged in case of exception.

position_type getPosition()

Returns the current position in the tape as an opaque integer (its value is internal and should not be relied upon in client code). This posiiton can later be used in the methods clearDerivativesAfter, resetTo, and computeAdjointsTo.

void clearDerivativesAfter(position_type pos)

Clears all derivatives after the given position in the tape (resets them to zero). Derivatives before this point keep their value, meaning that further calls to computeAdjoints will potentially increment these adjoints further.

void resetTo(position_type pos)

Resets the tape back to the given position. All statements recorded after this point will be discarded.

Warning

If variables registered after the given postion (are dependent variables computed after this position) are used again after a call to resetTo, the behaviour is undefined, as their slot in the tape is no longer valid.

void computeAdjointsTo(position_type pos)

Like computeAdjoints, but stops rolling back the adjoints at the given position in the tape.

void clearAll()

Clears the stored tape info and brings it back to its initial state.

While this clears the content, it leaves allocated memory untouched. This may be a performance gain compared to repeated construction/destruction of tapes of the same time, for example in a path-wise AD Monte-Carlo.

Derivatives

T &derivative(slot_type s)

Get a reference to the derivative associated with the slot s.

Parameters:
slot_type s

The slot of the derivative

Throws:

OutOfRange – if the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined in this case) Gives strong exception safety guarantee - tape state unchanged in case of exception.

const T &derivative(slot_type s) const

Get a const reference to the derivative associated with the slot s.

Parameters:
slot_type s

The slot of the derivative

Throws:

OutOfRange – if the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined in this case) Gives strong exception safety guarantee - tape state unchanged in case of exception.

T getDerivative(slot_type s) const

Get the value of the derivative associated with the slot s.

Parameters:
slot_type s

The slot of the derivative

Throws:

OutOfRange – if the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined in this case) Gives strong exception safety guarantee - tape state unchanged in case of exception.

void setDerivative(slot_type s, const T &v)

Set the value of the derivative associated with the slot s.

Parameters:
slot_type s

The slot of the derivative

const T &v

The value to assign to the derivative

Throws:

OutOfRange – if the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined in this case) Gives strong exception safety guarantee - tape state unchanged in case of exception.

void clearDerivatives()

Resets all stored derivatives to 0 (but leaving the recorded data in place). This can be used to calculate derivatives w.r.t. multiple outputs, as the same tape can be rolled back multiple times.

Status

void printStatus() const

Prints the number of recorded operations, statements, and registered variables to stdout.

std::size_t getMemory() const

Returns the memory in bytes that is occupied by the tape.

Returns:

Memory in bytes

Checkpointing

void insertCallback(callback_type cb)

Insert a checkpoint callback into the tape.

During computing adjoints (computeAdjoints()), this callback is called when the tape reaches the current position, allowing users to implement their own adjoint computation.

Note that the parameter is provided by pointer, but the tape does not take ownership. It is the responsibility of the user to free the memory for the callback object. Alternatively, the Checkpoint Callback Memory Management API can be used to have the tape destroy the callbacks automatically.

Parameters:
callback_type cb

Pointer to a CheckpointCallback instance.

T getAndResetOutputAdjoint(slot_type slot)

Obtains and resets the output adjoint to 0

This function should be called by CheckpointCallback<TapeType>::computeAdjoints() to get the current value of the adjoint. It also resets its adjoint to 0 on the tape to allow re-use of that variable.

Parameters:
slot_type slot

The slot of the output variable.

Returns:

The value of the variable’s derivative (i.e. its adjoint)

Throws:

OutOfRange – If the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined) Gives strong exception safety guarantee - tape state unchanged in case of exception.

void incrementAdjoint(slot_type slot, const T &x)

Increments the adjoint of the given slot by the value x.

This function should be called at the end of a CheckpointCallback<TapeType>::computeAdjoints() implementation, to update the input adjoints with the computed adjoint increments.

Parameters:
slot_type slot

Slot of the input variable to increment

const T &x

The value to be added to the adjoint

Throws:

OutOfRange – If the given slot is not associated with a stored derivative. (Only thrown in debug mode, otherwise the behaviour is undefined) Gives strong exception safety guarantee - tape state unchanged in case of exception.

void newNestedRecording()

Starts a new nested recording that can be rolled-back on its own. It must be ended with endNestedRecording().

It is intended for use within a CheckpointCallback<TapeType>::computeAdjoints() implementation, when from a checkpoint, the adjoints are computed using XAD in a nested recording.

To avoid forgetting the call to endNestedRecording(), consider using the RAII class ScopedNestedRecording.

void endNestedRecording()

Ends a nested recording.

Checkpoint Callback Memory Management

void pushCallback(callback_type cb)

Let this tape handle the de-allocation of the given callback.

When the tape is destructed, it also destructs all callbacks that have been registered using this function.

Use this if checkpoints are created in a stateless function to avoid having to track and destroy checkpoint callbacks manually.

Parameters:
callback_type cb

Pointer to a dynamically-allocated checkpoint callback

callback_type getLastCallback()

Obtains the last CheckpointCallback object that has been pushed with pushCallback().

This can be useful if multiple subsequent checkpoints can be added to the same checkpoint callback object.

Returns:

Pointer to the last callback object that has been pushed.

Throws:

OutOfRange – if the callback stack is empty Gives strong exception safety guarantee - tape state unchanged in case of exception.

size_type getNumCallbacks() const

Gets the number of callback objects that have been pushed by pushCallback()

Returns:

Number of callback objects registered

bool haveCallbacks() const

Checks if there have been any checkpoint callbacks registered by pushCallback()

Returns:

true if there is at least one pushed callback object.

void popCallback()

Removes the callback object that has been last pushed by pushCallback()

Throws:

OutOfRange – if the stack of callbacks is empty Gives strong exception safety guarantee - tape state unchanged in case of exception.

Nested Tape

template <typename TapeType>
class
 ScopedNestedRecording

Convenience RAII class to ensure that a call to Tape<T>::newNestedRecording() is always followed by the corresponding Tape<T>::endNestedRecording().

It should be constructed on the stack. On creation it starts a nested recording on the corresponding tape, and on destruction it ends the nested recording. This is useful for checkpoint callbacks, i.e. within the implementation of CheckpointCallback<TapeType>::computeAdjoints().

ScopedNestedRecording(TapeType *t)

Start a new nested recording on the given tape and track it with this object.

Parameters:
TapeType *t

pointer to the associated tape

~ScopedNestedRecording()

Ends the nested recording with the associated tape

void computeAdjoints()

Computes adjoints within the nested recording

void incrementAdjoint(TapeType::slot_type slot,
                      const
 TapeType::value_type &value
)

Increment the adjoint given by the slot by the given value. See Tape<T>::incrementAdjoint() for details.

TapeType *getTape()

Returns the underlying tape for this nested recording.

Returns:

Pointer to the underlying tape


Last update: September 2022