Skip to content

Unified threading - #44

Open
hakkelt wants to merge 75 commits into
kul-optec:masterfrom
hakkelt:unified-threading
Open

hakkelt wants to merge 75 commits into
kul-optec:masterfrom
hakkelt:unified-threading

Conversation

@hakkelt

@hakkelt hakkelt commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR unifies the interface for enabling/disabling threading across operators and adds multithreading support to all possible operators, replacing scattered per-operator implementations with a consistent policy infrastructure.

Important Changes

Threading Policy Infrastructure

  • New src/threading_policy.jl module introducing a unified threading framework:
    • is_threaded trait for operators with optional threading support
    • supports_threading, default_threaded, and threading_threshold per-operator policies
    • adapt_operator and copy_operator functions for consistent threading control
    • Benchmark-driven thresholds (from benchmark/threading_sweep.jl) replace previous guesses

Operator Threading Coverage

  • Elementwise nonlinear operators (9 operators: Pow, Exp, Sin, Cos, Atan, Tanh, Sech, Sigmoid, SoftPlus)

    • Each now supports per-operator threading with measured crossover thresholds
    • SoftMax explicitly excluded with reduction reason documented
  • FiniteDiff optimization: eliminated allocations from b[idx_1] .- b[idx_2] materialization using @views

    • 45x speedup at n=1e6 (2581µs → 57µs)
  • Calculus operators (Sum, HCAT, VCAT, DCAT, Compose, Scale, BroadCast, etc.) add forwarding traits

  • Batching operators (BatchOp, SimpleBatchOp, SpreadingBatchOp)

    • Fixed nesting safety: per-thread copies now properly respect threading decisions
    • Eliminated silent no-op behavior on threading adaptation

Bug Fixes

  • Variation adjoint: Fixed BoundsError on length-2 dimensions
  • Batching regression: Correct operator adaptation through _per_thread_operators (branch on is_thread_safe)
  • JET dispatch: FiniteDiff now has literal D=1 method to avoid runtime dispatch
  • OperatorBroadCast adjoint: Budgeted loop threading to prevent oversubscription

Test Expansion

  • Comprehensive JET coverage: 245 new tests with @test_opt and @test_call for threading APIs
  • Copy_operator coverage added for Sum, DiagOp, Variation, BroadCast, OperatorWrapper, HadamardProd, HCAT
  • GPU test environment activated only when needed
  • Threading-specific test suite with 276 passing tests

Infrastructure & Cleanup

  • GPU test environment lazy activation to reduce CI overhead
  • Removed AcceleratedDCTs from test dependencies
  • NestedThreading.jl replaces hand-rolled thread budgeting
  • Runic formatting applied throughout

Performance Baselines

  • Benchmark suite (benchmark/threading_sweep.jl) captures 243 measurements for future regression detection
  • Batching perf tests stabilized with minimum across repetitions at measured optimal workload sizes
  • Two-part threading policy for DCAT: budget first, then threshold-gate

@hakkelt
hakkelt force-pushed the unified-threading branch from 0287558 to a8a806a Compare August 19, 2026 15:01
@codecov

codecov Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.68646% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.07%. Comparing base (907d673) to head (72ee810).

Files with missing lines Patch % Lines
src/batching/SpreadingBatchOp.jl 82.35% 9 Missing ⚠️
src/threading_policy.jl 89.74% 8 Missing ⚠️
src/batching/SimpleBatchOp.jl 72.00% 7 Missing ⚠️
src/nonlinearoperators/Pow.jl 71.42% 6 Missing ⚠️
src/nonlinearoperators/SoftMax.jl 91.07% 5 Missing ⚠️
src/nonlinearoperators/SoftPlus.jl 68.75% 5 Missing ⚠️
src/properties.jl 80.00% 4 Missing ⚠️
src/calculus/BroadCast.jl 76.92% 3 Missing ⚠️
ext/GpuExt/linearoperators/getindex.jl 50.00% 2 Missing ⚠️
src/calculus/DCAT.jl 97.59% 2 Missing ⚠️
... and 17 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #44      +/-   ##
==========================================
- Coverage   90.13%   90.07%   -0.06%     
==========================================
  Files          51       52       +1     
  Lines        3678     4142     +464     
==========================================
+ Hits         3315     3731     +416     
- Misses        363      411      +48     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hakkelt
hakkelt marked this pull request as ready for review August 24, 2026 19:44
@hakkelt
hakkelt force-pushed the unified-threading branch 2 times, most recently from 525f485 to c9c9fda Compare August 27, 2026 05:11
@hakkelt

hakkelt commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@lostella, this PR is also ready for review.

Sorry, this is quite a large PR again, but the goal is straightforward: add multithreading support for all operators where possible via a unified API (and fix minor issues encountered along the way while implementing the feature).

The proposed changes don't break the current API, and the default value of the parameter governing multithreading aims to maximize out-of-the-box performance. Based on the operator type and input size, it enables multithreading only when preliminary benchmarks indicate it is beneficial. The goal of adding the parameter named threaded to all operator constructors is that threading can be disabled in mul! functions for this operator. This is useful if mul! is intended to be called from an already multithreaded context, but this is optional because Julia threads compose quite well on their own with low overhead.

This is the suggested unified API:

op1 = Variation(Float64, (64,64))
x, y = rand(64, 64), (4096, 2)
mul!(y, op1, x) # this runs multithreaded

op2 = Variation(Float64, (64,64); threaded=false)
x, y = rand(64, 64), (4096, 2)
mul!(y, op2, x) # this runs single-threaded

op3 = Variation(Float64, (10,10))
x, y = rand(10, 10), (100, 2)
mul!(y, op3, x) # this runs single-threaded because the input is too small to benefit from multithreading (2^10 elements is the threshold for Variation)

A side-effect of this work is that the copy_operator function doesn't blindly deep-copy the operators, but allows changing storage_type and/or threaded properties of the produced copies. It was required because calculus and batching operators need to create a non-threaded copy of the wrapped operators to achieve maximal performance when their operation is performed in a threaded manner.

@lostella
lostella self-requested a review August 29, 2026 05:17
Comment thread FFTWOperators/src/DFT.jl Resolved
Comment thread src/calculus/DCAT.jl Resolved
Comment thread src/calculus/DCAT.jl Resolved
Comment thread src/calculus/HCAT.jl Resolved
hakkelt referenced this pull request in hakkelt/AbstractOperators.jl Sep 19, 2026
- DFT.jl: use _with_fftw_threads (try/finally) for the c2c plan
  construction instead of raw set_num_threads/get_num_threads,
  matching DCT.jl/RDFT.jl/IRDFT.jl.
- DCAT.jl: forward threaded = is_block_threaded(H) in permute and
  remove_displacement reconstruction, matching VCAT.jl precedent.
- HCAT.jl: forward threaded = is_block_threaded(L) in remove_slicing
  (permute/remove_displacement already did this).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Benchmark Results (Julia v1.13.0)

Run once per thread count (-t 1, -t 2, ...) so single- and multi-threaded paths are
each measured under a controlled, pinned thread count rather than mixed into one
ambiguous run -- see each operator's own size-based threading policy for why a "default"
run at more than one thread cannot otherwise be assumed serial or parallel.

1 thread

🚀 8 benchmarks improved in time · 🐢 1 time regression detected · 🚀 7 benchmarks use less memory

Time benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 7.84 ± 0.15 μs —
batching/SimpleBatchOp/adjoint-single 32.1 ± 4.3 μs — —
batching/SimpleBatchOp/forward — 6.62 ± 0.2 μs —
batching/SimpleBatchOp/forward-single 27.8 ± 4.5 μs — —
batching/SpreadingBatchOp/adjoint-single 17.2 ± 1.8 μs 5.06 ± 0.15 μs 3.4 ± 0.72 🚀
batching/SpreadingBatchOp/forward-single 15.1 ± 1.7 μs 4.28 ± 0.2 μs 3.53 ± 0.88 🚀
calculus/AffineAdd/adjoint 6.86 ± 0.23 μs 6.89 ± 0.19 μs 0.996 ± 0.086
calculus/AffineAdd/forward 13.6 ± 0.63 μs 13.6 ± 0.51 μs 1 ± 0.12
calculus/Ax_mul_Bx/forward 19.2 ± 0.76 μs 18.8 ± 1.3 μs 1.02 ± 0.17
calculus/Ax_mul_Bx/jacobian-adjoint 48.5 ± 2.8 μs 47.8 ± 2.7 μs 1.01 ± 0.17
calculus/Ax_mul_Bxt/forward 19 ± 1 μs 18.9 ± 1.2 μs 1 ± 0.17
calculus/Ax_mul_Bxt/jacobian-adjoint 43.6 ± 1.9 μs 43.5 ± 2.2 μs 1 ± 0.13
calculus/Axt_mul_Bx/forward 23.9 ± 0.75 μs 22.9 ± 1.2 μs 1.05 ± 0.12
calculus/Axt_mul_Bx/jacobian-adjoint 49 ± 3.9 μs 48.9 ± 3.9 μs 1 ± 0.23
calculus/BroadCast/identity — 90.3 ± 3.1 μs —
calculus/BroadCast/identity-single 88.9 ± 3.1 μs — —
calculus/BroadCast/operator-adjoint — 14.7 ± 0.25 μs —
calculus/BroadCast/operator-forward — 396 ± 51 ns —
calculus/BroadCast/operator-single-adjoint 14.6 ± 0.45 μs — —
calculus/BroadCast/operator-single-forward 395 ± 55 ns — —
calculus/Compose/adjoint 8.76 ± 0.48 μs 8.85 ± 0.62 μs 0.99 ± 0.18
calculus/Compose/forward 8.88 ± 0.58 μs 9.1 ± 0.97 μs 0.976 ± 0.24
calculus/DCAT/adjoint 19.2 ± 1 μs 21.1 ± 2.5 μs 0.91 ± 0.24
calculus/DCAT/forward 21.9 ± 1.8 μs 22.6 ± 2.8 μs 0.971 ± 0.29
calculus/HCAT/adjoint 19.9 ± 1.8 μs 18.4 ± 1.2 μs 1.08 ± 0.24
calculus/HCAT/forward 32.8 ± 4.4 μs 32.3 ± 4.1 μs 1.02 ± 0.38
calculus/HadamardProd/forward 764 ± 17 μs 751 ± 18 μs 1.02 ± 0.065
calculus/HadamardProd/jacobian-adjoint 832 ± 25 μs 800 ± 19 μs 1.04 ± 0.081
calculus/Jacobian/sigmoid-adjoint 158 ± 3.1 μs 164 ± 5 μs 0.96 ± 0.069
calculus/Reshape/adjoint 6.89 ± 0.21 μs 6.98 ± 0.21 μs 0.987 ± 0.083
calculus/Reshape/forward 6.81 ± 0.19 μs 6.76 ± 0.19 μs 1.01 ± 0.081
calculus/Scale/adjoint 13 ± 0.42 μs 13 ± 0.48 μs 0.998 ± 0.098
calculus/Scale/forward 13.1 ± 0.28 μs 13 ± 0.46 μs 1.01 ± 0.084
calculus/Sum/adjoint 28.9 ± 3.8 μs 28.7 ± 4.2 μs 1.01 ± 0.4
calculus/Sum/forward 28.5 ± 4.8 μs 28.6 ± 4 μs 0.996 ± 0.44
calculus/VCAT/adjoint 31.4 ± 2.5 μs 31.6 ± 5.8 μs 0.995 ± 0.4
calculus/VCAT/forward 17.8 ± 0.99 μs 17.4 ± 1.2 μs 1.02 ± 0.18
dspoperators/Filt/adjoint 318 ± 10 μs 285 ± 5.1 μs 1.12 ± 0.082
dspoperators/Filt/forward 304 ± 9 μs 285 ± 5.5 μs 1.07 ± 0.075
dspoperators/MIMOFilt/adjoint 246 ± 4.3 μs 274 ± 11 μs 0.9 ± 0.08
dspoperators/MIMOFilt/forward 247 ± 4.1 μs 247 ± 0.98 μs 0.999 ± 0.034
dspoperators/Xcorr/adjoint 57.7 ± 1.5 μs 57.1 ± 1.5 μs 1.01 ± 0.076
dspoperators/Xcorr/forward 477 ± 46 μs 491 ± 54 μs 0.971 ± 0.28
fftwoperators/DFT/adjoint 115 ± 4.2 μs 115 ± 3.5 μs 0.998 ± 0.094
fftwoperators/DFT/forward 103 ± 3.7 μs 103 ± 4.5 μs 1 ± 0.11
linearoperators/DiagOp/adjoint — 499 ± 46 μs —
linearoperators/DiagOp/adjoint-single 479 ± 37 μs — —
linearoperators/DiagOp/forward — 491 ± 62 μs —
linearoperators/DiagOp/forward-single 488 ± 63 μs — —
linearoperators/Eye/forward 763 ± 62 μs 812 ± 66 μs 0.939 ± 0.22
linearoperators/FiniteDiff/adjoint 595 ± 75 μs 173 ± 16 μs 3.44 ± 1.1 🚀
linearoperators/FiniteDiff/forward 644 ± 1.5e+02 μs 160 ± 19 μs 4.02 ± 2.1 🚀
linearoperators/GetIndex/adjoint 1.55 ± 0.17 ms 1.49 ± 0.21 ms 1.04 ± 0.37
linearoperators/GetIndex/forward 999 ± 1.2e+02 μs 884 ± 1.2e+02 μs 1.13 ± 0.41
linearoperators/LBFGS/mul 59.4 ± 1.5 μs 59.1 ± 1.8 μs 1.01 ± 0.08
linearoperators/LBFGS/update 9.45 ± 0.31 μs 9.45 ± 0.29 μs 1 ± 0.091
linearoperators/LMatrixOp/adjoint 800 ± 1.3e+02 μs 722 ± 1.8e+02 μs 1.11 ± 0.66
linearoperators/LMatrixOp/forward 176 ± 7.4 μs 174 ± 8.2 μs 1.01 ± 0.13
linearoperators/MatrixOp/adjoint 245 ± 13 μs 147 ± 16 μs 1.67 ± 0.4 🚀
linearoperators/MatrixOp/forward 245 ± 10 μs 141 ± 7.7 μs 1.74 ± 0.24 🚀
linearoperators/MyLinOp/adjoint 482 ± 43 μs 604 ± 53 μs 0.799 ± 0.2
linearoperators/MyLinOp/forward 510 ± 86 μs 493 ± 61 μs 1.04 ± 0.43
linearoperators/Variation/adjoint — 533 ± 6.8 μs —
linearoperators/Variation/adjoint-single 567 ± 15 μs — —
linearoperators/Variation/forward — 157 ± 26 μs —
linearoperators/Variation/forward-single 154 ± 26 μs — —
linearoperators/ZeroPad/adjoint 112 ± 13 μs 177 ± 3.8 μs 0.636 ± 0.15 🐢
linearoperators/ZeroPad/forward 119 ± 13 μs 123 ± 17 μs 0.968 ± 0.34
linearoperators/Zeros/forward 857 ± 1.6e+02 μs 865 ± 1.1e+02 μs 0.99 ± 0.45
nfftoperators/NFFTOp/adjoint 253 ± 9.5 μs 256 ± 11 μs 0.987 ± 0.12
nfftoperators/NFFTOp/forward 255 ± 9.3 μs 254 ± 13 μs 1 ± 0.13
nonlinearoperators/Atan/forward 387 ± 8.4 μs 388 ± 4.7 μs 0.997 ± 0.05
nonlinearoperators/Atan/jacobian-adjoint 21.9 ± 0.65 μs 22 ± 0.037 μs 0.997 ± 0.059
nonlinearoperators/Cos/forward 356 ± 6.9 μs 358 ± 5.7 μs 0.995 ± 0.05
nonlinearoperators/Cos/jacobian-adjoint 403 ± 13 μs 381 ± 9.6 μs 1.06 ± 0.088
nonlinearoperators/Exp/forward 472 ± 11 μs 475 ± 8.7 μs 0.993 ± 0.061
nonlinearoperators/Exp/jacobian-adjoint 521 ± 11 μs 531 ± 13 μs 0.981 ± 0.064
nonlinearoperators/Pow/forward 444 ± 34 μs 474 ± 61 μs 0.936 ± 0.28
nonlinearoperators/Pow/jacobian-adjoint 368 ± 10 μs 364 ± 9.3 μs 1.01 ± 0.076
nonlinearoperators/Sech/forward 226 ± 3.5 μs 224 ± 4.6 μs 1.01 ± 0.052
nonlinearoperators/Sech/jacobian-adjoint 561 ± 15 μs 522 ± 12 μs 1.07 ± 0.077
nonlinearoperators/Sigmoid/forward 256 ± 4.6 μs 249 ± 4.6 μs 1.03 ± 0.053
nonlinearoperators/Sigmoid/jacobian-adjoint 317 ± 5 μs 334 ± 6.8 μs 0.952 ± 0.049
nonlinearoperators/Sin/forward 370 ± 8.7 μs 369 ± 8.1 μs 1 ± 0.064
nonlinearoperators/Sin/jacobian-adjoint 368 ± 5.6 μs 364 ± 10 μs 1.01 ± 0.065
nonlinearoperators/SoftMax/forward 318 ± 4.6 μs 160 ± 4.7 μs 1.99 ± 0.13 🚀
nonlinearoperators/SoftMax/jacobian-adjoint 375 ± 7.4 μs 184 ± 3 μs 2.04 ± 0.1 🚀
nonlinearoperators/SoftPlus/forward 869 ± 16 μs 921 ± 15 μs 0.943 ± 0.046
nonlinearoperators/SoftPlus/jacobian-adjoint 306 ± 14 μs 278 ± 7.6 μs 1.1 ± 0.12
nonlinearoperators/Tanh/forward 297 ± 6.2 μs 288 ± 6.9 μs 1.03 ± 0.065
nonlinearoperators/Tanh/jacobian-adjoint 237 ± 3.4 μs 239 ± 7.3 μs 0.989 ± 0.067
normaloperators/DFT/mul 6.24 ± 0.16 μs 6.26 ± 0.13 μs 0.996 ± 0.065
normaloperators/DiagOp/mul 511 ± 55 μs 502 ± 41 μs 1.02 ± 0.27
normaloperators/NFFTOp/mul 151 ± 5.3 μs 153 ± 4.9 μs 0.99 ± 0.094
waveletoperators/WaveletOp/adjoint 2.83 ± 0.062 ms 2.83 ± 0.067 ms 1 ± 0.065
waveletoperators/WaveletOp/forward 1.81 ± 0.043 ms 1.78 ± 0.094 ms 1.02 ± 0.12
Memory benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 0 allocs (0 bytes) —
batching/SimpleBatchOp/adjoint-single 704 allocs (275.00 KiB) — —
batching/SimpleBatchOp/forward — 0 allocs (0 bytes) —
batching/SimpleBatchOp/forward-single 384 allocs (265.00 KiB) — —
batching/SpreadingBatchOp/adjoint-single 386 allocs (139.09 KiB) 34 allocs (1.59 KiB) 87.3 🚀
batching/SpreadingBatchOp/forward-single 224 allocs (134.00 KiB) 32 allocs (1.50 KiB) 89.3 🚀
calculus/AffineAdd/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/AffineAdd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/BroadCast/identity — 1 allocs (48 bytes) —
calculus/BroadCast/identity-single 1 allocs (48 bytes) — —
calculus/BroadCast/operator-adjoint — 119 allocs (4.48 KiB) —
calculus/BroadCast/operator-forward — 0 allocs (0 bytes) —
calculus/BroadCast/operator-single-adjoint 119 allocs (4.48 KiB) — —
calculus/BroadCast/operator-single-forward 0 allocs (0 bytes) — —
calculus/Compose/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/DCAT/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/HCAT/adjoint 131 allocs (4.70 KiB) 71 allocs (2.55 KiB) 1.85 🚀
calculus/HCAT/forward 70 allocs (2.52 KiB) 40 allocs (1.44 KiB) 1.75 🚀
calculus/HadamardProd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/HadamardProd/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
calculus/Jacobian/sigmoid-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Reshape/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Reshape/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Scale/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/VCAT/adjoint 130 allocs (4.67 KiB) 70 allocs (2.52 KiB) 1.86 🚀
calculus/VCAT/forward 71 allocs (2.55 KiB) 41 allocs (1.47 KiB) 1.73 🚀
dspoperators/Filt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Filt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
fftwoperators/DFT/adjoint 3 allocs (256.09 KiB) 3 allocs (256.09 KiB) 1
fftwoperators/DFT/forward 3 allocs (256.09 KiB) 3 allocs (256.09 KiB) 1
linearoperators/DiagOp/adjoint — 0 allocs (0 bytes) —
linearoperators/DiagOp/adjoint-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward — 0 allocs (0 bytes) —
linearoperators/DiagOp/forward-single 0 allocs (0 bytes) — —
linearoperators/Eye/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/FiniteDiff/adjoint 11 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/FiniteDiff/forward 6 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/GetIndex/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/GetIndex/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LBFGS/mul 65 allocs (1.33 KiB) 65 allocs (1.33 KiB) 1
linearoperators/LBFGS/update 3 allocs (48 bytes) 3 allocs (48 bytes) 1
linearoperators/LMatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LMatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/adjoint — 0 allocs (0 bytes) —
linearoperators/Variation/adjoint-single 0 allocs (0 bytes) — —
linearoperators/Variation/forward — 768 allocs (24.00 KiB) —
linearoperators/Variation/forward-single 512 allocs (16.00 KiB) — —
linearoperators/ZeroPad/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/ZeroPad/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Zeros/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nfftoperators/NFFTOp/adjoint 152 allocs (5.28 KiB) 147 allocs (5.14 KiB) 1.03
nfftoperators/NFFTOp/forward 149 allocs (5.17 KiB) 144 allocs (5.03 KiB) 1.03
nonlinearoperators/Atan/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Atan/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Exp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Exp/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Sigmoid/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sigmoid/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/jacobian-adjoint 3 allocs (512.07 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Tanh/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Tanh/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DFT/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DiagOp/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/NFFTOp/mul 7 allocs (208 bytes) 2 allocs (64 bytes) 3.25 🚀
waveletoperators/WaveletOp/adjoint 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1
waveletoperators/WaveletOp/forward 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1

2 threads

🚀 25 benchmarks improved in time · 🚀 6 benchmarks use less memory · 🐢 3 memory regressions detected

Time benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 7.83 ± 0.22 μs —
batching/SimpleBatchOp/adjoint-single 31.1 ± 2.5 μs — —
batching/SimpleBatchOp/forward — 6.72 ± 0.16 μs —
batching/SimpleBatchOp/forward-single 27.4 ± 4 μs — —
batching/SpreadingBatchOp/adjoint-copying — 4.58 ± 0.1 μs —
batching/SpreadingBatchOp/adjoint-fixed_operator — 4.55 ± 0.11 μs —
batching/SpreadingBatchOp/adjoint-locking — 4.61 ± 0.056 μs —
batching/SpreadingBatchOp/adjoint-single 17.4 ± 4.9 μs 5.01 ± 0.16 μs 3.46 ± 2 🚀
batching/SpreadingBatchOp/forward-copying — 3.92 ± 0.12 μs —
batching/SpreadingBatchOp/forward-fixed_operator — 3.93 ± 0.12 μs —
batching/SpreadingBatchOp/forward-locking — 3.89 ± 0.11 μs —
batching/SpreadingBatchOp/forward-single 15.2 ± 1.4 μs 4.24 ± 0.21 μs 3.59 ± 0.73 🚀
calculus/AffineAdd/adjoint 7 ± 0.21 μs 6.78 ± 0.11 μs 1.03 ± 0.071
calculus/AffineAdd/forward 13.6 ± 0.47 μs 13.3 ± 0.36 μs 1.02 ± 0.09
calculus/Ax_mul_Bx/forward 19 ± 0.93 μs 19.9 ± 0.87 μs 0.956 ± 0.13
calculus/Ax_mul_Bx/jacobian-adjoint 47.4 ± 2.9 μs 47.8 ± 2 μs 0.992 ± 0.15
calculus/Ax_mul_Bxt/forward 19.2 ± 0.85 μs 19.9 ± 0.79 μs 0.962 ± 0.11
calculus/Ax_mul_Bxt/jacobian-adjoint 43.3 ± 1.8 μs 42.7 ± 1.8 μs 1.01 ± 0.12
calculus/Axt_mul_Bx/forward 23.3 ± 0.7 μs 23.4 ± 0.72 μs 0.998 ± 0.086
calculus/Axt_mul_Bx/jacobian-adjoint 48.3 ± 3.7 μs 47.8 ± 3.2 μs 1.01 ± 0.21
calculus/BroadCast/identity — 103 ± 7 μs —
calculus/BroadCast/identity-single 89 ± 3.1 μs — —
calculus/BroadCast/operator-adjoint — 14.8 ± 0.17 μs —
calculus/BroadCast/operator-forward — 378 ± 60 ns —
calculus/BroadCast/operator-single-adjoint 14.7 ± 0.19 μs — —
calculus/BroadCast/operator-single-forward 412 ± 56 ns — —
calculus/Compose/adjoint 8.83 ± 0.46 μs 8.63 ± 0.4 μs 1.02 ± 0.14
calculus/Compose/forward 8.67 ± 0.46 μs 8.52 ± 0.36 μs 1.02 ± 0.14
calculus/DCAT/adjoint 20 ± 1.8 μs 19.3 ± 1.1 μs 1.03 ± 0.22
calculus/DCAT/adjoint-threaded — 132 ± 3.4 μs —
calculus/DCAT/forward 22.4 ± 2.2 μs 21.5 ± 1.6 μs 1.04 ± 0.26
calculus/DCAT/forward-threaded — 131 ± 4.1 μs —
calculus/HCAT/adjoint 19.3 ± 1.2 μs 18.3 ± 0.95 μs 1.05 ± 0.17
calculus/HCAT/adjoint-threaded — 246 ± 3.7 μs —
calculus/HCAT/forward 32.3 ± 3.4 μs 31.9 ± 5.2 μs 1.01 ± 0.39
calculus/HCAT/forward-threaded — 624 ± 7.4 μs —
calculus/HadamardProd/forward 758 ± 19 μs 354 ± 20 μs 2.14 ± 0.26 🚀
calculus/HadamardProd/jacobian-adjoint 818 ± 19 μs 396 ± 14 μs 2.06 ± 0.17 🚀
calculus/Jacobian/sigmoid-adjoint 157 ± 3.5 μs 74.5 ± 0.31 μs 2.11 ± 0.096 🚀
calculus/Reshape/adjoint 6.9 ± 0.17 μs 6.72 ± 0.15 μs 1.03 ± 0.069
calculus/Reshape/forward 6.79 ± 0.17 μs 6.47 ± 0.2 μs 1.05 ± 0.082
calculus/Scale/adjoint 13.1 ± 0.32 μs 13 ± 0.17 μs 1.01 ± 0.056
calculus/Scale/adjoint-threaded — 3.64 ± 0.0076 ms —
calculus/Scale/forward 14.3 ± 0.29 μs 12.9 ± 0.24 μs 1.11 ± 0.061
calculus/Scale/forward-threaded — 3.1 ± 0.014 ms —
calculus/Sum/adjoint 28 ± 2.1 μs 28.6 ± 9.1 μs 0.978 ± 0.64
calculus/Sum/forward 27.9 ± 2.2 μs 28.2 ± 9.2 μs 0.988 ± 0.66
calculus/VCAT/adjoint 31.5 ± 3.2 μs 30.9 ± 4.4 μs 1.02 ± 0.36
calculus/VCAT/adjoint-threaded — 225 ± 4.2 μs —
calculus/VCAT/forward 17.9 ± 0.97 μs 18.3 ± 1.4 μs 0.977 ± 0.18
calculus/VCAT/forward-threaded — 101 ± 1.9 μs —
dspoperators/Filt/adjoint 318 ± 10 μs 309 ± 20 μs 1.03 ± 0.15
dspoperators/Filt/forward 322 ± 10 μs 285 ± 7.3 μs 1.13 ± 0.091
dspoperators/MIMOFilt/adjoint 247 ± 0.54 μs 247 ± 0.61 μs 1 ± 0.0066
dspoperators/MIMOFilt/forward 277 ± 13 μs 247 ± 4.1 μs 1.12 ± 0.11
dspoperators/Xcorr/adjoint 57.3 ± 1.8 μs 57.5 ± 1.5 μs 0.995 ± 0.082
dspoperators/Xcorr/forward 471 ± 64 μs 507 ± 62 μs 0.929 ± 0.34
fftwoperators/DFT/adjoint 154 ± 11 μs 157 ± 12 μs 0.983 ± 0.2
fftwoperators/DFT/forward 126 ± 12 μs 134 ± 17 μs 0.94 ± 0.29
linearoperators/DiagOp/adjoint — 272 ± 24 μs —
linearoperators/DiagOp/adjoint-single 488 ± 59 μs — —
linearoperators/DiagOp/adjoint-threaded 266 ± 34 μs — —
linearoperators/DiagOp/forward — 280 ± 42 μs —
linearoperators/DiagOp/forward-single 474 ± 49 μs — —
linearoperators/DiagOp/forward-threaded 260 ± 36 μs — —
linearoperators/Eye/forward 772 ± 65 μs 767 ± 36 μs 1.01 ± 0.19
linearoperators/FiniteDiff/adjoint 554 ± 63 μs 119 ± 18 μs 4.65 ± 1.8 🚀
linearoperators/FiniteDiff/forward 578 ± 1.4e+02 μs 142 ± 15 μs 4.06 ± 2.1 🚀
linearoperators/GetIndex/adjoint 1.19 ± 0.12 ms 1.34 ± 0.18 ms 0.886 ± 0.3
linearoperators/GetIndex/forward 846 ± 1.3e+02 μs 779 ± 1.2e+02 μs 1.09 ± 0.47
linearoperators/LBFGS/mul 58.8 ± 1.6 μs 58.8 ± 1.3 μs 0.999 ± 0.072
linearoperators/LBFGS/update 9.29 ± 0.36 μs 9.55 ± 0.29 μs 0.973 ± 0.097
linearoperators/LMatrixOp/adjoint 484 ± 1.9e+02 μs 529 ± 1.9e+02 μs 0.916 ± 0.97
linearoperators/LMatrixOp/forward 176 ± 6.4 μs 171 ± 4.6 μs 1.03 ± 0.093
linearoperators/MatrixOp/adjoint 251 ± 12 μs 241 ± 15 μs 1.04 ± 0.17
linearoperators/MatrixOp/forward 248 ± 9.9 μs 246 ± 16 μs 1.01 ± 0.15
linearoperators/MyLinOp/adjoint 463 ± 34 μs 460 ± 51 μs 1.01 ± 0.27
linearoperators/MyLinOp/forward 548 ± 98 μs 519 ± 60 μs 1.06 ± 0.45
linearoperators/Variation/adjoint — 398 ± 10 μs —
linearoperators/Variation/adjoint-single 533 ± 9.7 μs — —
linearoperators/Variation/forward — 108 ± 19 μs —
linearoperators/Variation/forward-single 157 ± 20 μs — —
linearoperators/ZeroPad/adjoint 110 ± 9.5 μs 121 ± 20 μs 0.916 ± 0.35
linearoperators/ZeroPad/forward 116 ± 9.7 μs 119 ± 14 μs 0.975 ± 0.28
linearoperators/Zeros/forward 726 ± 1.4e+02 μs 786 ± 1.7e+02 μs 0.923 ± 0.54
nfftoperators/NFFTOp/adjoint 255 ± 11 μs 252 ± 8.2 μs 1.01 ± 0.11
nfftoperators/NFFTOp/forward 254 ± 9.8 μs 260 ± 11 μs 0.977 ± 0.11
nonlinearoperators/Atan/forward 390 ± 5.8 μs 161 ± 6 μs 2.42 ± 0.19 🚀
nonlinearoperators/Atan/jacobian-adjoint 22 ± 0.046 μs 19.5 ± 0.5 μs 1.13 ± 0.058
nonlinearoperators/Cos/forward 357 ± 8.1 μs 153 ± 4.4 μs 2.33 ± 0.17 🚀
nonlinearoperators/Cos/jacobian-adjoint 396 ± 12 μs 174 ± 4.8 μs 2.28 ± 0.19 🚀
nonlinearoperators/Exp/forward 472 ± 11 μs 230 ± 11 μs 2.06 ± 0.22 🚀
nonlinearoperators/Exp/jacobian-adjoint 522 ± 14 μs 250 ± 10 μs 2.09 ± 0.2 🚀
nonlinearoperators/Pow/forward 453 ± 34 μs 308 ± 22 μs 1.47 ± 0.3
nonlinearoperators/Pow/jacobian-adjoint 362 ± 9.1 μs 186 ± 7.2 μs 1.95 ± 0.18 🚀
nonlinearoperators/Sech/forward 226 ± 3 μs 104 ± 1.6 μs 2.18 ± 0.088 🚀
nonlinearoperators/Sech/jacobian-adjoint 566 ± 15 μs 244 ± 5.6 μs 2.32 ± 0.16 🚀
nonlinearoperators/Sigmoid/forward 257 ± 4 μs 126 ± 2.1 μs 2.04 ± 0.092 🚀
nonlinearoperators/Sigmoid/jacobian-adjoint 316 ± 6.7 μs 148 ± 3.6 μs 2.13 ± 0.14 🚀
nonlinearoperators/Sin/forward 370 ± 9.2 μs 169 ± 3.5 μs 2.19 ± 0.14 🚀
nonlinearoperators/Sin/jacobian-adjoint 369 ± 7.5 μs 156 ± 5 μs 2.36 ± 0.18 🚀
nonlinearoperators/SoftMax/forward 317 ± 6.6 μs 185 ± 3.8 μs 1.71 ± 0.1 🚀
nonlinearoperators/SoftMax/jacobian-adjoint 372 ± 7.6 μs 212 ± 1.8 μs 1.76 ± 0.077 🚀
nonlinearoperators/SoftPlus/forward 864 ± 17 μs 422 ± 10 μs 2.05 ± 0.13 🚀
nonlinearoperators/SoftPlus/jacobian-adjoint 300 ± 13 μs 134 ± 3.1 μs 2.24 ± 0.22 🚀
nonlinearoperators/Tanh/forward 295 ± 6.4 μs 130 ± 1.6 μs 2.26 ± 0.11 🚀
nonlinearoperators/Tanh/jacobian-adjoint 242 ± 4.1 μs 107 ± 3.4 μs 2.25 ± 0.16 🚀
normaloperators/DFT/mul 7.33 ± 0.14 μs 6.27 ± 0.16 μs 1.17 ± 0.073
normaloperators/DiagOp/mul 487 ± 49 μs 462 ± 37 μs 1.05 ± 0.27
normaloperators/NFFTOp/mul 153 ± 5.3 μs 154 ± 7.7 μs 0.998 ± 0.12
waveletoperators/WaveletOp/adjoint 2.83 ± 0.068 ms 2.85 ± 0.11 ms 0.992 ± 0.092
waveletoperators/WaveletOp/forward 1.8 ± 0.046 ms 1.61 ± 0.015 ms 1.12 ± 0.061
Memory benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 0 allocs (0 bytes) —
batching/SimpleBatchOp/adjoint-single 704 allocs (275.00 KiB) — —
batching/SimpleBatchOp/forward — 0 allocs (0 bytes) —
batching/SimpleBatchOp/forward-single 384 allocs (265.00 KiB) — —
batching/SpreadingBatchOp/adjoint-copying — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-fixed_operator — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-locking — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-single 386 allocs (139.09 KiB) 34 allocs (1.59 KiB) 87.3 🚀
batching/SpreadingBatchOp/forward-copying — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-fixed_operator — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-locking — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-single 224 allocs (134.00 KiB) 32 allocs (1.50 KiB) 89.3 🚀
calculus/AffineAdd/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/AffineAdd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/BroadCast/identity — 1 allocs (48 bytes) —
calculus/BroadCast/identity-single 1 allocs (48 bytes) — —
calculus/BroadCast/operator-adjoint — 119 allocs (4.48 KiB) —
calculus/BroadCast/operator-forward — 0 allocs (0 bytes) —
calculus/BroadCast/operator-single-adjoint 119 allocs (4.48 KiB) — —
calculus/BroadCast/operator-single-forward 0 allocs (0 bytes) — —
calculus/Compose/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/DCAT/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/adjoint-threaded — 28 allocs (2.42 KiB) —
calculus/DCAT/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/forward-threaded — 28 allocs (2.42 KiB) —
calculus/HCAT/adjoint 131 allocs (4.70 KiB) 71 allocs (2.55 KiB) 1.85 🚀
calculus/HCAT/adjoint-threaded — 125 allocs (6.58 KiB) —
calculus/HCAT/forward 70 allocs (2.52 KiB) 40 allocs (1.44 KiB) 1.75 🚀
calculus/HCAT/forward-threaded — 56 allocs (2.12 KiB) —
calculus/HadamardProd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/HadamardProd/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
calculus/Jacobian/sigmoid-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Reshape/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Reshape/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Scale/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/adjoint-threaded — 0 allocs (0 bytes) —
calculus/Scale/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/forward-threaded — 0 allocs (0 bytes) —
calculus/Sum/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/VCAT/adjoint 130 allocs (4.67 KiB) 70 allocs (2.52 KiB) 1.86 🚀
calculus/VCAT/adjoint-threaded — 98 allocs (3.72 KiB) —
calculus/VCAT/forward 71 allocs (2.55 KiB) 41 allocs (1.47 KiB) 1.73 🚀
calculus/VCAT/forward-threaded — 80 allocs (4.53 KiB) —
dspoperators/Filt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Filt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
fftwoperators/DFT/adjoint 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
fftwoperators/DFT/forward 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
linearoperators/DiagOp/adjoint — 0 allocs (0 bytes) —
linearoperators/DiagOp/adjoint-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/adjoint-threaded 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward — 0 allocs (0 bytes) —
linearoperators/DiagOp/forward-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward-threaded 0 allocs (0 bytes) — —
linearoperators/Eye/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/FiniteDiff/adjoint 11 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/FiniteDiff/forward 6 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/GetIndex/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/GetIndex/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LBFGS/mul 65 allocs (1.33 KiB) 65 allocs (1.33 KiB) 1
linearoperators/LBFGS/update 3 allocs (48 bytes) 3 allocs (48 bytes) 1
linearoperators/LMatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LMatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/adjoint — 0 allocs (0 bytes) —
linearoperators/Variation/adjoint-single 0 allocs (0 bytes) — —
linearoperators/Variation/forward — 4 allocs (128 bytes) —
linearoperators/Variation/forward-single 512 allocs (16.00 KiB) — —
linearoperators/ZeroPad/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/ZeroPad/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Zeros/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nfftoperators/NFFTOp/adjoint 152 allocs (5.28 KiB) 148 allocs (5.20 KiB) 1.02
nfftoperators/NFFTOp/forward 149 allocs (5.17 KiB) 145 allocs (5.11 KiB) 1.01
nonlinearoperators/Atan/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Atan/jacobian-adjoint 0 allocs (0 bytes) 2 allocs (104 bytes) 0 🐢
nonlinearoperators/Cos/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Exp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Exp/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Sigmoid/forward 0 allocs (0 bytes) 2 allocs (88 bytes) 0 🐢
nonlinearoperators/Sigmoid/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/jacobian-adjoint 3 allocs (512.07 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Tanh/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Tanh/jacobian-adjoint 0 allocs (0 bytes) 2 allocs (88 bytes) 0 🐢
normaloperators/DFT/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DiagOp/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/NFFTOp/mul 7 allocs (208 bytes) 3 allocs (176 bytes) 1.18
waveletoperators/WaveletOp/adjoint 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1
waveletoperators/WaveletOp/forward 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1

Ratio interpretation: values > 1 mean the PR is faster; values < 1 mean slower.
🚀 significant speedup · 🐢 significant slowdown

@kul-optec kul-optec deleted a comment from github-actions Bot Sep 19, 2026
@hakkelt

hakkelt commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushing the commit that fixed the bugs spotted by @lostella caused all test jobs to fail because a new version of both Documenter and JET was released, which were stricter about some checks (or because the Julia version was changed to 1.13?) -- this is the reason why a couple of seemingly unrelated commits were also pushed.

Anyway, @lostella, please let me know if the PR can be merged or if you need more time for further review.

@lostella

Copy link
Copy Markdown
Member

@hakkelt this is now beyond my current context length limits (eheh), so I'll have Claude help out and report asap.

Comment thread src/calculus/DCAT.jl Resolved
Comment thread src/linearoperators/DiagOp.jl Resolved
Comment thread src/linearoperators/LBFGS.jl Resolved
Comment thread src/calculus/DCAT.jl Resolved
Comment thread src/threading_policy.jl Resolved
Comment thread src/calculus/HCAT.jl Resolved
Comment thread src/threading_policy.jl Resolved
Comment thread test/test_threading_policy.jl Resolved
Comment thread src/calculus/DCAT.jl Resolved
Comment thread test/wavelets/test_wavelet_operators.jl Resolved
hakkelt and others added 12 commits November 2, 2026 19:35
…t merge

Adds AcceleratedDCTs test dependency, disables Aqua's persistent_tasks
check, and carries forward the test coverage expansion, test refactors,
and small dead-code/API cleanups developed on gpu-refactor after the
GPU-support work was squash-merged upstream as 861af22.
It requires Julia >= 1.11 and is already added dynamically at runtime
for that case in test/utils.jl; listing it statically forced dependency
resolution to fail on the Julia 1.10 LTS CI job.
The private thread_count_functions registry and the @restrict_threading /
@enable_full_threading macros in src/utils.jl are replaced by the new
standalone NestedThreading.jl package.

Beyond relocating the mechanism this fixes a latent correctness bug: the
old save/restore was per scope, so two concurrent batched mul! calls
captured each other's already-restricted thread counts and left BLAS
permanently pinned at 1 with no scope active (reproducible before this
commit, fixed after). NestedThreading refcounts budgets process-wide,
snapshotting once and restoring once, and applies the minimum over all
active scopes so nesting can only narrow a budget, never widen it.

- src/utils.jl: delete the threading machinery.
- SpreadingBatchOp/SimpleBatchOp: @restrict_threading @threads -> the
  @budgeted_threads macro; the LOCKING @sync/@Spawn sites use the
  function form with_restricted_threads.
- FFTWOperators: drop the __init__ that reached into AbstractOperators'
  private registry; the FFTW pool now registers via NestedThreading's own
  package extension.
- NFFTOperators: delete set_nfft_threading_expr and its two macros in
  favour of a with_nfft_threading helper over NestedThreading's scoped
  API, at all six call sites.
- Inner libraries now get nthreads() / trip_count threads instead of
  always 1, so a 2-element batch on 8 threads runs BLAS at 4.
- Add a regression test for the concurrency bug and for budget nesting.

NestedThreading is wired in via [sources] pending registration in
General, so these changes are not ready to push yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
BroadCast.jl's threaded adjoint mul! runs an outer @threads loop whose
body calls mul! on arbitrary sub-operators, which may themselves use
BLAS/FFTW. It never went through the old @restrict_threading, so it was a
genuine oversubscription source that the migration had left in place.

This was the last raw @threads in src/, so @threads is dropped from the
imports; @Spawn and nthreads are still used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
`TestUtils` is the setup module of nearly every testitem, and it unconditionally ran
`GPUEnv.activate(; persist=true)` plus the FFTW-specific `AcceleratedDCTs` install. Any
filtered run paid full GPU environment activation even when it selected no GPU test.

Move both into a new `GpuEnvSetup` @testmodule (test/gpu_env_setup.jl). TestItemRunner
evaluates setup modules lazily, only for testitems that survive the filter, so activation
now happens iff a `:gpu` testitem is selected. All 47 `:gpu` testitems gain
`GpuEnvSetup` in their setup list.

`runtests.jl` publishes `ABSTRACTOPERATORS_TEST_GPU` from the filter closure, set from the
tags of the items it actually accepts. Deriving it from the filter *string* would be wrong:
GPU testitems also carry their category tags, so `:linearoperator` selects `Eye (GPU)` too.
`GpuEnvSetup` errors when the flag is false, which can only mean a testitem missing its
`:gpu` tag pulled the module in.

Audit results:
- `:gpu` testitems keep their inline `using GPUEnv`; the import is a name binding and costs
  nothing, only `activate` was expensive.
- test_gpu_quality.jl's two items are tagged `:gpu` but exercise GpuExt through JLArrays
  alone, a direct test dep, so they deliberately do not use `GpuEnvSetup`.

Measured: single non-GPU testitem 3m20s -> 8.7s. Pass counts unchanged --
`:jet` 224/224 and `:misc,:quality` 185/185 with zero GPU activation, `:calculus`
1557/1557 (still activates, correctly: it selects the `*(GPU)` items by their
`:calculus` tag), `"Eye (GPU)"` 27/27 through the new setup module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Phases 1-4 of THREADING_PLAN.md. Threading was previously expressed three different ways
with four scattered thresholds, and `copy_operator`'s deepcopy fallback silently ignored
both of its keywords.

Infrastructure (src/threading_policy.jl):
- `is_threaded` trait, `supports_threading`, `default_threaded`/`threading_threshold`
  per-operator policy, and `adapt_operator`.
- `copy_operator` now always copies; the "share when thread-safe" short-circuit moved to
  `adapt_operator`. The deepcopy fallback refuses `storage_type`/`threaded` instead of
  ignoring them, naming the type that needs a `_copy_operator_impl`.
- Thresholds are transcriptions of benchmark/threading_sweep.jl (new), not guesses; each
  carries a PROVENANCE line. The sweep contradicted the plan's hypothesis that
  FastBroadcast wins or ties everywhere: for a pure copy `@.. thread=true` is
  time-identical to serial at every swept size, while `@batch` crosses over at 2^15.

Nesting safety (the bug the plan called out):
- create_BatchOp, create_threaded_SpreadingBatchOp and OperatorBroadCast adapted only the
  per-thread *copies*, leaving instance 1 threaded. All three now go through
  `_per_thread_operators`, which also branches on `is_thread_safe` to decide share vs copy
  -- `require_thread_safe` cannot manufacture thread safety, so it must not pretend to.
- `opType` had to move after the adaptation: threading is a type parameter.

Elementwise threading: all 10 nonlinear operators (SoftMax excluded, with the reduction
reason recorded), FiniteDiff, and forwarding traits for the calculus operators.

FiniteDiff `mul!` no longer allocates: `b[idx_1] .- b[idx_2]` materialised two temporaries
per call. With `@views` it is allocation-free and 45x faster at n=1e6 (2581us -> 57us).

Two problems the tests surfaced, both fixed in source rather than in the assertion:
- `adapt_operator(op; threaded=false)` silently no-opped on forwarders, which lacked
  `is_threaded` and so inherited the `false` default while their children stayed threaded.
- JET flagged runtime dispatch in `FiniteDiff(T, dims)`, which fell through to the
  `dir::Int` method and its `Val(dir)`. Split out a D=1 literal method.

The two batching perf tests needed wider margins, not weaker ones: the FiniteDiff
allocation fix sped the serial baseline up ~3x, shrinking an honest 2.5x speedup to 1.7x
and letting runner noise flip a bare `<`. Both now take a minimum across repetitions at a
workload size measured to favour batch parallelism (smaller-items/more-items was measured
to hurt).

Tests: :batching 283, :jet 245 (incl. new @test_opt/@test_call threading coverage),
:quality 61, :calculus+:linearoperator+:nonlinearoperator+:misc 2698, :Threading+:Syntax
276 -- all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Phase 5 of THREADING_PLAN.md, for DCAT: its blocks are fully independent in both
directions, with no accumulation and no shared output.

The policy needed a second condition the plan did not anticipate. Aggregate work alone
does not predict a win: 2 blocks x 2^18 elements is a 2^19 aggregate but measured only
1.03x, because two blocks cap the gain at 2x and the threading overhead eats it, while the
same 2^19 aggregate across 8 blocks measured 1.9x. So `default_block_threaded` requires
both `THRESHOLD_BLOCK_PARALLEL` (2^18, measured) and `MIN_BLOCKS_FOR_PARALLEL` (4). That
pair admits every measured win and excludes every measured loss:

  nb=8 bs=2^16  1.90x  threaded      nb=8 bs=2^12  0.26x  not threaded
  nb=4 bs=2^16  1.18x  threaded      nb=2 bs=2^18  1.03x  not threaded
  nb=8 bs=2^18  9.67x  threaded

The threaded `mul!` is deliberately not `@generated`: a generated body must be pure and the
threading macros expand to closures. The parallel loop is therefore a plain function and
only the single-block body is generated, selected by `Val(i)`. That costs one dynamic
dispatch per block -- nanoseconds against a whole child `mul!` -- and keeps the per-block
type stability a closure over a heterogeneous tuple would have lost. `@budgeted_threads`
rather than `@batch` because each body may itself reach BLAS/FFTW.

Nesting safety applies here too: when the block loop threads, the constructor adapts the
blocks to `threaded = false`.

VCAT-forward and HCAT-adjoint from the plan are not included; they need their own
measurement before shipping, per the plan's own rule that unmeasured variants are dropped.

Tests: :Threading,:DCAT,:calculus 1769; :jet,:quality,:misc 462 -- all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…ression

Phase 6 of THREADING_PLAN.md, plus a regression this phase uncovered.

The regression: Phase 1's strict `copy_operator` fallback made batching *any* FFTW/DSP
operator raise. Those operators own scratch buffers, so a threaded batch needs one private
copy per thread; but they have no `_copy_operator_impl`, and the fallback refused the
`threaded = false` request the batch layer makes. The fix belongs in the fallback's rule,
not in per-operator patches: an operator with no threaded path cannot have a `threaded`
request violated, so deepcopy is a correct answer for it. That is the same rule
`_satisfies_constraints` already used, now applied consistently. `storage_type` is still
refused, since deepcopy genuinely cannot honour it.

Per subpackage:
- NFFTOp already carried a `threaded` field; it gains the traits and a `_copy_operator_impl`
  that shares the immutable plan and dcf while giving the copy its own scratch buffer.
  Requests that would need a different plan (thread count, storage backend) raise rather
  than being silently ignored.
- DFT/IDFT gain `threaded` as the package-wide spelling of the existing `num_threads`, and
  record the plan-time count so `is_threaded` can report it. FFTW is a counted pool, so this
  is fixed at construction; switching it replans.
- DSP and Wavelet operators declare `supports_threading = false` explicitly rather than
  inheriting the default, which is what lets a threaded batch wrap them.

Two bugs caught while writing this, both mine:
- A "cleanup" of a trailing `'` deleted the adjoint postfix from five IDFT constructors,
  turning IDFT into a plain DFT and silently disabling the DFT/IDFT combination rules. The
  FFTW combination tests caught it; there is now a test asserting IDFT stays adjoint-wrapped.
- The DFT copy replanned from the codomain element type, which is wrong for a real-input
  DFT (`C == Complex{D}`) and produced an operator with a complex domain.

DCT/RDFT/IRDFT are not wired for plan-time thread counts -- they have no `num_threads`
plumbing at all today -- so they are declared unthreaded rather than half-wired. Recorded in
the plan's "Not done" section along with VCAT/HCAT block parallelism and the four legacy
thresholds that still gate the pre-existing operators.

THREADING_PLAN.md gains an execution record: measured-vs-guessed thresholds, the three plan
hypotheses that measurement contradicted, and what was deliberately left out.

Tests: full suite 3856 passed, 0 failed, 0 errored (21m47s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…all operators

Follow-up round covering VCAT/HCAT block threading, the remaining FFTW transforms, the
legacy thresholds, per-operator thresholds, and one semantics change that ties them
together.

`threaded` is now `Bool` only, with one meaning everywhere, resolved in one place
(`_resolve_threaded`):

  false            veto -- never threads. The only hard directive, and it must be: nesting
                   safety depends on a block/batch loop switching its children off and
                   being able to rely on it.
  true (default)   permission -- threads only if the policy also agrees.

Three families previously disagreed. Elementwise and block operators treated `true` as an
absolute override, batch operators as a permission, and the FFTW transforms bypassed the
policy entirely -- so a 256-point threaded DFT reported is_threaded == true while measuring
0.02x. `is_threaded` now reports what the operator will actually do, never what was asked
for; consequently a single-threaded session or GPU storage reports false without any
special-casing. `copy_operator`/`adapt_operator` keep accepting `nothing`, since there
`threaded` is a *constraint* ("no constraint, preserve") rather than a setting -- without it
a plain copy could not preserve an explicitly serial operator.

VCAT-forward and HCAT-adjoint now thread their block loops, and measuring them forced two
corrections to the Phase 5 policy:
- per-block, not aggregate: at a fixed 2^18 aggregate VCAT measures 1.32x with 4 blocks of
  2^16 but 0.85x with 16 blocks of 2^14;
- per-operator: at 4 blocks of 2^16 VCAT measures 1.32x while HCAT measures 0.76x, because
  HCAT's adjoint carries per-block ArrayPartition indexing. One shared constant would have
  shipped that regression.

FFTW does thread r2r and r2c (measured 3.08x r2c, 2.25x DCT, 1.91x IDCT at n=2^22), so
DCT/IDCT/RDFT/IRDFT gained plan-time thread counts, and all six transforms are now gated on
measured crossovers (c2c 2^13, r2r 2^15, r2c 2^15). `num_threads` stays an explicit command
rather than a permission -- the escape hatch for callers who know their workload.

The four legacy thresholds are gone, which fixed two live defects:
- Variation's two constructors thresholded in different *units* (bytes vs elements), so the
  same 10000-element input produced opposite threading;
- `_should_thread(::AbstractOperator)` had no size component at all, so batch operators
  threaded four-element work.
Scale's legacy 1e4 cutoff was ~400x below its measured 2^22 crossover.

Thresholds are now per-operator transcriptions from benchmark/operator_thresholds.jl (new),
which measures each operator's real `mul!`. The cost classes proved too coarse: within
"transcendental" the crossovers span 2^8 (SoftPlus) to 2^11 (integer Pow), and Pow splits
internally by exponent kind since `x^0.5` lowers to `exp(p*log(x))`.

Also unified: SignAlternation had its own `threaded && nthreads() > 1` with no size policy
and no `is_threaded` method, so its trait always reported false regardless of what it did.

JET caught a regression that no functional test could: reassigning the captured `A` inside
the DCAT/VCAT/HCAT constructors boxed it to `Any`, cascading into 11 runtime-dispatch sites
in a constructor that should infer fully. Results were correct throughout; only @test_opt
saw it.

Tests: full suite 3971 passed, 0 failed. Three batching tests needed updating because they
encoded the old "threaded=true forces threading" assumption -- one of them was reading
`batch_op.operator[1]`, which on the single-threaded struct silently *slices* the wrapped
operator instead of erroring; it now goes through the `_wrapped_operator` accessor.

Known, unfixed, pre-existing: Variation's adjoint throws BoundsError when the trailing
dimension is exactly 2. Present on master; recorded in THREADING_PLAN.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
The adjoint's per-dimension branch chain tested `i == 2` before
`i == size(y, d)`, so the last column of a dimension of length exactly 2
took the interior formula and read one element past the end. This hit
*any* dimension of size 2, not only the trailing one -- (2,8) and
(8,2,3) failed alongside (8,2) -- and the result was mathematically
wrong there, not merely out of bounds: column n of the transpose is
`y_1 + y_n` when n == 2, while the interior formula gives
`y_1 + y_2 - y_3`.

Rewrite the contribution as three independent terms
(`_variation_adjoint_term`) rather than mutually exclusive branches, so
the n == 2 case correctly takes both the `i == 2` and `i == n` terms.
Both mul! methods now share one body so the threaded and serial paths
cannot drift apart.

`N` is passed to the shared body as a plain Int, deliberately not a Val:
inside Polyester's `@batch` the loop body becomes a closure that does not
carry the enclosing method's static parameters, so a `Val(N)` argument is
built from a runtime value and every element dispatches dynamically. That
allocated 293 MB at n = 2^22 and turned a 4.5x speedup into 0.25x, while
leaving every correctness test green -- hence the new allocation testitem,
timings being too noisy to assert on.

Also reject singleton dimensions at construction. They have no finite
difference to take and already failed with a BoundsError inside the
*forward* kernel, so an ArgumentError naming the cause is strictly
better; the array-based constructor now delegates to the tuple one so the
guard and the threading policy each exist in one place.

Verified against the dense forward matrix's exact transpose over 16
shapes x 2 element types x 2 threading modes. Threaded adjoint is back to
baseline (12527 us vs 12668 us at n = 2^22, zero allocations) and the
serial path is ~1.75x faster at mid sizes (3528 vs 6195 us at 2^18), the
three-term form replacing unpredictable branches with straight-line
arithmetic. Measured crossover is unchanged at 2^10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
The plan is fully executed and its content is now recorded where it is
actually used: threshold provenance in `threading_threshold` docstrings,
policy rationale in `src/threading_policy.jl`, and the measurement
methodology in `benchmark/operator_thresholds.jl`. Nothing references the
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
hakkelt and others added 22 commits November 2, 2026 19:35
The `Th` type parameter selected by the threading policy is consumed as
`@.. thread = Th`, which requires a `FastBroadcast.True()`/`False()` instance,
not the bare `True`/`False` type. Passing the type made every threaded
elementwise `mul!` fail with `MethodError: no method matching DataType()`.

Affects the nine elementwise non-linear operators plus `DiagOp` and `Scale`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For operators whose codomain and domain shapes have different types the
`size(L)` tuple is heterogeneous, so indexing it with a runtime `i` widens the
result to a `Union`. That `Union` propagated through `ndims`, `count_dims`,
`ndoms` and `check`, turning every comparison in the `mul!` argument check into
a runtime dispatch under `@test_opt`.

Index the size tuple with literal indices at each of those sites instead:
`ndims` maps over `size(L)`, `ndoms` recomputes from `size(L)` rather than
mapping over `ndims(L)`, and `check` destructures the tuple once. `size(L, i)`
itself is public API and keeps its runtime-index method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`y[range, d]` builds a `SubArray` whose index tuple mixes a `StepRange` and an
`Int`. Base's `ensure_indexable` maps over that tuple, and abstract
interpretation widens the per-element type, which left the resulting view's
index type non-concrete and made every broadcast in the kernel a reported
runtime dispatch.

Compute the column offset explicitly and index `y` with a single range instead,
in both the threaded and the non-threaded method. Behaviour is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts commit e7753ac.

The "failed to optimize due to recursion" reports it worked around were
downstream of the non-concrete `size`/`ndims`/`ndoms` chain, now fixed at the
root. With that fix in place the `@generated size` methods infer fine while
binding only the type parameters they use, and the full `:jet` slice passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An explanatory comment between the docstring and `ndims(L::AbstractOperator)`
detached the docstring from the method, so Documenter failed the build with
`no docs found for 'ndims' in @docs block in docs/src/properties.md`. Move the
comment above the docstring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_fbbool(::Type{FastBroadcast.True})` accepted the bare type, so an operator
that stored the type instead of the singleton instance still reported the right
`is_threaded` trait and only failed later, inside `@.. thread = Th`, with
`MethodError: no method matching DataType()`. No caller passes a type any more;
removing the methods turns that mistake into an immediate `MethodError` at the
trait instead of a silent mis-encoding.

Replace the two tests that pinned the type form with a check that the operators
which carry the flag really store an instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md allows only a fixed set of lowercase type tags and CamelCase
operator names, so :Threading was neither. Rename it to :threading and
add that tag to the documented list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four review findings on the block-parallel loops:

- Data race: a threaded block loop called `mul!` on the same operator from
  several tasks when one non-thread-safe operator filled several blocks,
  directly (`DCAT(C, C)`) or wrapped (`DCAT(C, 2 * C)`), so the blocks
  shared its scratch buffers and the output was wrong on every call.
  Each non-thread-safe block now gets its own copy (`_block_loop_operator`).
- Storage: only the first block's storage was checked, so a CPU block in
  front of device blocks threaded the loop. Every block is checked now; a
  multi-domain block counts as CPU when all its components are.
- Lost flag: `copy_operator(dcat; threaded = true)` forced the blocks
  serial even when the constructor then left the block loop serial, and
  `get_normal_op(::DCAT)`, `VCAT.remove_slicing`, `getindex`, `Jacobian`
  and the combination rules rebuilt block operators without their
  block-loop flag, so a `threaded = false` veto was lost. `HCAT(A, buf)`
  and `VCAT(A, buf)` take `threaded` for that.
- `check`: HCAT's `Tuple` `mul!` methods (forward, and serial and threaded
  adjoint) did not call `check`, so a wrong size surfaced as a block's
  error, or a `TaskFailedException` from the threaded loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `copy_operator(DiagOp(T, dims, 2.0))` threw a `MethodError` from
  `_array_wrapper(::Float64)`. A scalar diagonal now keeps the operator's
  own storage type.
- Copying an `LBFGS` built a fresh operator and dropped the curvature
  history (`s_M`, `y_M`, `ys_M`, `currmem`, `curridx`, `H`), so the copy
  acted as the identity. The history is the operator's state: the copy now
  starts from it, with its own buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FFTW and DSP operators on device storage reported `is_threaded == true`
  above the size gate: `_fftw_num_threads` and `_dsp_fftw_num_threads`
  never looked at storage, so `RDFT`, `IRDFT`, `Conv` and `Xcorr` on a
  `CuArray` recorded `Threads.nthreads()` and `adapt_operator(op;
  threaded = false)` replanned them on every call. Both now return 1 for
  device storage, whatever was asked.
- Device-backed `MatrixOp`/`LMatrixOp` entered the restricted BLAS scope
  (a process-wide lock plus a setter round trip for every pool) on every
  `mul!`, although a device `gemm` uses none of those pools. The scope is
  now skipped for non-CPU storage: 2x2 `JLArray` `MatrixOp` `mul!` 5.44 us
  and 64 B before, 0.073 us and 0 B after (bare `mul!`: 0.064 us); the
  CPU `threaded = false` path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`==` on SimpleBatchOp and SpreadingBatchOp compared the wrapped operators
and the sizes but not the domain/codomain batch masks, so two batch
operators batching along different axes compared equal while giving
different results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`threaded = true` only permits threading, and with `-t 1` it correctly
resolves to serial, but the threading contract helper and the threading
traits tests asserted `is_threaded(...) == true` unconditionally. CI only
passed because it sets `JULIA_NUM_THREADS: 2`. Those assertions now
expect `Threads.nthreads() > 1`; none is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was the one :gpu testitem still on `setup = [TestUtils]` only. Without
`GpuEnvSetup`, `gpu_backends()` returns nothing and the testitem passes
without asserting anything, or not, depending on test order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
copy_operator, get_normal_op, permute, remove_displacement and Jacobian
passed blocks that share no scratch through the constructor, which copied
the non-thread-safe ones again. copy_operator(D; threaded = true) also
built threaded block copies (FFTW plans) only to replace them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JLArray gemm allocates by itself, so the test failed on CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batch operators fell back to `supports_threading == false`, so
`adapt_operator(b; threaded = false)` returned a threaded batch op unchanged.
Their copies also dropped `threaded` for the wrapped operator, while a serial
batch loop reported `is_threaded == false` even around a threaded operator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copying every block that is not thread-safe detached the loop's blocks from the
caller's objects, so e.g. `update!` on an `LBFGS` block no longer reached a
threaded DCAT. Copy a block only when it shares a non-thread-safe operator with
another block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`is_threaded` and `supports_threading` now default to asking the operator's
children, so the forwarders no longer repeat the same two methods and a new
wrapper cannot forget them. Also:

- `_block_loop_operators` walks each block's tree once.
- `_children(::SpreadingBatchOpCopying)` iterates instead of allocating.
- `_should_thread(op, storage_type)` takes `storage_type` as a default argument.
- `DiagOp` copy uses `_fbbool`.
- `DFT`/`IRDFT` copies with nothing to change return the operator itself.
- Batch operator docstrings no longer mention a `nothing` default for `threaded`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `is_threaded` and `supports_threading` referred to the internal `_children` with an
  `@ref` that Documenter cannot resolve, which fails the docs build.
- The `_children` docstring still promised a tuple and described the old `false`
  default.
- The `SimpleBatchOp` comment described a removed method.
- The `DFT`/`IRDFT` copy comments now say why returning `op` is a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Benchmark Results (Julia v1.13.3)

Run once per thread count (-t 1, -t 2, ...) so single- and multi-threaded paths are
each measured under a controlled, pinned thread count rather than mixed into one
ambiguous run -- see each operator's own size-based threading policy for why a "default"
run at more than one thread cannot otherwise be assumed serial or parallel.

1 thread

🚀 6 benchmarks improved in time · 🚀 7 benchmarks use less memory

Time benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 6.32 ± 0.15 μs —
batching/SimpleBatchOp/adjoint-single 33.9 ± 5.2 μs — —
batching/SimpleBatchOp/forward — 5.39 ± 0.12 μs —
batching/SimpleBatchOp/forward-single 28.9 ± 4 μs — —
batching/SpreadingBatchOp/adjoint-single 17.7 ± 3 μs 4.23 ± 0.2 μs 4.19 ± 1.5 🚀
batching/SpreadingBatchOp/forward-single 15 ± 2.9 μs 3.65 ± 0.2 μs 4.1 ± 1.7 🚀
calculus/AffineAdd/adjoint 4.61 ± 0.085 μs 4.57 ± 0.08 μs 1.01 ± 0.051
calculus/AffineAdd/forward 10.6 ± 1.1 μs 12.7 ± 2.2 μs 0.84 ± 0.34
calculus/Ax_mul_Bx/forward 27.8 ± 0.61 μs 27.7 ± 0.64 μs 1 ± 0.064
calculus/Ax_mul_Bx/jacobian-adjoint 50.1 ± 3.1 μs 50.6 ± 3.4 μs 0.989 ± 0.18
calculus/Ax_mul_Bxt/forward 27.8 ± 0.56 μs 27.8 ± 0.57 μs 1 ± 0.057
calculus/Ax_mul_Bxt/jacobian-adjoint 49.8 ± 3.1 μs 49.9 ± 3 μs 0.998 ± 0.17
calculus/Axt_mul_Bx/forward 31.6 ± 0.57 μs 31.5 ± 0.53 μs 1 ± 0.049
calculus/Axt_mul_Bx/jacobian-adjoint 45.7 ± 1.6 μs 45.9 ± 1.7 μs 0.994 ± 0.1
calculus/BroadCast/identity — 52.2 ± 4 μs —
calculus/BroadCast/identity-single 52.1 ± 3.6 μs — —
calculus/BroadCast/operator-adjoint — 14.3 ± 0.26 μs —
calculus/BroadCast/operator-forward — 231 ± 10 ns —
calculus/BroadCast/operator-single-adjoint 14 ± 0.38 μs — —
calculus/BroadCast/operator-single-forward 240 ± 10 ns — —
calculus/Compose/adjoint 8.14 ± 0.89 μs 9.24 ± 1.6 μs 0.881 ± 0.36
calculus/Compose/forward 8.63 ± 1.1 μs 10.2 ± 1.6 μs 0.848 ± 0.34
calculus/DCAT/adjoint 15.4 ± 2 μs 16.9 ± 1.5 μs 0.91 ± 0.29
calculus/DCAT/forward 16.5 ± 1.8 μs 18.6 ± 2.1 μs 0.885 ± 0.28
calculus/HCAT/adjoint 17.1 ± 2.2 μs 17.9 ± 2 μs 0.955 ± 0.33
calculus/HCAT/forward 27.3 ± 1.2 μs 27.8 ± 2.1 μs 0.982 ± 0.18
calculus/HadamardProd/forward 702 ± 7 μs 698 ± 8.3 μs 1.01 ± 0.031
calculus/HadamardProd/jacobian-adjoint 782 ± 8.8 μs 789 ± 7.1 μs 0.991 ± 0.029
calculus/Jacobian/sigmoid-adjoint 123 ± 1.1 μs 116 ± 11 μs 1.06 ± 0.19
calculus/Reshape/adjoint 4.65 ± 0.085 μs 4.57 ± 0.065 μs 1.02 ± 0.047
calculus/Reshape/forward 4.93 ± 0.24 μs 4.77 ± 0.21 μs 1.03 ± 0.14
calculus/Scale/adjoint 6.93 ± 0.1 μs 6.83 ± 0.07 μs 1.01 ± 0.036
calculus/Scale/forward 7.24 ± 0.24 μs 7.02 ± 0.21 μs 1.03 ± 0.093
calculus/Sum/adjoint 26.4 ± 2.8 μs 26.7 ± 2.8 μs 0.987 ± 0.29
calculus/Sum/forward 27.1 ± 3.3 μs 27.1 ± 2.8 μs 0.997 ± 0.32
calculus/VCAT/adjoint 27.8 ± 3.8 μs 27.3 ± 2.7 μs 1.02 ± 0.34
calculus/VCAT/forward 15.9 ± 2.6 μs 15.5 ± 1.6 μs 1.03 ± 0.4
dspoperators/Filt/adjoint 462 ± 10 μs 462 ± 11 μs 1 ± 0.064
dspoperators/Filt/forward 472 ± 13 μs 472 ± 13 μs 0.999 ± 0.08
dspoperators/MIMOFilt/adjoint 158 ± 1.3 μs 160 ± 1 μs 0.986 ± 0.021
dspoperators/MIMOFilt/forward 165 ± 1.3 μs 165 ± 1.6 μs 0.999 ± 0.025
dspoperators/Xcorr/adjoint 51.6 ± 2.8 μs 55.3 ± 2.7 μs 0.934 ± 0.14
dspoperators/Xcorr/forward 345 ± 53 μs 344 ± 33 μs 1 ± 0.36
fftwoperators/DFT/adjoint 131 ± 3.7 μs 130 ± 4.3 μs 1 ± 0.088
fftwoperators/DFT/forward 120 ± 3.5 μs 121 ± 3.7 μs 0.992 ± 0.084
linearoperators/DiagOp/adjoint — 200 ± 4.8 μs —
linearoperators/DiagOp/adjoint-single 201 ± 5.8 μs — —
linearoperators/DiagOp/forward — 199 ± 6 μs —
linearoperators/DiagOp/forward-single 198 ± 6.9 μs — —
linearoperators/Eye/forward 295 ± 8.3 μs 294 ± 8.3 μs 1 ± 0.08
linearoperators/FiniteDiff/adjoint 265 ± 9.5 μs 64.7 ± 3.6 μs 4.09 ± 0.54 🚀
linearoperators/FiniteDiff/forward 262 ± 8.8 μs 54.5 ± 2.5 μs 4.82 ± 0.55 🚀
linearoperators/GetIndex/adjoint 606 ± 13 μs 610 ± 16 μs 0.993 ± 0.066
linearoperators/GetIndex/forward 504 ± 18 μs 520 ± 16 μs 0.97 ± 0.093
linearoperators/LBFGS/mul 42.1 ± 0.58 μs 42.1 ± 0.56 μs 0.999 ± 0.038
linearoperators/LBFGS/update 9.13 ± 1 μs 8.9 ± 0.88 μs 1.03 ± 0.3
linearoperators/LMatrixOp/adjoint 184 ± 25 μs 173 ± 23 μs 1.06 ± 0.41
linearoperators/LMatrixOp/forward 113 ± 6.1 μs 110 ± 8.2 μs 1.03 ± 0.19
linearoperators/MatrixOp/adjoint 170 ± 4.2 μs 169 ± 7.1 μs 1.01 ± 0.098
linearoperators/MatrixOp/forward 168 ± 3.8 μs 167 ± 3.1 μs 1.01 ± 0.059
linearoperators/MyLinOp/adjoint 202 ± 12 μs 200 ± 6.7 μs 1.01 ± 0.14
linearoperators/MyLinOp/forward 196 ± 7 μs 201 ± 7.2 μs 0.974 ± 0.099
linearoperators/Variation/adjoint — 430 ± 4.5 μs —
linearoperators/Variation/adjoint-single 468 ± 4.4 μs — —
linearoperators/Variation/forward — 72.9 ± 7.8 μs —
linearoperators/Variation/forward-single 67.5 ± 4.9 μs — —
linearoperators/ZeroPad/adjoint 109 ± 0.66 μs 109 ± 0.42 μs 0.998 ± 0.014
linearoperators/ZeroPad/forward 39.3 ± 1.8 μs 39.7 ± 2.2 μs 0.991 ± 0.14
linearoperators/Zeros/forward 426 ± 21 μs 413 ± 19 μs 1.03 ± 0.14
nfftoperators/NFFTOp/adjoint 186 ± 5.8 μs 188 ± 8.9 μs 0.991 ± 0.11
nfftoperators/NFFTOp/forward 187 ± 6.3 μs 189 ± 6.3 μs 0.991 ± 0.094
nonlinearoperators/Atan/forward 287 ± 21 μs 264 ± 53 μs 1.08 ± 0.46
nonlinearoperators/Atan/jacobian-adjoint 12.2 ± 1.1 μs 13.4 ± 9.1 μs 0.913 ± 1.3
nonlinearoperators/Cos/forward 228 ± 17 μs 242 ± 15 μs 0.944 ± 0.18
nonlinearoperators/Cos/jacobian-adjoint 314 ± 21 μs 260 ± 23 μs 1.21 ± 0.27
nonlinearoperators/Exp/forward 407 ± 4.9 μs 400 ± 4.6 μs 1.02 ± 0.034
nonlinearoperators/Exp/jacobian-adjoint 460 ± 5.2 μs 453 ± 5 μs 1.02 ± 0.032
nonlinearoperators/Pow/forward 467 ± 4.4 μs 429 ± 4.5 μs 1.09 ± 0.031
nonlinearoperators/Pow/jacobian-adjoint 324 ± 4.5 μs 325 ± 4.2 μs 0.999 ± 0.038
nonlinearoperators/Sech/forward 125 ± 0.97 μs 124 ± 4 μs 1.01 ± 0.067
nonlinearoperators/Sech/jacobian-adjoint 522 ± 21 μs 475 ± 18 μs 1.1 ± 0.12
nonlinearoperators/Sigmoid/forward 262 ± 3.9 μs 255 ± 4.3 μs 1.03 ± 0.046
nonlinearoperators/Sigmoid/jacobian-adjoint 250 ± 4 μs 235 ± 7.8 μs 1.06 ± 0.079
nonlinearoperators/Sin/forward 245 ± 16 μs 240 ± 15 μs 1.02 ± 0.19
nonlinearoperators/Sin/jacobian-adjoint 285 ± 13 μs 268 ± 21 μs 1.07 ± 0.19
nonlinearoperators/SoftMax/forward 262 ± 4.1 μs 132 ± 1.1 μs 1.99 ± 0.071 🚀
nonlinearoperators/SoftMax/jacobian-adjoint 274 ± 4.4 μs 141 ± 2 μs 1.94 ± 0.083 🚀
nonlinearoperators/SoftPlus/forward 685 ± 4.2 μs 700 ± 3.2 μs 0.979 ± 0.015
nonlinearoperators/SoftPlus/jacobian-adjoint 298 ± 7 μs 282 ± 5 μs 1.06 ± 0.062
nonlinearoperators/Tanh/forward 156 ± 0.91 μs 153 ± 1.3 μs 1.02 ± 0.021
nonlinearoperators/Tanh/jacobian-adjoint 146 ± 2.4 μs 150 ± 4.7 μs 0.976 ± 0.07
normaloperators/DFT/mul 3.44 ± 0.025 μs 3.42 ± 0.03 μs 1.01 ± 0.023
normaloperators/DiagOp/mul 208 ± 4.6 μs 207 ± 5.4 μs 1 ± 0.069
normaloperators/NFFTOp/mul 113 ± 2.2 μs 116 ± 2.4 μs 0.978 ± 0.055
waveletoperators/WaveletOp/adjoint 2.85 ± 0.092 ms 2.89 ± 0.024 ms 0.987 ± 0.065
waveletoperators/WaveletOp/forward 1.07 ± 0.029 ms 1.11 ± 0.046 ms 0.965 ± 0.097
Memory benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 0 allocs (0 bytes) —
batching/SimpleBatchOp/adjoint-single 704 allocs (275.00 KiB) — —
batching/SimpleBatchOp/forward — 0 allocs (0 bytes) —
batching/SimpleBatchOp/forward-single 384 allocs (265.00 KiB) — —
batching/SpreadingBatchOp/adjoint-single 386 allocs (139.09 KiB) 34 allocs (1.59 KiB) 87.3 🚀
batching/SpreadingBatchOp/forward-single 224 allocs (134.00 KiB) 32 allocs (1.50 KiB) 89.3 🚀
calculus/AffineAdd/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/AffineAdd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/BroadCast/identity — 1 allocs (48 bytes) —
calculus/BroadCast/identity-single 1 allocs (48 bytes) — —
calculus/BroadCast/operator-adjoint — 119 allocs (4.48 KiB) —
calculus/BroadCast/operator-forward — 0 allocs (0 bytes) —
calculus/BroadCast/operator-single-adjoint 119 allocs (4.48 KiB) — —
calculus/BroadCast/operator-single-forward 0 allocs (0 bytes) — —
calculus/Compose/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/DCAT/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/HCAT/adjoint 131 allocs (4.70 KiB) 71 allocs (2.55 KiB) 1.85 🚀
calculus/HCAT/forward 70 allocs (2.52 KiB) 40 allocs (1.44 KiB) 1.75 🚀
calculus/HadamardProd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/HadamardProd/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
calculus/Jacobian/sigmoid-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Reshape/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Reshape/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Scale/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/VCAT/adjoint 130 allocs (4.67 KiB) 70 allocs (2.52 KiB) 1.86 🚀
calculus/VCAT/forward 71 allocs (2.55 KiB) 41 allocs (1.47 KiB) 1.73 🚀
dspoperators/Filt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Filt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
fftwoperators/DFT/adjoint 3 allocs (256.09 KiB) 3 allocs (256.09 KiB) 1
fftwoperators/DFT/forward 3 allocs (256.09 KiB) 3 allocs (256.09 KiB) 1
linearoperators/DiagOp/adjoint — 0 allocs (0 bytes) —
linearoperators/DiagOp/adjoint-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward — 0 allocs (0 bytes) —
linearoperators/DiagOp/forward-single 0 allocs (0 bytes) — —
linearoperators/Eye/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/FiniteDiff/adjoint 11 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/FiniteDiff/forward 6 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/GetIndex/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/GetIndex/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LBFGS/mul 65 allocs (1.33 KiB) 65 allocs (1.33 KiB) 1
linearoperators/LBFGS/update 3 allocs (48 bytes) 3 allocs (48 bytes) 1
linearoperators/LMatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LMatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/adjoint — 0 allocs (0 bytes) —
linearoperators/Variation/adjoint-single 0 allocs (0 bytes) — —
linearoperators/Variation/forward — 768 allocs (24.00 KiB) —
linearoperators/Variation/forward-single 512 allocs (16.00 KiB) — —
linearoperators/ZeroPad/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/ZeroPad/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Zeros/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nfftoperators/NFFTOp/adjoint 152 allocs (5.28 KiB) 148 allocs (5.17 KiB) 1.02
nfftoperators/NFFTOp/forward 149 allocs (5.17 KiB) 145 allocs (5.06 KiB) 1.02
nonlinearoperators/Atan/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Atan/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Exp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Exp/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Sigmoid/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sigmoid/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/jacobian-adjoint 3 allocs (512.07 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Tanh/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Tanh/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DFT/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DiagOp/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/NFFTOp/mul 7 allocs (208 bytes) 3 allocs (96 bytes) 2.17 🚀
waveletoperators/WaveletOp/adjoint 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1
waveletoperators/WaveletOp/forward 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1

2 threads

🚀 25 benchmarks improved in time · 🐢 1 time regression detected · 🚀 6 benchmarks use less memory

Time benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 6.34 ± 0.14 μs —
batching/SimpleBatchOp/adjoint-single 36.3 ± 6.5 μs — —
batching/SimpleBatchOp/forward — 5.45 ± 0.14 μs —
batching/SimpleBatchOp/forward-single 31.4 ± 2.9 μs — —
batching/SpreadingBatchOp/adjoint-copying — 3.88 ± 0.08 μs —
batching/SpreadingBatchOp/adjoint-fixed_operator — 3.87 ± 0.078 μs —
batching/SpreadingBatchOp/adjoint-locking — 3.88 ± 0.083 μs —
batching/SpreadingBatchOp/adjoint-single 19.2 ± 3.2 μs 4.23 ± 0.18 μs 4.53 ± 1.6 🚀
batching/SpreadingBatchOp/forward-copying — 3.31 ± 0.089 μs —
batching/SpreadingBatchOp/forward-fixed_operator — 3.31 ± 0.086 μs —
batching/SpreadingBatchOp/forward-locking — 3.31 ± 0.086 μs —
batching/SpreadingBatchOp/forward-single 16.3 ± 1.8 μs 3.68 ± 0.16 μs 4.42 ± 1.1 🚀
calculus/AffineAdd/adjoint 4.66 ± 0.12 μs 4.61 ± 0.085 μs 1.01 ± 0.062
calculus/AffineAdd/forward 11.1 ± 1.3 μs 11.2 ± 1.5 μs 0.987 ± 0.36
calculus/Ax_mul_Bx/forward 27.7 ± 0.47 μs 27.8 ± 0.45 μs 0.999 ± 0.047
calculus/Ax_mul_Bx/jacobian-adjoint 50.4 ± 2.8 μs 55.7 ± 3.5 μs 0.904 ± 0.15
calculus/Ax_mul_Bxt/forward 27.8 ± 0.49 μs 27.8 ± 0.35 μs 1 ± 0.043
calculus/Ax_mul_Bxt/jacobian-adjoint 49.7 ± 2.8 μs 54.7 ± 3.2 μs 0.909 ± 0.15
calculus/Axt_mul_Bx/forward 31.6 ± 0.47 μs 31.6 ± 0.55 μs 0.999 ± 0.045
calculus/Axt_mul_Bx/jacobian-adjoint 45.8 ± 1.5 μs 46.8 ± 1.8 μs 0.98 ± 0.099
calculus/BroadCast/identity — 34.6 ± 4.4 μs —
calculus/BroadCast/identity-single 54.7 ± 2.7 μs — —
calculus/BroadCast/operator-adjoint — 14.4 ± 0.27 μs —
calculus/BroadCast/operator-forward — 240 ± 9.5 ns —
calculus/BroadCast/operator-single-adjoint 13.8 ± 0.32 μs — —
calculus/BroadCast/operator-single-forward 241 ± 10 ns — —
calculus/Compose/adjoint 8.64 ± 0.91 μs 9.66 ± 1.2 μs 0.894 ± 0.3
calculus/Compose/forward 9.14 ± 1.2 μs 10.2 ± 1.3 μs 0.892 ± 0.33
calculus/DCAT/adjoint 17.3 ± 3.1 μs 15 ± 0.98 μs 1.15 ± 0.44
calculus/DCAT/adjoint-threaded — 54.6 ± 2 μs —
calculus/DCAT/forward 15.8 ± 1.4 μs 17 ± 1.7 μs 0.929 ± 0.25
calculus/DCAT/forward-threaded — 53.4 ± 1.4 μs —
calculus/HCAT/adjoint 18.1 ± 2.5 μs 18.8 ± 1.8 μs 0.962 ± 0.33
calculus/HCAT/adjoint-threaded — 97.6 ± 3.6 μs —
calculus/HCAT/forward 27 ± 1.9 μs 27.6 ± 2.5 μs 0.977 ± 0.23
calculus/HCAT/forward-threaded — 216 ± 9 μs —
calculus/HadamardProd/forward 700 ± 5.5 μs 241 ± 22 μs 2.91 ± 0.54 🚀
calculus/HadamardProd/jacobian-adjoint 778 ± 8.6 μs 290 ± 22 μs 2.69 ± 0.41 🚀
calculus/Jacobian/sigmoid-adjoint 123 ± 1.2 μs 61.6 ± 3.9 μs 2 ± 0.25 🚀
calculus/Reshape/adjoint 4.68 ± 0.09 μs 4.62 ± 0.08 μs 1.01 ± 0.052
calculus/Reshape/forward 5.08 ± 0.27 μs 4.99 ± 0.21 μs 1.02 ± 0.14
calculus/Scale/adjoint 7.02 ± 0.12 μs 6.88 ± 0.1 μs 1.02 ± 0.047
calculus/Scale/adjoint-threaded — 2.38 ± 0.0017 ms —
calculus/Scale/forward 7.27 ± 0.24 μs 7.29 ± 0.28 μs 0.997 ± 0.1
calculus/Scale/forward-threaded — 2.63 ± 0.028 ms —
calculus/Sum/adjoint 26.6 ± 3.1 μs 25.5 ± 3.7 μs 1.04 ± 0.39
calculus/Sum/forward 27.2 ± 3.8 μs 26.2 ± 3.4 μs 1.04 ± 0.4
calculus/VCAT/adjoint 27 ± 2.5 μs 27.4 ± 3.9 μs 0.984 ± 0.33
calculus/VCAT/adjoint-threaded — 102 ± 1.3 μs —
calculus/VCAT/forward 15.2 ± 0.49 μs 15.7 ± 1.5 μs 0.968 ± 0.19
calculus/VCAT/forward-threaded — 53.9 ± 0.46 μs —
dspoperators/Filt/adjoint 462 ± 10 μs 462 ± 8.9 μs 1 ± 0.06
dspoperators/Filt/forward 472 ± 14 μs 466 ± 14 μs 1.01 ± 0.085
dspoperators/MIMOFilt/adjoint 158 ± 1 μs 161 ± 1.4 μs 0.982 ± 0.022
dspoperators/MIMOFilt/forward 165 ± 1.3 μs 165 ± 1.5 μs 0.997 ± 0.024
dspoperators/Xcorr/adjoint 52.1 ± 2.7 μs 53.5 ± 9.1 μs 0.973 ± 0.35
dspoperators/Xcorr/forward 331 ± 16 μs 339 ± 38 μs 0.977 ± 0.24
fftwoperators/DFT/adjoint 121 ± 5.5 μs 117 ± 4.2 μs 1.03 ± 0.12
fftwoperators/DFT/forward 103 ± 6.6 μs 100 ± 3.4 μs 1.02 ± 0.15
linearoperators/DiagOp/adjoint — 122 ± 7.7 μs —
linearoperators/DiagOp/adjoint-single 202 ± 5.9 μs — —
linearoperators/DiagOp/adjoint-threaded 120 ± 5.1 μs — —
linearoperators/DiagOp/forward — 121 ± 5.2 μs —
linearoperators/DiagOp/forward-single 200 ± 6.3 μs — —
linearoperators/DiagOp/forward-threaded 119 ± 5.3 μs — —
linearoperators/Eye/forward 293 ± 9.6 μs 293 ± 7.6 μs 0.997 ± 0.083
linearoperators/FiniteDiff/adjoint 263 ± 10 μs 37.8 ± 3.6 μs 6.95 ± 1.4 🚀
linearoperators/FiniteDiff/forward 258 ± 8.8 μs 38.6 ± 6.4 μs 6.68 ± 2.3 🚀
linearoperators/GetIndex/adjoint 608 ± 11 μs 609 ± 12 μs 0.998 ± 0.054
linearoperators/GetIndex/forward 519 ± 17 μs 519 ± 20 μs 1 ± 0.1
linearoperators/LBFGS/mul 42.3 ± 0.78 μs 42.4 ± 0.66 μs 0.999 ± 0.048
linearoperators/LBFGS/update 9.18 ± 0.85 μs 9.73 ± 1 μs 0.943 ± 0.26
linearoperators/LMatrixOp/adjoint 170 ± 18 μs 171 ± 22 μs 0.997 ± 0.34
linearoperators/LMatrixOp/forward 112 ± 5.1 μs 108 ± 6.9 μs 1.04 ± 0.16
linearoperators/MatrixOp/adjoint 170 ± 4.2 μs 173 ± 4.4 μs 0.987 ± 0.07
linearoperators/MatrixOp/forward 168 ± 3.2 μs 170 ± 7.7 μs 0.99 ± 0.097
linearoperators/MyLinOp/adjoint 203 ± 6 μs 200 ± 5.1 μs 1.01 ± 0.079
linearoperators/MyLinOp/forward 201 ± 5.3 μs 200 ± 5.3 μs 1 ± 0.075
linearoperators/Variation/adjoint — 321 ± 4.1 μs —
linearoperators/Variation/adjoint-single 431 ± 4.6 μs — —
linearoperators/Variation/forward — 42.4 ± 5.4 μs —
linearoperators/Variation/forward-single 67.2 ± 5.4 μs — —
linearoperators/ZeroPad/adjoint 109 ± 0.92 μs 144 ± 0.61 μs 0.755 ± 0.014 🐢
linearoperators/ZeroPad/forward 39.5 ± 2.9 μs 39.6 ± 2.2 μs 0.998 ± 0.18
linearoperators/Zeros/forward 421 ± 13 μs 418 ± 26 μs 1.01 ± 0.14
nfftoperators/NFFTOp/adjoint 184 ± 5.4 μs 184 ± 5.2 μs 1 ± 0.081
nfftoperators/NFFTOp/forward 186 ± 5.4 μs 189 ± 5.1 μs 0.983 ± 0.078
nonlinearoperators/Atan/forward 307 ± 33 μs 90.6 ± 1.8 μs 3.39 ± 0.74 🚀
nonlinearoperators/Atan/jacobian-adjoint 12.4 ± 0.71 μs 13.3 ± 4.3 μs 0.935 ± 0.61
nonlinearoperators/Cos/forward 237 ± 21 μs 61.6 ± 1.7 μs 3.84 ± 0.7 🚀
nonlinearoperators/Cos/jacobian-adjoint 267 ± 18 μs 68.8 ± 15 μs 3.88 ± 1.8 🚀
nonlinearoperators/Exp/forward 408 ± 4.9 μs 196 ± 0.67 μs 2.07 ± 0.052 🚀
nonlinearoperators/Exp/jacobian-adjoint 460 ± 5 μs 212 ± 3.7 μs 2.17 ± 0.089 🚀
nonlinearoperators/Pow/forward 467 ± 4.4 μs 232 ± 4.5 μs 2.02 ± 0.087 🚀
nonlinearoperators/Pow/jacobian-adjoint 317 ± 4.3 μs 144 ± 4.1 μs 2.21 ± 0.14 🚀
nonlinearoperators/Sech/forward 126 ± 4.1 μs 60.9 ± 0.34 μs 2.06 ± 0.14 🚀
nonlinearoperators/Sech/jacobian-adjoint 488 ± 11 μs 151 ± 5.8 μs 3.23 ± 0.28 🚀
nonlinearoperators/Sigmoid/forward 263 ± 4.1 μs 119 ± 0.21 μs 2.2 ± 0.069 🚀
nonlinearoperators/Sigmoid/jacobian-adjoint 249 ± 4 μs 118 ± 9.3 μs 2.1 ± 0.34 🚀
nonlinearoperators/Sin/forward 126 ± 36 μs 66.8 ± 0.37 μs 1.89 ± 1.1
nonlinearoperators/Sin/jacobian-adjoint 230 ± 37 μs 68.6 ± 14 μs 3.35 ± 1.8 🚀
nonlinearoperators/SoftMax/forward 262 ± 4.6 μs 195 ± 0.98 μs 1.34 ± 0.049 🚀
nonlinearoperators/SoftMax/jacobian-adjoint 273 ± 4.4 μs 218 ± 1.6 μs 1.25 ± 0.044 🚀
nonlinearoperators/SoftPlus/forward 685 ± 4.1 μs 331 ± 4.3 μs 2.07 ± 0.059 🚀
nonlinearoperators/SoftPlus/jacobian-adjoint 296 ± 8 μs 130 ± 0.77 μs 2.28 ± 0.13 🚀
nonlinearoperators/Tanh/forward 156 ± 1.1 μs 71.6 ± 0.2 μs 2.17 ± 0.033 🚀
nonlinearoperators/Tanh/jacobian-adjoint 147 ± 5.2 μs 71.5 ± 0.38 μs 2.05 ± 0.15 🚀
normaloperators/DFT/mul 3.61 ± 0.055 μs 3.44 ± 0.035 μs 1.05 ± 0.038
normaloperators/DiagOp/mul 207 ± 4.4 μs 209 ± 4.9 μs 0.99 ± 0.063
normaloperators/NFFTOp/mul 117 ± 4.1 μs 117 ± 3.2 μs 1 ± 0.089
waveletoperators/WaveletOp/adjoint 2.89 ± 0.021 ms 2.89 ± 0.035 ms 1 ± 0.028
waveletoperators/WaveletOp/forward 1.08 ± 0.04 ms 1.1 ± 0.037 ms 0.986 ± 0.098
Memory benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint — 0 allocs (0 bytes) —
batching/SimpleBatchOp/adjoint-single 704 allocs (275.00 KiB) — —
batching/SimpleBatchOp/forward — 0 allocs (0 bytes) —
batching/SimpleBatchOp/forward-single 384 allocs (265.00 KiB) — —
batching/SpreadingBatchOp/adjoint-copying — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-fixed_operator — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-locking — 34 allocs (1.59 KiB) —
batching/SpreadingBatchOp/adjoint-single 386 allocs (139.09 KiB) 34 allocs (1.59 KiB) 87.3 🚀
batching/SpreadingBatchOp/forward-copying — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-fixed_operator — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-locking — 32 allocs (1.50 KiB) —
batching/SpreadingBatchOp/forward-single 224 allocs (134.00 KiB) 32 allocs (1.50 KiB) 89.3 🚀
calculus/AffineAdd/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/AffineAdd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/BroadCast/identity — 1 allocs (48 bytes) —
calculus/BroadCast/identity-single 1 allocs (48 bytes) — —
calculus/BroadCast/operator-adjoint — 119 allocs (4.48 KiB) —
calculus/BroadCast/operator-forward — 0 allocs (0 bytes) —
calculus/BroadCast/operator-single-adjoint 119 allocs (4.48 KiB) — —
calculus/BroadCast/operator-single-forward 0 allocs (0 bytes) — —
calculus/Compose/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/DCAT/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/adjoint-threaded — 30 allocs (2.30 KiB) —
calculus/DCAT/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/forward-threaded — 30 allocs (2.30 KiB) —
calculus/HCAT/adjoint 131 allocs (4.70 KiB) 71 allocs (2.55 KiB) 1.85 🚀
calculus/HCAT/adjoint-threaded — 127 allocs (6.44 KiB) —
calculus/HCAT/forward 70 allocs (2.52 KiB) 40 allocs (1.44 KiB) 1.75 🚀
calculus/HCAT/forward-threaded — 56 allocs (2.12 KiB) —
calculus/HadamardProd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/HadamardProd/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
calculus/Jacobian/sigmoid-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Reshape/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Reshape/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Scale/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/adjoint-threaded — 0 allocs (0 bytes) —
calculus/Scale/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/forward-threaded — 0 allocs (0 bytes) —
calculus/Sum/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/VCAT/adjoint 130 allocs (4.67 KiB) 70 allocs (2.52 KiB) 1.86 🚀
calculus/VCAT/adjoint-threaded — 98 allocs (3.72 KiB) —
calculus/VCAT/forward 71 allocs (2.55 KiB) 41 allocs (1.47 KiB) 1.73 🚀
calculus/VCAT/forward-threaded — 82 allocs (4.39 KiB) —
dspoperators/Filt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Filt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
fftwoperators/DFT/adjoint 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
fftwoperators/DFT/forward 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
linearoperators/DiagOp/adjoint — 0 allocs (0 bytes) —
linearoperators/DiagOp/adjoint-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/adjoint-threaded 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward — 0 allocs (0 bytes) —
linearoperators/DiagOp/forward-single 0 allocs (0 bytes) — —
linearoperators/DiagOp/forward-threaded 0 allocs (0 bytes) — —
linearoperators/Eye/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/FiniteDiff/adjoint 11 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/FiniteDiff/forward 6 allocs (4.00 MiB) 0 allocs (0 bytes) N/A 🚀
linearoperators/GetIndex/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/GetIndex/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LBFGS/mul 65 allocs (1.33 KiB) 65 allocs (1.33 KiB) 1
linearoperators/LBFGS/update 3 allocs (48 bytes) 3 allocs (48 bytes) 1
linearoperators/LMatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LMatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/adjoint — 0 allocs (0 bytes) —
linearoperators/Variation/adjoint-single 0 allocs (0 bytes) — —
linearoperators/Variation/forward — 4 allocs (128 bytes) —
linearoperators/Variation/forward-single 512 allocs (16.00 KiB) — —
linearoperators/ZeroPad/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/ZeroPad/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Zeros/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nfftoperators/NFFTOp/adjoint 152 allocs (5.28 KiB) 149 allocs (5.23 KiB) 1.01
nfftoperators/NFFTOp/forward 149 allocs (5.17 KiB) 146 allocs (5.14 KiB) 1.01
nonlinearoperators/Atan/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Atan/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Exp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Exp/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/jacobian-adjoint 6 allocs (512.14 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Sigmoid/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sigmoid/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/jacobian-adjoint 3 allocs (512.07 KiB) 0 allocs (0 bytes) N/A 🚀
nonlinearoperators/Tanh/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Tanh/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DFT/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DiagOp/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/NFFTOp/mul 7 allocs (208 bytes) 4 allocs (208 bytes) 1
waveletoperators/WaveletOp/adjoint 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1
waveletoperators/WaveletOp/forward 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1

Ratio interpretation: values > 1 mean the PR is faster; values < 1 mean slower.
🚀 significant speedup · 🐢 significant slowdown

hakkelt and others added 3 commits November 3, 2026 15:45
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `update!` call used random gradients, so the curvature check could reject
the pair and leave the operator an identity map that any stale copy would also
match. Use `g = 2x`, assert that the update changed the operator, and compare
with `≈`: inside the block loop BLAS runs single-threaded and rounds the LBFGS
dot products differently, which failed the exact comparison on Julia 1.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wdown

- A literal power in a threaded `@..` broadcast passes `^` and the exponent in
  `Ref`s, which cost 2 allocations per call in Sigmoid forward and the Tanh and
  Atan Jacobian-adjoints. Use `inv` or a scalar helper instead.
- The fused Sigmoid Jacobian-adjoint was ~1.3x slower than master on one thread.
  Computing `exp` in its own pass first is faster both serial and threaded
  (32768 elements: 264 -> 187 us serial, 146 -> 102 us on 2 threads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hakkelt

hakkelt commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

@hakkelt this is now beyond my current context length limits (eheh), so I'll have Claude help out and report asap.

Well, that's fully understandable given the size of this PR. 😄 I reviewed it manually before I submitted, but it seems that either I wasn't careful enough or it is beyond the capabilities of a human reviewer to catch all bugs at this scale...

Anyway, I generated fixes for the spotted bugs and reviewed the quality of the changes and whether they fix the problematic behavior both manually and by AI. Some other bugs were found and fixed along the way, and I merged two minor PRs that upgraded two dependencies (GPUEnv and JET), so that's why this PR was rebased on master.

Btw, do you agree with the proposed API, @lostella? The threaded name for the keyword argument, and that it means permission for multithreading, and the actual behavior (whether to multithread or not) is decided based on thresholds measured offline?

@hakkelt
hakkelt requested a review from lostella November 3, 2026 15:53

This branch has not been deployed

No deployments
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