Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
8935f4e
Add the traversal label oracle and /resolve support profiling
adamant-pwn Nov 1, 2026
9daa8da
Add the labeled traversal walker (v1)
adamant-pwn Nov 1, 2026
cf6790e
Rebuild the label encoder's hash buckets on load
adamant-pwn Nov 1, 2026
da2d211
Merge codex/traversal-v1-a-oracle-resolve (the label encoder fix)
adamant-pwn Nov 1, 2026
0e7fc6e
Follow every continuation of a label with an unlimited branch allowance
adamant-pwn Nov 1, 2026
1483d51
Keep g++-12 from a false array-bounds error in the walker tests
adamant-pwn Nov 2, 2026
a333abf
Merge branch 'master' into a-oracle-resolve
adamant-pwn Nov 8, 2026
278cedf
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
6961a59
Keep seed rank order and batch explicit label reads in /resolve
adamant-pwn Nov 8, 2026
c7487d3
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
6233a41
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
546ea5c
Refuse ambiguous label names
adamant-pwn Nov 8, 2026
efa09d8
Check derived header labels without resolving them
adamant-pwn Nov 8, 2026
e61a077
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
7547cf5
Bound discovery and skip columns without sequence records
adamant-pwn Nov 8, 2026
b02de7e
Skip columns without sequence records in header derivation
adamant-pwn Nov 8, 2026
16f286c
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
74b4c46
Keep the label cache bounded and make CoordToHeader::load all-or-nothing
adamant-pwn Nov 8, 2026
0ab4fa4
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
df00737
Merge remote-tracking branch 'origin/master' into a-oracle-resolve
adamant-pwn Nov 8, 2026
ffb5662
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
32426c3
Note that the label encoder's saved buckets can be dropped
adamant-pwn Nov 8, 2026
2efa49b
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
456b19a
Drop the merge_overlapping and min_block_kmers options
adamant-pwn Nov 8, 2026
958d9c3
Treat the split limit as a cap that truncates the arm
adamant-pwn Nov 8, 2026
711e2b6
Keep the derivation's working set within the live labels
adamant-pwn Nov 8, 2026
a979608
Read the time budget before a level's annotation fetch
adamant-pwn Nov 8, 2026
17e67cc
Check the branch allowance and reverse edge reuse in the walker tests
adamant-pwn Nov 8, 2026
ded320d
Remove the output-bases cap, which could never trip
adamant-pwn Nov 8, 2026
a6e6746
Expose the walk-free seed checks as check_seed
adamant-pwn Nov 8, 2026
7794210
Define check_seed outside the anonymous namespace
adamant-pwn Nov 8, 2026
9f1ff46
Select one seed again in the label-cap test
adamant-pwn Nov 8, 2026
062d5b7
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
a91576a
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
1457c72
Build /resolve discovery on the /search signature code
adamant-pwn Nov 8, 2026
6c20d6f
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
d39598a
Use remove_if to drop record-less columns from row tuples
adamant-pwn Nov 8, 2026
034a076
Always return the segment sequences
adamant-pwn Nov 8, 2026
c9b7d1d
Address cubic's notes on discovery and the test helper
adamant-pwn Nov 8, 2026
6746be6
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
9ad2f7a
Address cubic's notes on walker validation
adamant-pwn Nov 8, 2026
e80caa0
Keep the header index at 8 bytes per record
adamant-pwn Nov 8, 2026
f5306b0
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
4ad31af
Share the per-user CanonicalDBG wrapper with the aligner
adamant-pwn Nov 8, 2026
430a4f7
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
9c22969
Drop the longest_first seed policy and seed merging
adamant-pwn Nov 8, 2026
c036e8c
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 8, 2026
9c4578c
Keep decoded row-diff rows across a walk's reads
adamant-pwn Nov 9, 2026
c5c10a7
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
efc9a11
Pin the three max_support picks; say what the fetch counters count
adamant-pwn Nov 9, 2026
c90a7fc
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
d37c7b7
Drop the seed overlaps and the selection counts
adamant-pwn Nov 9, 2026
ad561bd
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
c93bd61
Drop what /resolve and the walker do not use
adamant-pwn Nov 9, 2026
df17e36
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
620d2bd
Drop SupportProfile::bp_interval
adamant-pwn Nov 9, 2026
6ca4fd0
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
0efd1f0
Report only how many labels discovery cut, and drop the dropped-label…
adamant-pwn Nov 9, 2026
7fa70b8
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
183590e
Drop the walker's digest of the derived labels it cut
adamant-pwn Nov 9, 2026
6ef125c
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
d96c5e9
Report zero passed labels on early returns; discover only resubmittab…
adamant-pwn Nov 9, 2026
83ac80a
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
2872128
Count the returned labels as kept
adamant-pwn Nov 9, 2026
d1b684c
Brace an if around EXPECT_GE (g++ -Werror=dangling-else)
adamant-pwn Nov 9, 2026
3f7adb1
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
64ba2aa
Sort discovered labels by support
adamant-pwn Nov 9, 2026
527c3ed
Walk radii from 1 in the reference test; 0 is refused
adamant-pwn Nov 9, 2026
b498d5a
Pick seeds to cover the labels; reuse master's helpers
adamant-pwn Nov 9, 2026
f16fdeb
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
4f5ba8a
Walker: reuse resolve_support, folly's FNV and the shared helpers
adamant-pwn Nov 9, 2026
327b322
Merge remote-tracking branch 'origin/master' into a-oracle-resolve
adamant-pwn Nov 9, 2026
bb83650
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
27006dc
Throw instead of exiting on unindexed queries; check label ambiguity …
adamant-pwn Nov 9, 2026
da1a5af
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
a4a0f04
Report an unsupported named seed as NO_CARRIER; test the cap checks
adamant-pwn Nov 9, 2026
7c57920
Pass errors of sequential query tasks to the caller; report both colu…
adamant-pwn Nov 9, 2026
a0e1cc2
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
0eb8545
Replace the LRU caches of NodeFirstCache and CanonicalDBG with a lock…
adamant-pwn Nov 9, 2026
7cc5bcf
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 9, 2026
69e4426
Merge remote-tracking branch 'origin/master' into a-oracle-resolve
adamant-pwn Nov 10, 2026
ea2c512
Merge branch 'a-oracle-resolve' into b-walker
adamant-pwn Nov 10, 2026
a1f569d
Merge master (#708 squashed) into b-walker
adamant-pwn Nov 10, 2026
c1f19f6
Walker: header labels follow their record's coordinates
adamant-pwn Nov 11, 2026
17e08fa
Walker: review fixes for caps, zero time budget and test coverage
adamant-pwn Nov 11, 2026
f174d08
Walker: one seed-placement path for named and derived records
adamant-pwn Nov 11, 2026
9bd9af4
Walker: sort a tuple row whose coordinates are not ascending
adamant-pwn Nov 11, 2026
066924c
Walker: skip an entry without coordinates, finish a comment
adamant-pwn Nov 11, 2026
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
60 changes: 45 additions & 15 deletions metagraph/src/graph/traversal/label_oracle.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "label_oracle.hpp"

#include <algorithm>
#include <limits>
#include <set>

#include "graph/annotated_dbg.hpp"
Expand Down Expand Up @@ -153,6 +154,7 @@ LabelQuery::LabelQuery(const LabelOracle &oracle,
if (labels_.empty())
throw std::invalid_argument("Empty permitted label set");

record_coords_.assign(labels_.size(), { 0, 0 });
std::set<std::pair<Column, uint64_t>> headers;
for (LabelId i = 0; i < labels_.size(); ++i) {
const LabelRef &ref = labels_[i];
Expand All @@ -168,6 +170,7 @@ LabelQuery::LabelQuery(const LabelOracle &oracle,
throw std::invalid_argument("Duplicate label '" + ref.name + "'");
const auto [first, last] = oracle_.coord_to_header()->sequence_coords(ref.column, ref.seq_id);
header_ranges_[ref.column].push_back(HeaderRange{ first, last, i });
record_coords_[i] = { first, last };
}
for (auto it = header_ranges_.begin(); it != header_ranges_.end(); ++it) {
std::sort(it.value().begin(), it.value().end(),
Expand All @@ -178,19 +181,19 @@ LabelQuery::LabelQuery(const LabelOracle &oracle,
tuples_ = !header_ranges_.empty() || (oracle_.has_coordinates() && oracle_.is_row_diff());
}

void LabelQuery::hits_from_row(const BinaryMatrix::SetBitPositions &row, NodeHits *hits) const {
void LabelQuery::hits_from_row(const BinaryMatrix::SetBitPositions &row, Hits *hits) const {
for (Column c : row) {
auto it = column_labels_.find(c);
if (it != column_labels_.end())
hits->push_back(it->second);
hits->labels.push_back(it->second);
}
std::sort(hits->begin(), hits->end());
std::sort(hits->labels.begin(), hits->labels.end());
}

void LabelQuery::hits_from_tuples(const MultiIntMatrix::RowTuples &row, NodeHits *hits) const {
void LabelQuery::hits_from_tuples(const MultiIntMatrix::RowTuples &row, Hits *hits) const {
for (const auto &[c, coords] : row) {
if (auto it = column_labels_.find(c); it != column_labels_.end())
hits->push_back(it->second);
hits->labels.push_back(it->second);
auto rt = header_ranges_.find(c);
if (rt == header_ranges_.end())
continue;
Expand All @@ -200,15 +203,20 @@ void LabelQuery::hits_from_tuples(const MultiIntMatrix::RowTuples &row, NodeHits
auto it = std::upper_bound(ranges.begin(), ranges.end(), coord,
[](uint64_t x, const HeaderRange &r) { return x < r.first; });
if (it != ranges.begin() && coord <= (--it)->last)
hits->push_back(it->label);
hits->coords.push_back(HeaderCoord{ it->label, coord });
}
}
std::sort(hits->begin(), hits->end());
hits->erase(std::unique(hits->begin(), hits->end()), hits->end());
std::sort(hits->coords.begin(), hits->coords.end());
// a header label is present once, however many of its coordinates the key has
for (size_t i = 0; i < hits->coords.size(); ++i) {
if (!i || hits->coords[i].label != hits->coords[i - 1].label)
hits->labels.push_back(hits->coords[i].label);
}
std::sort(hits->labels.begin(), hits->labels.end());
}

void LabelQuery::read_hits(const std::vector<node_index> &keys,
tsl::hopscotch_map<node_index, NodeHits> *out) {
tsl::hopscotch_map<node_index, Hits> *out) {
if (keys.empty())
return;
std::vector<Row> rows;
Expand All @@ -229,16 +237,23 @@ void LabelQuery::read_hits(const std::vector<node_index> &keys,
}
}

std::vector<LabelQuery::NodeHits> LabelQuery::fetch(const std::vector<node_index> &keys) {
std::vector<const LabelQuery::Hits*>
LabelQuery::fetch_hits(const std::vector<node_index> &keys) {
// on average 16 header coordinates per cached key
const size_t max_cached_coords = max_cache_size_ > std::numeric_limits<size_t>::max() / 16
? std::numeric_limits<size_t>::max() : max_cache_size_ * 16;

uncached_.clear();
std::vector<node_index> missing;
tsl::hopscotch_set<node_index> requested;
for (node_index key : keys) {
if (key != npos && !cache_.count(key) && requested.insert(key).second)
missing.push_back(key);
}
// when the new keys do not fit, clear the cache and read every key of this batch
if (cache_.size() + missing.size() > max_cache_size_) {
if (cache_.size() + missing.size() > max_cache_size_ || cached_coords_ > max_cached_coords) {
Comment thread
adamant-pwn marked this conversation as resolved.
Resolved
cache_.clear();
cached_coords_ = 0;
missing.clear();
requested.clear();
for (node_index key : keys) {
Expand All @@ -248,14 +263,29 @@ std::vector<LabelQuery::NodeHits> LabelQuery::fetch(const std::vector<node_index
}

// a batch larger than the cache is answered without caching it
tsl::hopscotch_map<node_index, NodeHits> uncached;
auto *found = missing.size() > max_cache_size_ ? &uncached : &cache_;
const bool cached = missing.size() <= max_cache_size_;
auto *found = cached ? &cache_ : &uncached_;
read_hits(missing, found);
if (cached) {
for (node_index key : missing) {
cached_coords_ += cache_.at(key).coords.size();
}
}

std::vector<NodeHits> result(keys.size());
// nothing is inserted from here on: the addresses of the hits are stable
std::vector<const Hits*> result(keys.size(), &empty_);
for (size_t i = 0; i < keys.size(); ++i) {
if (keys[i] != npos)
result[i] = found->at(keys[i]);
result[i] = &found->at(keys[i]);
}
return result;
}

std::vector<LabelQuery::NodeHits> LabelQuery::fetch(const std::vector<node_index> &keys) {
const std::vector<const Hits*> hits = fetch_hits(keys);
std::vector<NodeHits> result(keys.size());
for (size_t i = 0; i < keys.size(); ++i) {
result[i] = hits[i]->labels;
}
return result;
}
Expand Down
55 changes: 46 additions & 9 deletions metagraph/src/graph/traversal/label_oracle.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ class LabelOracle {
/**
* Batched membership of a fixed set of permitted labels at annotation keys. Column
* labels are read from whole rows; header labels (one sequence of a column) from tuple
* rows, a coordinate counting for a label when it falls into that label's sequence.
* Results are cached per key, up to |max_cache_size| keys, then the cache is cleared. A
* batch with more keys than that is answered without caching it. On a row-diff
* annotation, the decoded rows are kept too (RowDiffCache), so that the rows of the next
* step of a walk are decoded from them.
* rows, a coordinate counting for a label when it falls into that label's sequence. For
* header labels the coordinates themselves are kept too (Hits::coords), so that a walk
* can follow a record in coordinate order instead of by membership.
* Results are cached per key, up to |max_cache_size| keys (and 16 header coordinates per
* key on average), then the cache is cleared. A batch with more keys than that is
* answered without caching it. On a row-diff annotation, the decoded rows are kept too
* (RowDiffCache), so that the rows of the next step of a walk are decoded from them.
*/
class LabelQuery {
public:
Expand All @@ -124,8 +126,35 @@ class LabelQuery {
std::vector<LabelRef> labels,
size_t max_cache_size = 1'000'000);

// A coordinate of a HEADER label's sequence record
struct HeaderCoord {
LabelId label;
uint64_t coord;
bool operator==(const HeaderCoord &o) const {
return label == o.label && coord == o.coord;
}
bool operator<(const HeaderCoord &o) const {
return label != o.label ? label < o.label : coord < o.coord;
}
};
struct Hits {
// the permitted labels present at the key, ascending
NodeHits labels;
// for the HEADER labels among them, the coordinates of the key that fall into the
// label's record, ascending by (label, coordinate)
std::vector<HeaderCoord> coords;
};

// Hits for every annotation key (npos yields no hits).
std::vector<NodeHits> fetch(const std::vector<node_index> &keys);
// The same with the header coordinates, without copying them: the pointers are valid
// until the next fetch() or fetch_hits().
std::vector<const Hits*> fetch_hits(const std::vector<node_index> &keys);

// The first and last coordinate of a HEADER label's record ({ 0, 0 } for a COLUMN label)
const std::pair<uint64_t, uint64_t>& record_coords(LabelId label) const {
return record_coords_[label];
}

private:
struct HeaderRange {
Expand All @@ -135,16 +164,18 @@ class LabelQuery {
};

void read_hits(const std::vector<node_index> &keys,
tsl::hopscotch_map<node_index, NodeHits> *out);
tsl::hopscotch_map<node_index, Hits> *out);
void hits_from_row(const annot::matrix::BinaryMatrix::SetBitPositions &row,
NodeHits *hits) const;
Hits *hits) const;
void hits_from_tuples(const annot::matrix::MultiIntMatrix::RowTuples &row,
NodeHits *hits) const;
Hits *hits) const;

const LabelOracle &oracle_;
std::vector<LabelRef> labels_;
bool tuples_ = false;
size_t max_cache_size_;
// per label, the coordinate range of its record
std::vector<std::pair<uint64_t, uint64_t>> record_coords_;

// column -> label id for COLUMN labels
tsl::hopscotch_map<Column, LabelId> column_labels_;
Expand All @@ -153,7 +184,13 @@ class LabelQuery {
// mapped to its sequence
tsl::hopscotch_map<Column, std::vector<HeaderRange>> header_ranges_;

tsl::hopscotch_map<node_index, NodeHits> cache_;
tsl::hopscotch_map<node_index, Hits> cache_;
// the header coordinates held by |cache_|: a repetitive k-mer has many per label, so
// the cache is bounded by them as well as by its keys
size_t cached_coords_ = 0;
// the hits of a batch too large to cache, kept until the next fetch
tsl::hopscotch_map<node_index, Hits> uncached_;
const Hits empty_ {};
annot::matrix::RowDiffCache<annot::matrix::BinaryMatrix::SetBitPositions> row_cache_;
annot::matrix::RowDiffCache<annot::matrix::MultiIntMatrix::RowTuples> tuple_cache_;
};
Expand Down
Loading
Loading