Skip to content

Commit a9a431d

Browse files
authored
Update file_load_bench (#36741)
### Details: - New scenario to test: read + calculations - Add assert for debug build. So user will not be able to build this target in debug accidently ### Tickets: - *ticket-id* ### AI Assistance: - *AI assistance used: yes* - *If yes, summarize how AI was used and what human validation was performed (build/tests/manual checks).*
1 parent b6184fb commit a9a431d

2 files changed

Lines changed: 55 additions & 47 deletions

File tree

src/core/tests/file_load_benchmark.cpp

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
#include "common_test_utils/common_utils.hpp"
3939
#include "common_test_utils/file_utils.hpp"
4040

41+
// These benchmarks measure wall-clock timing and are meaningless (and extremely slow for
42+
// multi-GB files) in a Debug (-O0) build.
43+
#ifndef NDEBUG
44+
# error \
45+
"file_load_benchmark.cpp must be built in Release mode: rebuild with -DCMAKE_BUILD_TYPE=Release, or delete this #error to build in Debug anyway."
46+
#endif
47+
4148
namespace ov::test {
4249

4350
namespace {
@@ -211,38 +218,37 @@ void loop_touch_mem_lock(const std::filesystem::path& path, size_t /*file_size*/
211218
ensure_memory_resident(mapped); // should be near no-op and just lock/unlock resident pages
212219
}
213220

214-
void parallel_loop_sync_then_memcpy(const std::filesystem::path& path, size_t file_size) {
215-
auto mapped = load_mmap_object(path);
216-
util::vm_prefetch(mapped->data(), mapped->size(), std::thread::hardware_concurrency());
217-
constexpr size_t chunk_size = 128 * util::one_mib;
218-
std::vector<char> buffer(std::min(chunk_size, file_size));
219-
volatile char sink = 0;
221+
// --- "compute" scenario -----------------------------------------------------------------
222+
// Instead of a raw memcpy or mlock(), run a std::transform pass over the mapped bytes (e.g.
223+
// mimicking a dequantization/dtype-conversion pass over model weights).
224+
void compute_over_mapped(const std::shared_ptr<ov::MappedMemory>& mapped) {
225+
constexpr size_t chunk_size = 128 * util::one_mib; // 128 MiB chunks
226+
const size_t file_size = mapped->size();
227+
std::vector<uint64_t> out(std::min(chunk_size, file_size) / sizeof(uint64_t));
228+
uint64_t acc = 0;
220229
for (size_t offset = 0; offset < file_size; offset += chunk_size) {
221-
const size_t copy_size = std::min(chunk_size, file_size - offset);
222-
std::memcpy(buffer.data(), mapped->data() + offset, copy_size);
223-
sink += buffer[0] + buffer[copy_size / 2] + buffer[copy_size - 1]; // prevents optimization
230+
const size_t n = std::min(chunk_size, file_size - offset);
231+
const size_t n_words = n / sizeof(uint64_t);
232+
const auto* first = reinterpret_cast<const uint64_t*>(mapped->data() + offset);
233+
std::transform(first, first + n_words, out.begin(), [](uint64_t v) {
234+
return v * 3u + 7u;
235+
});
236+
if (n_words > 0)
237+
acc += out[0] + out[n_words / 2] + out[n_words - 1]; // prevents optimization
224238
}
239+
volatile uint64_t sink = acc;
240+
(void)sink;
225241
}
226242

227-
void mmap_then_memcpy(const std::filesystem::path& path, size_t file_size) {
243+
void mmap_then_compute(const std::filesystem::path& path, size_t /*file_size*/) {
228244
auto mapped = load_mmap_object(path);
229-
constexpr size_t chunk_size = 128 * util::one_mib;
230-
std::vector<char> buffer(std::min(chunk_size, file_size));
231-
volatile char sink = 0;
232-
for (size_t offset = 0; offset < file_size; offset += chunk_size) {
233-
const size_t copy_size = std::min(chunk_size, file_size - offset);
234-
std::memcpy(buffer.data(), mapped->data() + offset, copy_size);
235-
sink += buffer[0] + buffer[copy_size / 2] + buffer[copy_size - 1]; // prevents optimization
236-
}
245+
compute_over_mapped(mapped);
237246
}
238247

239-
// Strategy: ifstream into a single pre-allocated buffer — one kernel→user copy
240-
void ifstream_read(const std::filesystem::path& path, size_t file_size) {
241-
std::vector<char> read_buffer(file_size);
242-
std::ifstream f(path, std::ios::binary);
243-
f.read(read_buffer.data(), static_cast<std::streamsize>(file_size));
244-
volatile char sink = read_buffer[0] + read_buffer[file_size / 2] + read_buffer[file_size - 1];
245-
(void)sink;
248+
void mmap_prefetch_then_compute(const std::filesystem::path& path, size_t /*file_size*/) {
249+
auto mapped = load_mmap_object(path);
250+
mapped->hint_prefetch();
251+
compute_over_mapped(mapped);
246252
}
247253

248254
void mmap_prefetch_then_memcpy_partial(const std::filesystem::path& path,
@@ -271,7 +277,7 @@ void mmap_prefetch_then_memcpy_partial(const std::filesystem::path& path,
271277

272278
class FileLoadBenchmark : public ::testing::Test {};
273279

274-
TEST_F(FileLoadBenchmark, strategies_read_memcpy) {
280+
TEST_F(FileLoadBenchmark, read_into_mmap_and_compute) {
275281
const std::vector<size_t> sizes_mib = {10, 100, 500, 1000};
276282
constexpr int warmup = 0;
277283
constexpr int runs = 3;
@@ -285,37 +291,28 @@ TEST_F(FileLoadBenchmark, strategies_read_memcpy) {
285291
files.push_back(tf);
286292
}
287293

288-
// Collect results: [file_idx] -> {mmap_prefetch_memcpy, mmap_memcpy, ifstream}
294+
// Collect results: [file_idx] -> {no hint, sync prefetch}
289295
struct Row {
290296
size_t mib;
291-
long long t_hint_prefetch;
292-
long long t_no_prefault;
293-
long long t_ifstream;
297+
long long t_no_hint;
298+
long long t_sync_prefetch;
294299
};
295300
std::vector<Row> results;
296301

297302
for (const auto& tf : files) {
298303
Row r{};
299304
r.mib = tf.size_mib;
300-
r.t_ifstream = bench(
301-
[&]() {
302-
strategy::ifstream_read(tf.path, tf.size_bytes());
303-
},
304-
tf.path,
305-
tf.size_bytes(),
306-
warmup,
307-
runs);
308-
r.t_no_prefault = bench(
305+
r.t_no_hint = bench(
309306
[&]() {
310-
strategy::mmap_then_memcpy(tf.path, tf.size_bytes());
307+
strategy::mmap_then_compute(tf.path, tf.size_bytes());
311308
},
312309
tf.path,
313310
tf.size_bytes(),
314311
warmup,
315312
runs);
316-
r.t_hint_prefetch = bench(
313+
r.t_sync_prefetch = bench(
317314
[&]() {
318-
strategy::parallel_loop_sync_then_memcpy(tf.path, tf.size_bytes());
315+
strategy::mmap_prefetch_then_compute(tf.path, tf.size_bytes());
319316
},
320317
tf.path,
321318
tf.size_bytes(),
@@ -325,10 +322,20 @@ TEST_F(FileLoadBenchmark, strategies_read_memcpy) {
325322
}
326323

327324
printf("\n--- Latency (ms, mean of %d runs, cold cache) ---\n", runs);
328-
printf("%-10s | %18s | %13s | %13s\n", "Size (MiB)", "parallel loop sync", "default mmap", "ifstream");
329-
printf("%-10s-|-%18s-|-%13s-|-%13s\n", "----------", "------------------", "-------------", "-------------");
325+
printf("%-10s | %17s | %13s\n", "Size (MiB)", "sync prefetch", "mmap+compute");
326+
printf("%-10s-|-%17s-|-%13s\n", "----------", "-----------------", "-------------");
327+
for (const auto& r : results) {
328+
printf("%-10zu | %14lld ms | %10lld ms\n", r.mib, r.t_sync_prefetch, r.t_no_hint);
329+
}
330+
331+
printf("\n--- Throughput (MiB/s) ---\n");
332+
printf("%-10s | %17s | %13s\n", "Size (MiB)", "sync prefetch", "mmap+compute");
333+
printf("%-10s-|-%17s-|-%13s\n", "----------", "-----------------", "-------------");
330334
for (const auto& r : results) {
331-
printf("%-10zu | %15lld ms | %10lld ms | %10lld ms\n", r.mib, r.t_hint_prefetch, r.t_no_prefault, r.t_ifstream);
335+
printf("%-10zu | %12.0f MiB/s | %8.0f MiB/s\n",
336+
r.mib,
337+
throughput_mibs(r.mib, r.t_sync_prefetch),
338+
throughput_mibs(r.mib, r.t_no_hint));
332339
}
333340
}
334341

src/core/tests/file_load_benchmark_guide.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ is printed once at the start of the run.
3434

3535
| Test | Description |
3636
|------|-------------|
37-
| `strategies_read_memcpy` | Compares cold-cache load-and-copy performance across three strategies — `ifstream` read into a preallocated buffer, `mmap` + `memcpy`, and `mmap` + `hint_prefetch` + `memcpy`. Reports latency and throughput. |
3837
| `strategies_mlock` | Measures the cost of making an entire file resident in memory without an additional user copy. |
39-
| `hint_prefetch_with_offset_table` | Stresses partial-region `hint_prefetch` on a single 1200 MB file across a matrix of starting offsets and region sizes. Highlights alignment and offset effects on prefetch latency. |
38+
| `read_into_mmap_and_compute` | **compute scenario.** Compares a `std::transform` pass over the mapped bytes (mimicking a dequantization/dtype-conversion pass) with and without a preceding synchronous `hint_prefetch`, instead of `mlock()` or `memcpy()`. Files up to 10 GB. |
39+
| `hint_prefetch_with_offset_table` | Stresses partial-region `hint_prefetch` on a single 1200 MB file across a matrix of starting offsets and region sizes. Highlights alignment and offset effects on prefetch latency. |
40+

0 commit comments

Comments
 (0)