# simple tests
import unittest
import torch
import numpy as np
from tinygrad.helpers import getenv, GlobalCounters
if getenv("TINY_BACKEND2"):
  import extra.torch_backend.backend2
  device = "cpu"
else:
  import extra.torch_backend.backend
  device = "tiny"

class TestTorchBackend(unittest.TestCase):
  def test_randperm_generator_out(self):
    n = 10
    out = torch.empty(n, dtype=torch.long, device=device)
    res = torch.randperm(n, out=out).cpu().numpy()
    np.testing.assert_equal(set(res), set(range(n)))
    np.testing.assert_equal(out.cpu().numpy(), res)

    res2 = torch.randperm(n).cpu().numpy()
    np.testing.assert_equal(set(res2), set(range(n)))

  def test_numpy_ones(self):
    a = torch.ones(4, device=device)
    np.testing.assert_equal(a.cpu().numpy(), [1,1,1,1])

  def test_numpy_ones_int32(self):
    a = torch.ones(4, dtype=torch.int32, device=device)
    assert a.dtype == torch.int32
    np.testing.assert_equal(a.cpu().numpy(), [1,1,1,1])

  def test_plus(self):
    a = torch.ones(4, device=device)
    b = torch.ones(4, device=device)
    c = a+b
    np.testing.assert_equal(c.cpu().numpy(), [2,2,2,2])

  def test_expand(self):
    a = torch.Tensor([1,2,3,4]).to(device)
    out = a.reshape(4,1).expand(4,4)
    np.testing.assert_equal(out.cpu().numpy(), [[1,1,1,1],[2,2,2,2],[3,3,3,3],[4,4,4,4]])

  def test_reshape(self):
    a = torch.Tensor([[1,2],[3,4]]).to(device)
    np.testing.assert_equal(a.reshape(4).cpu().numpy(), [1,2,3,4])
    np.testing.assert_equal(a.reshape(2,1,2).cpu().numpy(), [[[1,2]],[[3,4]]])
    np.testing.assert_equal(a.unsqueeze(1).cpu().numpy(), [[[1,2]],[[3,4]]])
    np.testing.assert_equal(a.unsqueeze(1).unsqueeze(1).cpu().numpy(), [[[[1,2]]],[[[3,4]]]])
    np.testing.assert_equal(a.unsqueeze(1).unsqueeze(1).squeeze().cpu().numpy(), [[1,2],[3,4]])

  def test_permute(self):
    a = torch.Tensor([[1,2],[3,4]]).to(device)
    print(a.stride())
    null = a.permute(0,1)
    perm = a.permute(1,0)
    back = perm.permute(1,0)
    np.testing.assert_equal(a.cpu().numpy(), [[1,2],[3,4]])
    np.testing.assert_equal(null.cpu().numpy(), [[1,2],[3,4]])
    np.testing.assert_equal(perm.cpu().numpy(), [[1,3],[2,4]])
    np.testing.assert_equal(back.cpu().numpy(), [[1,2],[3,4]])

  def test_shrink(self):
    a = torch.Tensor([1,2,3,4]).to(device)
    np.testing.assert_equal(a[:3].cpu().numpy(), [1,2,3])
    np.testing.assert_equal(a[1:].cpu().numpy(), [2,3,4])

  def test_as_strided(self):
    a = torch.arange(70, device=device).reshape(1,1,10,7)
    a = a.as_strided((1,1,10,5), (0,0,7,1), storage_offset=0)
    a = a.as_strided((1,1,5,5), (50,50,7,1), storage_offset=21)
    np.testing.assert_equal(a.cpu().numpy().sum(-1), [[[115,150,185,220,255]]])

  def test_storage_offset_of_computed_tensor(self):
    # a computed result owns its storage, so a slice anywhere in its history must not shift the offset
    a = torch.arange(8., device=device)
    self.assertEqual((a[3:]+1).storage_offset(), 0)

  def test_storage_offset_through_aliases(self):
    a = torch.arange(8., device=device)[3:]
    self.assertEqual(a.detach().storage_offset(), 3)
    self.assertEqual(a.view(torch.int32).storage_offset(), 3)
    torch.add(torch.ones(5, device=device), torch.ones(5, device=device), out=a)
    self.assertEqual(a.detach().storage_offset(), 3)

  def test_out_refreshes_views_of_base(self):
    a = torch.zeros(4, device=device)
    v = a[2:]
    torch.add(torch.ones(4, device=device), torch.ones(4, device=device), out=a)
    np.testing.assert_equal(v.cpu().numpy(), [2., 2.])

  @unittest.expectedFailure  # TODO: storage offset assumes a contiguous source, use UOp.contiguous_view_offset
  def test_storage_offset_non_contiguous_source(self):
    a = torch.arange(12., device=device).reshape(3,4)
    self.assertEqual(a.permute(1,0)[1:].storage_offset(), 1)
    self.assertEqual(a.flatten()[3:].flip(0).storage_offset(), 0)

  def test_as_strided_explicit_zero_offset(self):
    # storage_offset=0 is a real offset, not "unspecified": it must not fall back to the input's own offset
    a = torch.arange(6., device=device)
    np.testing.assert_equal(a[3:].as_strided((2,), (1,), 0).cpu().numpy(), [0,1])
    np.testing.assert_equal(a[3:].as_strided((2,), (1,)).cpu().numpy(), [3,4])

  def test_empty_strided_default_dtype(self):
    self.assertEqual(torch.empty_strided((2,3), (1,2), device=device).dtype, torch.get_default_dtype())

  @unittest.expectedFailure  # TODO: empty_strided ignores the requested strides, the backend treats everything as contiguous
  def test_empty_strided_honors_strides(self):
    self.assertEqual(tuple(torch.empty_strided((2,3), (1,2), device=device).stride()), (1,2))

  @unittest.expectedFailure  # TODO: torch refuses an out= that overlaps an input, we compute silently
  def test_out_overlapping_input_is_rejected(self):
    x = torch.arange(6., device=device)
    with self.assertRaises(RuntimeError): torch.add(x[:-1], 10, out=x[1:])

  def test_out_disjoint_input_is_allowed(self):
    # torch permits an out= that shares a base with an input as long as they do not overlap
    x, xc = torch.arange(6., device=device), torch.arange(6.)
    torch.add(x[:3], 10, out=x[3:]); torch.add(xc[:3], 10, out=xc[3:])
    np.testing.assert_equal(x.cpu().numpy(), xc.numpy())

  def test_plus_inplace(self):
    a = torch.ones(4, device=device)
    b = torch.ones(4, device=device)
    a += b
    a += b
    np.testing.assert_equal(a.cpu().numpy(), [3,3,3,3])

  def test_exp2(self):
    a = torch.ones(4, device=device)
    b = a.exp2()
    np.testing.assert_equal(b.cpu().numpy(), [2,2,2,2])

  def test_amax(self):
    x = torch.tensor([[[ 1.5,  2.3,  3.1,  4.7],
                       [ 5.2,  6.8,  7.4,  12.9],
                       [ 9.0, 12.3, 11.6, 10.1]],
                      [[13.2, 16.9, 15.5, 14.1],
                       [17.1, 24.9, 19.8, 20.2],
                       [21.0, 22.3, 23.6, 18.4]]], device=device)

    y1 = torch.amax(x)
    expected = np.array([24.9], dtype=np.float32)
    np.testing.assert_equal(y1.cpu().numpy(), expected)

    y2 = torch.amax(x, dim=(1,2))
    expected = np.array([12.9, 24.9], dtype=np.float32)
    np.testing.assert_equal(y2.cpu().numpy(), expected)

    y3 = torch.amax(x, dim=2)
    expected = np.array([[4.7, 12.9, 12.3], [16.9, 24.9, 23.6]], dtype=np.float32)
    np.testing.assert_equal(y3.cpu().numpy(), expected)


  def test_amin(self):
    x = torch.tensor([[[ 1.5,  2.3,  3.1,  4.7],
                       [ 5.2,  6.8,  7.4,  12.9],
                       [ 9.0, 12.3, 11.6, 10.1]],
                      [[13.2, 16.9, 15.5, 14.1],
                       [17.1, 24.9, 19.8, 20.2],
                       [21.0, 22.3, 23.6, 18.4]]], device=device)

    y1 = torch.amin(x)
    expected = np.array([1.5], dtype=np.float32)
    np.testing.assert_equal(y1.cpu().numpy(), expected)

    y2 = torch.amin(x, dim=(1,2))
    expected = np.array([1.5, 13.2], dtype=np.float32)
    np.testing.assert_equal(y2.cpu().numpy(), expected)

    y3 = torch.amin(x, dim=2)
    expected = np.array([[1.5, 5.2, 9.0], [13.2, 17.1, 18.4]], dtype=np.float32)
    np.testing.assert_equal(y3.cpu().numpy(), expected)

  def test_argmax_argmin(self):
    a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
    c = a.cpu()
    for got, want in [(a.argmax(), c.argmax()), (a.argmin(0), c.argmin(0)), (a.argmax(1, keepdim=True), c.argmax(1, keepdim=True)),
                      (torch.min(a, 1).indices, torch.min(c, 1).indices), (torch.max(a, 1).indices, torch.max(c, 1).indices),
                      (torch.min(a, 1).values, torch.min(c, 1).values), (torch.min(a, 1, keepdim=True).indices, torch.min(c, 1, keepdim=True).indices)]:
      self.assertEqual(got.dtype, want.dtype)  # torch's arg reduces are int64, tinygrad's are int32
      np.testing.assert_equal(got.cpu().numpy(), want.numpy())

  def test_isfinite(self):
    a = torch.ones(4, device=device)
    np.testing.assert_equal(torch.isfinite(a).cpu().numpy(), [True, True, True, True])

  def test_eq(self):
    a = torch.ones(4, device=device)
    b = torch.ones(4, device=device)
    c = a == b
    print(c.cpu())

  def test_maxpool2d_backward(self):
    x = torch.arange(3*3, dtype=torch.float32, device=device).reshape(1, 1, 3, 3).requires_grad_(True)
    torch.nn.functional.max_pool2d(x, kernel_size=2, stride=1).sum().backward()
    np.testing.assert_equal(x.grad.squeeze().cpu().numpy(), [[0, 0, 0], [0, 1, 1], [0, 1, 1]])

  def test_copy_cast(self):
    x = torch.zeros(4, device=device, dtype=torch.int64)
    y = torch.ones(4, device=device, dtype=torch.float32).to(dtype=torch.int64)
    res1 = x ^ y # an operation that only works on int types
    print(res1.cpu())
    y = y.cpu().float().to(device=device, dtype=torch.int64)
    res2 = x ^ y
    print(res2.cpu())

  def test_topk(self):
    # test topk return_types
    a = torch.tensor([1, 3, 2, 4], device=device)
    out = torch.topk(a, k=2)
    np.testing.assert_equal(out.values.cpu().numpy(), [4, 3])
    np.testing.assert_equal(out.indices.cpu().numpy(), [3, 1])

  def test_masked_select(self):
    a = torch.tensor([4, 3, 2, 1], device=device)
    mask = torch.tensor([True, False, True, False], device=device)
    out = torch.masked_select(a, mask)
    np.testing.assert_equal(out.cpu().numpy(), [4, 2])
    mask = torch.tensor(True, device=device)
    out = torch.masked_select(a, mask)
    np.testing.assert_equal(out.cpu().numpy(), [4, 3, 2, 1])

  def test_isin_tensor_tensor_out(self):
    a = torch.tensor([1, 2, 3], device=device)
    b = torch.tensor([2, 4], device=device)
    expected_base = torch.tensor([False, True, False], device=device)
    for assume_unique in [False, True]:
      for invert, expected in [(False, expected_base), (True, ~expected_base)]:
        out = torch.empty_like(a, dtype=torch.bool)
        res = torch.ops.aten.isin.Tensor_Tensor_out(a, b, invert=invert, assume_unique=assume_unique, out=out)
        np.testing.assert_equal(out.cpu().numpy(), expected.cpu().numpy())

  def test_uniform(self):
    for torch_dtype in [torch.float32, torch.float16]:
      a = torch.rand(10, 10, device=device, dtype=torch_dtype)
      self.assertEqual(a.dtype, torch_dtype)

  def test_normal(self):
    for torch_dtype in [torch.float32, torch.float16]:
      a = torch.randn(10, 10, device=device, dtype=torch_dtype)
      self.assertEqual(a.dtype, torch_dtype)

  def test_equal(self):
    tensor_a = torch.tensor([[1, 2], [3, 4]], device=device)
    tensor_b = torch.tensor([[1, 2], [3, 4]], device=device)
    tensor_c = torch.tensor([[1, 2], [1, 2]], device=device)
    assert torch.equal(tensor_a, tensor_b)
    assert not torch.equal(tensor_a, tensor_c)

  @unittest.skip("# TODO: this test is slow")
  def test_linalg_svd(self):
    A = torch.randn(5, 5, device=device)
    U, S, Vh = torch.linalg.svd(A)
    np.testing.assert_equal(U.shape, (5,5))
    np.testing.assert_equal(Vh.shape, (5,5))
    np.testing.assert_allclose(torch.dist(A, U @ torch.diag(S) @ Vh).cpu().numpy(), 0, atol=1e-5)

    A = torch.randn(5, 3, device=device)
    U, S, Vh = torch.linalg.svd(A, full_matrices=False)
    np.testing.assert_equal(U.shape, (5,3))
    np.testing.assert_equal(Vh.shape, (3,3))
    np.testing.assert_allclose(torch.dist(A, U @ torch.diag(S) @ Vh).cpu().numpy(), 0, atol=1e-5)

  def test_linalg_eigh(self):
    a = torch.tensor([[1, 2], [2, 1]], dtype=torch.float32, device=device)
    w, v = torch.linalg.eigh(a)
    np.testing.assert_equal(w.cpu().numpy(), [-1, 3])
    recon = (v @ torch.diag(w) @ v.T).cpu().numpy()
    np.testing.assert_allclose(recon, a.cpu().numpy(), atol=1e-6)

  def test_linalg_det(self):
    a = torch.diag(torch.tensor([1,2,3,4,5], dtype = torch.float32, device=device))
    b = torch.linalg.det(a)
    np.testing.assert_equal(b.cpu().numpy(), 120.0)

  def test_linalg_cross(self):
    a = torch.tensor([[1, 0, 0], [0, 1, 0]], dtype=torch.float32, device=device)
    b = torch.tensor([[0, 0, 1]], dtype=torch.float32, device=device)
    cross = torch.linalg.cross(a, b)
    np.testing.assert_equal(cross.cpu().numpy(), np.array([[0, -1, 0], [1, 0, 0]], dtype=np.float32))

  def test_scalar_assign(self):
    a = torch.tensor([1, 2, 3], device=device)
    a[1] = 4
    np.testing.assert_equal(a.cpu().numpy(), [1, 4, 3])

  @unittest.skip("meh")
  def test_str(self):
    a = torch.ones(4, device=device)
    print(str(a))

  def test_floor_div(self):
    a = torch.tensor([10., 7., 5.], device=device)
    b = torch.tensor([3., 2., 2.], device=device)
    result = a // b
    np.testing.assert_equal(result.cpu().numpy(), [3., 3., 2.])

  def test_mnist_index(self):
    # from tinygrad.nn.datasets import mnist
    X_train, Y_train = Tensor.randint(60000, 1, 28, 28, dtype='uchar').realize(), Tensor.randint(60000, dtype='uchar').realize()
    GlobalCounters.reset()
    X_train = torch.tensor(X_train.float().numpy(), device=device)
    Y_train = torch.tensor(Y_train.cast('int64').numpy(), device=device)
    samples = torch.randint(0, X_train.shape[0], (32,))
    X,Y = X_train[samples], Y_train[samples]
    X.cpu(), Y.cpu()
    self.assertLessEqual(GlobalCounters.global_ops, 25_000_000)

  def _test_diagonal(self, *shape):
    a = torch.randn(*shape, dtype=torch.float32, device=device)
    ref = np.diagonal(a.cpu().numpy(), axis1=-2, axis2=-1)
    diag = torch.linalg.diagonal(a)
    np.testing.assert_equal(diag.cpu().numpy(), ref)
    np.testing.assert_equal(diag[-1].cpu().numpy(), ref[-1])

  def test_diagonal_cube(self): self._test_diagonal(3, 3, 3)
  def test_diagonal_rectangular(self): self._test_diagonal(4, 5, 6)
  def test_diagonal_4d(self): self._test_diagonal(2, 3, 4, 5)

  def test_pad_circular_simple(self):
    a = torch.arange(4, dtype=torch.float32, device=device).reshape(1,1,2,2)
    padded = torch.nn.functional.pad(a, (1,1,1,1), mode="circular")
    expected = np.array([[[[3.,2.,3.,2.], [1.,0.,1.,0.], [3.,2.,3.,2.], [1.,0.,1.,0.]]]], dtype=np.float32)
    np.testing.assert_allclose(padded.cpu().numpy(), expected)

  def test_pad_circular_backward(self):
    a = torch.arange(4, dtype=torch.float32, device=device).reshape(1,1,2,2).requires_grad_(True)
    padded = torch.nn.functional.pad(a, (1,1,1,1), mode="circular")
    loss = padded.sum()
    loss.backward()
    expected_grad = np.array([[[[4., 4.], [4., 4.]]]], dtype=np.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad)


  def test_matmul_backward(self):
    x = torch.randn(3, 4, device=device, dtype=torch.float32, requires_grad=True)
    y = torch.randn(4, 5, device=device, dtype=torch.float32, requires_grad=True)
    z = (x @ y).sum()
    z.backward()
    assert x.grad is not None
    assert y.grad is not None
    assert x.grad.shape == x.shape
    assert y.grad.shape == y.shape

  def test_matmul_broadcast_backward(self):
    x = torch.randn(2, 3, 4, device=device, dtype=torch.float32, requires_grad=True)
    y = torch.randn(4, 5, device=device, dtype=torch.float32, requires_grad=True)
    z = (x @ y).sum()
    z.backward()
    assert x.grad is not None
    assert y.grad is not None
    assert x.grad.shape == x.shape
    assert y.grad.shape == y.shape

  def test_diag_vector_to_matrix(self):
    vec = torch.tensor([1., 2., 3., 4., 5.], dtype=torch.float32, device=device)
    mat = torch.diag(vec)
    expected = np.diag([1., 2., 3., 4., 5.])
    np.testing.assert_allclose(mat.cpu().numpy(), expected, rtol=1e-5)
    assert mat.shape == (5, 5)

  def test_diagonal_matrix_to_vector(self):
    mat = torch.tensor([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]], dtype=torch.float32, device=device)
    vec = torch.linalg.diagonal(mat)
    expected = np.array([1., 5., 9.])
    np.testing.assert_allclose(vec.cpu().numpy(), expected, rtol=1e-5)
    assert vec.shape == (3,)

  def test_permute_2(self):
    a = torch.randn(2, 3, 4, dtype=torch.float32, device=device)
    b = a.permute(2, 0, 1)
    assert b.shape == (4, 2, 3)
    np.testing.assert_equal(b.cpu().numpy(), a.cpu().numpy().transpose(2, 0, 1))

  def test_batchnorm_backward_realized_stats(self):
    # the saved stats are a function of input in training, so grad_input must flow through them even when handed in realized.
    # the backward eps is unused in training: torch differentiates the save_invstd it was given
    x0, g0 = torch.randn(8, 4, 3, 3), torch.randn(8, 4, 3, 3)
    def run(dev, bwd_eps):
      x, go = x0.to(dev), g0.to(dev)
      w, b = torch.linspace(0.5, 2.0, 4).to(dev), torch.zeros(4, device=dev)
      rm, rv = torch.zeros(4, device=dev), torch.ones(4, device=dev)
      out, sm, si = torch.ops.aten.native_batch_norm(x, w, b, rm, rv, True, 0.1, 1e-5)
      grads = torch.ops.aten.native_batch_norm_backward(go, x, w, rm, rv, sm.clone().detach(), si.clone().detach(),
                                                        True, bwd_eps, [True,True,True])
      return [t.cpu().numpy() for t in grads]
    for bwd_eps in [1e-5, 0.3]:
      for got, want in zip(run(device, bwd_eps), run("cpu", bwd_eps)): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)

  def test_groupnorm_backward(self):
    def run(dev):
      x = torch.arange(24., device=dev).reshape(2, 4, 3).requires_grad_()
      w = torch.linspace(0.5, 2.0, 4).to(dev).requires_grad_()
      torch.nn.functional.group_norm(x, 2, w, torch.zeros(4, device=dev)).square().sum().backward()
      return x.grad.cpu().numpy(), w.grad.cpu().numpy()
    for got, want in zip(run(device), run("cpu")): np.testing.assert_allclose(got, want, atol=1e-4, rtol=1e-3)

  def test_mse_smooth_l1_loss_backward(self):
    def run(dev, loss):
      x = torch.arange(4., device=dev).requires_grad_()
      loss(x, torch.ones(4, device=dev)).backward()
      return x.grad.cpu().numpy()
    for loss in [torch.nn.functional.mse_loss, torch.nn.functional.smooth_l1_loss]:
      np.testing.assert_allclose(run(device, loss), run("cpu", loss), atol=1e-6)

  def test_batchnorm_unsqueeze(self):
    bn = torch.nn.BatchNorm2d(4).to(device)
    x = torch.randn(8, 4, 3, 3, device=device)
    out = bn(x)
    self.assertEqual(out.shape, x.shape)

  def test_slice_inplace_zero(self):
    a = torch.ones((3, 3), device=device)
    b = a[1:, 1:]
    b.zero_()
    expected = np.array([[1., 1., 1.],
                         [1., 0., 0.],
                         [1., 0., 0.]])
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_slice_inplace_fill(self):
    a = torch.ones((3, 3), device=device)
    b = a[1:, 1:]
    b.fill_(5.0)
    expected = np.array([[1., 1., 1.],
                         [1., 5., 5.],
                         [1., 5., 5.]])
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_fill_tensor_value(self):
    a = torch.zeros((2, 2), dtype=torch.float32, device=device)
    value = torch.tensor(3, dtype=torch.int64, device=device)
    a.fill_(value)
    expected = np.full((2, 2), 3, dtype=np.float32)
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_slice_inplace_mul(self):
    a = torch.ones((3, 3), device=device)
    b = a[1:, 1:]
    b *= 2
    expected = np.array([[1., 1., 1.],
                         [1., 2., 2.],
                         [1., 2., 2.]])
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_permute_slice_zero(self):
    a = torch.ones((3, 3), device=device)
    b = a[1:, 1:].permute(1, 0)
    b.zero_()
    expected = np.array([[1., 1., 1.],
                         [1., 0., 0.],
                         [1., 0., 0.]])
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_permute_slice_mul(self):
    a = torch.ones((3, 3), device=device)
    b = a[1:, 1:].permute(1, 0)
    b *= 2
    expected = np.array([[1., 1., 1.],
                         [1., 2., 2.],
                         [1., 2., 2.]])
    np.testing.assert_equal(a.cpu().numpy(), expected)

  def test_simple_slice_setitem(self):
    a = torch.tensor([10, 20, 30], device=device)
    a[1] = 99
    np.testing.assert_equal(a.cpu().numpy(), [10, 99, 30])

  def test_2d_slice_setitem(self):
    a = torch.zeros((3, 3), device=device)
    a[1, 2] = 99
    self.assertEqual(a[1, 2].item(), 99)
    self.assertEqual(a.sum().item(), 99)

  def test_view_copy(self):
    a = torch.tensor([10, 20, 30], device=device)
    view = a[1]
    view.copy_(torch.tensor(88, device=device))
    np.testing.assert_equal(a.cpu().numpy(), [10, 88, 30])

  def test_diag_2d_input(self):
    a = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], device=device)
    d = torch.diag(a)
    np.testing.assert_equal(d.cpu().numpy(), [1, 5, 9])

  def test_diag_1d_input(self):
    a = torch.tensor([1, 2, 3], device=device)
    d = torch.diag(a)
    expected = [[1, 0, 0], [0, 2, 0], [0, 0, 3]]
    np.testing.assert_equal(d.cpu().numpy(), expected)

  def test_permute_view_tracking(self):
    a = torch.ones((2, 3, 4), device=device)
    b = a.permute(2, 0, 1)
    self.assertEqual(b.shape, (4, 2, 3))

  def test_detach_view_creation(self):
    a = torch.tensor([1.0, 2.0, 3.0], device=device)
    b = a.detach()
    np.testing.assert_equal(b.cpu().numpy(), [1.0, 2.0, 3.0])

  def test_view_zero_inplace(self):
    a = torch.ones((4, 4), device=device)
    view = a[1:3, 1:3]
    view.zero_()
    self.assertEqual(view.sum().item(), 0)

  def test_view_fill_inplace(self):
    a = torch.zeros((4, 4), device=device)
    view = a[1:3, 1:3]
    view.fill_(5)
    self.assertEqual(view.sum().item(), 20)

  def test_permute_contiguous(self):
    a = torch.tensor([[1, 2], [3, 4]], device=device)
    b = a.permute(1, 0)
    c = b.contiguous()
    expected = [[1, 3], [2, 4]]
    np.testing.assert_equal(c.cpu().numpy(), expected)

  def test_diag_2d_extract_diagonal(self):
    a = torch.tensor([[1, 2], [3, 4]], device=device)
    result = torch.diag(a)
    np.testing.assert_equal(result.cpu().numpy(), [1, 4])

  def test_slice_inplace_multiply_offset_preservation(self):
    a = torch.tensor([1, 2, 3], device=device)
    a[1:] *= 2
    np.testing.assert_equal(a.cpu().numpy(), [1, 4, 6])

  def test_slice_inplace_mul_pattern(self):
    a = torch.tensor([1, 2, 3, 4], device=device)
    a[:2] *= 3
    a[2:] *= 2
    np.testing.assert_equal(a.cpu().numpy(), [3, 6, 6, 8])

  def test_chained_slice_column(self):
    a = torch.arange(16, dtype=torch.float32, device=device).reshape(4, 4)
    torch_res = a[:, 1:2][:, 0:1].cpu().numpy()
    cpu_res = torch.arange(16, dtype=torch.float32).reshape(4, 4)[:, 1:2][:, 0:1].numpy()
    np.testing.assert_equal(torch_res, cpu_res)

  def test_slice_with_step(self):
    a = torch.arange(20, dtype=torch.float32, device=device)
    torch_res = a[::2][1:4].cpu().numpy()
    cpu_res = torch.arange(20, dtype=torch.float32)[::2][1:4].numpy()
    np.testing.assert_equal(torch_res, cpu_res)

  def test_select_out_of_range_dim(self):
    a = torch.arange(12, dtype=torch.int32, device=device).reshape(3, 4)
    with self.assertRaises(IndexError): a.select(5, 0)

  def test_select_collapses_the_only_dim(self):
    a = torch.arange(3, dtype=torch.int32, device=device)
    self.assertEqual(a.select(0, 1).shape, ())
    np.testing.assert_equal(a.select(0, 1).cpu().numpy(), 1)

  def test_slice_negative_dim(self):
    a = torch.arange(13, dtype=torch.int32, device=device).repeat(8, 1)
    torch_chunks = a.chunk(3, -1)
    cpu_chunks = torch.arange(13, dtype=torch.int32).repeat(8, 1).chunk(3, -1)
    assert len(torch_chunks) == len(cpu_chunks)
    for i in range(len(torch_chunks)):
      np.testing.assert_equal(torch_chunks[i].cpu().numpy(), cpu_chunks[i].numpy())

  def test_dot_vector_matrix(self):
    a = torch.arange(65, dtype=torch.float32, device=device)
    b = torch.arange(65*45, dtype=torch.float32, device=device).reshape(65, 45)
    torch_res = a.matmul(b).reshape(-1).cpu().numpy()
    cpu_res = torch.arange(65, dtype=torch.float32).matmul(torch.arange(65*45, dtype=torch.float32).reshape(65, 45)).numpy()
    np.testing.assert_equal(torch_res, cpu_res)

  def test_alias_passthrough(self):
    a = torch.randn(3, 3, device=device)
    alias_view = torch.ops.aten.alias(a)
    alias_view += 1
    np.testing.assert_equal(a.cpu().numpy(), alias_view.cpu().numpy())

  def test_split_simple_vector(self):
    a = torch.arange(10, dtype=torch.float32, device=device)
    torch_chunks = a.split([1,4,5])
    cpu_chunks = torch.arange(10, dtype=torch.float32).split([1,4,5])
    for tc, cc in zip(torch_chunks, cpu_chunks):
      np.testing.assert_equal(tc.cpu().numpy(), cc.cpu().numpy())

  def test_split_matches_torch(self):
    a = torch.arange(10, dtype=torch.float32, device=device)
    torch_chunks = a.split([1,4,5])
    tiny_chunks = [chunk.cpu().numpy() for chunk in torch_chunks]
    cpu_chunks = [torch.arange(10, dtype=torch.float32).split([1,4,5])[i].numpy() for i in range(3)]
    for tr, cr in zip(tiny_chunks, cpu_chunks): np.testing.assert_equal(tr, cr)

  def test_sum_matches_torch(self):
    a = torch.arange(6, dtype=torch.float32, device=device).reshape(2,3)
    torch_res = a.sum().cpu().numpy()
    cpu_res = torch.arange(6, dtype=torch.float32).reshape(2,3).sum().numpy()
    np.testing.assert_equal(torch_res, cpu_res)

  def test_view_matches_torch(self):
    a = torch.arange(6, dtype=torch.float32, device=device)
    torch_res = a.view(2, 3).cpu().numpy()
    cpu_res = torch.arange(6, dtype=torch.float32).view(2, 3).numpy()
    np.testing.assert_equal(torch_res, cpu_res)

  def test_view_zero_with_indices(self):
    a = torch.tensor([1, 2, 3, 4], device=device)
    a[1:3].zero_()
    np.testing.assert_equal(a.cpu().numpy(), [1, 0, 0, 4])

  def test_view_fill_with_indices(self):
    a = torch.tensor([1, 2, 3, 4], device=device)
    a[::2].fill_(9)
    np.testing.assert_equal(a.cpu().numpy(), [9, 2, 9, 4])

  def test_nested_slice_inplace_ops(self):
    a = torch.tensor([1, 2, 3, 4, 5, 6], device=device)
    a[:3] += 10
    a[3:] *= 2
    np.testing.assert_equal(a.cpu().numpy(), [11, 12, 13, 8, 10, 12])

  def test_diag_1d(self):
    a = torch.tensor([1, 2, 3], device=device)
    result = torch.diag(a)
    expected = [[1, 0, 0], [0, 2, 0], [0, 0, 3]]
    np.testing.assert_equal(result.cpu().numpy(), expected)

  def test_diag_backward(self):
    a = torch.randn(5, dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diag(a)
    b.sum().backward()
    assert a.grad is not None

  def test_diagonal(self):
    a = torch.tensor([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diagonal(a)
    expected = torch.tensor([1., 5., 9.], dtype=torch.float32)
    self.assertEqual(b.shape, (3,))
    np.testing.assert_allclose(b.detach().cpu().numpy(), expected.numpy(), rtol=1e-5)

  def test_diagonal_backward(self):
    a = torch.randn(5, 5, dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diagonal(a)
    b.sum().backward()
    assert a.grad is not None

  def test_expand_backward(self):
    a = torch.randn(4, 3, 1, 6, dtype=torch.float32, device=device, requires_grad=True)
    b = a.expand(4, 3, 2, 6)
    b.sum().backward()
    assert a.grad is not None

  def test_einsum_backward(self):
    a = torch.randn(10, 10, dtype=torch.float32, device=device, requires_grad=True)
    b = torch.einsum('ij->ji', a)
    b.sum().backward()
    assert a.grad is not None

  def test_diag_backward_gradient_values(self):
    a = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diag(a)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.ones(3, dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_diag_backward_gradient_values_2d_to_1d(self):
    a = torch.tensor([[1.0, 2.0, 3.0],
                      [4.0, 5.0, 6.0],
                      [7.0, 8.0, 9.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diagonal(a)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[1.0, 0.0, 0.0],
                                   [0.0, 1.0, 0.0],
                                   [0.0, 0.0, 1.0]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_expand_backward_gradient_values(self):
    a = torch.tensor([[1.0], [2.0], [3.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = a.expand(3, 4)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[4.0], [4.0], [4.0]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_expand_backward_with_leading_dims(self):
    a = torch.tensor([[1.0, 2.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = a.expand(3, 1, 2)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[3.0, 3.0]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_diag_2d_to_1d_backward(self):
    a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diag(a)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[1.0, 0.0], [0.0, 1.0]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_expand_complex_backward(self):
    a = torch.tensor([[[1.0, 2.0]]], dtype=torch.float32, device=device, requires_grad=True)
    b = a.expand(2, 3, 2)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[[6.0, 6.0]]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_diag_backward_with_scaling(self):
    a = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diag(a)
    loss = (b * torch.tensor([[2.0, 0.0, 0.0],
                               [0.0, 3.0, 0.0],
                               [0.0, 0.0, 4.0]], device=device)).sum()
    loss.backward()
    expected_grad = torch.tensor([2.0, 3.0, 4.0], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_repeat_basic(self):
    a = torch.tensor([1, 2, 3], dtype=torch.float32, device=device)
    b = a.repeat(2, 1)
    expected = torch.tensor([[1, 2, 3], [1, 2, 3]], dtype=torch.float32)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

  def test_repeat_multidim(self):
    a = torch.arange(6, dtype=torch.float32, device=device).reshape(2, 3)
    b = a.repeat(2, 3)
    expected = torch.arange(6, dtype=torch.float32).reshape(2, 3).repeat(2, 3)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

  def test_repeat_backward(self):
    a = torch.tensor([[1.0, 2.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = a.repeat(3, 2)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([[6.0, 6.0]], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_cumsum_1d(self):
    a = torch.tensor([1, 2, 3, 4], dtype=torch.float32, device=device)
    b = torch.cumsum(a, dim=0)
    expected = torch.tensor([1, 3, 6, 10], dtype=torch.float32)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

  def test_cumsum_2d(self):
    a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
    b = torch.cumsum(a, dim=0)
    expected = torch.arange(12, dtype=torch.float32).reshape(3, 4).cumsum(dim=0)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

    c = torch.cumsum(a, dim=1)
    expected = torch.arange(12, dtype=torch.float32).reshape(3, 4).cumsum(dim=1)
    np.testing.assert_equal(c.cpu().numpy(), expected.numpy())

  def test_cumsum_backward(self):
    a = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.cumsum(a, dim=0)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.tensor([4.0, 3.0, 2.0, 1.0], dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_constant_pad_nd_1d(self):
    a = torch.tensor([1, 2, 3], dtype=torch.float32, device=device)
    b = torch.nn.functional.pad(a, (1, 2), mode='constant', value=0)
    expected = torch.tensor([0, 1, 2, 3, 0, 0], dtype=torch.float32)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

  def test_constant_pad_nd_2d(self):
    a = torch.arange(6, dtype=torch.float32, device=device).reshape(2, 3)
    b = torch.nn.functional.pad(a, (1, 1, 1, 1), mode='constant', value=0)
    expected = torch.nn.functional.pad(torch.arange(6, dtype=torch.float32).reshape(2, 3), (1, 1, 1, 1), mode='constant', value=0)
    np.testing.assert_equal(b.cpu().numpy(), expected.numpy())

  def test_constant_pad_nd_2d_backward(self):
    a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.nn.functional.pad(a, (1, 1, 1, 1), mode='constant', value=0)
    loss = b.sum()
    loss.backward()
    expected_grad = torch.ones((2, 2), dtype=torch.float32)
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected_grad.numpy(), rtol=1e-5)

  def test_negative_strides_cumsum_backward(self):
    a = torch.randn(5, device=device, requires_grad=True)
    b = torch.cumsum(a, dim=0)
    b.sum().backward()
    grad = a.grad.cpu().numpy()
    self.assertEqual(len(grad), 5)

  def test_cumsum_fix_gradient_values(self):
    a = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.cumsum(a, dim=0)
    loss = b.sum()
    loss.backward()
    expected = np.array([4.0, 3.0, 2.0, 1.0])
    np.testing.assert_allclose(a.grad.cpu().numpy(), expected, rtol=1e-5)

  def test_cumsum_arange_large(self):
    # Tests cumsum with an unrealized arange input with size > 512 (the split threshold)
    # This exercises the _split_cumalu path which uses a two-stage algorithm
    for size in [513, 1022]:
      a = torch.arange(size, dtype=torch.float32, device=device)
      result = torch.cumsum(a, dim=0)
      expected = torch.arange(size, dtype=torch.float32).cumsum(dim=0)
      np.testing.assert_allclose(result.cpu().numpy(), expected.numpy(), rtol=1e-5)

  def test_diag_1d_to_2d(self):
    a = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32, device=device, requires_grad=True)
    b = torch.diag(a)
    expected = [[1, 0, 0], [0, 2, 0], [0, 0, 3]]
    np.testing.assert_equal(b.detach().cpu().numpy(), expected)

  def test_diag_2d_to_1d(self):
    c = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float32, device=device)
    d = torch.diag(c)
    np.testing.assert_equal(d.cpu().numpy(), [1, 5, 9])

  def test_biased_conv2d(self):
    # Test case for two sequential conv2d with same weights/bias and ReLU in between, this is as special case from test_ops.py
    torch.manual_seed(0)
    C = 8
    x_cpu = torch.randn(1, C, 5, 5, requires_grad=True)
    w_cpu = torch.randn(C, C, 1, 1, requires_grad=True)
    b_cpu = torch.randn(C, requires_grad=True)
    x_tiny = x_cpu.detach().to(device).requires_grad_(True)
    w_tiny = w_cpu.detach().to(device).requires_grad_(True)
    b_tiny = b_cpu.detach().to(device).requires_grad_(True)
    out_cpu = torch.nn.functional.conv2d(torch.nn.functional.conv2d(x_cpu, w_cpu, b_cpu).relu(), w_cpu, b_cpu)
    out_tiny = torch.nn.functional.conv2d(torch.nn.functional.conv2d(x_tiny, w_tiny, b_tiny).relu(), w_tiny, b_tiny)
    grad_out = torch.randn_like(out_cpu)
    out_cpu.backward(grad_out)
    out_tiny.backward(grad_out.to(device))
    np.testing.assert_allclose(x_tiny.grad.cpu().numpy(), x_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
    np.testing.assert_allclose(w_tiny.grad.cpu().numpy(), w_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)
    np.testing.assert_allclose(b_tiny.grad.cpu().numpy(), b_cpu.grad.numpy(), atol=1e-4, rtol=1e-3)

  def test_write_through_detach_of_unrealized(self):
    a = torch.empty(4, device=device)
    a.detach().fill_(3)
    np.testing.assert_equal(a.cpu().numpy(), [3, 3, 3, 3])

  def test_square_transpose_inplace(self):
    # a same-shape transpose is not a reshape: writing the transposed values straight back would scramble the base
    a = torch.tensor([[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]], device=device)
    a.transpose(0, 1).add_(100)
    np.testing.assert_equal(a.cpu().numpy(), [[100., 101., 102.], [103., 104., 105.], [106., 107., 108.]])

  def test_interpolate(self):
    a = torch.arange(4, dtype=torch.float32, device=device).reshape(1, 1, 2, 2)
    nearest = torch.nn.functional.interpolate(a, scale_factor=2.0)
    np.testing.assert_equal(nearest.cpu().numpy()[0, 0], [[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 3, 3], [2, 2, 3, 3]])
    linear = torch.nn.functional.interpolate(a, size=(4, 4), mode="bilinear", align_corners=False)
    ref = torch.nn.functional.interpolate(a.cpu(), size=(4, 4), mode="bilinear", align_corners=False)
    np.testing.assert_allclose(linear.cpu().numpy(), ref.numpy(), rtol=1e-5)

  def test_interpolate_bicubic_area(self):
    a = torch.arange(32, dtype=torch.float32, device=device).reshape(1, 2, 4, 4)
    for mode, scale in [("bicubic", 2.0), ("area", 0.5)]:
      ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=scale, mode=mode)
      np.testing.assert_allclose(torch.nn.functional.interpolate(a, scale_factor=scale, mode=mode).cpu().numpy(), ref.numpy(), atol=1e-4)

  @unittest.expectedFailure
  def test_interpolate_bicubic_backward(self):
    # the forward comes from a decomposition, but aten::upsample_bicubic2d_backward has none (nor does
    # aten::_adaptive_avg_pool2d_backward, for area), so training through these modes needs a real kernel
    x = torch.arange(32., dtype=torch.float32, device=device).reshape(1, 2, 4, 4).requires_grad_()
    torch.nn.functional.interpolate(x, scale_factor=2.0, mode="bicubic").sum().backward()

  @unittest.expectedFailure
  def test_interpolate_inexact_scale(self):
    # torch forwards the raw scale_factor, Tensor.interpolate recomputes it from output_size, and they disagree here
    a = torch.arange(6, dtype=torch.float32, device=device).reshape(1, 1, 2, 3)
    tiny = torch.nn.functional.interpolate(a, scale_factor=2.5, mode="bilinear")
    ref = torch.nn.functional.interpolate(a.cpu(), scale_factor=2.5, mode="bilinear")
    np.testing.assert_allclose(tiny.cpu().numpy(), ref.numpy(), rtol=1e-5)

  def test_logical_or_xor(self):
    a = torch.tensor([True, True, False, False], device=device)
    b = torch.tensor([True, False, True, False], device=device)
    np.testing.assert_equal(torch.logical_or(a, b).cpu().numpy(), [True, True, True, False])
    np.testing.assert_equal(torch.logical_xor(a, b).cpu().numpy(), [False, True, True, False])
    # bool-valued whatever the input dtype, so this is not | and ^
    i, j = torch.tensor([2, 0, 5, 0], device=device), torch.tensor([0, 0, 1, 1], device=device)
    np.testing.assert_equal(torch.logical_or(i, j).cpu().numpy(), [True, False, True, True])
    np.testing.assert_equal(torch.logical_xor(i, j).cpu().numpy(), [True, False, False, True])

  def test_slice_scatter(self):
    # the scatters are functional: they return a new tensor and must leave the one they were given alone
    a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
    out = torch.slice_scatter(a, torch.ones(1, 4, device=device), 0, 0, 1)
    np.testing.assert_equal(out.cpu().numpy(), [[1, 1, 1, 1], [4, 5, 6, 7], [8, 9, 10, 11]])
    np.testing.assert_equal(a.cpu().numpy(), np.arange(12, dtype=np.float32).reshape(3, 4))

  def test_slice_scatter_casts_src(self):
    a = torch.zeros(3, 4, device=device)
    out = torch.slice_scatter(a, torch.ones(1, 4, dtype=torch.int32, device=device), 0, 0, 1)
    self.assertEqual(out.dtype, torch.float32)
    np.testing.assert_equal(out.cpu().numpy()[0], np.ones(4, dtype=np.float32))

  def test_select_scatter(self):
    a = torch.arange(12, dtype=torch.float32, device=device).reshape(3, 4)
    out = torch.select_scatter(a, torch.ones(4, device=device), 0, 1)
    np.testing.assert_equal(out.cpu().numpy(), [[0, 1, 2, 3], [1, 1, 1, 1], [8, 9, 10, 11]])

  def test_diagonal_scatter(self):
    a = torch.zeros(3, 3, device=device)
    out = torch.diagonal_scatter(a, torch.arange(3, dtype=torch.float32, device=device))
    np.testing.assert_equal(out.cpu().numpy(), np.diag([0., 1., 2.]))
    np.testing.assert_equal(a.cpu().numpy(), np.zeros((3, 3), dtype=np.float32))

  def test_copy_functional(self):
    # without an impl this segfaults rather than fails: a regression here takes the whole run down
    a = torch.arange(4, dtype=torch.float32, device=device)
    out = torch.ops.aten.copy(a, torch.zeros(4, device=device))
    np.testing.assert_equal(out.cpu().numpy(), [0., 0., 0., 0.])
    np.testing.assert_equal(a.cpu().numpy(), [0., 1., 2., 3.])

from tinygrad import Tensor
class TestBackendHelpers(unittest.TestCase):
  def test_unwrap_rejects_foreign_tensor(self):
    # unwrap casts to the tiny impl, so a tensor from another backend must be refused rather than reinterpreted
    with self.assertRaises(RuntimeError): extra.torch_backend.backend.unwrap(torch.ones(4))

  def test_update_metadata_rejects_foreign_tensor(self):
    # resizing a tensor we don't own would expose memory past its allocation
    t = torch.ones(4)
    with self.assertRaises(RuntimeError): extra.torch_backend.backend.mod.update_metadata(t, [8], [1], 0)
    self.assertEqual(t.shape, (4,))

  def test_unwrap_parameter_and_detached(self):
    # nn.Parameter and detach rebuild the base OpaqueTensorImpl, which unwrap still has to accept
    extra.torch_backend.backend.unwrap(torch.nn.Parameter(torch.ones(4, device="tiny")))
    extra.torch_backend.backend.unwrap(torch.ones(4, device="tiny").detach())

  def test_calculate_storage_offset_no_shrink(self):
    t = Tensor.ones(3, 4)
    assert extra.torch_backend.backend.calculate_storage_offset(t) == 0

  def test_calculate_storage_offset_with_shrink(self):
    t = Tensor.ones(10, 10)[2:5, 3:7]
    # strides for (10, 10) are [10, 1]
    # offset = 2*10 + 3*1 = 23
    assert extra.torch_backend.backend.calculate_storage_offset(t) == 23

  def test_calculate_storage_offset_multiple_shrinks(self):
    t = Tensor.ones(5, 6, 7)[1:3, 2:4, 3:5]
    # strides for (5, 6, 7) are [42, 7, 1]
    # offset = 1*42 + 2*7 + 3*1 = 42 + 14 + 3 = 59
    assert extra.torch_backend.backend.calculate_storage_offset(t) == 59

  def test_calculate_storage_offset_with_reshape(self):
    t = Tensor.ones(10, 10)
    orig_offset = extra.torch_backend.backend.calculate_storage_offset(t)
    assert orig_offset == 0
    t = t.reshape(100)
    assert extra.torch_backend.backend.calculate_storage_offset(t) == orig_offset

  def test_slice_values_match_torch(self):
    torch_cpu = torch.arange(100, dtype=torch.float32).reshape(10, 10)
    torch_tiny = torch_cpu.to(device)
    sliced_cpu = torch_cpu[2:5, 3:7]
    sliced_tiny = torch_tiny[2:5, 3:7]
    np.testing.assert_equal(sliced_tiny.cpu().numpy(), sliced_cpu.numpy())

  def test_slice_values_match_torch_3d(self):
    torch_cpu_3d = torch.arange(210, dtype=torch.float32).reshape(5, 6, 7)
    torch_tiny_3d = torch_cpu_3d.to(device)
    sliced_cpu_3d = torch_cpu_3d[1:3, 2:4, 3:5]
    sliced_tiny_3d = torch_tiny_3d[1:3, 2:4, 3:5]
    np.testing.assert_equal(sliced_tiny_3d.cpu().numpy(), sliced_cpu_3d.numpy())

  def test_topk_out(self):
    a = torch.tensor([1, 3, 2, 4], device=device)
    values = torch.empty(2, device=device)
    indices = torch.empty(2, dtype=torch.int64, device=device)
    ret_values, ret_indices = torch.topk(a, k=2, out=(values, indices))
    np.testing.assert_equal(values.cpu().numpy(), [4, 3])
    np.testing.assert_equal(indices.cpu().numpy(), [3, 1])
    assert ret_values is values
    assert ret_indices is indices

  def test_sort_out(self):
    a = torch.tensor([3, 1, 4, 2], device=device)
    values = torch.empty(4, device=device)
    indices = torch.empty(4, dtype=torch.int64, device=device)
    ret_values, ret_indices = torch.sort(a, out=(values, indices))
    np.testing.assert_equal(values.cpu().numpy(), [1, 2, 3, 4])
    np.testing.assert_equal(indices.cpu().numpy(), [1, 3, 0, 2])
    assert ret_values is values
    assert ret_indices is indices

  def test_cat_out(self):
    a = torch.tensor([1, 2], device=device)
    b = torch.tensor([3, 4], device=device)
    out = torch.empty(4, device=device)
    ret = torch.cat([a, b], out=out)
    np.testing.assert_equal(out.cpu().numpy(), [1, 2, 3, 4])
    assert ret is out

  def test_cat_out_empty_1d(self):
    # Test tiny and cpu to show test passes on torch cpu
    for test_device in device, "cpu":
      a = torch.tensor([], device=device)
      b = torch.tensor([1, 2, 3, 4], device=device).reshape((2, 2))
      out = torch.empty((2, 2), device=device)
      for dim in 0, 1, -1, -2:
        ret = torch.cat([a, b], out=out, dim=dim)
        np.testing.assert_equal(out.cpu().numpy(), [[1, 2], [3, 4]])
        assert ret is out

  def test_cat_all_empty(self):
    for test_device in device, "cpu":
      a = torch.tensor([], device=device)
      out = torch.empty((0,), device=device)
      for dim in 0, -1:
        ret = torch.cat([a, a], out=out, dim=dim)
        np.testing.assert_equal(out.cpu().numpy(), [])
        assert ret is out

  def test_scatter_add_out(self):
    src = torch.tensor([[1, 2, 3], [4, 5, 6]], device=device, dtype=torch.float32)
    index = torch.tensor([[0, 1, 2], [0, 1, 2]], device=device)
    input = torch.zeros(3, 3, device=device, dtype=torch.float32)
    out = torch.zeros(3, 3, device=device, dtype=torch.float32)
    ret = torch.scatter_add(input, 0, index, src, out=out)
    expected = torch.tensor([[5, 0, 0], [0, 7, 0], [0, 0, 9]], dtype=torch.float32)
    np.testing.assert_allclose(out.cpu().numpy(), expected.cpu().numpy())
    assert ret is out

  def test_floor_divide_inplace_identity(self):
    x = torch.tensor([10, 20, 30, 40], dtype=torch.int32, device=device)
    y = torch.tensor([2, 4, 5, 8], dtype=torch.int32, device=device)
    ret = x.floor_divide_(y)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [5, 5, 6, 5])

  def test_lshift_inplace_identity(self):
    x = torch.tensor([1, 2, 3, 4], dtype=torch.int32, device=device)
    ret = x.__ilshift__(2)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [4, 8, 12, 16])

  def test_rshift_inplace_identity(self):
    x = torch.tensor([16, 32, 48, 64], dtype=torch.int32, device=device)
    ret = x.__irshift__(2)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [4, 8, 12, 16])

  def test_relu_inplace_identity(self):
    x = torch.tensor([-1.0, 2.0, -3.0, 4.0], device=device)
    ret = x.relu_()
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [0.0, 2.0, 0.0, 4.0])

  def test_random_inplace_identity(self):
    x = torch.zeros(10, dtype=torch.int32, device=device)
    ret = x.random_()
    assert ret is x
    assert x.shape == (10,)

  def test_random_from_inplace_identity(self):
    x = torch.zeros(10, dtype=torch.int32, device=device)
    ret = x.random_(5, 10)
    assert ret is x
    # values should be in range [5, 10)
    assert torch.all(x >= 5).item() and torch.all(x < 10).item()

  def test_uniform_inplace_identity(self):
    x = torch.zeros(10, device=device)
    ret = x.uniform_(0.0, 1.0)
    assert ret is x
    # values should be in range [0, 1)
    assert torch.all(x >= 0.0).item() and torch.all(x < 1.0).item()

  def test_normal_inplace_identity(self):
    x = torch.zeros(100, device=device)
    ret = x.normal_(0.0, 1.0)
    assert ret is x
    # just check that values changed from zeros
    assert not torch.all(x == 0.0).item()

  def test_logical_or_inplace_identity(self):
    x = torch.tensor([True, False, True, False], device=device)
    y = torch.tensor([False, False, True, True], device=device)
    ret = x.logical_or_(y)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [True, False, True, True])

  def test_masked_fill_scalar_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    mask = torch.tensor([True, False, True, False], device=device)
    ret = x.masked_fill_(mask, 0.0)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [0.0, 2.0, 0.0, 4.0])

  def test_masked_fill_tensor_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    mask = torch.tensor([True, False, True, False], device=device)
    value = torch.tensor(99.0, device=device)
    ret = x.masked_fill_(mask, value)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [99.0, 2.0, 99.0, 4.0])

  def test_zero_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    ret = x.zero_()
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [0.0, 0.0, 0.0, 0.0])

  def test_fill_scalar_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    ret = x.fill_(5.0)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [5.0, 5.0, 5.0, 5.0])

  def test_fill_tensor_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    value = torch.tensor(7.0, device=device)
    ret = x.fill_(value)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [7.0, 7.0, 7.0, 7.0])

  def test_add_tensor_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    y = torch.tensor([10.0, 20.0, 30.0, 40.0], device=device)
    ret = x.add_(y)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [11.0, 22.0, 33.0, 44.0])

  def test_add_scalar_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    ret = x.add_(10.0)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [11.0, 12.0, 13.0, 14.0])

  def test_mul_tensor_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    y = torch.tensor([2.0, 3.0, 4.0, 5.0], device=device)
    ret = x.mul_(y)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [2.0, 6.0, 12.0, 20.0])

  def test_mul_scalar_inplace_identity(self):
    x = torch.tensor([1.0, 2.0, 3.0, 4.0], device=device)
    ret = x.mul_(2.0)
    assert ret is x
    np.testing.assert_equal(x.cpu().numpy(), [2.0, 4.0, 6.0, 8.0])

if __name__ == "__main__":
  unittest.main()
