Skip to content
Open
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
12 changes: 12 additions & 0 deletions backends/xnnpack/runtime/XNNPACKBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ class XnnpackBackend final
return first_err;
}

public:
/** See xnnpack::get_packed_cache_report(). */
xnnpack::PackedCacheReport packed_cache_report() const {
return options_.weights_cache_manager().report();
}

private:
mutable xnnpack::XnnpackBackendOptions options_;

Expand All @@ -272,5 +278,11 @@ Backend backend{xnnpack::xnnpack_backend_key, &backend_instance};
static auto success_with_compiler = register_backend(backend);
} // namespace

namespace xnnpack {
PackedCacheReport get_packed_cache_report() {
return backend_instance.packed_cache_report();
}
} // namespace xnnpack

} // namespace backends
} // namespace executorch
115 changes: 115 additions & 0 deletions backends/xnnpack/runtime/XNNPACKBackend.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#pragma once

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

namespace executorch::backends::xnnpack {
/// The key for the backend. This is used to register the backend, check
/// availability, and get/set options.
Expand Down Expand Up @@ -31,6 +36,7 @@
// @lint-ignore CLANGTIDY facebook-hte-CArray
const char save_weight_cache_on_disk_option_key[] = "save_weight_cache_on_disk";


/// Workspace sharing mode. This is a backend option that can be set via the
/// set_option API to control memory sharing between CALL_DELEGATE instances.
/// This is useful for reducing memory consumption.
Expand Down Expand Up @@ -61,4 +67,113 @@
// maximum enum value.
Count,
};

/**
* Whether packed weights ended up in the mmap'd file or on heap.
*
* Reported as telemetry rather than through get_option(): these are runtime
* observations, not settable configuration, and a host needs all of them from
* one consistent instant. Fetching them as separate options would run a
* separate aggregation per key, so the heap/mapped ratio — the whole point of
* the measurement — could be assembled from different moments.
*/
enum class PackedCacheState : int32_t {
/// No cache path configured — the caller never opted in.
Disabled = 0,
/// The cache file opened. Does NOT imply zero heap; see PackedCacheStats.
FileBacked = 1,
/// A path was configured but the file could not be used.
HeapFallback = 2,
};

/** Why an individual allocation was served from heap. */
enum class PackedCacheHeapReason : int32_t {
None = 0,
/// The instance has no cache path: it never opted into file backing, so
/// heap is the intended behaviour rather than a fallback. Bucketed
/// separately and excluded from heap_bytes — a process that mixes an
/// opted-in model with a non-opted-in one would otherwise report the
/// latter's packed weights as if the former had fallen back.
NotOptedIn = 1,
/// Unnamed constant — can never be reloaded by name. By design.
UnnamedConstant = 2,
/// Incidental re-pack after a successful load. By design *if* the loaded
/// cache is complete; a large volume here means it was not.
RepackAfterLoad = 3,
/// No usable file descriptor at allocation time.
NoFileBacking = 4,
/// ftruncate() to extend the file failed.
GrowFailed = 5,
/// mmap() of the grown region failed.
MmapFailed = 6,
/// Not a reason; bounds the per-reason counters. Matches the
/// WorkspaceSharingMode convention in this header.
Count,
};

/** Which step failed when a configured path still ended up on heap. */
enum class PackedCacheFailure : int32_t {
None = 0,
OpenFailed = 1,
TruncateFailed = 2,
GrowFailed = 3,
MmapFailed = 4,
};

/**
* `last_errno` is the errno of the failing syscall — ENOSPC distinguishes
* "device out of disk" from a permissions or path problem. Free space is
* deliberately NOT sampled: the disk-space APIs are on Apple's Required Reason
* list and would force a PrivacyInfo declaration on every iOS consumer of this
* header (cf. the lseek-over-fstat note in XNNWeightsCache::load_packed_cache).
*
* `heap_bytes` against `mapped_bytes` is the load-bearing pair. `state` alone
* calls a partially-loaded cache healthy: the file opens, so it reports
* FileBacked, while every launch re-packs the missing entries into heap. Keep
* `state` anyway — with all byte counters at zero it is the only thing
* separating "feature off" from "cache hit perfectly, nothing to pack".
*/
struct PackedCacheStats {
PackedCacheState state{PackedCacheState::Disabled};
PackedCacheFailure failure{PackedCacheFailure::None};
int32_t last_errno{0};
/// Cache file size as of the last successful save.
int64_t file_bytes{0};
/// Packed bytes served from heap when the file was supposed to serve them.
/// Excludes NotOptedIn, so this is only ever "bytes that should have been
/// file-backed and were not".
int64_t heap_bytes{0};
/// Packed bytes served from the mmap'd file (clean, file-backed).
int64_t mapped_bytes{0};
/// Reason accounting for the largest share of heap_bytes.
PackedCacheHeapReason heap_reason{PackedCacheHeapReason::None};
};

/** One live cache instance and its own counters. */
struct PackedCacheEntry {
/// Cache file path. Empty for the shared heap-only instance handed to
/// callers that never configured one.
std::string path;
PackedCacheStats stats;
};

/**
* Aggregate plus the per-instance breakdown behind it.
*
* Both come from one pass, so the summary and the detail always describe the
* same instant. The breakdown exists because the aggregate alone cannot be
* attributed: a process running several models folds them into one number, so
* a fallback in one model is indistinguishable from a fallback in another.
* The manager already keys caches by path — this stops discarding that.
*
* Takes no per-instance lock: the counters are atomics, so this never waits
* on a model compile.
*/
struct PackedCacheReport {
PackedCacheStats aggregate;
std::vector<PackedCacheEntry> per_cache;
};

PackedCacheReport get_packed_cache_report();

} // namespace executorch::backends::xnnpack
115 changes: 112 additions & 3 deletions backends/xnnpack/runtime/XNNWeightsCache.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
Expand Down Expand Up @@ -128,6 +128,73 @@
}
#endif

void XNNWeightsCache::record_cache_failure(
PackedCacheFailure failure,
int err) noexcept {
state_.store(
static_cast<int32_t>(PackedCacheState::HeapFallback),
std::memory_order_relaxed);
failure_.store(static_cast<int32_t>(failure), std::memory_order_relaxed);
last_errno_.store(err, std::memory_order_relaxed);
}

PackedCacheStats XNNWeightsCache::stats() const noexcept {
PackedCacheStats out;
out.state =
static_cast<PackedCacheState>(state_.load(std::memory_order_relaxed));
out.failure = static_cast<PackedCacheFailure>(
failure_.load(std::memory_order_relaxed));
out.last_errno = last_errno_.load(std::memory_order_relaxed);
out.file_bytes = file_bytes_.load(std::memory_order_relaxed);
out.mapped_bytes = mapped_bytes_.load(std::memory_order_relaxed);
int64_t worst = 0;
for (size_t i = 0; i < heap_bytes_by_reason_.size(); ++i) {
if (i == static_cast<size_t>(PackedCacheHeapReason::NotOptedIn)) {
continue; // intended heap use, not a fallback
}
const int64_t bytes = heap_bytes_by_reason_[i].load(
std::memory_order_relaxed);
out.heap_bytes += bytes;
if (bytes > worst) {
worst = bytes;
out.heap_reason = static_cast<PackedCacheHeapReason>(i);
}
}
return out;
}

void XNNWeightsCache::record_heap_alloc(
size_t n,
PackedCacheHeapReason reason) noexcept {
// Re-bucket every reason to NotOptedIn when no path was configured. Such an
// instance is the shared heap-only cache handed to callers that never asked
// for file backing; counting its bytes as a fallback would inflate the
// metric for whichever model in the process *did* opt in.
// packed_cache_path_ is set once before the instance is published and never
// mutated, so this read needs no synchronization.
const PackedCacheHeapReason bucket = packed_cache_path_.empty()
? PackedCacheHeapReason::NotOptedIn
: reason;
heap_bytes_by_reason_[static_cast<size_t>(bucket)].fetch_add(
static_cast<int64_t>(n), std::memory_order_relaxed);
}

void XNNWeightsCache::record_mapped_alloc(size_t n) noexcept {
mapped_bytes_.fetch_add(
static_cast<int64_t>(n), std::memory_order_relaxed);
}

void XNNWeightsCache::mark_cache_file_backed() noexcept {
// Only ever upgrades Disabled -> FileBacked. A fallback already recorded
// describes memory the process is carrying, so a later success must not
// mask it.
int32_t expected = static_cast<int32_t>(PackedCacheState::Disabled);
state_.compare_exchange_strong(
expected,
static_cast<int32_t>(PackedCacheState::FileBacked),
std::memory_order_relaxed);
}

Error XNNWeightsCache::initialize_for_runtime(
MemoryAllocator* runtime_allocator,
const NamedDataMap* named_data_map) {
Expand All @@ -148,6 +215,11 @@
// double-mmaps the same file.
if (!name_to_packed_data_metadata_.empty()) {
packed_file_fd_ = open_locked(packed_cache_path_, O_RDWR);
if (packed_file_fd_ < 0) {
record_cache_failure(PackedCacheFailure::OpenFailed, errno);
} else {
mark_cache_file_backed();
}
return Error::Ok;
}

Expand All @@ -161,26 +233,39 @@
packed_cache_path_.c_str(),
name_to_packed_data_metadata_.size());
packed_file_fd_ = open_locked(packed_cache_path_, O_RDWR);
// The loaded entries are already mmap'd, so reads stay file-backed even
// if the write fd could not be reopened; only new packs would go to heap.
mark_cache_file_backed();
return Error::Ok;
}

// Fresh write. Skip O_TRUNC in open_locked so a concurrent holder's
// mmap stays valid; truncate explicitly only after we hold the lock.
packed_file_fd_ = open_locked(packed_cache_path_, O_RDWR | O_CREAT);
if (packed_file_fd_ < 0) {
const int err = errno;
ET_LOG(
Error,
"open(O_RDWR|O_CREAT) failed for %s (errno=%d); heap fallback this init",
packed_cache_path_.c_str(),
err);
record_cache_failure(PackedCacheFailure::OpenFailed, err);
return Error::Ok;
}
if (ftruncate(packed_file_fd_, 0) != 0) {
const int err = errno;
ET_LOG(
Error,
"ftruncate(0) failed for %s (errno=%d); heap fallback this init",
packed_cache_path_.c_str(),
errno);
err);
record_cache_failure(PackedCacheFailure::TruncateFailed, err);
close(packed_file_fd_);
packed_file_fd_ = -1;
return Error::Ok;
}
reset_for_fresh_write();
mark_cache_file_backed();
ET_LOG(
Info,
"Opened packed weight file for writing: %s",
Expand Down Expand Up @@ -394,6 +479,11 @@
// instead of re-packing into heap (dirty memory) every time.
if (context->last_lookup_unnamed_ ||
(context->loaded_from_disk_ && !seed_mismatch_repack)) {
context->record_heap_alloc(
n,
context->last_lookup_unnamed_
? PackedCacheHeapReason::UnnamedConstant
: PackedCacheHeapReason::RepackAfterLoad);
return context->reserve_space_heap(n);
}
if (context->packed_file_fd_ >= 0) {
Expand All @@ -403,13 +493,16 @@
size_t map_size = (n + page_size - 1) & ~(page_size - 1);

if (ftruncate(context->packed_file_fd_, file_offset + map_size) != 0) {
const int err = errno;
ET_LOG(
Error,
"reserve_space ftruncate to %zu failed (errno=%d)",
file_offset + map_size,
errno);
err);
context->record_cache_failure(PackedCacheFailure::GrowFailed, err);
close(context->packed_file_fd_);
context->packed_file_fd_ = -1;
context->record_heap_alloc(n, PackedCacheHeapReason::GrowFailed);
return context->reserve_space_heap(n);
}

Expand All @@ -421,13 +514,16 @@
context->packed_file_fd_,
file_offset);
if (ptr == MAP_FAILED) {
const int err = errno;
ET_LOG(
Error,
"reserve_space mmap %zu bytes failed (errno=%d)",
map_size,
errno);
err);
context->record_cache_failure(PackedCacheFailure::MmapFailed, err);
close(context->packed_file_fd_);
context->packed_file_fd_ = -1;
context->record_heap_alloc(n, PackedCacheHeapReason::MmapFailed);
return context->reserve_space_heap(n);
}

Expand All @@ -439,12 +535,14 @@
kPackedAllocationAlignment);

context->packed_file_used_ = file_offset + map_size;
context->record_mapped_alloc(map_size);
context->file_ptr_to_region_index_[ptr] = context->mmap_regions_.size();
context->mmap_regions_.push_back({ptr, map_size});
context->ptr_to_file_offset_[ptr] = file_offset;
return ptr;
}
#endif
context->record_heap_alloc(n, PackedCacheHeapReason::NoFileBacking);
return context->reserve_space_heap(n);
}

Expand Down Expand Up @@ -609,6 +707,7 @@
// trailer drops the old entry. Monitoring file_bytes over time tells
// us when GC or a size cap is needed.
const size_t file_bytes = index_start + buf.size();
file_bytes_.store(static_cast<int64_t>(file_bytes), std::memory_order_relaxed);
ET_LOG(
Info,
"Saved packed weight index: %u entries at offset %zu, file_bytes=%zu",
Expand Down Expand Up @@ -783,6 +882,16 @@
mmap_regions_at_last_save_ = mmap_regions_.size();
mmap_regions_synced_ = mmap_regions_.size();
loaded_from_disk_ = true;
// Recorded only on the success path — the truncated-entry branch above
// munmaps and rolls back, so counting at the mmap call would over-report.
//
// These two lines are what make the warm case legible. On a steady-state
// launch every look_up hits, reserve_space is never called, and nothing is
// saved, so without them a fully working cache reports heap=0/mapped=0/
// file=0 — identical to the feature being off, and with no denominator the
// heap:mapped ratio is meaningless exactly when it matters most.
record_mapped_alloc(file_size);
file_bytes_.store(static_cast<int64_t>(file_size), std::memory_order_relaxed);
return true;
#else
return false;
Expand Down
Loading
Loading