Add BitonicSort, a GPU sorting network for small arrays and short slices - #126
Merged
Merged
Conversation
shreyas-omkar
force-pushed
the
sh/bitonic-sort
branch
from
September 16, 2026 02:45
730c10d to
d386d80
Compare
Member
Author
|
Hey @maleadt please take a look. The win is |
shreyas-omkar
marked this pull request as ready for review
September 16, 2026 09:39
Add `BitonicSort` as an opt-in GPU sort (`alg=BitonicSort()`), a portable sorting network on KernelAbstractions primitives. Small inputs sort entirely in shared memory; larger inputs use global compare-exchange for long strides and shared-memory batches for short ones. Supports the same 32/64-bit eltypes as RadixSort with forward or reverse ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `dims` path to `sort!`/`sort` (`sort(A; dims=d)`), sorting each 1-D slice independently. On the GPU each slice is sorted by its own workgroup entirely in shared memory, so per-slice sorting of many small slices is fast; each slice must fit the single-workgroup budget. Only BitonicSort implements a `dims` path; the CPU backend falls back to per-slice Base.sort!. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sort the network ascending in `ord` with the reflected first step of every merge level, and skip comparators whose partner lies past the end: any length sorts without padding, so the sentinel buffer, the eltype whitelist and the Forward/Reverse restriction go away and NaNs order like Base. Every kernel takes the slice layout from JuliaGPU#117, so `dims` needs no separate path, slices of any length work, and slices shorter than a workgroup share a tile. Tunables live on `BitonicSort(; block_size, items_per_thread)` with backend defaults from `bitonic_defaults`.
maleadt
force-pushed
the
sh/bitonic-sort
branch
from
September 16, 2026 12:37
d386d80 to
3df08bc
Compare
BitonicSort, a GPU sorting network for small arrays and short slices
Member
|
Slightly reworked the implementation on top of your two commits and rebased the branch onto #117, so the Changes made:
Numbers (RTX 5080, Float32, |
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
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BitonicSortis a new opt-in algorithm forsort!andsorton GPU backends, whole-array or alongdims. Each workgroup sorts a tile ofblock_size * items_per_threadelements (2048 by default) entirely in local memory, so an array or slice that fits a tile is sorted by a single launch, and slices shorter than a workgroup are packed several per launch. Longer inputs add one global-memory pass per long stride of the network. This is the case merge sort handles worst and radix sort cannot handle at all (RadixSorthas nodims), and it is where the new algorithm wins by a wide margin:The algorithm is not stable and has no
sortpermpath (anArgumentError), so it stays opt-in:MergeSortremains the default for both whole-array anddimssorts. Everything else followsBase.sort!:lt,by,revandordercompose the same way, NaNs and signed zeros order likeisless, and any element type works. Both tunables must be powers of two;block_sizefalls back to thesort!keyword and then tobitonic_defaults(backend), which backends can specialise (256 threads, 8 items per thread).How it works
A bitonic network sorts by a fixed sequence of compare-exchange steps: for each merge level
kk = 2, 4, ..., Nthe stridesj = kk/2, ..., 1. Two things keep the kernels simple:iwith its mirror imagei ⊻ (kk - 1), which turns two ascending runs into a bitonic sequence without ever sorting a run descending. Because no comparator moves a larger element below a smaller one, elements past the end behave as if padded with values that sort last, so pairs whose partner is past the length are simply skipped. Any length sorts in place with no padding buffer, no sentinel, and no restriction on eltype or comparator.bitonic_tile!runs all strides below the tile size for a range of levels in local memory, andbitonic_global!runs one long stride over global memory with one thread per pair. A sort is one tile launch, then per larger level its global steps followed by one tile launch.Along
dims, each launch covers every slice: long slices are split across tiles like the flat case, and slices shorter than a workgroup share a tile (SPAN < CAPin the tile kernel), which is what makes many tiny slices fast.Performance
sort!,Float32, whole call, ms. "Merge" isMergeSort(the default, from #117 fordims); "vendor" is the backend package's ownsort!where it has one.RTX 5080,
sort!(A; dims=1), slices x count. CUBDeviceSegmentedSort::SortKeys(what PyTorch uses) is listed for scale; it is kernel time only and needs contiguous segments.dims=2(strided slices) is within 10% ofdims=1up to 1024-element slices. The submitted version is faster for medium slices because it compared with>and had no bounds guards; the genericislesscomparator alone costs about 10%.RTX 5080, whole-array
sort!:Apple M1 (Metal.jl) and Intel Xe iGPU (oneAPI.jl, which forwards
sort!to AK),sort!(A; dims=1):For whole-array sorts on those two GPUs bitonic is only ahead of merge sort below about 16k elements, and the global passes are slow on the Intel iGPU (16M: 779 ms vs 383 ms merge), which the docstring reflects: use it for small arrays and short slices.
Tuning sweeps over
block_sizein {128, 256, 512} anditems_per_threadin {1, ..., 32} on all three GPUs put the default (256, 8) within 10% of the best setting for everydimsworkload; 512 x 32 exceeds Metal's 32 KiB local memory for 64-bit elements.