Skip to content
Merged
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: 7 additions & 5 deletions cpp/include/kvikio/error.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
Expand All @@ -21,10 +21,12 @@ struct CUfileException : public std::runtime_error {

class GenericSystemError : public std::system_error {
public:
GenericSystemError(const std::string& msg);
GenericSystemError(const char* msg);
GenericSystemError(const GenericSystemError& other) = default;
GenericSystemError& operator=(const GenericSystemError& other) = default;
GenericSystemError(int err_code, std::string const& msg);
GenericSystemError(int err_code, char const* msg);
GenericSystemError(std::string const& msg);
GenericSystemError(char const* msg);
GenericSystemError(GenericSystemError const& other) = default;
GenericSystemError& operator=(GenericSystemError const& other) = default;
virtual ~GenericSystemError() noexcept = default;
};

Expand Down
81 changes: 73 additions & 8 deletions cpp/include/kvikio/file_utils.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
Expand Down Expand Up @@ -179,18 +179,77 @@ std::pair<std::size_t, std::size_t> get_page_cache_info(std::string const& file_
std::pair<std::size_t, std::size_t> get_page_cache_info(int fd);

/**
* @brief Clear the page cache
* @brief Drop page cache for a specific file.
*
* Advises the kernel to evict cached pages for the specified file descriptor using `posix_fadvise`
* with `POSIX_FADV_DONTNEED`.
*
* @param fd Open file descriptor
* @param offset Starting byte offset (default: 0 for beginning of file)
* @param length Number of bytes to drop (default: 0, meaning entire file from offset)
* @param sync_first Whether to flush dirty pages to disk before dropping. If `true`, `fdatasync`
* will be called prior to dropping. This ensures dirty pages become clean and thus droppable. Can
* be set to `false` if we are certain no dirty pages exist for this file.
*
* @note This is the preferred method for benchmark cache invalidation as it:
* - Requires no elevated privileges
* - Affects only the specified file, not other processes
* - Has minimal overhead (no child process spawned)
* @note The page cache dropping takes place in granularity of full pages. If the specified range
* does not align to page boundaries, partial pages at the start and end of the range are retained.
* Only pages fully contained within the range are dropped.
* @note For dropping page cache system-wide (requires elevated privileges), see
* `drop_system_page_cache()`.
*
* @exception kvikio::GenericSystemError if the file descriptor is invalid, or the file cannot be
* synchronized, or the attempt to drop the page cache fails.
*/
void drop_file_page_cache(int fd,
std::size_t offset = 0,
std::size_t length = 0,
bool sync_first = true);

/**
* @brief Drop page cache for a specific file.
*
* Convenience overload that opens the file, drops its page cache, and closes it.
*
* @param file_path Path to the file
* @param offset Starting byte offset (default: 0 for beginning of file)
* @param length Number of bytes to drop (default: 0, meaning entire file from offset)
* @param sync_first Whether to flush dirty pages to disk before dropping. If `true`, `fdatasync`
* will be called prior to dropping. This ensures dirty pages become clean and thus droppable. Can
* be set to `false` if we are certain no dirty pages exist for this file.
*
* @note For dropping page cache system-wide (requires elevated privileges), see
* `drop_system_page_cache()`.
* @note See `drop_file_page_cache(int, std::size_t, std::size_t, bool)` for detailed behavior and
* caveats
*
* @exception kvikio::GenericSystemError if the file cannot be opened, or the file cannot be
* synchronized, or the attempt to drop the page cache fails.
*/
void drop_file_page_cache(std::string const& file_path,
std::size_t offset = 0,
std::size_t length = 0,
bool sync_first = true);

/**
* @brief Drop the system page cache.
*
* @param reclaim_dentries_and_inodes Whether to free reclaimable slab objects which include
* dentries and inodes.
* - If `true`, equivalent to executing `/sbin/sysctl vm.drop_caches=3`;
* - If `false`, equivalent to executing `/sbin/sysctl vm.drop_caches=1`.
* @param clear_dirty_pages Whether to trigger the writeback process to clear the dirty pages. If
* `true`, `sync` will be called prior to cache clearing.
* @return Whether the page cache has been successfully cleared
* @param sync_first Whether to flush dirty pages to disk before dropping. If `true`, `sync` will be
* called prior to dropping. This ensures dirty pages become clean and thus droppable.
* @return Whether the page cache has been successfully dropped.
*
* @note This function creates a child process and executes the cache clearing shell command in the
* following order
* @note This drops page cache system-wide, affecting all processes. For dropping cache for a
* specific file without elevated privileges, see `drop_file_page_cache(int, std::size_t,
* std::size_t, bool)`.
* @note This function creates a child process and executes the cache dropping shell command in the
* following order:
* - Execute the command without `sudo` prefix. This is for the superuser and also for specially
* configured systems where unprivileged users cannot execute `/usr/bin/sudo` but can execute
* `/sbin/sysctl`. If this step succeeds, the function returns `true` immediately.
Expand All @@ -199,7 +258,13 @@ std::pair<std::size_t, std::size_t> get_page_cache_info(int fd);
*
* @exception kvikio::GenericSystemError if somehow the child process could not be created.
*/
bool clear_page_cache(bool reclaim_dentries_and_inodes = true, bool clear_dirty_pages = true);
bool drop_system_page_cache(bool reclaim_dentries_and_inodes = true, bool sync_first = true);

/**
* @brief Drop the system page cache. Deprecated. Use `drop_system_page_cache` instead.
**/
[[deprecated]] bool clear_page_cache(bool reclaim_dentries_and_inodes = true,
bool clear_dirty_pages = true);

/**
* @brief Information about a block device.
Expand Down
15 changes: 11 additions & 4 deletions cpp/src/error.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -10,13 +10,20 @@

namespace kvikio {

GenericSystemError::GenericSystemError(const std::string& msg) : GenericSystemError(msg.c_str()) {}
GenericSystemError::GenericSystemError(int err_code, std::string const& msg)
: std::system_error(err_code, std::generic_category(), msg)
{
}

GenericSystemError::GenericSystemError(const char* msg)
: std::system_error(errno, std::generic_category(), msg)
GenericSystemError::GenericSystemError(int err_code, char const* msg)
: std::system_error(err_code, std::generic_category(), msg)
{
}

GenericSystemError::GenericSystemError(std::string const& msg) : GenericSystemError(errno, msg) {}

GenericSystemError::GenericSystemError(char const* msg) : GenericSystemError(errno, msg) {}

namespace detail {

void log_error(std::string_view err_msg, int line_number, char const* filename)
Expand Down
36 changes: 32 additions & 4 deletions cpp/src/file_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -219,11 +219,34 @@ std::pair<std::size_t, std::size_t> get_page_cache_info(int fd)
return {num_pages_in_page_cache, num_pages};
}

bool clear_page_cache(bool reclaim_dentries_and_inodes, bool clear_dirty_pages)
void drop_file_page_cache(int fd, std::size_t offset, std::size_t length, bool sync_first)
{
KVIKIO_NVTX_FUNC_RANGE();

if (sync_first) { SYSCALL_CHECK(fdatasync(fd), "fdatasync failed"); }

// POSIX_FADV_DONTNEED informs the kernel that the specified data will not be accessed in the near
// future, which subsequently attempts to free the associated cached pages.
// A `length` of 0 means until the end of the file from the offset.
auto err_code = posix_fadvise(
fd, static_cast<std::size_t>(offset), static_cast<std::size_t>(length), POSIX_FADV_DONTNEED);
if (err_code != 0) { throw kvikio::GenericSystemError(err_code, "posix_fadvise failed"); }
}

void drop_file_page_cache(std::string const& file_path,
std::size_t offset,
std::size_t length,
bool sync_first)
{
FileWrapper file(file_path, "r", false, FileHandle::m644);
drop_file_page_cache(file.fd(), offset, length, sync_first);
}

bool drop_system_page_cache(bool reclaim_dentries_and_inodes, bool sync_first)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes: drop_system_page_cache 's return value reflects whether the users have the elevated privilege to drop the system wide cache using /proc/sys/vm/drop_caches, whereas drop_file_page_cache has no privilege requirement. Hence the return value asymmetry.

{
KVIKIO_NVTX_FUNC_RANGE();
if (clear_dirty_pages) { sync(); }
std::string param = reclaim_dentries_and_inodes ? "3" : "1";
if (sync_first) { sync(); }
std::string const param = reclaim_dentries_and_inodes ? "3" : "1";

auto exec_cmd = [](std::string_view cmd) -> bool {
// Prevent the output from the command from mixing with the original process' output.
Expand Down Expand Up @@ -258,6 +281,11 @@ bool clear_page_cache(bool reclaim_dentries_and_inodes, bool clear_dirty_pages)
return false;
}

bool clear_page_cache(bool reclaim_dentries_and_inodes, bool clear_dirty_pages)
{
return drop_system_page_cache(reclaim_dentries_and_inodes, clear_dirty_pages);
}

BlockDeviceInfo get_block_device_info(std::string const& file_path)
{
KVIKIO_NVTX_FUNC_RANGE();
Expand Down
119 changes: 118 additions & 1 deletion cpp/tests/test_file_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,133 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <filesystem>
#include <kvikio/error.hpp>
#include <kvikio/file_handle.hpp>
#include <kvikio/file_utils.hpp>
#include <kvikio/utils.hpp>

#include "utils/utils.hpp"

using ::testing::HasSubstr;
using ::testing::ThrowsMessage;

class PageCacheTest : public testing::Test {
protected:
void SetUp() override
{
_filepath = _tmp_dir.path() / "test";
// The file is 10 pages long
_filesize = 10 * kvikio::get_page_size();

kvikio::FileHandle file(_filepath.string(), "w");
std::vector<std::byte> v(_filesize, {});
auto fut = file.pwrite(v.data(), _filesize);
fut.get();
}

void TearDown() override {}

// Read a range of the file to populate the page cache
void WarmPageCache(std::size_t file_offset, std::size_t size)
{
kvikio::FileHandle file(_filepath.string(), "r");
std::vector<std::byte> v(file.nbytes());
auto fut = file.pread(v.data(), size, file_offset);
fut.get();
}

kvikio::test::TempDir _tmp_dir;
std::filesystem::path _filepath;
std::size_t _filesize;
};

TEST_F(PageCacheTest, drop_file_page_cache_full_range)
{
// Read the full file
WarmPageCache(0, _filesize);

{
// Verify pages are cached
auto [cached_pages, total_pages] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, total_pages); // All pages should be resident
}

kvikio::drop_file_page_cache(_filepath.string());

{
// Verify pages are evicted
auto [cached_pages, _] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, 0); // No pages should be resident
}
}

TEST_F(PageCacheTest, drop_file_page_cache_partial_range)
{
// Read the full file
WarmPageCache(0, _filesize);

// Drop pages 3, 4, 5, 6
// Skip pages 0, 1, 2, and 7, 8, 9
std::size_t file_offset = 3 * kvikio::get_page_size();
std::size_t length = 4 * kvikio::get_page_size();
kvikio::drop_file_page_cache(_filepath.string(), file_offset, length);

// 6 pages remain in the page cache
auto [cached_pages, _] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, 6);
Copy link
Contributor Author

@kingcrimsontianyu kingcrimsontianyu Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file only occupies 10 pages, and on a normally operating CI, I'd expect the file data to be fully resident in the page cache. In an extreme case when the CI system receives complete memory pressure, it is possible, on paper, that some pages of this test file get evicted, where the condition should be EXPECT_LE(cached_pages, 6); but I consider that case extremely unlikely.

I'm open to changing this to LE for absolute correctness though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's see if this causes issues in CI :)

}

// Test the fd-based overload
TEST_F(PageCacheTest, drop_file_page_cache_with_fd)
{
WarmPageCache(0, _filesize);

{
auto [cached_pages, total_pages] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, total_pages);
}

kvikio::FileHandle file(_filepath.string(), "r");
kvikio::drop_file_page_cache(file.fd());

{
auto [cached_pages, _] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, 0);
}
}

TEST_F(PageCacheTest, drop_file_page_cache_invalid_fd)
{
EXPECT_THROW(kvikio::drop_file_page_cache(-1), kvikio::GenericSystemError);
}

TEST_F(PageCacheTest, drop_file_page_cache_nonexistent_file)
{
EXPECT_THROW(kvikio::drop_file_page_cache("/nonexistent/path/file.bin"),
kvikio::GenericSystemError);
}

TEST_F(PageCacheTest, drop_file_page_cache_unaligned_range)
{
// Read the full file
WarmPageCache(0, _filesize);

// Attempt to drop pages 3 (half), 4, 5, 6, 7 (half)
// Actually drop pages 4, 5, 6
std::size_t file_offset = 3 * kvikio::get_page_size() + kvikio::get_page_size() / 2;
std::size_t length = 4 * kvikio::get_page_size();
kvikio::drop_file_page_cache(_filepath.string(), file_offset, length);

// 7 pages remain in the page cache
auto [cached_pages, _] = kvikio::get_page_cache_info(_filepath.string());
EXPECT_EQ(cached_pages, 7);
}

TEST(FileUtilsTest, get_block_device_info)
{
EXPECT_THAT(
Expand Down
4 changes: 4 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ CuFile

.. autofunction:: get_page_cache_info

.. autofunction:: drop_file_page_cache

.. autofunction:: drop_system_page_cache

.. autofunction:: clear_page_cache

.. currentmodule:: kvikio.buffer
Expand Down
12 changes: 11 additions & 1 deletion python/kvikio/kvikio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,29 @@
from kvikio._lib.defaults import CompatMode # noqa: F401
from kvikio._version import __git_commit__, __version__
from kvikio.buffer import bounce_buffer_free, memory_deregister, memory_register
from kvikio.cufile import CuFile, clear_page_cache, get_page_cache_info
from kvikio.cufile import (
CuFile,
clear_page_cache,
drop_file_page_cache,
drop_system_page_cache,
get_page_cache_info,
)
from kvikio.mmap import Mmap
from kvikio.remote_file import RemoteEndpointType, RemoteFile, is_remote_file_available
from kvikio.stream import stream_deregister, stream_register
from kvikio.utils import kvikio_deprecation_notice

__all__ = [
"__git_commit__",
"__version__",
"clear_page_cache",
"CuFile",
"drop_file_page_cache",
"drop_system_page_cache",
"Mmap",
"get_page_cache_info",
"is_remote_file_available",
"kvikio_deprecation_notice",
"RemoteEndpointType",
"RemoteFile",
"stream_register",
Expand Down
Loading