We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[Bug] Performance varies according to the batch size
# 🐛 Bug

<!-- A clear and concise description of what the bug is. -->

The accuracy of the classifier trained with the codes (https://github.com/cornellius-gp/gpytorch/tree/master/examples/06_PyTorch_NN_Integration_DKL) varies according to the batch size. It should be consistent regardless of the batch size. Am I missing something?

## To reproduce

** Code snippet to reproduce **
```python
from torch.optim import SGD, Adam
from torch.optim.lr_scheduler import MultiStepLR
import torch.nn.functional as F
from torch import nn
import torch
import os
import torchvision.datasets as dset
import torchvision.transforms as transforms
import gpytorch
import math
import tqdm


normalize = transforms.Normalize(mean=[0.5071, 0.4867, 0.4408], std=[0.2675, 0.2565, 0.2761])
aug_trans = [transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip()]
common_trans = [transforms.ToTensor(), normalize]
train_compose = transforms.Compose(aug_trans + common_trans)
test_compose = transforms.Compose(common_trans)


dataset = "cifar10"


if ('CI' in os.environ):  # this is for running the notebook in our testing framework
    train_set = torch.utils.data.TensorDataset(torch.randn(8, 3, 32, 32), torch.rand(8).round().long())
    test_set = torch.utils.data.TensorDataset(torch.randn(4, 3, 32, 32), torch.rand(4).round().long())
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=4, shuffle=True)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=2, shuffle=False)
    num_classes = 2
elif dataset == 'cifar10':
    train_set = dset.CIFAR10('data', train=True, transform=train_compose, download=True)
    test_set = dset.CIFAR10('data', train=False, transform=test_compose)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=16, shuffle=False)
    num_classes = 10
elif dataset == 'cifar100':
    train_set = dset.CIFAR100('data', train=True, transform=train_compose, download=True)
    test_set = dset.CIFAR100('data', train=False, transform=test_compose)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=256, shuffle=False)
    num_classes = 100
else:
    raise RuntimeError('dataset must be one of "cifar100" or "cifar10"')

from densenet import DenseNet

class DenseNetFeatureExtractor(DenseNet):
    def forward(self, x):
        features = self.features(x)
        out = F.relu(features, inplace=True)
        out = F.avg_pool2d(out, kernel_size=self.avgpool_size).view(features.size(0), -1)
        return out

feature_extractor = DenseNetFeatureExtractor(block_config=(6, 6, 6), num_classes=num_classes)
num_features = feature_extractor.classifier.in_features

class GaussianProcessLayer(gpytorch.models.ApproximateGP):
    def __init__(self, num_dim, grid_bounds=(-10., 10.), grid_size=64):
        variational_distribution = gpytorch.variational.CholeskyVariationalDistribution(
            num_inducing_points=grid_size, batch_shape=torch.Size([num_dim])
        )

        # Our base variational strategy is a GridInterpolationVariationalStrategy,
        # which places variational inducing points on a Grid
        # We wrap it with a IndependentMultitaskVariationalStrategy so that our output is a vector-valued GP
        variational_strategy = gpytorch.variational.IndependentMultitaskVariationalStrategy(
            gpytorch.variational.GridInterpolationVariationalStrategy(
                self, grid_size=grid_size, grid_bounds=[grid_bounds],
                variational_distribution=variational_distribution,
            ), num_tasks=num_dim,
        )
        super().__init__(variational_strategy)

        self.covar_module = gpytorch.kernels.ScaleKernel(
            gpytorch.kernels.RBFKernel(
                lengthscale_prior=gpytorch.priors.SmoothedBoxPrior(
                    math.exp(-1), math.exp(1), sigma=0.1, transform=torch.exp
                )
            )
        )
        self.mean_module = gpytorch.means.ConstantMean()
        self.grid_bounds = grid_bounds

    def forward(self, x):
        mean = self.mean_module(x)
        covar = self.covar_module(x)
        return gpytorch.distributions.MultivariateNormal(mean, covar)


class DKLModel(gpytorch.Module):
    def __init__(self, feature_extractor, num_dim, grid_bounds=(-10., 10.)):
        super(DKLModel, self).__init__()
        self.feature_extractor = feature_extractor
        self.gp_layer = GaussianProcessLayer(num_dim=num_dim, grid_bounds=grid_bounds)
        self.grid_bounds = grid_bounds
        self.num_dim = num_dim

    def forward(self, x):
        features = self.feature_extractor(x)
        features = gpytorch.utils.grid.scale_to_bounds(features, self.grid_bounds[0], self.grid_bounds[1])
        # This next line makes it so that we learn a GP for each feature
        features = features.transpose(-1, -2).unsqueeze(-1)
        res = self.gp_layer(features)
        return res

model = DKLModel(feature_extractor, num_dim=num_features)
likelihood = gpytorch.likelihoods.SoftmaxLikelihood(num_features=model.num_dim, num_classes=num_classes)


# If you run this example without CUDA, I hope you like waiting!
if torch.cuda.is_available():
    model = model.cuda()
    likelihood = likelihood.cuda()

# load trained model
state_dict = torch.load('dkl_cifar_checkpoint.dat')
model_state_dict = state_dict['model']
likelihood_state_dict = state_dict['likelihood']

model.load_state_dict(model_state_dict)
likelihood.load_state_dict(likelihood_state_dict)

def test():
    model.eval()
    likelihood.eval()

    correct = 0
    with torch.no_grad(), gpytorch.settings.num_likelihood_samples(16):
        for data, target in test_loader:
            if torch.cuda.is_available():
                data, target = data.cuda(), target.cuda()
            output = likelihood(model(data))  # This gives us 16 samples from the predictive distribution
            pred = output.probs.mean(0).argmax(-1)  # Taking the mean over all of the sample we've drawn
            correct += pred.eq(target.view_as(pred)).cpu().sum()
    print('Test set: Accuracy: {}/{} ({}%)'.format(
        correct, len(test_loader.dataset), 100. * correct / float(len(test_loader.dataset))
    ))

with gpytorch.settings.use_toeplitz(False):
    test()

```

## Expected Behavior

<!-- A clear and concise description of what you expected to happen. -->
When the batch size is set to 256, the accuracy on the test set is 90.9% as expected. However, if the batch size is set to 16, the accuracy drops to 74.8%.


## System information

**Please complete the following information:**
- GPyTorch Version: 1.3.0
- PyTorch Version: 1.8.0.dev20201117
- Ubuntu 18.04

## Additional context
Add any other context about the problem here.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gpytorch/utils/grid.py`
Content:
```
1 #!/usr/bin/env python3
2 
3 import math
4 from typing import List, Tuple
5 
6 import torch
7 
8 
9 def scale_to_bounds(x, lower_bound, upper_bound):
10     """
11     Scale the input data so that it lies in between the lower and upper bounds.
12 
13     :param x: the input data
14     :type x: torch.Tensor (... x n x d)
15     :param float lower_bound: lower bound of scaled data
16     :param float upper_bound: upper bound of scaled data
17     :return: scaled data
18     :rtype: torch.Tensor (... x n x d)
19     """
20     # Scale features so they fit inside grid bounds
21     min_val = x.min()
22     max_val = x.max()
23     diff = max_val - min_val
24     x = (x - min_val) * (0.95 * (upper_bound - lower_bound) / diff) + 0.95 * lower_bound
25     return x
26 
27 
28 def choose_grid_size(train_inputs, ratio=1.0, kronecker_structure=True):
29     """
30     Given some training inputs, determine a good grid size for KISS-GP.
31 
32     :param x: the input data
33     :type x: torch.Tensor (... x n x d)
34     :param ratio: Amount of grid points per data point (default: 1.)
35     :type ratio: float, optional
36     :param kronecker_structure: Whether or not the model will use Kronecker structure in the grid
37         (set to True unless there is an additive or product decomposition in the prior)
38     :type kronecker_structure: bool, optional
39     :return: Grid size
40     :rtype: int
41     """
42     # Scale features so they fit inside grid bounds
43     num_data = train_inputs.numel() if train_inputs.dim() == 1 else train_inputs.size(-2)
44     num_dim = 1 if train_inputs.dim() == 1 else train_inputs.size(-1)
45     if kronecker_structure:
46         return int(ratio * math.pow(num_data, 1.0 / num_dim))
47     else:
48         return ratio * num_data
49 
50 
51 def convert_legacy_grid(grid: torch.Tensor) -> List[torch.Tensor]:
52     return [grid[:, i] for i in range(grid.size(-1))]
53 
54 
55 def create_data_from_grid(grid: List[torch.Tensor]) -> torch.Tensor:
56     """
57     :param grid: Each Tensor is a 1D set of increments for the grid in that dimension
58     :type grid: List[torch.Tensor]
59     :return: The set of points on the grid going by column-major order
60     :rtype: torch.Tensor
61     """
62     if torch.is_tensor(grid):
63         grid = convert_legacy_grid(grid)
64     ndims = len(grid)
65     assert all(axis.dim() == 1 for axis in grid)
66     projections = torch.meshgrid(*grid)
67     grid_tensor = torch.stack(projections, axis=-1)
68     # Note that if we did
69     #     grid_data = grid_tensor.reshape(-1, ndims)
70     # instead, we would be iterating through the points of our grid from the
71     # last data dimension to the first data dimension. However, due to legacy
72     # reasons, we need to iterate from the first data dimension to the last data
73     # dimension when creating grid_data
74     grid_data = grid_tensor.permute(*(reversed(range(ndims + 1)))).reshape(ndims, -1).transpose(0, 1)
75     return grid_data
76 
77 
78 def create_grid(
79     grid_sizes: List[int], grid_bounds: List[Tuple[float, float]], extend: bool = True, device="cpu", dtype=torch.float,
80 ) -> List[torch.Tensor]:
81     """
82     Creates a grid represented by a list of 1D Tensors representing the
83     projections of the grid into each dimension
84 
85     If `extend`, we extend the grid by two points past the specified boundary
86     which can be important for getting good grid interpolations.
87 
88     :param grid_sizes: Sizes of each grid dimension
89     :type grid_sizes: List[int]
90     :param grid_bounds: Lower and upper bounds of each grid dimension
91     :type grid_sizes: List[Tuple[float, float]]
92     :param device: target device for output (default: cpu)
93     :type device: torch.device, optional
94     :param dtype: target dtype for output (default: torch.float)
95     :type dtype: torch.dtype, optional
96     :return: Grid points for each dimension. Grid points are stored in a :obj:`torch.Tensor` with shape `grid_sizes[i]`.
97     :rtype: List[torch.Tensor]
98     """
99     grid = []
100     for i in range(len(grid_bounds)):
101         grid_diff = float(grid_bounds[i][1] - grid_bounds[i][0]) / (grid_sizes[i] - 2)
102         if extend:
103             proj = torch.linspace(
104                 grid_bounds[i][0] - grid_diff, grid_bounds[i][1] + grid_diff, grid_sizes[i], device=device, dtype=dtype,
105             )
106         else:
107             proj = torch.linspace(grid_bounds[i][0], grid_bounds[i][1], grid_sizes[i], device=device, dtype=dtype,)
108         grid.append(proj)
109     return grid
110
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gpytorch/utils/grid.py b/gpytorch/utils/grid.py
--- a/gpytorch/utils/grid.py
+++ b/gpytorch/utils/grid.py
@@ -1,15 +1,62 @@
 #!/usr/bin/env python3
 
 import math
+import warnings
 from typing import List, Tuple
 
 import torch
 
 
-def scale_to_bounds(x, lower_bound, upper_bound):
+class ScaleToBounds(torch.nn.Module):
     """
     Scale the input data so that it lies in between the lower and upper bounds.
 
+    In training (`self.train()`), this module adjusts the scaling factor to the minibatch of data.
+    During evaluation (`self.eval()`), this module uses the scaling factor from the previous minibatch of data.
+
+    :param float lower_bound: lower bound of scaled data
+    :param float upper_bound: upper bound of scaled data
+
+    Example:
+        >>> train_x = torch.randn(10, 5)
+        >>> module = gpytorch.utils.grid.ScaleToBounds(lower_bound=-1., upper_bound=1.)
+        >>>
+        >>> module.train()
+        >>> scaled_train_x = module(train_x)  # Data should be between -0.95 and 0.95
+        >>>
+        >>> module.eval()
+        >>> test_x = torch.randn(10, 5)
+        >>> scaled_test_x = module(test_x)  # Scaling is based on train_x
+    """
+
+    def __init__(self, lower_bound, upper_bound):
+        super().__init__()
+        self.lower_bound = float(lower_bound)
+        self.upper_bound = float(upper_bound)
+        self.register_buffer("min_val", torch.tensor(lower_bound))
+        self.register_buffer("max_val", torch.tensor(upper_bound))
+
+    def forward(self, x):
+        if self.training:
+            min_val = x.min()
+            max_val = x.max()
+            self.min_val.data = min_val
+            self.max_val.data = max_val
+        else:
+            min_val = self.min_val
+            max_val = self.max_val
+            # Clamp extreme values
+            x = x.clamp(min_val, max_val)
+
+        diff = max_val - min_val
+        x = (x - min_val) * (0.95 * (self.upper_bound - self.lower_bound) / diff) + 0.95 * self.lower_bound
+        return x
+
+
+def scale_to_bounds(x, lower_bound, upper_bound):
+    """
+    DEPRECATRED: Use :obj:`~gpytorch.utils.grid.ScaleToBounds` instead.
+
     :param x: the input data
     :type x: torch.Tensor (... x n x d)
     :param float lower_bound: lower bound of scaled data
@@ -17,6 +64,10 @@
     :return: scaled data
     :rtype: torch.Tensor (... x n x d)
     """
+    warnings.warn(
+        "The `scale_to_bounds` method is deprecated. Use the `gpytorch.utils.grid.ScaleToBounds` module instead.",
+        DeprecationWarning,
+    )
     # Scale features so they fit inside grid bounds
     min_val = x.min()
     max_val = x.max()

{"golden_diff": "diff --git a/gpytorch/utils/grid.py b/gpytorch/utils/grid.py\n--- a/gpytorch/utils/grid.py\n+++ b/gpytorch/utils/grid.py\n@@ -1,15 +1,62 @@\n #!/usr/bin/env python3\n \n import math\n+import warnings\n from typing import List, Tuple\n \n import torch\n \n \n-def scale_to_bounds(x, lower_bound, upper_bound):\n+class ScaleToBounds(torch.nn.Module):\n     \"\"\"\n     Scale the input data so that it lies in between the lower and upper bounds.\n \n+    In training (`self.train()`), this module adjusts the scaling factor to the minibatch of data.\n+    During evaluation (`self.eval()`), this module uses the scaling factor from the previous minibatch of data.\n+\n+    :param float lower_bound: lower bound of scaled data\n+    :param float upper_bound: upper bound of scaled data\n+\n+    Example:\n+        >>> train_x = torch.randn(10, 5)\n+        >>> module = gpytorch.utils.grid.ScaleToBounds(lower_bound=-1., upper_bound=1.)\n+        >>>\n+        >>> module.train()\n+        >>> scaled_train_x = module(train_x)  # Data should be between -0.95 and 0.95\n+        >>>\n+        >>> module.eval()\n+        >>> test_x = torch.randn(10, 5)\n+        >>> scaled_test_x = module(test_x)  # Scaling is based on train_x\n+    \"\"\"\n+\n+    def __init__(self, lower_bound, upper_bound):\n+        super().__init__()\n+        self.lower_bound = float(lower_bound)\n+        self.upper_bound = float(upper_bound)\n+        self.register_buffer(\"min_val\", torch.tensor(lower_bound))\n+        self.register_buffer(\"max_val\", torch.tensor(upper_bound))\n+\n+    def forward(self, x):\n+        if self.training:\n+            min_val = x.min()\n+            max_val = x.max()\n+            self.min_val.data = min_val\n+            self.max_val.data = max_val\n+        else:\n+            min_val = self.min_val\n+            max_val = self.max_val\n+            # Clamp extreme values\n+            x = x.clamp(min_val, max_val)\n+\n+        diff = max_val - min_val\n+        x = (x - min_val) * (0.95 * (self.upper_bound - self.lower_bound) / diff) + 0.95 * self.lower_bound\n+        return x\n+\n+\n+def scale_to_bounds(x, lower_bound, upper_bound):\n+    \"\"\"\n+    DEPRECATRED: Use :obj:`~gpytorch.utils.grid.ScaleToBounds` instead.\n+\n     :param x: the input data\n     :type x: torch.Tensor (... x n x d)\n     :param float lower_bound: lower bound of scaled data\n@@ -17,6 +64,10 @@\n     :return: scaled data\n     :rtype: torch.Tensor (... x n x d)\n     \"\"\"\n+    warnings.warn(\n+        \"The `scale_to_bounds` method is deprecated. Use the `gpytorch.utils.grid.ScaleToBounds` module instead.\",\n+        DeprecationWarning,\n+    )\n     # Scale features so they fit inside grid bounds\n     min_val = x.min()\n     max_val = x.max()\n", "issue": "[Bug] Performance varies according to the batch size\n# \ud83d\udc1b Bug\r\n\r\n<!-- A clear and concise description of what the bug is. -->\r\n\r\nThe accuracy of the classifier trained with the codes (https://github.com/cornellius-gp/gpytorch/tree/master/examples/06_PyTorch_NN_Integration_DKL) varies according to the batch size. It should be consistent regardless of the batch size. Am I missing something?\r\n\r\n## To reproduce\r\n\r\n** Code snippet to reproduce **\r\n```python\r\nfrom torch.optim import SGD, Adam\r\nfrom torch.optim.lr_scheduler import MultiStepLR\r\nimport torch.nn.functional as F\r\nfrom torch import nn\r\nimport torch\r\nimport os\r\nimport torchvision.datasets as dset\r\nimport torchvision.transforms as transforms\r\nimport gpytorch\r\nimport math\r\nimport tqdm\r\n\r\n\r\nnormalize = transforms.Normalize(mean=[0.5071, 0.4867, 0.4408], std=[0.2675, 0.2565, 0.2761])\r\naug_trans = [transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip()]\r\ncommon_trans = [transforms.ToTensor(), normalize]\r\ntrain_compose = transforms.Compose(aug_trans + common_trans)\r\ntest_compose = transforms.Compose(common_trans)\r\n\r\n\r\ndataset = \"cifar10\"\r\n\r\n\r\nif ('CI' in os.environ):  # this is for running the notebook in our testing framework\r\n    train_set = torch.utils.data.TensorDataset(torch.randn(8, 3, 32, 32), torch.rand(8).round().long())\r\n    test_set = torch.utils.data.TensorDataset(torch.randn(4, 3, 32, 32), torch.rand(4).round().long())\r\n    train_loader = torch.utils.data.DataLoader(train_set, batch_size=4, shuffle=True)\r\n    test_loader = torch.utils.data.DataLoader(test_set, batch_size=2, shuffle=False)\r\n    num_classes = 2\r\nelif dataset == 'cifar10':\r\n    train_set = dset.CIFAR10('data', train=True, transform=train_compose, download=True)\r\n    test_set = dset.CIFAR10('data', train=False, transform=test_compose)\r\n    train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)\r\n    test_loader = torch.utils.data.DataLoader(test_set, batch_size=16, shuffle=False)\r\n    num_classes = 10\r\nelif dataset == 'cifar100':\r\n    train_set = dset.CIFAR100('data', train=True, transform=train_compose, download=True)\r\n    test_set = dset.CIFAR100('data', train=False, transform=test_compose)\r\n    train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)\r\n    test_loader = torch.utils.data.DataLoader(test_set, batch_size=256, shuffle=False)\r\n    num_classes = 100\r\nelse:\r\n    raise RuntimeError('dataset must be one of \"cifar100\" or \"cifar10\"')\r\n\r\nfrom densenet import DenseNet\r\n\r\nclass DenseNetFeatureExtractor(DenseNet):\r\n    def forward(self, x):\r\n        features = self.features(x)\r\n        out = F.relu(features, inplace=True)\r\n        out = F.avg_pool2d(out, kernel_size=self.avgpool_size).view(features.size(0), -1)\r\n        return out\r\n\r\nfeature_extractor = DenseNetFeatureExtractor(block_config=(6, 6, 6), num_classes=num_classes)\r\nnum_features = feature_extractor.classifier.in_features\r\n\r\nclass GaussianProcessLayer(gpytorch.models.ApproximateGP):\r\n    def __init__(self, num_dim, grid_bounds=(-10., 10.), grid_size=64):\r\n        variational_distribution = gpytorch.variational.CholeskyVariationalDistribution(\r\n            num_inducing_points=grid_size, batch_shape=torch.Size([num_dim])\r\n        )\r\n\r\n        # Our base variational strategy is a GridInterpolationVariationalStrategy,\r\n        # which places variational inducing points on a Grid\r\n        # We wrap it with a IndependentMultitaskVariationalStrategy so that our output is a vector-valued GP\r\n        variational_strategy = gpytorch.variational.IndependentMultitaskVariationalStrategy(\r\n            gpytorch.variational.GridInterpolationVariationalStrategy(\r\n                self, grid_size=grid_size, grid_bounds=[grid_bounds],\r\n                variational_distribution=variational_distribution,\r\n            ), num_tasks=num_dim,\r\n        )\r\n        super().__init__(variational_strategy)\r\n\r\n        self.covar_module = gpytorch.kernels.ScaleKernel(\r\n            gpytorch.kernels.RBFKernel(\r\n                lengthscale_prior=gpytorch.priors.SmoothedBoxPrior(\r\n                    math.exp(-1), math.exp(1), sigma=0.1, transform=torch.exp\r\n                )\r\n            )\r\n        )\r\n        self.mean_module = gpytorch.means.ConstantMean()\r\n        self.grid_bounds = grid_bounds\r\n\r\n    def forward(self, x):\r\n        mean = self.mean_module(x)\r\n        covar = self.covar_module(x)\r\n        return gpytorch.distributions.MultivariateNormal(mean, covar)\r\n\r\n\r\nclass DKLModel(gpytorch.Module):\r\n    def __init__(self, feature_extractor, num_dim, grid_bounds=(-10., 10.)):\r\n        super(DKLModel, self).__init__()\r\n        self.feature_extractor = feature_extractor\r\n        self.gp_layer = GaussianProcessLayer(num_dim=num_dim, grid_bounds=grid_bounds)\r\n        self.grid_bounds = grid_bounds\r\n        self.num_dim = num_dim\r\n\r\n    def forward(self, x):\r\n        features = self.feature_extractor(x)\r\n        features = gpytorch.utils.grid.scale_to_bounds(features, self.grid_bounds[0], self.grid_bounds[1])\r\n        # This next line makes it so that we learn a GP for each feature\r\n        features = features.transpose(-1, -2).unsqueeze(-1)\r\n        res = self.gp_layer(features)\r\n        return res\r\n\r\nmodel = DKLModel(feature_extractor, num_dim=num_features)\r\nlikelihood = gpytorch.likelihoods.SoftmaxLikelihood(num_features=model.num_dim, num_classes=num_classes)\r\n\r\n\r\n# If you run this example without CUDA, I hope you like waiting!\r\nif torch.cuda.is_available():\r\n    model = model.cuda()\r\n    likelihood = likelihood.cuda()\r\n\r\n# load trained model\r\nstate_dict = torch.load('dkl_cifar_checkpoint.dat')\r\nmodel_state_dict = state_dict['model']\r\nlikelihood_state_dict = state_dict['likelihood']\r\n\r\nmodel.load_state_dict(model_state_dict)\r\nlikelihood.load_state_dict(likelihood_state_dict)\r\n\r\ndef test():\r\n    model.eval()\r\n    likelihood.eval()\r\n\r\n    correct = 0\r\n    with torch.no_grad(), gpytorch.settings.num_likelihood_samples(16):\r\n        for data, target in test_loader:\r\n            if torch.cuda.is_available():\r\n                data, target = data.cuda(), target.cuda()\r\n            output = likelihood(model(data))  # This gives us 16 samples from the predictive distribution\r\n            pred = output.probs.mean(0).argmax(-1)  # Taking the mean over all of the sample we've drawn\r\n            correct += pred.eq(target.view_as(pred)).cpu().sum()\r\n    print('Test set: Accuracy: {}/{} ({}%)'.format(\r\n        correct, len(test_loader.dataset), 100. * correct / float(len(test_loader.dataset))\r\n    ))\r\n\r\nwith gpytorch.settings.use_toeplitz(False):\r\n    test()\r\n\r\n```\r\n\r\n## Expected Behavior\r\n\r\n<!-- A clear and concise description of what you expected to happen. -->\r\nWhen the batch size is set to 256, the accuracy on the test set is 90.9% as expected. However, if the batch size is set to 16, the accuracy drops to 74.8%.\r\n\r\n\r\n## System information\r\n\r\n**Please complete the following information:**\r\n- GPyTorch Version: 1.3.0\r\n- PyTorch Version: 1.8.0.dev20201117\r\n- Ubuntu 18.04\r\n\r\n## Additional context\r\nAdd any other context about the problem here.\r\n\n", "before_files": [{"content": "#!/usr/bin/env python3\n\nimport math\nfrom typing import List, Tuple\n\nimport torch\n\n\ndef scale_to_bounds(x, lower_bound, upper_bound):\n    \"\"\"\n    Scale the input data so that it lies in between the lower and upper bounds.\n\n    :param x: the input data\n    :type x: torch.Tensor (... x n x d)\n    :param float lower_bound: lower bound of scaled data\n    :param float upper_bound: upper bound of scaled data\n    :return: scaled data\n    :rtype: torch.Tensor (... x n x d)\n    \"\"\"\n    # Scale features so they fit inside grid bounds\n    min_val = x.min()\n    max_val = x.max()\n    diff = max_val - min_val\n    x = (x - min_val) * (0.95 * (upper_bound - lower_bound) / diff) + 0.95 * lower_bound\n    return x\n\n\ndef choose_grid_size(train_inputs, ratio=1.0, kronecker_structure=True):\n    \"\"\"\n    Given some training inputs, determine a good grid size for KISS-GP.\n\n    :param x: the input data\n    :type x: torch.Tensor (... x n x d)\n    :param ratio: Amount of grid points per data point (default: 1.)\n    :type ratio: float, optional\n    :param kronecker_structure: Whether or not the model will use Kronecker structure in the grid\n        (set to True unless there is an additive or product decomposition in the prior)\n    :type kronecker_structure: bool, optional\n    :return: Grid size\n    :rtype: int\n    \"\"\"\n    # Scale features so they fit inside grid bounds\n    num_data = train_inputs.numel() if train_inputs.dim() == 1 else train_inputs.size(-2)\n    num_dim = 1 if train_inputs.dim() == 1 else train_inputs.size(-1)\n    if kronecker_structure:\n        return int(ratio * math.pow(num_data, 1.0 / num_dim))\n    else:\n        return ratio * num_data\n\n\ndef convert_legacy_grid(grid: torch.Tensor) -> List[torch.Tensor]:\n    return [grid[:, i] for i in range(grid.size(-1))]\n\n\ndef create_data_from_grid(grid: List[torch.Tensor]) -> torch.Tensor:\n    \"\"\"\n    :param grid: Each Tensor is a 1D set of increments for the grid in that dimension\n    :type grid: List[torch.Tensor]\n    :return: The set of points on the grid going by column-major order\n    :rtype: torch.Tensor\n    \"\"\"\n    if torch.is_tensor(grid):\n        grid = convert_legacy_grid(grid)\n    ndims = len(grid)\n    assert all(axis.dim() == 1 for axis in grid)\n    projections = torch.meshgrid(*grid)\n    grid_tensor = torch.stack(projections, axis=-1)\n    # Note that if we did\n    #     grid_data = grid_tensor.reshape(-1, ndims)\n    # instead, we would be iterating through the points of our grid from the\n    # last data dimension to the first data dimension. However, due to legacy\n    # reasons, we need to iterate from the first data dimension to the last data\n    # dimension when creating grid_data\n    grid_data = grid_tensor.permute(*(reversed(range(ndims + 1)))).reshape(ndims, -1).transpose(0, 1)\n    return grid_data\n\n\ndef create_grid(\n    grid_sizes: List[int], grid_bounds: List[Tuple[float, float]], extend: bool = True, device=\"cpu\", dtype=torch.float,\n) -> List[torch.Tensor]:\n    \"\"\"\n    Creates a grid represented by a list of 1D Tensors representing the\n    projections of the grid into each dimension\n\n    If `extend`, we extend the grid by two points past the specified boundary\n    which can be important for getting good grid interpolations.\n\n    :param grid_sizes: Sizes of each grid dimension\n    :type grid_sizes: List[int]\n    :param grid_bounds: Lower and upper bounds of each grid dimension\n    :type grid_sizes: List[Tuple[float, float]]\n    :param device: target device for output (default: cpu)\n    :type device: torch.device, optional\n    :param dtype: target dtype for output (default: torch.float)\n    :type dtype: torch.dtype, optional\n    :return: Grid points for each dimension. Grid points are stored in a :obj:`torch.Tensor` with shape `grid_sizes[i]`.\n    :rtype: List[torch.Tensor]\n    \"\"\"\n    grid = []\n    for i in range(len(grid_bounds)):\n        grid_diff = float(grid_bounds[i][1] - grid_bounds[i][0]) / (grid_sizes[i] - 2)\n        if extend:\n            proj = torch.linspace(\n                grid_bounds[i][0] - grid_diff, grid_bounds[i][1] + grid_diff, grid_sizes[i], device=device, dtype=dtype,\n            )\n        else:\n            proj = torch.linspace(grid_bounds[i][0], grid_bounds[i][1], grid_sizes[i], device=device, dtype=dtype,)\n        grid.append(proj)\n    return grid\n", "path": "gpytorch/utils/grid.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n\nimport math\nimport warnings\nfrom typing import List, Tuple\n\nimport torch\n\n\nclass ScaleToBounds(torch.nn.Module):\n    \"\"\"\n    Scale the input data so that it lies in between the lower and upper bounds.\n\n    In training (`self.train()`), this module adjusts the scaling factor to the minibatch of data.\n    During evaluation (`self.eval()`), this module uses the scaling factor from the previous minibatch of data.\n\n    :param float lower_bound: lower bound of scaled data\n    :param float upper_bound: upper bound of scaled data\n\n    Example:\n        >>> train_x = torch.randn(10, 5)\n        >>> module = gpytorch.utils.grid.ScaleToBounds(lower_bound=-1., upper_bound=1.)\n        >>>\n        >>> module.train()\n        >>> scaled_train_x = module(train_x)  # Data should be between -0.95 and 0.95\n        >>>\n        >>> module.eval()\n        >>> test_x = torch.randn(10, 5)\n        >>> scaled_test_x = module(test_x)  # Scaling is based on train_x\n    \"\"\"\n\n    def __init__(self, lower_bound, upper_bound):\n        super().__init__()\n        self.lower_bound = float(lower_bound)\n        self.upper_bound = float(upper_bound)\n        self.register_buffer(\"min_val\", torch.tensor(lower_bound))\n        self.register_buffer(\"max_val\", torch.tensor(upper_bound))\n\n    def forward(self, x):\n        if self.training:\n            min_val = x.min()\n            max_val = x.max()\n            self.min_val.data = min_val\n            self.max_val.data = max_val\n        else:\n            min_val = self.min_val\n            max_val = self.max_val\n            # Clamp extreme values\n            x = x.clamp(min_val, max_val)\n\n        diff = max_val - min_val\n        x = (x - min_val) * (0.95 * (self.upper_bound - self.lower_bound) / diff) + 0.95 * self.lower_bound\n        return x\n\n\ndef scale_to_bounds(x, lower_bound, upper_bound):\n    \"\"\"\n    DEPRECATRED: Use :obj:`~gpytorch.utils.grid.ScaleToBounds` instead.\n\n    :param x: the input data\n    :type x: torch.Tensor (... x n x d)\n    :param float lower_bound: lower bound of scaled data\n    :param float upper_bound: upper bound of scaled data\n    :return: scaled data\n    :rtype: torch.Tensor (... x n x d)\n    \"\"\"\n    warnings.warn(\n        \"The `scale_to_bounds` method is deprecated. Use the `gpytorch.utils.grid.ScaleToBounds` module instead.\",\n        DeprecationWarning,\n    )\n    # Scale features so they fit inside grid bounds\n    min_val = x.min()\n    max_val = x.max()\n    diff = max_val - min_val\n    x = (x - min_val) * (0.95 * (upper_bound - lower_bound) / diff) + 0.95 * lower_bound\n    return x\n\n\ndef choose_grid_size(train_inputs, ratio=1.0, kronecker_structure=True):\n    \"\"\"\n    Given some training inputs, determine a good grid size for KISS-GP.\n\n    :param x: the input data\n    :type x: torch.Tensor (... x n x d)\n    :param ratio: Amount of grid points per data point (default: 1.)\n    :type ratio: float, optional\n    :param kronecker_structure: Whether or not the model will use Kronecker structure in the grid\n        (set to True unless there is an additive or product decomposition in the prior)\n    :type kronecker_structure: bool, optional\n    :return: Grid size\n    :rtype: int\n    \"\"\"\n    # Scale features so they fit inside grid bounds\n    num_data = train_inputs.numel() if train_inputs.dim() == 1 else train_inputs.size(-2)\n    num_dim = 1 if train_inputs.dim() == 1 else train_inputs.size(-1)\n    if kronecker_structure:\n        return int(ratio * math.pow(num_data, 1.0 / num_dim))\n    else:\n        return ratio * num_data\n\n\ndef convert_legacy_grid(grid: torch.Tensor) -> List[torch.Tensor]:\n    return [grid[:, i] for i in range(grid.size(-1))]\n\n\ndef create_data_from_grid(grid: List[torch.Tensor]) -> torch.Tensor:\n    \"\"\"\n    :param grid: Each Tensor is a 1D set of increments for the grid in that dimension\n    :type grid: List[torch.Tensor]\n    :return: The set of points on the grid going by column-major order\n    :rtype: torch.Tensor\n    \"\"\"\n    if torch.is_tensor(grid):\n        grid = convert_legacy_grid(grid)\n    ndims = len(grid)\n    assert all(axis.dim() == 1 for axis in grid)\n    projections = torch.meshgrid(*grid)\n    grid_tensor = torch.stack(projections, axis=-1)\n    # Note that if we did\n    #     grid_data = grid_tensor.reshape(-1, ndims)\n    # instead, we would be iterating through the points of our grid from the\n    # last data dimension to the first data dimension. However, due to legacy\n    # reasons, we need to iterate from the first data dimension to the last data\n    # dimension when creating grid_data\n    grid_data = grid_tensor.permute(*(reversed(range(ndims + 1)))).reshape(ndims, -1).transpose(0, 1)\n    return grid_data\n\n\ndef create_grid(\n    grid_sizes: List[int], grid_bounds: List[Tuple[float, float]], extend: bool = True, device=\"cpu\", dtype=torch.float,\n) -> List[torch.Tensor]:\n    \"\"\"\n    Creates a grid represented by a list of 1D Tensors representing the\n    projections of the grid into each dimension\n\n    If `extend`, we extend the grid by two points past the specified boundary\n    which can be important for getting good grid interpolations.\n\n    :param grid_sizes: Sizes of each grid dimension\n    :type grid_sizes: List[int]\n    :param grid_bounds: Lower and upper bounds of each grid dimension\n    :type grid_sizes: List[Tuple[float, float]]\n    :param device: target device for output (default: cpu)\n    :type device: torch.device, optional\n    :param dtype: target dtype for output (default: torch.float)\n    :type dtype: torch.dtype, optional\n    :return: Grid points for each dimension. Grid points are stored in a :obj:`torch.Tensor` with shape `grid_sizes[i]`.\n    :rtype: List[torch.Tensor]\n    \"\"\"\n    grid = []\n    for i in range(len(grid_bounds)):\n        grid_diff = float(grid_bounds[i][1] - grid_bounds[i][0]) / (grid_sizes[i] - 2)\n        if extend:\n            proj = torch.linspace(\n                grid_bounds[i][0] - grid_diff, grid_bounds[i][1] + grid_diff, grid_sizes[i], device=device, dtype=dtype,\n            )\n        else:\n            proj = torch.linspace(grid_bounds[i][0], grid_bounds[i][1], grid_sizes[i], device=device, dtype=dtype,)\n        grid.append(proj)\n    return grid\n", "path": "gpytorch/utils/grid.py"}]}