Skip to content

Commit d8e2fd6

Browse files
committed
Merge remote-tracking branch 'origin/263-parallel-decompress-read' into staging
2 parents fd9556d + c838f9d commit d8e2fd6

6 files changed

Lines changed: 293 additions & 42 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,9 @@ jobs:
329329
run: cmake --build build --parallel --config Release
330330

331331
- name: Test
332+
shell: bash
332333
run: |
334+
set -euxo pipefail
333335
if [ "$CDASH_SUBMIT" = "true" ]; then
334336
ctest -D Experimental --test-dir build -C Release --output-on-failure
335337
else

h5cpp/H5Tmeta.hpp

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -386,29 +386,13 @@ namespace h5::meta {
386386
: is_transport_contiguous_t<typename meta::decay<T>::type> {};
387387

388388
// Trivially copyable aggregates are safe for bulk memcpy: no padding surprises,
389-
// no non-trivial copy semantics. Excludes arrays and text already handled above,
390-
// and plugin-registered types which may have non-trivial HDF5 field ordering.
389+
// no non-trivial copy semantics. Excludes arrays and text already handled above.
391390
template <class T> struct is_transport_contiguous_impl_t<T, std::enable_if_t<
392391
std::is_aggregate_v<T> &&
393392
!is_array_like<T>::value &&
394393
!is_text_like<T>::value &&
395-
!is_reflected_compound_t<T>::value &&
396394
std::is_trivially_copyable_v<T>>> : std::true_type {};
397395

398-
template <class T> struct is_transport_contiguous_impl_t<T, std::enable_if_t<is_reflected_compound_t<T>::value>> {
399-
private:
400-
static_assert(compiler_meta_t<T>::version == metadata_version,
401-
"H5CPP compiler metadata version mismatch");
402-
using fields_t = typename compiler_meta_t<T>::fields_t;
403-
template <std::size_t... Is>
404-
static constexpr bool check_contiguous(std::index_sequence<Is...>) noexcept {
405-
return (... && is_transport_contiguous_v<
406-
typename std::tuple_element_t<Is, fields_t>::field_type>);
407-
}
408-
public:
409-
static constexpr bool value = check_contiguous(std::make_index_sequence<std::tuple_size_v<fields_t>>{});
410-
};
411-
412396
// Gap 1: contiguous STL sequence containers (vector<T>, span<T>, linalg types, etc.)
413397
// Triggers when T exposes a data() pointer and size(), but is not a C/std::array,
414398
// not text, and not arithmetic.
@@ -591,30 +575,16 @@ namespace h5::meta {
591575
static constexpr std::size_t bytes(const T&) noexcept { return sizeof(T); }
592576
};
593577

594-
// Reflected compound structs (plugin-registered)
595-
template <class T>
596-
struct access_traits_t<T, std::enable_if_t<is_reflected_compound_t<T>::value>> {
597-
using element_t = T;
598-
using pointer_t = const T*;
599-
static constexpr access_t kind = access_t::object;
600-
static constexpr bool is_trivially_packable = is_transport_contiguous_v<T>;
601-
static const T* data(const T& v) noexcept { return &v; }
602-
static T* data(T& v) noexcept { return &v; }
603-
static constexpr std::array<std::size_t,0> size(const T&) noexcept { return {}; }
604-
static constexpr std::size_t bytes(const T&) noexcept { return sizeof(T); }
605-
};
606-
607578
// Plain aggregates: any struct/class that is an aggregate but not arithmetic,
608-
// array-like, text-like, or already plugin-registered via is_reflected_compound_t.
579+
// array-like, or text-like.
609580
// Provides the memory-access contract so h5::write(ds, pod_value) works once
610581
// storage_traits_impl_t<T> is populated (old dt_t path or future C++26 reflection).
611582
template <class T>
612583
struct access_traits_t<T, std::enable_if_t<
613584
std::is_aggregate_v<T> &&
614585
!std::is_arithmetic_v<T> &&
615586
!is_array_like<T>::value &&
616-
!is_text_like<T>::value &&
617-
!is_reflected_compound_t<T>::value>> {
587+
!is_text_like<T>::value>> {
618588
using element_t = T;
619589
using pointer_t = const T*;
620590
static constexpr access_t kind = access_t::object;

h5cpp/H5Zpipeline.hpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,28 @@ namespace h5{ namespace impl {
6363
static constexpr size_t alignment = H5CPP_MEM_ALIGNMENT;
6464
static constexpr size_t default_capacity = 256 * 1024 * 1024;
6565

66-
struct aligned_deleter {
67-
void operator()(char* ptr) const { std::free(ptr); }
66+
struct arena_deleter {
67+
void operator()(char* ptr) const {
68+
#ifdef _WIN32
69+
_aligned_free(ptr);
70+
#else
71+
std::free(ptr);
72+
#endif
73+
}
6874
};
69-
std::unique_ptr<char, aligned_deleter> base;
75+
std::unique_ptr<char, arena_deleter> base;
7076
char* bump = nullptr;
7177
char* end = nullptr;
7278

7379
explicit chunk_arena_t(size_t capacity = default_capacity) {
7480
void* ptr = nullptr;
81+
#ifdef _WIN32
82+
ptr = _aligned_malloc(capacity, alignment);
83+
#else
7584
if (posix_memalign(&ptr, alignment, capacity) != 0)
85+
ptr = nullptr;
86+
#endif
87+
if (!ptr)
7688
throw std::bad_alloc();
7789
base.reset(static_cast<char*>(ptr));
7890
bump = base.get();
@@ -103,7 +115,13 @@ namespace h5{ namespace impl {
103115
private:
104116
[[nodiscard]] char* allocate_fallback(size_t size) {
105117
void* ptr = nullptr;
118+
#ifdef _WIN32
119+
ptr = _aligned_malloc(size, alignment);
120+
#else
106121
if (posix_memalign(&ptr, alignment, size) != 0)
122+
ptr = nullptr;
123+
#endif
124+
if (!ptr)
107125
throw std::bad_alloc();
108126
return static_cast<char*>(ptr);
109127
}
@@ -374,8 +392,10 @@ template< class Derived>
374392
if (rank == 1 && (O[0] % B[0]) == 0) [[likely]] {
375393
constexpr hsize_t prefetch_distance = 4;
376394
for (hsize_t j = 0; j < N[0]; j += B[0]) {
395+
#ifndef _WIN32
377396
if (j + (prefetch_distance + 1) * B[0] < N[0])
378397
__builtin_prefetch(ptr + (j + prefetch_distance * B[0]) * element_size, 0, 3);
398+
#endif
379399
hsize_t bytes_in_chunk = (j + B[0] <= N[0]) ? B[0] : (N[0] - j);
380400
hsize_t bytes_to_copy = bytes_in_chunk * element_size;
381401
if (bytes_to_copy < block_size) [[unlikely]]

h5cpp/H5Zpipeline_pool.hpp

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
4040
// pipeline (and its shared_ptr to the pool) goes away. Blocks the
4141
// destructor on each front future.
4242
while (!in_flight_.empty()) drain_in_flight(/*blocking=*/true);
43+
while (!read_in_flight_.empty()) drain_in_flight_read(nullptr, /*blocking=*/true);
4344
}
4445

4546
pool_pipeline_t(const pool_pipeline_t&) = delete;
@@ -52,11 +53,19 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
5253
void write_chunk_impl(const hsize_t* offset, std::size_t nbytes, const void* src);
5354
void read_chunk_impl (const hsize_t* offset, std::size_t nbytes, void* dst);
5455

56+
// Parallel read override — shadows pipeline_t<pool_pipeline_t>::read.
57+
// Rank-1 chunked datasets are decompressed across the worker pool;
58+
// rank>1 falls back to the synchronous base-class path.
59+
void read(const h5::ds_t& ds, const h5::offset_t& offset,
60+
const h5::stride_t& stride, const h5::block_t& block,
61+
const h5::count_t& count, const h5::dxpl_t& dxpl, void* ptr);
62+
5563
// Public quiesce — block until every in-flight future has been
5664
// drained. Called by pt_t::flush so users get the expected
5765
// "data on disk after flush returns" semantics for the pool path.
5866
void drain() {
5967
while (!in_flight_.empty()) drain_in_flight(/*blocking=*/true);
68+
while (!read_in_flight_.empty()) drain_in_flight_read(nullptr, /*blocking=*/true);
6069
}
6170

6271
private:
@@ -72,9 +81,20 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
7281

7382
void drain_in_flight(bool blocking);
7483

84+
// Result of a worker decompression task for the parallel read path.
85+
struct read_result_t {
86+
std::unique_ptr<std::byte[]> data;
87+
std::size_t copy_size{0};
88+
std::size_t dst_offset{0};
89+
};
90+
91+
void drain_in_flight_read(char* dst, bool blocking);
92+
7593
std::shared_ptr<worker_pool_t> pool_;
7694
unsigned cap_{0};
7795
std::deque<std::future<result_t>> in_flight_;
96+
std::deque<std::future<read_result_t>> read_in_flight_;
97+
char* read_dst_ = nullptr;
7898
};
7999

80100
// ─── write path ──────────────────────────────────────────────────────────────
@@ -202,13 +222,144 @@ inline void pool_pipeline_t::write_chunk_impl(const hsize_t* offset_in,
202222
drain_in_flight(/*blocking=*/true);
203223
}
204224

205-
// ─── read path (synchronous for v1) ──────────────────────────────────────────
225+
// ─── read path ───────────────────────────────────────────────────────────────
206226
//
207-
// Phase 1.3.3 keeps read synchronous, identical to basic_pipeline_t::read_chunk_impl.
208-
// Parallel decompression is a deliberate follow-up: it requires read-ahead
209-
// (the read path consumes chunks in order, but the user's buffer slots are
210-
// known up-front, so prefetching can fan out across the pool). Tracked in
211-
// Phase 1.5+ work.
227+
// Phase 1.3.3 kept read synchronous. Phase 1.5 adds parallel decompression
228+
// for rank-1 chunked datasets by running the reverse filter chain on the
229+
// worker pool while the calling thread performs sequential H5Dread_chunk I/O.
230+
231+
inline void pool_pipeline_t::drain_in_flight_read(char* dst, bool blocking) {
232+
using namespace std::chrono_literals;
233+
char* base = dst ? dst : read_dst_;
234+
if (!base) return; // no destination available (shouldn't happen outside read())
235+
236+
if (blocking) {
237+
if (read_in_flight_.empty()) return;
238+
auto r = read_in_flight_.front().get();
239+
std::memcpy(base + r.dst_offset, r.data.get(), r.copy_size);
240+
read_in_flight_.pop_front();
241+
return;
242+
}
243+
while (!read_in_flight_.empty() &&
244+
read_in_flight_.front().wait_for(0s) == std::future_status::ready) {
245+
auto r = read_in_flight_.front().get();
246+
std::memcpy(base + r.dst_offset, r.data.get(), r.copy_size);
247+
read_in_flight_.pop_front();
248+
}
249+
}
250+
251+
inline void pool_pipeline_t::read(const h5::ds_t& ds, const h5::offset_t& offset,
252+
const h5::stride_t& stride, const h5::block_t& block,
253+
const h5::count_t& count, const h5::dxpl_t& dxpl, void* ptr) {
254+
// Fallback to synchronous base-class path when no pool is present or when
255+
// the dataset is not rank-1. Rank>1 parallel decompression is future work.
256+
if (!pool_ || rank != 1) {
257+
pipeline_t<pool_pipeline_t>::read(ds, offset, stride, block, count, dxpl, ptr);
258+
return;
259+
}
260+
261+
this->dxpl = dxpl;
262+
this->ds = ds;
263+
264+
h5::offset_t offset_;
265+
h5::count_t count_;
266+
for (hsize_t i = 0; i < rank; ++i) {
267+
offset_[i] = offset[i];
268+
count_[i] = count[i] * block[i];
269+
}
270+
271+
// Rank-1 chunk decomposition (mirrors split_to_chunk_read).
272+
hsize_t n = count_[0]; // total elements to read
273+
hsize_t b = B[0]; // elements per chunk
274+
hsize_t ry = n % b; // trailing partial chunk
275+
276+
// Snapshot filter chain into a POD closure capture (same pattern as write).
277+
struct filter_chain_t {
278+
filter::call_t filter[H5CPP_MAX_FILTER];
279+
unsigned flags[H5CPP_MAX_FILTER];
280+
std::size_t cd_size[H5CPP_MAX_FILTER];
281+
unsigned cd_values[H5CPP_MAX_FILTER][H5CPP_MAX_FILTER_PARAM];
282+
hsize_t tail;
283+
};
284+
filter_chain_t fc{};
285+
fc.tail = this->tail;
286+
std::memcpy(fc.filter, this->filter, sizeof(fc.filter));
287+
std::memcpy(fc.flags, this->flags, sizeof(fc.flags));
288+
std::memcpy(fc.cd_size, this->cd_size, sizeof(fc.cd_size));
289+
std::memcpy(fc.cd_values, this->cd_values, sizeof(fc.cd_values));
290+
291+
const std::size_t scratch = filter::filter_scratch_bound(block_size);
292+
char* dst = static_cast<char*>(ptr);
293+
read_dst_ = dst;
294+
const std::size_t block_sz = this->block_size;
295+
296+
for (hsize_t j = 0; j < n; j += b) {
297+
hsize_t chunk_offset = j + offset_[0];
298+
hsize_t copy_size = (ry != 0 && j == n - ry) ? ry * element_size : b * element_size;
299+
hsize_t dst_offset = j * element_size;
300+
301+
// Main thread performs HDF5 I/O (H5Dread_chunk is not thread-safe).
302+
auto compressed = std::make_unique<std::byte[]>(scratch);
303+
std::uint32_t filter_mask = 0;
304+
hsize_t C[1] = {chunk_offset};
305+
#if H5_VERSION_GE(2,0,0)
306+
std::size_t buf_size = scratch;
307+
H5Dread_chunk2(static_cast<::hid_t>(ds), static_cast<::hid_t>(dxpl),
308+
C, &filter_mask, compressed.get(), &buf_size);
309+
#else
310+
H5Dread_chunk(static_cast<::hid_t>(ds), static_cast<::hid_t>(dxpl),
311+
C, &filter_mask, compressed.get());
312+
#endif
313+
314+
// Submit decompression to the worker pool.
315+
auto fut = pool_->submit([
316+
compressed = std::move(compressed), block_sz, copy_size,
317+
dst_offset, scratch, fc
318+
]() mutable -> read_result_t {
319+
read_result_t result;
320+
result.copy_size = copy_size;
321+
result.dst_offset = dst_offset;
322+
323+
if (fc.tail == 0) {
324+
result.data = std::make_unique<std::byte[]>(copy_size);
325+
std::memcpy(result.data.get(), compressed.get(), copy_size);
326+
return result;
327+
}
328+
329+
auto work_buf = std::make_unique<std::byte[]>(scratch);
330+
void* src = compressed.get();
331+
void* dst_buf = work_buf.get();
332+
std::size_t length = block_sz;
333+
334+
for (hsize_t fi = fc.tail; fi > 0; --fi) {
335+
const hsize_t idx = fi - 1;
336+
length = fc.filter[idx](dst_buf, src, length,
337+
fc.flags[idx] | H5Z_FLAG_REVERSE,
338+
fc.cd_size[idx], fc.cd_values[idx]);
339+
std::swap(src, dst_buf);
340+
}
341+
342+
result.data = std::make_unique<std::byte[]>(copy_size);
343+
std::memcpy(result.data.get(), src, copy_size);
344+
return result;
345+
});
346+
347+
read_in_flight_.push_back(std::move(fut));
348+
349+
// Opportunistic drain — if front future is ready, copy and free buffer.
350+
drain_in_flight_read(dst, /*blocking=*/false);
351+
352+
// Bounded back-pressure: block when the deque reaches the cap.
353+
while (read_in_flight_.size() >= cap_)
354+
drain_in_flight_read(dst, /*blocking=*/true);
355+
}
356+
357+
// Drain any remaining decompression tasks.
358+
while (!read_in_flight_.empty())
359+
drain_in_flight_read(dst, /*blocking=*/true);
360+
361+
read_dst_ = nullptr;
362+
}
212363

213364
inline void pool_pipeline_t::read_chunk_impl(const hsize_t* offset_in,
214365
std::size_t nbytes,

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ add_test_case(H5Zfletcher32)
9898
add_test_case(H5Zlz4)
9999
add_test_case(H5Zzstd)
100100
add_test_case(H5Zszip)
101+
add_test_case(H5Zpipeline_parallel_read)
101102
add_test_case(H5Zgorilla)
102103
# add_test_case(H5Iall)
103104
add_test_case(H5async) # Phase II scaffold lifecycle + submit_and_wait tests

0 commit comments

Comments
 (0)