You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
12 KiB
303 lines
12 KiB
2 days ago
|
# end to end tests of tinygrad that you think might be edge cases.
|
||
|
# using the documentation, write code you think should work.
|
||
|
# you can compare the outputs to torch or numpy, or just tinygrad assert/raise while doing things that should be valid
|
||
|
|
||
|
# i'm not interested in tests that currently pass, i'm only interested in tests that you think should pass but don't.
|
||
|
# mark them with @unittest.expectedFailure
|
||
|
# all the tests in here didn't pass until bugs were fixed
|
||
|
# get creative! think about things that failed in pytorch or tensorflow for a long time until they were fixed.
|
||
|
# every test should surface a unique bug. if tinygrad throws an error saying something is not supported, this is probably not a bug.
|
||
|
# the tests don't have to test the same parts of the code that these current ones test, more diversity is better
|
||
|
|
||
|
# focus on making tinygrad throw runtime errors or assertions for valid things, or find clear numerical mismatches from pytorch
|
||
|
# confirm any bugs found are valid by doing the same thing in pytorch in the test.
|
||
|
# for any failing tests, explain in a comment why tinygrad is wrong and what the desired behavior should be.
|
||
|
# don't worry about running mypy or linters. focus on writing more of these tests and running them to confirm broken behavior.
|
||
|
# surface level bugs, like issues with empty tensors, input validation, or nans, are not that interesting.
|
||
|
# focus on bugs that would frustrate real users.
|
||
|
|
||
|
# these are not bugs, these are desired behavior. don't add failing tests for them:
|
||
|
# tinygrad only accepts tinygrad dtypes or strings of the tinygrad dtype.
|
||
|
# boolean indexing, or anything with unknown output shape of tensor at compile time isn't supported.
|
||
|
# invalid indexing in things like gather and one_hot is not an error in tinygrad. nothing that depends on the value is
|
||
|
# repeat_interleave doesn't support a tensor as the dim. check tinygrad type signature before claiming something is a bug
|
||
|
|
||
|
import unittest
|
||
|
import numpy as np
|
||
|
import torch
|
||
|
from tinygrad import Tensor, dtypes, nn
|
||
|
|
||
|
class TestNaNEdgeCases(unittest.TestCase):
|
||
|
# we don't need more of these. it's unclear if torch's behavior is desired here
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_max_nan(self):
|
||
|
# Reductions with NaN should propagate NaN like PyTorch.
|
||
|
arr = [1.0, float('nan'), 3.0]
|
||
|
torch_out = torch.tensor(arr).max().item()
|
||
|
out = Tensor(arr).max().numpy()
|
||
|
if np.isnan(torch_out):
|
||
|
self.assertTrue(np.isnan(out))
|
||
|
else:
|
||
|
np.testing.assert_equal(out, torch_out)
|
||
|
|
||
|
@unittest.skip("passes on webgpu")
|
||
|
@unittest.expectedFailure
|
||
|
def test_argmax_nan(self):
|
||
|
# PyTorch returns the index of the NaN, tinygrad returns the index of the maximum value.
|
||
|
arr = [1.0, float('nan'), 3.0]
|
||
|
torch_idx = torch.tensor(arr).argmax().item()
|
||
|
idx = Tensor(arr).argmax().item()
|
||
|
self.assertEqual(idx, torch_idx)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_sort_with_nan(self):
|
||
|
# Sorting a tensor containing NaN should keep NaN at the end like PyTorch.
|
||
|
arr = [1.0, float('nan'), 3.0]
|
||
|
torch_vals, torch_idxs = torch.tensor(arr).sort()
|
||
|
vals, idxs = Tensor(arr).sort()
|
||
|
np.testing.assert_equal(vals.numpy(), torch_vals.numpy())
|
||
|
np.testing.assert_equal(idxs.numpy(), torch_idxs.numpy().astype(np.int32))
|
||
|
|
||
|
class TestEmptyTensorEdgeCases(unittest.TestCase):
|
||
|
# we don't need more of these
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_sort_empty(self):
|
||
|
# Sorting an empty tensor works in PyTorch and should return empty
|
||
|
# values and indices. tinygrad raises an error instead.
|
||
|
torch_vals, torch_idxs = torch.tensor([]).sort()
|
||
|
values, indices = Tensor([]).sort()
|
||
|
np.testing.assert_equal(values.numpy(), torch_vals.numpy())
|
||
|
np.testing.assert_equal(indices.numpy(), torch_idxs.numpy().astype(np.int32))
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_max_empty(self):
|
||
|
# Max on an empty tensor should also raise an error.
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.tensor([]).max()
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
Tensor([]).max()
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_argmax_empty(self):
|
||
|
# Argmax on an empty tensor should raise an error like torch does.
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.tensor([]).argmax()
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
Tensor([]).argmax()
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_masked_select_empty(self):
|
||
|
# Masked select on empty tensors should return an empty tensor.
|
||
|
torch_out = torch.tensor([], dtype=torch.float32).masked_select(torch.tensor([], dtype=torch.bool))
|
||
|
out = Tensor([], dtype=dtypes.float32).masked_select(Tensor([], dtype=dtypes.bool))
|
||
|
np.testing.assert_equal(out.numpy(), torch_out.numpy())
|
||
|
|
||
|
class TestRollEdgeCases(unittest.TestCase):
|
||
|
# we don't need more of these
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_roll_mismatched_dims(self):
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.roll(torch.arange(9).reshape(3, 3), 1, dims=(0, 1))
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
Tensor.arange(9).reshape(3, 3).roll(1, dims=(0, 1))
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_roll_extra_shift(self):
|
||
|
# tinygrad ignores extra shift values instead of raising
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.roll(torch.arange(10), (1, 2), dims=0)
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
Tensor.arange(10).roll((1, 2), dims=0)
|
||
|
|
||
|
class TestDropoutProbabilityEdgeCases(unittest.TestCase):
|
||
|
# we don't need more of these
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_dropout_rate_one(self):
|
||
|
# out is full of NaNs it should be 0s
|
||
|
with Tensor.train():
|
||
|
out = Tensor.ones(100).dropout(1.0)
|
||
|
np.testing.assert_allclose(out.numpy(), np.zeros(100))
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_dropout_invalid_prob(self):
|
||
|
# negative dropout probability should raise an error
|
||
|
with self.assertRaises(ValueError):
|
||
|
torch.nn.functional.dropout(torch.ones(10), -0.1, True)
|
||
|
with Tensor.train():
|
||
|
out = Tensor.ones(10).dropout(-0.1)
|
||
|
np.testing.assert_allclose(out.numpy(), np.ones(10))
|
||
|
|
||
|
class TestInputValidation(unittest.TestCase):
|
||
|
# we don't need more of these, input validation bugs are not very interesting, many are WONTFIX
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_repeat_negative(self):
|
||
|
# repeating with a negative value should error like PyTorch
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.tensor([1, 2, 3]).repeat(-1, 2)
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
Tensor([1, 2, 3]).repeat(-1, 2)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_negative_weight_decay(self):
|
||
|
with self.assertRaises(ValueError):
|
||
|
torch.optim.AdamW([torch.tensor([1.], requires_grad=True)], lr=0.1, weight_decay=-0.1)
|
||
|
with self.assertRaises(ValueError):
|
||
|
nn.optim.AdamW([Tensor([1.], requires_grad=True)], lr=0.1, weight_decay=-0.1)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_negative_lr(self):
|
||
|
with self.assertRaises(ValueError):
|
||
|
torch.optim.SGD([torch.tensor([1.], requires_grad=True)], lr=-0.1)
|
||
|
with self.assertRaises(ValueError):
|
||
|
nn.optim.SGD([Tensor([1.], requires_grad=True)], lr=-0.1)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_negative_momentum(self):
|
||
|
with self.assertRaises(ValueError):
|
||
|
torch.optim.SGD([torch.tensor([1.], requires_grad=True)], lr=0.1, momentum=-0.1)
|
||
|
with self.assertRaises(ValueError):
|
||
|
nn.optim.SGD([Tensor([1.], requires_grad=True)], lr=0.1, momentum=-0.1)
|
||
|
|
||
|
class TestZeroFolding(unittest.TestCase):
|
||
|
# we don't need more of these
|
||
|
|
||
|
# folding rules treat x/x, x//x and x%x as constants even when x can be zero
|
||
|
@unittest.expectedFailure
|
||
|
def test_divide_by_self_with_zero(self):
|
||
|
x = Tensor([0.0, 1.0])
|
||
|
torch_out = torch.tensor([0.0, 1.0]) / torch.tensor([0.0, 1.0])
|
||
|
out = (x / x).numpy()
|
||
|
np.testing.assert_allclose(out, torch_out.numpy(), equal_nan=True)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_floordiv_by_self_with_zero(self):
|
||
|
x = Tensor([0])
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.tensor([0]) // torch.tensor([0])
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
(x // x).numpy()
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_mod_by_self_with_zero(self):
|
||
|
x = Tensor([0])
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
torch.tensor([0]) % torch.tensor([0])
|
||
|
with self.assertRaises(RuntimeError):
|
||
|
(x % x).numpy()
|
||
|
|
||
|
class TestArangeUOpValidationIssue(unittest.TestCase):
|
||
|
# these fail with UOp verification error.
|
||
|
# we don't need more of these involving arange
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_large_arange_sum(self):
|
||
|
# Summing a huge arange should either succeed or raise a MemoryError.
|
||
|
n = 2**31 + 3
|
||
|
expected = (n - 1) * n // 2
|
||
|
out = Tensor.arange(n).sum().item()
|
||
|
self.assertEqual(out, expected)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_large_arange_index(self):
|
||
|
# Indexing a huge arange should return the correct value instead of failing
|
||
|
# with a UOp verification error.
|
||
|
n = 2**31 + 3
|
||
|
out = Tensor.arange(n)[0].item()
|
||
|
self.assertEqual(out, 0)
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_large_arange_permute(self):
|
||
|
# Permuting a huge tensor should not trigger UOp verification failures.
|
||
|
n = 2**31 + 3
|
||
|
out = Tensor.arange(n).reshape(n, 1).permute(1, 0)
|
||
|
self.assertEqual(out.shape, (1, n))
|
||
|
out.realize()
|
||
|
|
||
|
class TestAssignIssues(unittest.TestCase):
|
||
|
# these are good failures. i'm not sure we need more, but we need to fix these.
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_assign_permuted_view_constant(self):
|
||
|
# assigning to a permuted view should modify the underlying tensor
|
||
|
arr = np.arange(6).reshape(2, 3).astype(np.float32)
|
||
|
torch_tensor = torch.tensor(arr)
|
||
|
torch_tensor.t().copy_(torch.tensor([[5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]))
|
||
|
t = Tensor(arr).contiguous().realize()
|
||
|
t.permute(1, 0).assign(Tensor([[5.0, 6.0], [7.0, 8.0], [9.0, 10.0]]))
|
||
|
np.testing.assert_allclose(t.numpy(), torch_tensor.numpy())
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_assign_shrink_view_constant(self):
|
||
|
# assigning to a shrunk view should update the base tensor
|
||
|
arr = np.arange(9).reshape(3, 3).astype(np.float32)
|
||
|
torch_tensor = torch.tensor(arr)
|
||
|
torch_tensor[1:3, 1:3] = torch.ones(2, 2)
|
||
|
t = Tensor(arr).contiguous().realize()
|
||
|
t.shrink(((1, 3), (1, 3))).assign(Tensor.ones(2, 2))
|
||
|
np.testing.assert_allclose(t.numpy(), torch_tensor.numpy())
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_assign_broadcast(self):
|
||
|
# broadcasting during assign should behave like PyTorch
|
||
|
torch_tensor = torch.zeros(3, 5)
|
||
|
torch_tensor[:] = torch.arange(5)
|
||
|
t = Tensor.zeros(3, 5)
|
||
|
t.assign(Tensor.arange(5))
|
||
|
np.testing.assert_allclose(t.numpy(), torch_tensor.numpy())
|
||
|
|
||
|
class TestUOpValidationIssue(unittest.TestCase):
|
||
|
# these fail with UOp verification error.
|
||
|
# we want more of these with diverse errors!
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_tensor_index_overflow(self):
|
||
|
# Advanced indexing on tensors expanded past int32 should not error, but
|
||
|
# tinygrad fails with a UOp verification error.
|
||
|
val = Tensor([1])
|
||
|
big = val.expand(2**31 + 3)
|
||
|
idx = Tensor([0, 2**31 + 2])
|
||
|
np.testing.assert_equal(big[idx].numpy(), np.array([1, 1]))
|
||
|
|
||
|
def test_float_floordiv_scalar(self):
|
||
|
with self.assertRaisesRegex(RuntimeError, "UOp verification failed"):
|
||
|
(Tensor.arange(4, dtype=dtypes.float32) // 2).realize()
|
||
|
|
||
|
def test_float_floordiv_tensor(self):
|
||
|
with self.assertRaisesRegex(RuntimeError, "UOp verification failed"):
|
||
|
(Tensor.arange(4, dtype=dtypes.float32) // Tensor.ones(4, dtype=dtypes.float32)).realize()
|
||
|
|
||
|
class TestEdgeCases(unittest.TestCase):
|
||
|
# add tests exposing new and diverse kinds of bugs that might impact real users here
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_circular_pad_negative(self):
|
||
|
# negative pads with circular mode should wrap like PyTorch
|
||
|
arr = np.arange(9).reshape(1, 1, 3, 3).astype(np.float32)
|
||
|
torch_out = torch.nn.functional.pad(torch.tensor(arr), (1, -1, 1, -1), mode='circular')
|
||
|
out = Tensor(arr).pad((1, -1, 1, -1), mode='circular')
|
||
|
np.testing.assert_equal(out.numpy(), torch_out.numpy())
|
||
|
|
||
|
@unittest.expectedFailure
|
||
|
def test_arange_float_step(self):
|
||
|
# float steps should match PyTorch exactly
|
||
|
torch_out = torch.arange(0, 2, 0.3).numpy()
|
||
|
out = Tensor.arange(0, 2, 0.3).numpy()
|
||
|
np.testing.assert_allclose(out, torch_out)
|
||
|
|
||
|
@unittest.skip("this is flaky")
|
||
|
@unittest.expectedFailure
|
||
|
def test_topk_ties_indices(self):
|
||
|
# topk should match PyTorch tie-breaking behavior when values are equal
|
||
|
arr = [1.0, 1.0, 1.0, 1.0]
|
||
|
_, ti = torch.tensor(arr).topk(2)
|
||
|
_, i = Tensor(arr).topk(2)
|
||
|
np.testing.assert_equal(i.numpy(), ti.numpy().astype(np.int32))
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
unittest.main()
|