I think .data is deprecated, so without it, neural network will work.
so,
w1.data = w1.data - 0.01 * w1.grad.item()
w1.grad.data.zero_()

becomes
w1 = w1 - 0.01 * w1.grad.item()
w1.grad.zero_()

also, I think we should use in-place, otherwise a new w1 would be created, and its gradient is None, so zeroing it out is an invalid operation, for example,
w1 = w1 - 0.01 * w1.grad.item()
w1.grad.zero_()

will give error,
AttributeError: 'NoneType' object has no attribute 'zero_'

as we created new w1, for which grad is None.
so, we use in-place,
w1 -= 0.01 * w1.grad.item()
w1.grad.zero_()

this way, new w1 will not be created.
also, if we modify w1 like this, then it will give error,
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.

so, we use
with torch.no_grad():
  w1 -= 0.01 * w1.grad*item()
  w1.grad.zero_()

this will ensure that these computations have required_grad=False, even when for w1 requires_grad is set as True
also, if you use square in your loss, then it gives big values, as listed in your post, so if we use only difference, and use absolute value of it, then it will start working, for example,
# Loss function
def loss(y_pred, y_val):
    return (y_pred - y_val).abs()

when I make these changes, and train for 1000 epochs, then I get
Prediction (after training) 4 43.99794006347656