diff --git a/devtools/etdump/buffer_data_sink.cpp b/devtools/etdump/buffer_data_sink.cpp new file mode 100644 index 00000000000..08bac801ef2 --- /dev/null +++ b/devtools/etdump/buffer_data_sink.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +using ::executorch::runtime::Error; +using ::executorch::runtime::Result; + +namespace executorch { +namespace etdump { + +Result BufferDataSink::write(const void* ptr, size_t length) { + if (length == 0) { + return offset_; + } + + uint8_t* last_data_end = debug_buffer_.data() + offset_; + + // The beginning of the next data blob must be aligned to the alignment + uint8_t* cur_data_begin = internal::align_pointer(last_data_end, alignment_); + uint8_t* cur_data_end = cur_data_begin + length; + + if (cur_data_end > debug_buffer_.data() + debug_buffer_.size()) { + ET_LOG(Error, "Ran out of space to store intermediate outputs."); + return Error::OutOfResources; + } + + // Zero out the padding between data blobs + memset(last_data_end, 0, cur_data_begin - last_data_end); + memcpy(cur_data_begin, ptr, length); + offset_ = (size_t)(cur_data_end - debug_buffer_.data()); + + return (size_t)(cur_data_begin - debug_buffer_.data()); +} + +Result BufferDataSink::get_storage_size() const { + return debug_buffer_.size(); +} + +size_t BufferDataSink::get_used_bytes() const { + return offset_; +} + +} // namespace etdump +} // namespace executorch diff --git a/devtools/etdump/buffer_data_sink.h b/devtools/etdump/buffer_data_sink.h new file mode 100644 index 00000000000..522203443d4 --- /dev/null +++ b/devtools/etdump/buffer_data_sink.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace executorch { +namespace etdump { + +/** + * BufferDataSink is a concrete implementation of the DataSinkBase class, + * designed to store debug data in a pre-allocated, user-owned buffer. This + * class provides methods to write raw data and tensor data into the buffer, + * ensuring proper alignment and managing padding as needed. + */ +class BufferDataSink : public DataSinkBase { + public: + /** + * Constructs a BufferDataSink with a given buffer. + * + * @param[in] buffer A Span object representing the buffer where data will be + * stored. + * @param[in] alignment The alignment requirement for the buffer. It must be + * a power of two. Default is 64. + */ + explicit BufferDataSink( + ::executorch::runtime::Span buffer, + size_t alignment = 64) + : debug_buffer_(buffer), offset_(0), alignment_(alignment) {} + + // Uncopiable and unassignable to avoid double assignment and free of the + // internal buffer. + BufferDataSink(const BufferDataSink&) = delete; + BufferDataSink& operator=(const BufferDataSink&) = delete; + + // Movable to be compatible with Result. + BufferDataSink(BufferDataSink&&) = default; + BufferDataSink& operator=(BufferDataSink&&) = default; + + ~BufferDataSink() override = default; + + /** + * Write data into the debug buffer and return the offset of the starting + * location of the data within the buffer. + * + * @param[in] ptr A pointer to the data to be written into the storage. + * @param[in] size The size of the data in bytes. + * @return A Result object containing either: + * - The offset of the starting location of the data within the + * debug buffer, or + * - An error code indicating the failure reason, if any issue + * occurs during the write process. + */ + ::executorch::runtime::Result write(const void* ptr, size_t size) + override; + + /** + * Retrieves the total size of the buffer. + * + * @return A Result object containing the total size of the buffer in bytes. + */ + ::executorch::runtime::Result get_storage_size() const; + + /** + * Retrieves the number of bytes currently used in the buffer. + * + * @return The amount of data currently stored in the buffer in bytes. + */ + size_t get_used_bytes() const override; + + private: + // A Span object representing the buffer used for storing debug data. + ::executorch::runtime::Span debug_buffer_; + + // The offset of the next available location in the buffer. + size_t offset_; + + // The alignment of the buffer. + size_t alignment_; +}; + +} // namespace etdump +} // namespace executorch diff --git a/devtools/etdump/data_sink_base.h b/devtools/etdump/data_sink_base.h new file mode 100644 index 00000000000..602c249ce9d --- /dev/null +++ b/devtools/etdump/data_sink_base.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace executorch { +namespace etdump { + +/** + * DataSinkBase is an abstract class that users can inherit and implement + * to customize the storage and management of debug data in ETDumpGen. This + * class provides a basic and essential interface for writing datablob to a + * user-defined storage, retrieving storage capacity, and tracking the amount of + * data stored. + */ +class DataSinkBase { + public: + /** + * Virtual destructor to ensure proper cleanup of derived classes. + */ + + virtual ~DataSinkBase() = default; + /** + * Write data into the debug storage. This method should be implemented + * by derived classes to handle the specifics of data storage. + * + * This function should return the offset of the starting location of the + * data within the debug storage if the write operation succeeds, or an + * Error code if any issue occurs during the write process. + * + * @param[in] ptr A pointer to the data to be written into the storage. + * @param[in] length The size of the data in bytes. + * @return A Result object containing either: + * - The offset of the starting location of the data within the + * debug storage, which will be recorded in the corresponding + * metadata of ETDump, or + * - An error code indicating the failure reason, if any issue + * occurs during the write process. + */ + virtual ::executorch::runtime::Result write( + const void* ptr, + size_t length) = 0; + + /** + * Get the number of bytes currently used in the debug storage. + * + * @return The amount of data currently stored in bytes. + */ + virtual size_t get_used_bytes() const = 0; +}; + +} // namespace etdump +} // namespace executorch diff --git a/devtools/etdump/etdump_flatcc.cpp b/devtools/etdump/etdump_flatcc.cpp index a34b5188c53..ec52621a956 100644 --- a/devtools/etdump/etdump_flatcc.cpp +++ b/devtools/etdump/etdump_flatcc.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -94,16 +95,6 @@ etdump_Tensor_ref_t add_tensor_entry( return etdump_Tensor_end(builder_); } -static uint8_t* alignPointer(void* ptr, size_t alignment) { - intptr_t addr = reinterpret_cast(ptr); - if ((addr & (alignment - 1)) == 0) { - // Already aligned. - return reinterpret_cast(ptr); - } - addr = (addr | (alignment - 1)) + 1; - return reinterpret_cast(addr); -} - } // namespace // Constructor implementation @@ -113,9 +104,10 @@ ETDumpGen::ETDumpGen(Span buffer) { // Initialize the flatcc builder_ using the buffer and buffer size. if (buffer.data() != nullptr) { - builder_ = (struct flatcc_builder*)alignPointer(buffer.data(), 64); - uintptr_t buffer_with_builder = - (uintptr_t)alignPointer(builder_ + sizeof(struct flatcc_builder), 64); + builder_ = + (struct flatcc_builder*)internal::align_pointer(buffer.data(), 64); + uintptr_t buffer_with_builder = (uintptr_t)internal::align_pointer( + builder_ + sizeof(struct flatcc_builder), 64); size_t builder_size = (size_t)(buffer_with_builder - (uintptr_t)buffer.data()); size_t min_buf_size = max_alloc_buf_size + builder_size; @@ -513,7 +505,7 @@ size_t ETDumpGen::copy_tensor_to_debug_buffer(executorch::aten::Tensor tensor) { return static_cast(-1); } uint8_t* offset_ptr = - alignPointer(debug_buffer_.data() + debug_buffer_offset_, 64); + internal::align_pointer(debug_buffer_.data() + debug_buffer_offset_, 64); debug_buffer_offset_ = (offset_ptr - debug_buffer_.data()) + tensor.nbytes(); ET_CHECK_MSG( debug_buffer_offset_ <= debug_buffer_.size(), diff --git a/devtools/etdump/targets.bzl b/devtools/etdump/targets.bzl index bf4807aa442..1d762e983c8 100644 --- a/devtools/etdump/targets.bzl +++ b/devtools/etdump/targets.bzl @@ -87,8 +87,54 @@ def define_common_targets(): exported_external_deps = ["flatccrt"], ) + runtime.cxx_library( + name = "utils", + srcs = [], + exported_headers = [ + "utils.h", + ], + visibility = [ + + ], + ) + for aten_mode in get_aten_mode_options(): aten_suffix = "_aten" if aten_mode else "" + + runtime.cxx_library( + name = "data_sink_base" + aten_suffix, + exported_headers = [ + "data_sink_base.h", + ], + exported_deps = [ + "//executorch/runtime/core/exec_aten/util:scalar_type_util" + aten_suffix, + ], + visibility = [ + "//executorch/...", + "@EXECUTORCH_CLIENTS", + ], + ) + + runtime.cxx_library( + name = "buffer_data_sink" + aten_suffix, + headers = [ + "buffer_data_sink.h", + ], + srcs = [ + "buffer_data_sink.cpp", + ], + deps = [ + ":utils", + ], + exported_deps = [ + "//executorch/runtime/core/exec_aten:lib" + aten_suffix, + ":data_sink_base" + aten_suffix, + ], + visibility = [ + "//executorch/...", + "@EXECUTORCH_CLIENTS", + ], + ) runtime.cxx_library( name = "etdump_flatcc" + aten_suffix, srcs = [ @@ -106,6 +152,7 @@ def define_common_targets(): ], exported_deps = [ ":etdump_schema_flatcc", + ":utils", "//executorch/runtime/core:event_tracer" + aten_suffix, "//executorch/runtime/core/exec_aten/util:scalar_type_util" + aten_suffix, ], diff --git a/devtools/etdump/tests/buffer_data_sink_test.cpp b/devtools/etdump/tests/buffer_data_sink_test.cpp new file mode 100644 index 00000000000..984f7776300 --- /dev/null +++ b/devtools/etdump/tests/buffer_data_sink_test.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include + +using namespace ::testing; +using ::executorch::aten::ScalarType; +using ::executorch::aten::Tensor; +using ::executorch::runtime::Error; +using ::executorch::runtime::Result; +using ::executorch::runtime::Span; +using torch::executor::testing::TensorFactory; + +class BufferDataSinkTest : public ::testing::Test { + protected: + void SetUp() override { + torch::executor::runtime_init(); + // Allocate a small buffer for testing + buffer_size_ = 128; // Small size for testing + buffer_ptr_ = malloc(buffer_size_); + buffer_ = Span(static_cast(buffer_ptr_), buffer_size_); + data_sink_ = std::make_unique(buffer_); + } + + void TearDown() override { + free(buffer_ptr_); + } + + size_t buffer_size_; + void* buffer_ptr_; + Span buffer_; + std::unique_ptr data_sink_; +}; + +TEST_F(BufferDataSinkTest, StorageSizeCheck) { + Result ret = data_sink_->get_storage_size(); + ASSERT_EQ(ret.error(), Error::Ok); + + size_t storage_size = ret.get(); + EXPECT_EQ(storage_size, buffer_size_); +} + +TEST_F(BufferDataSinkTest, WriteOneTensorAndCheckData) { + TensorFactory tf; + Tensor tensor = tf.make({1, 4}, {1.0, 2.0, 3.0, 4.0}); + + Result ret = + data_sink_->write(tensor.const_data_ptr(), tensor.nbytes()); + ASSERT_EQ(ret.error(), Error::Ok); + + size_t offset = ret.get(); + + EXPECT_NE(offset, static_cast(-1)); + + // Check that the data in the buffer matches the tensor data + const float* buffer_data = + reinterpret_cast(buffer_.data() + offset); + for (size_t i = 0; i < tensor.numel(); ++i) { + EXPECT_EQ(buffer_data[i], tensor.const_data_ptr()[i]); + } +} + +TEST_F(BufferDataSinkTest, WriteMultiTensorsAndCheckData) { + TensorFactory tf; + std::vector tensors = { + tf.make({1, 4}, {1.0, 2.0, 3.0, 4.0}), + tf.make({1, 4}, {5.0, 6.0, 7.0, 8.0})}; + for (const auto& tensor : tensors) { + Result ret = + data_sink_->write(tensor.const_data_ptr(), tensor.nbytes()); + ASSERT_EQ(ret.error(), Error::Ok); + + size_t offset = ret.get(); + EXPECT_NE(offset, static_cast(-1)); + // Check that the data in the buffer matches the tensor data + const float* buffer_data = + reinterpret_cast(buffer_.data() + offset); + for (size_t i = 0; i < tensor.numel(); ++i) { + EXPECT_EQ(buffer_data[i], tensor.const_data_ptr()[i]); + } + } +} + +TEST_F(BufferDataSinkTest, PointerAlignmentCheck) { + TensorFactory tf; + Tensor tensor = tf.make({1, 4}, {1.0, 2.0, 3.0, 4.0}); + Result ret = + data_sink_->write(tensor.const_data_ptr(), tensor.nbytes()); + ASSERT_EQ(ret.error(), Error::Ok); + + size_t offset = ret.get(); + EXPECT_NE(offset, static_cast(-1)); + // Check that the offset pointer is 64-byte aligned + const uint8_t* offset_ptr = buffer_.data() + offset; + EXPECT_EQ(reinterpret_cast(offset_ptr) % 64, 0); +} + +TEST_F(BufferDataSinkTest, WriteUntilOverflow) { + TensorFactory tf; + Tensor tensor = tf.zeros({1, 8}); // Large tensor to fill the buffer + + // Write tensors until we run out of space + for (size_t i = 0; i < 2; i++) { + Result ret = + data_sink_->write(tensor.const_data_ptr(), tensor.nbytes()); + ASSERT_EQ(ret.error(), Error::Ok); + } + + // Attempting to write another tensor should raise an error + Result ret = + data_sink_->write(tensor.const_data_ptr(), tensor.nbytes()); + ASSERT_EQ(ret.error(), Error::OutOfResources); +} diff --git a/devtools/etdump/tests/targets.bzl b/devtools/etdump/tests/targets.bzl index 5299b7c1cb7..c91267ff467 100644 --- a/devtools/etdump/tests/targets.bzl +++ b/devtools/etdump/tests/targets.bzl @@ -19,3 +19,14 @@ def define_common_targets(): "//executorch/runtime/core/exec_aten/testing_util:tensor_util", ], ) + + runtime.cxx_test( + name = "buffer_data_sink_test", + srcs = [ + "buffer_data_sink_test.cpp", + ], + deps = [ + "//executorch/devtools/etdump:buffer_data_sink", + "//executorch/runtime/core/exec_aten/testing_util:tensor_util", + ], + ) diff --git a/devtools/etdump/utils.h b/devtools/etdump/utils.h new file mode 100644 index 00000000000..8f9a78a1f99 --- /dev/null +++ b/devtools/etdump/utils.h @@ -0,0 +1,32 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include + +#pragma once + +namespace executorch { +namespace etdump { +namespace internal { + +/** + * Aligns a pointer to the next multiple of `alignment`. + * + * @param[in] ptr Pointer to align. + * @param[in] alignment Alignment to align to. Must be a power of 2. + * + * @returns A pointer aligned to `alignment`. + */ +inline uint8_t* align_pointer(void* ptr, size_t alignment) { + intptr_t addr = reinterpret_cast(ptr); + if ((addr & (alignment - 1)) == 0) { + // Already aligned. + return reinterpret_cast(ptr); + } + addr = (addr | (alignment - 1)) + 1; + return reinterpret_cast(addr); +} + +} // namespace internal +} // namespace etdump +} // namespace executorch diff --git a/runtime/core/error.h b/runtime/core/error.h index 7fbd92b7c08..73e343a5c45 100644 --- a/runtime/core/error.h +++ b/runtime/core/error.h @@ -82,6 +82,9 @@ enum class Error : error_code_t { /// Error caused by the contents of external data. InvalidExternalData = 0x24, + /// Does not have enough resources to perform the requested operation. + OutOfResources = 0x25, + /* * Delegate errors. */