From fd7fc7d684451e496b9e687fd3f9a20843fd1df0 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 9 Jul 2026 15:27:45 -0400 Subject: [PATCH 1/4] Add permutedims! benchmark suite for AirSpeedVelocity BenchmarkTools suite covering the HPTT/TTC reference cases, small sizes (including the strided-rs cases), and non-SIMD-aligned sizes, over element types and thread counts, configurable via ArgParse. Co-Authored-By: Claude Fable 5 --- benchmark/Project.toml | 8 ++ benchmark/README.md | 40 ++++++ benchmark/benchmarks.jl | 288 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 benchmark/Project.toml create mode 100644 benchmark/README.md create mode 100644 benchmark/benchmarks.jl diff --git a/benchmark/Project.toml b/benchmark/Project.toml new file mode 100644 index 0000000..00fa53f --- /dev/null +++ b/benchmark/Project.toml @@ -0,0 +1,8 @@ +[deps] +ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" + +[compat] +ArgParse = "1" +BenchmarkTools = "1" diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..744567c --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,40 @@ +# Strided.jl benchmarks + +This directory contains a [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl) suite (`benchmarks.jl`, defining `SUITE`) for the `permutedims!` machinery, in the format expected by [AirSpeedVelocity.jl](https://github.com/MilesCranmer/AirSpeedVelocity.jl) and PkgBenchmark.jl. +See the header of `benchmarks.jl` for the case selection. + +## Running manually + +`benchmarks.jl` only defines `SUITE`, but its contents are configurable through command-line arguments, which can be passed after a `--` separator: + +```bash +julia --project=benchmark benchmark/benchmarks.jl --help +julia --project=benchmark -t 8 \ + -e 'include("benchmark/benchmarks.jl"); display(run(SUITE; verbose=true))' \ + -- -g small -T Float64 -f sz=1000 +``` + +The first use requires instantiating the environment: + +```bash +julia --project=benchmark -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate()' +``` + +Multithreaded variants are generated only when Julia is started with more than one thread (`-t` / `JULIA_NUM_THREADS`). +The full unfiltered grid (57 HPTT cases + 21 small cases + 42 unaligned cases, × 2 element types × 2 thread counts) takes on the order of an hour. + +## Comparing revisions with AirSpeedVelocity + +```bash +julia -e 'using Pkg; Pkg.add("AirSpeedVelocity")' # once, in the global env + +export JULIA_NUM_THREADS=8 +benchpkg Strided --rev=v2.6.1,main,dirty --path=. --bench-on=main --filter=T=Float64 +``` + +from the repository root. +`benchpkgtable` / `benchpkgplot` format the results. +AirSpeedVelocity includes the script with empty `ARGS`, so the default (full) configuration applies; restrict it with `--filter`, which matches substrings of the benchmark names (element type, thread count, permutation, and size are all part of the name). + +Note that `--bench-on` requires a revision that already contains this suite; to benchmark with an uncommitted version of the script, pass it explicitly along with its non-standard dependencies, e.g. `--script=benchmark/benchmarks.jl --add=ArgParse`. +The `--output-dir` must exist beforehand, and the current AirSpeedVelocity version (0.6.5) crashes on `--bench-on=dirty`, so prefer a committed revision there. diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl new file mode 100644 index 0000000..b1e2258 --- /dev/null +++ b/benchmark/benchmarks.jl @@ -0,0 +1,288 @@ +# Benchmark suite for Strided.jl, in the standard BenchmarkTools.jl format +# expected by AirSpeedVelocity.jl / PkgBenchmark.jl: this file defines a +# `SUITE::BenchmarkGroup` which the harness loads and runs for each revision. +# This file never runs the suite itself. +# +# The suite is configured through command-line arguments (see --help). When +# included by a harness (empty ARGS), the default configuration is used: the +# full suite, which takes on the order of an hour per revision. Use the +# harness' own filtering (e.g. `benchpkg --filter`) to restrict it. For a +# manual run, arguments can be passed after `--`: +# +# julia --project=benchmark -t 8 \ +# -e 'include("benchmark/benchmarks.jl"); display(run(SUITE; verbose=true))' \ +# -- --groups=small --eltypes=Float64 +# +# The suite focuses on `permutedims!` of strided arrays, with three groups of +# cases: +# * "hptt": the reference benchmark of HPTT / the TTC paper +# (https://github.com/springer13/hptt/blob/master/benchmark/benchmark.sh), +# reproduced verbatim at the original (large, bandwidth-bound) sizes, with +# the 0-based permutations translated to 1-based. +# * "small": small to medium sizes, including the permutation cases of +# strided-rs (https://github.com/tensor4all/strided-rs), to track per-call +# overhead, dispatch cost, and the non-threaded fast paths. +# * "unaligned": non-SIMD-aligned sizes (powers of two ± 1, primes, mixed +# odd shapes) that exercise vector-remainder loops and blocking edges. +# +# Structure of the suite: +# SUITE["permutedims!"][group]["T=$T"]["nthreads=$nt"]["p=$(p)_sz=$(sz)"] + +using ArgParse +using BenchmarkTools +using Strided +using Strided: StridedView + +function parse_config(args) + s = ArgParseSettings(; + prog = "benchmark/benchmarks.jl", + description = "Benchmark suite for permutedims! on strided arrays; " * + "defines `SUITE` without running it.", + ) + #! format: off + @add_arg_table! s begin + "--eltypes", "-T" + help = "comma-separated element types to benchmark" + default = "Float64,ComplexF64" + "--groups", "-g" + help = "comma-separated case groups to benchmark (hptt, small, unaligned)" + default = "hptt,small,unaligned" + "--nthreads", "-n" + help = "comma-separated Strided thread counts; values above the " * + "number of Julia threads (set with julia -t) are clamped" + default = "1,$(Threads.nthreads())" + "--filter", "-f" + help = "only include benchmarks whose full name contains this substring" + default = "" + end + #! format: on + return parse_args(args, s) +end + +const CONFIG = parse_config(ARGS) + +const ELTYPES = map(split(CONFIG["eltypes"], ',')) do s + T = getfield(Base, Symbol(strip(s))) + T isa Type || error("--eltypes: `$s` is not a type") + return T +end +const GROUPS = strip.(split(CONFIG["groups"], ',')) +const NTHREADS = sort!( + unique( + clamp.( + parse.(Int, strip.(split(CONFIG["nthreads"], ','))), 1, Threads.nthreads() + ) + ) +) + +# Arrays shorter than this are never threaded by Strided's kernel, so threaded +# variants of such cases would just duplicate the single-threaded numbers. +const MINTHREADLENGTH = isdefined(Strided, :MINTHREADLENGTH) ? Strided.MINTHREADLENGTH : 1 << 15 + +# The HPTT reference benchmark: (permutation, input size). Each permutation +# comes in three size variants: balanced dims, large first dim, large last dim. +const HPTT_CASES = [ + # 2D + ((2, 1), (7264, 7264)), + ((2, 1), (43408, 1216)), + ((2, 1), (1216, 43408)), + # 3D + ((1, 3, 2), (368, 384, 384)), + ((1, 3, 2), (2144, 64, 384)), + ((1, 3, 2), (368, 64, 2307)), + ((2, 1, 3), (384, 384, 355)), + ((2, 1, 3), (2320, 384, 59)), + ((2, 1, 3), (384, 2320, 59)), + ((3, 2, 1), (384, 355, 384)), + ((3, 2, 1), (2320, 59, 384)), + ((3, 2, 1), (384, 59, 2320)), + # 4D + ((1, 4, 3, 2), (80, 96, 75, 96)), + ((1, 4, 3, 2), (464, 16, 75, 96)), + ((1, 4, 3, 2), (80, 16, 75, 582)), + ((3, 2, 4, 1), (96, 75, 96, 75)), + ((3, 2, 4, 1), (608, 12, 96, 75)), + ((3, 2, 4, 1), (96, 12, 608, 75)), + ((3, 1, 4, 2), (96, 75, 96, 75)), + ((3, 1, 4, 2), (608, 12, 96, 75)), + ((3, 1, 4, 2), (96, 12, 608, 75)), + ((2, 1, 4, 3), (96, 96, 75, 75)), + ((2, 1, 4, 3), (608, 96, 12, 75)), + ((2, 1, 4, 3), (96, 608, 12, 75)), + ((4, 3, 2, 1), (96, 75, 75, 96)), + ((4, 3, 2, 1), (608, 12, 75, 96)), + ((4, 3, 2, 1), (96, 12, 75, 608)), + # 5D + ((1, 5, 3, 2, 4), (32, 48, 28, 28, 48)), + ((1, 5, 3, 2, 4), (176, 8, 28, 28, 48)), + ((1, 5, 3, 2, 4), (32, 8, 28, 28, 298)), + ((4, 3, 2, 5, 1), (48, 28, 28, 48, 28)), + ((4, 3, 2, 5, 1), (352, 4, 28, 48, 28)), + ((4, 3, 2, 5, 1), (48, 4, 28, 352, 28)), + ((3, 1, 5, 2, 4), (48, 28, 48, 28, 28)), + ((3, 1, 5, 2, 4), (352, 4, 48, 28, 28)), + ((3, 1, 5, 2, 4), (48, 4, 352, 28, 28)), + ((2, 4, 1, 5, 3), (48, 48, 28, 28, 28)), + ((2, 4, 1, 5, 3), (352, 48, 4, 28, 28)), + ((2, 4, 1, 5, 3), (48, 352, 4, 28, 28)), + ((5, 4, 3, 2, 1), (48, 28, 28, 28, 48)), + ((5, 4, 3, 2, 1), (352, 4, 28, 28, 48)), + ((5, 4, 3, 2, 1), (48, 4, 28, 28, 352)), + # 6D + ((1, 4, 3, 6, 5, 2), (16, 32, 15, 32, 15, 15)), + ((1, 4, 3, 6, 5, 2), (48, 10, 15, 32, 15, 15)), + ((1, 4, 3, 6, 5, 2), (16, 10, 15, 103, 15, 15)), + ((4, 3, 1, 6, 2, 5), (32, 15, 15, 32, 15, 15)), + ((4, 3, 1, 6, 2, 5), (112, 5, 15, 32, 15, 15)), + ((4, 3, 1, 6, 2, 5), (32, 5, 15, 112, 15, 15)), + ((3, 1, 5, 2, 6, 4), (32, 15, 32, 15, 15, 15)), + ((3, 1, 5, 2, 6, 4), (112, 5, 32, 15, 15, 15)), + ((3, 1, 5, 2, 6, 4), (32, 5, 112, 15, 15, 15)), + ((4, 3, 6, 2, 1, 5), (32, 15, 15, 32, 15, 15)), + ((4, 3, 6, 2, 1, 5), (112, 5, 15, 32, 15, 15)), + ((4, 3, 6, 2, 1, 5), (32, 5, 15, 112, 15, 15)), + ((6, 5, 4, 3, 2, 1), (32, 15, 15, 15, 15, 32)), + ((6, 5, 4, 3, 2, 1), (112, 5, 15, 15, 15, 32)), + ((6, 5, 4, 3, 2, 1), (32, 5, 15, 15, 15, 112)), +] + +# Small and medium sizes: per-call overhead and non-threaded fast paths, plus +# the permutation cases of strided-rs (1000², 4000², 32⁴ reversed). +const SMALL_CASES = [ + ((2, 1), (16, 16)), + ((2, 1), (64, 64)), + ((2, 1), (256, 256)), + ((2, 1), (1000, 1000)), + ((2, 1), (4000, 4000)), + ((3, 2, 1), (16, 16, 16)), + ((3, 2, 1), (64, 64, 64)), + ((2, 3, 1), (16, 16, 16)), + ((2, 3, 1), (64, 64, 64)), + ((4, 3, 2, 1), (8, 8, 8, 8)), + ((4, 3, 2, 1), (16, 16, 16, 16)), + ((4, 3, 2, 1), (32, 32, 32, 32)), + ((2, 1, 4, 3), (8, 8, 8, 8)), + ((2, 1, 4, 3), (16, 16, 16, 16)), + ((2, 1, 4, 3), (32, 32, 32, 32)), + ((3, 4, 1, 2), (8, 8, 8, 8)), + ((3, 4, 1, 2), (16, 16, 16, 16)), + ((3, 4, 1, 2), (32, 32, 32, 32)), + ((2, 3, 4, 1), (8, 8, 8, 8)), + ((2, 3, 4, 1), (16, 16, 16, 16)), + ((2, 3, 4, 1), (32, 32, 32, 32)), +] + +# Sizes that are not multiples of the SIMD vector width: powers of two ± 1, +# primes, and mixed odd/rectangular shapes. These defeat aligned vectorization +# and exercise the vector-remainder loops and blocking edge cases that the +# power-of-two sizes of the other groups never hit. +const UNALIGNED_CASES = [ + ((2, 1), (17, 17)), + ((2, 1), (63, 63)), + ((2, 1), (129, 129)), + ((2, 1), (251, 251)), + ((2, 1), (999, 1001)), + ((2, 1), (1021, 1021)), + ((3, 2, 1), (15, 15, 15)), + ((3, 2, 1), (17, 17, 17)), + ((3, 2, 1), (63, 63, 63)), + ((3, 2, 1), (65, 65, 65)), + ((3, 2, 1), (31, 32, 33)), + ((3, 2, 1), (37, 61, 89)), + ((2, 3, 1), (15, 15, 15)), + ((2, 3, 1), (17, 17, 17)), + ((2, 3, 1), (63, 63, 63)), + ((2, 3, 1), (65, 65, 65)), + ((2, 3, 1), (31, 32, 33)), + ((2, 3, 1), (37, 61, 89)), + ((4, 3, 2, 1), (7, 7, 7, 7)), + ((4, 3, 2, 1), (9, 9, 9, 9)), + ((4, 3, 2, 1), (15, 15, 15, 15)), + ((4, 3, 2, 1), (17, 17, 17, 17)), + ((4, 3, 2, 1), (31, 31, 31, 31)), + ((4, 3, 2, 1), (33, 33, 33, 33)), + ((2, 1, 4, 3), (7, 7, 7, 7)), + ((2, 1, 4, 3), (9, 9, 9, 9)), + ((2, 1, 4, 3), (15, 15, 15, 15)), + ((2, 1, 4, 3), (17, 17, 17, 17)), + ((2, 1, 4, 3), (31, 31, 31, 31)), + ((2, 1, 4, 3), (33, 33, 33, 33)), + ((3, 4, 1, 2), (7, 7, 7, 7)), + ((3, 4, 1, 2), (9, 9, 9, 9)), + ((3, 4, 1, 2), (15, 15, 15, 15)), + ((3, 4, 1, 2), (17, 17, 17, 17)), + ((3, 4, 1, 2), (31, 31, 31, 31)), + ((3, 4, 1, 2), (33, 33, 33, 33)), + ((2, 3, 4, 1), (7, 7, 7, 7)), + ((2, 3, 4, 1), (9, 9, 9, 9)), + ((2, 3, 4, 1), (15, 15, 15, 15)), + ((2, 3, 4, 1), (17, 17, 17, 17)), + ((2, 3, 4, 1), (31, 31, 31, 31)), + ((2, 3, 4, 1), (33, 33, 33, 33)), +] + +casename(p, sz) = "p=" * join(p) * "_sz=" * join(sz, 'x') + +# Arrays are allocated (and touched) in the per-sample setup rather than at +# suite-construction time: with the full grid of cases the arrays would +# otherwise all be alive at once, at up to ~0.9 GB each. `evals` is fixed so +# that tuning never re-runs the expensive setup, and is > 1 only for cases too +# short to time reliably in a single evaluation. +function addcases!(group::BenchmarkGroup, cases, T, nt; samples, seconds) + for (p, sz) in cases + nt > 1 && prod(sz) < MINTHREADLENGTH && continue + dsz = getindex.(Ref(sz), p) + evals = max(1, (1 << 15) ÷ prod(sz)) + group[casename(p, sz)] = @benchmarkable( + permutedims!(StridedView(B), StridedView(A), $p), + setup = ( + Strided.set_num_threads($nt); + A = rand($T, $sz); + B = fill!(Array{$T}(undef, $dsz), zero($T)) + ), + evals = evals, + samples = samples, + seconds = seconds, + ) + end + return group +end + +function filtered(group::BenchmarkGroup, pattern::AbstractString, prefix::String = "") + out = BenchmarkGroup(group.tags) + for (key, value) in group + name = prefix * "/" * key + if value isa BenchmarkGroup + sub = filtered(value, pattern, name) + isempty(sub.data) || (out[key] = sub) + elseif occursin(pattern, name) + out[key] = value + end + end + return out +end + +const SUITE = BenchmarkGroup() +let permute = addgroup!(SUITE, "permutedims!") + for name in GROUPS + cases, samples, seconds = if name == "hptt" + HPTT_CASES, 10, 10 + elseif name == "small" + SMALL_CASES, 1000, 5 + elseif name == "unaligned" + UNALIGNED_CASES, 1000, 5 + else + error("--groups: unknown group `$name`") + end + g = addgroup!(permute, name) + for T in ELTYPES + tg = addgroup!(g, "T=$T") + for nt in NTHREADS + addcases!(addgroup!(tg, "nthreads=$nt"), cases, T, nt; samples, seconds) + end + end + end + if !isempty(CONFIG["filter"]) + SUITE["permutedims!"] = filtered(permute, CONFIG["filter"]) + end +end From dde5c61a95b728e6cca5f26b3101ded6d8481d90 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 9 Jul 2026 15:36:08 -0400 Subject: [PATCH 2/4] Move benchmark cases to a TOML file Cases (and per-group sampling parameters) now live in benchmark/cases.toml; an alternative file can be passed with --cases. Co-Authored-By: Claude Fable 5 --- benchmark/Project.toml | 2 + benchmark/README.md | 2 +- benchmark/benchmarks.jl | 196 +++++++--------------------------------- benchmark/cases.toml | 154 +++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 164 deletions(-) create mode 100644 benchmark/cases.toml diff --git a/benchmark/Project.toml b/benchmark/Project.toml index 00fa53f..c75c69c 100644 --- a/benchmark/Project.toml +++ b/benchmark/Project.toml @@ -2,7 +2,9 @@ ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] ArgParse = "1" BenchmarkTools = "1" +TOML = "1" diff --git a/benchmark/README.md b/benchmark/README.md index 744567c..be31c54 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,7 +1,7 @@ # Strided.jl benchmarks This directory contains a [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl) suite (`benchmarks.jl`, defining `SUITE`) for the `permutedims!` machinery, in the format expected by [AirSpeedVelocity.jl](https://github.com/MilesCranmer/AirSpeedVelocity.jl) and PkgBenchmark.jl. -See the header of `benchmarks.jl` for the case selection. +The benchmark cases are defined in `cases.toml` (see its comments for the format and the provenance of the default groups); a different cases file can be passed with `--cases`. ## Running manually diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index b1e2258..4ef767c 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -13,17 +13,10 @@ # -e 'include("benchmark/benchmarks.jl"); display(run(SUITE; verbose=true))' \ # -- --groups=small --eltypes=Float64 # -# The suite focuses on `permutedims!` of strided arrays, with three groups of -# cases: -# * "hptt": the reference benchmark of HPTT / the TTC paper -# (https://github.com/springer13/hptt/blob/master/benchmark/benchmark.sh), -# reproduced verbatim at the original (large, bandwidth-bound) sizes, with -# the 0-based permutations translated to 1-based. -# * "small": small to medium sizes, including the permutation cases of -# strided-rs (https://github.com/tensor4all/strided-rs), to track per-call -# overhead, dispatch cost, and the non-threaded fast paths. -# * "unaligned": non-SIMD-aligned sizes (powers of two ± 1, primes, mixed -# odd shapes) that exercise vector-remainder loops and blocking edges. +# The suite focuses on `permutedims!` of strided arrays. The cases are grouped +# and defined in a TOML file, by default the cases.toml next to this script +# (see its comments for the format and the provenance of the default groups: +# hptt, small, unaligned); a different file can be passed with --cases. # # Structure of the suite: # SUITE["permutedims!"][group]["T=$T"]["nthreads=$nt"]["p=$(p)_sz=$(sz)"] @@ -32,6 +25,7 @@ using ArgParse using BenchmarkTools using Strided using Strided: StridedView +using TOML function parse_config(args) s = ArgParseSettings(; @@ -44,9 +38,13 @@ function parse_config(args) "--eltypes", "-T" help = "comma-separated element types to benchmark" default = "Float64,ComplexF64" + "--cases", "-c" + help = "TOML file defining the benchmark cases" + default = joinpath(@__DIR__, "cases.toml") "--groups", "-g" - help = "comma-separated case groups to benchmark (hptt, small, unaligned)" - default = "hptt,small,unaligned" + help = "comma-separated case groups to benchmark; " * + "defaults to all groups in the cases file" + default = "" "--nthreads", "-n" help = "comma-separated Strided thread counts; values above the " * "number of Julia threads (set with julia -t) are clamped" @@ -66,7 +64,6 @@ const ELTYPES = map(split(CONFIG["eltypes"], ',')) do s T isa Type || error("--eltypes: `$s` is not a type") return T end -const GROUPS = strip.(split(CONFIG["groups"], ',')) const NTHREADS = sort!( unique( clamp.( @@ -79,147 +76,24 @@ const NTHREADS = sort!( # variants of such cases would just duplicate the single-threaded numbers. const MINTHREADLENGTH = isdefined(Strided, :MINTHREADLENGTH) ? Strided.MINTHREADLENGTH : 1 << 15 -# The HPTT reference benchmark: (permutation, input size). Each permutation -# comes in three size variants: balanced dims, large first dim, large last dim. -const HPTT_CASES = [ - # 2D - ((2, 1), (7264, 7264)), - ((2, 1), (43408, 1216)), - ((2, 1), (1216, 43408)), - # 3D - ((1, 3, 2), (368, 384, 384)), - ((1, 3, 2), (2144, 64, 384)), - ((1, 3, 2), (368, 64, 2307)), - ((2, 1, 3), (384, 384, 355)), - ((2, 1, 3), (2320, 384, 59)), - ((2, 1, 3), (384, 2320, 59)), - ((3, 2, 1), (384, 355, 384)), - ((3, 2, 1), (2320, 59, 384)), - ((3, 2, 1), (384, 59, 2320)), - # 4D - ((1, 4, 3, 2), (80, 96, 75, 96)), - ((1, 4, 3, 2), (464, 16, 75, 96)), - ((1, 4, 3, 2), (80, 16, 75, 582)), - ((3, 2, 4, 1), (96, 75, 96, 75)), - ((3, 2, 4, 1), (608, 12, 96, 75)), - ((3, 2, 4, 1), (96, 12, 608, 75)), - ((3, 1, 4, 2), (96, 75, 96, 75)), - ((3, 1, 4, 2), (608, 12, 96, 75)), - ((3, 1, 4, 2), (96, 12, 608, 75)), - ((2, 1, 4, 3), (96, 96, 75, 75)), - ((2, 1, 4, 3), (608, 96, 12, 75)), - ((2, 1, 4, 3), (96, 608, 12, 75)), - ((4, 3, 2, 1), (96, 75, 75, 96)), - ((4, 3, 2, 1), (608, 12, 75, 96)), - ((4, 3, 2, 1), (96, 12, 75, 608)), - # 5D - ((1, 5, 3, 2, 4), (32, 48, 28, 28, 48)), - ((1, 5, 3, 2, 4), (176, 8, 28, 28, 48)), - ((1, 5, 3, 2, 4), (32, 8, 28, 28, 298)), - ((4, 3, 2, 5, 1), (48, 28, 28, 48, 28)), - ((4, 3, 2, 5, 1), (352, 4, 28, 48, 28)), - ((4, 3, 2, 5, 1), (48, 4, 28, 352, 28)), - ((3, 1, 5, 2, 4), (48, 28, 48, 28, 28)), - ((3, 1, 5, 2, 4), (352, 4, 48, 28, 28)), - ((3, 1, 5, 2, 4), (48, 4, 352, 28, 28)), - ((2, 4, 1, 5, 3), (48, 48, 28, 28, 28)), - ((2, 4, 1, 5, 3), (352, 48, 4, 28, 28)), - ((2, 4, 1, 5, 3), (48, 352, 4, 28, 28)), - ((5, 4, 3, 2, 1), (48, 28, 28, 28, 48)), - ((5, 4, 3, 2, 1), (352, 4, 28, 28, 48)), - ((5, 4, 3, 2, 1), (48, 4, 28, 28, 352)), - # 6D - ((1, 4, 3, 6, 5, 2), (16, 32, 15, 32, 15, 15)), - ((1, 4, 3, 6, 5, 2), (48, 10, 15, 32, 15, 15)), - ((1, 4, 3, 6, 5, 2), (16, 10, 15, 103, 15, 15)), - ((4, 3, 1, 6, 2, 5), (32, 15, 15, 32, 15, 15)), - ((4, 3, 1, 6, 2, 5), (112, 5, 15, 32, 15, 15)), - ((4, 3, 1, 6, 2, 5), (32, 5, 15, 112, 15, 15)), - ((3, 1, 5, 2, 6, 4), (32, 15, 32, 15, 15, 15)), - ((3, 1, 5, 2, 6, 4), (112, 5, 32, 15, 15, 15)), - ((3, 1, 5, 2, 6, 4), (32, 5, 112, 15, 15, 15)), - ((4, 3, 6, 2, 1, 5), (32, 15, 15, 32, 15, 15)), - ((4, 3, 6, 2, 1, 5), (112, 5, 15, 32, 15, 15)), - ((4, 3, 6, 2, 1, 5), (32, 5, 15, 112, 15, 15)), - ((6, 5, 4, 3, 2, 1), (32, 15, 15, 15, 15, 32)), - ((6, 5, 4, 3, 2, 1), (112, 5, 15, 15, 15, 32)), - ((6, 5, 4, 3, 2, 1), (32, 5, 15, 15, 15, 112)), -] - -# Small and medium sizes: per-call overhead and non-threaded fast paths, plus -# the permutation cases of strided-rs (1000², 4000², 32⁴ reversed). -const SMALL_CASES = [ - ((2, 1), (16, 16)), - ((2, 1), (64, 64)), - ((2, 1), (256, 256)), - ((2, 1), (1000, 1000)), - ((2, 1), (4000, 4000)), - ((3, 2, 1), (16, 16, 16)), - ((3, 2, 1), (64, 64, 64)), - ((2, 3, 1), (16, 16, 16)), - ((2, 3, 1), (64, 64, 64)), - ((4, 3, 2, 1), (8, 8, 8, 8)), - ((4, 3, 2, 1), (16, 16, 16, 16)), - ((4, 3, 2, 1), (32, 32, 32, 32)), - ((2, 1, 4, 3), (8, 8, 8, 8)), - ((2, 1, 4, 3), (16, 16, 16, 16)), - ((2, 1, 4, 3), (32, 32, 32, 32)), - ((3, 4, 1, 2), (8, 8, 8, 8)), - ((3, 4, 1, 2), (16, 16, 16, 16)), - ((3, 4, 1, 2), (32, 32, 32, 32)), - ((2, 3, 4, 1), (8, 8, 8, 8)), - ((2, 3, 4, 1), (16, 16, 16, 16)), - ((2, 3, 4, 1), (32, 32, 32, 32)), -] +# The benchmark cases, as a Dict mapping group name => group spec (sampling +# parameters and case list). +const CASEGROUPS = TOML.parsefile(CONFIG["cases"]) +const GROUPS = if isempty(strip(CONFIG["groups"])) + sort!(collect(keys(CASEGROUPS))) +else + strip.(split(CONFIG["groups"], ',')) +end -# Sizes that are not multiples of the SIMD vector width: powers of two ± 1, -# primes, and mixed odd/rectangular shapes. These defeat aligned vectorization -# and exercise the vector-remainder loops and blocking edge cases that the -# power-of-two sizes of the other groups never hit. -const UNALIGNED_CASES = [ - ((2, 1), (17, 17)), - ((2, 1), (63, 63)), - ((2, 1), (129, 129)), - ((2, 1), (251, 251)), - ((2, 1), (999, 1001)), - ((2, 1), (1021, 1021)), - ((3, 2, 1), (15, 15, 15)), - ((3, 2, 1), (17, 17, 17)), - ((3, 2, 1), (63, 63, 63)), - ((3, 2, 1), (65, 65, 65)), - ((3, 2, 1), (31, 32, 33)), - ((3, 2, 1), (37, 61, 89)), - ((2, 3, 1), (15, 15, 15)), - ((2, 3, 1), (17, 17, 17)), - ((2, 3, 1), (63, 63, 63)), - ((2, 3, 1), (65, 65, 65)), - ((2, 3, 1), (31, 32, 33)), - ((2, 3, 1), (37, 61, 89)), - ((4, 3, 2, 1), (7, 7, 7, 7)), - ((4, 3, 2, 1), (9, 9, 9, 9)), - ((4, 3, 2, 1), (15, 15, 15, 15)), - ((4, 3, 2, 1), (17, 17, 17, 17)), - ((4, 3, 2, 1), (31, 31, 31, 31)), - ((4, 3, 2, 1), (33, 33, 33, 33)), - ((2, 1, 4, 3), (7, 7, 7, 7)), - ((2, 1, 4, 3), (9, 9, 9, 9)), - ((2, 1, 4, 3), (15, 15, 15, 15)), - ((2, 1, 4, 3), (17, 17, 17, 17)), - ((2, 1, 4, 3), (31, 31, 31, 31)), - ((2, 1, 4, 3), (33, 33, 33, 33)), - ((3, 4, 1, 2), (7, 7, 7, 7)), - ((3, 4, 1, 2), (9, 9, 9, 9)), - ((3, 4, 1, 2), (15, 15, 15, 15)), - ((3, 4, 1, 2), (17, 17, 17, 17)), - ((3, 4, 1, 2), (31, 31, 31, 31)), - ((3, 4, 1, 2), (33, 33, 33, 33)), - ((2, 3, 4, 1), (7, 7, 7, 7)), - ((2, 3, 4, 1), (9, 9, 9, 9)), - ((2, 3, 4, 1), (15, 15, 15, 15)), - ((2, 3, 4, 1), (17, 17, 17, 17)), - ((2, 3, 4, 1), (31, 31, 31, 31)), - ((2, 3, 4, 1), (33, 33, 33, 33)), -] +function parsecases(spec, path) + return map(spec["cases"]) do c + p = (Int.(c["p"])...,) + sz = (Int.(c["size"])...,) + (length(p) == length(sz) && isperm(p)) || + error("$path: `p = $(c["p"])` is not a permutation matching `size = $(c["size"])`") + return (p, sz) + end +end casename(p, sz) = "p=" * join(p) * "_sz=" * join(sz, 'x') @@ -265,15 +139,11 @@ end const SUITE = BenchmarkGroup() let permute = addgroup!(SUITE, "permutedims!") for name in GROUPS - cases, samples, seconds = if name == "hptt" - HPTT_CASES, 10, 10 - elseif name == "small" - SMALL_CASES, 1000, 5 - elseif name == "unaligned" - UNALIGNED_CASES, 1000, 5 - else - error("--groups: unknown group `$name`") - end + haskey(CASEGROUPS, name) || + error("--groups: group `$name` not found in $(CONFIG["cases"])") + spec = CASEGROUPS[name] + cases = parsecases(spec, CONFIG["cases"]) + samples, seconds = spec["samples"], spec["seconds"] g = addgroup!(permute, name) for T in ELTYPES tg = addgroup!(g, "T=$T") diff --git a/benchmark/cases.toml b/benchmark/cases.toml new file mode 100644 index 0000000..21d8f29 --- /dev/null +++ b/benchmark/cases.toml @@ -0,0 +1,154 @@ +# Benchmark cases for benchmarks.jl. Each group carries its BenchmarkTools +# sampling parameters and a list of cases, where `p` is the permutation and +# `size` the input size (so the output has size `size[p]`). + +# The reference benchmark of HPTT / the TTC paper, reproduced verbatim at the +# original (large, bandwidth-bound) sizes, with the 0-based permutations +# translated to 1-based: +# https://github.com/springer13/hptt/blob/master/benchmark/benchmark.sh +# Each permutation comes in three size variants: balanced dims, large first +# dim, large last dim. +[hptt] +samples = 10 +seconds = 10 +cases = [ + { p = [2, 1], size = [7264, 7264] }, + { p = [2, 1], size = [43408, 1216] }, + { p = [2, 1], size = [1216, 43408] }, + { p = [1, 3, 2], size = [368, 384, 384] }, + { p = [1, 3, 2], size = [2144, 64, 384] }, + { p = [1, 3, 2], size = [368, 64, 2307] }, + { p = [2, 1, 3], size = [384, 384, 355] }, + { p = [2, 1, 3], size = [2320, 384, 59] }, + { p = [2, 1, 3], size = [384, 2320, 59] }, + { p = [3, 2, 1], size = [384, 355, 384] }, + { p = [3, 2, 1], size = [2320, 59, 384] }, + { p = [3, 2, 1], size = [384, 59, 2320] }, + { p = [1, 4, 3, 2], size = [80, 96, 75, 96] }, + { p = [1, 4, 3, 2], size = [464, 16, 75, 96] }, + { p = [1, 4, 3, 2], size = [80, 16, 75, 582] }, + { p = [3, 2, 4, 1], size = [96, 75, 96, 75] }, + { p = [3, 2, 4, 1], size = [608, 12, 96, 75] }, + { p = [3, 2, 4, 1], size = [96, 12, 608, 75] }, + { p = [3, 1, 4, 2], size = [96, 75, 96, 75] }, + { p = [3, 1, 4, 2], size = [608, 12, 96, 75] }, + { p = [3, 1, 4, 2], size = [96, 12, 608, 75] }, + { p = [2, 1, 4, 3], size = [96, 96, 75, 75] }, + { p = [2, 1, 4, 3], size = [608, 96, 12, 75] }, + { p = [2, 1, 4, 3], size = [96, 608, 12, 75] }, + { p = [4, 3, 2, 1], size = [96, 75, 75, 96] }, + { p = [4, 3, 2, 1], size = [608, 12, 75, 96] }, + { p = [4, 3, 2, 1], size = [96, 12, 75, 608] }, + { p = [1, 5, 3, 2, 4], size = [32, 48, 28, 28, 48] }, + { p = [1, 5, 3, 2, 4], size = [176, 8, 28, 28, 48] }, + { p = [1, 5, 3, 2, 4], size = [32, 8, 28, 28, 298] }, + { p = [4, 3, 2, 5, 1], size = [48, 28, 28, 48, 28] }, + { p = [4, 3, 2, 5, 1], size = [352, 4, 28, 48, 28] }, + { p = [4, 3, 2, 5, 1], size = [48, 4, 28, 352, 28] }, + { p = [3, 1, 5, 2, 4], size = [48, 28, 48, 28, 28] }, + { p = [3, 1, 5, 2, 4], size = [352, 4, 48, 28, 28] }, + { p = [3, 1, 5, 2, 4], size = [48, 4, 352, 28, 28] }, + { p = [2, 4, 1, 5, 3], size = [48, 48, 28, 28, 28] }, + { p = [2, 4, 1, 5, 3], size = [352, 48, 4, 28, 28] }, + { p = [2, 4, 1, 5, 3], size = [48, 352, 4, 28, 28] }, + { p = [5, 4, 3, 2, 1], size = [48, 28, 28, 28, 48] }, + { p = [5, 4, 3, 2, 1], size = [352, 4, 28, 28, 48] }, + { p = [5, 4, 3, 2, 1], size = [48, 4, 28, 28, 352] }, + { p = [1, 4, 3, 6, 5, 2], size = [16, 32, 15, 32, 15, 15] }, + { p = [1, 4, 3, 6, 5, 2], size = [48, 10, 15, 32, 15, 15] }, + { p = [1, 4, 3, 6, 5, 2], size = [16, 10, 15, 103, 15, 15] }, + { p = [4, 3, 1, 6, 2, 5], size = [32, 15, 15, 32, 15, 15] }, + { p = [4, 3, 1, 6, 2, 5], size = [112, 5, 15, 32, 15, 15] }, + { p = [4, 3, 1, 6, 2, 5], size = [32, 5, 15, 112, 15, 15] }, + { p = [3, 1, 5, 2, 6, 4], size = [32, 15, 32, 15, 15, 15] }, + { p = [3, 1, 5, 2, 6, 4], size = [112, 5, 32, 15, 15, 15] }, + { p = [3, 1, 5, 2, 6, 4], size = [32, 5, 112, 15, 15, 15] }, + { p = [4, 3, 6, 2, 1, 5], size = [32, 15, 15, 32, 15, 15] }, + { p = [4, 3, 6, 2, 1, 5], size = [112, 5, 15, 32, 15, 15] }, + { p = [4, 3, 6, 2, 1, 5], size = [32, 5, 15, 112, 15, 15] }, + { p = [6, 5, 4, 3, 2, 1], size = [32, 15, 15, 15, 15, 32] }, + { p = [6, 5, 4, 3, 2, 1], size = [112, 5, 15, 15, 15, 32] }, + { p = [6, 5, 4, 3, 2, 1], size = [32, 5, 15, 15, 15, 112] }, +] + +# Small to medium sizes, including the permutation cases of strided-rs +# (https://github.com/tensor4all/strided-rs), to track per-call overhead, +# dispatch cost, and the non-threaded fast paths. +[small] +samples = 1000 +seconds = 5 +cases = [ + { p = [2, 1], size = [16, 16] }, + { p = [2, 1], size = [64, 64] }, + { p = [2, 1], size = [256, 256] }, + { p = [2, 1], size = [1000, 1000] }, + { p = [2, 1], size = [4000, 4000] }, + { p = [3, 2, 1], size = [16, 16, 16] }, + { p = [3, 2, 1], size = [64, 64, 64] }, + { p = [2, 3, 1], size = [16, 16, 16] }, + { p = [2, 3, 1], size = [64, 64, 64] }, + { p = [4, 3, 2, 1], size = [8, 8, 8, 8] }, + { p = [4, 3, 2, 1], size = [16, 16, 16, 16] }, + { p = [4, 3, 2, 1], size = [32, 32, 32, 32] }, + { p = [2, 1, 4, 3], size = [8, 8, 8, 8] }, + { p = [2, 1, 4, 3], size = [16, 16, 16, 16] }, + { p = [2, 1, 4, 3], size = [32, 32, 32, 32] }, + { p = [3, 4, 1, 2], size = [8, 8, 8, 8] }, + { p = [3, 4, 1, 2], size = [16, 16, 16, 16] }, + { p = [3, 4, 1, 2], size = [32, 32, 32, 32] }, + { p = [2, 3, 4, 1], size = [8, 8, 8, 8] }, + { p = [2, 3, 4, 1], size = [16, 16, 16, 16] }, + { p = [2, 3, 4, 1], size = [32, 32, 32, 32] }, +] + +# Sizes that are not multiples of the SIMD vector width: powers of two +- 1, +# primes, and mixed odd/rectangular shapes. These defeat aligned vectorization +# and exercise the vector-remainder loops and blocking edge cases that the +# power-of-two sizes of the other groups never hit. +[unaligned] +samples = 1000 +seconds = 5 +cases = [ + { p = [2, 1], size = [17, 17] }, + { p = [2, 1], size = [63, 63] }, + { p = [2, 1], size = [129, 129] }, + { p = [2, 1], size = [251, 251] }, + { p = [2, 1], size = [999, 1001] }, + { p = [2, 1], size = [1021, 1021] }, + { p = [3, 2, 1], size = [15, 15, 15] }, + { p = [3, 2, 1], size = [17, 17, 17] }, + { p = [3, 2, 1], size = [63, 63, 63] }, + { p = [3, 2, 1], size = [65, 65, 65] }, + { p = [3, 2, 1], size = [31, 32, 33] }, + { p = [3, 2, 1], size = [37, 61, 89] }, + { p = [2, 3, 1], size = [15, 15, 15] }, + { p = [2, 3, 1], size = [17, 17, 17] }, + { p = [2, 3, 1], size = [63, 63, 63] }, + { p = [2, 3, 1], size = [65, 65, 65] }, + { p = [2, 3, 1], size = [31, 32, 33] }, + { p = [2, 3, 1], size = [37, 61, 89] }, + { p = [4, 3, 2, 1], size = [7, 7, 7, 7] }, + { p = [4, 3, 2, 1], size = [9, 9, 9, 9] }, + { p = [4, 3, 2, 1], size = [15, 15, 15, 15] }, + { p = [4, 3, 2, 1], size = [17, 17, 17, 17] }, + { p = [4, 3, 2, 1], size = [31, 31, 31, 31] }, + { p = [4, 3, 2, 1], size = [33, 33, 33, 33] }, + { p = [2, 1, 4, 3], size = [7, 7, 7, 7] }, + { p = [2, 1, 4, 3], size = [9, 9, 9, 9] }, + { p = [2, 1, 4, 3], size = [15, 15, 15, 15] }, + { p = [2, 1, 4, 3], size = [17, 17, 17, 17] }, + { p = [2, 1, 4, 3], size = [31, 31, 31, 31] }, + { p = [2, 1, 4, 3], size = [33, 33, 33, 33] }, + { p = [3, 4, 1, 2], size = [7, 7, 7, 7] }, + { p = [3, 4, 1, 2], size = [9, 9, 9, 9] }, + { p = [3, 4, 1, 2], size = [15, 15, 15, 15] }, + { p = [3, 4, 1, 2], size = [17, 17, 17, 17] }, + { p = [3, 4, 1, 2], size = [31, 31, 31, 31] }, + { p = [3, 4, 1, 2], size = [33, 33, 33, 33] }, + { p = [2, 3, 4, 1], size = [7, 7, 7, 7] }, + { p = [2, 3, 4, 1], size = [9, 9, 9, 9] }, + { p = [2, 3, 4, 1], size = [15, 15, 15, 15] }, + { p = [2, 3, 4, 1], size = [17, 17, 17, 17] }, + { p = [2, 3, 4, 1], size = [31, 31, 31, 31] }, + { p = [2, 3, 4, 1], size = [33, 33, 33, 33] }, +] From a87c05adf590cfc51e910b4bd563e8af504c6234 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 10 Jul 2026 10:17:03 -0400 Subject: [PATCH 3/4] Redefine cases by shape + swept total length, add copy baseline and strided views Cases now fix a shape (dimension ratios) and permutation and sweep a list of total lengths, so each can be normalized against a same-length memory copy (auto-generated `copy` group) for a machine-independent efficiency. Adds a `strided` group whose inputs/outputs are genuinely non-contiguous strided views (non-unit leading and second-leading strides), replacing the explicit-size hptt/small/unaligned groups. Co-Authored-By: Claude Opus 4.8 (1M context) --- benchmark/README.md | 11 ++- benchmark/benchmarks.jl | 164 ++++++++++++++++++++++--------- benchmark/cases.toml | 209 +++++++++++++--------------------------- 3 files changed, 194 insertions(+), 190 deletions(-) diff --git a/benchmark/README.md b/benchmark/README.md index be31c54..0545750 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,7 +1,12 @@ # Strided.jl benchmarks This directory contains a [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl) suite (`benchmarks.jl`, defining `SUITE`) for the `permutedims!` machinery, in the format expected by [AirSpeedVelocity.jl](https://github.com/MilesCranmer/AirSpeedVelocity.jl) and PkgBenchmark.jl. -The benchmark cases are defined in `cases.toml` (see its comments for the format and the provenance of the default groups); a different cases file can be passed with `--cases`. + +Each case fixes a *shape* (dimension ratios) and a *permutation* and is swept over a list of total lengths, so its time can be divided by that of a plain memory copy of the same length — the auto-generated `copy` group — to get a machine-independent efficiency (`≥ 1`, approaching `1` when bandwidth-bound). +The default groups (`balanced`, `skewed`, `strided`) live in `cases.toml`; the `strided` group makes the input/output non-contiguous strided views to exercise the paths a dense array never hits. +See the comments in `cases.toml` for the format; a different cases file can be passed with `--cases`. + +The copy baseline is single-threaded, so multi-threaded permutation ratios are relative to a single-threaded memory copy. ## Running manually @@ -11,7 +16,7 @@ The benchmark cases are defined in `cases.toml` (see its comments for the format julia --project=benchmark benchmark/benchmarks.jl --help julia --project=benchmark -t 8 \ -e 'include("benchmark/benchmarks.jl"); display(run(SUITE; verbose=true))' \ - -- -g small -T Float64 -f sz=1000 + -- -g balanced -T Float64 -f L=1048576 ``` The first use requires instantiating the environment: @@ -21,7 +26,7 @@ julia --project=benchmark -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate( ``` Multithreaded variants are generated only when Julia is started with more than one thread (`-t` / `JULIA_NUM_THREADS`). -The full unfiltered grid (57 HPTT cases + 21 small cases + 42 unaligned cases, × 2 element types × 2 thread counts) takes on the order of an hour. +The full unfiltered grid takes on the order of an hour; restrict it with `-g`/`-T`/`-n`/`-f` (or `benchpkg --filter`). ## Comparing revisions with AirSpeedVelocity diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index 4ef767c..cb5b4e8 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -11,15 +11,25 @@ # # julia --project=benchmark -t 8 \ # -e 'include("benchmark/benchmarks.jl"); display(run(SUITE; verbose=true))' \ -# -- --groups=small --eltypes=Float64 +# -- --groups=balanced --eltypes=Float64 # -# The suite focuses on `permutedims!` of strided arrays. The cases are grouped -# and defined in a TOML file, by default the cases.toml next to this script -# (see its comments for the format and the provenance of the default groups: -# hptt, small, unaligned); a different file can be passed with --cases. +# The suite focuses on `permutedims!` of strided arrays. Each case fixes a +# *shape* (dimension ratios) and a *permutation*, and is swept over a list of +# total lengths; the cases are defined in a TOML file, by default the cases.toml +# next to this script (see its comments for the format and the groups). A +# different file can be passed with --cases. +# +# Alongside `permutedims!`, the suite auto-generates a "copy" group: a plain +# `copyto!` on `Vector`s of each swept length. Since a permutation and a copy +# both read and write the same number of elements, dividing a permutation's +# time by the copy time of the same length gives a machine-independent +# efficiency (>= 1, approaching 1 when bandwidth-bound). The copy baseline is +# single-threaded, so multi-threaded permutation ratios are relative to a +# single-threaded memory copy. # # Structure of the suite: -# SUITE["permutedims!"][group]["T=$T"]["nthreads=$nt"]["p=$(p)_sz=$(sz)"] +# SUITE["permutedims!"][group]["T=$T"]["nthreads=$nt"][case] +# SUITE["copy"]["T=$T"]["L=$L"] using ArgParse using BenchmarkTools @@ -68,16 +78,14 @@ const NTHREADS = sort!( unique( clamp.( parse.(Int, strip.(split(CONFIG["nthreads"], ','))), 1, Threads.nthreads() - ) - ) + ), + ), ) # Arrays shorter than this are never threaded by Strided's kernel, so threaded # variants of such cases would just duplicate the single-threaded numbers. const MINTHREADLENGTH = isdefined(Strided, :MINTHREADLENGTH) ? Strided.MINTHREADLENGTH : 1 << 15 -# The benchmark cases, as a Dict mapping group name => group spec (sampling -# parameters and case list). const CASEGROUPS = TOML.parsefile(CONFIG["cases"]) const GROUPS = if isempty(strip(CONFIG["groups"])) sort!(collect(keys(CASEGROUPS))) @@ -85,40 +93,97 @@ else strip.(split(CONFIG["groups"], ',')) end -function parsecases(spec, path) - return map(spec["cases"]) do c +# Column-major strides of a dense array of size `sz`. +colstrides(sz::NTuple{N,Int}) where {N} = ntuple(i -> prod(ntuple(j -> sz[j], i - 1)), N) + +# Dimensions realizing (approximately) `total` elements at the given shape ratio. +function shapedims(shape::NTuple{N,<:Real}, total::Int) where {N} + c = (total / prod(shape))^(1 / N) + return ntuple(i -> max(1, round(Int, shape[i] * c)), N) +end + +# A StridedView of logical size `sz` whose axes are spaced by `mult` inside a +# freshly allocated (page-faulted) backing buffer. `mult` all ones gives a dense +# view (the unit-stride fast path); a leading value > 1 gives a non-unit +# innermost stride, a later value > 1 gives gaps between higher dimensions. +function make_view(::Type{T}, sz::NTuple{N,Int}, mult::NTuple{N,Int}) where {T,N} + all(isone, mult) && return StridedView(rand(T, sz)) + bufsz = ntuple(i -> sz[i] * mult[i], N) + strides = ntuple(i -> mult[i] * colstrides(bufsz)[i], N) + return StridedView(rand(T, prod(bufsz)), sz, strides) +end + +# Expand a group spec into concrete case instances (one per swept total). +function expand(spec, group) + totals = haskey(spec, "totals") ? Int.(spec["totals"]) : Int[] + insts = NamedTuple[] + for c in spec["cases"] p = (Int.(c["p"])...,) - sz = (Int.(c["size"])...,) - (length(p) == length(sz) && isperm(p)) || - error("$path: `p = $(c["p"])` is not a permutation matching `size = $(c["size"])`") - return (p, sz) + isperm(p) || error("$group: `p = $(c["p"])` is not a permutation") + N = length(p) + min = haskey(c, "stride_in") ? (Int.(c["stride_in"])...,) : ntuple(one, N) + mout = haskey(c, "stride_out") ? (Int.(c["stride_out"])...,) : ntuple(one, N) + (length(min) == N == length(mout)) || + error("$group: `stride_in`/`stride_out` must have length $N") + if haskey(c, "size") + dims = (Int.(c["size"])...,) + push!(insts, (; p, dims, min, mout, L = prod(dims), shape = nothing)) + elseif haskey(c, "shape") + shape = (Float64.(c["shape"])...,) + length(shape) == N || error("$group: `shape` must have length $N") + isempty(totals) && + error("$group: case uses `shape` but the group has no `totals`") + for total in totals + push!(insts, (; p, dims = shapedims(shape, total), min, mout, L = total, shape)) + end + else + error("$group: each case needs a `size` or a `shape`") + end end + return insts end -casename(p, sz) = "p=" * join(p) * "_sz=" * join(sz, 'x') +function casename(inst) + s = "p=" * join(inst.p) * "_L=" * string(inst.L) + inst.shape === nothing || (s *= "_shape=" * join(round.(Int, inst.shape), 'x')) + any(!isone, inst.min) && (s *= "_sin=" * join(inst.min, 'x')) + any(!isone, inst.mout) && (s *= "_sout=" * join(inst.mout, 'x')) + return s +end # Arrays are allocated (and touched) in the per-sample setup rather than at -# suite-construction time: with the full grid of cases the arrays would -# otherwise all be alive at once, at up to ~0.9 GB each. `evals` is fixed so -# that tuning never re-runs the expensive setup, and is > 1 only for cases too -# short to time reliably in a single evaluation. -function addcases!(group::BenchmarkGroup, cases, T, nt; samples, seconds) - for (p, sz) in cases - nt > 1 && prod(sz) < MINTHREADLENGTH && continue - dsz = getindex.(Ref(sz), p) - evals = max(1, (1 << 15) ÷ prod(sz)) - group[casename(p, sz)] = @benchmarkable( - permutedims!(StridedView(B), StridedView(A), $p), - setup = ( - Strided.set_num_threads($nt); - A = rand($T, $sz); - B = fill!(Array{$T}(undef, $dsz), zero($T)) - ), - evals = evals, - samples = samples, - seconds = seconds, - ) - end +# suite-construction time: the full grid would otherwise keep every array alive +# at once. `evals` is fixed so that tuning never re-runs the setup, and is > 1 +# only for cases too short to time reliably in a single evaluation. +function addcase!(group::BenchmarkGroup, inst, ::Type{T}, nt; samples, seconds) where {T} + L = prod(inst.dims) + nt > 1 && L < MINTHREADLENGTH && return group + dsz = ntuple(i -> inst.dims[inst.p[i]], length(inst.p)) + evals = max(1, (1 << 15) ÷ L) + p, sz, min, mout = inst.p, inst.dims, inst.min, inst.mout + group[casename(inst)] = @benchmarkable( + permutedims!(dst, src, $p), + setup = ( + Strided.set_num_threads($nt); + src = make_view($T, $sz, $min); + dst = make_view($T, $dsz, $mout) + ), + evals = evals, + samples = samples, + seconds = seconds, + ) + return group +end + +function addcopy!(group::BenchmarkGroup, L, ::Type{T}) where {T} + evals = max(1, (1 << 15) ÷ L) + group["L=$L"] = @benchmarkable( + copyto!(dst, src), + setup = (src = rand($T, $L); dst = Vector{$T}(undef, $L)), + evals = evals, + samples = 100, + seconds = 5, + ) return group end @@ -136,23 +201,34 @@ function filtered(group::BenchmarkGroup, pattern::AbstractString, prefix::String return out end -const SUITE = BenchmarkGroup() -let permute = addgroup!(SUITE, "permutedims!") +const SUITE = let + suite = BenchmarkGroup() + permute = addgroup!(suite, "permutedims!") + lengths = Set{Int}() for name in GROUPS haskey(CASEGROUPS, name) || error("--groups: group `$name` not found in $(CONFIG["cases"])") spec = CASEGROUPS[name] - cases = parsecases(spec, CONFIG["cases"]) + insts = expand(spec, name) samples, seconds = spec["samples"], spec["seconds"] g = addgroup!(permute, name) for T in ELTYPES tg = addgroup!(g, "T=$T") for nt in NTHREADS - addcases!(addgroup!(tg, "nthreads=$nt"), cases, T, nt; samples, seconds) + ng = addgroup!(tg, "nthreads=$nt") + for inst in insts + addcase!(ng, inst, T, nt; samples, seconds) + push!(lengths, inst.L) + end end end end - if !isempty(CONFIG["filter"]) - SUITE["permutedims!"] = filtered(permute, CONFIG["filter"]) + copies = addgroup!(suite, "copy") + for T in ELTYPES + tg = addgroup!(copies, "T=$T") + for L in sort!(collect(lengths)) + addcopy!(tg, L, T) + end end + isempty(CONFIG["filter"]) ? suite : filtered(suite, CONFIG["filter"]) end diff --git a/benchmark/cases.toml b/benchmark/cases.toml index 21d8f29..c290890 100644 --- a/benchmark/cases.toml +++ b/benchmark/cases.toml @@ -1,154 +1,77 @@ -# Benchmark cases for benchmarks.jl. Each group carries its BenchmarkTools -# sampling parameters and a list of cases, where `p` is the permutation and -# `size` the input size (so the output has size `size[p]`). +# Benchmark cases for benchmarks.jl. +# +# Each case is a permutation applied to an array whose *shape* (the ratio of its +# dimensions) is fixed while its *total length* is swept, so a case can be +# compared against a plain memory copy of the same length (the auto-generated +# "copy" group) to get a machine-independent efficiency. Dimensions are derived +# from `shape` and each `total` as d_i = round(s_i * (total / prod(s))^(1/N)), +# so the realized length is approximately (not exactly) `total`. +# +# [group] fields: +# samples, seconds BenchmarkTools budget (per case; `seconds` caps the big +# cases, evals stabilize the tiny ones) +# totals list of target element counts to sweep +# cases list of cases, each a table with: +# p 1-based permutation (output axis i comes from input p[i]) +# shape relative dimension ratios, length N == length(p) +# (alternatively `size` for explicit dims, ignoring totals) +# stride_in optional per-input-axis extent multiplier (default all 1) +# stride_out optional per-output-axis extent multiplier (default all 1) +# A multiplier > 1 makes the array a non-contiguous strided +# view: a leading >1 gives a non-unit innermost stride, a +# later >1 gives gaps between higher dimensions. -# The reference benchmark of HPTT / the TTC paper, reproduced verbatim at the -# original (large, bandwidth-bound) sizes, with the 0-based permutations -# translated to 1-based: -# https://github.com/springer13/hptt/blob/master/benchmark/benchmark.sh -# Each permutation comes in three size variants: balanced dims, large first -# dim, large last dim. -[hptt] -samples = 10 -seconds = 10 +# Balanced shapes (all dimensions equal) across ranks 2-6, plus a couple of +# non-reversing permutations. The bandwidth-bound baseline for the transpose. +[balanced] +samples = 100 +seconds = 5 +totals = [1024, 32768, 1048576, 8388608, 67108864] cases = [ - { p = [2, 1], size = [7264, 7264] }, - { p = [2, 1], size = [43408, 1216] }, - { p = [2, 1], size = [1216, 43408] }, - { p = [1, 3, 2], size = [368, 384, 384] }, - { p = [1, 3, 2], size = [2144, 64, 384] }, - { p = [1, 3, 2], size = [368, 64, 2307] }, - { p = [2, 1, 3], size = [384, 384, 355] }, - { p = [2, 1, 3], size = [2320, 384, 59] }, - { p = [2, 1, 3], size = [384, 2320, 59] }, - { p = [3, 2, 1], size = [384, 355, 384] }, - { p = [3, 2, 1], size = [2320, 59, 384] }, - { p = [3, 2, 1], size = [384, 59, 2320] }, - { p = [1, 4, 3, 2], size = [80, 96, 75, 96] }, - { p = [1, 4, 3, 2], size = [464, 16, 75, 96] }, - { p = [1, 4, 3, 2], size = [80, 16, 75, 582] }, - { p = [3, 2, 4, 1], size = [96, 75, 96, 75] }, - { p = [3, 2, 4, 1], size = [608, 12, 96, 75] }, - { p = [3, 2, 4, 1], size = [96, 12, 608, 75] }, - { p = [3, 1, 4, 2], size = [96, 75, 96, 75] }, - { p = [3, 1, 4, 2], size = [608, 12, 96, 75] }, - { p = [3, 1, 4, 2], size = [96, 12, 608, 75] }, - { p = [2, 1, 4, 3], size = [96, 96, 75, 75] }, - { p = [2, 1, 4, 3], size = [608, 96, 12, 75] }, - { p = [2, 1, 4, 3], size = [96, 608, 12, 75] }, - { p = [4, 3, 2, 1], size = [96, 75, 75, 96] }, - { p = [4, 3, 2, 1], size = [608, 12, 75, 96] }, - { p = [4, 3, 2, 1], size = [96, 12, 75, 608] }, - { p = [1, 5, 3, 2, 4], size = [32, 48, 28, 28, 48] }, - { p = [1, 5, 3, 2, 4], size = [176, 8, 28, 28, 48] }, - { p = [1, 5, 3, 2, 4], size = [32, 8, 28, 28, 298] }, - { p = [4, 3, 2, 5, 1], size = [48, 28, 28, 48, 28] }, - { p = [4, 3, 2, 5, 1], size = [352, 4, 28, 48, 28] }, - { p = [4, 3, 2, 5, 1], size = [48, 4, 28, 352, 28] }, - { p = [3, 1, 5, 2, 4], size = [48, 28, 48, 28, 28] }, - { p = [3, 1, 5, 2, 4], size = [352, 4, 48, 28, 28] }, - { p = [3, 1, 5, 2, 4], size = [48, 4, 352, 28, 28] }, - { p = [2, 4, 1, 5, 3], size = [48, 48, 28, 28, 28] }, - { p = [2, 4, 1, 5, 3], size = [352, 48, 4, 28, 28] }, - { p = [2, 4, 1, 5, 3], size = [48, 352, 4, 28, 28] }, - { p = [5, 4, 3, 2, 1], size = [48, 28, 28, 28, 48] }, - { p = [5, 4, 3, 2, 1], size = [352, 4, 28, 28, 48] }, - { p = [5, 4, 3, 2, 1], size = [48, 4, 28, 28, 352] }, - { p = [1, 4, 3, 6, 5, 2], size = [16, 32, 15, 32, 15, 15] }, - { p = [1, 4, 3, 6, 5, 2], size = [48, 10, 15, 32, 15, 15] }, - { p = [1, 4, 3, 6, 5, 2], size = [16, 10, 15, 103, 15, 15] }, - { p = [4, 3, 1, 6, 2, 5], size = [32, 15, 15, 32, 15, 15] }, - { p = [4, 3, 1, 6, 2, 5], size = [112, 5, 15, 32, 15, 15] }, - { p = [4, 3, 1, 6, 2, 5], size = [32, 5, 15, 112, 15, 15] }, - { p = [3, 1, 5, 2, 6, 4], size = [32, 15, 32, 15, 15, 15] }, - { p = [3, 1, 5, 2, 6, 4], size = [112, 5, 32, 15, 15, 15] }, - { p = [3, 1, 5, 2, 6, 4], size = [32, 5, 112, 15, 15, 15] }, - { p = [4, 3, 6, 2, 1, 5], size = [32, 15, 15, 32, 15, 15] }, - { p = [4, 3, 6, 2, 1, 5], size = [112, 5, 15, 32, 15, 15] }, - { p = [4, 3, 6, 2, 1, 5], size = [32, 5, 15, 112, 15, 15] }, - { p = [6, 5, 4, 3, 2, 1], size = [32, 15, 15, 15, 15, 32] }, - { p = [6, 5, 4, 3, 2, 1], size = [112, 5, 15, 15, 15, 32] }, - { p = [6, 5, 4, 3, 2, 1], size = [32, 5, 15, 15, 15, 112] }, + { p = [2, 1], shape = [1, 1] }, + { p = [3, 2, 1], shape = [1, 1, 1] }, + { p = [2, 3, 1], shape = [1, 1, 1] }, + { p = [4, 3, 2, 1], shape = [1, 1, 1, 1] }, + { p = [3, 4, 1, 2], shape = [1, 1, 1, 1] }, + { p = [5, 4, 3, 2, 1], shape = [1, 1, 1, 1, 1] }, + { p = [6, 5, 4, 3, 2, 1], shape = [1, 1, 1, 1, 1, 1] }, ] -# Small to medium sizes, including the permutation cases of strided-rs -# (https://github.com/tensor4all/strided-rs), to track per-call overhead, -# dispatch cost, and the non-threaded fast paths. -[small] -samples = 1000 +# Skewed shapes: one dimension much larger than the rest, either leading (fat +# first axis) or trailing (fat last axis), as in the HPTT/TTC reference set. +[skewed] +samples = 100 seconds = 5 +totals = [1024, 32768, 1048576, 8388608, 67108864] cases = [ - { p = [2, 1], size = [16, 16] }, - { p = [2, 1], size = [64, 64] }, - { p = [2, 1], size = [256, 256] }, - { p = [2, 1], size = [1000, 1000] }, - { p = [2, 1], size = [4000, 4000] }, - { p = [3, 2, 1], size = [16, 16, 16] }, - { p = [3, 2, 1], size = [64, 64, 64] }, - { p = [2, 3, 1], size = [16, 16, 16] }, - { p = [2, 3, 1], size = [64, 64, 64] }, - { p = [4, 3, 2, 1], size = [8, 8, 8, 8] }, - { p = [4, 3, 2, 1], size = [16, 16, 16, 16] }, - { p = [4, 3, 2, 1], size = [32, 32, 32, 32] }, - { p = [2, 1, 4, 3], size = [8, 8, 8, 8] }, - { p = [2, 1, 4, 3], size = [16, 16, 16, 16] }, - { p = [2, 1, 4, 3], size = [32, 32, 32, 32] }, - { p = [3, 4, 1, 2], size = [8, 8, 8, 8] }, - { p = [3, 4, 1, 2], size = [16, 16, 16, 16] }, - { p = [3, 4, 1, 2], size = [32, 32, 32, 32] }, - { p = [2, 3, 4, 1], size = [8, 8, 8, 8] }, - { p = [2, 3, 4, 1], size = [16, 16, 16, 16] }, - { p = [2, 3, 4, 1], size = [32, 32, 32, 32] }, + { p = [2, 1], shape = [32, 1] }, + { p = [2, 1], shape = [1, 32] }, + { p = [3, 2, 1], shape = [16, 1, 1] }, + { p = [3, 2, 1], shape = [1, 1, 16] }, + { p = [4, 3, 2, 1], shape = [8, 1, 1, 1] }, + { p = [4, 3, 2, 1], shape = [1, 1, 1, 8] }, + { p = [6, 5, 4, 3, 2, 1], shape = [8, 1, 1, 1, 1, 1] }, + { p = [6, 5, 4, 3, 2, 1], shape = [1, 1, 1, 1, 1, 8] }, ] -# Sizes that are not multiples of the SIMD vector width: powers of two +- 1, -# primes, and mixed odd/rectangular shapes. These defeat aligned vectorization -# and exercise the vector-remainder loops and blocking edge cases that the -# power-of-two sizes of the other groups never hit. -[unaligned] -samples = 1000 +# Truly strided views: the input or output is a non-contiguous slice of a larger +# buffer. `stride_in/out = [2, 1, ...]` gives a non-unit innermost stride (the +# strongly non-contiguous case that defeats the unit-stride fast path); +# `[1, 2, ...]` keeps the innermost dimension contiguous but adds a gap between +# successive second-axis slices. +[strided] +samples = 100 seconds = 5 +totals = [1024, 32768, 1048576, 8388608, 67108864] cases = [ - { p = [2, 1], size = [17, 17] }, - { p = [2, 1], size = [63, 63] }, - { p = [2, 1], size = [129, 129] }, - { p = [2, 1], size = [251, 251] }, - { p = [2, 1], size = [999, 1001] }, - { p = [2, 1], size = [1021, 1021] }, - { p = [3, 2, 1], size = [15, 15, 15] }, - { p = [3, 2, 1], size = [17, 17, 17] }, - { p = [3, 2, 1], size = [63, 63, 63] }, - { p = [3, 2, 1], size = [65, 65, 65] }, - { p = [3, 2, 1], size = [31, 32, 33] }, - { p = [3, 2, 1], size = [37, 61, 89] }, - { p = [2, 3, 1], size = [15, 15, 15] }, - { p = [2, 3, 1], size = [17, 17, 17] }, - { p = [2, 3, 1], size = [63, 63, 63] }, - { p = [2, 3, 1], size = [65, 65, 65] }, - { p = [2, 3, 1], size = [31, 32, 33] }, - { p = [2, 3, 1], size = [37, 61, 89] }, - { p = [4, 3, 2, 1], size = [7, 7, 7, 7] }, - { p = [4, 3, 2, 1], size = [9, 9, 9, 9] }, - { p = [4, 3, 2, 1], size = [15, 15, 15, 15] }, - { p = [4, 3, 2, 1], size = [17, 17, 17, 17] }, - { p = [4, 3, 2, 1], size = [31, 31, 31, 31] }, - { p = [4, 3, 2, 1], size = [33, 33, 33, 33] }, - { p = [2, 1, 4, 3], size = [7, 7, 7, 7] }, - { p = [2, 1, 4, 3], size = [9, 9, 9, 9] }, - { p = [2, 1, 4, 3], size = [15, 15, 15, 15] }, - { p = [2, 1, 4, 3], size = [17, 17, 17, 17] }, - { p = [2, 1, 4, 3], size = [31, 31, 31, 31] }, - { p = [2, 1, 4, 3], size = [33, 33, 33, 33] }, - { p = [3, 4, 1, 2], size = [7, 7, 7, 7] }, - { p = [3, 4, 1, 2], size = [9, 9, 9, 9] }, - { p = [3, 4, 1, 2], size = [15, 15, 15, 15] }, - { p = [3, 4, 1, 2], size = [17, 17, 17, 17] }, - { p = [3, 4, 1, 2], size = [31, 31, 31, 31] }, - { p = [3, 4, 1, 2], size = [33, 33, 33, 33] }, - { p = [2, 3, 4, 1], size = [7, 7, 7, 7] }, - { p = [2, 3, 4, 1], size = [9, 9, 9, 9] }, - { p = [2, 3, 4, 1], size = [15, 15, 15, 15] }, - { p = [2, 3, 4, 1], size = [17, 17, 17, 17] }, - { p = [2, 3, 4, 1], size = [31, 31, 31, 31] }, - { p = [2, 3, 4, 1], size = [33, 33, 33, 33] }, + { p = [2, 1], shape = [1, 1], stride_in = [2, 1] }, + { p = [2, 1], shape = [1, 1], stride_in = [1, 2] }, + { p = [2, 1], shape = [1, 1], stride_out = [2, 1] }, + { p = [2, 1], shape = [1, 1], stride_out = [1, 2] }, + { p = [3, 2, 1], shape = [1, 1, 1], stride_in = [2, 1, 1] }, + { p = [3, 2, 1], shape = [1, 1, 1], stride_in = [1, 2, 1] }, + { p = [3, 2, 1], shape = [1, 1, 1], stride_out = [2, 1, 1] }, + { p = [4, 3, 2, 1], shape = [1, 1, 1, 1], stride_in = [2, 1, 1, 1] }, + { p = [4, 3, 2, 1], shape = [1, 1, 1, 1], stride_in = [1, 2, 1, 1] }, + { p = [4, 3, 2, 1], shape = [1, 1, 1, 1], stride_out = [2, 1, 1, 1] }, ] From 36532e3465d04198aa34a1b5f40f4d53421de2db Mon Sep 17 00:00:00 2001 From: lkdvos Date: Fri, 10 Jul 2026 10:49:37 -0400 Subject: [PATCH 4/4] format --- benchmark/benchmarks.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index cb5b4e8..7398ad6 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -94,10 +94,10 @@ else end # Column-major strides of a dense array of size `sz`. -colstrides(sz::NTuple{N,Int}) where {N} = ntuple(i -> prod(ntuple(j -> sz[j], i - 1)), N) +colstrides(sz::NTuple{N, Int}) where {N} = ntuple(i -> prod(ntuple(j -> sz[j], i - 1)), N) # Dimensions realizing (approximately) `total` elements at the given shape ratio. -function shapedims(shape::NTuple{N,<:Real}, total::Int) where {N} +function shapedims(shape::NTuple{N, <:Real}, total::Int) where {N} c = (total / prod(shape))^(1 / N) return ntuple(i -> max(1, round(Int, shape[i] * c)), N) end @@ -106,7 +106,7 @@ end # freshly allocated (page-faulted) backing buffer. `mult` all ones gives a dense # view (the unit-stride fast path); a leading value > 1 gives a non-unit # innermost stride, a later value > 1 gives gaps between higher dimensions. -function make_view(::Type{T}, sz::NTuple{N,Int}, mult::NTuple{N,Int}) where {T,N} +function make_view(::Type{T}, sz::NTuple{N, Int}, mult::NTuple{N, Int}) where {T, N} all(isone, mult) && return StridedView(rand(T, sz)) bufsz = ntuple(i -> sz[i] * mult[i], N) strides = ntuple(i -> mult[i] * colstrides(bufsz)[i], N)