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

[RFC] Add tests and rudimentary protections for default-constructed PortableCollections #44844

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions DataFormats/Common/interface/DeviceProduct.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <cassert>
#include <memory>
#include <optional>

namespace edm {
class DeviceProductBase {
Expand Down Expand Up @@ -49,7 +50,7 @@ namespace edm {

template <typename M, typename... Args>
explicit DeviceProduct(std::shared_ptr<M> metadata, Args&&... args)
: DeviceProductBase(std::move(metadata)), data_(std::forward<Args>(args)...) {}
: DeviceProductBase(std::move(metadata)), data_(std::in_place_t{}, std::forward<Args>(args)...) {}

DeviceProduct(const DeviceProduct&) = delete;
DeviceProduct& operator=(const DeviceProduct&) = delete;
Expand All @@ -66,11 +67,14 @@ namespace edm {
T const& getSynchronized(Args&&... args) const {
auto const& md = metadata<M>();
md.synchronize(std::forward<Args>(args)...);
return data_;
return *data_;
}

private:
T data_; //!
// Using optional to allow T to not have a default constructor
// Default-constructed DeviceProduct should in any case appear
// only when edm::Wrapper::present == false
std::optional<T> data_; //!
};
} // namespace edm
#endif
4 changes: 4 additions & 0 deletions DataFormats/Common/test/BuildFile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@
<bin file="StdArray_t.cpp">
<use name="catch2"/>
</bin>

<bin file="DeviceProduct_t.cpp">
<use name="catch2"/>
</bin>
70 changes: 70 additions & 0 deletions DataFormats/Common/test/DeviceProduct_t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

#include "DataFormats/Common/interface/DeviceProduct.h"

namespace {
class Metadata {
public:
Metadata(int v = 0) : value_(v) {}
void synchronize(int& ret) const { ret = value_; }

private:
int const value_;
};

class Product {
public:
Product() = default;
Product(int v) : value_(v) {}

int value() const { return value_; }

private:
int value_ = 0;
};

class ProductNoDefault {
public:
ProductNoDefault(int v) : value_(v) {}

int value() const { return value_; }

private:
int value_;
};

} // namespace

TEST_CASE("DeviceProduct", "[DeviceProduct]") {
SECTION("Default constructor") { [[maybe_unused]] edm::DeviceProduct<int> prod; }

SECTION("Default-constructible data product") {
SECTION("Default constructed") {
edm::DeviceProduct<Product> prod(std::make_shared<Metadata>(3));

int md = 0;
auto const& data = prod.getSynchronized<Metadata>(md);
REQUIRE(md == 3);
REQUIRE(data.value() == 0);
}

SECTION("Explicitly constructed") {
edm::DeviceProduct<Product> prod(std::make_shared<Metadata>(4), 42);

int md = 0;
auto const& data = prod.getSynchronized<Metadata>(md);
REQUIRE(md == 4);
REQUIRE(data.value() == 42);
}
}

SECTION("Non-default-constructible data product") {
edm::DeviceProduct<ProductNoDefault> prod(std::make_shared<Metadata>(5), 314);

int md = 0;
auto const& data = prod.getSynchronized<Metadata>(md);
REQUIRE(md == 5);
REQUIRE(data.value() == 314);
}
}
1 change: 1 addition & 0 deletions DataFormats/Portable/interface/PortableCollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ namespace cms::alpakatools {
template <typename TQueue>
static auto copyAsync(TQueue& queue, PortableHostCollection<TLayout> const& srcData) {
using TDevice = typename alpaka::trait::DevType<TQueue>::type;
assert(srcData.isValid());
PortableDeviceCollection<TLayout, TDevice> dstData(srcData->metadata().size(), queue);
alpaka::memcpy(queue, dstData.buffer(), srcData.buffer());
return dstData;
Expand Down
28 changes: 16 additions & 12 deletions DataFormats/Portable/interface/PortableDeviceCollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,21 @@ class PortableDeviceCollection {
using Buffer = cms::alpakatools::device_buffer<TDev, std::byte[]>;
using ConstBuffer = cms::alpakatools::const_device_buffer<TDev, std::byte[]>;

PortableDeviceCollection() = default;

PortableDeviceCollection(int32_t elements, TDev const& device)
: buffer_{cms::alpakatools::make_device_buffer<std::byte[]>(device, Layout::computeDataSize(elements))},
layout_{buffer_->data(), elements},
layout_{buffer_.data(), elements},
view_{layout_} {
// Alpaka set to a default alignment of 128 bytes defining ALPAKA_DEFAULT_HOST_MEMORY_ALIGNMENT=128
assert(reinterpret_cast<uintptr_t>(buffer_->data()) % Layout::alignment == 0);
assert(reinterpret_cast<uintptr_t>(buffer_.data()) % Layout::alignment == 0);
}

template <typename TQueue, typename = std::enable_if_t<alpaka::isQueue<TQueue>>>
PortableDeviceCollection(int32_t elements, TQueue const& queue)
: buffer_{cms::alpakatools::make_device_buffer<std::byte[]>(queue, Layout::computeDataSize(elements))},
layout_{buffer_->data(), elements},
layout_{buffer_.data(), elements},
view_{layout_} {
// Alpaka set to a default alignment of 128 bytes defining ALPAKA_DEFAULT_HOST_MEMORY_ALIGNMENT=128
assert(reinterpret_cast<uintptr_t>(buffer_->data()) % Layout::alignment == 0);
assert(reinterpret_cast<uintptr_t>(buffer_.data()) % Layout::alignment == 0);
}

// non-copyable
Expand All @@ -54,6 +52,12 @@ class PortableDeviceCollection {
// default destructor
~PortableDeviceCollection() = default;

// has a valid buffer?
bool isValid() const { return true; }

// a way to access the number of elements without the buffer validity assertion
auto size() const { return view_.metadata().size(); }

// access the View
View& view() { return view_; }
ConstView const& view() const { return view_; }
Expand All @@ -66,14 +70,14 @@ class PortableDeviceCollection {
ConstView const* operator->() const { return &view_; }

// access the Buffer
Buffer buffer() { return *buffer_; }
ConstBuffer buffer() const { return *buffer_; }
ConstBuffer const_buffer() const { return *buffer_; }
Buffer buffer() { return buffer_; }
ConstBuffer buffer() const { return buffer_; }
ConstBuffer const_buffer() const { return buffer_; }

private:
std::optional<Buffer> buffer_; //!
Layout layout_; //
View view_; //!
Buffer buffer_; //!
Layout layout_; //
View view_; //!
};

// generic SoA-based product in device memory
Expand Down
86 changes: 74 additions & 12 deletions DataFormats/Portable/interface/PortableHostCollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "DataFormats/Portable/interface/PortableCollectionCommon.h"

// generic SoA-based product in host memory
//
// Moving from PortableHostCollection leaves the source object in a
// "not isValid()" state that has 0 size
template <typename T>
class PortableHostCollection {
public:
Expand Down Expand Up @@ -47,27 +50,86 @@ class PortableHostCollection {
PortableHostCollection& operator=(PortableHostCollection const&) = delete;

// movable
PortableHostCollection(PortableHostCollection&&) = default;
PortableHostCollection& operator=(PortableHostCollection&&) = default;
PortableHostCollection(PortableHostCollection&& other):
buffer_(std::move(other.buffer_)),
layout_(std::move(other.layout_)),
view_(std::move(other.view_))
{
other.buffer_.reset();
Copy link
Contributor

Choose a reason for hiding this comment

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

... and I just learned something new 🤷🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... and I just learned something new 🤷🏻

Just curious, what was that?

other.view_ = View();
}
PortableHostCollection& operator=(PortableHostCollection&& other) {
// Protection for self-assignment without if-statements, following
// what was suggested in the C++ core guidelines
{
auto tmp = std::move(other.buffer_);
other.buffer_.reset();
buffer_.reset();
Copy link
Contributor

@fwyzard fwyzard Sep 2, 2024

Choose a reason for hiding this comment

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

Is this reset() necessary ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this reset() necessary ?

I'd think (now) it would not be necessary, and I was just following the pattern in an example https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-move-self (that uses raw pointers where the delete in the assigned-to-object's member is necessary). I'd expect the assignment on the following line

buffer_ = std::move(tmp);

to work equally well in both cases of self-assignment and assignment from a different object. Any existing value in buffer_ (be it a valid Buffer or a moved-from Buffer) should get destructed first before initializing the content of buffer_ from tmp.

buffer_ = std::move(tmp);
}
{
auto tmp = std::move(other.view_);
other.view_ = View();
view_ = View();
view_ = std::move(tmp);
}
Comment on lines +70 to +75
Copy link
Contributor

Choose a reason for hiding this comment

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

it should (to be verified) safe to do just

Suggested change
{
auto tmp = std::move(other.view_);
other.view_ = View();
view_ = View();
view_ = std::move(tmp);
}
{
view_ = other.view_; // self-assignment is safe
other.view_ = View();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

however, what about the layout_ ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it should (to be verified) safe to do just

This is because View contains only pointers and fundamental types? Right, then I'd also expect the self-assignment to be safe.

however, what about the layout_ ?

"Whoops", I guess, thanks for catching. Then I need to think a test that would demonstrate it failing.

return *this;
}

// default destructor
~PortableHostCollection() = default;

// has a valid buffer?
bool isValid() const { return buffer_.has_value(); }

// a way to access the number of elements without the buffer validity assertion
auto size() const { return view_.metadata().size(); }

// access the View
View& view() { return view_; }
ConstView const& view() const { return view_; }
ConstView const& const_view() const { return view_; }
View& view() {
assert(isValid());
return view_;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively the View accessors could be left unchecked, and require the users to explicitly check isValid() or/and the column/scalar accessors of the View to be non-nullptr when it is not clearly guaranteed that the PortableCollection is non-default constructed.

(we could also throw an exception instead of assert(), but maybe the use of default-constructed PortableCollection could be more of a logic error rather than something that would depend e.g. on the data?)

ConstView const& view() const {
assert(isValid());
return view_;
}
ConstView const& const_view() const {
assert(isValid());
return view_;
}

View& operator*() { return view_; }
ConstView const& operator*() const { return view_; }
View& operator*() {
assert(isValid());
return view_;
}
ConstView const& operator*() const {
assert(isValid());
return view_;
}

View* operator->() { return &view_; }
ConstView const* operator->() const { return &view_; }
View* operator->() {
assert(isValid());
return &view_;
}
ConstView const* operator->() const {
assert(isValid());
return &view_;
}

// access the Buffer
Buffer buffer() { return *buffer_; }
ConstBuffer buffer() const { return *buffer_; }
ConstBuffer const_buffer() const { return *buffer_; }
Buffer buffer() {
assert(isValid());
return *buffer_;
}
ConstBuffer buffer() const {
assert(isValid());
return *buffer_;
}
ConstBuffer const_buffer() const {
assert(isValid());
return *buffer_;
}

// part of the ROOT read streamer
static void ROOTReadStreamer(PortableHostCollection* newObj, Layout& layout) {
Expand Down
9 changes: 9 additions & 0 deletions DataFormats/Portable/test/BuildFile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
<use name="DataFormats/SoATemplate"/>
<use name="catch2"/>
</bin>

<bin name="TestDataFormatsPortableCollection" file="alpaka/test_catch2_*.cc">
<use name="DataFormats/Portable"/>
<use name="DataFormats/SoATemplate"/>
<use name="HeterogeneousCore/AlpakaInterface"/>
<use name="alpaka"/>
<use name="catch2"/>
<flags ALPAKA_BACKENDS="1"/>
</bin>
2 changes: 2 additions & 0 deletions DataFormats/Portable/test/alpaka/test_catch2_main.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#define CATCH_CONFIG_MAIN
#include <catch.hpp>
Loading