Skip to content

SIGSEGV in TopHits.merge when partials come from parallel Pipeline.search_hmm workers #103

Description

@aretasg

reprod_hmm.hmm.txt

Summary

Calling pyhmmer.plan7.TopHits.merge(*partials) on TopHits returned
from Pipeline.search_hmm() calls that ran in parallel Python threads
crashes the interpreter with SIGSEGV (exit 139).

The same crash surfaces through pyhmmer.hmmer.hmmsearch(hmms, targets)
whenever it auto-selects parallel="targets" (i.e. len(hmms) < cpus
and targets are a DigitalSequenceBlock), because the reverse dispatcher
calls TopHits.merge on partial results internally
(hmmer/_hmmsearch.py:263).

Environment

Reproduces on both platforms tested; likely OS-independent.

  • pyhmmer 0.12.1 (also reproduces on 0.12.0)
  • macOS 15.7.7 arm64 (Darwin 24.6.0), Python 3.12.12
  • Linux 6.10.14 x86_64, glibc 2.41 (Debian slim in Docker), Python 3.12.13
  • backend="threading" (the default)

Minimal reproducer

The HMM (reprod_hmm.hmm, 60 KB, attached) is a single IMGT-derived
antibody heavy-chain profile (M=128). A Builder.build_msa-constructed
HMM of similar size does not reproduce the crash, so something about
hmmbuild-produced profiles is required to trigger it.

import pyhmmer
from concurrent.futures import ThreadPoolExecutor

alphabet = pyhmmer.easel.Alphabet.amino()
with pyhmmer.plan7.HMMFile("reprod_hmm.hmm") as hf:
    hmm = next(iter(hf))

seq = ("EVQLVESGGGLVQPGRSLRLSCAASGFTFDDYAMHWVRQAPGKGLEWVSAITWNSGHIDYADSVEGRF"
       "TISRDNAKNSLYLQMNSLRAEDTAVYYCAKVSYLSTASSLDYWGQGTLVTVSS")

def search(_):
    p = pyhmmer.plan7.Pipeline(alphabet,
                               background=pyhmmer.plan7.Background(alphabet))
    b = pyhmmer.easel.DigitalSequenceBlock(alphabet)
    for j in range(25):
        b.append(pyhmmer.easel.TextSequence(
            name=f"s{j}".encode(), sequence=seq
        ).digitize(alphabet))
    return p.search_hmm(hmm, b)          # returns TopHits

with ThreadPoolExecutor(max_workers=4) as ex:
    partials = list(ex.map(search, range(4)))

print("partials sizes:", [len(p) for p in partials])   # prints [25,25,25,25]
merged = pyhmmer.plan7.TopHits.merge(*partials)        # SIGSEGV here
print("merged OK", len(merged))                         # never reached
$ python reprod.py ; echo "exit=$?"
partials sizes: [25, 25, 25, 25]
exit=139

What I've ruled out

  • Not the search itself. Iterating each partial TopHits individually
    after the ThreadPoolExecutor block (without calling merge) works
    cleanly.
  • Not a data race in workers. Replacing return p.search_hmm(hmm, b)
    with return len(p.search_hmm(hmm, b)) — same work, just discarding
    the TopHits before it leaves the worker — runs to completion. Keeping
    a reference to the worker's Pipeline alive alongside the TopHits
    does not prevent the crash. Points at a raw-pointer or lifetime
    issue inside TopHits.merge itself.
  • Not the HMM size. A 120-position HMM built via
    Builder.build_msa(...) and searched the same way does not crash.

Notes

Passing parallel="queries" (or cpus=1) to hmmer.hmmsearch avoids
the merge code path entirely, but sacrifices the target-parallel
speedup — for len(hmms)=1 this equals single-threaded execution.

Attachments

  • reprod_hmm.hmm.txt — rename to .hmm after download. GitHub blocks
    .hmm uploads; the file is unchanged plain-ASCII HMMER3 format (60 KB).

reprod_hmm.hmm.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions