Skip to content

Add PyTorch GPU/CPU port of phg_v2's PS4G Viterbi HMM - #187

Open
zrm22 wants to merge 3 commits into
mainfrom
worktree-hmm-ps4g-port
Open

Add PyTorch GPU/CPU port of phg_v2's PS4G Viterbi HMM#187
zrm22 wants to merge 3 commits into
mainfrom
worktree-hmm-ps4g-port

Conversation

@zrm22

@zrm22 zrm22 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Ports phg_v2's PathFinderHMMPS4G (Kotlin, hmm-from-ps4g-claude branch) to PyTorch as src/python/hmm/ps4g_hmm.py: haploid and diploid Viterbi path finding with binomial emissions and gamete-switch transitions (with inbreeding-coefficient F0/F1 mixing for diploid), all built on the fly from a PS4G file (docs/ps4g_specifications.md). Reuses the existing GPU-capable python.hmm.viterbi.viterbi_decode.
  • Adds a runnable CLI, src/python/hmm/impute_ps4g.py, mirroring the Kotlin ImputePathFromPs4g command's options and midpoint-coordinate BED output, with a --device auto|cpu|cuda flag.
  • Fixes a latent dtype bug in ps4g_io.load_ps4g_file: pandas silently inferred gameteSet/refContig as int64 whenever every row in a PS4G file happened to have a single gamete (no comma) or a purely-numeric contig name (e.g. "1"), breaking downstream string operations on otherwise-valid PS4G files.
  • Adds unit tests (tests/python/hmm/test_ps4g_hmm.py) anchored on the exact counts from the Kotlin DiploidPS4GEmissionProbabilityTest.kt oracle, and integration tests (tests/python/hmm/test_impute_ps4g_cli.py) mirroring ImputePathFromPs4gTest.kt scenarios (single-gamete calls, exact BED midpoint coordinates, recombination switching, inbred-homozygous diploid, n-parents restriction, CLI validation errors).

Test plan

  • pixi run -- python -m pytest tests/python/hmm -q — 38/38 passed, including GPU-vs-CPU path-parity test (ran for real on an H200 in this environment, not skipped).
  • pixi run -- python -m pytest tests/python -q (excluding pre-existing, unrelated broken modules: mamba_ssm not installed, GLIBCXX mismatch for scipy/numba) — confirmed identical pre-existing failure set before/after this change (0 regressions).
  • Manual end-to-end CLI smoke test: haploid and diploid runs on a hand-built PS4G fixture produce correct, sensible BED output; --device cpu and --device cuda produce byte-identical output.

Zachary Miller and others added 2 commits July 2, 2026 16:30
Ports PathFinderHMMPS4G.kt (haploid + diploid, from the phg_v2
hmm-from-ps4g-claude branch) to PyTorch: binomial emissions and gamete-switch
transitions (with inbreeding-coefficient mixing for diploid) are built on the
fly from a PS4G file and decoded via the existing python.hmm.viterbi Viterbi
implementation, which runs on CPU or GPU. Adds a runnable CLI
(impute_ps4g.py) mirroring ImputePathFromPs4g's options/output, plus unit and
integration tests anchored on the Kotlin source's own test oracles.

Also fixes a latent dtype bug in ps4g_io.load_ps4g_file: pandas silently
inferred gameteSet/refContig as int64 whenever every PS4G row happened to
have a single gamete or a purely-numeric contig name, breaking downstream
string operations on legitimate PS4G files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds scripts/generate_synthetic_ps4g.py (parameterized synthetic PS4G file
generator - neither repo had one) and scripts/benchmark_ps4g_hmm.py (times
end-to-end CLI wall-clock across implementations/sizes/path-types, 3 repeats
each), plus the resulting results CSV.

Findings: Kotlin wins haploid at every size tested (cheap O(T·N) loop, so
per-process startup dominates); the PyTorch port is up to ~10x faster than
Kotlin for diploid at the largest tier (100K positions: 4.0s vs 40.7s
median), where Kotlin's O(T·S^2) nested-loop Viterbi degrades sharply and the
vectorized port doesn't. GPU is never faster than CPU in this workload -
Viterbi decoding is inherently sequential (one small tensor op per position),
so CUDA kernel-launch overhead never amortizes at these state-space sizes.

Also fixes a real bug in impute_ps4g.py's standalone sys.path fallback (used
an off-by-one `.parents[1]` instead of `.parents[2]`, so running the script
directly - as the benchmark needs to, without pixi's PYTHONPATH activation -
raised ModuleNotFoundError).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zrm22

zrm22 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Speed benchmark: Kotlin vs Python-CPU vs Python-GPU

Added scripts/generate_synthetic_ps4g.py + scripts/benchmark_ps4g_hmm.py and ran them: end-to-end CLI wall-clock (subprocess start to exit, 3 repeats, median reported) for phg impute-path-from-ps4g (Kotlin, JDK 21) vs this branch's impute_ps4g.py on CPU and on an NVIDIA H200, for haploid and diploid, across 4 synthetic PS4G sizes (1K / 10K / 100K positions × 10 gametes, plus a 500-position × 20-gamete "wide" tier).

size type kotlin python-cpu python-gpu
small (1K) haploid 0.57s 0.91s 1.15s
medium (10K) haploid 0.68s 1.12s 1.54s
large (100K) haploid 1.42s 2.99s 5.34s
small (1K) diploid 1.10s 0.94s 1.24s
medium (10K) diploid 5.04s 1.28s 1.77s
large (100K) diploid 40.67s 4.03s 6.44s

Takeaways:

  • Kotlin wins haploid at every size — haploid emissions are O(T·N) and cheap, so per-process startup (JVM ~0.5s vs Python+torch import ~0.9s) dominates.
  • Python wins diploid at scale — up to ~10x faster (4.0s vs 40.7s at 100K positions). Kotlin's diploid Viterbi is O(T·S²) over ordered parent-pair states (nested loops) and degrades sharply; the PyTorch port builds dense tensors and lets viterbi_decode do vectorized per-step math instead.
  • GPU never wins — Python-GPU is slower than Python-CPU in every config tested. Viterbi decoding is inherently sequential (one small tensor op per genomic position), so CUDA kernel-launch overhead never amortizes at these state-space sizes (10-20 gametes). Would likely need much larger state spaces (many more gametes/parents) or batched/padded multi-contig decoding to see GPU pay off.

Also fixed a real bug found while building this: impute_ps4g.py's standalone sys.path fallback used the wrong .parents[] index, so running the script directly (without pixi run's PYTHONPATH activation) raised ModuleNotFoundError.

Full results: scripts/benchmark_results/ps4g_hmm_benchmark_20260702_183536.csv. Interactive report: https://claude.ai/code/artifact/1397c8bf-29c9-4c52-ab34-09433d4f3159

Standalone summary of the Kotlin vs Python-CPU vs Python-GPU benchmark
(TL;DR, methodology, full results table, and reproduction steps) so the
findings can be shared without needing the CSV or the live artifact link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@zrm22 zrm22 self-assigned this Jul 20, 2026
@zrm22
zrm22 marked this pull request as ready for review July 20, 2026 13:03
else:
# Ambiguous: true gamete plus 1-2 random decoys.
n_extra = rng.randint(1, min(2, gametes - 1))
decoys = rng.sample([g for g in range(gametes) if g != current_gamete], n_extra)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strategy for choosing decoys doesn't cover the case where the true current gamete is closely related to a decoy, since decoy hits are evenly distributed and independent of previous hits. Is there a way to weight the decoy choice to simulate relatedness between gametes?

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.

4 participants