Push zunrplorkwkt #70

Merged
coissac merged 93 commits from push-zunrplorkwkt into main 2026-08-28 23:15:38 +00:00
60 changed files with 3521 additions and 13 deletions
Showing only changes of commit a4eb20e67e - Show all commits
+28
View File
@@ -939,6 +939,34 @@
<li class="md-nav__item">
<a href="/implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -948,6 +948,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -946,6 +946,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -948,6 +948,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -9,7 +9,7 @@
<link rel="prev" href="../../../implementation/obitaxonomy/">
<link rel="prev" href="../../../implementation/benchmark_query_testing/">
<link rel="next" href="../../index_architecture/">
@@ -948,6 +948,34 @@
<li class="md-nav__item">
<a href="../../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+127
View File
@@ -948,6 +948,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1289,6 +1317,17 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#query-never-benefits-from-sparse-row-major-access-found-2026-08-19-not-implemented" class="md-nav__link">
<span class="md-ellipsis">
query never benefits from sparse row-major access (found 2026-08-19, not implemented)
</span>
</a>
</li>
</ul>
@@ -1552,6 +1591,17 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#query-never-benefits-from-sparse-row-major-access-found-2026-08-19-not-implemented" class="md-nav__link">
<span class="md-ellipsis">
query never benefits from sparse row-major access (found 2026-08-19, not implemented)
</span>
</a>
</li>
</ul>
@@ -2319,6 +2369,83 @@ scan the full unsampled index — never threaded <code>--subsample</code>/<code>
out of scope here since the reported problem was specifically about the
<code>--sankoff</code>/<code>--tnt</code> pipeline's redundant/inconsistent scans, not these
two standalone flags.</p>
<h2 id="query-never-benefits-from-sparse-row-major-access-found-2026-08-19-not-implemented"><code>query</code> never benefits from sparse row-major access (found 2026-08-19, not implemented)</h2>
<p>Benchmarked <code>obikmer query</code> against <code>global_index_presence</code> (dense-packed)
vs. <code>global_index_presence_sparse</code> (<code>pack --sparse</code>), 100k simulated reads
× 2 specimens (<code>benchmark/</code>, see
<a href="../../implementation/benchmark_query_testing/">benchmark_query_testing.md</a>).
Correctness: 0 mismatches — sparse and dense return bit-identical query
results. Performance: sparse consistently <em>slower</em> than dense (~30-50%,
reproducible across two runs with warm disk cache), the opposite of
<code>pack --sparse</code>'s stated intent ("faster for single-row access... like
query").</p>
<p><strong>Root cause, read from source, not measured in isolation:</strong>
<code>KmerPartition::query_partition_with</code> (<code>obikpartitionner/src/query_layer.rs:155-220</code>)
is architecturally column-major: stage 2 walks <code>for g in 0..n_cols { for
slot in hit_slots { layer.col_value(g, slot) } }</code>, documented (correctly)
as the right locality strategy for the packed/columnar formats, where
<code>col_value</code><code>PersistentBitMatrix::get</code> is a genuine O(1) mmap'd column
read (<code>persistent.rs:110-113</code>).</p>
<p>For <code>Self::Sparse</code>, that same <code>get(c, slot)</code> (<code>persistent.rs:114-118</code>)
allocates a full <code>n_cols</code>-wide buffer and calls <code>fill_row</code> — materializing
the <em>entire row</em> — just to return one cell. Called from inside the
column-major double loop, this reconstructs the same row once per genome
column touched: O(hits × n_cols) full-row rebuilds instead of O(hits).
<code>PersistentSparseBitMatrix</code>'s own native row-major decode
(<code>for_each_genome_in_row</code>, <code>sparse.rs:164-177</code>, used correctly by its own
<code>row</code>/<code>fill_row</code>/<code>fill_sub_matrix</code>) is never reached from the query path
at all.</p>
<p><strong><code>fill_sub_matrix</code> (the existing <code>BinaryMatrix</code> trait primitive,
<code>traits.rs:13-37</code>) is not the right replacement for <code>query</code> either</strong>, even
once its own dispatch bug is fixed (see next section) — its output shape
is inherently column-dense: <code>out[col]</code> gets an entry for every column,
including columns with zero hits among the requested slots. On real
sparse data (a hit typically touching a handful of genomes out of dozens)
that's still O(n_cols) output regardless of true sparsity. What <code>query</code>
actually wants is the sparse triple stream <code>(slot, col, value)</code> it already
consumes as <code>QueryHit::Value</code> — not a materialized sub-matrix.</p>
<p><strong>Proposed primitive</strong> (design only, not implemented — explicit ask: keep
count matrices <em>not excluded</em>, even though effort right now is
presence/absence only):</p>
<div class="highlight"><pre><span></span><code><span class="sd">/// Visit every nonzero cell among `slots`. Order unspecified.</span>
<span class="k">fn</span><span class="w"> </span><span class="nf">for_each_nonzero</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">],</span><span class="w"> </span><span class="n">f</span><span class="p">:</span><span class="w"> </span><span class="nc">impl</span><span class="w"> </span><span class="nb">FnMut</span><span class="p">(</span><span class="kt">usize</span><span class="w"> </span><span class="cm">/*idx into slots*/</span><span class="p">,</span><span class="w"> </span><span class="kt">usize</span><span class="w"> </span><span class="cm">/*col*/</span><span class="p">,</span><span class="w"> </span><span class="kt">u32</span><span class="w"> </span><span class="cm">/*value*/</span><span class="p">));</span>
</code></pre></div>
<ul>
<li>On <code>PersistentSparseBitMatrix</code>: native override, one pass per slot via
the existing (currently private) <code>for_each_genome_in_row</code> — O(Σ row
nnz), zero n_cols-wide allocation. This is the whole point: expose code
that already exists rather than write anything new for the sparse side.</li>
<li>On <code>PersistentBitMatrix::{Packed,Columnar,Implicit}</code>: <strong>provided
default</strong>, derived from <code>fill_sub_matrix</code> (materialize, then filter to
true cells) — reuses the already-optimal column-major/mmap path for
those formats, no new code needed there either.</li>
<li><strong>On <code>PersistentCompactIntMatrix</code> (counts)</strong>: same provided-default
treatment, derived from its own existing <code>fill_sub_matrix</code> (<code>u32</code>-typed
already, <code>intmatrix.rs:387</code>) — not hand-optimized (no sparse count
format exists — "Explicitly deferred" per <code>traits.rs:9-12</code>), but not
excluded either: it gets a working, not-pathological implementation for
free today, on the same trait, ready for a native override the day a
sparse count format lands. This is why the signature carries <code>u32</code>
rather than <code>bool</code> — presence is <code>0</code>/<code>1</code>, counts are <code>u32</code>, one trait
covers both without a bool/u32 split forcing counts out of the design.</li>
</ul>
<p>Would let <code>query_partition_with</code>'s stage 2 collapse to one
<code>layer.matrix().for_each_nonzero(&amp;hit_slots, |i, g, v| on_event(...))</code>
call per layer, format-agnostic, with each backend's existing (or
default-derived) implementation deciding the actual access pattern.</p>
<p><strong>Separately, an existing bug in the generic path</strong> (found while tracing
this, itself not yet fixed): <code>PersistentBitMatrix::fill_sub_matrix</code>
(<code>persistent.rs:190-215</code>, the enum wrapper backing <code>BinaryMatrix</code>'s
default trait impl) does <em>not</em> delegate to
<code>PersistentSparseBitMatrix::fill_sub_matrix</code> for <code>Self::Sparse</code> — it
reimplements the same naive per-(column, slot) <code>fill_row_bool</code> loop
instead, bypassing the efficient native method one file over
(<code>sparse.rs:249-258</code>). <code>obikphylo::siblings::cache::Mat</code>
(<code>cache.rs:138-145</code>) independently built its own parallel enum wrapper
that dispatches correctly — a sign this was worked around rather than
fixed at the source. Any future <code>for_each_nonzero</code> work should fix this
dispatch too (or route through it), rather than adding a third
independently-dispatching wrapper.</p>
File diff suppressed because it is too large Load Diff
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -497,6 +497,18 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="../benchmark_query_testing/">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1085,6 +1085,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1208,6 +1208,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1085,6 +1085,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1157,6 +1157,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1152,6 +1152,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1163,6 +1163,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+34 -11
View File
@@ -412,6 +412,18 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="../benchmark_query_testing/">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
@@ -1144,29 +1156,40 @@ Pass 1 — byte max, SIMD-vectorizable, O(n)
</code></pre></div>
<hr/>
<h2 id="matrix-types">Matrix types</h2>
<p>Four matrix types, two encodings × two formats:</p>
<p>Both matrix types are enums behind a transparent API — the caller never matches on the variant. <code>PersistentCompactIntMatrix</code> has two variants (<code>Columnar</code>, <code>Packed</code>). <code>PersistentBitMatrix</code> has four:</p>
<table>
<thead>
<tr>
<th></th>
<th>Columnar format</th>
<th>Packed format</th>
<th>Variant</th>
<th>Storage</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Bit</strong></td>
<td><code>PersistentBitMatrix</code> (Columnar variant)</td>
<td><code>PersistentBitMatrix</code> (Packed variant)</td>
<td><code>Columnar</code></td>
<td>one <code>.pbiv</code>/<code>.pciv</code> file per column + <code>meta.json</code></td>
<td>build-time default (<code>*Builder::new</code>)</td>
</tr>
<tr>
<td><strong>Int</strong></td>
<td><code>PersistentCompactIntMatrix</code> (Columnar variant)</td>
<td><code>PersistentCompactIntMatrix</code> (Packed variant)</td>
<td><code>Packed</code></td>
<td>single <code>matrix.pbmx</code> mmap file</td>
<td>query-optimised, produced by <code>pack_bit_matrix</code>/<code>pack_compact_int_matrix</code></td>
</tr>
<tr>
<td><code>Sparse</code> (bit only)</td>
<td><code>sparse_meta.json</code> + PFIV/Elias-Fano component files, row-major</td>
<td><code>pack --sparse</code>; see <a href="../../architecture/siblings/">siblings.md</a> for the sparse-vs-dense access-pattern trade-off</td>
</tr>
<tr>
<td><code>Implicit</code> (bit only)</td>
<td>no file at all</td>
<td>mono-genome presence layers — <code>n_cols</code> is always reported as <code>1</code>, every value is <code>true</code></td>
</tr>
</tbody>
</table>
<p>Both matrix types are enums (<code>Columnar</code> / <code>Packed</code> / <code>Implicit</code> for bit) behind a transparent API. <code>col_view(c)</code> returns the appropriate view directly:</p>
<p><code>PersistentBitMatrix::open(layer_dir)</code> auto-detects the variant, in order: <code>matrix.pbmx</code> → Packed, <code>presence/meta.json</code> → Columnar, <code>presence/sparse_meta.json</code> → Sparse, <code>layer_meta.json</code> (no presence dir at all) → Implicit. <code>col_view</code>/<code>col</code>/<code>sub_matrix</code> panic on <code>Sparse</code>/<code>Implicit</code> where the operation has no direct-slice equivalent (Sparse is k-mer-major, not column-major; Implicit has no backing storage) — callers needing per-column data on those variants go through <code>row</code>/<code>fill_row</code>.</p>
<p><code>col_view(c)</code> returns the appropriate view directly:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// PersistentBitMatrix</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">col_view</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">c</span><span class="p">:</span><span class="w"> </span><span class="kt">usize</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nc">BitSliceView</span><span class="o">&lt;'</span><span class="nb">_</span><span class="o">&gt;</span>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -940,6 +940,67 @@
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#layerd-raw-mapping-iteration-and-batch-access" class="md-nav__link">
<span class="md-ellipsis">
Layer\&lt;D> — raw mapping, iteration, and batch access
</span>
</a>
<nav class="md-nav" aria-label="Layer\&lt;D&gt; — raw mapping, iteration, and batch access">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#raw-kmer-slot-mapping" class="md-nav__link">
<span class="md-ellipsis">
Raw kmer → slot mapping
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#kmer-iteration" class="md-nav__link">
<span class="md-ellipsis">
Kmer iteration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#batch-lookup-on-payload-vectorsviews" class="md-nav__link">
<span class="md-ellipsis">
Batch lookup on payload vectors/views
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sub_matrix-fill_sub_matrix" class="md-nav__link">
<span class="md-ellipsis">
sub_matrix / fill_sub_matrix
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -1263,6 +1324,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1638,6 +1727,67 @@
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#layerd-raw-mapping-iteration-and-batch-access" class="md-nav__link">
<span class="md-ellipsis">
Layer\&lt;D> — raw mapping, iteration, and batch access
</span>
</a>
<nav class="md-nav" aria-label="Layer\&lt;D&gt; — raw mapping, iteration, and batch access">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#raw-kmer-slot-mapping" class="md-nav__link">
<span class="md-ellipsis">
Raw kmer → slot mapping
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#kmer-iteration" class="md-nav__link">
<span class="md-ellipsis">
Kmer iteration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#batch-lookup-on-payload-vectorsviews" class="md-nav__link">
<span class="md-ellipsis">
Batch lookup on payload vectors/views
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sub_matrix-fill_sub_matrix" class="md-nav__link">
<span class="md-ellipsis">
sub_matrix / fill_sub_matrix
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -2000,6 +2150,38 @@
</code></pre></div>
<p>Mode 3 (<code>PersistentBitMatrix</code>) has no <code>push_layer</code> on <code>LayeredMap</code>; callers build directly via <code>Layer&lt;PersistentBitMatrix&gt;::build_presence</code>.</p>
<hr />
<h2 id="layerd-raw-mapping-iteration-and-batch-access">Layer\&lt;D&gt; — raw mapping, iteration, and batch access</h2>
<p>Beyond <code>query</code>/<code>find</code> (membership-checked), <code>Layer&lt;D&gt;</code> exposes lower-level access used by consumers that already know a kmer is in the layer (e.g. cross-partition sibling resolution) or that need to sweep every kmer/slot without paying for a membership check each time.</p>
<h3 id="raw-kmer-slot-mapping">Raw kmer → slot mapping</h3>
<div class="highlight"><pre><span></span><code><span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">index</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">kmer</span><span class="p">:</span><span class="w"> </span><span class="nc">CanonicalKmer</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="kt">usize</span>
<span class="nc">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">index_batch</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">kmers</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="n">CanonicalKmer</span><span class="p">])</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">usize</span><span class="o">&gt;</span>
</code></pre></div>
<p>Pure MPHF mapping, no evidence/fingerprint check — equivalent to <code>MphfOnly::index</code>. Only meaningful when the caller already knows <code>kmer</code> belongs to the layer; on an absent kmer the MPHF still returns <em>some</em> slot (undefined, not <code>None</code>).</p>
<h3 id="kmer-iteration">Kmer iteration</h3>
<p>Four iterators, all built from <code>unitigs.bin</code> (physical layout order, <strong>not</strong> correlated with MPHF slot numbers):</p>
<div class="highlight"><pre><span></span><code><span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">iter_kmers</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nc">KmerIter</span><span class="o">&lt;&#39;</span><span class="nb">_</span><span class="o">&gt;</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">enumerate_kmers</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nc">Enumerate</span><span class="o">&lt;</span><span class="n">KmerIter</span><span class="o">&lt;&#39;</span><span class="nb">_</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="c1">// (order_index, kmer)</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">iter_kmers_batch</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p">:</span><span class="w"> </span><span class="kt">usize</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nc">KmerBatchIter</span><span class="o">&lt;&#39;</span><span class="nb">_</span><span class="o">&gt;</span><span class="w"> </span><span class="c1">// Vec&lt;CanonicalKmer&gt; of size ≤ n</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">enumerate_kmers_batch</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p">:</span><span class="w"> </span><span class="kt">usize</span><span class="p">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nc">impl</span><span class="w"> </span><span class="nb">Iterator</span><span class="o">&lt;</span><span class="n">Item</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="kt">usize</span><span class="p">,</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">CanonicalKmer</span><span class="o">&gt;</span><span class="p">)</span><span class="o">&gt;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nb">Send</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="o">&#39;</span><span class="nb">static</span>
</code></pre></div>
<p><code>KmerIter</code>/<code>KmerBatchIter</code> own a clone of the underlying <code>Arc&lt;UnitigFileReader&gt;</code> rather than borrowing <code>self</code><code>Send + 'static</code>, streamed from disk one kmer at a time, never materialised as a whole. Multiple instances can coexist concurrently, each with its own cursor. <code>enumerate_kmers_batch</code>'s index is the batch's starting offset in iteration order (a multiple of <code>n</code> except for the final, possibly shorter, batch).</p>
<h3 id="batch-lookup-on-payload-vectorsviews">Batch lookup on payload vectors/views</h3>
<p><code>PersistentCompactIntVec</code>, <code>PersistentBitVec</code>, <code>IntSliceView</code>, <code>BitSliceView</code> all expose:</p>
<div class="highlight"><pre><span></span><code><span class="k">fn</span><span class="w"> </span><span class="nf">get_batch</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">])</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span>
<span class="k">fn</span><span class="w"> </span><span class="nf">fill_batch</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">],</span><span class="w"> </span><span class="n">out</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="nc">mut</span><span class="w"> </span><span class="p">[</span><span class="n">T</span><span class="p">])</span>
</code></pre></div>
<p>Both sort <code>slots</code> internally for sequential mmap access, then reorder results back to the caller's original order. <code>fill_batch</code> fills a caller-provided buffer, avoiding the <code>Vec</code> allocation.</p>
<h3 id="sub_matrix-fill_sub_matrix">sub_matrix / fill_sub_matrix</h3>
<div class="highlight"><pre><span></span><code><span class="c1">// Layer&lt;PersistentCompactIntMatrix&gt;</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">sub_matrix</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">])</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u32</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="c1">// column-first</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">fill_sub_matrix</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">],</span><span class="w"> </span><span class="n">out</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="nc">mut</span><span class="w"> </span><span class="p">[</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u32</span><span class="o">&gt;</span><span class="p">])</span>
<span class="c1">// Layer&lt;PersistentBitMatrix&gt; (and any D: BinaryMatrix, e.g. PersistentSparseBitMatrix)</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">sub_matrix</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">])</span><span class="w"> </span><span class="p">-&gt;</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;&gt;</span>
<span class="k">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">fill_sub_matrix</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">slots</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="p">[</span><span class="kt">usize</span><span class="p">],</span><span class="w"> </span><span class="n">out</span><span class="p">:</span><span class="w"> </span><span class="kp">&amp;</span><span class="nc">mut</span><span class="w"> </span><span class="p">[</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">bool</span><span class="o">&gt;</span><span class="p">])</span>
</code></pre></div>
<p>Column-first to match the on-disk column-major layout. <code>fill_sub_matrix</code> sorts <code>slots</code> once, then calls each column's <code>fill_batch</code> in turn — no redundant per-column sort. On <code>PersistentSparseBitMatrix</code> (k-mer-major, no column method) this degrades to a row-by-row decode; see <a href="../../architecture/siblings/">siblings.md</a>.</p>
<hr />
<h2 id="layeredstores-and-aggregation-traits">LayeredStore\&lt;S&gt; and aggregation traits</h2>
<p><code>LayeredStore&lt;S&gt;</code> is a generic aggregation wrapper over <code>Vec&lt;S&gt;</code>. It propagates three traits from <code>obicompactvec::traits</code> up the hierarchy via blanket impls:</p>
<div class="highlight"><pre><span></span><code><span class="k">pub</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">LayeredStore</span><span class="o">&lt;</span><span class="n">S</span><span class="o">&gt;</span><span class="p">(</span><span class="k">pub</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">S</span><span class="o">&gt;</span><span class="p">);</span>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1113,6 +1113,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+29 -1
View File
@@ -12,7 +12,7 @@
<link rel="prev" href="../select/">
<link rel="next" href="../../architecture/sequences/invariant/">
<link rel="next" href="../benchmark_query_testing/">
@@ -1125,6 +1125,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1225,6 +1225,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1208,6 +1208,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1074,6 +1074,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1196,6 +1196,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1146,6 +1146,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1052,6 +1052,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1174,6 +1174,34 @@
<li class="md-nav__item">
<a href="../benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1048,6 +1048,34 @@
<li class="md-nav__item">
<a href="implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1134,6 +1134,34 @@
<li class="md-nav__item">
<a href="../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1047,6 +1047,34 @@
<li class="md-nav__item">
<a href="../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
Binary file not shown.
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1019,6 +1019,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1085,6 +1085,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
@@ -1461,6 +1461,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1019,6 +1019,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -944,6 +944,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+28
View File
@@ -1063,6 +1063,34 @@
<li class="md-nav__item">
<a href="../../implementation/benchmark_query_testing/" class="md-nav__link">
<span class="md-ellipsis">
Benchmark: query-path testing
</span>
</a>
</li>
</ul>
</nav>
+126
View File
@@ -756,3 +756,129 @@ scan the full unsampled index — never threaded `--subsample`/`--entropy`,
out of scope here since the reported problem was specifically about the
`--sankoff`/`--tnt` pipeline's redundant/inconsistent scans, not these
two standalone flags.
## `query` never benefits from sparse row-major access (found 2026-08-19, not implemented)
Benchmarked `obikmer query` against `global_index_presence` (dense-packed)
vs. `global_index_presence_sparse` (`pack --sparse`), 100k simulated reads
× 2 specimens (`benchmark/`, see
[benchmark_query_testing.md](../implementation/benchmark_query_testing.md)).
Correctness: 0 mismatches — sparse and dense return bit-identical query
results. Performance: sparse consistently *slower* than dense (~30-50%,
reproducible across two runs with warm disk cache), the opposite of
`pack --sparse`'s stated intent ("faster for single-row access... like
query").
**Root cause, read from source, not measured in isolation:**
`KmerPartition::query_partition_with` (`obikpartitionner/src/query_layer.rs:155-220`)
is architecturally column-major: stage 2 walks `for g in 0..n_cols { for
slot in hit_slots { layer.col_value(g, slot) } }`, documented (correctly)
as the right locality strategy for the packed/columnar formats, where
`col_value``PersistentBitMatrix::get` is a genuine O(1) mmap'd column
read (`persistent.rs:110-113`).
For `Self::Sparse`, that same `get(c, slot)` (`persistent.rs:114-118`)
allocates a full `n_cols`-wide buffer and calls `fill_row` — materializing
the *entire row* — just to return one cell. Called from inside the
column-major double loop, this reconstructs the same row once per genome
column touched: O(hits × n_cols) full-row rebuilds instead of O(hits).
`PersistentSparseBitMatrix`'s own native row-major decode
(`for_each_genome_in_row`, `sparse.rs:164-177`, used correctly by its own
`row`/`fill_row`/`fill_sub_matrix`) is never reached from the query path
at all.
**`fill_sub_matrix` (the existing `BinaryMatrix` trait primitive,
`traits.rs:13-37`) is not the right replacement for `query` either**, even
once its own dispatch bug is fixed (see next section) — its output shape
is inherently column-dense: `out[col]` gets an entry for every column,
including columns with zero hits among the requested slots. On real
sparse data (a hit typically touching a handful of genomes out of dozens)
that's still O(n_cols) output regardless of true sparsity. What `query`
actually wants is the sparse triple stream `(slot, col, value)` it already
consumes as `QueryHit::Value` — not a materialized sub-matrix.
**Proposed primitive** (design only, not implemented — explicit ask: keep
count matrices *not excluded*, even though effort right now is
presence/absence only):
Not a closure-driven `for_each` — a real `Iterator`, one concrete struct
per matrix format, so the traversal state (current position in the sorted
slot list, current column, permutation, sparse-row decode cursor…) lives
in named struct fields instead of being threaded implicitly through
recursion or a captured closure. RPITIT (stable since 1.75, and this
workspace is edition 2024) means the trait method can return it without
naming or boxing the concrete type:
```rust
/// Yields every nonzero cell among `slots`, in implementation-defined order.
fn nonzero_iter<'a>(&'a self, slots: &'a [usize]) -> impl Iterator<Item = (usize, usize, u32)> + 'a;
// item: (idx into `slots`, col, value)
```
This is the one foundational primitive per format — both `fill_sub_matrix`
and a `for_each`-style callback become trivial, free consumers of it
(`.for_each(f)` is already `Iterator::for_each` from std; `fill_sub_matrix`
becomes "drain the iterator, scatter into `out[][]`"), instead of two
independently-maintained traversals that can silently diverge (see the bug
below — this is exactly how it happened).
- **On `PersistentSparseBitMatrix`**: the struct is nearly free to write —
it wraps the existing (currently private) `for_each_genome_in_row`
per-row decode, advancing to the next `slots` entry on exhaustion. O(Σ
row nnz), zero `n_cols`-wide allocation.
- **On `PersistentBitMatrix::{Packed,Columnar}`**: revised — cheaper than
first thought, by reusing the same split already used for
`fill_matrix`'s own implementation instead of hand-writing a resumable
state machine at the matrix level. The base-vector layer
(`BitSliceView`, `views.rs`) already separates the two concerns:
`fill_batch_sorted` (`views.rs:55-60`, sorted-slot batch lookup) and a
genuine per-bit `Iterator` (`BitSliceIter`, `views.rs:94+`) sit side by
side there, one level below the matrix. Adding a
"positions among `sorted_slots` where the bit is set" iterator at that
same vector level is a `filter` over the existing `get()` — no new
state machine, since `std::iter::Filter` already *is* one, generated by
the compiler:
```rust
fn nonzero_among_sorted<'s>(&'s self, sorted_slots: &'s [usize]) -> impl Iterator<Item = usize> + 's {
sorted_slots.iter().copied().filter(move |&slot| self.get(slot))
}
```
The matrix-level `nonzero_iter` then composes these per column with
`flat_map` over `0..n_cols` (each column's hits, tagged with `c`,
slot mapped back through the sort permutation `fill_batch`/
`fill_batch_sorted` already carry) — again a combinator chain, not a
hand-rolled struct. Same algorithm, same mmap/sort locality as today's
`fill_sub_matrix`; just assembled from `std` iterator adaptors instead
of a loop body writing into a buffer, mirroring the vector/matrix split
the codebase already uses for `fill_batch_sorted` rather than
introducing a new shape.
- **`Implicit`**: trivial (`slots.iter().map(|&i| (i, 0, 1))`, one column,
always present).
- **On `PersistentCompactIntMatrix` (counts)**: same treatment as
`Packed`/`Columnar` — no sparse count format exists yet ("Explicitly
deferred" per `traits.rs:9-12`), so no native low-effort case the way
`Sparse` has one, but not excluded either: the iterator's `Item` is
already `(usize, usize, u32)`, not `bool`, specifically so presence
(`0`/`1`) and counts (arbitrary `u32`) share one primitive instead of a
bool/u32 split forcing counts out of the design. Ready for a native
sparse-count struct later without a signature change.
Would let `query_partition_with`'s stage 2 collapse to one
`for (i, g, v) in layer.matrix().nonzero_iter(&hit_slots) { on_event(...) }`
per layer, format-agnostic, each backend's struct deciding the actual
traversal.
**This also closes the existing dispatch bug for free, by construction**:
`PersistentBitMatrix::fill_sub_matrix` (`persistent.rs:190-215`, the enum
wrapper backing `BinaryMatrix`'s trait impl) today does *not* delegate to
`PersistentSparseBitMatrix::fill_sub_matrix` for `Self::Sparse` — it
reimplements the same naive per-(column, slot) `fill_row_bool` loop
instead, bypassing the efficient native method one file over
(`sparse.rs:249-258`). `obikphylo::siblings::cache::Mat`
(`cache.rs:138-145`) independently built its own parallel enum wrapper
that dispatches correctly — evidence this was worked around rather than
fixed at the source: two hand-written traversals for the same format,
free to drift apart, and they did. If `fill_sub_matrix` itself is
rewritten as "drain `nonzero_iter`, scatter into `out[][]`", there is only
one traversal per format left to get right — the bug class doesn't just
get fixed once, it stops being possible to reintroduce.
Binary file not shown.