Skip to content

Some work on improving HMM engine performance - #7

Open
jeromekelleher wants to merge 2 commits into
tskit-dev:mainfrom
jeromekelleher:tuning-experiments
Open

jeromekelleher wants to merge 2 commits into
tskit-dev:mainfrom
jeromekelleher:tuning-experiments

Conversation

@jeromekelleher

Copy link
Copy Markdown
Member

No description provided.

Add late-stage tsinfer workloads (1000 Genomes chr20p ancestor groups
1483-1485 and sample matching) to the compression experiments, and
measure the proposed delta and affine encodings on them.

On these workloads a quarter to a third of the stored Viterbi entries
were redundant: when edges change, nodes get explicit copies of their
inherited values, and the engine stored the traceback before coalescing
them away. That churn limited a delta encoding of the traceback to
1.1-1.7x.

Viterbi now coalesces before storing the traceback, and no longer after.
A node is removed only if its value and its recombination_required flag
both equal the ones it inherits, so the traceback, in which unlisted nodes
take the flag of their nearest listed ancestor, is unchanged. Nodes kept
for their flag are removed at a later site. Forward and backward still
coalesce after storing, on values only.

What was tried:
1. Flag-aware coalescing before storing plus the old pass after: minimal
   lists, but two passes per site made tsls 1.36-1.48x tsinfer-hmm.
2. A single flag-aware pass before storing, with an uncached walk to the
   nearest listed ancestor: +2-4% instructions over the old engine.
3. As 2, with the inherited flag cached along tree paths alongside the
   inherited likelihood (recombination_required_cache): landed. It uses
   no more instructions than the old engine (callgrind: -0.2% on 20
   group 1485 ancestors, -0.3% on one sample), and head-to-head wall
   times agree within noise.

Results: the stored lists now equal the change points, Viterbi churn
falls 5-8x, engine traceback entries fall 31-47%, and a delta encoding
on top saves a further 5-8x. tsls still finds tsinfer's exact path on all
benchmark queries.

The compression experiments gain:
- a tsinfer case in capture.py, with --cases and merged manifests;
- churn, value change, flag change and affine residual measures in
  analysis.py;
- an Encodings section and work plots in report.py.
The README records the findings and the sequence of attempts.
tsls stores a smaller traceback than tsinfer-hmm but was still slower,
because storing the traceback is under 1% of the time in both engines.
Profiling both on the same 1000 Genomes chr20p queries (callgrind,
limited to ancestor_matcher_find_path) showed tsls doing 1.26x
(samples) to 1.49x (late ancestors) the instructions, mostly in the
allele lookup and in building the first tree of each query.

What was changed, and how it landed:

1. Allele lookup. tsinfer-hmm sets root 0 to the ancestral state, so its
   walk up the tree checks only for a set allele; tsls allows several
   roots and checked for the root at every step. tsls now sets every
   root's allele (roots are all in the likelihood list) before each
   site's update, and the Viterbi, forward and backward loops use
   get_marked_allelic_state. A first version kept an assert in the walk;
   because the Python module is built with asserts on, it cost as much
   as the check it replaced (12.71 G against 12.50 G instructions on one
   sample), so the walk has no assert. The traceback keeps
   get_allelic_state.
2. First-tree build. For Viterbi, init_sweep now builds the first tree
   with plain edge insertions and removals
   (ancestor_matcher_next_tree_topology), as tsinfer-hmm does, and no
   longer initialises num_below. Forward and backward, which track
   num_below, are unchanged.

Instructions, tsinfer-hmm / tsls before / tsls after: 20 group 1485
ancestors 6.65 / 9.89 / 7.44 G; one sample 9.92 / 12.50 / 10.96 G.

Paired wall times (best of 3, alternating engines per query), tsls over
tsinfer-hmm: group 1485 ancestors 1.35 -> 1.10, samples 1.06 -> 1.06.
Change 2 is the win; change 1 saves instructions but no time, because the
walk is limited by memory access. run.py ratios proved unreliable for
comparing engines, since it runs the engines in blocks and machine speed
drifts; the README documents this and the paired method.

Paths are unchanged: tsls still finds tsinfer's path on every query
checked, and the tsls, C and benchmarking tests pass.
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