Skip to content

speed up the 128-bit ksw2_extd2/extz2 inner loops - #64

Open
nh13 wants to merge 1 commit into
lh3:masterfrom
nh13:perf/ksw2-extension-kernels
Open

speed up the 128-bit ksw2_extd2/extz2 inner loops#64
nh13 wants to merge 1 commit into
lh3:masterfrom
nh13:perf/ksw2-extension-kernels

Conversation

@nh13

@nh13 nh13 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Four changes to the shared anti-diagonal loop. Output is unchanged.

The per-diagonal score prepass compared against three constants to pick the match,
mismatch and N scores. It now indexes a 16-byte table with sf ^ qrr and one byte
shuffle, 5 ops down to 2. Query N is remapped to 8 so every index stays <= 12,
where NEON's vqtbl1q_u8 and SSSE3's _mm_shuffle_epi8 agree.

Each DP rail was shifted one lane with srli+slli+or. Carrying the whole previous
vector instead of its last byte makes that one vext on NEON and one palignr on
SSSE3 -- three rails in extd2, two in extz2.

The exact-max path widened four int8 to int32 at a time; it now does sixteen,
keeping the four-lane accumulator and the update order so max_t is unchanged.

The direction byte was built with four and/orr pairs and a blend. NEON spells each
as one BIT, which clang will not emit from the portable form: vbslq_u8(k, g, d)
lowers to (k & g) | (~k & d), and for a constant splat known-bits proves
~k & d == d and folds it back. The instruction is written out for aarch64;
elsewhere the original or/and and blendv are kept. Two wrappers rather than one,
because the flag sites need d's k bits already clear while the argmax site is a
real blend, and sharing one form between them would be wrong off aarch64.

The first three help any SIMD target; the fourth is aarch64-only. Left-alignment
core loop, 77 to 67 instructions per 16 cells on aarch64.

Timings use the four HG002 sets from zenodo.org/records/19703025, the same ones
map.mak drives, aligned against GRCh38 with ALT contigs and decoys rather than
hs38. Mapping phase only, -t 16, SAM to /dev/null, five reps per arm interleaved
with order reversal:

              Graviton4, gcc 11.5      Xeon 8488C, gcc 11.5
              base     this     %      base     this     %

HiFi 10k 8.425s 7.480s +11.2 11.316s 10.539s +6.9
ONT 10k 9.909s 8.892s +10.3 14.078s 13.148s +6.6
WGS 1M pairs 9.907s 9.624s +2.9 14.976s 14.515s +3.1
Hi-C 1M pairs 73.766s 73.076s +0.9 - - -

Rep spread was 0.01-0.77%. Long reads gain most because bw_long leaves their gap
fills effectively unbanded, so extd2 is most of their runtime; on short reads it is
a much smaller share. The x86 column is a baseline -msse4.2 build, so it does not
include whatever the AVX2/AVX-512 dispatch in #20 would add.

SAM output is byte-identical to r421 on 200k WGS pairs and 10k HiFi reads, on both
architectures. A differential harness that builds the old and new kernels into one
binary and compares them per pair reports 60000 pairs identical in score and CIGAR
at qlen 100, 150 and 250, for both extd2 and extz2.

@lh3

lh3 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

I will have a look later. Does the change keep the identical output?

@nh13

nh13 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Yes — identical, verified at the kernel and at the aligner.

Kernel, per-pair exact. Both kernels are compiled twice into one binary (baseline from f0e1174, candidate from this branch, symbol-renamed so they link side by side) and run over identical deterministic workloads at lengths 100/127/150/200/250:

mode pairs result
extend (extz2) 120,000 identical
extd2 (score and CIGAR) 120,000 identical
rescue (ksw2_ll, untouched) 120,000 identical

That gate is not vacuous: flipping a single direction bit in the candidate (0x400x60) turns the extd2 check into CHECK FAIL: 14762/120000 pairs differ, with scores still matching and only the CIGARs diverging — which is why that mode hashes the CIGAR as well as the score.

Aligner, byte-identical SAM. Rebased onto f0e1174. @PG excluded, everything else compared by digest. Inputs are the HG002 sets from zenodo 19703025, sized so each run is ~45-60 s wall; the long-read sets are the 10k files concatenated (×11 HiFi, ×9 ONT) and WGS is the 5M-pair 1kg HG00096 set doubled to 10M pairs. Mac Studio M3 Ultra, -t 16, 3 reps with the arm order alternated:

data records SAM wall (stock → cand) CPU (stock → cand)
WGS, 10M pairs, -x sr 20,065,582 identical 57.06 → 55.02 s (1.037×) 896.9 → 864.3 s (1.038×)
HiFi, 110k reads, -x lr 122,111 identical 44.71 → 40.52 s (1.103×) 662.2 → 596.5 s (1.110×)
ONT, 90k reads, -x lr 127,935 identical 50.38 → 46.42 s (1.085×) 765.9 → 705.1 s (1.086×)

Rep-to-rep spread is under 0.3% in every cell (e.g. stock WGS wall 56.99/57.08/57.11 s), and the ranges are non-overlapping throughout.

Two robustness checks on those numbers. The same digests come out of a second machine (M2 Max) at 8 threads rather than 16, so the output is invariant to host and thread count. And the ratios are unchanged at 1/10th the input size — single-copy runs gave 1.037/1.101/1.081× against 1.038/1.110/1.086× here — so the concatenation is not flattering the result through a warm cache.

Kernel throughput (M3 Ultra, arm64/NEON, n=20000, 7 reps), 128-bit baseline → candidate:

qlen extz2 extd2
100 1953.4 → 2092.3 Mc/s (1.07×) 2131.9 → 2529.9 Mc/s (1.19×)
150 2286.8 → 2468.2 Mc/s (1.08×) 2700.7 → 3248.5 Mc/s (1.20×)
250 2654.3 → 2983.9 Mc/s (1.12×) 3196.2 → 3924.2 Mc/s (1.23×)

One caveat worth stating: the short-read gain is the least reliable figure here. It is small, and unlike the long-read numbers it did not reproduce across hosts — the M2 Max measured 1.07× where this machine measures 1.04×. Treat WGS as "a few percent, host-dependent"; the HiFi and ONT figures agreed across both machines to within 0.3%.


🤖 Measurements run and this comment drafted with Claude Code (Claude Opus 5); reviewed before posting.

@lh3

lh3 commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Thanks. Will have a look when I have time

Four changes to the shared anti-diagonal loop, all output-neutral.

The per-diagonal score prepass compared against three constants to pick the
match, mismatch and N scores. It now indexes a 16-byte table with sf ^ qrr and
one byte shuffle, 5 ops down to 2. Query N is remapped to 8 so every index stays
<= 12, where NEON's vqtbl1q_u8 and SSSE3's _mm_shuffle_epi8 agree.

Each DP rail was shifted one lane with srli+slli+or. Carrying the whole previous
vector instead of its last byte makes that a single vext (alignr on x86), three
rails in the dual-gap kernel and two in the single-piece one.

The exact-max path widened four int8 to int32 at a time; it now does sixteen,
keeping the four-lane accumulator and the update order so max_t is unchanged.

The direction byte was built with four and/orr pairs and a blend. NEON spells
each as one BIT, which clang will not emit from the portable form: vbslq_u8(k,
g, d) lowers to (k & g) | (~k & d), and for a constant splat known-bits proves
~k & d == d and folds it back. The instruction is written out for arm64, with
the plain or/and and blendv kept elsewhere -- the flag sites need d's k bits
already clear, the argmax site is a real blend, and sharing one form between
them would be wrong off arm64.

Together the left-alignment core loop goes from 77 to 67 instructions per 16
cells on aarch64. Measured on a 16-core Graviton4 at -t 16, mapping phase only:
HiFi 10k 8.425s -> 7.480s, ONT 10k 9.909s -> 8.892s, WGS 1M pairs 9.907s ->
9.624s, Hi-C 1M pairs 73.766s -> 73.076s. Long reads gain most because bw_long
leaves their gap fills effectively unbanded, so extd2 is most of their runtime.

SAM output is byte-identical to r421 on 200k WGS pairs and 10k HiFi reads, and
a differential harness comparing the old and new kernels directly reports 60000
pairs identical in both score and CIGAR at qlen 100, 150 and 250.
@nh13
nh13 force-pushed the perf/ksw2-extension-kernels branch from bc0384a to a917828 Compare August 23, 2026 00:23
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.

2 participants