Skip to content

Commit 4ed9675

Browse files
lucylqfacebook-github-bot
authored andcommitted
Back FreeableBuffer with int64_t (#14570)
Summary: This diff backs FreeableBuffer with an int64_t, instead of a void* ## Usecase PTE file lives on a 32-bit system, where void* is 4 bytes. PTD file lives on a 36-bit system, which requires int64_t to address it. We want to fetch addresses from the ptd file and pass them to accelerator. Executorch APIs return a FreeableBuffer when fetching data (data_loader.h, named_data_map.h). FreeableBuffer is currently backed by a void*, which could truncate a 36-bit address, when on a 32-bit system. Note that we still want existing void* behavior to load segments etc., and only want int64_t behavior when fetching weights from the named_data_map. ## Potential concerns * Increased memory usage; additional 4 bytes for each FreeableBuffer + some extra for the std::variant template. For the PTE file, this is order of the number of segments, which is usually small. * Increased runtime latency; calls to the existing void* API perform truncation checks now. ## Alternatives Why we choose this solution? Seems to be the least intrusive out of a number of solutions. 1. Compiler macro to switch the backing value of FreeableBuffer to int64_t for specific builds. However void* and int64_ are both required - void* for the existing use cases (fetching delegate blobs). Both APIs must exist. 2. Template FreeableBuffer on void* and int64_t. Messy, as the template is contagious; it will need to be applied to data_loader.h, named_data_map.h, and potentially program/method. Imagine this will bloat the core runtime with templating. 3. Store int64_t addresses in FreeableBuffer, and ask user to parse the address to load the data. This avoids changing FreeableBuffer, but is not semantically correct as the API is not returning a buffer of data, but an address that the user must parse and then load data from. 4. Add a specific API to named_data_map.h that returns an int64_t buffer. Not a good use of API surface. https://docs.google.com/document/d/11dMXh1N66rfY-8aO3N-ra2dDPx0RGCoc1rlCSN80Zf8/edit?tab=t.0 Differential Revision: D83007972
1 parent 6531b4a commit 4ed9675

2 files changed

Lines changed: 196 additions & 21 deletions

File tree

runtime/core/freeable_buffer.h

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
#pragma once
1010

1111
#include <cstddef>
12+
#include <cstdint>
13+
#include <variant>
14+
15+
#include <executorch/runtime/core/error.h>
16+
#include <executorch/runtime/core/result.h>
17+
#include <executorch/runtime/platform/assert.h>
1218

1319
namespace executorch {
1420
namespace runtime {
@@ -20,20 +26,34 @@ class FreeableBuffer final {
2026
public:
2127
// Callback signature for the function that does the freeing.
2228
using FreeFn = void (*)(void* context, void* data, size_t size);
29+
using FreeUInt64Fn =
30+
void (*)(void* context, uint64_t data_uint64, size_t size);
31+
32+
private:
33+
struct PointerData {
34+
const void* data_;
35+
FreeFn free_fn_;
36+
};
37+
38+
struct UInt64Data {
39+
// A pointer value cast to uint64_t.
40+
uint64_t data_;
41+
FreeUInt64Fn free_fn_;
42+
};
2343

44+
public:
2445
/**
2546
* Creates an empty FreeableBuffer with size zero and a null data pointer.
2647
*/
2748
FreeableBuffer()
28-
: free_fn_(nullptr),
49+
: data_(PointerData{nullptr, nullptr}),
2950
free_fn_context_(nullptr),
30-
data_(nullptr),
3151
size_(0) {}
3252

3353
/**
3454
* Creates a FreeableBuffer with an optional free function.
3555
*
36-
* @param[in] data The data of the segment.
56+
* @param[in] data The data of the segment, as a void*.
3757
* @param[in] size The size of the segment data, in bytes.
3858
* @param[in] free_fn Optional function to free the data. Guaranteed to be
3959
* called exactly once before the FreeableBuffer is destroyed. May be
@@ -47,23 +67,51 @@ class FreeableBuffer final {
4767
size_t size,
4868
FreeFn free_fn,
4969
void* free_fn_context = nullptr)
50-
: free_fn_(free_fn),
70+
: data_(PointerData{data, free_fn}),
71+
free_fn_context_(free_fn_context),
72+
size_(size) {}
73+
74+
/**
75+
* Creates a FreeableBuffer with an optional free function.
76+
*
77+
* NOTE: most users should use the other ctor with FreeFn.
78+
* This variant exists for situations where the FreeableBuffer points to
79+
* memory on a different core whose pointer value is larger than the local
80+
* core's void*.
81+
*
82+
* @param[in] data Pointer to the data of the segment, cast to a uint64_t
83+
* value.
84+
* @param[in] size The size of the segment data, in bytes.
85+
* @param[in] free_fn Optional function to free the data. Guaranteed to be
86+
* called exactly once before the FreeableBuffer is destroyed. May be
87+
* nullptr. NOTE: This function must be thread-safe. If it modifies common
88+
* state, the function must do its own locking.
89+
* @param[in] free_fn_context Opaque pointer to pass as the `context`
90+
* parameter of `free_fn`. May be nullptr.
91+
*/
92+
explicit FreeableBuffer(
93+
const uint64_t data_uint64,
94+
size_t size,
95+
FreeUInt64Fn free_fn,
96+
void* free_fn_context = nullptr)
97+
: data_(UInt64Data{data_uint64, free_fn}),
5198
free_fn_context_(free_fn_context),
52-
data_(data),
5399
size_(size) {}
54100

55101
/**
56102
* Move ctor. Takes the ownership of the data previously owned by `rhs`,
57103
* leaving `rhs` pointing to nullptr.
58104
*/
59105
FreeableBuffer(FreeableBuffer&& rhs) noexcept
60-
: free_fn_(rhs.free_fn_),
106+
: data_(rhs.data_),
61107
free_fn_context_(rhs.free_fn_context_),
62-
data_(rhs.data_),
63108
size_(rhs.size_) {
64-
rhs.free_fn_ = nullptr;
109+
if (std::holds_alternative<PointerData>(rhs.data_)) {
110+
rhs.data_ = PointerData{nullptr, nullptr};
111+
} else {
112+
rhs.data_ = UInt64Data{0, nullptr};
113+
}
65114
rhs.free_fn_context_ = nullptr;
66-
rhs.data_ = nullptr;
67115
rhs.size_ = 0;
68116
}
69117

@@ -75,11 +123,22 @@ class FreeableBuffer final {
75123
* Frees the data if not already free. Safe to call multiple times.
76124
*/
77125
void Free() {
78-
if (data_ != nullptr) {
79-
if (free_fn_ != nullptr) {
80-
free_fn_(free_fn_context_, const_cast<void*>(data_), size_);
126+
if (std::holds_alternative<PointerData>(data_)) {
127+
PointerData& ptr_data = std::get<PointerData>(data_);
128+
if (ptr_data.data_ != nullptr && ptr_data.free_fn_ != nullptr) {
129+
// Do not need to check for truncation here, as free_fn_ is only set
130+
// using the void* ctor.
131+
ptr_data.free_fn_(
132+
free_fn_context_, const_cast<void*>(ptr_data.data_), size_);
81133
}
82-
data_ = nullptr;
134+
ptr_data.data_ = nullptr;
135+
size_ = 0;
136+
} else {
137+
UInt64Data& int64_data = std::get<UInt64Data>(data_);
138+
if (int64_data.data_ != 0 && int64_data.free_fn_ != nullptr) {
139+
int64_data.free_fn_(free_fn_context_, int64_data.data_, size_);
140+
}
141+
int64_data.data_ = static_cast<uint64_t>(0);
83142
size_ = 0;
84143
}
85144
}
@@ -95,7 +154,23 @@ class FreeableBuffer final {
95154
* Pointer to the data. Returns nullptr if the data has been freed.
96155
*/
97156
const void* data() const {
98-
return data_;
157+
ET_CHECK_MSG(
158+
std::holds_alternative<PointerData>(data_),
159+
"FreeableBuffer is backed by an uint64_t, please use the data_uint64_type() API.");
160+
return std::get<PointerData>(data_).data_;
161+
}
162+
163+
/**
164+
* Data address as a uint64_t. Returns zero if the data has been freed.
165+
* Most users should use data(). data_uint64_type() is only helpful in
166+
* situations where the FreeableBuffer points to memory on a different core
167+
* whose pointer value is larger than the local core's void *.
168+
*/
169+
uint64_t data_uint64_type() const {
170+
ET_CHECK_MSG(
171+
std::holds_alternative<UInt64Data>(data_),
172+
"FreeableBuffer is backed by a void*, please use the data() API.");
173+
return std::get<UInt64Data>(data_).data_;
99174
}
100175

101176
private:
@@ -104,9 +179,15 @@ class FreeableBuffer final {
104179
FreeableBuffer& operator=(FreeableBuffer&& rhs) noexcept = delete;
105180
FreeableBuffer& operator=(const FreeableBuffer& rhs) = delete;
106181

107-
FreeFn free_fn_;
182+
// This stores either a PointerData or a UInt64Data structure. Most users
183+
// should use the PointerData variant and the void* ctor. This creates a
184+
// FreeableBuffer backed by void*, accessed using the void* getter data().
185+
// The UInt64Data variant is only helpful in situations where the
186+
// FreeableBuffer points to memory on a different core whose pointer value
187+
// is larger than the local core's void*.
188+
std::variant<PointerData, UInt64Data> data_;
189+
108190
void* free_fn_context_;
109-
const void* data_;
110191
size_t size_;
111192
};
112193

runtime/core/test/freeable_buffer_test.cpp

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88

99
#include <executorch/runtime/core/freeable_buffer.h>
1010

11+
#include <executorch/test/utils/DeathTest.h>
1112
#include <gtest/gtest.h>
1213

1314
using namespace ::testing;
15+
using executorch::runtime::Error;
1416
using executorch::runtime::FreeableBuffer;
1517

1618
struct FreeCallArgs {
1719
size_t calls;
18-
void* data;
20+
std::variant<const void*, uint64_t> data;
1921
size_t size;
2022
};
2123

@@ -26,6 +28,13 @@ void RecordFree(void* context, void* data, size_t size) {
2628
call->size = size;
2729
}
2830

31+
void RecordInt64Free(void* context, uint64_t data, size_t size) {
32+
auto* call = reinterpret_cast<FreeCallArgs*>(context);
33+
call->calls++;
34+
call->data = data;
35+
call->size = size;
36+
}
37+
2938
TEST(FreeableBufferTest, EmptyTest) {
3039
FreeableBuffer fb;
3140
EXPECT_EQ(fb.data(), nullptr);
@@ -47,6 +56,22 @@ TEST(FreeableBufferTest, DataAndSizeTest) {
4756
fb.Free();
4857
EXPECT_EQ(fb.size(), 0);
4958
EXPECT_EQ(fb.data(), nullptr);
59+
60+
// Use uint64_t constructor.
61+
const uint64_t i64 = 1;
62+
FreeableBuffer fb2(
63+
/*data_uint64=*/i64,
64+
/*size=*/sizeof(i64),
65+
/*free_fn=*/nullptr);
66+
67+
// It should return the ctor params unmodified.
68+
EXPECT_EQ(fb2.size(), sizeof(i64));
69+
EXPECT_EQ(fb2.data_uint64_type(), i64);
70+
71+
// Freeing should clear them, even though free_fn is nullptr.
72+
fb2.Free();
73+
EXPECT_EQ(fb2.size(), 0);
74+
EXPECT_EQ(fb2.data_uint64_type(), 0);
5075
}
5176

5277
TEST(FreeableBufferTest, FreeTest) {
@@ -68,7 +93,7 @@ TEST(FreeableBufferTest, FreeTest) {
6893
// Called once during Free() with the expected data/size.
6994
fb.Free();
7095
EXPECT_EQ(call.calls, 1);
71-
EXPECT_EQ(call.data, &i);
96+
EXPECT_EQ(std::get<const void*>(call.data), &i);
7297
EXPECT_EQ(call.size, sizeof(i));
7398

7499
// A second call to Free() should not call the function again.
@@ -78,6 +103,31 @@ TEST(FreeableBufferTest, FreeTest) {
78103

79104
// The destructor should not have called the function again.
80105
EXPECT_EQ(call.calls, 1);
106+
107+
// Test with uint64_t constructor and free function.
108+
FreeCallArgs call2 = {};
109+
{
110+
uint64_t i64 = 1;
111+
FreeableBuffer fb(
112+
/*data_uint64=*/i64,
113+
/*size=*/sizeof(i64),
114+
/*free_fn=*/RecordInt64Free,
115+
/*free_fn_context=*/&call2);
116+
117+
// Not called during construction.
118+
EXPECT_EQ(call2.calls, 0);
119+
120+
// Called once during Free() with the expected data/size.
121+
fb.Free();
122+
EXPECT_EQ(call2.calls, 1);
123+
EXPECT_EQ(std::get<uint64_t>(call2.data), i64);
124+
EXPECT_EQ(call2.size, sizeof(i64));
125+
126+
// A second call to Free() should not call the function again.
127+
fb.Free();
128+
EXPECT_EQ(call2.calls, 1);
129+
}
130+
EXPECT_EQ(call2.calls, 1);
81131
}
82132

83133
TEST(FreeableBufferTest, DestructorTest) {
@@ -99,7 +149,7 @@ TEST(FreeableBufferTest, DestructorTest) {
99149

100150
// The destructor should have freed the data.
101151
EXPECT_EQ(call.calls, 1);
102-
EXPECT_EQ(call.data, &i);
152+
EXPECT_EQ(std::get<const void*>(call.data), &i);
103153
EXPECT_EQ(call.size, sizeof(i));
104154
}
105155

@@ -127,14 +177,58 @@ TEST(FreeableBufferTest, MoveTest) {
127177
// The destination FreeableBuffer should have the data.
128178
EXPECT_EQ(fb_dst.size(), sizeof(i));
129179
EXPECT_EQ(fb_dst.data(), &i);
130-
131180
// Freeing the source FreeableBuffer should not call the free function.
132181
fb_src.Free();
133182
EXPECT_EQ(call.calls, 0);
134183

135184
// Freeing the destination FreeableBuffer should call the free function.
136185
fb_dst.Free();
137186
EXPECT_EQ(call.calls, 1);
138-
EXPECT_EQ(call.data, &i);
139187
EXPECT_EQ(call.size, sizeof(i));
188+
189+
// Using uint64_t ctor
190+
FreeCallArgs call2 = {};
191+
const uint64_t i64 = 1;
192+
FreeableBuffer fb_src2(
193+
/*data_uint64=*/i64,
194+
/*size=*/sizeof(i64),
195+
/*free_fn=*/RecordInt64Free,
196+
/*free_fn_context=*/&call2);
197+
EXPECT_EQ(fb_src2.size(), sizeof(i64));
198+
EXPECT_EQ(fb_src2.data_uint64_type(), i64);
199+
200+
// Move it into a second FreeableBuffer.
201+
FreeableBuffer fb_dst2(std::move(fb_src2));
202+
203+
// The source FreeableBuffer should now be empty.
204+
EXPECT_EQ(fb_src2.size(), 0); // NOLINT(bugprone-use-after-move)
205+
EXPECT_EQ(fb_src2.data_uint64_type(), 0); // NOLINT(bugprone-use-after-move)
206+
207+
// The destination FreeableBuffer should have the data.
208+
EXPECT_EQ(fb_dst2.size(), sizeof(i64));
209+
EXPECT_EQ(fb_dst2.data_uint64_type(), i64);
210+
// Freeing the source FreeableBuffer should not call the free function.
211+
fb_src2.Free();
212+
EXPECT_EQ(call2.calls, 0);
213+
214+
// Freeing the destination FreeableBuffer should call the free function.
215+
fb_dst2.Free();
216+
EXPECT_EQ(call2.calls, 1);
217+
EXPECT_EQ(call2.size, sizeof(i64));
218+
}
219+
220+
TEST(FreeableBufferTest, APIMisuseDeathTest) {
221+
int i;
222+
FreeableBuffer fb(
223+
/*data=*/&i,
224+
/*size=*/sizeof(i),
225+
/*free_fn=*/nullptr);
226+
ET_EXPECT_DEATH(fb.data_uint64_type(), ".*");
227+
228+
uint64_t i64 = 1;
229+
FreeableBuffer fb2(
230+
/*data_uint64=*/i64,
231+
/*size=*/sizeof(i64),
232+
/*free_fn=*/nullptr);
233+
ET_EXPECT_DEATH(fb2.data(), ".*");
140234
}

0 commit comments

Comments
 (0)