Hi! I have a very simple example Module below, where I have a circular buffer, and attempt to learn a _delay parameter (i.e. distance between a read and write pointer) that transforms an input sin wave into a phase-shifted and zero-padded output. The goal is to emulate a delay line (as in DSP), learning an unknown delay parameter given some arbitrary input and reference output.
Currently this gives errors that the element 0 of tensors does not require grad and does not have a grad_fn - maybe, the problem (as represented) isn’t differentiable?
Any advice as to why this doesn’t work would be appreciate, and practical suggestions for representing this problem in PyTorch! Thanks 
Error:
Traceback (most recent call last):
  File "delay_eg.py", line 59, in <module>
    loss.backward()
  File "venv391/lib/python3.9/site-packages/torch/_tensor.py", line 307, in backward
    torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
  File "venv391/lib/python3.9/site-packages/torch/autograd/__init__.py", line 154, in backward
    Variable._execution_engine.run_backward(
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

Re-producible code (note that the read pointer is fractional!):
import numpy as np
import torch

class Delay(torch.nn.Module):

    def __init__(self, N=44100, offset=1):
        super().__init__()
        self.N = N
        self.register_buffer('circ_buffer', torch.zeros(N, 1, dtype=torch.float))
        self._delay = torch.nn.Parameter(torch.FloatTensor([offset]))
        self.reset_state()

    def reset_state(self):
        self.circ_buffer[self.circ_buffer > 0.0] = 0.0
        self.circ_buffer[self.circ_buffer < 0.0] = 0.0
        self._read_ptr = 0.0
        self._write_ptr = int(self._delay.item())

    def forward(self, x):
        y = torch.zeros(*x.shape, dtype=torch.float).to(x.device)
        for i in range(x.shape[0]):
            x_i = x[i].item()
            # Write
            self.circ_buffer[self._write_ptr] = x_i
            # Read (with linear interpolation)
            i0 = np.floor(self._read_ptr).astype(int)
            i1 = (i0 + 1) % self.N
            frac = self._read_ptr - np.floor(self._read_ptr)
            y[i] = self.circ_buffer[i0] + frac*(self.circ_buffer[i1]-self.circ_buffer[i0])
            # Pointers
            self._write_ptr += 1
            if self._write_ptr == self.N:
                self._write_ptr = 0
            self._read_ptr += 1
            if self._read_ptr >= self.N:
                self._read_ptr -= self.N
        return y

if __name__ == "__main__":
    device = torch.device("cpu")

    # Simple sin wave
    ts = np.arange(0.0, 0.3, 1/44100)
    xs = np.sin(2*np.pi*5*ts + 0)
    # Phase shift, with zero padding
    delay = int(xs.shape[0]/1.31)
    ys = np.concatenate([np.zeros(xs.shape[0]-delay), xs[:delay]])

    model = Delay().to(device)
    learning_rate = 1e-3
    optimizer = torch.optim.Adam(params=model.parameters(), lr=learning_rate)
    device = next(model.parameters()).device
    loss_fn = torch.nn.MSELoss()

    for _ in range(100):
        xst = torch.tensor(xs).float().to(device)
        yst = torch.tensor(ys).float().to(device)
        model.reset_state()
        optimizer.zero_grad()
        loss = loss_fn(model(xst), yst)
        loss.backward()
        optimizer.step()
        print(loss.item())