Skip to content

Updating linear operators to use BasicLinOp instead of LinOp

Goran Flegar edited this page Feb 7, 2018 · 1 revision

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.

BasicLinOp<>

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 the Array assignment operator. This has now been fixed by preventing the assignment operator from changing the Executor of the LinOp.
  • A new clone_to() method has been added to LinOp, which enables cloning the operator to a different executor.
  • The clone_type() method has been renamed to create_null_clone() to better express what is being done (let me know if you prefer create_empty_clone or have a better name) EDIT: this was renamed to create_empty_clone
  • A BasicLinOp CRTP has been added which provides default implementations of most of the LinOp "management" functions (like copy, move, clone, clear, etc.). All of the concrete LinOps we have have been modified accordingly. This also reduces the complexity of implementing new LinOps, as now you only need to implement the two overloads of apply() to make it work (unless you want special behavior of your LinOp). 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 the create static method.
  • Since the implementation of this create method doesn't support initializer lists as arguments, the affected create methods of Dense have been replaced by top-level initialize 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:

  1. 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)
  2. 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

Modifying gko::matrix::Ell

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.

Clone this wiki locally