Skip to content

Commit 1b7008b

Browse files
authored
File-backed mmap for XNNPACK packed weights (#19862)
Differential Revision: D106673663 Pull Request resolved: #19862
1 parent 9e394da commit 1b7008b

7 files changed

Lines changed: 327 additions & 23 deletions

File tree

backends/xnnpack/runtime/XNNPACKBackend.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ class XnnpackBackend final
9898
weights_cache_mutex_, std::defer_lock);
9999
if (use_weight_cache) {
100100
lock_weights_cache.lock();
101+
102+
const auto& cache_path = options_.get_packed_cache_path();
103+
if (!cache_path.empty()) {
104+
weights_cache_->set_packed_cache_path(cache_path);
105+
}
106+
101107
weights_cache_->initialize_for_runtime(
102108
context.get_runtime_allocator(), named_data_map);
103109
workspace->set_uses_weight_cache();

backends/xnnpack/runtime/XNNPACKBackend.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ const char workspace_sharing_mode_option_key[] = "workspace_sharing_mode";
1313
// across delegate instances. Changes only affect subsequently loaded models.
1414
const char weight_cache_option_key[] = "weight_cache_enabled";
1515

16+
/// Path for the packed weight file. When set, reserve_space() allocates from
17+
/// a MAP_SHARED file instead of heap; msync makes pages clean on iOS.
18+
// Must remain a C array (not const char*) so it can bind to the
19+
// BackendOptions::set_option(const char (&)[N], ...) template overloads.
20+
// @lint-ignore CLANGTIDY facebook-hte-CArray
21+
const char packed_cache_path_option_key[] = "packed_cache_path";
22+
1623
/// Workspace sharing mode. This is a backend option that can be set via the
1724
/// set_option API to control memory sharing between CALL_DELEGATE instances.
1825
/// This is useful for reducing memory consumption.

backends/xnnpack/runtime/XNNWeightsCache.cpp

Lines changed: 161 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
#include <executorch/backends/xnnpack/runtime/XNNWeightsCache.h>
1010
#include <executorch/runtime/core/error.h>
1111
#include <executorch/runtime/core/memory_allocator.h>
12+
#ifndef _WIN32
13+
#include <fcntl.h>
14+
#include <sys/file.h>
15+
#include <sys/mman.h>
1216
#include <sys/stat.h>
17+
#include <unistd.h>
18+
#include <cerrno>
19+
#endif
1320
#include <xnnpack.h>
1421
#include <exception>
1522
#include <memory>
@@ -41,13 +48,63 @@ XNNWeightsCache::XNNWeightsCache() {
4148
(enum xnn_status(*)(void*))XNNWeightsCache::delete_cache;
4249
}
4350

51+
XNNWeightsCache::~XNNWeightsCache() {
52+
#ifndef _WIN32
53+
for (auto& region : mmap_regions_) {
54+
if (region.addr != nullptr && region.addr != MAP_FAILED) {
55+
munmap(region.addr, region.size);
56+
}
57+
}
58+
mmap_regions_.clear();
59+
if (packed_file_fd_ >= 0) {
60+
close(packed_file_fd_);
61+
packed_file_fd_ = -1;
62+
}
63+
#endif
64+
}
65+
4466
Error XNNWeightsCache::initialize_for_runtime(
4567
MemoryAllocator* runtime_allocator,
4668
const NamedDataMap* named_data_map) {
4769
runtime_allocator_ = runtime_allocator;
4870
named_data_map_ = named_data_map;
4971
is_finalized_ = false;
5072

73+
#ifndef _WIN32
74+
// Open the file for packed weights. Each reserve_space() call
75+
// independently mmaps a region of the file. Once packed_file_disabled_
76+
// is set we never re-open — re-opening with O_TRUNC would corrupt any
77+
// still-live mappings into the same path and cause SIGBUS on access.
78+
if (!packed_cache_path_.empty() && packed_file_fd_ < 0 &&
79+
!packed_file_disabled_) {
80+
packed_file_fd_ =
81+
open(packed_cache_path_.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0600);
82+
if (packed_file_fd_ < 0) {
83+
ET_LOG(
84+
Error,
85+
"Failed to open packed weight file: %s (errno=%d)",
86+
packed_cache_path_.c_str(),
87+
errno);
88+
} else if (flock(packed_file_fd_, LOCK_EX | LOCK_NB) != 0) {
89+
// Another XNNWeightsCache instance (this process or another) is
90+
// already using this path. O_TRUNC above would corrupt its mappings.
91+
// Disable mmap for this instance to prevent collision; fall back to
92+
// heap allocation for the remainder of this cache's lifetime.
93+
ET_LOG(
94+
Error,
95+
"Another instance is using packed weight cache file %s (errno=%d); "
96+
"disabling mmap path",
97+
packed_cache_path_.c_str(),
98+
errno);
99+
close(packed_file_fd_);
100+
packed_file_fd_ = -1;
101+
packed_file_disabled_ = true;
102+
} else {
103+
ET_LOG(Info, "Opened packed weight file: %s", packed_cache_path_.c_str());
104+
}
105+
}
106+
#endif
107+
51108
return Error::Ok;
52109
}
53110

@@ -73,6 +130,26 @@ Result<std::vector<std::string>> XNNWeightsCache::finalize_for_runtime() {
73130
}
74131
}
75132

133+
#ifndef _WIN32
134+
// Schedule async flush for newly added regions only.
135+
// MS_ASYNC returns immediately; OS flushes in the background.
136+
if (mmap_regions_.size() > mmap_regions_synced_) {
137+
size_t new_count = mmap_regions_.size() - mmap_regions_synced_;
138+
for (size_t i = mmap_regions_synced_; i < mmap_regions_.size(); ++i) {
139+
if (mmap_regions_[i].addr != nullptr) {
140+
msync(mmap_regions_[i].addr, mmap_regions_[i].size, MS_ASYNC);
141+
}
142+
}
143+
mmap_regions_synced_ = mmap_regions_.size();
144+
ET_LOG(
145+
Info,
146+
"Scheduled async flush: %zu new regions (%zu total), %zu MB packed weights",
147+
new_count,
148+
mmap_regions_.size(),
149+
packed_file_used_ / (1024 * 1024));
150+
}
151+
#endif
152+
76153
return packed_data_names;
77154
}
78155

@@ -111,12 +188,30 @@ Error XNNWeightsCache::delete_packed_data(
111188
entry->second.ref_count--;
112189
if (entry->second.ref_count == 0) {
113190
void* packed_data_ptr = packed_data_ptrs_[entry->second.offset];
114-
// Erase the key/value from the map frees the pointer holding the packed
115-
// data
191+
// Erase the key/value from the map frees the pointer holding the
192+
// packed data. No-op on the file-backed mmap path, where the
193+
// container is not populated.
116194
packed_pointer_to_container_.erase(packed_data_ptr);
117-
// remove the pointer from the packed_data_ptrs_
195+
#ifndef _WIN32
196+
// File-backed mmap path: munmap the region so VM and page-cache
197+
// usage is released, not just retained until cache destruction.
198+
// The vector slot is set to nullptr below so existing offsets remain
199+
// valid for any concurrent lookups.
200+
auto region_it = file_ptr_to_region_index_.find(packed_data_ptr);
201+
if (region_it != file_ptr_to_region_index_.end()) {
202+
size_t idx = region_it->second;
203+
MmapRegion& region = mmap_regions_[idx];
204+
if (region.addr != nullptr && region.addr != MAP_FAILED) {
205+
munmap(region.addr, region.size);
206+
region.addr = nullptr;
207+
region.size = 0;
208+
}
209+
file_ptr_to_region_index_.erase(region_it);
210+
}
211+
#endif
212+
// Remove the pointer from packed_data_ptrs_.
118213
packed_data_ptrs_[entry->second.offset] = nullptr;
119-
// Erase the name to packed metadata entry
214+
// Erase the name to packed metadata entry.
120215
name_to_packed_data_metadata_.erase(entry->first);
121216
}
122217
}
@@ -158,38 +253,80 @@ size_t XNNWeightsCache::look_up(
158253
return packed_weight_entry->second.offset;
159254
}
160255

161-
/**
162-
* Reserve space in the weight cache for n bytes of weight data, aligned to
163-
* context->kPackedAllocationAlignment. This function will return nullptr if
164-
* the allocation fails.
165-
*/
166256
void* XNNWeightsCache::reserve_space(XNNWeightsCache* context, size_t n) {
167-
// MemoryAllocator* allocator = context->runtime_allocator_;
168-
// void* reserved_pointer = allocator->allocate(n,
169-
// context->kPackedAllocationAlignment);
257+
#ifndef _WIN32
258+
if (context->packed_file_fd_ >= 0) {
259+
size_t page_size = sysconf(_SC_PAGESIZE);
260+
size_t file_offset =
261+
(context->packed_file_used_ + page_size - 1) & ~(page_size - 1);
262+
size_t map_size = (n + page_size - 1) & ~(page_size - 1);
263+
264+
if (ftruncate(context->packed_file_fd_, file_offset + map_size) != 0) {
265+
ET_LOG(
266+
Error,
267+
"ftruncate to %zu failed (errno=%d)",
268+
file_offset + map_size,
269+
errno);
270+
close(context->packed_file_fd_);
271+
context->packed_file_fd_ = -1;
272+
// Existing mmap_regions_ still reference this inode. Disable the
273+
// file-backed path permanently so a future initialize_for_runtime
274+
// doesn't re-open + O_TRUNC the same path and trigger SIGBUS on the
275+
// stale mappings.
276+
context->packed_file_disabled_ = true;
277+
return context->reserve_space_heap(n);
278+
}
170279

171-
// return reserved_pointer;
280+
void* ptr = mmap(
281+
nullptr,
282+
map_size,
283+
PROT_READ | PROT_WRITE,
284+
MAP_SHARED,
285+
context->packed_file_fd_,
286+
file_offset);
287+
if (ptr == MAP_FAILED) {
288+
ET_LOG(Error, "mmap %zu bytes failed (errno=%d)", map_size, errno);
289+
close(context->packed_file_fd_);
290+
context->packed_file_fd_ = -1;
291+
context->packed_file_disabled_ = true;
292+
return context->reserve_space_heap(n);
293+
}
294+
295+
// mmap returns page-aligned (>= 4 KiB), which trivially satisfies the
296+
// 64-byte kPackedAllocationAlignment XNNPACK expects. Assert defensively.
297+
ET_DCHECK_MSG(
298+
(reinterpret_cast<uintptr_t>(ptr) % kPackedAllocationAlignment) == 0,
299+
"mmap returned ptr not aligned to %zu bytes",
300+
kPackedAllocationAlignment);
301+
302+
context->packed_file_used_ = file_offset + map_size;
303+
context->file_ptr_to_region_index_[ptr] = context->mmap_regions_.size();
304+
context->mmap_regions_.push_back({ptr, map_size});
305+
return ptr;
306+
}
307+
#endif
308+
309+
return context->reserve_space_heap(n);
310+
}
311+
312+
void* XNNWeightsCache::reserve_space_heap(size_t n) {
172313
try {
173314
std::string data_container;
174-
size_t raw_allocation_size = n + context->kPackedAllocationAlignment - 1;
315+
size_t raw_allocation_size = n + kPackedAllocationAlignment - 1;
175316
data_container.resize(raw_allocation_size);
176317

177318
void* maybe_aligned_space = data_container.data();
178319
void* aligned_space = std::align(
179-
context->kPackedAllocationAlignment,
320+
kPackedAllocationAlignment,
180321
n,
181322
maybe_aligned_space,
182323
raw_allocation_size // Note that std::align mutates this value.
183324
);
184325
ET_CHECK_MSG(aligned_space != nullptr, "Memory alignment failed.");
185326

186-
context->packed_pointer_to_container_[aligned_space] =
187-
std::move(data_container);
327+
packed_pointer_to_container_[aligned_space] = std::move(data_container);
188328
return aligned_space;
189329
} catch (std::bad_alloc& e) {
190-
// XNNPACK can gracefully handle allocation failures, so return nullptr.
191-
// We want to be able to recover from a failed attempt to load a large
192-
// model without a crash.
193330
ET_LOG(
194331
Error,
195332
"XNN weight cache failed to allocate %zu bytes: %s.",
@@ -267,6 +404,10 @@ enum xnn_status XNNWeightsCache::delete_cache(XNNWeightsCache* context) {
267404
return xnn_status_success;
268405
}
269406

407+
void XNNWeightsCache::set_packed_cache_path(const std::string& path) {
408+
packed_cache_path_ = path;
409+
}
410+
270411
} // namespace delegate
271412
} // namespace xnnpack
272413
} // namespace backends

backends/xnnpack/runtime/XNNWeightsCache.h

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ struct PackedDataMeta {
4141
class XNNWeightsCache {
4242
public:
4343
XNNWeightsCache();
44+
~XNNWeightsCache();
45+
46+
// Owns OS resources (file descriptor, mmap regions). Non-copyable,
47+
// non-movable. cppcoreguidelines-special-member-functions.
48+
XNNWeightsCache(const XNNWeightsCache&) = delete;
49+
XNNWeightsCache& operator=(const XNNWeightsCache&) = delete;
50+
XNNWeightsCache(XNNWeightsCache&&) = delete;
51+
XNNWeightsCache& operator=(XNNWeightsCache&&) = delete;
4452

4553
/**
4654
* Initializes the XNNWeightsCache for the next xnn_create_runtime
@@ -73,29 +81,31 @@ class XNNWeightsCache {
7381
*/
7482
inline size_t get_num_unpacked_data() {
7583
return unpacked_data_.size();
76-
};
84+
}
7785

7886
/**
7987
* Returns the names of all unpacked data
8088
*/
8189
inline std::vector<std::string> get_unpacked_data_names() {
8290
std::vector<std::string> names;
91+
names.reserve(unpacked_data_to_name_.size());
8392
for (const auto& pair : unpacked_data_to_name_) {
8493
names.push_back(pair.second);
8594
}
8695
return names;
87-
};
96+
}
8897

8998
/**
9099
* Returns the packed data names
91100
*/
92101
inline std::vector<std::string> get_packed_data_names() {
93102
std::vector<std::string> names;
103+
names.reserve(name_to_packed_data_metadata_.size());
94104
for (const auto& pair : name_to_packed_data_metadata_) {
95105
names.push_back(pair.first);
96106
}
97107
return names;
98-
};
108+
}
99109

100110
/**
101111
* Loads unpacked named data from the NamedDataMap into this XNNWeightsCache
@@ -115,6 +125,19 @@ class XNNWeightsCache {
115125
*/
116126
Error delete_packed_data(const std::vector<std::string>& packed_names);
117127

128+
/**
129+
* Set the path for the file-backed packed weight storage.
130+
* When set, reserve_space() allocates from a MAP_SHARED file instead
131+
* of heap, and finalize_for_runtime() calls msync to make pages clean.
132+
*
133+
* The path MUST be unique per XNNWeightsCache instance — sharing it
134+
* across instances (or processes) would mean O_TRUNC corrupts the other
135+
* holder's mappings (SIGBUS on access). initialize_for_runtime() takes
136+
* an advisory exclusive flock on the file; if the lock fails the mmap
137+
* path is disabled for this instance and allocations fall back to heap.
138+
*/
139+
void set_packed_cache_path(const std::string& path);
140+
118141
private:
119142
// Runtime Allocator used to reserve memory for packed weights
120143
MemoryAllocator* runtime_allocator_;
@@ -137,6 +160,29 @@ class XNNWeightsCache {
137160
// whether or not the weight cache is finalized
138161
bool is_finalized_;
139162

163+
// File-backed mmap for packed weights. When packed_cache_path_ is set,
164+
// reserve_space() allocates from this mmap'd file instead of heap.
165+
// After msync, pages become clean file-backed → 0 phys_footprint.
166+
//
167+
std::string packed_cache_path_;
168+
int packed_file_fd_{-1};
169+
size_t packed_file_used_{0};
170+
// Set after an unrecoverable mmap/ftruncate failure. Prevents re-opening
171+
// the cache file on subsequent initialize_for_runtime() calls — re-opening
172+
// with O_TRUNC would truncate the inode beneath any still-live mmap pages
173+
// and the next access would raise SIGBUS. Once disabled, all reserve_space
174+
// calls fall back to heap allocation for the lifetime of this cache.
175+
bool packed_file_disabled_{false};
176+
struct MmapRegion {
177+
void* addr;
178+
size_t size;
179+
};
180+
std::vector<MmapRegion> mmap_regions_;
181+
size_t mmap_regions_synced_{0};
182+
// For file-backed packed allocations, maps the returned ptr to its index
183+
// in mmap_regions_, so delete_packed_data() can munmap when ref_count==0.
184+
std::unordered_map<void*, size_t> file_ptr_to_region_index_;
185+
140186
// Function pointers to override XNNPACK's default xnn_weights_cache_provider
141187
// functions.
142188
static size_t look_up(
@@ -145,6 +191,10 @@ class XNNWeightsCache {
145191

146192
static void* reserve_space(XNNWeightsCache* context, size_t n);
147193

194+
// Heap-backed allocation path. Used when the mmap path is not configured
195+
// or has failed for this allocation.
196+
void* reserve_space_heap(size_t n);
197+
148198
static size_t look_up_or_insert(
149199
XNNWeightsCache* context,
150200
const xnn_weights_cache_look_up_key* cache_key,

0 commit comments

Comments
 (0)