Skip to content

perf(ksw2): arm64 128-bit extd2/extz2 kernel improvements (+10-11% on HiFi/ONT) - #29

Closed
nh13 wants to merge 1 commit into
masterfrom
perf/ksw2-extension-kernels
Closed

perf(ksw2): arm64 128-bit extd2/extz2 kernel improvements (+10-11% on HiFi/ONT)#29
nh13 wants to merge 1 commit into
masterfrom
perf/ksw2-extension-kernels

Conversation

@nh13

@nh13 nh13 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Four kernel changes to the 128-bit ksw_extd2/ksw_extz2, plus the direction-byte BIT selects. Output is byte-identical to stock.

This is the arm64 half of the extd2 story. It is orthogonal to lh3#20, which adds a new AVX2/AVX-512 path and a dispatcher while leaving the 128-bit kernel untouched — zero file overlap, and the two compose: x86 gets the wide path, arm64 gets a faster reference kernel.

What is here

commit change effect
6ac3a39 score the prepass through an XOR-indexed 16-byte shuffle LUT (vqtbl1q_u8) 12 → 9 instrs / 16 B
e85a6e9, 58c5334 shift each DP rail with one vext instead of srli/slli/or −6 instrs / 16 cells
d63548c widen the exact-max H[] 16 int8 at a time rather than 4
64a5c6c emit the direction-byte selects as NEON BIT 71 → 67 instrs / 16 cells

Left-alignment core loop, cumulative: 77 → 67 instructions per 16 cells.

On the last one: clang will not emit BIT from the portable spelling. vbslq_u8(k, g, d) lowers to (k & g) | (~k & d), and for a constant splat known-bits proves ~k & d == d, folding it back to and/orr. Laundering k through an empty asm blocks the fold and is worse — the backend materialises ~k and emits three ops. Only a direct BIT gets the one-instruction form, so it is written out with a portable fallback off arm64.

Two wrappers rather than one there, deliberately: the four flag sites OR into bits of d known clear, but the b > z site is a true blend where d already holds 1, so d | (g & k) would yield 3 where the blend must yield 2. One shared OR-shaped fallback is correct on arm64 and silently wrong everywhere else.

Measurements

c8g.4xlarge (Graviton4, 16 physical cores, no SMT), -t 16, mapping phase, SAM to /dev/null, 5 reps interleaved with order reversal, rep spread 0.01–0.49%:

dataset stock this branch speedup
hifi-10k 8.425 s 7.480 s +11.22%
ont-10k 9.909 s 8.892 s +10.26%
wgs-1M 9.907 s 9.624 s +2.86%
hic-1M 73.766 s 73.076 s +0.94%

The gain tracks each dataset's ksw_extd2_sse share of runtime, which is what a genuine extd2 change must do. ont-10k was an out-of-sample check against a model built from the other three: 53.8% implied share against 54.9% profiled.

Long reads dominate because bw_long makes their gap fills effectively unbanded, so extd2 is 55–65% of compute there against ~9% on short-read WGS.

Correctness

A differential harness that builds the old and new kernels into one binary and compares them per pair: 60,000 pairs identical to stock in score AND cigar across qlen 100/150/250.

That gate had to be repaired first: it was running EXTEND rather than extd2, its workload pinned e2 == e so the second affine piece was never selected, and it planted only 1 bp indels, which are always cheaper on the first piece. A deliberately corrupted 0x40 direction bit passed. These numbers come from the fixed gate.

SAM output is byte-identical to stock ebc59ea over 200k reads.

x86

Unaffected. The bit asm is inside #if defined(__ARM_NEON); compiling both variants for x86-64 (gcc 15.2, -O3 -msse4.2 -mpopcnt) gives identical instruction counts and disassembly differing only in the order of two constant materialisations.

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 2 times, most recently from f7020b1 to bc0384a Compare August 7, 2026 19:13
@nh13

nh13 commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Submitted upstream as lh3#64, from this same branch. Closing the fork-side review copy so there is one place to review.

The branch was rebased onto current master (r421) and squashed to a single commit for submission; content is unchanged. The manifest entry now records upstream = { pr = 64, status = "open" }.

@nh13 nh13 closed this Aug 7, 2026
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