blob: e6d87e5ebe018b7ae79cf760a341b6d7704d4990 [file] [log] [blame] [edit]
// Copyright 2022 Google LLC
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.
#include <xnnpack/cache.h>
#include <algorithm> // For std::rotate.
#include <cstdint> // For uintptr_t.
#include <cstdint> // For uintptr_t.
#include <cstring> // For memcpy.
#include <cstring> // For memcpy.
#include <thread> // For memcpy.
#include <xnnpack.h>
#include <xnnpack/common.h>
#include <gtest/gtest.h>
static void* cache_end(const xnn_weights_cache* cache) {
return reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(cache->cache.weights.start) + cache->cache.weights.size);
}
static void write_weights(xnn_weights_cache* cache, const std::string& str) {
ASSERT_NE(nullptr, xnn_reserve_space_in_weights_cache(cache, str.length()));
std::memcpy(cache_end(cache), str.data(), str.length());
};
TEST(WEIGHTS_CACHE, init_and_release)
{
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, init_with_size_and_release)
{
constexpr size_t four_mb = 4194304;
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache_with_size(&cache, four_mb));
// Allocation can be rounded up to alignment, so check GE instead of EQ.
ASSERT_GE(cache.cache.weights.capacity, four_mb);
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, release_null)
{
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(nullptr));
}
TEST(WEIGHTS_CACHE, get_or_insert)
{
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
write_weights(&cache, "1234");
ASSERT_EQ(0, xnn_get_or_insert_weights_cache(&cache, cache.cache.weights.start, 4));
ASSERT_EQ(0, cache.cache.hits);
ASSERT_EQ(1, cache.cache.misses);
ASSERT_EQ(4, cache.cache.weights.size);
void* span2_weights = cache_end(&cache);
// Simulate a cache hit.
write_weights(&cache, "1234");
ASSERT_EQ(0, xnn_get_or_insert_weights_cache(&cache, span2_weights, 4));
ASSERT_EQ(1, cache.cache.hits);
ASSERT_EQ(1, cache.cache.misses);
ASSERT_EQ(4, cache.cache.weights.size);
void* span3_weights = cache_end(&cache);
// Simulate a cache miss.
write_weights(&cache, "5678");
ASSERT_EQ(4, xnn_get_or_insert_weights_cache(&cache, span3_weights, 4));
ASSERT_EQ(1, cache.cache.hits);
ASSERT_EQ(2, cache.cache.misses);
ASSERT_EQ(2, cache.cache.num_entries);
ASSERT_EQ(8, cache.cache.weights.size);
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, grow) {
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
size_t old_num_buckets = cache.cache.num_buckets;
for (size_t i = 0, expected_offset = 0; i < old_num_buckets; i++) {
// Add many entries to force cache to grow.
const std::string s = std::to_string(i);
write_weights(&cache, s);
ASSERT_EQ(expected_offset, xnn_get_or_insert_weights_cache(&cache, cache_end(&cache), s.length()));
expected_offset += s.length();
}
ASSERT_EQ(0, cache.cache.hits);
ASSERT_EQ(old_num_buckets, cache.cache.num_entries);
// Check that cache has grown.
ASSERT_LT(old_num_buckets, cache.cache.num_buckets);
// Check that all the entries are still in cache.
for (size_t i = 0, expected_offset = 0; i < old_num_buckets; i++) {
const std::string s = std::to_string(i);
write_weights(&cache, s);
ASSERT_EQ(expected_offset, xnn_get_or_insert_weights_cache(&cache, cache_end(&cache), s.length()));
expected_offset += s.length();
}
// And now all of the lookups should be cache hits.
ASSERT_EQ(old_num_buckets, cache.cache.hits);
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_MEMORY, allocate_and_release) {
xnn_weights_buffer b;
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, XNN_DEFAULT_WEIGHTS_BUFFER_SIZE));
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_MEMORY, grow) {
xnn_weights_buffer b;
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, 8));
// Allocations rounded to page size, so it might not be 8.
size_t old_capacity = b.capacity;
std::string junk = "1234";
std::memcpy(b.start, junk.data(), junk.length());
b.size += junk.length();
ASSERT_EQ(b.size, 4);
const uintptr_t old_weights = reinterpret_cast<uintptr_t>(b.start);
// This should be a no-op, since we have enough space.
ASSERT_EQ(xnn_status_success, xnn_reserve_weights_memory(&b, 4));
ASSERT_EQ(old_weights, reinterpret_cast<uintptr_t>(b.start));
// Simulate copying bytes until we are full.
b.size += (old_capacity - b.size);
const size_t old_size = b.size;
ASSERT_EQ(xnn_status_success, xnn_reserve_weights_memory(&b, 4));
// After growing, the new capacity should be bigger than the old one.
ASSERT_LT(old_capacity, b.capacity);
// At least 4 bytes free.
ASSERT_GE(b.capacity, b.size + 4);
// But size stays the same.
ASSERT_EQ(old_size, b.size);
// Check that after growing, the contents remain.
std::string actual = std::string(static_cast<char*>(b.start), static_cast<char*>(b.start) + junk.length());
ASSERT_EQ(junk, actual);
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_CACHE, finalize_empty) {
xnn_weights_buffer b;
const size_t initial_capacity = 1024 * 1024; // 1MB.
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, initial_capacity));
ASSERT_EQ(0, b.size);
ASSERT_EQ(initial_capacity, b.capacity);
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_memory(&b));
ASSERT_EQ(0, b.size);
ASSERT_EQ(0, b.capacity);
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_CACHE, finalize) {
xnn_weights_buffer b;
const size_t initial_capacity = 1024 * 1024; // 1MB.
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, initial_capacity));
const size_t actual_capacity = b.capacity;
const std::string junk = "1234";
std::memcpy(b.start, junk.data(), junk.length());
b.size += junk.length();
ASSERT_EQ(4, b.size);
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_memory(&b));
#if XNN_PLATFORM_WEB
// Web does not support partial unmapping.
ASSERT_EQ(actual_capacity, b.capacity);
#else
// The actual capacity depends on page size, since it is aligned, just check that it shrunk.
ASSERT_GE(actual_capacity, b.capacity);
#endif
ASSERT_EQ(4, b.size);
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_CACHE, finalize_twice) {
xnn_weights_buffer b;
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, XNN_DEFAULT_WEIGHTS_BUFFER_SIZE));
const std::string junk = "1234";
std::memcpy(b.start, junk.data(), junk.length());
b.size += junk.length();
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_memory(&b));
const size_t capacity = b.capacity;
// Finalizing twice does not error.
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_memory(&b));
// Capacity does not change.
ASSERT_EQ(capacity, b.capacity);
ASSERT_EQ(4, b.size);
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_CACHE, finalize_capacity_smaller_than_page_aligned_size) {
xnn_weights_buffer b;
// Small capacity that is smaller than page sizes on all platforms.
ASSERT_EQ(xnn_status_success, xnn_allocate_weights_memory(&b, 8));
const std::string junk = "1234";
std::memcpy(b.start, junk.data(), junk.length());
b.size += junk.length();
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_memory(&b));
ASSERT_EQ(4, b.size);
ASSERT_EQ(xnn_status_success, xnn_release_weights_memory(&b));
}
TEST(WEIGHTS_CACHE, write_many_cache_hits) {
#if XNN_PLATFORM_WEB && !defined(__EMSCRIPTEN_PTHREADS__)
GTEST_SKIP();
#endif
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
const std::string weights = "0123456789abcdefghij";
const size_t weights_size = weights.size();
auto write = [&] {
write_weights(&cache, weights);
xnn_get_or_insert_weights_cache(&cache, cache_end(&cache), weights_size);
};
constexpr size_t num_threads = 20;
std::vector<std::thread> threads;
threads.reserve(num_threads);
for (size_t i = 0; i < num_threads; i++) {
threads.emplace_back(write);
}
for (size_t i = 0; i < num_threads; i++) {
threads[i].join();
}
ASSERT_EQ(num_threads - 1, cache.cache.hits);
ASSERT_EQ(1, cache.cache.num_entries);
ASSERT_EQ(weights_size, cache.cache.weights.size);
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, write_many_cache_misses) {
#if XNN_PLATFORM_WEB && !defined(__EMSCRIPTEN_PTHREADS__)
GTEST_SKIP();
#endif
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
EXPECT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
const std::string weights = "0123456789abcdefghij";
const size_t weights_size = weights.size();
auto write = [&](size_t i) {
std::string rotated_weights = weights;
std::rotate(rotated_weights.begin(), rotated_weights.begin() + i,
rotated_weights.end());
write_weights(&cache, rotated_weights);
xnn_get_or_insert_weights_cache(&cache, cache_end(&cache), weights_size);
};
constexpr size_t num_threads = 20;
ASSERT_LE(num_threads, weights_size);
std::vector<std::thread> threads;
threads.reserve(num_threads);
for (size_t i = 0; i < num_threads; i++) {
threads.emplace_back(write, i);
}
for (size_t i = 0; i < num_threads; i++) {
threads[i].join();
}
ASSERT_EQ(0, cache.cache.hits);
ASSERT_EQ(num_threads, cache.cache.num_entries);
ASSERT_EQ(weights_size * num_threads, cache.cache.weights.size);
EXPECT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, operations_on_finalized_cache_hard) {
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
ASSERT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_cache(&cache, xnn_weights_cache_finalization_kind_hard));
// Finalizing a finalized cache is an error.
ASSERT_NE(xnn_status_success, xnn_finalize_weights_cache(&cache, xnn_weights_cache_finalization_kind_hard));
// Trying to reserve is an error.
ASSERT_EQ(nullptr, xnn_reserve_space_in_weights_cache(&cache, 1));
// We should not be able to insert into the weights cache, and also this shouldn't timeout by unlocking a mutex which
// has not been locked (since xnn_reserve_space_in_weights_cache above failed).
ASSERT_EQ(XNN_CACHE_NOT_FOUND, xnn_get_or_insert_weights_cache(&cache, cache.cache.weights.start, 4));
ASSERT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, operations_on_finalized_cache_soft) {
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
ASSERT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_cache(&cache, xnn_weights_cache_finalization_kind_soft));
// Finalizing a finalized cache is an error.
ASSERT_NE(xnn_status_success, xnn_finalize_weights_cache(&cache, xnn_weights_cache_finalization_kind_soft));
// Trying to reserve too much is an error.
ASSERT_EQ(nullptr, xnn_reserve_space_in_weights_cache(&cache, cache.cache.weights.capacity + 1));
ASSERT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}
TEST(WEIGHTS_CACHE, insert_into_finalized_cache_soft) {
ASSERT_EQ(xnn_status_success, xnn_initialize(/*allocator=*/nullptr));
struct xnn_weights_cache cache;
ASSERT_EQ(xnn_status_success, xnn_init_weights_cache(&cache));
write_weights(&cache, "1234");
ASSERT_EQ(0, xnn_get_or_insert_weights_cache(&cache, cache.cache.weights.start, 4));
ASSERT_EQ(xnn_status_success, xnn_finalize_weights_cache(&cache, xnn_weights_cache_finalization_kind_soft));
// Inserting into a finalized cache is okay as long as cache memory has space and it is a cache hit.
ASSERT_LT(cache.cache.weights.size + 4, cache.cache.weights.capacity);
write_weights(&cache, "1234");
void* cached_weights = cache_end(&cache);
ASSERT_EQ(0, xnn_get_or_insert_weights_cache(&cache, cached_weights, 4));
ASSERT_EQ(4, cache.cache.weights.size);
// Sufficient space, but Cache miss.
write_weights(&cache, "4567");
ASSERT_EQ(XNN_CACHE_NOT_FOUND, xnn_get_or_insert_weights_cache(&cache, cached_weights, 4));
// Not enough space in the finalized weights cache.
std::string big_string(cache.cache.weights.capacity, '5');
// Don't use write_weights here as it asserts xnn_reserve_space_in_weights_cache does not return nullptr.
ASSERT_EQ(nullptr, xnn_reserve_space_in_weights_cache(&cache, big_string.length()));
ASSERT_EQ(xnn_status_success, xnn_release_weights_cache(&cache));
}