From 4838528734daa76598fb9251dee7d08e89840507 Mon Sep 17 00:00:00 2001 From: Erik Garrison Date: Thu, 26 Mar 2026 15:59:24 -0500 Subject: [PATCH] active-set block skipping for label propagation rounds 2+ After each round's pointer jumping, track which positions changed labels in an AtomicBitVec. In subsequent rounds, skip blocks where no source or target endpoint position was in the changed set. This is correct because unchanged positions cannot benefit from re-hooking. Round 3+ skip 56-58M of 58M blocks (~1s per round vs ~8s without skip). Round 2 skips 1.5M blocks (most positions change after round 1). Total phase 2: ~50s vs ~62s. Output identical (2727 nodes, 52047bp). --- src/transclosure.rs | 90 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/src/transclosure.rs b/src/transclosure.rs index b7989c8..a1bece7 100644 --- a/src/transclosure.rs +++ b/src/transclosure.rs @@ -93,6 +93,23 @@ impl LabelProp { changed } + /// Pointer jumping pass that tracks which positions changed. + /// Sets changed_bv[i] for any position whose label changed. + /// Returns the total number of changes. + fn pointer_jump_tracking(&self, changed_bv: &AtomicBitVec) -> u64 { + let changed = AtomicU64::new(0); + self.parent.par_iter().enumerate().for_each(|(i, p)| { + let cur = p.load(Ordering::Relaxed); + let grandparent = self.parent[cur as usize].load(Ordering::Relaxed); + if cur != grandparent { + p.store(grandparent, Ordering::Relaxed); + changed_bv.set(i, true, Ordering::Relaxed); + changed.fetch_add(1, Ordering::Relaxed); + } + }); + changed.load(Ordering::Relaxed) + } + /// Full pointer chase to root (for final readout, not used during rounds) fn find(&self, mut i: usize) -> u32 { loop { @@ -998,11 +1015,15 @@ pub fn compute_transitive_closures( let component_seqs = find_component_sequences(&seqidx, &q_curr_bv_final); let mut phase2_round = 0u32; + let mut active_positions: Option = None; loop { let hooks_made = AtomicU64::new(0); - let _blocks_skipped = 0u64; // removed: no block-level skipping + let blocks_skipped = AtomicU64::new(0); + let active_ref = &active_positions; - // Hook step: for each block, propagate minimum labels + // Hook step: for each block, propagate minimum labels. + // After round 1, skip blocks where no source or target position + // changed in the previous round (tracked by active_positions bitmap). component_seqs.par_iter().for_each(|&seq_id| { let seq_off = seqidx.nth_seq_offset(seq_id).unwrap(); let seq_len = seqidx.nth_seq_length(seq_id).unwrap(); @@ -1016,6 +1037,40 @@ pub fn compute_transitive_closures( return; } + // Skip blocks with no changed positions (rounds 2+) + if let Some(active) = active_ref { + let t_first = offset(target_pos) as usize; + if t_first < input_seq_length + && q_curr_bv_ref.get(t_first, Ordering::Relaxed) + { + let s_rank = rank_table[start as usize] as usize; + let t_rank = rank_table[t_first] as usize; + // Check if any endpoint was active + let block_len = end - start; + let s_last_rank = rank_table[(end - 1) as usize] as usize; + let mut t_last_p = target_pos; + incr_pos_by(&mut t_last_p, (block_len - 1) as usize); + let t_last_off = offset(t_last_p) as usize; + let t_last_rank = if t_last_off < input_seq_length + && q_curr_bv_ref.get(t_last_off, Ordering::Relaxed) + { + rank_table[t_last_off] as usize + } else { + s_rank // force active if target out of bounds + }; + + // Skip if no endpoint or its neighbors changed + if !active.get(s_rank, Ordering::Relaxed) + && !active.get(t_rank, Ordering::Relaxed) + && !active.get(s_last_rank, Ordering::Relaxed) + && !active.get(t_last_rank, Ordering::Relaxed) + { + blocks_skipped.fetch_add(1, Ordering::Relaxed); + return; + } + } + } + // Process block using range hook when possible. // If both source and target first positions are in curr_bv, // use the fast rank-range path (consecutive ranks within a sequence). @@ -1058,36 +1113,37 @@ pub fn compute_transitive_closures( .ok(); }); - // Pointer jump until stable - let mut jump_changes = labels.pointer_jump(); - while jump_changes > 0 { - jump_changes = labels.pointer_jump(); + // Pointer jump until stable, tracking which positions changed + // so the next round can skip blocks with no changed positions. + let changed_bv = AtomicBitVec::new(q_curr_bv_count); + loop { + let jump_changes = labels.pointer_jump_tracking(&changed_bv); + if jump_changes == 0 { + break; + } } let hooks = hooks_made.load(Ordering::Relaxed); phase2_round += 1; + let skipped = blocks_skipped.load(Ordering::Relaxed); eprintln!( - "[transclosure] {:.3}s phase2 round {}: {} hooks", + "[transclosure] {:.3}s phase2 round {}: {} hooks, {} blocks skipped", start_time.elapsed().as_secs_f64(), phase2_round, hooks, + skipped, ); if hooks == 0 { break; } - // Aggressive pointer jumping to fully flatten the label tree. - // Makes subsequent rounds converge faster by ensuring all labels - // point directly to roots before the next round's skip checks. - if hooks < 10_000 { - for _ in 0..20 { - if labels.pointer_jump() == 0 { - break; - } - } - } + // Update active_positions: only positions that changed labels + // in this round (hooks or pointer jumping) need to be checked + // in the next round. A block can be skipped if none of its + // source or target positions are in the changed set. + active_positions = Some(changed_bv); } eprintln!(