Skip to content

Commit 75aa0b4

Browse files
dbortfacebook-github-bot
authored andcommitted
MmapDataLoader can handle non-page-aligned offsets (#1975)
Summary: Pull Request resolved: #1975 Instead of requiring that load offsets are aligned to the size of the OS page, map in the set of pages that overlap the requested offset + size. This will let us load .pte files with 4096-aligned segments onto iOS devices, which use 16384-byte OS pages. It also means that we could reduce the segment alignment to `max(delegate_alignment, constant_tensor_alignment)` to reduce the size of .pte files. Reviewed By: shoumikhin Differential Revision: D53772818 fbshipit-source-id: d08cef5345d1abaad060a6b40cd0db18a7f1b94b
1 parent f739212 commit 75aa0b4

File tree

3 files changed

+122
-49
lines changed

3 files changed

+122
-49
lines changed

exir/capture/_config.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ class ExecutorchBackendConfig:
6262
extract_constant_segment: bool = True
6363

6464
# When extracting segments, the starting offset of each segment will be
65-
# aligned to this value (in bytes). When using mmap() to load segments, this
66-
# should be a multiple of the OS page size.
65+
# aligned to this value (in bytes). Must be a power of two.
6766
segment_alignment: int = 4096
6867

6968
# If provided, the minimum alignment of tensor buffers in the program. Must

extension/data_loader/mmap_data_loader.cpp

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ namespace torch {
2626
namespace executor {
2727
namespace util {
2828

29+
namespace {
30+
31+
struct Range {
32+
// Address or offset.
33+
uintptr_t start;
34+
// Size in bytes.
35+
size_t size;
36+
};
37+
38+
/**
39+
* Given an address region, returns the start offset and byte size of the set of
40+
* pages that completely covers the region.
41+
*/
42+
Range get_overlapping_pages(uintptr_t offset, size_t size, size_t page_size) {
43+
size_t page_mask = ~(page_size - 1);
44+
// The address of the page that starts at or before the beginning of the
45+
// region.
46+
uintptr_t start = offset & page_mask;
47+
// The address of the page that starts after the end of the region.
48+
uintptr_t end = (offset + size + ~page_mask) & page_mask;
49+
return {
50+
/*start=*/start,
51+
/*size=*/static_cast<size_t>(end - start),
52+
};
53+
}
54+
55+
} // namespace
56+
2957
MmapDataLoader::~MmapDataLoader() {
3058
// file_name_ can be nullptr if this instance was moved from, but freeing a
3159
// null pointer is safe.
@@ -41,7 +69,7 @@ Result<MmapDataLoader> MmapDataLoader::from(
4169
// Cache the page size.
4270
long page_size = sysconf(_SC_PAGESIZE);
4371
if (page_size < 0) {
44-
ET_LOG(Error, "Could not get page size: %s (%d)", strerror(errno), errno);
72+
ET_LOG(Error, "Could not get page size: %s (%d)", ::strerror(errno), errno);
4573
return Error::AccessFailed;
4674
}
4775
if ((page_size & ~(page_size - 1)) != page_size) {
@@ -53,7 +81,11 @@ Result<MmapDataLoader> MmapDataLoader::from(
5381
int fd = ::open(file_name, O_RDONLY);
5482
if (fd < 0) {
5583
ET_LOG(
56-
Error, "Failed to open %s: %s (%d)", file_name, strerror(errno), errno);
84+
Error,
85+
"Failed to open %s: %s (%d)",
86+
file_name,
87+
::strerror(errno),
88+
errno);
5789
return Error::AccessFailed;
5890
}
5991

@@ -89,9 +121,28 @@ Result<MmapDataLoader> MmapDataLoader::from(
89121
}
90122

91123
namespace {
92-
/// FreeableBuffer::FreeFn-compatible callback.
93-
void MunmapSegment(__ET_UNUSED void* context, void* data, size_t size) {
94-
::munmap(data, size);
124+
/**
125+
* FreeableBuffer::FreeFn-compatible callback.
126+
*
127+
* `context` is actually the OS page size as a uintptr_t.
128+
*/
129+
void MunmapSegment(void* context, void* data, size_t size) {
130+
const uintptr_t page_size = reinterpret_cast<uintptr_t>(context);
131+
132+
Range range =
133+
get_overlapping_pages(reinterpret_cast<uintptr_t>(data), size, page_size);
134+
int ret = ::munmap(reinterpret_cast<void*>(range.start), range.size);
135+
if (ret < 0) {
136+
// Let the user know that something went wrong, but there's nothing we can
137+
// do about it.
138+
ET_LOG(
139+
Error,
140+
"munmap(0x%zx, %zu) failed: %s (ignored)",
141+
(size_t)range.start,
142+
range.size,
143+
::strerror(errno),
144+
errno);
145+
}
95146
}
96147
} // namespace
97148

@@ -109,13 +160,6 @@ Result<FreeableBuffer> MmapDataLoader::Load(size_t offset, size_t size) {
109160
offset,
110161
size,
111162
file_size_);
112-
ET_CHECK_OR_RETURN_ERROR(
113-
(offset & ~(page_size_ - 1)) == offset,
114-
InvalidArgument,
115-
"File %s: offset 0x%zx not aligned to 0x%zx",
116-
file_name_,
117-
offset,
118-
page_size_);
119163
ET_CHECK_OR_RETURN_ERROR(
120164
// Recommended by a lint warning.
121165
offset <= std::numeric_limits<off_t>::max(),
@@ -128,43 +172,64 @@ Result<FreeableBuffer> MmapDataLoader::Load(size_t offset, size_t size) {
128172
return FreeableBuffer(nullptr, 0, /*free_fn=*/nullptr);
129173
}
130174

175+
// Find the range of pages that covers the requested region.
176+
Range range =
177+
get_overlapping_pages(static_cast<uintptr_t>(offset), size, page_size_);
178+
131179
// Map the pages read-only. MAP_PRIVATE vs. MAP_SHARED doesn't matter since
132180
// the data is read-only, but use PRIVATE just to further avoid accidentally
133181
// modifying the file.
134-
void* pages = mmap(
135-
nullptr, size, PROT_READ, MAP_PRIVATE, fd_, static_cast<off_t>(offset));
182+
void* pages = ::mmap(
183+
nullptr,
184+
range.size,
185+
PROT_READ,
186+
MAP_PRIVATE,
187+
fd_,
188+
static_cast<off_t>(range.start));
136189
ET_CHECK_OR_RETURN_ERROR(
137190
pages != MAP_FAILED,
138191
AccessFailed,
139192
"Failed to map %s: mmap(..., size=%zd, ..., fd=%d, offset=0x%zx)",
140193
file_name_,
141-
size,
194+
range.size,
142195
fd_,
143-
offset);
196+
range.start);
144197

145198
if (mlock_config_ == MlockConfig::UseMlock ||
146199
mlock_config_ == MlockConfig::UseMlockIgnoreErrors) {
147-
int err = mlock(pages, size);
200+
int err = ::mlock(pages, size);
148201
if (err < 0) {
149202
ET_LOG(
150203
Error,
151204
"File %s: mlock(%p, %zu) failed: %s (%d)",
152205
file_name_,
153206
pages,
154207
size,
155-
strerror(errno),
208+
::strerror(errno),
156209
errno);
157210
if (mlock_config_ == MlockConfig::UseMlockIgnoreErrors) {
158211
ET_LOG(Info, "Ignoring mlock() error");
159212
} else {
160-
munmap(pages, size);
213+
::munmap(pages, size);
161214
return Error::NotSupported;
162215
}
163216
}
164217
// No need to keep track of this. munmap() will unlock as a side effect.
165218
}
166219

167-
return FreeableBuffer(pages, size, MunmapSegment);
220+
// The requested data is at an offset into the mapped pages.
221+
const void* data = static_cast<const uint8_t*>(pages) + offset - range.start;
222+
223+
return FreeableBuffer(
224+
// The callback knows to unmap the whole pages that encompass this region.
225+
data,
226+
size,
227+
MunmapSegment,
228+
/*free_fn_context=*/
229+
reinterpret_cast<void*>(
230+
// Pass the cached OS page size to the callback so it doesn't need to
231+
// query it again.
232+
static_cast<uintptr_t>(page_size_)));
168233
}
169234

170235
Result<size_t> MmapDataLoader::size() const {

extension/data_loader/test/mmap_data_loader_test.cpp

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,42 @@ void MmapDataLoaderTest::test_in_bounds_loads_succeed(
153153
EXPECT_EQ(fb->size(), size);
154154
EXPECT_EQ(0, std::memcmp(fb->data(), &contents[offset], fb->size()));
155155
}
156+
157+
//
158+
// Unaligned offsets and sizes
159+
//
160+
161+
// Load a single, partial page with an offset that is not a multiple of the
162+
// page size.
163+
{
164+
const size_t offset = 128; // Small power of 2
165+
EXPECT_LT(offset, page_size_);
166+
const size_t size = page_size_ / 2;
167+
Result<FreeableBuffer> fb = mdl->Load(offset, size);
168+
ASSERT_EQ(fb.error(), Error::Ok);
169+
EXPECT_EQ(fb->size(), size);
170+
EXPECT_EQ(0, std::memcmp(fb->data(), &contents[offset], fb->size()));
171+
}
172+
173+
// Load multiple pages from a non-page-aligned but power-of-two offset.
174+
{
175+
const size_t offset = page_size_ + 128; // Small power of 2
176+
const size_t size = page_size_ * 3 + page_size_ / 2 + 1; // Odd size
177+
Result<FreeableBuffer> fb = mdl->Load(offset, size);
178+
ASSERT_EQ(fb.error(), Error::Ok);
179+
EXPECT_EQ(fb->size(), size);
180+
EXPECT_EQ(0, std::memcmp(fb->data(), &contents[offset], fb->size()));
181+
}
182+
183+
// Load multiple pages from an offset that is not a power of 2.
184+
{
185+
const size_t offset = page_size_ * 2 + 3; // Not a power of 2
186+
const size_t size = page_size_ * 3 + page_size_ / 2 + 1; // Odd size
187+
Result<FreeableBuffer> fb = mdl->Load(offset, size);
188+
ASSERT_EQ(fb.error(), Error::Ok);
189+
EXPECT_EQ(fb->size(), size);
190+
EXPECT_EQ(0, std::memcmp(fb->data(), &contents[offset], fb->size()));
191+
}
156192
}
157193

158194
TEST_F(MmapDataLoaderTest, InBoundsLoadsSucceedNoMlock) {
@@ -239,33 +275,6 @@ TEST_F(MmapDataLoaderTest, OutOfBoundsLoadFails) {
239275
}
240276
}
241277

242-
TEST_F(MmapDataLoaderTest, UnalignedOffsetFails) {
243-
// Create a multi-page file; contents don't matter.
244-
const size_t contents_size = 8 * page_size_;
245-
auto contents = std::make_unique<uint8_t[]>(contents_size);
246-
memset(contents.get(), 0x55, contents_size);
247-
TempFile tf(contents.get(), contents_size);
248-
249-
Result<MmapDataLoader> mdl = MmapDataLoader::from(tf.path().c_str());
250-
ASSERT_EQ(mdl.error(), Error::Ok);
251-
252-
// Loading from an unaligned offset should fail, even if the offset is
253-
// a power of 2 and size is a whole page.
254-
{
255-
Result<FreeableBuffer> fb =
256-
mdl->Load(/*offset=*/page_size_ / 2, /*size=*/page_size_);
257-
EXPECT_NE(fb.error(), Error::Ok);
258-
}
259-
260-
// Loading from an unaligned offset should fail, even if the offset is
261-
// a power of 2 and size ends at the next page.
262-
{
263-
Result<FreeableBuffer> fb =
264-
mdl->Load(/*offset=*/page_size_ / 2, /*size=*/page_size_ / 2);
265-
EXPECT_NE(fb.error(), Error::Ok);
266-
}
267-
}
268-
269278
TEST_F(MmapDataLoaderTest, FromMissingFileFails) {
270279
// Wrapping a file that doesn't exist should fail.
271280
Result<MmapDataLoader> mdl = MmapDataLoader::from(

0 commit comments

Comments
 (0)