Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Version 0.5.4

- Switch to the nearest implementation from [LTLA/nclist-cpp](https://github.com/LTLA/nclist-cpp).
- Improved worksharing across threads for `find_overlaps_groups` and `nearest_groups`. The idea is to ensure that all threads are occupied, even when the groups are of different sizes. This is done with some more fine-grained parallelization during both index building and overlap identification.

## Version 0.5.0 - 0.5.3

- Switch to [LTLA/nclist-cpp](https://github.com/LTLA/nclist-cpp) for overlap and intersection operations.
Expand Down
4 changes: 2 additions & 2 deletions lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ include(FetchContent)
FetchContent_Declare(
nclist
GIT_REPOSITORY https://github.com/LTLA/nclist-cpp
GIT_TAG v0.2.0
# GIT_TAG master
# GIT_TAG v0.2.0
GIT_TAG master

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@LTLA please add a new tag to nclist-cpp

)

FetchContent_MakeAvailable(nclist)
Expand Down
280 changes: 216 additions & 64 deletions lib/src/nclssearch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,31 @@ struct NCListSearchHandler {

self_starts.assign(starts_ptr, starts_ptr + n);
self_ends.assign(ends_ptr, ends_ptr + n);

sorted_starts.resize(n);
sorted_ends.resize(n);
for (Index i = 0; i < n; ++i) {
sorted_starts[i] = {self_starts[i], i};
sorted_ends[i] = {self_ends[i], i};
}

std::sort(sorted_starts.begin(), sorted_starts.end());
std::sort(sorted_ends.begin(), sorted_ends.end());
}

nclist::Nclist<Index, Position> nclist_obj;

std::vector<Position> self_starts;
std::vector<Position> self_ends;
std::vector<std::pair<Position, Index>> sorted_starts;
std::vector<std::pair<Position, Index>> sorted_ends;
};

py::object perform_follow(
NCListSearchHandler &self,
py::array_t<Position> query_starts,
const std::string& select,
int num_threads = 1) {

auto q_starts_ptr = static_cast<const Position*>(query_starts.request().ptr);
Index n_queries = query_starts.request().shape[0];

std::vector<std::pair<Position, Index>> sorted_ends;
sorted_ends.resize(self.self_ends.size());
for (Index i = 0; i < self.self_ends.size(); ++i) {
sorted_ends[i] = {self.self_ends[i], i};
}

std::sort(sorted_ends.begin(), sorted_ends.end());

std::vector<Index> results(n_queries);
std::vector<std::thread> workers;
workers.reserve(num_threads);
Expand All @@ -77,9 +74,9 @@ py::object perform_follow(
Position q_start = q_starts_ptr[i];

// Binary search on sorted ends to find the last subject ending before the query starts
auto it = std::upper_bound(self.sorted_ends.begin(), self.sorted_ends.end(), std::make_pair(q_start, Index(-1)));
auto it = std::upper_bound(sorted_ends.begin(), sorted_ends.end(), std::make_pair(q_start, Index(-1)));

if (it == self.sorted_ends.begin()) {
if (it == sorted_ends.begin()) {
results[i] = -1; // No preceding range
} else {
--it;
Expand Down Expand Up @@ -113,9 +110,18 @@ py::object perform_precede(
py::array_t<Position> query_ends,
const std::string& select,
int num_threads = 1) {

auto q_ends_ptr = static_cast<const Position*>(query_ends.request().ptr);
Index n_queries = query_ends.request().shape[0];

std::vector<std::pair<Position, Index>> sorted_starts;
sorted_starts.resize(self.self_starts.size());
for (Index i = 0; i < self.self_starts.size(); ++i) {
sorted_starts[i] = {self.self_starts[i], i};
}

std::sort(sorted_starts.begin(), sorted_starts.end());

std::vector<Index> results(n_queries);
std::vector<std::thread> workers;
workers.reserve(num_threads);
Expand All @@ -135,9 +141,9 @@ py::object perform_precede(
Position q_end = q_ends_ptr[i];

// Binary search on sorted starts to find the first subject starting after the query ends
auto it = std::lower_bound(self.sorted_starts.begin(), self.sorted_starts.end(), std::make_pair(q_end, Index(-1)));
auto it = std::lower_bound(sorted_starts.begin(), sorted_starts.end(), std::make_pair(q_end, Index(-1)));

if (it == self.sorted_starts.end()) {
if (it == sorted_starts.end()) {
results[i] = -1; // No following range
} else {
results[i] = it->second;
Expand Down Expand Up @@ -170,11 +176,18 @@ py::object perform_nearest(
py::array_t<Position> query_starts,
py::array_t<Position> query_ends,
const std::string& select,
int num_threads=1) {
int num_threads=1,
bool adjacent_equals_overlap=false) {

auto q_starts_ptr = static_cast<const Position*>(query_starts.request().ptr);
auto q_ends_ptr = static_cast<const Position*>(query_ends.request().ptr);
Index n_queries = query_starts.request().shape[0];

bool quit_on_first = (select == "arbitrary");
if (select != "all" && select != "arbitrary" && !quit_on_first) {
throw std::runtime_error("Invalid 'select' parameter. Must be 'all' or 'arbitrary'.");
}

std::vector<std::vector<Index>> all_results(n_queries);
std::vector<std::thread> workers;
workers.reserve(num_threads);
Expand All @@ -190,57 +203,23 @@ py::object perform_nearest(
}

workers.emplace_back([&](int first, int length) -> void {
nclist::OverlapsAnyWorkspace<Index> ws_any;
nclist::OverlapsAnyParameters<Position> ol_params;
ol_params.min_overlap = 1;
std::vector<Index> nearest_matches;

for (Index i = first, last = first + length; i < last; ++i) {
Position q_start = q_starts_ptr[i];
Position q_end = q_ends_ptr[i];

std::vector<std::pair<Position, Index>> candidates;

std::vector<Index> overlaps;
nclist::overlaps_any(self.nclist_obj, q_start, q_end, ol_params, ws_any, overlaps);
for (const auto& ol_idx : overlaps) {
candidates.emplace_back(0, ol_idx);
}

auto it_b = std::upper_bound(self.sorted_ends.begin(), self.sorted_ends.end(), std::make_pair(q_start, std::numeric_limits<Index>::max()));
if (it_b != self.sorted_ends.begin()) {
--it_b;
candidates.emplace_back(q_start - it_b->first, it_b->second);
}

auto it_a = std::lower_bound(self.sorted_starts.begin(), self.sorted_starts.end(), std::make_pair(q_end, Index(-1)));
if (it_a != self.sorted_starts.end()) {
candidates.emplace_back(it_a->first - q_end, it_a->second);
}

if (candidates.empty()) {
continue;
}

Position min_dist = candidates[0].first;
for (size_t k = 1; k < candidates.size(); ++k) {
if (candidates[k].first < min_dist) {
min_dist = candidates[k].first;
nclist::NearestWorkspace<Index> ws_nearest;
nclist::NearestParameters<Position> params;
params.quit_on_first = quit_on_first;
params.adjacent_equals_overlap = adjacent_equals_overlap;

nclist::nearest(self.nclist_obj, q_starts_ptr[i], q_ends_ptr[i], params, ws_nearest, nearest_matches);

if (!nearest_matches.empty()) {
if (select == "arbitrary" && !quit_on_first) {
all_results[i] = {nearest_matches.back()};
} else {
all_results[i] = nearest_matches;
}
}

std::vector<Index> best_hits;
for (const auto& cand : candidates) {
if (cand.first == min_dist) {
best_hits.push_back(cand.second);
}
}

if (select == "arbitrary") {
std::sort(best_hits.begin(), best_hits.end());
all_results[i] = {best_hits[0]};
} else {
all_results[i] = best_hits;
}
}
}, jobs_so_far, current_jobs);
jobs_so_far += current_jobs;
Expand Down Expand Up @@ -280,6 +259,166 @@ py::object perform_nearest(
}
}

struct NearestGroupInfo {
const Index* ptr;
std::size_t size;
};

pybind11::tuple perform_nearest_groups(
py::array_t<Position> self_starts,
py::array_t<Position> self_ends,
const std::vector<py::array_t<Index>>& self_groups,
py::array_t<Position> query_starts,
py::array_t<Position> query_ends,
const std::vector<py::array_t<Index>>& query_groups,
const std::string& select,
int num_threads=1,
bool adjacent_equals_overlap=false) {

auto s_starts_ptr = static_cast<const Position*>(self_starts.request().ptr);
auto s_ends_ptr = static_cast<const Position*>(self_ends.request().ptr);
auto q_starts_ptr = static_cast<const Position*>(query_starts.request().ptr);
auto q_ends_ptr = static_cast<const Position*>(query_ends.request().ptr);
std::size_t n_groups = self_groups.size();
if (n_groups != query_groups.size()) {
throw std::runtime_error("The number of self/subject groups must be equal to the number of query groups.");
}

bool quit_on_first = (select == "arbitrary");
if (select != "all" && select != "arbitrary" && !quit_on_first) {
throw std::runtime_error("Invalid 'select' parameter. Must be 'all' or 'arbitrary'.");
}

std::vector<NearestGroupInfo> self_group_info(n_groups);
std::vector<NearestGroupInfo> query_group_info(n_groups);
for (std::size_t i = 0; i < n_groups; ++i) {
auto s_req = self_groups[i].request();
self_group_info[i] = {static_cast<const Index*>(s_req.ptr), static_cast<std::size_t>(s_req.shape[0])};
auto q_req = query_groups[i].request();
query_group_info[i] = {static_cast<const Index*>(q_req.ptr), static_cast<std::size_t>(q_req.shape[0])};
}

// Building the indices in parallel with a simple worker pool.
std::vector<nclist::Nclist<Index, Position> > built(n_groups);
{
std::vector<std::thread> workers;
workers.reserve(num_threads);
std::mutex mut;
std::size_t group = 0;

for (int i = 0; i < num_threads; ++i) {
workers.emplace_back([&]() -> void {
while (1) {
std::unique_lock lck(mut);
if (group == n_groups) {
break;
}
const auto curgroup = group;
++group;
lck.unlock();

const auto& s_info = self_group_info[curgroup];
if (s_info.size != 0) {
built[curgroup] = nclist::build<Index, Position>(s_info.size, s_info.ptr, s_starts_ptr, s_ends_ptr);
}
}
});
}

for (auto& worker : workers) {
worker.join();
}
}

// Now running through all groups to find overlaps in parallel.
std::vector<std::vector<std::vector<Index> > > all_group_results(n_groups);
std::vector<std::thread> workers;
workers.reserve(num_threads);
std::vector<std::size_t> total_hits_per_thread(num_threads);

for (std::size_t g = 0; g < n_groups; ++g) {
const auto& s_info = self_group_info[g];
const auto& q_info = query_group_info[g];
if (s_info.size == 0 || q_info.size == 0) {
continue;
}

const std::size_t num_jobs = q_info.size / num_threads;
const std::size_t num_remaining = q_info.size % num_threads;
std::size_t jobs_so_far = 0;

const auto& nclist_obj = built[g];
auto& current_group_results = all_group_results[g];
current_group_results.resize(q_info.size);

workers.clear();
for (int i = 0; i < num_threads; ++i) {
const std::size_t current_jobs = num_jobs + (i < num_remaining);
if (current_jobs == 0) {
break;
}

workers.emplace_back([&](int thread, std::size_t first, std::size_t length) -> void {
std::size_t current_total_hits = 0;

for (std::size_t k = first, last = first + length; k < last; ++k) {
const Index original_query_idx = q_info.ptr[k];
auto& single_query_matches = current_group_results[k];

nclist::NearestWorkspace<Index> ws_nearest;
nclist::NearestParameters<Position> params;
params.quit_on_first = quit_on_first;
params.adjacent_equals_overlap = adjacent_equals_overlap;

nclist::nearest(nclist_obj, q_starts_ptr[original_query_idx], q_ends_ptr[original_query_idx], params, ws_nearest, single_query_matches);

if (!single_query_matches.empty()) {
if (select == "arbitrary" && !quit_on_first) {
single_query_matches.front() = single_query_matches.back();
single_query_matches.resize(1);
}
}

current_total_hits += single_query_matches.size();
}

// Don't add directly to this value in the inner loop, to reduce the risk of false sharing.
total_hits_per_thread[thread] += current_total_hits;
}, i, jobs_so_far, current_jobs);

jobs_so_far += current_jobs;
}

for (auto& worker : workers) {
worker.join();
}
}

const std::size_t total_hits = std::accumulate(total_hits_per_thread.begin(), total_hits_per_thread.end(), static_cast<std::size_t>(0));
py::array_t<Index> query_hits(total_hits);
py::array_t<Index> self_hits(total_hits);
auto q_res_ptr = static_cast<Index*>(query_hits.request().ptr);
auto s_res_ptr = static_cast<Index*>(self_hits.request().ptr);

std::size_t current_pos = 0;
for (std::size_t group_idx = 0, all_group_size=all_group_results.size(); group_idx < all_group_size; ++group_idx) {
const auto& group_res = all_group_results[group_idx];
const auto& q_info = query_group_info[group_idx];

for (std::size_t query_in_group_idx = 0, group_res_size = group_res.size(); query_in_group_idx < group_res_size; ++query_in_group_idx) {
const auto& relative_subject_matches = group_res[query_in_group_idx];
if (!relative_subject_matches.empty()) {
Index original_query_idx = q_info.ptr[query_in_group_idx];
std::copy(relative_subject_matches.begin(), relative_subject_matches.end(), s_res_ptr + current_pos);
std::fill(q_res_ptr + current_pos, q_res_ptr + current_pos + relative_subject_matches.size(), original_query_idx);
current_pos += relative_subject_matches.size();
}
}
}

return py::make_tuple(query_hits, self_hits);
}


void init_nclistsearch(pybind11::module &m){

Expand All @@ -304,5 +443,18 @@ void init_nclistsearch(pybind11::module &m){
py::arg("query_ends"),
py::arg("select") = "arbitrary",
py::arg("num_threads") = 1,
py::arg("adjacent_equals_overlap") = false,
"Find nearest ranges in both directions.");

m.def("nearest_groups", &perform_nearest_groups,
py::arg("self_starts"),
py::arg("self_ends"),
py::arg("self_groups"),
py::arg("query_starts"),
py::arg("query_ends"),
py::arg("query_groups"),
py::arg("select") = "arbitrary",
py::arg("num_threads") = 1,
py::arg("adjacent_equals_overlap") = false,
"Find nearest ranges in both directions, respecting group boundaries.");
}
Loading