Hi,
That looks mostly good.
A few updates: the backward is not differentiable (as it used numpy) so you can mark it as such. The backward should return as many values as the forward’s inputs. If an input is not differentiable, it should return None for it. You can use the gradcheck tool we have to check gradient via finite difference (if your function is smooth and differentiable).
That would look like:
import torch

from torch.autograd import Function, gradcheck
from torch.autograd.function import once_differentiable

class SPLFunction(Function):
   
    @staticmethod
    def forward(ctx, bin, x):
        ctx.save_for_backward(bin, x)

        x = x.detach().numpy()
        bin = bin.detach().numpy()
        
        bin_steps = np.arange(2, 22+20/63, 20/63)[:-1]
        spline_curve = UnivariateSpline(bin_steps, bin, ext=3)
        spline_curve.set_smoothing_factor(0.5)
        
        result = spline_curve(x)
        return torch.as_tensor(result)

    @once_differentiable
    @staticmethod
    def backward(ctx, grad_output):
        bin, x = ctx.saved_tensors

        bin_steps = np.arange(2, 22+20/63, 20/63)[:-1]
        spline_curve = UnivariateSpline(bin_steps, bin, ext=3)
        spline_curve.set_smoothing_factor(0.5)
        
        df = spline_curve.derivative(x)
        df = torch.from_numpy(df*grad_output)

        return None, df # Bin is not differentiable?

def apply_spline(bin, x):
    return SPLFunction.apply(bin, x)

gradcheck(apply_spline, (example_bin, example_x))