Template Numerical Library: Vectors

Template Numerical Library version\ main:c7077e4fd

Loading...
Searching...
No Matches
Vectors

# Introduction

This chapter introduces vectors in TNL. Vector, in addition to Array, offers also basic operations from linear algebra. The reader will mainly learn how to do BLAS level 1 operations in TNL. Thanks to the design of TNL, it is easier to implement, independent of the hardware architecture, and in some cases even faster then BLAS or cuBLAS implementation.

# Dynamic vectors

Vector is a class template similar to Array, which is defined in the TNL::Containers namespace and has four template parameters:

- Real is type of data to be stored in the vector
- Device is the device to be used for the execution of vector operations. It can be any class defined in the TNL::Devices namespace.
- Index is the type to be used for indexing the vector elements.
- Allocator is the type of the allocator used for the allocation and deallocation of memory used by the vector. By default, an appropriate allocator for the specified Device is selected with TNL::Allocators::Default.

Unlike Array, Vector requires that the Real type is numeric or a type for which basic algebraic operations are defined. What kind of algebraic operations are required depends on what vector operations the user will call. Vector is derived from Array so it inherits all its methods. In the same way the Array has its counterpart ArraView, Vector has VectorView which is derived from ArrayView. See Arrays for more details.

In addition to the comparison operators == and != defined for Array and ArrayView, the comparison operators <, <=, >, and >= are available for Vector and VectorView. The comparison follows the lexicographic order and it is performed by an algorithm equivalent to std::lexicographical_compare.

## Horizontal operations

By horizontal operations we mean vector expressions where we have one or more vectors as an input and a vector as an output. Horizontal operations in TNL are performed by the expression templates. It makes algebraic operations with vectors easy to do and very efficient at the same time. In some cases, one gets code even more efficient compared to BLAS and cuBLAS. See the following example.

1#include <iostream>
2#include <TNL/Containers/Vector.h>
3#include <TNL/Containers/VectorView.h>
4
5using namespace TNL;
6using namespace TNL::Containers;
7
8template< typename Device >
9void
10expressions()
11{
12 using RealType = float;
13 using VectorType = Vector< RealType, Device >;
14
15 /****
16 * Create vectors
17 */
18 const int size = 11;
19 VectorType a( size );
20 VectorType b( size );
21 VectorType c( size );
22 a.forAllElements(
23 [] __cuda_callable__( int i, RealType& value )
24 {
25 value = 3.14 * ( i - 5.0 ) / 5.0;
26 } );
27 b = a * a;
28 c = 3 * a + sign( a ) * sin( a );
29 std::cout << "a = " << a << '\n';
30 std::cout << "sin( a ) = " << sin( a ) << '\n';
31 std::cout << "abs( sin( a ) ) = " << abs( sin( a ) ) << '\n';
32 std::cout << "b = " << b << '\n';
33 std::cout << "c = " << c << '\n';
34}
35
36int
37main( int argc, char* argv[] )
38{
39 /****
40 * Perform test on CPU
41 */
42 std::cout << "Expressions on CPU ...\n";
43 expressions< Devices::Host >();
44
45 /****
46 * Perform test on GPU
47 */
48#ifdef __CUDACC__
49 std::cout << '\n';
50 std::cout << "Expressions on GPU ...\n";
51 expressions< Devices::Cuda >();
52#endif
53}
__cuda_callable__
#define __cuda_callable__
Definition Macros.h:49
std::cout
TNL::Containers::Vector
Vector extends Array with algebraic operations.
Definition Vector.h:37
TNL::Containers
Namespace for TNL containers.
Definition Array.h:17
TNL
The main TNL namespace.
Definition AtomicOperations.h:15
TNL::abs
__cuda_callable__ T abs(const T &n)
This function returns absolute value of given number n.
Definition Math.h:75
TNL::sign
constexpr T sign(const T &value)
This function represents the signum function.
Definition Math.h:536
TNL::sin
__cuda_callable__ auto sin(const T &value) -> decltype(std::sin(value))
This function returns sine of the given value.
Definition Math.h:285

Output is:

The expression is evaluated on the same device where the vectors are allocated, this is done automatically. One cannot, however, mix vectors from different devices in one expression.

Vector expressions consist of operands, operators, and functions. Operands may be vectors, vector views, or other vector expressions. Operators available for vector expressions are listed in the following table, where v is a result vector and expr, expr1, expr2 are vector expressions:

Expression | Meaning
--- | ---
v = expr1 + expr2 | v[ i ] = expr1[ i ] + expr2[ i ]
v = expr1 - expr2 | v[ i ] = expr1[ i ] - expr2[ i ]
v = expr1 * expr2 | v[ i ] = expr1[ i ] * expr2[ i ]
v = expr1 / expr2 | v[ i ] = expr1[ i ] / expr2[ i ]
v = expr1 / expr2 | v[ i ] = expr1[ i ] / expr2[ i ]
v = expr1 % expr2 | v[ i ] = expr1[ i ] % expr2[ i ]
v = expr1 && expr2 | v[ i ] = expr1[ i ] && expr2[ i ]
`v = expr1 || expr2` | `v[ i ] = expr1[ i ] || expr2[ i ]`
v = expr1 & expr2 | v[ i ] = expr1[ i ] & expr2[ i ]
`v = expr1 | expr2` | `v[ i ] = expr1[ i ] | expr2[ i ]`
v = +expr1 | v[ i ] = +expr1[ i ]
v = -expr1 | v[ i ] = -expr1[ i ]
v = !expr1 | v[ i ] = !expr1[ i ]
v = ~expr1 | v[ i ] = ~expr1[ i ]

Additionally, vector expressions may contain any function listed in the following table, where v is a result vector and expr, expr1, expr2 are vector expressions:

Expression | Meaning
--- | ---
v = TNL::equalTo( expr1, expr2 ) | v[ i ] = expr1[ i ] == expr2[ i ]
v = TNL::notEqualTo( expr1, expr2 ) | v[ i ] = expr1[ i ] != expr2[ i ]
v = TNL::greater( expr1, expr2 ) | v[ i ] = expr1[ i ] > expr2[ i ]
v = TNL::greaterEqual( expr1, expr2 ) | v[ i ] = expr1[ i ] >= expr2[ i ]
v = TNL::less( expr1, expr2 ) | v[ i ] = expr1[ i ] < expr2[ i ]
v = TNL::lessEqual( expr1, expr2 ) | v[ i ] = expr1[ i ] <= expr2[ i ]
v = TNL::minimum( expr1, expr2 ) | v[ i ] = min( expr1[ i ], expr2[ i ] )
v = TNL::maximum( expr1, expr2 ) | v[ i ] = max( expr1[ i ], expr2[ i ] )
v = TNL::abs( expr ) | v[ i ] = abs( expr[ i ] )
v = TNL::sin( expr ) | v[ i ] = sin( expr[ i ] )
v = TNL::cos( expr ) | v[ i ] = cos( expr[ i ] )
v = TNL::tan( expr ) | v[ i ] = tan( expr[ i ] )
v = TNL::asin( expr ) | v[ i ] = asin( expr[ i ] )
v = TNL::acos( expr ) | v[ i ] = acos( expr[ i ] )
v = TNL::atan( expr ) | v[ i ] = atan( expr[ i ] )
v = TNL::sinh( expr ) | v[ i ] = sinh( expr[ i ] )
v = TNL::cosh( expr ) | v[ i ] = cosh( expr[ i ] )
v = TNL::tanh( expr ) | v[ i ] = tanh( expr[ i ] )
v = TNL::asinh( expr ) | v[ i ] = asinh( expr[ i ] )
v = TNL::acosh( expr ) | v[ i ] = acosh( expr[ i ] )
v = TNL::atanh( expr ) | v[ i ] = atanh( expr[ i ] )
v = TNL::exp( expr ) | v[ i ] = exp( expr[ i ] )
v = TNL::log( expr ) | v[ i ] = log( expr[ i ] )
v = TNL::log10( expr ) | v[ i ] = log10( expr[ i ] )
v = TNL::log2( expr ) | v[ i ] = log2( expr[ i ] )
v = TNL::sqrt( expr ) | v[ i ] = sqrt( expr[ i ] )
v = TNL::cbrt( expr ) | v[ i ] = cbrt( expr[ i ] )
v = TNL::pow( expr ) | v[ i ] = pow( expr[ i ] )
v = TNL::floor( expr ) | v[ i ] = floor( expr[ i ] )
v = TNL::ceil( expr ) | v[ i ] = ceil( expr[ i ] )
v = TNL::sign( expr ) | v[ i ] = sign( expr[ i ] )

## Vertical operations

By vertical operations we mean (parallel) reduction based operations where we have one vector expressions as an input and one value as an output. For example computing scalar product, vector norm or finding minimum or maximum of vector elements is based on reduction. See the following example.

1#include <iostream>
2#include <TNL/Containers/Vector.h>
3#include <TNL/Containers/VectorView.h>
4
5using namespace TNL;
6using namespace TNL::Containers;
7
8template< typename Device >
9void
10expressions()
11{
12 using RealType = float;
13 using VectorType = Vector< RealType, Device >;
14 using ViewType = VectorView< RealType, Device >;
15
16 /****
17 * Create vectors
18 */
19 const int size = 11;
20 VectorType a_v( size );
21 VectorType b_v( size );
22 VectorType c_v( size );
23 ViewType a = a_v.getView();
24 ViewType b = b_v.getView();
25 ViewType c = c_v.getView();
26 a.forAllElements(
27 [] __cuda_callable__( int i, RealType& value )
28 {
29 value = i;
30 } );
31 b.forAllElements(
32 [] __cuda_callable__( int i, RealType& value )
33 {
34 value = i - 5.0;
35 } );
36 c = -5;
37
38 std::cout << "a == " << a << '\n';
39 std::cout << "b == " << b << '\n';
40 std::cout << "c == " << c << '\n';
41 auto [ min_a_val, min_a_pos ] = argMin( a );
42 auto [ max_a_val, max_a_pos ] = argMax( a );
43 auto [ min_b_val, min_b_pos ] = argMin( b );
44 auto [ max_b_val, max_b_pos ] = argMax( b );
45 std::cout << "min( a ) == " << min_a_val << " at " << min_a_pos << '\n';
46 std::cout << "max( a ) == " << max_a_val << " at " << max_a_pos << '\n';
47 std::cout << "min( b ) == " << min_b_val << " at " << min_b_pos << '\n';
48 std::cout << "max( b ) == " << max_b_val << " at " << max_b_pos << '\n';
49 std::cout << "min( abs( b ) ) == " << min( abs( b ) ) << '\n';
50 std::cout << "sum( b ) == " << sum( b ) << '\n';
51 std::cout << "sum( abs( b ) ) == " << sum( abs( b ) ) << '\n';
52 std::cout << "Scalar product: ( a, b ) == " << ( a, b ) << '\n';
53 std::cout << "Scalar product: ( a + 3, abs( b ) / 2 ) == " << ( a + 3, abs( b ) / 2 ) << '\n';
54 const bool cmp = all( lessEqual( abs( a + b ), abs( a ) + abs( b ) ) );
55 std::cout << "all( lessEqual( abs( a + b ), abs( a ) + abs( b ) ) ) == " << ( cmp ? "true" : "false" ) << '\n';
56 auto [ equal_val, equal_pos ] = argAny( equalTo( a, 5 ) );
57 if( equal_val )
58 std::cout << equal_pos << "-th element of a is equal to 5\n";
59 else
60 std::cout << "No element of a is equal to 5\n";
61}
62
63int
64main( int argc, char* argv[] )
65{
66 /****
67 * Perform test on CPU
68 */
69 std::cout << "Expressions on CPU ...\n";
70 expressions< Devices::Host >();
71
72 /****
73 * Perform test on GPU
74 */
75#ifdef __CUDACC__
76 std::cout << '\n';
77 std::cout << "Expressions on GPU ...\n";
78 expressions< Devices::Cuda >();
79#endif
80}
TNL::Containers::VectorView
VectorView extends ArrayView with algebraic operations.
Definition VectorView.h:24
TNL::min
constexpr ResultType min(const T1 &a, const T2 &b)
This function returns minimum of two numbers.
Definition Math.h:24

Output is:

The following table shows vertical operations that can be used on vector expressions:

Expression | Meaning
--- | ---
v = TNL::min( expr ) | v is the minimum of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
auto [ v, i ] = TNL::argMin( expr ) | v is the minimum of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ], i is the index of the smallest element.
v = TNL::max( expr ) | v is the maximum of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
auto [ v, i ] = TNL::argMax( expr ) | v is the maximum of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ], i is the index of the largest element.
v = TNL::sum( expr ) | v is the sum of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::maxNorm( expr ) | v is the maximum norm of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::l1Norm( expr ) | v is the l1 norm of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::l2Norm( expr ) | v is the l2 norm of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::lpNorm( expr, p ) | v is the lp norm of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::product( expr ) | v is product of expr[ 0 ], expr[ 1 ], ..., expr[ n-1 ].
v = TNL::all( expr ) | v is the result of expr[ 0 ] && expr[ 1 ] && ... && expr[ n-1 ].
v = TNL::any( expr ) | v is the result of `expr[ 0 ] || expr[ 1 ] || ... || expr[ n-1 ]`.
auto [ v, i ] = TNL::argAny( expr ) | v is the result of `expr[ 0 ] || expr[ 1 ] || ... || expr[ n-1 ]`, i is the index of the first true element.

# Static vectors

Static vectors are derived from static arrays and so they are allocated on the stack and can be created in CUDA kernels as well. Their size is fixed and given by a template parameter. The StaticVector class template is defined in the TNL::Containers namespace and has two template parameters:

1. Size is the vector size.
2. Real is type of elements stored in the vector.

The interface of StaticVector is similar to Vector. StaticVector also supports expression templates, which make the use of static vectors simple and efficient at the same time, and lexicographic comparison by operators <, <=, > and >=.

Example:

#include <iostream>
#include <TNL/Containers/StaticVector.h>
using namespace TNL;
using namespace TNL::Containers;
int
main( int argc, char* argv[] )
{
using Vector = StaticVector< 3, double >;
Vector v1( 1.0 );
Vector v2( 1.0, 2.0, 3.0 );
Vector v3( v1 - v2 / 2.0 );
Vector v4( 0.0 );
Vector v5( 1.0 );
v4 += v2;
v5 *= v3;
std::cout << "v1 = " << v1 << '\n';
std::cout << "v2 = " << v2 << '\n';
std::cout << "v3 = " << v3 << '\n';
std::cout << "v4 = " << v4 << '\n';
std::cout << "v5 = " << v5 << '\n';
std::cout << "abs( v3 - 2.0 ) = " << abs( v3 - 2.0 ) << '\n';
std::cout << "v2 * v2 = " << v2 * v2 << '\n';
}
TNL::Containers::StaticVector
Vector with constant size.
Definition StaticVector.h:19

The output looks as:

v1 = [ 1, 1, 1 ]
v2 = [ 1, 2, 3 ]
v3 = [ 0.5, 0, -0.5 ]
v4 = [ 1, 2, 3 ]
v5 = [ 0.5, 0, -0.5 ]
abs( v3 - 2.0 ) = [ 1.5, 2, 2.5 ]
v2 * v2 = [ 1, 4, 9 ]

# Distributed vectors

DistributedVector extends DistributedArray with algebraic operations. The functionality is similar to how Vector extends Array. DistributedVector also supports expression templates and other operations present in Vector.

Example:

#include <iostream>
#include <TNL/Containers/BlockPartitioning.h>
#include <TNL/Containers/DistributedVector.h>
#include <TNL/MPI/ScopedInitializer.h>
/***
* The following works for any device (CPU, GPU ...).
*/
template< typename Device >
void
distributedVectorExample()
{
using VectorType = TNL::Containers::DistributedVector< int, Device >;
using IndexType = typename VectorType::IndexType;
using LocalRangeType = typename VectorType::LocalRangeType;
const TNL::MPI::Comm communicator = MPI_COMM_WORLD;
// We set the global vector size to a prime number to force non-uniform distribution.
const int size = 97;
const int ghosts = ( communicator.size() > 1 ) ? 4 : 0;
const LocalRangeType localRange = TNL::Containers::splitRange< IndexType >( size, communicator );
VectorType v( localRange, ghosts, size, communicator );
v.forElements(
0,
size,
[] __cuda_callable__( int idx, int& value )
{
value = idx;
} );
std::cout << "Rank " << communicator.rank() << " has subrange " << localRange << '\n';
const int sum = TNL::sum( v );
if( communicator.rank() == 0 )
std::cout << "Global sum is " << sum << '\n';
}
int
main( int argc, char* argv[] )
{
TNL::MPI::ScopedInitializer mpi( argc, argv );
if( TNL::MPI::GetRank() == 0 )
std::cout << "The first test runs on CPU ...\n";
distributedVectorExample< TNL::Devices::Host >();
#ifdef __CUDACC__
TNL::MPI::Barrier();
if( TNL::MPI::GetRank() == 0 )
std::cout << "The second test runs on GPU ...\n";
distributedVectorExample< TNL::Devices::Cuda >();
#endif
}
TNL::Containers::DistributedVector
DistributedVector extends DistributedArray with algebraic operations.
Definition DistributedVector.h:25
TNL::MPI::Comm
An RAII wrapper for custom MPI communicators.
Definition Comm.h:63
TNL::MPI::Comm::size
int size() const
Returns the size of the group associated with a communicator.
Definition Comm.h:228
TNL::MPI::Comm::rank
int rank() const
Determines the rank of the calling process in the communicator.
Definition Comm.h:221
TNL::Containers::splitRange
Subrange< Index > splitRange(Index rangeBegin, Index rangeEnd, int rank, int num_subintervals)
A helper function which splits a one-dimensional range.
Definition BlockPartitioning.h:27
TNL::MPI::ScopedInitializer
Definition ScopedInitializer.h:63

The output looks as:

Rank 3 detected 0 GPUs.
Rank 3 has subrange TNL::Containers::Subrange<int>( 73, 97 )
Rank 0 detected 0 GPUs.
The first test runs on CPU ...
Rank 0 has subrange TNL::Containers::Subrange<int>( 0, 25 )
Rank 2 detected 0 GPUs.
Rank 2 has subrange TNL::Containers::Subrange<int>( 49, 73 )
Rank 1 detected 0 GPUs.
Rank 1 has subrange TNL::Containers::Subrange<int>( 25, 49 )
Global sum is 4656