Here is updated code with a few more comments and print statements:
import torch
from torch.autograd import Function
from torch.autograd.gradcheck import gradcheck
from numpy import flip
import numpy as np
from scipy.signal import convolve2d, correlate2d
import torch.nn as nn
#from torch.nn.modules.module import Module
from torch.nn.parameter import Parameter

class ScipyConv2dFunction(Function):
  @staticmethod
  def forward(ctx, input, filter, bias):
    # detach so we can cast to NumPy
    print("printing input, filter, bias type before detach")
    print(type(input), type(filter), type(bias))
    input, filter, bias = input.detach(), filter.detach(), bias.detach()
    #result = correlate2d(input.cpu().numpy(), filter.cpu().numpy(), mode='valid')
    print("printing input, filter, bias type after detach")
    print(type(input), type(filter), type(bias))
    result = correlate2d(input.numpy(), filter.numpy(), mode='valid')
    #result += bias.cpu().numpy()
    result += bias.numpy()
    ctx.save_for_backward(input, filter, bias)
    return torch.as_tensor(result, dtype=input.dtype)

  @staticmethod
  def backward(ctx, grad_output):
    grad_output = grad_output.detach()
    input, filter, bias = ctx.saved_tensors
    grad_output = grad_output.cpu().numpy()
    grad_bias = np.sum(grad_output, keepdims=True)
    #grad_input = convolve2d(grad_output, filter.cpu().numpy(), mode='full')
    grad_input = convolve2d(grad_output, filter.numpy(), mode='full')
    # the previous line can be expressed equivalently as:
    # grad_input = correlate2d(grad_output, flip(flip(filter.numpy(), axis=0), axis=1), mode='full')
    #grad_filter = correlate2d(input.cpu().numpy(), grad_output, mode='valid')
    grad_filter = correlate2d(input.numpy(), grad_output, mode='valid')
    return torch.from_numpy(grad_input).to(device), torch.from_numpy(grad_filter).to(torch.float).to(device), torch.from_numpy(grad_bias).to(torch.float).to(device)
    #return torch.from_numpy(grad_input), torch.from_numpy(grad_filter).to(torch.float).to("cuda:0"), torch.from_numpy(grad_bias).to(torch.float)
    #return torch.from_numpy(grad_input), torch.from_numpy(grad_filter).to(torch.float).to("cpu"), torch.from_numpy(grad_bias).to(torch.float)

class ScipyConv2d(nn.Module):
  def __init__(self, filter_width, filter_height):
    super(ScipyConv2d, self).__init__()
    self.filter = Parameter(torch.randn(filter_width, filter_height))
    self.bias = Parameter(torch.randn(1, 1))
  def forward(self, input):
    return ScipyConv2dFunction.apply(input, self.filter, self.bias)

class DataParallelPassthrough(torch.nn.DataParallel):
  def __getattr__(self, name):
    try:
      return super().__getattr__(name)
    except AttributeError:
      return getattr(self.module, name)

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#Check to see if CUDA is there:
print("We have CUDA:")
print(torch.cuda.is_available())

net = ScipyConv2d(3, 3)
net = DataParallelPassthrough(net)
net.to(device)

print("Filter and bias: ", list(net.parameters()))
input = torch.randn(10, 10, requires_grad=True)
input = input.to(device)
output = net(input)
print("Output from the convolution: ", output)
output.backward(torch.randn(8, 8))
print("Gradient for the input map: ",input.grad)
print("The above code is a check. Now let's try something else...")

input = [torch.randn(20, 20, device=device, dtype=torch.double, requires_grad=True)]
output = net(input)
test = gradcheck(moduleConv, input, eps=1e-6, atol=1e-4)
print("Are the gradients correct: ", test)

The error code reads:
TypeError                                 Traceback (most recent call last)

<ipython-input-...> in <module>()
     68 input = torch.randn(10, 10, requires_grad=True)
     69 input = input.to(device)
---> 70 output = net(input)
     71 print("Output from the convolution: ", output)
     72 output.backward(torch.randn(8, 8))

/usr/local/lib/python3.6/dist-packages/torch/nn/modules/module.py in __call__(self, *input, **kwargs)
    548             result = self._slow_forward(*input, **kwargs)
    549         else:
--> 550             result = self.forward(*input, **kwargs)
    551         for hook in self._forward_hooks.values():
    552             hook_result = hook(self, input, result)

/usr/local/lib/python3.6/dist-packages/torch/nn/parallel/data_parallel.py in forward(self, *inputs, **kwargs)
    151         inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
    152         if len(self.device_ids) == 1:
--> 153             return self.module(*inputs[0], **kwargs[0])
    154         replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
    155         outputs = self.parallel_apply(replicas, inputs, kwargs)

/usr/local/lib/python3.6/dist-packages/torch/nn/modules/module.py in __call__(self, *input, **kwargs)
    548             result = self._slow_forward(*input, **kwargs)
    549         else:
--> 550             result = self.forward(*input, **kwargs)
    551         for hook in self._forward_hooks.values():
    552             hook_result = hook(self, input, result)

<ipython-input-...> in forward(self, input)
     47     self.bias = Parameter(torch.randn(1, 1))
     48   def forward(self, input):
---> 49     return ScipyConv2dFunction.apply(input, self.filter, self.bias)
     50 
     51 class DataParallelPassthrough(torch.nn.DataParallel):

<ipython-input-...> in forward(ctx, input, filter, bias)
     19     print("printing input, filter, bias type after detach")
     20     print(type(input), type(filter), type(bias))
---> 21     result = correlate2d(input.numpy(), filter.numpy(), mode='valid')
     22     #result += bias.cpu().numpy()
     23     result += bias.numpy()

TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

So line 21 is the problem.
So the use case is the following- I have a numpy function that takes in and spits out values which does operations that PyTorch can’t do. I’m trying to have PyTorch read in/do the ML forward pass/backprop on the Google Colab GPUs. I’m trying to move all of the computation off of the CPU and onto the GPUs and I was hoping that class DataParallelPassthrough would take care of the custom modules around the numpy/scipy function, however it’s not working. As a base case (and to reuse code that I know works) I’m just pasting together this and this.