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

Q: howto create std::mdspan<T> from std::span<std::span<T>>? #221

Closed
RalphSteinhagen opened this issue Dec 15, 2022 · 5 comments
Closed

Comments

@RalphSteinhagen
Copy link

RalphSteinhagen commented Dec 15, 2022

Hi, first thanks bringing multi-dimensional spans into the C++ standards and for providing this implementation. Much appreciated. 👍

I am evaluating whether we could simplify our user API by aggregating multiple std::span<T> into one std::[experimental::]span<T>. For the use-case with a single backing STL container this seems to be fairly simple, i.e.

    // backing data type -- just for illustration
    std::vector val = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
    std::vector err = {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0};

    // interface returning following spans
    std::span<double> val_span{val};  // first span
    std::span<double> val_err{err};   // second span

    auto wio = std::experimental::mdspan(val_span.data(), 1, 10);
    print_stream("wio", wio);

but how would this look like if I want to aggregate two std::span<T>, e.g.

    std::vector vector_of_spans = {val_span, val_err};
    auto span_of_span = std::span{vector_of_spans};

    // N.B. 1st extent is fixed, 2nd extent is dynamic
    auto wio2 = std::experimental::mdspan(span_of_span, 2, 10); // <- fails here, how could this be done?
    print_stream("wio2", wio2);

I tried to overload

template <typename T>
struct default_accessor<std::span<std::span<T>>> { /*...*/ }

without much success: https://godbolt.org/z/creYGGdK8

Guess it's probably trivial once you know how ... but I don't. Would you be able to give me some pointers/hints?

Any help would be much appreciated. Thanks in advance!

@mhoemmen
Copy link
Contributor

@RalphSteinhagen thanks so much for your interest in mdspan!

... but how would this look like if I want to aggregate two std::span<T> ...

I would not recommend mdspan for this case. It could work, but it would violate the spirit of the design by forcing you to couple the layout and the accessor.

mdspan's design

An mdspan is just three things: a data handle, a layout mapping, and an accessor. The layout mapping maps (i, j) to a 1-D integer offset k, and the accessor maps the mdspan's data handle (e.g., pointer) and the offset k to a reference. wio[i, j] just means wio.accessor().access(wio.data_handle(), wio.mapping(i, j)). You can't customize an mdspan to do anything different; all customization comes through the layout and accessor (which defines the data handle type).

Note that the accessor does not see (i, j). It only sees the offset k, a 1-D offset. This strongly suggests a design that decouples the layout mapping and the accessor as much as possible. It's not in the spirit of the design to use a "side channel" to pass information from the layout mapping to the accessor on every array access. Examples of "side channels" include some encoding of information into the offset (e.g., k = (i << 32) ^ j)), the accessor knowing the layout mapping (e.g., holding a reference to it) and "inverting" it to get (i, j) back from k, the layout mapping actually knowing the data handle, or some (not at all thread-safe) hidden state in the accessor that the layout mapping would modify.

Custom layout and accessor

It's possible to write an custom layout and accessor pair that could make this work, but this would not follow the spirit of the design, because it would require a side channel between the layout mapping and the accessor.

The accessor policy requirements tell us what type aliases and (member) functions a custom accessor must have. The data_handle_type alias defines the "pointer" type that the accessor's access function sees. A span-of-spans doesn't necessarily view a contiguous block of memory. Thus, data_handle_type would have to be the span-of-spans itself.

That part is fine. The issue is with the access function. The span-of-spans can't use an offset k. It needs (i, j) to access an element. In order to get (i, j) from k, the accessor would have to invert the layout mapping. For example, if the accessor knows the extents and knows that the layout is row major and exhaustive (e.g., layout_right), the accessor could compute i = k / extent(1) and j = k % extent(1). However, that's an example of "side channel" communication between the accessor and the layout mapping. The point of having those two things be separate is that you could view the same data with different layouts. Coupling the accessor and layout mapping would prevent that.

Segmented span

One could attempt to work around this by making the data handle itself decode the offset. This would amount to a "segmented span." For example, if the outer span outer has extent is M and the inner spans all have the same extent N, the segmented span's operator[](size_t k) could map the 1-D offset k into i = k / N and j = k % N, then return outer[i][j].

However, this approach just moves the side channel from the accessor into the data handle. mdspan gets data_handle_type from the accessor; the data handle is part of the accessor. General segmented spans can still be useful in mdspan, but it's against the spirit of the design for the segmented span to encode the layout.

What should you do?

It sounds like what you want to do is imbue the span-of-spans with mdspan's interface. If that's what you want, then you could just write a wrapper that provides, e.g., reference operator[](size_t i, size_t j) const. If you wanted to write code that could take either this wrapper or an mdspan, you could template on the view type and constrain based on the common interface of the wrapper and mdspan. I don't have time at the moment to write that constraint, but it's more or less just defining what properties you would want this "common interface" to have.

@RalphSteinhagen
Copy link
Author

@mhoemmen thanks for the detailed, very educative, and well-thought-out answer! This is much appreciated!

It sounds like what you want to do is imbue the span-of-spans with mdspan's interface. [..] just write a wrapper that provides, e.g., reference operator[](size_t i, size_t j) const

That was exactly my intent. We have some applications where the error propagation boils down to matrix-vector multiplications, but this isn't it. The solution you pointed out is much easier, simpler, and cleaner for the intended use case (i.e. expose aggregate spans of values and corresponding measurement uncertainty) ... I like it!
On second thought, maybe -- with your reasoning above -- for both efficiency and readability reasons it may be better to expose both spans via properly named accessors.

Thanks a lot for your time and recommendation! Again, this is much appreciated! 👍

@RalphSteinhagen
Copy link
Author

... and @mhoemmen I owe you a ☕ or 🍺 in case we ever meet in person!

@mhoemmen
Copy link
Contributor

@RalphSteinhagen Glad to help out! : - )

... for both efficiency and readability reasons it may be better to expose both spans via properly named accessors.

struct ComputationResult {
  std::span<double> values;
  std::span<double> errors;
};

or if you want to insist that values and errors have the same size:

class ComputationResult {
public:
  ComputationResult() = default;
  ComputationResult(std::span<double> the_values, std::span<double> the_errors) : 
    values_(the_values.data()), errors_(the_errors.data()), size_(the_values.size())
  {
    assert(values.size() == errors.size()); // constructor precondition
  }
  std::span<const double> values() const {
    return {values_, size_};
  }
  std::span<const double> errors() const {
    return {errors_, size_};
  }

private:
  double* values_ = nullptr;
  double* errors_ = nullptr;
  size_t size_ = 0;
};

@RalphSteinhagen
Copy link
Author

@mhoemmen yes, that's we actually decided for as a follow-up of your earlier comment.

Sometimes one (/I) just needs a gentle nudge towards ... "you can do better".

Thanks for that!

mhoemmen added a commit to mhoemmen/mdspan that referenced this issue Jul 26, 2023
kokkos: add in-place overwriting `triangular_matrix_{left,right}_product`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants