Repository: tinygrad/tinygrad

Issue:
Can't create tensor with empty buffer in metal, gpu 
```
# ====== Works ======

In [21]: Tensor([], device="TORCH").realize()
Out[21]: <Tensor <LB (0,) dtypes.float op=buffer<0, dtypes.float> st=ShapeTracker(shape=(0,), views=[View(shape=(0,), strides=(1,), offset=0, mas
k=None, contiguous=True, shape_strides=((0, 1),))])> on TORCH with grad None>

In [28]: Tensor([], device="LLVM").realize()
Out[28]: <Tensor <LB (0,) dtypes.float op=buffer<0, dtypes.float> st=ShapeTracker(shape=(0,), views=[View(shape=(0,), strides=(1,), offset=0, mask=None, contiguous=True, shape_strides=((0, 1),))])> on LLVM with grad None>


# ====== Doesn't Work ======

In [25]: Tensor([], device="GPU").realize()
LogicError: create_buffer failed: INVALID_BUFFER_SIZE

In [26]: Tensor([], device="METAL").realize()
AttributeError: 'NoneType' object has no attribute 'contents'
```

Not sure if this works on CUDA since I don't have nvidiagpu



Discussion:
I'm trying random shtuff and by adding
`elif size == 0: buf = cl.Buffer(CL.cl_ctxs[int(device)], cl.mem_flags.READ_WRITE, dtype.itemsize)`
to CLBuffer.__init__() fixes the GPU error.
Is this proper pyopencl?????
Does anyone know?
> I'm trying random shtuff and by adding `elif size == 0: buf = cl.Buffer(CL.cl_ctxs[int(device)], cl.mem_flags.READ_WRITE, dtype.itemsize)` to CLBuffer.**init**() fixes the GPU error. Is this proper pyopencl????? Does anyone know?

Yea, so this does make a buffer with a size of 1 item, because you can't really create a buffer of size 0, that's why stuff is failing.
I believe we should not create a size 0 Tensor. 
I guess when size == 0 we do `cl.Buffer(CL.cl_ctxs[int(device)], cl.mem_flags.READ_WRITE, dtype.itemsize)` is okay since this matches torch?
```
In [10]: torch.tensor([], dtype=torch.float32).element_size()
Out[10]: 4
```
which is the dtype.itemsize of a float32

----------------



> I believe we should not create a size 0 Tensor.

what do you mean? As in there should not be support for 0 shaped tensors or?
> I guess when size == 0 we do `cl.Buffer(CL.cl_ctxs[int(device)], cl.mem_flags.READ_WRITE, dtype.itemsize)` is okay since this matches torch?
> 
> ```
> In [10]: torch.tensor([], dtype=torch.float32).element_size()
> Out[10]: 4
> ```
> 
> which is the dtype.itemsize of a float32
> 
> > I believe we should not create a size 0 Tensor.
> 
> what do you mean? As in there should not be support for 0 shaped tensors or?

Apologies for any shortcomings in my English.
I mean **I am of the opinion that producing tensors with a size of 0 should be avoided.**
There is a distinction between 0 shaped tensors and tensors with 0 size.
If we wanna allocate device buffer of at least size 1byte to fix the problem, I can handle this with a single line in the coming allocator PR.
Yea, that should do it, and seems to match torch behaviour as well.

Resolving patch:
diff --git a/tinygrad/runtime/lib.py b/tinygrad/runtime/lib.py
--- a/tinygrad/runtime/lib.py
+++ b/tinygrad/runtime/lib.py
@@ -41,13 +41,13 @@ def _copyin(self, x:np.ndarray) -> None: raise NotImplementedError("must be impl
   @classmethod
   def fromCPU(cls, x:np.ndarray, **kwargs):
     ret = cls(prod(x.shape), dtypes.from_np(x.dtype), **kwargs)
-    ret._copyin(x)
+    if x.size > 0: ret._copyin(x)
     return ret
 
 class RawBufferMapped(RawBufferCopyIn):
   def _buffer(self) -> memoryview: raise NotImplementedError("must be implemented")
   # NOTE: this metadata prevents the backing buffer from being freed. hack can be removed with PEP688
-  def toCPU(self) -> np.ndarray: return np.frombuffer(self._buffer(), dtype=np.dtype(self.dtype.np, metadata={"backing": self}))  # type: ignore
+  def toCPU(self) -> np.ndarray: return np.frombuffer(self._buffer(), dtype=np.dtype(self.dtype.np, metadata={"backing": self}), count=self.size)  # type: ignore
   def _copyin(self, x:np.ndarray) -> None: np.copyto(self.toCPU(), x.reshape(-1))
 
 # this one is simple enough that i moved it out of the runtimes
@@ -60,7 +60,7 @@ def _copyout(self, x:np.ndarray) -> None: raise NotImplementedError("must be imp
 
   def toCPU(self) -> np.ndarray:
     x: np.ndarray = np.empty(self.size, dtype=self.dtype.np)
-    self._copyout(x)
+    if x.size > 0: self._copyout(x)
     return x
 
 class RawBufferTransfer(RawBuffer):
@@ -90,7 +90,7 @@ def _alloc_buffer(self, size, dtype, device, **kwargs):
     while len(self.aging_order[device]) and self.free_space[device] < 0: # When OOM removing lru buffers.
       bucket, epoch = self.aging_order[device].popleft()
       if self.cached_buffers[bucket] and self.cached_buffers[bucket][-1][1] == epoch: self._free_buffer(self.cached_buffers[bucket].pop()[0]) # Free cached buffer if it is still in cache.
-    newbuf = self._do_alloc(size, dtype, device, **kwargs)
+    newbuf = self._do_alloc(max(1, size), dtype, device, **kwargs)
     self.buffer_info[newbuf] = (size, dtype, device)
     return newbuf
   def _free_buffer(self, buf_to_free):


Test patch:
diff --git a/test/test_allocators.py b/test/test_allocators.py
--- a/test/test_allocators.py
+++ b/test/test_allocators.py
@@ -53,10 +53,11 @@ def cmp_trace_and_buf(buf, trace_ref): return trace_ref and trace_ref() == buf._
 
 class TestAllocators(unittest.TestCase):
   def test_lru_allocator_reusage(self):
+    mc, mu = GlobalCounters.mem_cached, GlobalCounters.mem_used
     def test():
       lru_allocator = FakeAllocator(2048)
       traced_buf = alloc_free_trace(lru_allocator, 16, dtypes.float32)
-      assert GlobalCounters.mem_cached == 16*dtypes.float32.itemsize, "Buffer should be cached"
+      assert GlobalCounters.mem_cached - mc == 16*dtypes.float32.itemsize, "Buffer should be cached"
       for _ in range(32):
         def __test():
           buf = alloc(lru_allocator, 16, dtypes.float32)
@@ -69,19 +70,20 @@ def __test():
           buf = alloc(lru_allocator, 16, dtypes.float32)
           assert usedbuf != buf, "Nobody should get used buffer"
         __test()
-      assert GlobalCounters.mem_used == 16*dtypes.float32.itemsize, "Only usedbuf is still allocated."
+      assert GlobalCounters.mem_used - mu == 16*dtypes.float32.itemsize, "Only usedbuf is still allocated."
     test()
     check_gc()
 
   def test_lru_allocator_cache_free(self):
+    mc, mu = GlobalCounters.mem_cached, GlobalCounters.mem_used
     def test():
       lru_allocator = FakeAllocator(128)
       refs = []
       for _ in range(32):
         refs.append(alloc_free_trace(lru_allocator, 16, dtypes.float32))
-      for sz in range(32):
+      for sz in range(1, 32):
         alloc_free_trace(lru_allocator, sz, dtypes.float32)
-        assert GlobalCounters.mem_used + GlobalCounters.mem_cached <= 128, "Should not allocate on device more than allowed (128)"
+        assert GlobalCounters.mem_used + GlobalCounters.mem_cached - mc - mu <= 128, "Should not allocate on device more than allowed (128)"
       for r in refs: assert r() is None, "All refs should be dead, since buffers were cleared from cache"
     test()
     check_gc()
diff --git a/test/test_tensor.py b/test/test_tensor.py
--- a/test/test_tensor.py
+++ b/test/test_tensor.py
@@ -220,5 +220,9 @@ def test_deepwalk_ctx_check(self):
     x = Tensor.randn(1, 1, 1)
     x.dot(layer).mean().backward()
 
+  def test_zerosized_tensors(self):
+    Tensor([]).realize()
+    Tensor([]).numpy()
+
 if __name__ == '__main__':
   unittest.main()