Skip to content

Add an exact structured pYIN Viterbi backend#11

Merged
sebastianrosenzweig merged 1 commit into
groupmm:mainfrom
ssmall256:pr/pyin-structured-viterbi
Jun 5, 2026
Merged

Add an exact structured pYIN Viterbi backend#11
sebastianrosenzweig merged 1 commit into
groupmm:mainfrom
ssmall256:pr/pyin-structured-viterbi

Conversation

@ssmall256

Copy link
Copy Markdown
Contributor

Summary

This PR adds an exact structured Viterbi backend to libf0.pyin() while
preserving the current dense implementation as the default.

The patch is intentionally narrow:

  • pYIN only
  • additive backend selection
  • parity tests
  • benchmark harness

What This Adds

  • libf0.pyin(..., viterbi_impl="legacy")
    • preserves current behavior
  • libf0.pyin(..., viterbi_impl="fast")
    • exact structured backend
  • compute_transition_structure_from_matrix(...)
    • builds sparse predecessor structure from the existing transition matrix
  • viterbi_log_likelihood_fast(...)
    • exact sparse structured Viterbi recurrence
  • focused parity tests
  • scripts/benchmark_pyin_viterbi.py

Why

libf0 pYIN already constructs a local transition matrix, but the current
decoder still scans all predecessor states for every target state.

This PR keeps the same transition probabilities and observation matrix, but
precomputes reachable predecessor structure once and evaluates the same dynamic
program only over reachable predecessors.

This is an exact implementation change, not an approximation.

Benchmark Notes

On the bundled quartet excerpt, local measurements showed:

  • decoder core:
    • 3928.850 ms legacy -> 8.753 ms fast
    • about 448.88x
  • full pyin():
    • 4607.144 ms legacy -> 694.988 ms fast
    • about 6.63x

Those results are why I think the explicit backend is worth exposing even
before any default change discussion.

Parity

On the exercised local workloads, parity was exact for:

  • decoder path
  • f0
  • time axis
  • confidence

The PR includes both low-level and end-to-end parity tests, plus an invalid
backend-mode test.

Validation

Commands used locally:

uv run pytest tests/test_algorithms.py -q
uv run python -m py_compile \
  libf0/pyin.py \
  tests/test_algorithms.py \
  scripts/benchmark_pyin_viterbi.py

Reviewer Notes

  • viterbi_impl="legacy" remains the default in this PR.
  • The new path is available explicitly as viterbi_impl="fast".
  • If maintainers want, default promotion can be discussed separately after
    review and broader benchmarking.

Scope

This PR is intentionally limited to pYIN smoothing. It does not refactor other
algorithms or change public defaults beyond adding the new backend option.

Context

This patch comes from a broader cross-repo study of exact structured Viterbi in
pitch trackers. That study produced repeated positive transfer in penn-mlx,
vendored upstream penn on both CPU and MPS, mlxcrepe, torchcrepe, and
librosa.pyin. libf0 was one of the clearest pYIN-specific wins, which is
why this PR stays tightly scoped to that decoder path.

@ssmall256

Copy link
Copy Markdown
Contributor Author

Follow-up update: this PR now uses a direct pYIN block-transition predecessor builder for instead of deriving sparse structure from a dense transition matrix.

What changed:

  • Added in
  • Fast path now uses this structured builder directly
  • Added/updated tests to verify structure + decode parity against the dense-matrix path
  • Updated benchmark harness to use the new structured builder

Validation run:

no tests ran in 0.00s (10 passed)

    • decoder parity: true
    • pyin parity (f0/time/conf): true

@ssmall256

Copy link
Copy Markdown
Contributor Author

Follow-up update (correcting my previous formatting-mangled comment): this PR now uses a direct pYIN block-transition predecessor builder for viterbi_impl="fast" instead of deriving sparse structure from a dense transition matrix.

What changed:

  • Added compute_transition_structure_pyin_block(...) in libf0/pyin.py
  • Fast path now uses this structured builder directly
  • Added/updated tests to verify structure and decode parity against the dense-matrix path
  • Updated benchmark harness to use the new structured builder

Validation run:

  • python -m pytest tests/test_algorithms.py -q (10 passed)
  • python -m py_compile libf0/pyin.py tests/test_algorithms.py scripts/benchmark_pyin_viterbi.py
  • PYTHONPATH=/Users/sam/Code/libf0 python scripts/benchmark_pyin_viterbi.py --warmup 1 --iters 2 --json
    • decoder parity: true
    • pyin parity (f0/time/conf): true

@ssmall256

Copy link
Copy Markdown
Contributor Author

Branch split update: Kronecker/block-transition optimization has been moved out to separate follow-up draft PR #12, so #11 is back to the original base scope.

@sebastianrosenzweig

sebastianrosenzweig commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Hi @ssmall256,
Many thanks for your contribution, I'm sorry for the delayed response - I didn't get a notification about your pull request. The pull request looks very clean, the speed improvements are evident and the code produces numerically identical results - well done! I am happy to merge this branch and take care of the other issues and merge requests once I have admin rights for this repo, @akustiker @simonschwaer @stefan-balke !

@sebastianrosenzweig sebastianrosenzweig self-requested a review June 5, 2026 09:21

@sebastianrosenzweig sebastianrosenzweig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

all good and clean!

@sebastianrosenzweig sebastianrosenzweig merged commit 9b56760 into groupmm:main Jun 5, 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.

2 participants