GSoC 2026 · Milestone 1 of 4 · scikit-bio

First Numba CPU Ports: Mantel and PERMANOVA

The official coding period began May 25, 2026. This is what shipped in the first stretch of work, what was in active review, and the engineering lessons that came out of it.

← GSoC hub scikit-bio ↗
2PRs merged
1PR in review
4 OSCI matrix
May 2026Week one

Merged Pull Requests

scikit-bio #2464

Numba CPU permuted Pearson helpers for Mantel

Direct Cython-to-Numba conversion of mantel_perm_pearsonr_cy and its condensed-form variant. The Numba implementation preserves the same loop structure and parallelism axis as the original Cython. A NUMBA_AVAILABLE dispatch keeps Numba an optional dependency: if it is not installed, the existing Cython path stays in service unchanged.

scikit-bio #2468

Numba-aware Mantel tests and CI workflow

Test infrastructure for the Numba conversion work: paired _cy and _numba tests against the same fixtures, dispatch fallback tests via patch.object(NUMBA_AVAILABLE, False), and a numba_code pytest marker. The CI workflow runs the marker across four operating systems (Ubuntu x86_64, Ubuntu ARM64, macOS, Windows) on Python 3.14.

Pull Request in Active Review

scikit-bio #2470

Numba CPU s_W helpers for PERMANOVA

Direct conversion of permanova_f_stat_sW_cy and the condensed-form variant. CI is green across the full Python matrix on four operating systems. Review feedback has been addressed through follow-up commits including a partial-sum simplification, a test class restructure that mirrors the Mantel test architecture, and replacement of an inline Python reference computation with a hardcoded value from the trusted Cython implementation.

Numba 0.65.1 parfor cycle bug

The natural Numba code for PERMANOVA's s_W crashes at compile time with AssertionError: unexpected cycle in lookup() from inside the parfor reduction-analysis pass. The crash needs two features together: a cross-iteration scalar reduction in the outer prange, and an if conditional inside the inner loop that reads from a parameter array. Either feature alone compiles fine.

The minimal trigger
@njit(parallel=True)
def natural_sW(mat, group_sizes, grouping):
    n = mat.shape[0]
    s_W = 0.0
    for row in prange(n - 1):
        group = grouping[row]
        local = 0.0
        for col in range(row + 1, n):
            if grouping[col] == group:        # conditional read of a parameter
                val = mat[row, col]
                local += val * val
        s_W += local / group_sizes[group]      # cross-iteration scalar reduction
    return s_W

The workaround replaces the scalar reduction with per-iteration writes to a partials array, summed once after the prange exits. The conditional stays in place. Beyond sidestepping the bug, this pattern has a useful property for the future GPU work: parallel=True reductions are a CPU-only Numba feature, but the per-iteration-slot pattern maps directly to @cuda.jit.

Engineering lesson: tests should validate against trusted references

One review point that came up on PR #2470 was about test design. The initial test class included a Python reimplementation of the s_W computation as the "expected value" for the paired _cy and _numba tests. That carries a subtle bug-masking risk: if the test reimplementation and the function under test share the same misunderstanding of the formula, both pass together while being wrong together.

The fix looks obvious in hindsight. Compute the expected value once using the trusted reference implementation (here, the pre-existing Cython), hardcode the resulting number, and assert against it. The test no longer carries an algorithmic implementation. It carries a single value from a trusted source.

Peer collaboration on PCoA

Another GSoC contributor on the same scikit-bio project is working on Numba CPU and GPU implementations of center_distance_matrix for PCoA. The first week of collaboration covered how to structure the work as separate CPU and GPU PRs, how to slot the new tests into the existing Numba CI workflow, and how the Mantel and PERMANOVA architecture can serve as a template for that branch.

What's queued next

Three pieces of work are queued behind PR #2470:

  1. Interface cleanup PR. Make Numba opt-in rather than auto-selected on installation, and rename the _numba function suffix to _nb to match the existing _cy convention. This will target Mantel and PERMANOVA at the same time.
  2. Block-permutation optimization for PERMANOVA. Process multiple permutations per matrix scan, with cache tiling and a parallelization axis flipped from matrix rows to permutation blocks. The C++ reference for this is in the scikit-bio-binaries repository.
  3. GPU @cuda.jit implementation. Builds on the optimized CPU code. The partials pattern from the parfor workaround already aligns with the structure needed for this transition.

All of this landed by the end of June — covered in the next milestone.