Repository: pykeen/pykeen
Issue: torch.fft breaks compatibility
Root cause: External Dependency Breakage

**Describe the bug**
Continuing on #264 , I cloned the master branch (up to this commit 4d543a2da86a26ce4303d01b7069b5e918cd368d ) and started a basic pipeline. 
Right upon importing the model the chain of imports crashes:
```
File ".../src/pykeen/nn/functional.py", line 19, in <module>
    import torch.fft
ModuleNotFoundError: No module named 'torch.fft'
```

This issue appears with torch 1.6, while torch 1.7.1 is running fine - I presume this is due to all the [hassle](https://pytorch.org/docs/stable/generated/torch.fft.html) around `fft` being it a function or a module.

This might lead to incompatibility of pykeen with all torch<1.7 versions 🤔  

**To Reproduce**
Steps to reproduce the behavior:
1. Clone the master (4d543a2da86a26ce4303d01b7069b5e918cd368d)
2. `from pykeen.models import TransE`
3. [See error]

**Expected behavior**
Should be loaded ok

**Environment:**

```
torch                 1.6.0
```



Looks like it’s time to implement a shady pykeen.compat module 😆 

```python
try:
    from torch.fft import fft  # works on pytorch >= 1.7 
except ModuleNotFoundError:
    from torch import fft      # works on pytorch < 1.7
```

Note: old PyTorch (<1.7) is not available on modern Python versions 3.8, 3.9, and beyond

Problem on PyTorch 1.7 - the signature is not the same in the two different ways of importing this function!

| Function | Signature |
|----------|-----------|
| `torch.rfft` | `rfft(input, signal_ndim, normalized=False, onesided=True) -> Tensor` |
| `torch.fft.rfft` | `rfft(input, n=None, dim=-1, norm=None) -> Tensor` |
    

| Function | Signature |
|----------|-----------|
| `torch.irfft` | `irfft(input, signal_ndim, normalized=False, onesided=True, signal_sizes=None) -> Tensor` |
| `torch.fft.irfft` | `irfft(input, n=None, dim=-1, norm=None) -> Tensor` |

Right now, PyKEEN is actually mixing usage of both between `pykeen.nn.functional` using new-style imports through the `torch.fft` module, and old-style usage in the HolE and RotatE models themselves.


Confirm, the issue does not appear in the `add-compat` branch, so I think it can be merged & closed. 

However, the CUDA OOM with automatic memory optimization still persists 😅 will continue in the relevant thread

@migalkin while the changes in #288 can already mitigate the import error, they break the actual usages of this code in the HolE model. I'll need help from either @mberr or @lvermue to write a solution that works for both

Starting to compare them:

### [torch.rfft](https://pytorch.org/docs/1.6.0/generated/torch.rfft.html#torch.rfft) (old)

#### Documentation
[...] This method supports 1D, 2D and 3D real-to-complex transforms, indicated by signal_ndim. [...]
* input (Tensor) - the input tensor of at least signal_ndim dimensions
* signal_ndim (int) - the number of dimensions in each signal. signal_ndim can only be 1, 2 or 3
* normalized (bool, optional) - controls whether to return normalized results. Default: False
* onesided (bool, optional) - controls whether to return half of results to avoid redundancy. Default: True

#### Usage
https://github.com/pykeen/pykeen/blob/de817c54ba68f26fa14ce9333a81896e59481994/src/pykeen/models/unimodal/hole.py#L120-L122

#### Normalized Usage
Explicitly providing all default arguments
```python
torch.rfft(input=x, signal_ndim=1, normalized=False, onesided=True)
```

### [torch.fft.rfft](https://pytorch.org/docs/stable/fft.html#torch.fft.rfft) (new)

#### Documentation
Computes the one dimensional Fourier transform of real-valued input. The FFT of a real signal is Hermitian-symmetric, X[i] = conj(X[-i]) so the output contains only the positive frequencies below the Nyquist frequency. To compute the full output, use fft()

* input (Tensor) - the real input tensor
* n (int, optional) - Signal length. If given, the input will either be zero-padded or trimmed to this length before computing the real FFT.
* dim (int, optional) - The dimension along which to take the one dimensional real FFT.
* norm (str, optional) - Normalization mode (Default is "backward" (no normalization)). For the forward transform (rfft()), these correspond to: 
  * "forward" - normalize by 1/n
  * "backward" - no normalization
  * "ortho" - normalize by 1/sqrt(n) (making the FFT orthonormal)

#### Usage
https://github.com/pykeen/pykeen/blob/de817c54ba68f26fa14ce9333a81896e59481994/src/pykeen/nn/functional.py#L416-L418

#### Normalized Usage
Explicitly providing all default arguments
```
torch.fft.rfft(x, n=None, dim=-1, norm="backward")
```


### Conclusion
The `signal_ndim` argument has been removed in favour of having separate methods for 1-3D FFT. The new version always uses the `one_sided=True` option.

For the inverse

### [torch.irfft](https://pytorch.org/docs/1.6.0/generated/torch.irfft.html#torch.irfft) (old)

#### Documentation
[...]
* input (Tensor) - the input tensor of at least signal_ndim + 1 dimensions
* signal_ndim (int) - the number of dimensions in each signal. signal_ndim can only be 1, 2 or 3
* normalized (bool, optional) - controls whether to return normalized results. Default: False
* onesided (bool, optional) - controls whether input was halfed to avoid redundancy, e.g., by rfft(). Default: True
* signal_sizes (list or torch.Size, optional) - the size of the original signal (without batch dimension). Default: None

#### Usage
https://github.com/pykeen/pykeen/blob/de817c54ba68f26fa14ce9333a81896e59481994/src/pykeen/models/unimodal/hole.py#L130-L131

#### Normalized Usage
Explicitly providing all default arguments
```python
torch.irfft(input=x, signal_ndim=1, normalized=False, onesided=True, signal_sizes=(h.shape[-1],))
```

### [torch.fft.rfft](https://pytorch.org/docs/stable/fft.html#torch.fft.rfft) (new)

#### Documentation
[...] `input` is interpreted as a one-sided Hermitian signal in the Fourier domain, as produced by rfft(). By the Hermitian property, the output will be real-valued.

* input (Tensor) - the input tensor representing a half-Hermitian signal
* n (int, optional) - Output signal length. This determines the length of the output signal. If given, the input will either be zero-padded or trimmed to this length before computing the real IFFT. Defaults to even output: n=2*(input.size(dim) - 1).
* dim (int, optional) - The dimension along which to take the one dimensional real IFFT.
* norm (str, optional) - Normalization mode. Calling the forward transform (rfft()) with the same normalization mode will apply an overall normalization of 1/n between the two transforms. This is required to make irfft() the exact inverse. For the backward transform (irfft()), these correspond to:
  * "forward" - no normalization
  * "backward" - normalize by 1/n
  * "ortho" - normalize by 1/sqrt(n) (making the real IFFT orthonormal)

#### Usage
https://github.com/pykeen/pykeen/blob/de817c54ba68f26fa14ce9333a81896e59481994/src/pykeen/nn/functional.py#L426-L427

#### Normalized Usage
Explicitly providing all default arguments
```
torch.fft.irfft(input=x, n=h.shape[-1], dim=-1, norm="backward")
```


### Conclusion
Same conclusion here: the `signal_ndim` argument has been removed in favour of having separate methods for 1-3D FFT. The new version always uses the `one_sided=True` option.