-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rethinking the RoughPy algebra module #63
Comments
Everything here has to be done with kernels (using the tools from platform/devices). This includes things like vector arithmetic, all of the different multiplications - Free tensor, (half) shuffles, Lie - and the various other operations that are needed. The advantage of this approach is that it is completely agnostic as to where the data is stored and where the computations should take place. These details are handled by the device runtime and the dispatcher. There are a couple of problems though. First, we have implementations of most of these operations as kernels, but we don't yet have a working implementation of the Lie multiplication. I expect that Lie multiplication won't be too hard to construct as a kernel for the "standard" Hall basis and densely stored vectors, provided we can understand the index pattern of the multiplication. Sparse computations are far more complicated. We don't know, a priori, which keys will have non-zero coefficients in the result. This is especially problematic for the non-standard bases - though no such bases are necessary at the moment. For ordered bases, we can do some work in advance to figure out which keys might appear in the result. The kernel can then populate the result buffer. However, we will have to do one additional step to filter out the coefficients that end up being zero - unless we drop this restriction on sparse representations. |
The next problem to tackle is kernel design. I think I've got this down now, but there is a lot to think about. So on the vector side most of the operations fall into either in-place or out-of-place binary operations. These necessarily need separate kernels because the operation mode is different. Moreover, each of the kernels need to be able to accommodate certain combinations of sparsity of input arguments. The name of the kernel will need to disambiguate between all of these possible cases. On top of this, there are a number of other design considerations:
With all this in mind, I've come up with a rather simple interface to deal with all this. The class Vector {
...
devices::Kernel get_kernel(OperationType kernel_type, string_view kernel_name, string_view suffix="") const;
void check_and_resize_for_operands(const Vector& lhs, const Vector& rhs);
void apply_binary_kernel(string_view kernel_name, const Vector& lhs, const Vector& rhs, optional<scalars::Scalar> multiplier {});
...
}; Here
The suffix is a string of letters I originally coded OCL kernels in C using macro hell to provide the many different scalar type support. However, this was rather silly for a number of reasons. First it is impossible to read. Second, and more importantly, there are much better ways to do this. Ideally, one might use SYCL and pre-compilation (see separated builds in #73) to use C++ templates to define the kernels in a sane way. Alternatively, we can just use CMake to construct the files at build time, which requires very little extra work. Here is a list of the vector kernels I think we need so far (name only, not including sparsity or scalar type variations).
We don't need scalar divide functions, since we can actually implement these using their scalar-multiply counterparts without too much effort. |
The algebra module of RoughPy is built around the assumption that the algebra types will be provided by an external library like libalgebra-lite (the current default) or libalgebra. However, this is unsatisfactory for a number of reasons:
I'm planning to shift RoughPy to use its own vector/algebra hierarchy built around its scalar system and a new basis/multiplication system designed to be modular, efficient, and (most importantly) simple. This will also allow us to build up from the device kernel system that is built into the scalar system, and will practically remove all templates from the algebra module. Of course, this needs to be properly thought through, so I opened this issue to keep track of my reasoning and designs, and also to allow comments from other contributors/users who might have feelings about this issue.
Vector design
In Libalgebra (lite), a vector is a class template defined (essentially) as follows:
This means that vectors with different coefficients and different bases have different types. This is desirable in C++, but it really causes a headache at the Python level. Regardless of whether it is mathematically sensible or not, we want (need) to be able to add vectors with different scalars to one another, provided that their bases are compatible, whenever addition is well-defined. Constructing the more esoteric scalar types in Python is a rather painful task, especially if the scalars in question are just integers. The scalar system already handles type-agnostic storage and conversion in a very easily extensible way, and this should already be completely sufficient for a vector type.
The second part is the bases. These are actually the most important thing that are imported from the external libraries. However, we already have to create a wrapper class that gives a consistent interface to each basis, which at best replicates effort. Porting the tensor basis and Lie basis into RoughPy makes far more sense than to rely on libalgebra lite purely for the bases.
The final consideration is the vector type, which is actually one of the more complex problems. It should not matter how the internal mechanics of a vector are represented, only that their bases are the same (and coefficients are compatible). Unfortunately, this is rather tricky with the current system. Using the scalar system, we can do both at once using the
KeyScalarArray
container that wraps an array of scalars together with an optional array of basis keys.On the topic of basis keys, each of the libraries keeps its basis keys in various different forms. I tried to homogenize these in RoughPy, but ultimately this solution was unsatisfactory. Reworking the algebras and bases also allows me to rethink the keys at the same time.
The new basic vector type should now look like this:
Note that
KeyScalarArray
contains the scalar type information along with the data. A vector is sparse ifm_data.has_keys()
is true, and dense otherwise. We'll have to rework theKeyScalarArray
slightly to accommodate my plans for basis keys, but otherwise the amount of changes is very minimal. We may have to include aContext
pointer in the vector, since this is used for many things but with the proposed changes this may no longer be necessary. Contexts formerly collected together the algebra types of the same configuration (e.g. width 5, depth 2, floating point coefficients from libalgebra-lite). However, with the new setup, there won't be any need for an environment that understands the real type of the algebras (rather than their interface). Contexts will still be useful, but not as essential.Key Design
This shift to a more generic structure will also mean a fairly substantial change to the way that basis keys are interpreted. All bases will, fundamentally, need to use the same key type as one another. However, this doesn't mean they all have to be identical. A key can be a pointer-like object that has two modes: index mode or pointer mode. In index mode, the content of the key is simply the index thereof in the basis total order (should it have an order). In pointer mode, the key is actually a pointer to some other data that defines the key, and it is up to the basis to interpret this form. Moreover, using bit-packing we can fit this neatly inside a pointer-sized type by stuffing the mode indicator bit into the low bits of a pointer, which are always zero for sufficiently aligned types. (We use the same technique in packing the scalar storage mode in scalars/packed_scalar_type_pointer.h.) If we go one step further, we can actually pack in a degree value alongside the index, using 1 byte of the 4/8 available (or less, maybe depending on the size of a pointer).
The pointer should be to a basis interface class that minimally provides a method to retrieve its basis, determine its type, compare two keys, and compute a hash value:
We can build on top of this in a hierarchy to implement the different levels of bases. Ordered bases need to implement "less" and "index", graded bases need to implement "degree", and word-like bases need to be ordered, graded, and implement "parents".
Basis design
The basis is responsible for decoding and interpreting
BasisKey
objects and providing a means for vectors to structure themselves correctly.Most of our key types are actually word-like, or tree-like in the sense that one can extract two parent sub-words that are combined to form the full word.
Tensor keys satisfy this by taking the first letter from the tailing word, and Hall basis keys satisfy this by taking the two unique Hall words whose bracket is the given word.
(For letters, this pairing is the "initial element" together with the letter itself.)
It is the responsiblity of the basis to perform the following actions:
BasisKey
into a string for printing.BasisKey
s are equal.BasisKey
.BasisKey
is valid for the basis.BasisKey
preceeds another,BasisKey
according to the basis order,BasisKey
at a given index within the basis order,BasisKey
,BasisKey
represents a letter or a word,BasisKey
representing a letter into a letter (integer),BasisKey
representing a word into a pair ofBasisKeys
representing it's parents.The text was updated successfully, but these errors were encountered: