Repository: scipy/scipy

Issue:
scipy.fft: Add a `plan` argument
See https://github.com/scipy/scipy/pull/10238#issuecomment-502058590

`scipy.fft` currently lacks any plan caching. For repeated transforms, this does a significant amount of duplicate work and makes `scipy.fft` slower than `scipy.fftpack` for repeated regular sized ffts. (For one off ffts, pocketfft is still much faster)

Things to look into/discuss:
- How much faster is pocketfft after adding a cache?
- Can we have users pass in the plans instead of caching?
- Should the user have control over the cache itself?


Discussion:
I've implemented a simple cache with a FIFO eviction policy.
<details>
<summary> get_plan function </summary>

```cpp
template<typename T, size_t size=4> shared_ptr<T> get_plan(size_t len)
	{
	static shared_ptr<T> cache[size];
	static size_t keys[size] = {0};
	static int head = 0;
	static mutex mut;
	{
	std::lock_guard<mutex> lock(mut);
	for (int i = 0; i < size; ++i)
		{
		if (keys[i] == len)
			{
			if (cache[i] != nullptr)
				return cache[i];

			break;
			}
		}
	}

	auto plan = make_shared<T>(len);
	{
	std::lock_guard<mutex> lock(mut);

	// If some else beat us to it, use theirs
	for (int i = 0; i < size; ++i)
		{
		if (keys[i] == len)
			{
			if (cache[i] != nullptr)
				return cache[i];

			break;
			}
		}

	keys[head] = len;
	cache[head] = plan;
	head++;

	if (head >= size)
		head = 0;
	}
	return plan;
	}

```
</details>

## Benchmarks

### Without cache
```
· `wheel_cache_size` has been renamed to `build_cache_size`. Update your `asv.conf.json` accordingly.
· Discovering benchmarks
· Running 5 total benchmarks (1 commits * 1 environments * 5 benchmarks)
[  0.00%] ·· Benchmarking existing-py_usr_bin_py
[ 10.00%] ··· Running (fft_basic.Fft.time_fft--)...
[ 40.00%] ··· Running (fft_basic.RFft.time_irfft--)..
[ 60.00%] ··· fft_basic.Fft.time_fft                                          ok
[ 60.00%] ··· ====== ======= =============== ============= ============
              --                               module                  
              -------------- ------------------------------------------
               size    type   scipy.fftpack    scipy.fft    numpy.fft  
              ====== ======= =============== ============= ============
               100     real     2.60±0.1μs    4.56±0.06μs   8.52±0.2μs 
               100    cmplx    2.82±0.04μs    4.34±0.07μs   8.29±0.2μs 
               256     real    3.14±0.05μs     5.53±0.1μs   10.2±0.2μs 
               256    cmplx     4.33±0.7μs     5.93±0.4μs   10.0±0.1μs 
               313     real      70.3±1μs      28.5±0.5μs    117±3μs   
               313    cmplx      120±3μs       26.9±0.5μs    116±1μs   
               512     real     4.74±0.1μs     7.47±0.2μs   14.2±0.7μs 
               512    cmplx     6.44±0.5μs     8.73±0.4μs   13.4±0.3μs 
               1000    real     8.83±0.5μs     11.2±0.2μs    24.5±2μs  
               1000   cmplx     10.7±0.1μs     14.8±0.5μs    21.2±2μs  
               1024    real     7.93±0.2μs      10.5±3μs    20.6±0.3μs 
               1024   cmplx     10.7±0.4μs     14.5±0.3μs   20.6±0.6μs 
               2048    real     14.1±0.2μs     17.9±0.4μs   37.5±0.3μs 
               2048   cmplx     22.5±0.8μs     28.8±0.3μs   35.6±0.2μs 
               4096    real      38.4±4μs      35.4±0.3μs    69.2±1μs  
               4096   cmplx      48.6±6μs      55.7±0.6μs    68.4±2μs  
               8192    real      68.1±2μs       75.0±1μs     233±30μs  
               8192   cmplx      113±2μs        128±1μs      149±2μs   
              ====== ======= =============== ============= ============

[ 70.00%] ··· fft_basic.Fft.time_ifft                                         ok
[ 70.00%] ··· ====== ======= =============== ============ ============
              --                               module                 
              -------------- -----------------------------------------
               size    type   scipy.fftpack   scipy.fft    numpy.fft  
              ====== ======= =============== ============ ============
               100     real    2.75±0.06μs    4.99±0.3μs   10.9±0.3μs 
               100    cmplx     3.49±0.1μs    4.55±0.4μs   10.8±0.2μs 
               256     real     3.57±0.4μs    6.13±0.5μs   12.4±0.2μs 
               256    cmplx     4.92±0.5μs    6.27±0.3μs   12.3±0.2μs 
               313     real      76.2±3μs     28.9±0.6μs    124±20μs  
               313    cmplx      125±3μs       35.0±4μs     118±6μs   
               512     real    5.05±0.08μs     8.49±1μs    17.0±0.3μs 
               512    cmplx     8.16±0.3μs    8.79±0.2μs    17.7±1μs  
               1000    real      15.0±9μs     12.3±0.7μs   25.7±0.5μs 
               1000   cmplx     18.0±0.5μs     16.4±2μs    25.7±0.5μs 
               1024    real    8.33±0.03μs    11.2±0.2μs    25.9±1μs  
               1024   cmplx     15.0±0.3μs    15.3±0.3μs   26.3±0.5μs 
               2048    real     16.9±0.4μs     20.2±4μs    42.3±0.6μs 
               2048   cmplx     30.7±0.6μs    29.7±0.9μs   43.6±0.5μs 
               4096    real     35.0±0.5μs    40.1±0.6μs    76.1±2μs  
               4096   cmplx     62.1±0.6μs     58.8±1μs     80.4±7μs  
               8192    real      79.5±2μs      79.1±1μs     172±4μs   
               8192   cmplx      136±2μs       129±2μs      172±2μs   
              ====== ======= =============== ============ ============

[ 80.00%] ··· fft_basic.Fftn.time_fftn                                        ok
[ 80.00%] ··· ========== ======= =============== =============
                 size      type       module                  
              ---------- ------- --------------- -------------
               100x100     real   scipy.fftpack     219±6μs   
               100x100     real     scipy.fft       136±3μs   
               100x100     real     numpy.fft       203±5μs   
               100x100    cmplx   scipy.fftpack     223±7μs   
               100x100    cmplx     scipy.fft       163±5μs   
               100x100    cmplx     numpy.fft       198±7μs   
               313x100     real   scipy.fftpack    12.3±0.2ms 
               313x100     real     scipy.fft       615±9μs   
               313x100     real     numpy.fft      10.8±0.1ms 
               313x100    cmplx   scipy.fftpack   12.0±0.05ms 
               313x100    cmplx     scipy.fft       942±10μs  
               313x100    cmplx     numpy.fft      10.8±0.3ms 
               1000x100    real   scipy.fftpack   2.14±0.09ms 
               1000x100    real     scipy.fft       947±40μs  
               1000x100    real     numpy.fft      4.03±0.7ms 
               1000x100   cmplx   scipy.fftpack    2.20±0.1ms 
               1000x100   cmplx     scipy.fft     1.30±0.03ms 
               1000x100   cmplx     numpy.fft     2.23±0.09ms 
               256x256     real   scipy.fftpack   1.32±0.01ms 
               256x256     real     scipy.fft       604±10μs  
               256x256     real     numpy.fft      2.11±0.7ms 
               256x256    cmplx   scipy.fftpack    1.42±0.1ms 
               256x256    cmplx     scipy.fft       848±20μs  
               256x256    cmplx     numpy.fft      2.69±0.8ms 
               512x512     real   scipy.fftpack    7.86±0.9ms 
               512x512     real     scipy.fft      2.64±0.3ms 
               512x512     real     numpy.fft       10.8±2ms  
               512x512    cmplx   scipy.fftpack    7.87±0.9ms 
               512x512    cmplx     scipy.fft      4.91±0.1ms 
               512x512    cmplx     numpy.fft       10.4±3ms  
              ========== ======= =============== =============

[ 90.00%] ··· fft_basic.RFft.time_irfft                                       ok
[ 90.00%] ··· ====== =============== ============ ============
              --                       module                 
              ------ -----------------------------------------
               size   scipy.fftpack   scipy.fft    numpy.fft  
              ====== =============== ============ ============
               100     2.71±0.08μs    4.99±0.4μs   11.7±0.3μs 
               256      3.59±0.4μs    5.57±0.2μs   13.0±0.2μs 
               313      70.5±0.9μs    6.73±0.3μs   19.1±0.3μs 
               512      4.61±0.3μs    7.29±0.2μs   15.4±0.1μs 
               1000     7.59±0.1μs    11.8±0.3μs   20.5±0.5μs 
               1024     6.79±0.1μs    10.1±0.3μs   19.8±0.5μs 
               2048     13.2±0.7μs    18.5±0.5μs   29.2±0.4μs 
               4096     29.6±0.5μs     38.0±1μs     54.5±4μs  
               8192     67.3±0.8μs     80.7±1μs     101±2μs   
              ====== =============== ============ ============

[100.00%] ··· fft_basic.RFft.time_rfft                                        ok
[100.00%] ··· ====== =============== ============= ============
              --                       module                  
              ------ ------------------------------------------
               size   scipy.fftpack    scipy.fft    numpy.fft  
              ====== =============== ============= ============
               100      2.60±0.2μs     4.23±0.1μs   7.76±0.2μs 
               256      3.44±0.4μs     6.06±0.4μs   9.07±0.4μs 
               313       73.1±3μs       30.4±3μs     83.4±6μs  
               512       5.62±2μs      7.75±0.3μs   11.1±0.9μs 
               1000     7.03±0.3μs     10.5±0.3μs   15.0±0.7μs 
               1024      8.02±4μs     9.72±0.07μs   13.9±0.4μs 
               2048      13.0±2μs       15.8±3μs    20.7±0.3μs 
               4096     23.7±0.6μs     31.6±0.3μs    38.2±1μs  
               8192     53.0±0.7μs      66.9±2μs     78.0±3μs  
              ====== =============== ============= ============

Running benchmarks for Scipy version 1.4.0.dev0+c120e22 at /mnt/d/git/scipy/build/testenv/lib/python3.7/site-packages/scipy/__init__.py

```

### With cache

```
· `wheel_cache_size` has been renamed to `build_cache_size`. Update your `asv.conf.json` accordingly.
· Discovering benchmarks
· Running 5 total benchmarks (1 commits * 1 environments * 5 benchmarks)
[  0.00%] ·· Benchmarking existing-py_usr_bin_py
[ 10.00%] ··· Running (fft_basic.Fft.time_fft--)...
[ 40.00%] ··· Running (fft_basic.RFft.time_irfft--)..
[ 60.00%] ··· fft_basic.Fft.time_fft                                          ok
[ 60.00%] ··· ====== ======= =============== ============= ============
              --                               module                  
              -------------- ------------------------------------------
               size    type   scipy.fftpack    scipy.fft    numpy.fft  
              ====== ======= =============== ============= ============
               100     real    2.64±0.05μs    3.08±0.08μs   8.63±0.1μs 
               100    cmplx    2.75±0.07μs     3.20±0.1μs   8.85±0.3μs 
               256     real     3.36±0.1μs     4.85±0.7μs   10.3±0.3μs 
               256    cmplx     4.40±0.5μs     4.33±0.1μs   9.81±0.3μs 
               313     real      72.8±2μs      15.6±0.3μs    116±3μs   
               313    cmplx      124±1μs       14.8±0.2μs    117±3μs   
               512     real     5.25±0.7μs     5.41±0.1μs   14.3±0.2μs 
               512    cmplx     6.01±0.1μs     6.59±0.1μs   13.4±0.3μs 
               1000    real     7.86±0.7μs     9.20±0.3μs    21.9±1μs  
               1000   cmplx    10.7±0.07μs     11.7±0.2μs   21.1±0.3μs 
               1024    real     7.43±0.5μs     8.42±0.2μs   21.8±0.9μs 
               1024   cmplx    10.4±0.07μs     11.4±0.3μs   20.6±0.4μs 
               2048    real     14.1±0.7μs     14.9±0.8μs    36.9±1μs  
               2048   cmplx     22.6±0.4μs     23.8±0.4μs   35.9±0.6μs 
               4096    real     28.2±0.4μs     30.0±0.6μs   68.9±0.8μs 
               4096   cmplx     46.2±0.5μs     46.9±0.6μs    65.4±1μs  
               8192    real     62.0±0.8μs     63.7±0.7μs    154±4μs   
               8192   cmplx      105±9μs        111±3μs      148±2μs   
              ====== ======= =============== ============= ============

[ 70.00%] ··· fft_basic.Fft.time_ifft                                         ok
[ 70.00%] ··· ====== ======= =============== ============= ============
              --                               module                  
              -------------- ------------------------------------------
               size    type   scipy.fftpack    scipy.fft    numpy.fft  
              ====== ======= =============== ============= ============
               100     real    2.58±0.04μs     3.16±0.2μs   10.9±0.3μs 
               100    cmplx     3.38±0.1μs     3.28±0.3μs   10.9±0.2μs 
               256     real    3.55±0.08μs    3.99±0.04μs   12.8±0.3μs 
               256    cmplx     4.70±0.3μs     4.75±0.2μs   12.7±0.3μs 
               313     real      74.6±2μs      15.5±0.1μs    118±6μs   
               313    cmplx      126±2μs       14.8±0.2μs    117±4μs   
               512     real     4.82±0.1μs     6.04±0.4μs   17.6±0.2μs 
               512    cmplx     8.10±0.2μs     7.23±0.5μs   18.1±0.3μs 
               1000    real     8.36±0.1μs     9.68±0.3μs   25.5±0.4μs 
               1000   cmplx     19.2±0.9μs     11.9±0.3μs   25.7±0.5μs 
               1024    real     8.07±0.3μs     9.54±0.3μs   24.6±0.5μs 
               1024   cmplx     14.7±0.2μs     12.3±0.2μs   25.8±0.4μs 
               2048    real     15.3±0.4μs     16.1±0.3μs    44.5±1μs  
               2048   cmplx     31.6±0.3μs     25.1±0.5μs   44.2±0.9μs 
               4096    real      33.8±1μs      39.9±10μs     78.1±1μs  
               4096   cmplx      62.1±1μs       58.2±7μs     80.9±1μs  
               8192    real     70.7±0.9μs     69.3±0.6μs    173±4μs   
               8192   cmplx      137±2μs        112±4μs      178±5μs   
              ====== ======= =============== ============= ============

[ 80.00%] ··· fft_basic.Fftn.time_fftn                                        ok
[ 80.00%] ··· ========== ======= =============== =============
                 size      type       module                  
              ---------- ------- --------------- -------------
               100x100     real   scipy.fftpack     213±4μs   
               100x100     real     scipy.fft       141±7μs   
               100x100     real     numpy.fft       205±4μs   
               100x100    cmplx   scipy.fftpack     226±8μs   
               100x100    cmplx     scipy.fft       181±6μs   
               100x100    cmplx     numpy.fft       205±4μs   
               313x100     real   scipy.fftpack    13.1±0.7ms 
               313x100     real     scipy.fft       600±20μs  
               313x100     real     numpy.fft      11.2±0.3ms 
               313x100    cmplx   scipy.fftpack    12.5±0.2ms 
               313x100    cmplx     scipy.fft       937±30μs  
               313x100    cmplx     numpy.fft      11.1±0.4ms 
               1000x100    real   scipy.fftpack    2.18±0.1ms 
               1000x100    real     scipy.fft       955±30μs  
               1000x100    real     numpy.fft      4.04±0.8ms 
               1000x100   cmplx   scipy.fftpack    2.37±0.2ms 
               1000x100   cmplx     scipy.fft     1.44±0.05ms 
               1000x100   cmplx     numpy.fft     2.32±0.09ms 
               256x256     real   scipy.fftpack   1.36±0.05ms 
               256x256     real     scipy.fft       611±20μs  
               256x256     real     numpy.fft      1.55±0.7ms 
               256x256    cmplx   scipy.fftpack   1.39±0.09ms 
               256x256    cmplx     scipy.fft       844±20μs  
               256x256    cmplx     numpy.fft      2.66±0.7ms 
               512x512     real   scipy.fftpack    7.17±0.7ms 
               512x512     real     scipy.fft      2.75±0.3ms 
               512x512     real     numpy.fft      10.5±0.8ms 
               512x512    cmplx   scipy.fftpack    7.68±0.5ms 
               512x512    cmplx     scipy.fft      5.18±0.3ms 
               512x512    cmplx     numpy.fft       10.8±3ms  
              ========== ======= =============== =============

[ 90.00%] ··· fft_basic.RFft.time_irfft                                       ok
[ 90.00%] ··· ====== =============== ============= ============
              --                       module                  
              ------ ------------------------------------------
               size   scipy.fftpack    scipy.fft    numpy.fft  
              ====== =============== ============= ============
               100     2.71±0.05μs     3.35±0.3μs   12.5±0.3μs 
               256      3.35±0.2μs    3.91±0.04μs   13.7±0.3μs 
               313       75.2±5μs      5.04±0.2μs    19.8±1μs  
               512      4.67±0.1μs    5.37±0.08μs   16.7±0.5μs 
               1000    7.79±0.04μs     8.81±0.4μs   21.0±0.5μs 
               1024     7.37±0.7μs     7.94±0.2μs   20.2±0.5μs 
               2048     13.0±0.4μs     14.6±0.7μs    31.4±2μs  
               4096     30.8±0.4μs     32.5±0.5μs    51.5±2μs  
               8192      68.4±2μs       68.8±4μs     102±1μs   
              ====== =============== ============= ============

[100.00%] ··· fft_basic.RFft.time_rfft                                        ok
[100.00%] ··· ====== =============== ============= ============
              --                       module                  
              ------ ------------------------------------------
               size   scipy.fftpack    scipy.fft    numpy.fft  
              ====== =============== ============= ============
               100      2.72±0.5μs    3.83±0.06μs   7.66±0.1μs 
               256      3.80±0.6μs    3.70±0.08μs   8.62±0.2μs 
               313       72.3±2μs       17.7±1μs     73.4±1μs  
               512      5.53±0.9μs    4.92±0.07μs    10.8±2μs  
               1000    6.94±0.08μs     8.19±0.6μs   14.4±0.3μs 
               1024     6.93±0.1μs     8.32±0.7μs   14.0±0.3μs 
               2048     12.4±0.7μs     12.8±0.2μs   22.0±0.3μs 
               4096      26.8±2μs      26.4±0.5μs   39.0±0.9μs 
               8192      58.9±1μs       57.6±1μs     79.8±3μs  
              ====== =============== ============= ============

Running benchmarks for Scipy version 1.4.0.dev0+c120e22 at /mnt/d/git/scipy/build/testenv/lib/python3.7/site-packages/scipy/__init__.py

```
A flexible API would be:
```
def fft(x, axis=-1, norm=None, plan='auto')
    """
    ...
    
    Parameters
    ----------
    ...
    plan : {'auto', None, scipy.fft.PocketFFTPlan}
        If 'auto' (default), use an internally managed FIFO cache of FFT plans.
        If None, a plan will be computed and used (the internal cache will not be modified).
        If a PocketFFTPlan instance is provided, it will be used and the internal
        cache will not be modified.

    See Also
    --------
    <functions related to cache management>
```
@peterbell10 WDYT both in terms of usability, and in terms of compat with PyFFTW / PyCUDA APIs?
I think having that level of customizability is good. An alternative API would be to have something like #4607 where the plan is an `FFT` callable object.


As far as compatibility with pyFFTW, I can't see any problems. For CuPy, there doesn't seem to be any support for plan objects but it has some internal cache. I also believe the underlying CUDA library mimics the FFTW interface so it should be possible. 
Actually, I found that cupy already supports a `plan` argument but only in `cupyx.scipy.fftpack` module and not `cupy.fft`.

I also noticed that these are symmetrical plans (i.e. `fft` can use the same plan as `ifft`) which doesn't seem to be the case for `pyfftw`. 


> Actually, I found that cupy already supports a plan argument but only in cupyx.scipy.fftpack module and not cupy.fft.

Yes, the CuPy core devs did not want to deviate from the NumPy API in the `cupy.fft` module, but were more open to additional optional arguments being added under the `cupyx` modules.

Also, see cupy/cupy#1936 which discusses adding R2C and C2R support to the n-dimensional planning. 
(Originally, CuPy did not have any n-dimensional plans and would just apply 1D plans separably along each axis in turn.  I added n-dimensional planning support for complex-to-complex transforms, but no one has gotten around to the R2C or C2R cases yet.)

From my earlier benchmark data I've plotted percentage difference in FFT time for the 1D case. The speedup is ~20% or more.
![fft_cacheing](https://user-images.githubusercontent.com/13238737/59967992-a91e2180-952a-11e9-9ced-d8ef8a39e39c.png)

@peterbell10 this does indeed seem worth adding. Maybe a LRU instead of FIFO, though, assuming there is not a big performance hit for adding it?

Does the API I proposed above make the most sense to you? Or do you prefer the callable approach from #4607 (I haven't looked closely at it)?
>@peterbell10 this does indeed seem worth adding. Maybe a LRU instead of FIFO, though, assuming there is not a big performance hit for adding it?

Sure, I only wrote it as a FIFO for the sake of quick prototyping. I wouldn't expect the LRU bookkeeping to add a significant performance overhead.

>Does the API I proposed above make the most sense to you? Or do you prefer the callable approach from #4607 (I haven't looked closely at it)?

I think both are reasonable and I don't have any real preference. The callable approach is similar to pyfftw's FFTW objects and your functional API is quite similar to CuPy's approach. Perhaps @grlee77 will have some insight as it sound like he's been involved in both of those?
I think the API Eric suggested is good. pyFFTW has "builders" which return a callable like in #4607, but it also has plan caching that can be used by the scipy and numpy interfaces and works basically like the "auto" case proposed in Eric's API.
I am not a big fan of the "keep-alive time" [approach used by pyFFTW](https://github.com/pyFFTW/pyFFTW/blob/e9966e5d215a00f89156bd88e1d42b884f85df78/pyfftw/interfaces/cache.py#L118-L128) and would be fine with something simple like an LRU cache.

I started a branch with a CUFFT plan cache for CuPy (see https://github.com/grlee77/cupy/commits/cufft_cache), but haven't started a PR for it.  It is using a key-based dictionary with tracking of the total amount of memory being used for the work arrays currently in the cache so that the user could potentially set an upper memory limit for the cache. If I recall correctly, pocketfft does not keep large work arrays allocated within the plans, so I don't think similar memory limit concerns would apply here.

> If I recall correctly, pocketfft does not keep large work arrays allocated within the plans, so I don't think similar memory limit concerns would apply here.

This should help. Also for users who are really concerned with memory, they can always use `None` or their own plans. Eventually we could also add support for setting limits on the MRU cache, either in number of plans or in approximate memory consumption. But I'd say let's start with a simple PR that will work in the (vast) majority of cases, and if someone wants to later add cache configuration options we can do it.

So @peterbell10 I think if you are satisfied with the API I proposed above and think it's sufficiently future compatible with the backend system we want to build, it's probably worth opening a PR to add this feature.
... also, I noticed that now https://13652-1460385-gh.circle-artifacts.com/0/html-scipyorg/roadmap.html#fourier-transform-enhancements needs to be updated :)
> If I recall correctly, pocketfft does not keep large work arrays allocated within the plans, so I don't think similar memory limit concerns would apply here.

It's correct that it doesn't store the work arrays, but it stores the full twiddle factors, which usually have almost the size of the array to be transformed, and in some cases (Bluestein!) several times the array size.

So the memory consumption is not inacceptably high, but you can still swamp your computer if you run a few very large 1D FFTs with different lengths.

One aspect missing from the API is plan creation:
- should we have different plan creation functions for each transform function? Or a single function with an extra argument e.g. `get_plan(shape, dtype, transform_type='fft')`.
- how specific should plans be: does the plan and fft function have to be given exactly the same parameters and array, or should there be some wiggle room?
> should we have different plan creation functions for each transform function? Or a single function with an extra argument e.g. get_plan(shape, dtype, transform_type='fft')

I would say a single one with a transform type.

> how specific should plans be: does the plan and fft function have to be given exactly the same parameters and array, or should there be some wiggle room?

I would say as specific as the args to `get_plan`, at least to start. So the same shape/dtype/transform will give you the same plan each time.
We cache now, so I'll close this
What about pre-planned transforms?
Feel free to reopen and re-title, or open a separate issue for that. We can also wait to implement until someone wants it
Reopening as `cupy` is interested in having pre-planned transforms in their `scipy.fft` backend.
Hello guys, while the discussion of supporting a `plan` argument in `cupyx.scipy.fftpack` is ongoing (see cupy/cupy#2743), I come up with some random thoughts and I'd like to learn what SciPy developers/maintainers would think. 

What if we add a keyword-only argument `plan=None` to `scipy.fftpack` (or to `scipy.fft` if you think more appropriate) and raise a `NotImplementedError` when any FFT function is called with `plan` not being `None`? The purpose is to let downstream libraries like pyFFTW and CuPy to reuse their own precomputed FFT plans (generated from FFTW and cuFFT, respectively).
@leofang adding that kwarg and behavior to `scipy.fft` sounds reasonable to me, what do you think @peterbell10 ?
For `scipy.fftpack` I don't think we should make any API changes after declaring it as legacy. 

For `scipy.fft`, I am a bit hesitant about enabling features that aren't supported by `scipy` itself. Partly because it could be confusing to users looking at the scipy docs. But also because it means there is no prototype for the behaviour the third-party backends are expected to follow.
> For `scipy.fftpack` I don't think we should make any API changes after declaring it as legacy.

I would think it is fine as long as it is not declared deprecated. For the proposed change, there is no new feature implemented or change of behavior, after all. I do not insist on touching this module though.

> For `scipy.fft`, I am a bit hesitant about enabling features that aren't supported by `scipy` itself. Partly because it could be confusing to users looking at the scipy docs. 

We could say something in the doc along the line that “this argument is reserved for FFT backend providers and is not used by SciPy itself.”

> But also because it means there is no prototype for the behaviour the third-party backends are expected to follow.

I don’t understand this. I thought it is obvious in the FFT community that when a `plan` is given, we execute it? Could you elaborate @peterbell10?

Alternatively, we may also just close this issue so as to make a guarantee that SciPy will not add the argument `plan` to any of its FFT modules, so that 3rd party providers can freely add it to extend the functionalities without worrying interference with SciPy? I am fine with either move.

@peterbell10 @larsoner Any comments? Thanks.
>>But also because it means there is no prototype for the behaviour the third-party backends are expected to follow.

>I thought it is obvious in the FFT community that when a plan is given, we execute it? Could you elaborate @peterbell10?

If all goes well then, yes, you should just get the same FFT computation but hopefully faster. I'm more concerned about the edge-cases where the FFT can or cannot be computed. How should errors be reported? Are the edges the same on all implementations? etc. These types of details can be important if we are keeping the interface generic.

That being said, I suppose since you aren't proposing SciPy add a `get_fft_plan` yet then nobody could actually use the interface in that way anyway.

> I'm more concerned about the edge-cases where the FFT can or cannot be computed. How should errors be reported? Are the edges the same on all implementations? etc. These types of details can be important if we are keeping the interface generic.

Thanks Peter! I agree these are legit concerns. But I also think if we're gonna raise `NotImplementedError` in SciPy, downstream vendors should have the freedom to determine the edge-case behaviors. 

> That being said, I suppose since you aren't proposing SciPy add a `get_fft_plan` yet then nobody could actually use the interface in that way anyway.

That's right. I am only proposing a (placeholder) consumer API. The producer APIs like `pyfftw.builders.*` or `cupyx.scipy.fftpack.get_fft_plan` should be determined by the vendors.

>> Alternatively, we may also just close this issue so as to make a guarantee that SciPy will not add the argument plan to any of its FFT modules, so that 3rd party providers can freely add it to extend the functionalities without worrying interference with SciPy? 

I  added this as an alternative solution in my earlier post. Could be the simplest & fastest way out?
I'm okay with adding a `plan=None` that raises `NotImplementedError` (for now) if not `None` in SciPy. Something like:
```
if plan is not None:
    raise NotImplementedError('Passing a precomputed plan is not yet supported by scipy.fft functions')
```
That both provides an API for other packages to follow, and leaves the door open for us to eventually allow passing plans via `scipy.fft.get_fft_plan` or so.
Thanks Eric! One last question to both of you before I start drafting a PR: Do we want to touch the `scipy.fft` module only and leave `scipy.fftpack` intact, or we wanna add `plan` to both?
fftpack is deprecated and we shouldn't change it anymore.

Just wait to hear from @peterbell10 that he's okay with this plan before making a PR.
> fftpack is deprecated and we shouldn't change it anymore.

As far as I recall, it is not officially deprecated, because eventually removing it would break too many codes.
On the other hand I'd suggest to leave it as it is now, or people won't get the message that `scipy.fft` is the package they should be using, at least for new projects.

Okay, just reserving a keyword with `plan=None` seems fairly harmless.

@mreineck I agree. I believe the phrasing used was "long term legacy", not deprecated. The intention being it won't be removed but will also not receive new features. With that in mind, it should be left as-is without a `plan` argument.

Resolving patch:
diff --git a/scipy/fft/_basic.py b/scipy/fft/_basic.py
--- a/scipy/fft/_basic.py
+++ b/scipy/fft/_basic.py
@@ -21,7 +21,8 @@ def _dispatch(func):
 
 
 @_dispatch
-def fft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def fft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+        plan=None):
     """
     Compute the 1-D discrete Fourier Transform.
 
@@ -52,6 +53,11 @@ def fft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``. See below for more
         details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -150,12 +156,12 @@ def fft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
     >>> plt.show()
 
     """
-
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
     """
     Compute the 1-D inverse discrete Fourier Transform.
 
@@ -197,6 +203,11 @@ def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -253,7 +264,8 @@ def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def rfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def rfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
     """
     Compute the 1-D discrete Fourier Transform for real input.
 
@@ -282,6 +294,11 @@ def rfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -340,7 +357,8 @@ def rfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the inverse of the n-point DFT for real input.
 
@@ -378,6 +396,11 @@ def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -435,7 +458,8 @@ def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def hfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def hfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
     """
     Compute the FFT of a signal that has Hermitian symmetry, i.e., a real
     spectrum.
@@ -463,6 +487,11 @@ def hfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -511,7 +540,8 @@ def hfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def ihfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
+def ihfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the inverse FFT of a signal that has Hermitian symmetry.
 
@@ -537,6 +567,11 @@ def ihfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -571,7 +606,8 @@ def ihfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def fftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def fftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
     """
     Compute the N-D discrete Fourier Transform.
 
@@ -603,6 +639,11 @@ def fftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -664,12 +705,12 @@ def fftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
     >>> plt.show()
 
     """
-
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-def ifftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def ifftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the N-D inverse discrete Fourier Transform.
 
@@ -709,6 +750,11 @@ def ifftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -765,7 +811,8 @@ def ifftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def fft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
+def fft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
     """
     Compute the 2-D discrete Fourier Transform
 
@@ -798,6 +845,11 @@ def fft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -853,12 +905,12 @@ def fft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
               0.  +0.j        ,   0.  +0.j        ]])
 
     """
-
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-def ifft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
+def ifft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the 2-D inverse discrete Fourier Transform.
 
@@ -898,6 +950,11 @@ def ifft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -943,12 +1000,12 @@ def ifft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
            [0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j]])
 
     """
-
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the N-D discrete Fourier Transform for real input.
 
@@ -983,6 +1040,11 @@ def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1041,7 +1103,8 @@ def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def rfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
+def rfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the 2-D FFT of a real array.
 
@@ -1062,6 +1125,11 @@ def rfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1079,12 +1147,12 @@ def rfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
     For more details see `rfftn`.
 
     """
-
     return (Dispatchable(x, np.ndarray),)
 
 
 @_dispatch
-def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
     """
     Compute the inverse of the N-D FFT of real input.
 
@@ -1125,6 +1193,11 @@ def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1184,8 +1257,8 @@ def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False,
-           workers=None):
+def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
     """
     Compute the 2-D inverse FFT of a real array.
 
@@ -1207,6 +1280,11 @@ def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False,
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1227,7 +1305,8 @@ def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False,
 
 
 @_dispatch
-def hfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def hfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the N-D FFT of Hermitian symmetric complex input, i.e., a
     signal with a real spectrum.
@@ -1265,6 +1344,11 @@ def hfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1334,7 +1418,8 @@ def hfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def hfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
+def hfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
     """
     Compute the 2-D FFT of a Hermitian complex array.
 
@@ -1355,6 +1440,11 @@ def hfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1376,7 +1466,8 @@ def hfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def ihfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
+def ihfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
     """
     Compute the N-D inverse discrete Fourier Transform for a real
     spectrum.
@@ -1410,6 +1501,11 @@ def ihfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
@@ -1464,8 +1560,8 @@ def ihfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None):
 
 
 @_dispatch
-def ihfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False,
-           workers=None):
+def ihfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
     """
     Compute the 2-D inverse FFT of a real spectrum.
 
@@ -1487,6 +1583,11 @@ def ihfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False,
         Maximum number of workers to use for parallel computation. If negative,
         the value wraps around from ``os.cpu_count()``.
         See :func:`~scipy.fft.fft` for more details.
+    plan: object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
 
     Returns
     -------
diff --git a/scipy/fft/_pocketfft/basic.py b/scipy/fft/_pocketfft/basic.py
--- a/scipy/fft/_pocketfft/basic.py
+++ b/scipy/fft/_pocketfft/basic.py
@@ -11,8 +11,11 @@
                      _workers)
 
 def c2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
-        workers=None):
+        workers=None, *, plan=None):
     """ Return discrete Fourier transform of real or complex sequence. """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
     overwrite_x = overwrite_x or _datacopied(tmp, x)
     norm = _normalization(norm, forward)
@@ -37,10 +40,13 @@ def c2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
 
 
 def r2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
-        workers=None):
+        workers=None, *, plan=None):
     """
     Discrete Fourier transform of a real sequence.
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
     norm = _normalization(norm, forward)
     workers = _workers(workers)
@@ -65,10 +71,13 @@ def r2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
 
 
 def c2r(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
-        workers=None):
+        workers=None, *, plan=None):
     """
     Return inverse discrete Fourier transform of real sequence x.
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
     norm = _normalization(norm, forward)
     workers = _workers(workers)
@@ -96,53 +105,80 @@ def c2r(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
 irfft.__name__ = 'irfft'
 
 
-def fft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def fft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+         *, plan=None):
     """
     2-D discrete Fourier transform.
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return fftn(x, s, axes, norm, overwrite_x, workers)
 
 
-def ifft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def ifft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+          *, plan=None):
     """
     2-D discrete inverse Fourier transform of real or complex sequence.
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return ifftn(x, s, axes, norm, overwrite_x, workers)
 
 
-def rfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def rfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+          *, plan=None):
     """
     2-D discrete Fourier transform of a real sequence
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return rfftn(x, s, axes, norm, overwrite_x, workers)
 
 
-def irfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def irfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+           *, plan=None):
     """
     2-D discrete inverse Fourier transform of a real sequence
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return irfftn(x, s, axes, norm, overwrite_x, workers)
 
 
-def hfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def hfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+          *, plan=None):
     """
     2-D discrete Fourier transform of a Hermitian sequence
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return hfftn(x, s, axes, norm, overwrite_x, workers)
 
 
-def ihfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None):
+def ihfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+           *, plan=None):
     """
     2-D discrete inverse Fourier transform of a Hermitian sequence
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     return ihfftn(x, s, axes, norm, overwrite_x, workers)
 
 
 def c2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
-         workers=None):
+         workers=None, *, plan=None):
     """
     Return multidimensional discrete Fourier transform.
     """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
 
     shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
@@ -167,8 +203,11 @@ def c2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
 ifftn.__name__ = 'ifftn'
 
 def r2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
-         workers=None):
+         workers=None, *, plan=None):
     """Return multidimensional discrete Fourier transform of real input"""
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
 
     if not np.isrealobj(tmp):
@@ -193,8 +232,11 @@ def r2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
 
 
 def c2rn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
-         workers=None):
+         workers=None, *, plan=None):
     """Multidimensional inverse discrete fourier transform with real output"""
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
     tmp = _asfarray(x)
 
     # TODO: Optimize for hermitian and real?


Test patch:
diff --git a/scipy/fft/tests/test_backend.py b/scipy/fft/tests/test_backend.py
--- a/scipy/fft/tests/test_backend.py
+++ b/scipy/fft/tests/test_backend.py
@@ -57,3 +57,33 @@ def test_backend_call(func, np_func, mock):
         assert_equal(mock.number_calls, 1)
 
     assert_allclose(func(x), answer, atol=1e-10)
+
+
+plan_funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
+              scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
+              scipy.fft.rfft, scipy.fft.rfft2, scipy.fft.rfftn,
+              scipy.fft.irfft, scipy.fft.irfft2, scipy.fft.irfftn,
+              scipy.fft.hfft, scipy.fft.hfft2, scipy.fft.hfftn,
+              scipy.fft.ihfft, scipy.fft.ihfft2, scipy.fft.ihfftn)
+
+plan_mocks = (mock_backend.fft, mock_backend.fft2, mock_backend.fftn,
+              mock_backend.ifft, mock_backend.ifft2, mock_backend.ifftn,
+              mock_backend.rfft, mock_backend.rfft2, mock_backend.rfftn,
+              mock_backend.irfft, mock_backend.irfft2, mock_backend.irfftn,
+              mock_backend.hfft, mock_backend.hfft2, mock_backend.hfftn,
+              mock_backend.ihfft, mock_backend.ihfft2, mock_backend.ihfftn)
+
+
+@pytest.mark.parametrize("func, mock", zip(plan_funcs, plan_mocks))
+def test_backend_plan(func, mock):
+    x = np.arange(20).reshape((10, 2))
+
+    with pytest.raises(NotImplementedError, match='precomputed plan'):
+        func(x, plan='foo')
+
+    with set_backend(mock_backend, only=True):
+        mock.number_calls = 0
+        y = func(x, plan='foo')
+        assert_equal(y, mock.return_value)
+        assert_equal(mock.number_calls, 1)
+        assert_equal(mock.last_args[1]['plan'], 'foo')