Skip to content

Commit fad254f

Browse files
Address review finding from GPT 5.6 Sol (#62)
1 parent b142364 commit fad254f

47 files changed

Lines changed: 905 additions & 282 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MIGRATION_GUIDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,9 +2286,10 @@ Important differences:
22862286
per-point radius is dynamic (the distance value at each point), matching
22872287
nifty; there is no fixed-radius mode.
22882288
- The algorithm is otherwise identical to nifty, including its float
2289-
arithmetic, so results match element-for-element. It uses an O(N²)
2290-
pairwise distance matrix; threshold the distance map first to keep the
2291-
candidate count modest.
2289+
arithmetic, so results match element-for-element. It is O(N²) in time but
2290+
uses only O(`number_of_threads` · N) auxiliary memory (no dense N×N matrix),
2291+
and `number_of_threads` parallelizes the pairwise evaluation. Still threshold
2292+
the distance map first to keep the candidate count modest.
22922293

22932294
## scikit-fmm
22942295

include/bioimage_cpp/affinities/compute_affinities.hxx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/detail/grid.hxx"
45
#include "bioimage_cpp/detail/threading.hxx"
56

67
#include <algorithm>
@@ -66,9 +67,9 @@ void compute_affinities_2d(
6667
}
6768

6869
// Sub-rectangle where (y, x) AND (y+dy, x+dx) are both in bounds.
69-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
70+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
7071
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
71-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
72+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
7273
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
7374
if (y_begin >= y_end || x_begin >= x_end) {
7475
continue;
@@ -143,11 +144,11 @@ void compute_affinities_3d(
143144
std::fill_n(mask_channel, volume, std::uint8_t{0});
144145
}
145146

146-
const auto z_begin = std::max<std::ptrdiff_t>(0, -dz);
147+
const auto z_begin = ::bioimage_cpp::detail::axis_begin_offset(dz, depth);
147148
const auto z_end = depth - std::max<std::ptrdiff_t>(0, dz);
148-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
149+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
149150
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
150-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
151+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
151152
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
152153
if (z_begin >= z_end || y_begin >= y_end || x_begin >= x_end) {
153154
continue;

include/bioimage_cpp/affinities/compute_embedding_distances.hxx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/detail/grid.hxx"
45
#include "bioimage_cpp/detail/threading.hxx"
56

67
#include <algorithm>
@@ -108,9 +109,9 @@ void compute_embedding_distances_2d(
108109
distances.data + static_cast<std::ptrdiff_t>(oi) * plane;
109110
std::fill_n(dist_channel, plane, ValueT{0});
110111

111-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
112+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
112113
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
113-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
114+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
114115
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
115116
if (y_begin >= y_end || x_begin >= x_end) {
116117
continue;
@@ -180,11 +181,11 @@ void compute_embedding_distances_3d(
180181
distances.data + static_cast<std::ptrdiff_t>(oi) * volume;
181182
std::fill_n(dist_channel, volume, ValueT{0});
182183

183-
const auto z_begin = std::max<std::ptrdiff_t>(0, -dz);
184+
const auto z_begin = ::bioimage_cpp::detail::axis_begin_offset(dz, depth);
184185
const auto z_end = depth - std::max<std::ptrdiff_t>(0, dz);
185-
const auto y_begin = std::max<std::ptrdiff_t>(0, -dy);
186+
const auto y_begin = ::bioimage_cpp::detail::axis_begin_offset(dy, height);
186187
const auto y_end = height - std::max<std::ptrdiff_t>(0, dy);
187-
const auto x_begin = std::max<std::ptrdiff_t>(0, -dx);
188+
const auto x_begin = ::bioimage_cpp::detail::axis_begin_offset(dx, width);
188189
const auto x_end = width - std::max<std::ptrdiff_t>(0, dx);
189190
if (z_begin >= z_end || y_begin >= y_end || x_begin >= x_end) {
190191
continue;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
#include <limits>
6+
#include <stdexcept>
7+
#include <string>
8+
9+
namespace bioimage_cpp::detail {
10+
11+
inline std::size_t checked_size_cast(const std::uint64_t value, const char *name) {
12+
if constexpr (sizeof(std::size_t) < sizeof(std::uint64_t)) {
13+
if (value > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
14+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
15+
}
16+
}
17+
return static_cast<std::size_t>(value);
18+
}
19+
20+
inline std::size_t checked_size_add(
21+
const std::size_t a,
22+
const std::size_t b,
23+
const char *name
24+
) {
25+
if (b > std::numeric_limits<std::size_t>::max() - a) {
26+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
27+
}
28+
return a + b;
29+
}
30+
31+
inline std::size_t checked_size_multiply(
32+
const std::size_t a,
33+
const std::size_t b,
34+
const char *name
35+
) {
36+
if (a != 0 && b > std::numeric_limits<std::size_t>::max() / a) {
37+
throw std::overflow_error(std::string(name) + " exceeds size_t range");
38+
}
39+
return a * b;
40+
}
41+
42+
inline std::uint64_t checked_u64_add(
43+
const std::uint64_t a,
44+
const std::uint64_t b,
45+
const char *name
46+
) {
47+
if (b > std::numeric_limits<std::uint64_t>::max() - a) {
48+
throw std::overflow_error(std::string(name) + " exceeds uint64 range");
49+
}
50+
return a + b;
51+
}
52+
53+
} // namespace bioimage_cpp::detail

include/bioimage_cpp/detail/grid.hxx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,32 @@ inline void valid_axis_range(
103103
const auto d = static_cast<std::size_t>(delta);
104104
hi = (d >= length) ? 0 : (length - d);
105105
} else {
106-
const auto d = static_cast<std::size_t>(-delta);
106+
// Avoid negating PTRDIFF_MIN. Converting first and subtracting from
107+
// zero computes the magnitude in the unsigned domain.
108+
const auto d = std::size_t{0} - static_cast<std::size_t>(delta);
107109
lo = (d >= length) ? length : d;
108110
hi = length;
109111
}
110112
}
111113

114+
// Number of leading positions to skip along an axis of length `length` for a
115+
// signed offset `delta` (i.e. `max(0, -delta)` clamped to `length`). Returns a
116+
// plain `std::ptrdiff_t` so the affinity kernels keep their inline
117+
// `ptrdiff_t` loop bounds and inner-loop codegen, while avoiding the undefined
118+
// behaviour of negating `delta == PTRDIFF_MIN`: magnitudes at least as large as
119+
// the axis (which make the whole offset channel empty) are clamped to `length`,
120+
// so the negation only runs when `-length < delta < 0` and is always safe.
121+
inline std::ptrdiff_t axis_begin_offset(
122+
const std::ptrdiff_t delta,
123+
const std::ptrdiff_t length
124+
) {
125+
if (delta >= 0) {
126+
return 0;
127+
}
128+
if (delta <= -length) {
129+
return length;
130+
}
131+
return -delta;
132+
}
133+
112134
} // namespace bioimage_cpp::detail

include/bioimage_cpp/detail/threading.hxx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
#include <algorithm>
44
#include <cstddef>
5+
#include <exception>
6+
#include <mutex>
7+
#include <stdexcept>
58
#include <thread>
69
#include <utility>
710
#include <vector>
@@ -27,34 +30,66 @@ inline std::size_t normalize_thread_count(
2730

2831
// Split [0, number_of_work_items) into n_threads contiguous chunks and invoke
2932
// `chunk(thread_id, begin, end)` on each chunk. Thread 0 runs on the calling
30-
// thread; threads 1..n_threads-1 run in parallel std::threads. The caller is
33+
// thread; threads 1..n_threads-1 run in parallel std::jthreads. The caller is
3134
// responsible for picking n_threads via normalize_thread_count and for the
3235
// thread safety of `chunk`.
36+
//
37+
// Exceptions thrown by `chunk` are captured rather than allowed to escape a
38+
// worker thread (which would call std::terminate). The first exception on any
39+
// thread is stored and rethrown on the calling thread after every worker has
40+
// been joined, so nanobind can translate it into a Python exception.
3341
template <class Chunk>
3442
void parallel_for_chunks(
3543
const std::size_t n_threads,
3644
const std::size_t number_of_work_items,
3745
Chunk &&chunk
3846
) {
47+
if (n_threads == 0) {
48+
throw std::invalid_argument("parallel_for_chunks requires n_threads >= 1");
49+
}
3950
const auto bounds = [&](const std::size_t thread_id) {
4051
const auto begin = thread_id * number_of_work_items / n_threads;
4152
const auto end = (thread_id + 1) * number_of_work_items / n_threads;
4253
return std::pair<std::size_t, std::size_t>{begin, end};
4354
};
4455

45-
std::vector<std::thread> threads;
56+
std::exception_ptr first_exception;
57+
std::mutex exception_mutex;
58+
const auto guarded_chunk = [&] (
59+
const std::size_t thread_id,
60+
const std::size_t begin,
61+
const std::size_t end
62+
) {
63+
try {
64+
chunk(thread_id, begin, end);
65+
} catch (...) {
66+
const std::lock_guard<std::mutex> lock(exception_mutex);
67+
if (!first_exception) {
68+
first_exception = std::current_exception();
69+
}
70+
}
71+
};
72+
73+
// jthread's destructor joins, so a failure while creating a later worker
74+
// cannot destroy an earlier still-joinable thread and terminate the
75+
// process. We still join explicitly below to rethrow only after all work
76+
// has completed.
77+
std::vector<std::jthread> threads;
4678
threads.reserve(n_threads > 0 ? n_threads - 1 : 0);
4779
for (std::size_t thread_id = 1; thread_id < n_threads; ++thread_id) {
4880
const auto [begin, end] = bounds(thread_id);
49-
threads.emplace_back([thread_id, begin, end, &chunk]() {
50-
chunk(thread_id, begin, end);
81+
threads.emplace_back([thread_id, begin, end, &guarded_chunk]() {
82+
guarded_chunk(thread_id, begin, end);
5183
});
5284
}
5385
const auto [begin, end] = bounds(0);
54-
chunk(0, begin, end);
86+
guarded_chunk(0, begin, end);
5587
for (auto &thread : threads) {
5688
thread.join();
5789
}
90+
if (first_exception) {
91+
std::rethrow_exception(first_exception);
92+
}
5893
}
5994

6095
} // namespace bioimage_cpp::detail

0 commit comments

Comments
 (0)