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

Host/Device accessors for mdspan #3686

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

fbusato
Copy link
Contributor

@fbusato fbusato commented Feb 5, 2025

@fbusato fbusato added the 3.0 Targeted for 3.0 release label Feb 5, 2025
@fbusato fbusato self-assigned this Feb 5, 2025
Copy link

copy-pr-bot bot commented Feb 5, 2025

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@fbusato
Copy link
Contributor Author

fbusato commented Feb 6, 2025

/ok to test

@fbusato
Copy link
Contributor Author

fbusato commented Feb 7, 2025

/ok to test

@fbusato
Copy link
Contributor Author

fbusato commented Feb 7, 2025

/ok to test

public:
using offset_policy = host_accessor;

_CCCL_HIDE_FROM_ABI host_accessor() noexcept(__is_ctor_noexcept) = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessors are not required to be default constructible. Please see https://eel.is/c++draft/mdspan.accessor.reqmts for the actual requirements, and https://eel.is/c++draft/views.multidim#mdspan.mdspan.cons-1.4 for how mdspan constrains its default constructor on whether its accessor is default constructible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty easy to fix in C++20. I have to remind myself how the work-around works in C++ < 20.

Suggested change
_CCCL_HIDE_FROM_ABI host_accessor() noexcept(__is_ctor_noexcept) = default;
requires(std::is_default_constructible_v<_Accessor>) // needs C++20
_CCCL_HIDE_FROM_ABI host_accessor() noexcept(__is_ctor_noexcept) = default;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to fix it. Without c++20, I cannot introduce a template parameter and use = default.
Also, I don't know what is the best way to use requires before c++20, without introducing a template parameter @miscco

_CCCL_HIDE_FROM_ABI host_accessor() noexcept(__is_ctor_noexcept) = default;

_CCCL_HIDE_FROM_ABI host_accessor(const host_accessor&) noexcept(__is_copy_ctor_noexcept) = default;

Copy link
Contributor

@mhoemmen mhoemmen Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A key feature of any accessor is the set of permitted conversions, both implicit and explicit. mdspan depends on accessors' conversions in order for its own conversions to work. Implicit conversions make mdspan assignment work; explicit conversions make explicit construction of an mdspan of one type from an mdspan of another type work. Even default_accessor has a conversion to permit assigning an mdspan of nonconst to an mdspan of const.

An accessor's conversions are defined by its constructors and/or conversion operators. Not exposing those from the parent class means that mdspan of host_accessor<Accessor> won't be able to perform any of the conversions that mdspan of Accessor can perform.

A public using _Accessor::_Accessor; declaration would pull in all of the base class' constructors, but it wouldn't pull in any conversion operators.

Another issue is that _Accessor might have a virtual destructor. That would be weird, but it's legal in the Standard and nothing in this class prevents it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section 5.8 of P2897R7 elaborates the design intent behind accessor conversions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A public using _Accessor::_Accessor; declaration would pull in all of the base class' constructors, but it wouldn't pull in any conversion operators.

For example, if Accessor A provides a conversion operator to B and a conversion operator to C, the only way to provide those in device_accessor<A> would be to use reflection to enumerate all conversion operators of A, and paste new ones into device_accessor<A> for device_accessor<B> and device_accessor<C>. While I'm not a C++ reflection expert, I don't think even P2996 reflection can do that, as it would require injecting member function definitions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I entirely missed that part. I added the constructors following the aligned_accessor model.

@fbusato
Copy link
Contributor Author

fbusato commented Feb 10, 2025

/ok to test

@fbusato fbusato requested a review from mhoemmen February 11, 2025 20:43
@fbusato
Copy link
Contributor Author

fbusato commented Feb 14, 2025

/ok to test

Copy link
Contributor

@mhoemmen mhoemmen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greetings and thanks for asking for feedback! : - ) I've added some comments.

Comment on lines +124 to +140
_CCCL_TEMPLATE(typename _NotUsed = void)
_CCCL_REQUIRES(_CCCL_TRAIT(_CUDA_VSTD::is_default_constructible, _Accessor))
_LIBCUDACXX_HIDE_FROM_ABI __host_accessor() noexcept(__is_ctor_noexcept)
: _Accessor{}
{}

_CCCL_TEMPLATE(typename _OtherElementType)
_CCCL_REQUIRES(_CCCL_TRAIT(_CUDA_VSTD::is_convertible, _OtherElementType (*)[], element_type (*)[]))
_LIBCUDACXX_HIDE_FROM_ABI constexpr __host_accessor(__host_accessor<_OtherElementType>) noexcept(
__is_copy_ctor_noexcept)
{}

_CCCL_TEMPLATE(typename _OtherElementType)
_CCCL_REQUIRES(_CCCL_TRAIT(_CUDA_VSTD::is_convertible, _OtherElementType (*)[], element_type (*)[]))
_LIBCUDACXX_HIDE_FROM_ABI constexpr __host_accessor(__managed_accessor<_OtherElementType>) noexcept(
__is_copy_ctor_noexcept)
{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous review, I mentioned the issue that this omits all the conversions to and from _Accessor that the _Accessor class defines. I just want to mention it again so I don't forget : - ) . Conversions are an important part of an accessor's interface, because they help define conversions for mdspan itself. For example, without default_accessor's converting constructor, you can't assign mdspan-of-nonconst to mdspan-of-const. Section 5.8 of P2897 elaborates this explanation.

Should we consider instead a CRTP base class approach to __*_accessor? That would let developers of custom accessors opt into the run-time pointer checking functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be that you reviewed an older version? I previously added conversions from/to default_accessor for host_accessor and device_accessor.
Honestly, I'm was sure about managed_accessor because it looks unsafe...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider instead a CRTP base class approach to __*_accessor? That would let developers of custom accessors opt into the run-time pointer checking functionality.

the idea is nice. I'm a bit concerned about the implications for users. Could host_accessor be roughly defined in the following way?

class host_accessor : public __host_accessor<cuda::std::default_accessor> { .. };

Copy link
Contributor

@mhoemmen mhoemmen Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fbusato wrote:

could be that you reviewed an older version? I previously added conversions from/to default_accessor for host_accessor and device_accessor.

The issue is with transitive conversions for a possibly generic _Accessor.

For example, suppose that I define two accessors A and B. A is weaker (more type-erased) than B. A defines an implicit conversion from B, and B defines an explicit conversion from A. (This follows the idiom that explicit conversions assert preconditions, while implicit conversions have no preconditions.) One example of this case is A = default_accessor<float>, and B = aligned_accessor<float, 16>.

struct A {
  // ... other accessor stuff ...
  A(B) {}
};

struct B {
  // ... other accessor stuff ...
  explicit B(A) {}
};

The problem is, these conversions don't carry over to {host,device,managed}_accessor. For example, host_accessor<A> does not have an implicit conversion from host_accessor<B>, and host_accessor<B> does not have an explicit conversion from host_accessor<A>.

Here's a draft for how to fix this. I would need to prototype this a bit to make sure it works.

template<class _Accessor>
class host_accessor {
private:
  _Accessor acc_;

  // ... details ...
public:
  // ... other constructors ...

  // This case covers host_accessor<default_accessor<OtherElementType>> as well.
  template<class InputAccessor>
  // If _Accessor is constructible from InputAccessor, then
  // host_accessor<_Accessor> is constructible from
  // host_accessor<InputAccessor>.
  requires(is_constructible<_Accessor, const InputAccessor&>)
  constexpr
  // Conversion is explicit if there is no implicit conversion
  // from InputAccessor to _Accessor.
  explicit(! is_convertible_v<const InputAccessor&, _Accessor>)
  host_accessor(const host_accessor<InputAccessor>& input_acc)
    : acc_(input_acc)
  {}
};

Copy link
Contributor

@mhoemmen mhoemmen Feb 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fbusato I just sent you an offline message with a link to an example of how to do this correctly. wrapper (see below) is like host_accessor or device_accessor -- an accessor that wraps another accessor. The link explains the design and includes tests and use examples.

template<class Accessor>
requires(not is_wrapper_v<Accessor>)
class wrapper : public Accessor {
public:
  using offset_policy = typename Accessor::offset_policy;
  using element_type = typename Accessor::element_type;
  using reference = typename Accessor::reference;
  using data_handle_type = typename Accessor::data_handle_type;

  constexpr wrapper() noexcept 
    requires(std::is_default_constructible_v<Accessor>)
  = default;

  // "Wrapping constructor" -- takes the inner accessor.
  constexpr wrapper(const Accessor& input_acc)
    : Accessor(input_acc)
  {}

  template<class OtherAccessor>
  requires(
    std::is_constructible_v<Accessor, OtherAccessor>
  )
  constexpr
  explicit(
    not std::is_convertible_v<OtherAccessor, Accessor>
  )
  wrapper(const wrapper<OtherAccessor>& input_acc)
    : Accessor(input_acc)
  {}

  constexpr reference
  access(data_handle_type p, size_t k) const {
    return Accessor::access(p, k);
  }

  constexpr typename offset_policy::data_handle_type
  offset(data_handle_type p, size_t k) const {
    return Accessor::offset(p, k);
  }
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.0 Targeted for 3.0 release
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

4 participants