openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
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

# 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()