FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ENH: wire Highway VQSelect for np.partition on aarch64/ppc64 by dmatth1 · Pull Request #31506 · numpy/numpy · GitHub

/ numpy Public

ENH: wire Highway VQSelect for np.partition on aarch64/ppc64 - #31506

Open
dmatth1 wants to merge 4 commits into
numpy:mainfrom
dmatth1:neon-qselect-highway
Open

dmatth1 wants to merge 4 commits into
numpy:mainfrom
dmatth1:neon-qselect-highway

Conversation

dmatth1 commented May 25, 2026

Copy link
Copy Markdown

PR summary

Uses Highway VQSelect to speed up np.partition for aarch64/Neon/Graviton. Highway VQSort was used for np.sort - see gh-24018 - so we match that pattern here.

A threshold (HWY_QSELECT_MIN_N = 16384) gates when we fallback to scalar, because below this number scalar is faster as the data lives in L1 cache and VQSelect setup dominates.

Benchmarks run on M1 macbook (Neon):

dtype n k scalar vqselect speedup
float32 100K 10 5.0 2.3 2.2×
float32 1M 100 6.8 2.6 2.6×
float32 10M 100 8.9 2.7 3.3×
float64 100K 10 8.0 3.6 2.2×
float64 1M 100 9.2 4.5 2.0×
int32 100K 10 6.3 1.3 4.9×
int32 1M 100 5.8 1.5 3.8×
int64 1M 100 5.9 2.9 2.0×

AI Disclosure

Claude used for coding, benchmarks with my review. Built and tests green locally

First-Commit Introduction

Hey, never committed to numpy before but I've been digging into simd-sort acceleration lately and noticed this gap for numpy with my Mac architecture

dmatth1 added 4 commits May 25, 2026 11:24
np.sort already uses Highway VQSort (PR numpy#24018); this
patch brings the same acceleration to np.partition via VQSelectStatic.

On non-x86 platforms the quickselect_dispatch function now tries
Highway VQSelect (covering int16/uint16/int32/uint32/int64/uint64/
float32/float64) before falling back to scalar introselect, matching
the existing x86/non-x86 split in quicksort.hpp. A single dispatch
file mirrors highway_qsort.dispatch.cpp and is registered for ASIMD
and VSX2 targets in meson.build; 16-bit integers ride the same file
since they are plain integers covered by base ASIMD (no ASIMDHP
target needed). float16 is excluded -- HWY_HAVE_FLOAT16 gating adds
complexity and float16 partition is uncommon; scalar introselect is
correct there. A minimum-size threshold (HWY_QSELECT_MIN_N = 16384)
skips VQSelect on L1-resident arrays where dispatch overhead
outweighs the vectorisation gain; the dead DISABLE_HIGHWAY_OPTIMIZATION
macro is removed.

np.argpartition is not covered; VQSelect is keys-only and cannot
maintain a parallel index array.

Threshold rationale: a size sweep on M1 Pro (N=64-65536, kth=N/2)
showed scalar introselect's random-pivot variance dominates below
16384 -- at N=8192 int64 regresses to 0.28x and float32 to 0.70x.
At N=16384 every type reliably wins (1.2x for int64, up to 11x for
16-bit types), so a single threshold is used for all widths.

Benchmark: M1 Pro NEON, numpy 2.4.4 scalar vs this branch (ns/elem)

  dtype    n       k    scalar  vqselect  speedup
  -------------------------------------------------
  float32  100K    10     5.0     2.3       2.2x
  float32  1M      100    6.8     2.6       2.6x
  float32  10M     100    8.9     2.7       3.3x
  float64  100K    10     8.0     3.6       2.2x
  float64  1M      100    9.2     4.5       2.0x
  float64  10M     100    9.3     4.5       2.1x
  int32    100K    10     6.3     1.3       4.9x
  int32    1M      100    5.8     1.5       3.8x
  int64    1M      100    5.9     2.9       2.0x
  -------------------------------------------------
  n < 16K: threshold active, falls back to scalar, parity
The fragment was named after the original (now-closed) PR 31504 on this
same branch. Rename to 31506 to match the live PR and follow numpy's
upcoming_changes naming convention.

charris commented May 27, 2026

Copy link
Copy Markdown
Member

@MaanasArora Ping.

Copy link
Copy Markdown
Member

@dmatth1 Could you please run bench_function_base.Partition.time_partition benchmarks in the numpy repository and report the changes on you mac?


@pytest.mark.skipif(
not _is_highway_qselect_platform(),
reason="Highway VQSelect dispatch only active on aarch64/ppc64"

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Is there a problem running this outside of aarch64/pcc64? If not, no harm adding more test coverage everywhere :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Looking over this a bit closer I think test_partition_highway is already run everywhere (same with test_partition_highway_16bit) so pretty sure we can just remove the two _native duplicates

dmatth1 commented May 27, 2026

Copy link
Copy Markdown
Author

Hey, I ran bench_function_base.Partition.time_partition benchmarks on my mac:

Ratio = aarch64 with VQSelect / scalar baseline

Current ungated Highway VQSelect

dtype random reversed uniform ordered sb/10 sb/100 sb/1000
int16 0.09 0.08 0.07 0.61 0.18 0.16 0.20–0.23
int32 0.34 0.12 0.14 1.17 0.61 same 0.47–0.86
int64 0.57 0.20 0.25 2.01–2.07 1.05–1.08 1.66–1.76 0.81–1.42
float32 0.45 0.25 0.35 2.71–2.74 0.79–0.84 1.73–1.78 0.78–1.85
float64 0.70 0.38 0.40 4.00–4.04 1.34–1.42 2.55–2.58 1.24–2.68

Good wins on random but the big regressions come from sorted-block and ordered. We can address these by introducing a gate to fall through to scalar when near-sorted or ordered:

Gated VQSelect

dtype random reversed uniform ordered sb/10 sb/100 sb/1000
int16 0.11 0.09 0.20 0.23 0.22 0.20 0.25–0.29
int32 0.38 0.13 0.26 0.26 0.69 1.20 0.53–0.95
int64 0.61 0.22 0.25 0.26 same 1.08 1.08
float32 0.51 0.29 0.42 same 1.06–1.11 1.25 1.13–1.28
float64 0.76 0.43 0.47 1.05 1.14–1.18 1.25–1.31 1.13–1.28

The gate introduces a bit of custom logic on the non-x86 path but nothing crazy. Do you prefer to add the gate addressing the ordered and sorted-block regressions? Or are those usecases pretty rare

Copy link
Copy Markdown
Contributor

Thanks, the benchmarks show clear boosts on your platform! @r-devulap has a better idea about the specialized libraries involved, so I'd defer mostly to him, but including VQSelect makes sense to me as long as the performance is consistent across all non-x86 platforms...

The gate introduces a bit of custom logic on the non-x86 path but nothing crazy. Do you prefer to add the gate addressing the ordered and sorted-block regressions? Or are those usecases pretty rare

The second benchmarks have been run with the gate active, correct? Is this essentially be a single-pass check over the array? I'm not sure if the overhead (and complexity) would be worth it, especially for large arrays (there seems to be a 10-20% overhead when the gate is run), but I may be wrong. Though sorting ordered arrays seems a less common use case at least.1

Footnotes

  1. Though near-sorted arrays are perhaps more common than sorted, and I'm not sure if the benchmarks would fare similarly poorly there. Probably worth extending the benchmarks on that front. Either way, probably not a prohibitive concern.

dmatth1 commented May 28, 2026

Copy link
Copy Markdown
Author

Correct the gate I'd propose is a single pass over the array counting the number of descents and only calling VQSelect when not ordered or sorted-block for floats. Does add that 10-20% overhead you called out so definitely a question of whether it's worth it for the added complexity and specificity. Something like

  // one descent-count pass drives both guards
  npy_intp descents = 0;
  for (npy_intp i = 1; i < num; i++) descents += (v[i - 1] > v[i]);                                                                                                                                                           

  // Guard 1 — already-partitioned fast-path
  if (descents == 0) {                       // non-decreasing                                                                                                                                                                
      bool no_op = true;
      if constexpr (std::is_floating_point_v<T>) {   // NaN compares false -> re-check
          npy_intp nan_count = 0;
          for (npy_intp i = 0; i < num; i++) nan_count += (v[i] != v[i]);
          no_op = (nan_count == 0);
      }
      if (no_op) return true;                 // already partitioned: postcondition met, no work
  }

  // Guard 2 — dtype-aware disorder gate
  bool use_highway = true;
  if constexpr (std::is_floating_point_v<T> || sizeof(T) == 8) {                                                                                                                                                              
      use_highway = (descents > num / 4);     // float/64-bit: scalar introselect wins near-sorted
  }                                           // 16/32-bit ints skip the gate (VQSelect wins anyway)
  if (use_highway) {
      #include "highway_qselect.dispatch.h"
      NPY_CPU_DISPATCH_CALL_XB(dispfunc = np::highway::qsort_simd::template QSelect, <TF>);
  }

Regarding other non-x86 platforms I think the main concern I would have there are some of the constants I've defined, HWY_QSELECT_MIN_N = 16384 and descents > num/4 - not entirely sure that they would hold for Graviton or ppc64/VSX2 (what's covered in the Highway qselect dispatch for numpy).

Though near-sorted arrays are perhaps more common than sorted, and I'm not sure if the benchmarks would fare similarly poorly there. Probably worth extending the benchmarks on that front. - Agreed!

seberg self-requested a review June 3, 2026 18:30

seberg commented Jun 4, 2026

Copy link
Copy Markdown
Member

I am wondering if this speed difference isn't something to report to highway and maybe fix there?
The gate seems pretty simple, but it's still annoying that we would do that in NumPy.

Copy link
Copy Markdown
Member

I am wondering if this speed difference isn't something to report to highway and maybe fix there?

+1. I think highway should ideally handle these regressions and fix them inside the library rather than bringing that complexity to NumPy. Perhaps we can explore that option first?

dmatth1 commented Jun 9, 2026

Copy link
Copy Markdown
Author

I believe this problem of VQSelect handling near-sorted and sorted poorly is a function of Neon vs x86 SIMD differences (2 lanes vs 8) so I'm not sure how Highway would address it beyond adding the gate I recommended - and that might have a negative downstream impact on other consumers of the library outside of Numpy or those with different data needs. Also makes us wait a little bit for Numpy to vendor the new version of Highway. However if we think Highway should be the ones to address the perf gap I'll open an issue!

Note: While working on this PR I discovered a corruption bug with VQSelect and NaN which could prevent us from merging this PR. I just opened an issue with Highway to address it google/highway#3098 so we might need 2 PR's in Highway.

Copy link
Copy Markdown
Member

While working on this PR I discovered a corruption bug with VQSelect and NaN which could prevent us from merging this PR. I just opened an issue with Highway to address it google/highway#3098 so we might need 2 PR's in Highway.

That means we are missing a test in NumPy. It would be great if you can add that test case here.

seberg removed their request for review June 17, 2026 11:49

seberg commented Jun 17, 2026

Copy link
Copy Markdown
Member

one duplicating comment, but maybe you can open an issue about the speed difference at highway as well? Since if they remove any speed difference, then this PR is truly unblocked, I guess.

dmatth1 commented Jun 19, 2026

Copy link
Copy Markdown
Author

Sure I can add the test @r-devulap. I opened the issue with Highway about the speed regression (google/highway#3139), will monitor that and see where that leads us

Copy link
Copy Markdown
Member

Just a quick bump that this has a conflict now.

This branch has not been deployed

No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants


Back | FazBrowse Home | New Git URL