-
Notifications
You must be signed in to change notification settings - Fork 90
Updating linear operators to use BasicLinOp instead of LinOp
This guide is supposed to give you some hints on how to convert a linear operator implemented using the old gko::LinOp
class as base to an operator which uses the new, more powerful, gko::BasicLinOp<>
CRTP.
It will be explained using the gko::matrix::Ell
class implemented here.
As a general introduction, here is the gitlab merge request description which introduced gko::BasicLinOp<>
:
This MR improves the LinOp
class, and solves some issues we encountered when using it:
- Objects derived from
LinOp
had a faulty assignment operator, as it was not conforming to the semantics of theArray
assignment operator. This has now been fixed by preventing the assignment operator from changing theExecutor
of theLinOp
. - A new
clone_to()
method has been added toLinOp
, which enables cloning the operator to a different executor. - The
clone_type()
method has been renamed tocreate_null_clone()
to better express what is being done (let me know if you prefercreate_empty_clone
or have a better name) EDIT: this was renamed tocreate_empty_clone
- A
BasicLinOp
CRTP has been added which provides default implementations of most of theLinOp
"management" functions (like copy, move, clone, clear, etc.). All of the concreteLinOp
s we have have been modified accordingly. This also reduces the complexity of implementing newLinOp
s, as now you only need to implement the two overloads ofapply()
to make it work (unless you want special behavior of yourLinOp
). So go on and sharpen you C++ skills by reading about Curiously Recurring Template Pattern if you don't know about it already. - The previous bullet implicitly closed [reference to private gitlab issue], as now all the solvers use the same default behavior when copying/cloning the object. This default creates a shallow copy, so the underlying system matrix is not copied, but another reference is just added to the same object (I had to do minor changes to some unit tests to account for this). We can easily change this later if we decide this is not what we want.
EDIT: The new commits added to the MR also add the following:
-
BasicLinOp
now also adds a default implementation of thecreate
static method. - Since the implementation of this create method doesn't support initializer lists as arguments, the affected
create
methods ofDense
have been replaced by top-levelinitialize
functions. These functions are more powerful, as they can also generate other types of matrices (as long as Dense can be converted to that type).
All of these new features ultimately resulted in a reduction of the code base by cca. 200 lines. Nothing better than getting new features by removing lines of code 😎
So, what gko::BasicLinOp
does is implement a lot of stuff for us. It does that by relying on several things:
- the concrete linear operator implementing
gko::BasicLinOp
should have copy and move assignment operators which behave as expected (usually, the default ones generated by the compiler work just fine) - the concrete linear operator should have a constructor which takes only an
gko::Executor
as a parameter, and constructs a 0-by-0 operator on that executor
Instead of inheriting LinOp
, we now use BasicLinOp
and send Ell
as a template parameter. BasicLinOp
will add LinOp
and ConvertibleTo<Ell>
in the inheritence tree for Ell
automatically:
class Ell : public LinOp,
public ConvertibleTo<Ell<ValueType, IndexType>>,
public ConvertibleTo<Dense<ValueType>>,
public ReadableFromMtx
should be changed to:
class Ell : public BasicLinOp<Ell<ValueType, IndexType>>,
public ConvertibleTo<Dense<ValueType>>,
public ReadableFromMtx
To allow BasicLinOp
to access protected constructors of Ell
and generate create
static methods which call this constructors for us, we need to specify it as a friend of Ell
. Also, to make sure that create
, convert_to
and move_to
methods generated by BasicLinOp
are visible even if we overload them in Ell
, we need to explicitly introduce them with the using
keyword:
friend class gko::matrix::Dense<ValueType>;
public:
using value_type = ValueType;
should be changed to:
friend class BasicLinOp<Ell>;
friend class Dense<ValueType>;
public:
using BasicLinOp<Ell>::create;
using BasicLinOp<Ell>::convert_to;
using BasicLinOp<Ell>::move_to;
using value_type = ValueType;
Most of the methods in Ell can now simply be removed, as they are generated by BasicLinOp
:
/**
* Creates an uninitialized Ell matrix of the specified size.
*
* @param exec Executor associated to the matrix
* @param num_rows number of rows
* @param num_cols number of columns
* @param num_nonzeros number of nonzeros
* @param max_nnz_row maximum number of nonzeros in one row
*/
static std::unique_ptr<Ell> create(std::shared_ptr<const Executor> exec,
size_type num_rows, size_type num_cols,
size_type num_nonzeros,
size_type max_nnz_row)
{
return std::unique_ptr<Ell>(
new Ell(exec, num_rows, num_cols, num_nonzeros, max_nnz_row));
}
/**
* Creates an empty ELL matrix.
*
* @param exec Executor associated to the matrix
*/
static std::unique_ptr<Ell> create(std::shared_ptr<const Executor> exec)
{
return create(exec, 0, 0, 0, 0);
}
void copy_from(const LinOp *other) override;
void copy_from(std::unique_ptr<LinOp> other) override;
void apply(const LinOp *b, LinOp *x) const override;
void apply(const LinOp *alpha, const LinOp *b, const LinOp *beta,
LinOp *x) const override;
std::unique_ptr<LinOp> clone_type() const override;
void clear() override;
void convert_to(Ell *other) const override;
void move_to(Ell *other) override;
void convert_to(Dense<ValueType> *other) const override;
void move_to(Dense<ValueType> *other) override;
void read_from_mtx(const std::string &filename) override;
should be changed to:
void apply(const LinOp *b, LinOp *x) const override;
void apply(const LinOp *alpha, const LinOp *b, const LinOp *beta,
LinOp *x) const override;
void convert_to(Dense<ValueType> *other) const override;
void move_to(Dense<ValueType> *other) override;
void read_from_mtx(const std::string &filename) override;
The implementations of the removed methods should also be deleted from the ell.cpp
file. As for the unit tests for these methods, lets still keep them - this ensures that BasicLinOp
does in fact generate the correct functionality for Ell
(this might not be the case for all classes).
Finally, a new protected constructor should be added which constructs a 0-by-0 matrix:
protected:
explicit Ell(std::shared_ptr<const Executor> exec)
: BasicLinOp<Ell>(exec, 0, 0, 0), max_nnz_row_(0) {}
This should be it! And ELL
should be working with the new class.
Note that there was also a slight change in the interface of gko::matrix::Dense
. The create
methods which take initializer list as input have been removed, and in their place there is now a standalone function gko::initialize
.
So constructs like gko::Dense::create(executor, {1, 2, 3, 4})
should be replaced with gko::initialize<gko::Dense::create>({1, 2, 3, 4}, executor)
. There should also be plenty more examples of using gko::initialize
in the unit tests.
Tutorial: Building a Poisson Solver
- Getting Started
- Implement: Matrices
- Implement: Solvers
- Optimize: Measuring Performance
- Optimize: Monitoring Progress
- Optimize: More Suitable Matrix Formats
- Optimize: Using a Preconditioner
- Optimize: Using GPUs
- Customize: Loggers
- Customize: Stopping Criterions
- Customize: Matrix Formats
- Customize: Solvers
- Customize: Preconditioners