Skip to content
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

Tpetra: Replace Tpetra_DefaultNode CMake option with Tpetra_DefaultExecutionSpace #56

Closed
mhoemmen opened this issue Dec 11, 2015 · 6 comments
Assignees
Labels
CLOSED_DUE_TO_INACTIVITY Issue or PR has been closed by the GitHub Actions bot due to inactivity. MARKED_FOR_CLOSURE Issue or PR is marked for auto-closure by the GitHub Actions bot. pkg: Tpetra TpetraRF

Comments

@mhoemmen
Copy link
Contributor

@trilinos/tpetra @rppawlo @crtrott

Tpetra will get rid of Node altogether at some point, and tie itself completely to Kokkos' execution and memory spaces. It would make sense to deprecate the Tpetra_DefaultNode CMake option and replace it with Tpetra_DefaultExecutionSpace.

@mhoemmen
Copy link
Contributor Author

This blocks Issue #57.

@mhoemmen mhoemmen added this to the Tpetra: Replace Node with Kokkos space milestone Dec 11, 2015
@mhoemmen mhoemmen self-assigned this Mar 6, 2016
bartlettroscoe added a commit to bartlettroscoe/TriBITS that referenced this issue Apr 3, 2016
This change I believe fixes the logic where TriBITS was enabling packages
listed in TEST_[REQUIRED|OPTIONAL]_PACKAGES even if tests and examples were
not enabled.  See trilinos/Trilinos#56.

This fails a bunch of the TriBITS tests because it now prints what type of
dependnecy ('lib' or 'test/example') the package is.  New tests targeted for
this use case need to be created and the existing failing tests need to be
fixed.
@mhoemmen mhoemmen added the task label Sep 16, 2016
@mhoemmen mhoemmen modified the milestones: Tpetra-backlog, Tpetra: Replace Node with Kokkos space Nov 2, 2016
@mhoemmen
Copy link
Contributor Author

Y'all check this out (I came up with it this morning):

// mfh 29 Nov 2017
// Demonstrate aliases for backwards compatibility
// of Tpetra objects' template parameters.
// Successfully builds with GCC 7.2 (not a typo, there's no "4").

#include <iostream>
#include <type_traits>

// Stub "Node" type.
template<class KokkosDeviceType>
struct WrapperNode {
    // Kokkos-style, tag this class as a node.
    using node = WrapperNode<KokkosDeviceType>;
};

// Stub Kokkos execution spaces.
struct Cuda {
    // Kokkos-style, tag this class as an execution space.
    using execution_space = Cuda;
};
struct OpenMP {
    using execution_space = OpenMP;
};
struct Serial {
    using execution_space = Serial;
};

// Default "Tpetra" template parameters.
typedef double default_scalar_type;
typedef int default_local_ordinal_type;
typedef long long default_global_ordinal_type;
typedef WrapperNode<Serial> default_node_type;

// Was KOKKOS_IMPL_IS_CONCEPT
#define TPETRA_IMPL_IS_CONCEPT( CONCEPT ) \
  template< typename T > struct is_ ## CONCEPT { \
  private: \
    template< typename , typename = std::true_type > struct have : std::false_type {}; \
    template< typename U > struct have<U,typename std::is_same<U,typename U:: CONCEPT >::type> : std::true_type {}; \
  public: \
    enum { value = is_ ## CONCEPT::template have<T>::value }; \
  };

// Define "is_node" concept.
// is_node<T>::value is true if and only if T is a WrapperNode<T> for some T.
TPETRA_IMPL_IS_CONCEPT( node )

// Define "is_execution_space" concept (Kokkos already has this).
TPETRA_IMPL_IS_CONCEPT( execution_space )

namespace Impl {

// The actual implementation of Vector.
// The four template parameters as given are the canonical ones,
// in their canonical order.
// This is how Tpetra declares its objects like Tpetra::Vector or Tpetra::CrsMatrix now.
template<class S = default_scalar_type,
  class LO = default_local_ordinal_type,
  class GO = default_global_ordinal_type,
  class NT = default_node_type>
class Vector {
public:
    static_assert (std::is_integral<LO>::value,
      "LO template parameter must be an integer.");
    static_assert (std::is_integral<GO>::value,
      "GO template parameter must be an integer.");
    static_assert (is_node<NT>::value,
      "NT template parameter must be a WrapperNode<T> for some T.");

    void print (std::ostream& out) const {
        out << "I am a Vector!" << std::endl;
    }
};

// Implementation detail.
// Tpetra will use VectorAlias to get the Impl::Vector specialization
// corresponding to the template parameters of Vector.
// Args are exactly the template parameters of Vector.
// (We may need to allow 5 for backwards compatibility; the last must be bool.)
template<class ... Args>
struct VectorAlias {
    static_assert (sizeof... (Args) < 5,
      "Vector must have no more than 4 template parameters");
};

// Specialization for zero template parameters.
template<>
struct VectorAlias<> {
private:
    using scalar_type = default_scalar_type;
    using local_ordinal_type = default_local_ordinal_type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type = default_node_type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 1 template parameter; must be Scalar.
template<class Scalar>
struct VectorAlias<Scalar> {
private:
    using scalar_type = typename std::decay<Scalar>::type;
    using local_ordinal_type = default_local_ordinal_type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type = default_node_type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Map from NT to the corresponding WrapperNode type.
// NT may be a WrapperNode, a Kokkos::Device,
// or a Kokkos execution or memory space.
template<class NT>
struct NodeType {
    using type =
      typename std::conditional<
        is_node<NT>::value,
        NT,
        typename std::conditional<
          is_execution_space<NT>::value,
          WrapperNode<NT>,
          NT
          >::type
        >::type;
};

// Specialization for 2 template parameters; first is always Scalar.
template<class Scalar, class Second>
struct VectorAlias<Scalar, Second> {
private:
    using scalar_type = typename std::decay<Scalar>::type;

    using second_type = typename std::decay<Second>::type;
    // If Second is an integer, assume that it is the local ordinal type,
    // for backwards compatibility.  Otherwise, assume that it is the node /
    // device / space type.
    using local_ordinal_type =
      typename std::conditional<
        std::is_integral<second_type>::value,
        second_type,
        int>::type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type =
      typename std::conditional<
        std::is_integral<second_type>::value,
        default_node_type,
        typename NodeType<second_type>::type
      >::type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 3 template parameters; first is always Scalar.
template<class Scalar, class Second, class Third>
struct VectorAlias<Scalar, Second, Third> {
private:
    using scalar_type = typename std::decay<Scalar>::type;
    using second_type = typename std::decay<Second>::type;
    using third_type = typename std::decay<Third>::type;

    // Second must be an integer.
    static_assert (std::is_integral<second_type>::value,
      "If you give Tpetra objects three template parameters, "
      "the second (an index type) must be an integer.");

    // Third could be an integer, a Node, or a
    // Kokkos Device / execution space / memory space.
    using global_ordinal_type =
      typename std::conditional<
        std::is_integral<third_type>::value,
        third_type,
        second_type>::type;
    // If the user only gives me one index type,
    // assume that they meant the global index type,
    // and make the local index type the default.
    using local_ordinal_type =
      typename std::conditional<
        std::is_integral<third_type>::value,
        second_type,
        default_local_ordinal_type>::type;
    // If Third is an integer, use the default node_type.
    // Otherwise, use Third to find the node_type.
    using node_type =
      typename std::conditional<
        std::is_integral<third_type>::value,
        default_node_type,
        typename NodeType<third_type>::type
      >::type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 4 template parameters; first is always Scalar.
template<class Scalar, class LO, class GO, class NT>
struct VectorAlias<Scalar, LO, GO, NT> {
private:
    static_assert (std::is_integral<LO>::value,
      "If you give Tpetra objects four template parameters, "
      "the second (the local index type) must be an integer.");
    static_assert (std::is_integral<GO>::value,
      "If you give Tpetra objects four template parameters, "
      "the third (the global index type) must be an integer.");

    using scalar_type = typename std::decay<Scalar>::type;
    using local_ordinal_type = LO;
    using global_ordinal_type = GO;
    using node_type = typename NodeType<NT>::type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

} // namespace Impl

// This is the Vector class that users will see.
// Users put their template parameters into this class.
// Tpetra aliases this to the matching Impl::Vector specialization.
template<class ... Args>
using Vector = typename Impl::VectorAlias<Args...>::type;

int main () {
    // Test is_node concept.
    static_assert (is_node<default_node_type>::value, "Oops, is_node is broken");
    static_assert (! is_node<Serial>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<Serial>>::value, "Oops, is_node is broken");
    static_assert (! is_node<OpenMP>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<OpenMP>>::value, "Oops, is_node is broken");
    static_assert (! is_node<Cuda>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<Cuda>>::value, "Oops, is_node is broken");
    static_assert (! is_node<int>::value, "Oops, is_node is broken");

    // Check that the Vector type alias behaves as expected.
    static_assert (std::is_same<
        Vector<>,
        Impl::Vector<default_scalar_type, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 0 arguments");
    static_assert (std::is_same<
        Vector<float>,
        Impl::Vector<float, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 1 argument");
    static_assert (std::is_same<
        Vector<float, short>,
        Impl::Vector<float, short, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 2 arguments");
    static_assert (std::is_same<
        Vector<float, short, long>,
        Impl::Vector<float, short, long, default_node_type>
      >::value,
      "Oops 3 arguments");
    static_assert (std::is_same<
        Vector<float, short, long, WrapperNode<Cuda>>,
        Impl::Vector<float, short, long, WrapperNode<Cuda>>
      >::value,
      "Oops 4 arguments");

    // Check that we're not instantiating redundant types.
    static_assert (std::is_same<
        Vector<>,
        Vector<default_scalar_type, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, short>,
        Vector<float, short, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, default_global_ordinal_type, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, long, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, short, long>,
        Vector<float, short, long, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, long, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    // Check that execution spaces work in place of Nodes.
    static_assert (std::is_same<
        Vector<float, long, Cuda>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );

    // Uncomment the line below to trigger the static_assert
    // that forbids more than 4 template parameters.
    // Vector<float, short, long, WrapperNode<Cuda>, double> v2;

    // Generate some output, so stuff actually compiles.
    Impl::Vector<double, int, long, WrapperNode<Serial>> v;
    v.print (std::cout);
    return 0;
}

@mhoemmen
Copy link
Contributor Author

Hm, I think I can generalize this for all Tpetra classes that take the same four template parameters, using template templates.

@mhoemmen
Copy link
Contributor Author

I AM LORD OF THE TEMPLATES

#include <iostream>
#include <type_traits>

// Stub "Node" type.
template<class KokkosDeviceType>
struct WrapperNode {
    // Kokkos-style, tag this class as a node.
    using node = WrapperNode<KokkosDeviceType>;
};

// Stub Kokkos execution spaces.
struct Cuda {
    // Kokkos-style, tag this class as an execution space.
    using execution_space = Cuda;
};
struct OpenMP {
    using execution_space = OpenMP;
};
struct Serial {
    using execution_space = Serial;
};

// Default "Tpetra" template parameters.
typedef double default_scalar_type;
typedef int default_local_ordinal_type;
typedef long long default_global_ordinal_type;
typedef WrapperNode<Serial> default_node_type;

// Was KOKKOS_IMPL_IS_CONCEPT
#define TPETRA_IMPL_IS_CONCEPT( CONCEPT ) \
  template< typename T > struct is_ ## CONCEPT { \
  private: \
    template< typename , typename = std::true_type > struct have : std::false_type {}; \
    template< typename U > struct have<U,typename std::is_same<U,typename U:: CONCEPT >::type> : std::true_type {}; \
  public: \
    enum { value = is_ ## CONCEPT::template have<T>::value }; \
  };

// Define "is_node" concept.
// is_node<T>::value is true if and only if T is a WrapperNode<T> for some T.
TPETRA_IMPL_IS_CONCEPT( node )

// Define "is_execution_space" concept (Kokkos already has this).
TPETRA_IMPL_IS_CONCEPT( execution_space )

namespace Impl {

// The actual implementation of Vector.
// The four template parameters as given are the canonical ones,
// in their canonical order.
// This is how Tpetra declares its objects like Tpetra::Vector or Tpetra::CrsMatrix now.
template<class S = default_scalar_type,
  class LO = default_local_ordinal_type, 
  class GO = default_global_ordinal_type, 
  class NT = default_node_type>
class Vector {
public:
    typedef S scalar_type;
    typedef LO local_ordinal_type;
    typedef GO global_ordinal_type;
    typedef NT node_type;

    static_assert (std::is_integral<LO>::value, 
      "LO template parameter must be an integer.");
    static_assert (std::is_integral<GO>::value, 
      "GO template parameter must be an integer.");
    static_assert (is_node<NT>::value, 
      "NT template parameter must be a WrapperNode<T> for some T.");

    void print (std::ostream& out) const {
        out << "I am a Vector!" << std::endl;
    }
};

// Implementation detail.
// Tpetra will use FourArgAlias to get the Impl::Vector specialization
// corresponding to the template parameters of Vector.
// First template parameter will be Impl::Vector.
// Remaining template parameter(s) (if any) Args
// are exactly the template parameters of Vector.
// (We may need to allow 5 for backwards compatibility; the last must be bool.)
template<template <class S, class LO, class GO, class NT> class Object,
        class ... Args>
struct FourArgAlias {
    static_assert (sizeof... (Args) < 5, 
      "This class must have no more than 4 template parameters");
};

// Specialization for zero template parameters.
template<template <class S, class LO, class GO, class NT> class Object>
struct FourArgAlias<Object> {
private:
    using scalar_type = default_scalar_type;
    using local_ordinal_type = default_local_ordinal_type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type = default_node_type;
public:
    using type = Object<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 1 template parameter; must be Scalar.
template<template <class S, class LO, class GO, class NT> class Object,
         class Scalar>
struct FourArgAlias<Object, Scalar> {
private:
    using scalar_type = typename std::decay<Scalar>::type;
    using local_ordinal_type = default_local_ordinal_type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type = default_node_type;
public:
    using type = Object<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Map from NT to the corresponding WrapperNode type.
// NT may be a WrapperNode, a Kokkos::Device, 
// or a Kokkos execution or memory space.
template<class NT>
struct NodeType {
    using type = 
      typename std::conditional<
        is_node<NT>::value, 
        NT, 
        typename std::conditional<
          is_execution_space<NT>::value,
          WrapperNode<NT>,
          NT
          >::type
        >::type;
};

// Specialization for 2 template parameters; first is always Scalar.
template<template <class S, class LO, class GO, class NT> class Object,
         class Scalar, 
         class Second>
struct FourArgAlias<Object, Scalar, Second> {
private:
    using scalar_type = typename std::decay<Scalar>::type;

    using second_type = typename std::decay<Second>::type;
    // If Second is an integer, assume that it is the local ordinal type,
    // for backwards compatibility.  Otherwise, assume that it is the node /
    // device / space type.
    using local_ordinal_type = 
      typename std::conditional<
        std::is_integral<second_type>::value,
        second_type,
        int>::type;
    using global_ordinal_type = default_global_ordinal_type;
    using node_type =
      typename std::conditional<
        std::is_integral<second_type>::value,
        default_node_type,
        typename NodeType<second_type>::type
      >::type;
public:
    using type = Object<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 3 template parameters; first is always Scalar.
template<template <class S, class LO, class GO, class NT> class Object,
         class Scalar, 
         class Second,
         class Third>
struct FourArgAlias<Object, Scalar, Second, Third> {
private:
    using scalar_type = typename std::decay<Scalar>::type;
    using second_type = typename std::decay<Second>::type;
    using third_type = typename std::decay<Third>::type;

    // Second must be an integer.
    static_assert (std::is_integral<second_type>::value, 
      "If you give Tpetra objects three template parameters, "
      "the second (an index type) must be an integer.");

    // Third could be an integer, a Node, or a 
    // Kokkos Device / execution space / memory space.
    using global_ordinal_type = 
      typename std::conditional<
        std::is_integral<third_type>::value, 
        third_type, 
        second_type>::type;
    // If the user only gives me one index type, 
    // assume that they meant the global index type, 
    // and make the local index type the default.
    using local_ordinal_type = 
      typename std::conditional<
        std::is_integral<third_type>::value,
        second_type,
        default_local_ordinal_type>::type;
    // If Third is an integer, use the default node_type.
    // Otherwise, use Third to find the node_type.
    using node_type =
      typename std::conditional<
        std::is_integral<third_type>::value,
        default_node_type,
        typename NodeType<third_type>::type
      >::type;
public:
    using type = Object<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

// Specialization for 4 template parameters; first is always Scalar.
template<template <class S, class LO, class GO, class NT> class Object,
         class Scalar, 
         class Second,
         class Third,
         class Fourth>
struct FourArgAlias<Object, Scalar, Second, Third, Fourth> {
private:
    static_assert (std::is_integral<Second>::value, 
      "If you give Tpetra objects four template parameters, "
      "the second (the local index type) must be an integer.");
    static_assert (std::is_integral<Third>::value, 
      "If you give Tpetra objects four template parameters, "
      "the third (the global index type) must be an integer.");

    using scalar_type = typename std::decay<Scalar>::type;
    using local_ordinal_type = Second;
    using global_ordinal_type = Third;
    using node_type = typename NodeType<Fourth>::type;
public:
    using type = Impl::Vector<scalar_type, local_ordinal_type, global_ordinal_type, node_type>;
};

} // namespace Impl

// This is the Vector class that users will see.
// Users put their template parameters into this class.
// Tpetra aliases this to the matching Impl::Vector specialization. 
//template<class ... Args>
//using Vector = typename Impl::VectorAlias<Args...>::type;

template<class ... Args>
using Vector = typename Impl::FourArgAlias<Impl::Vector, Args...>::type;

int main () {
    // Test is_node concept.
    static_assert (is_node<default_node_type>::value, "Oops, is_node is broken");
    static_assert (! is_node<Serial>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<Serial>>::value, "Oops, is_node is broken");
    static_assert (! is_node<OpenMP>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<OpenMP>>::value, "Oops, is_node is broken");
    static_assert (! is_node<Cuda>::value, "Oops, is_node is broken");
    static_assert (is_node<WrapperNode<Cuda>>::value, "Oops, is_node is broken");
    static_assert (! is_node<int>::value, "Oops, is_node is broken");

    // Check that the Vector type alias behaves as expected.
    static_assert (std::is_same<
        Vector<>,
        Impl::Vector<default_scalar_type, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 0 arguments");
    static_assert (std::is_same<
        Vector<float>,
        Impl::Vector<float, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 1 argument");
    static_assert (std::is_same<
        Vector<float, short>,
        Impl::Vector<float, short, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops 2 arguments");
    static_assert (std::is_same<
        Vector<float, short, long>,
        Impl::Vector<float, short, long, default_node_type>
      >::value,
      "Oops 3 arguments");
    static_assert (std::is_same<
        Vector<float, short, long, WrapperNode<Cuda>>,
        Impl::Vector<float, short, long, WrapperNode<Cuda>>
      >::value,
      "Oops 4 arguments");

    // Check that we're not instantiating redundant types.
    static_assert (std::is_same<
        Vector<>,
        Vector<default_scalar_type, default_local_ordinal_type, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, short>,
        Vector<float, short, default_global_ordinal_type, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, default_global_ordinal_type, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, long, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, short, long>,
        Vector<float, short, long, default_node_type>
      >::value,
      "Oops not the same type"
    );
    static_assert (std::is_same<
        Vector<float, long, WrapperNode<Cuda>>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );
    // Check that execution spaces work in place of Nodes.
    static_assert (std::is_same<
        Vector<float, long, Cuda>,
        Vector<float, default_local_ordinal_type, long, WrapperNode<Cuda>>
      >::value,
      "Oops not the same type"
    );

    // Uncomment the line below to trigger the static_assert
    // that forbids more than 4 template parameters.
    // Vector<float, short, long, WrapperNode<Cuda>, double> v2;

    // Generate some output, so stuff actually compiles.
    Impl::Vector<double, int, long, WrapperNode<Serial>> v;
    v.print (std::cout);
    return 0;
}

@github-actions
Copy link

This issue has had no activity for 365 days and is marked for closure. It will be closed after an additional 30 days of inactivity.
If you would like to keep this issue open please add a comment and remove the MARKED_FOR_CLOSURE label.
If this issue should be kept open even with no activity beyond the time limits you can add the label DO_NOT_AUTOCLOSE.

@github-actions github-actions bot added the MARKED_FOR_CLOSURE Issue or PR is marked for auto-closure by the GitHub Actions bot. label Dec 15, 2020
@github-actions
Copy link

This issue was closed due to inactivity for 395 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLOSED_DUE_TO_INACTIVITY Issue or PR has been closed by the GitHub Actions bot due to inactivity. MARKED_FOR_CLOSURE Issue or PR is marked for auto-closure by the GitHub Actions bot. pkg: Tpetra TpetraRF
Projects
Status: Done
Development

No branches or pull requests

2 participants