Repository: pytorch
Issue: `F.interpolate` and `F.grid_sample` - documentation error and bug
Labels: module: docs,triaged

### 🐛 Describe the bug

`F.interpolate` and `F.grid_sample` are closely related as documented [here](https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html) and [here](https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html#torch.nn.functional.grid_sample) respectively.

I ran into two separate issues with them:
1. The documentation is wrong and states the exact opposite behavior/use case for the `align_corners` flag of the function,
2. In the volumetric case, there is a bug that causes resolution-independent sampling with trilinear interpolation not to work at all.

To demonstrate both, I created minimal reproducible test cases that should be ready to use or adapt if wanted:

```
#!/usr/bin/env python3
import unittest

import torch
from torch.nn import functional as F


class TestGridSampleAndInterpolate(unittest.TestCase):

    """
    Reproducer for grid sample and interpolation behavior.
    """

    def test_2d(self):
        """
        Demonstrate functionality in the 2D case.

        Everything works correctly (as far as I can tell), but the behavior
        is opposite to the documentation as stated on the website.
        """
        torch.random.manual_seed(42)
        # We create a tensor with minimal size to demonstrate.
        # It must be a 4D tensor (N, C, H, W) since we need at least two
        # spatial dimensions for the `grid_sample` function later on.
        test_tensor = torch.rand((1, 1, 2, 2))
        # We use the interpolate function as documented. For this behavior it does
        # not matter whether we use the `scale_factor` or `size` parameters since
        # we aim for a non-fractional upscaling anyways.
        # First, we demonstrate that the described behavior in the documentation that
        # states that this causes sampling coordinates to be "referring to the corner
        # points of the input’s corner pixels, making the sampling more resolution
        # agnostic". It should make it completely resolution agnostic...
        test_tensor_scaled = F.interpolate(
            test_tensor, scale_factor=2.0, mode="bilinear", align_corners=False
        )
        # Now we generate 100 random sample point coordinates.
        # They have to be scaled into the target coordinate system ranging from -1 to 1.
        coords_xy = torch.rand(100, 2) * 2.0 - 1.0
        # We now sample the grid in both resolution using the `grid_sample` function
        # as described in the documentation.
        sample_results_original = F.grid_sample(
            test_tensor.expand((100, 1, 2, 2)),
            coords_xy[:, None, None, :],
            mode="bilinear",
            align_corners=False,
        )
        # We do the same with the upscaled grid.
        sample_results_scaled = F.grid_sample(
            test_tensor_scaled.expand((100, 1, 4, 4)),
            coords_xy[:, None, None, :],
            mode="bilinear",
            align_corners=False,
        )
        # And the results _should_ match - but they don't by a large margin.
        self.assertGreater(
            (sample_results_original - sample_results_scaled).abs().max(), 1e-1
        )
        # However, if we do the exact opposite of what's stated in the documentation
        # and use `align_corners=True` everything works as expected.
        test_tensor_scaled = F.interpolate(
            test_tensor, scale_factor=2.0, mode="bilinear", align_corners=True
        )
        sample_results_original = F.grid_sample(
            test_tensor.expand((100, 1, 2, 2)),
            coords_xy[:, None, None, :],
            mode="bilinear",
            align_corners=True,
        )
        # We do the same with the upscaled grid.
        sample_results_scaled = F.grid_sample(
            test_tensor_scaled.expand((100, 1, 4, 4)),
            coords_xy[:, None, None, :],
            mode="bilinear",
            align_corners=True,
        )
        self.assertTrue(torch.allclose(sample_results_original, sample_results_scaled))

    def test_3d(self):
        """
        Demonstrate functionality in the 3D case.

        Now comes the really interesting part: this seems not to work in the 3D
        case at all (except potentially in the degenerate case where the volume
        is composed of equal slices, making it bilinear interpolation - I have
        not tested that sufficiently much, though). This is very important for
        correct volume upsampling.
        """
        torch.random.manual_seed(42)
        # We create a tensor with minimal size to demonstrate.
        # It must be a 5D tensor (N, C, H, W, D) since we need at least three
        # spatial dimensions for the `grid_sample` function later on.
        test_tensor = torch.rand((1, 1, 2, 2, 2))
        # We again generate 100 random sample point coordinates.
        # They have to be scaled into the target coordinate system ranging from -1 to 1.
        coords_xyz = torch.rand(100, 3) * 2.0 - 1.0
        # We use the interpolate function as documented. For this behavior it does
        # not matter whether we use the `scale_factor` or `size` parameters since
        # we aim for a non-fractional upscaling anyways.
        # Here, we look at both modes (`align_corners=True` and `align_corners=False`)
        # and show that they both do not work as intended and advertised.
        for align_corners in [True, False]:
            test_tensor_scaled = F.interpolate(
                test_tensor,
                scale_factor=2.0,
                mode="trilinear",
                align_corners=align_corners,
            )
            # We now sample the grid in both resolution using the `grid_sample` function
            # as described in the documentation.
            sample_results_original = F.grid_sample(
                test_tensor.expand((100, 1, 2, 2, 2)),
                coords_xyz[:, None, None, None, :],
                # Using 'bilinear' here since the documentation mentions that
                # it is the correct mode for actually trilinear interpolation
                # in the volumetric case.
                mode="bilinear",
                align_corners=align_corners,
            )
            # We do the same with the upscaled grid.
            sample_results_scaled = F.grid_sample(
                test_tensor_scaled.expand((100, 1, 4, 4, 4)),
                coords_xyz[:, None, None, None, :],
                mode="bilinear",
                align_corners=False,
            )
            # And the results _should_ match at least in one of the two cases - here
            # they never do.
            self.assertGreater(
                (sample_results_original - sample_results_scaled).abs().max(), 1e-1
            )


if __name__ == "__main__":
    unittest.main()
```

All these tests are consistently passing for me on PyTorch 1.13.1 (since I currently swapped conditions for the tests to show the _undesired_ instead of the desired behavior).

As shown in the 2D case, there is no reason that these interpolations / sampling operations should not be exact, the maximum error in 2D using `align_corners=True` usually has errors <1e-8.

**Is this an edge case?** Of course I did not start out trying this on 2x2(x2) sized volumes or images, I just found it to be a fairly minimal reproducer, but the same effects occur on larger images/volumes just the same and across the entire image/volume.

It would be great if you could have a look at this!

### Versions

PyTorch version: 1.13.1+cu117
Is debug build: False
CUDA used to build PyTorch: 11.7
ROCM used to build PyTorch: N/A

OS: Ubuntu 22.04.1 LTS (x86_64)
GCC version: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Clang version: Could not collect
CMake version: version 3.25.2
Libc version: glibc-2.35

Python version: 3.10.9 (main, Dec 16 2022, 10:01:32) [GCC 11.3.0] (64-bit runtime)
Python platform: Linux-5.15.90.1-microsoft-standard-WSL2-x86_64-with-glibc2.35
Is CUDA available: True
CUDA runtime version: 11.7.99
CUDA_MODULE_LOADING set to: LAZY
GPU models and configuration: GPU 0: NVIDIA RTX A6000
Nvidia driver version: 527.27
cuDNN version: Could not collect
HIP runtime version: N/A
MIOpen runtime version: N/A
Is XNNPACK available: True

CPU:
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Address sizes:                   48 bits physical, 48 bits virtual
Byte Order:                      Little Endian
CPU(s):                          64
On-line CPU(s) list:             0-63
Vendor ID:                       AuthenticAMD
Model name:                      AMD Ryzen Threadripper PRO 3995WX 64-Cores
CPU family:                      23
Model:                           49
Thread(s) per core:              2
Core(s) per socket:              32
Socket(s):                       1
Stepping:                        0
BogoMIPS:                        5389.87
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy svm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext perfctr_core ssbd ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves clzero xsaveerptr arat npt nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold v_vmsave_vmload umip rdpid
Virtualization:                  AMD-V
Hypervisor vendor:               Microsoft
Virtualization type:             full
L1d cache:                       1 MiB (32 instances)
L1i cache:                       1 MiB (32 instances)
L2 cache:                        16 MiB (32 instances)
L3 cache:                        16 MiB (1 instance)
Vulnerability Itlb multihit:     Not affected
Vulnerability L1tf:              Not affected
Vulnerability Mds:               Not affected
Vulnerability Meltdown:          Not affected
Vulnerability Mmio stale data:   Not affected
Vulnerability Retbleed:          Mitigation; untrained return thunk; SMT enabled with STIBP protection
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Retpolines, IBPB conditional, STIBP always-on, RSB filling, PBRSB-eIBRS Not affected
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected

Versions of relevant libraries:
[pip3] flake8==6.0.0
[pip3] mypy-extensions==1.0.0
[pip3] numpy==1.23.5
[pip3] pytorch-lightning==1.9.4
[pip3] torch==1.13.1
[pip3] torchaudio==0.13.1
[pip3] torchmetrics==0.11.4
[pip3] torchvision==0.14.1
[conda] Could not collect

cc @svekars @carljparker