Skip to content

Commit afdb243

Browse files
authored
Add Windows impl for async mmap prefetch (#37063)
### Details: - Add Windows impl for async mmap prefetch - *...* ### Tickets: - [CVS-189523](https://jira.devtools.intel.com/browse/CVS-189523) ### AI Assistance: - *AI assistance used: no / yes* - *If yes, summarize how AI was used and what human validation was performed (build/tests/manual checks).*
1 parent a37cb6e commit afdb243

8 files changed

Lines changed: 242 additions & 200 deletions

File tree

src/common/util/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ target_sources(${TARGET_NAME}
6969
${CMAKE_CURRENT_SOURCE_DIR}/src/file_path.cpp
7070
${CMAKE_CURRENT_SOURCE_DIR}/src/file_util.cpp
7171
${CMAKE_CURRENT_SOURCE_DIR}/src/log.cpp
72-
${CMAKE_CURRENT_SOURCE_DIR}/src/memory.cpp
72+
${CMAKE_CURRENT_SOURCE_DIR}/src/memory_prefetch.cpp
7373
${CMAKE_CURRENT_SOURCE_DIR}/src/memory_prefetch.hpp
7474
${CMAKE_CURRENT_SOURCE_DIR}/src/native_stream.cpp
7575
${CMAKE_CURRENT_SOURCE_DIR}/src/native_streambuf.cpp

src/common/util/src/memory.cpp

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "memory_prefetch.hpp"
6+
7+
#include <condition_variable>
8+
#include <cstdint>
9+
#include <functional>
10+
#include <future>
11+
#include <list>
12+
#include <mutex>
13+
#include <new>
14+
#include <stdexcept>
15+
#include <thread>
16+
#include <vector>
17+
18+
#include "openvino/util/math_util.hpp"
19+
#include "openvino/util/memory.hpp"
20+
21+
namespace ov::util {
22+
23+
namespace {
24+
25+
// FIFO job queue feeding the shared page-toucher pool: a small pool of long-lived worker threads
26+
// is reused across calls instead of spawning/joining threads per prefetch request. Shared by both
27+
// the Linux and Windows vm_prefetch_async() implementations (see submit_page_toucher_tasks()
28+
// below).
29+
class TaskQueue {
30+
public:
31+
void push(std::list<std::function<void()>>&& batch) noexcept {
32+
{
33+
std::lock_guard lock(m_mutex);
34+
m_queue.splice(m_queue.end(), batch);
35+
}
36+
m_cv.notify_all();
37+
}
38+
39+
// Blocks until a job is available, or returns false once the queue is stopped and drained.
40+
bool wait_and_pop(std::function<void()>& job) noexcept {
41+
std::unique_lock<std::mutex> lock(m_mutex);
42+
m_cv.wait(lock, [this] {
43+
return m_stop || !m_queue.empty();
44+
});
45+
if (m_queue.empty()) {
46+
return false;
47+
}
48+
job = std::move(m_queue.front());
49+
m_queue.pop_front();
50+
return true;
51+
}
52+
53+
void stop() noexcept {
54+
{
55+
std::lock_guard<std::mutex> lock(m_mutex);
56+
m_stop = true;
57+
}
58+
m_cv.notify_all();
59+
}
60+
61+
private:
62+
std::mutex m_mutex;
63+
std::condition_variable m_cv;
64+
std::list<std::function<void()>> m_queue;
65+
bool m_stop = false;
66+
};
67+
68+
class ThreadPool {
69+
public:
70+
static ThreadPool& instance() {
71+
static ThreadPool pool;
72+
return pool;
73+
}
74+
75+
ThreadPool(const ThreadPool&) = delete;
76+
ThreadPool& operator=(const ThreadPool&) = delete;
77+
ThreadPool(ThreadPool&&) = delete;
78+
ThreadPool& operator=(ThreadPool&&) = delete;
79+
80+
std::vector<std::future<void>> submit(std::vector<std::function<void()>>&& jobs) {
81+
std::vector<std::future<void>> futures;
82+
futures.reserve(jobs.size());
83+
std::list<std::function<void()>> pending;
84+
for (auto& job : jobs) {
85+
auto task = std::make_shared<std::packaged_task<void()>>(std::move(job));
86+
futures.push_back(task->get_future());
87+
pending.emplace_back([task]() {
88+
(*task)();
89+
});
90+
}
91+
m_queue.push(std::move(pending));
92+
return futures;
93+
}
94+
95+
private:
96+
ThreadPool() {
97+
const auto workers_count =
98+
std::max<size_t>(1, std::min<size_t>(max_prefetch_threads, std::thread::hardware_concurrency()));
99+
m_workers.reserve(workers_count);
100+
for (size_t i = 0; i < workers_count; ++i) {
101+
m_workers.emplace_back([this]() {
102+
worker_loop();
103+
});
104+
}
105+
}
106+
107+
~ThreadPool() {
108+
m_queue.stop();
109+
for (auto& worker : m_workers) {
110+
if (worker.joinable()) {
111+
worker.join();
112+
}
113+
}
114+
}
115+
116+
void worker_loop() noexcept {
117+
std::function<void()> job;
118+
while (m_queue.wait_and_pop(job)) {
119+
job();
120+
}
121+
}
122+
123+
TaskQueue m_queue;
124+
std::vector<std::thread> m_workers;
125+
};
126+
127+
} // namespace
128+
129+
std::vector<std::future<void>> submit_page_toucher_tasks(void* ptr, size_t size, size_t num_threads) noexcept {
130+
try {
131+
const auto page_size = static_cast<size_t>(get_system_page_size());
132+
const auto chunk_size =
133+
std::max<size_t>(align_size_up(size / num_threads, page_size), default_parallel_io_min_chunk);
134+
135+
std::vector<std::function<void()>> jobs;
136+
jobs.reserve(ceil_div(size, chunk_size));
137+
138+
for (auto first = reinterpret_cast<const uint8_t*>(ptr), last = first + size; first < last;
139+
first += chunk_size) {
140+
jobs.emplace_back(PageToucher{first, std::min(first + chunk_size, last), page_size});
141+
}
142+
return ThreadPool::instance().submit(std::move(jobs));
143+
} catch (const std::bad_alloc&) {
144+
// Job/future/packaged_task allocation failed under memory pressure.
145+
return {};
146+
} catch (const std::length_error&) {
147+
// vector::reserve()'s requested capacity exceeded max_size() (e.g. a pathological
148+
// ptr/size/num_threads combination producing an absurd chunk count).
149+
return {};
150+
}
151+
}
152+
153+
} // namespace ov::util

src/common/util/src/memory_prefetch.hpp

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,21 @@ class PrefetchToken {
115115
};
116116

117117
/**
118-
* @brief Clamps [offset, offset + size) to [0, mapping_size) and page-aligns the result. Returns an
119-
* empty region (m_length == 0) for a null/empty mapping, an offset at or past the end, or a
120-
* sub-page request.
118+
* @brief Submits page-population jobs for [ptr, ptr + size) to the shared background thread pool,
119+
* splitting the range into up to @p num_threads chunks.
121120
*
122-
* @note This is a self-contained helper for @ref make_prefetch_plan / hint_prefetch_async() only.
123-
* It intentionally does not replace the pre-existing per-platform region helpers used by
124-
* hint_evict()/hint_prefetch(), so those call sites remain untouched.
121+
* @param ptr Page-aligned base address of the range.
122+
* @param size Multiple of the system page size.
123+
* @param num_threads Number of population jobs to split the range into.
124+
*
125+
* Returns an empty vector if the work could not be scheduled (e.g. an allocation failure).
126+
*/
127+
std::vector<std::future<void>> submit_page_toucher_tasks(void* ptr, size_t size, size_t num_threads) noexcept;
128+
129+
/**
130+
* @brief Clamps [offset, offset + size) to [0, mapping_size) and page-aligns the result, rounding
131+
* the length up so it is always a page multiple (both ends page-aligned). Returns an empty region
132+
* (m_length == 0) for a null/empty mapping, an offset at or past the end, or a sub-page request.
125133
*/
126134
inline AlignedRegion clamp_align_region(const void* data, size_t mapping_size, size_t offset, size_t size) noexcept {
127135
const auto page_size = static_cast<size_t>(get_system_page_size());
@@ -130,26 +138,9 @@ inline AlignedRegion clamp_align_region(const void* data, size_t mapping_size, s
130138
}
131139
const auto available = mapping_size - offset;
132140
const auto raw_len = (size == auto_size) ? available : std::min(size, available);
133-
return align_region(reinterpret_cast<uintptr_t>(data) + offset, raw_len, page_size);
134-
}
135-
136-
/** @brief Aligned region and page-aligned size for a hint_prefetch_async() call. */
137-
struct PrefetchPlan {
138-
uintptr_t m_address = 0;
139-
size_t m_aligned_size = 0;
140-
};
141-
142-
/**
143-
* @brief Computes the region and page-aligned size for a hint_prefetch_async() call, or an empty
144-
* plan (m_aligned_size == 0) when the region is below the parallel-I/O threshold (a real
145-
* population pass would not be worth it).
146-
*/
147-
inline PrefetchPlan make_prefetch_plan(const void* data, size_t mapping_size, size_t offset, size_t size) noexcept {
148-
if (const auto region = clamp_align_region(data, mapping_size, offset, size);
149-
region.m_length > default_parallel_io_threshold) {
150-
return {region.m_address, align_size_up(region.m_length, static_cast<size_t>(get_system_page_size()))};
151-
}
152-
return {};
141+
auto region = align_region(reinterpret_cast<uintptr_t>(data) + offset, raw_len, page_size);
142+
region.m_length = align_size_up(region.m_length, page_size);
143+
return region;
153144
}
154145

155146
/** @brief Upper bound on the shared page-population pool's worker threads. */

0 commit comments

Comments
 (0)