Skip to content

perf(fmi): keep the backward step's loop invariants in registers - #677

Merged
nh13 merged 1 commit into
mainfrom
perf/fmi-backward-step-invariants
Oct 31, 2026
Merged

nh13 merged 1 commit into
mainfrom
perf/fmi-backward-step-invariants

Conversation

@nh13

@nh13 nh13 commented Oct 30, 2026 •

Copy link
Copy Markdown

Summary

The lockstep SMEM driver's backward step (ls_advance_backward_step) extends every interval in a slot's prev[] by one base per outer step. Its two general loops (taken whenever numPrev > 1) read the slot's prev pointer, numPrev, min_intv and j, and the index's cp_occ and count[a], through s and this on every iteration. The loops store SMEMs back into prev[], and as far as the compiler can tell those stores may alias the slot and index fields, so it reloads them after each store: one extra L1 load ahead of each prev[p] load, compare and prefetch. This PR reads them once into locals before the loops and writes numPrev and j back once after. Output is byte-identical.

  • Hoisted values. prev, numPrev, min_intv, j, cp_occ and count[a] become locals for the two general loops; s->numPrev and s->j are written once after them (s->j = j - 1, the old s->j--).
  • New helper. backwardExt_konly_cp is backwardExt_konly on a caller-held cp_occ pointer and count[a], so the loops can pass the hoisted values. backwardExt_konly now forwards to it, so the arithmetic lives in one place. Its comment states that it keeps backwardExt_konly's contract: k is left stale on the s'→0 exit, so the caller must discard any s' < min_intv.
  • Unchanged. The numPrev == 1 straight-line path, the forward phase and the bwtSeed driver.

Correctness

  • Why it is exact. Inside the general loops nothing writes s->numPrev, s->j, s->min_intv, s->prev, cp_occ or count. The only slot writes are s->cur_j, s->match_count and a match_buf[] entry, each followed by break, and prev[numCurr++] writes at or behind the read index p, as before. prev[] and match_buf[] are driver-owned buffers that overlap neither the slot nor the index, so the hoisted values equal what the reloads would have read.
  • Test suites. Unit suite 568/568, and test/run_unit_tests.sh passes natively on arm64 (bwtSeed lockstep parity 95/95, SMEM lockstep parity 32/32; the SMEM parity test takes the general-loop path about 6,000 times). On an x86-64 build at the AVX2 tier, both lockstep parity tests pass. x86 compile checks pass at -mavx2 and -mavx512f -mavx512bw.
  • End to end. These runs are byte-identical to main (excluding @PG) on arm64 and on an x86 AVX2 build, with the lockstep driver (the default) and with the scalar driver (BWA3_BWTSEED_LOCKSTEP=0), and on arm64 also at lockstep widths of 3 (BWA3_SMEM_LOCKSTEP_N=3, BWA3_BWTSEED_LOCKSTEP_N=3): SE, PE and long-read runs with default options, -d 70, -w 30 and --compat=bwa-mem2, and PE with --ks-dedup=on/off, -k 10, -k 30, -y 1, -y 5 and BWA3_BWTSEED_JUMP=0/12.
  • Timing runs. SAM output was identical to main on every timing run on all three hosts.

Timing

Whole aligner, mem -t 16 on a 5M-pair WGS slice (HG00096, 2x150, hg38), clang 19.1.7, index in shared memory, 6 interleaved reps per build in a seeded shuffled order on the same host. This change was measured on its own against main. Figures are the paired per-rep delta vs main (median, bootstrap 95% CI).

host wall CPU (user)
c7i.4xlarge (Sapphire Rapids, AVX-512BW) −0.82% [−0.92, −0.65] −0.78% [−0.90, −0.64]
c6a.4xlarge (Zen 3, AVX2) −1.21% [−2.19, −0.27] −1.13% [−2.28, −0.28]
c8g.4xlarge (Graviton4, NEON) −0.79% [−2.04, +0.10] −0.89% [−2.07, +0.08]

Start-up included: the first 200k pairs of the same slice, -t 16, with the index loaded from the page cache on every run, 12 interleaved reps:

host wall CPU (user)
c7i.4xlarge (Sapphire Rapids, AVX-512BW) −0.71% [−1.07, −0.36] −0.64% [−0.86, −0.51]
c6a.4xlarge (Zen 3, AVX2) −0.32% [−0.64, +0.00] −1.00% [−1.46, −0.54]
c8g.4xlarge (Graviton4, NEON) −0.30% [−0.92, +0.00] −0.59% [−0.83, −0.42]

The 5M-pair runs show a gain on all three hosts; on Graviton4 the median is −0.8% but the interval reaches just past zero. A 200k run takes a few seconds, so the timer's 10 ms resolution is 0.3–0.6% of a run and the wall medians sit on that grid; the CPU-time intervals there exclude zero on all three hosts.

Stack

This is the sixth PR of a stacked series that reworks SMEM seeding and SA lookup. It is based on #676 and should be reviewed after it. The next PR in the series writes the forward phase's prev[] entries downward.

Split out of #656, thanks to @jmschrei for the original work.

Summary by CodeRabbit

  • Refactor
    • Updated internal backward-search processing. Search results and behavior remain unchanged, including single-entry handling and cases where an updated interval has zero entries.
    • No user-facing features or behavior changes are included in this update.

@nh13

nh13 commented Oct 30, 2026

Copy link
Copy Markdown
Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2380aa4 and 09b679f.

Files selected for processing: 2
  • src/FMI_search.cpp
  • src/FMI_search.h

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The backward-search helper now accepts cached checkpoint and count inputs. The general lockstep loops use those inputs and write updated loop state back after processing. Existing interval calculations, stopping conditions, emission, and deduplication remain in place.

Changes

Lockstep backward search

Layer / File(s) Summary
Cached backward extension inputs
src/FMI_search.h, src/FMI_search.cpp
backwardExt_konly delegates to a helper that accepts the checkpoint pointer and base count. The general loops cache index and checkpoint values, use the helper, and write updated loop state back. The single-entry specialization comment now notes its parity requirement and test coverage.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Possibly related PRs

Merge Risk: Minimal · up to 09b67

The optimization preserves the inspected backward-search calculations and state updates. No actionable merge-blocking issue remains; merge after normal checks pass.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Public-Repo Hygiene Inconclusive The visible title and diff contain no prohibited internal tool, bucket, account, hostname, IP, or local path. The supplied PR body is truncated after “on Grav…”. The full body is required for this che… Provide the complete PR body, or rerun the check with the full description available. Then scan the omitted text for non-public benchmark names, object-store references, account or instance IDs, internal hostnames, private IPs, and local fi…
Passed Passed checks: 7 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title follows Conventional Commits with the perf type and fmi scope. It names the touched FMI component and accurately describes the loop-invariant performance change.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Equivalence Contract Updated Passed The PR changes only FMI_search.cpp and FMI_search.h. The helper preserves the existing arithmetic while taking hoisted cp_occ and count[a] values, and the general loops preserve the prior predicates, …
Kernel Change Validation Reported Passed PASS: The pull request changes only src/FMI_search.cpp and src/FMI_search.h. It does not modify any file covered by this check, so the description-reporting requirements do not apply.
Simd Tier Coverage Passed No SIMD tier coverage risk is introduced. The diff changes scalar loop state and moves existing backward-extension arithmetic into backwardExt_konly_cp; it adds no vector intrinsic, tier dispatch, `…
Full details: Equivalence Contract Updated

Explanation

The PR changes only FMI_search.cpp and FMI_search.h. The helper preserves the existing arithmetic while taking hoisted cp_occ and count[a] values, and the general loops preserve the prior predicates, write order, and state updates. The PR description explicitly states that output is byte-identical and records verification through unit, lockstep-parity, cross-tier, and end-to-end comparisons on arm64 and x86 AVX2. This satisfies the contract even if the SMEM path is treated as potentially output-affecting; no equivalence-document update is required when the description gives explicit unchanged-output verification.

Full details: Simd Tier Coverage

Explanation

No SIMD tier coverage risk is introduced. The diff changes scalar loop state and moves existing backward-extension arithmetic into backwardExt_konly_cp; it adds no vector intrinsic, tier dispatch, SIMD_WIDTH use, or SIMD-dependent buffer size. The four changed _mm_prefetch calls only replace cp_occ with the cached cpo pointer. Existing target-specific occ_popcount64 SIMD code is unchanged.

Full details: Public-Repo Hygiene

Explanation

The visible title and diff contain no prohibited internal tool, bucket, account, hostname, IP, or local path. The supplied PR body is truncated after “on Grav…”. The full body is required for this check, and no full PR-description source is available in the repository or review refs.

Resolution

Provide the complete PR body, or rerun the check with the full description available. Then scan the omitted text for non-public benchmark names, object-store references, account or instance IDs, internal hostnames, private IPs, and local filesystem paths.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

@nh13

nh13 commented Oct 30, 2026

Copy link
Copy Markdown
Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

@nh13
nh13 force-pushed the perf/fmi-drop-cross-slot-t1-lookahead branch from ead8e02 to 4b15591 Compare October 30, 2026 22:42
@nh13
nh13 force-pushed the perf/fmi-backward-step-invariants branch from e2d25ce to f9343d2 Compare October 30, 2026 22:45
@nh13
nh13 force-pushed the perf/fmi-drop-cross-slot-t1-lookahead branch from 4b15591 to 25a09be Compare October 31, 2026 01:02
@nh13
nh13 force-pushed the perf/fmi-backward-step-invariants branch from f9343d2 to a31e381 Compare October 31, 2026 01:04
@nh13
nh13 force-pushed the perf/fmi-drop-cross-slot-t1-lookahead branch from 25a09be to 4897b39 Compare October 31, 2026 04:32
Base automatically changed from perf/fmi-drop-cross-slot-t1-lookahead to main October 31, 2026 09:13
ls_advance_backward_step's two general loops read the slot's prev
pointer, numPrev, min_intv and j, and the index's cp_occ and count[a],
through s and this on every iteration. The SMEM stores into prev[] may
alias those fields as far as the compiler can tell, so it reloads them
after each store, one extra L1 load ahead of each prev[p] load, compare
and prefetch.

Read them once into locals before the loops and write numPrev and j back
once after. backwardExt_konly_cp is backwardExt_konly on a caller-held
cp_occ pointer and count[a] (backwardExt_konly now forwards to it), so
the loops can pass the hoisted values. Its comment states that it keeps
backwardExt_konly's contract: k is left stale on the s'->0 exit, so the
caller must discard any s' < min_intv. prev[] and match_buf[] are
driver-owned buffers that overlap neither the slot nor the index, so the
result is byte-identical. The numPrev == 1 path is unchanged.

Co-authored-by: Jacob Schreiber <jacob.schreiber@umassmed.edu>
@nh13
nh13 force-pushed the perf/fmi-backward-step-invariants branch from a31e381 to 09b679f Compare October 31, 2026 09:23
@nh13

nh13 commented Oct 31, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Oct 31, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13
nh13 added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit adf0d41 Oct 31, 2026
29 checks passed
@nh13
nh13 deleted the perf/fmi-backward-step-invariants branch October 31, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant