We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Round trip `torch.save` / `torch.load` not working for `Hyperplane`
### Describe the bug

Crashing when trying to save and reload a `Hyperplane` using `torch`:

```
Traceback (most recent call last):
  File "save_hyperplane.py", line 16, in <module>
    plane = torch.load("./saved_plane.pt")
  File "/home/kyle/venv/lib/python3.8/site-packages/torch/serialization.py", line 789, in load
    return _load(opened_zipfile, map_location, pickle_module, **pickle_load_args)
  File "/home/kyle/venv/lib/python3.8/site-packages/torch/serialization.py", line 1131, in _load
    result = unpickler.load()
  File "/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py", line 48, in __getattr__
    self.used_attrs.add(name)
  File "/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py", line 48, in __getattr__
    self.used_attrs.add(name)
  File "/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py", line 48, in __getattr__
    self.used_attrs.add(name)
  [Previous line repeated 993 more times]
RecursionError: maximum recursion depth exceeded
```

### Reproduction steps

```bash
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install kornia
(venv) $ pip install numpy
(venv) $ python save_hyperplane.py
```

Content of save_hyperplane.py

```
from kornia.geometry.plane import Hyperplane
from kornia.geometry.vector import Vec3, Scalar
import torch

plane = Hyperplane(
    Vec3(torch.tensor([0,0,1])), Scalar(torch.tensor(0.5))
)

torch.save(plane, "./saved_plane.pt")
plane = torch.load("./saved_plane.pt")
```

### Expected behavior

Hyperplane is able to be saved and reloaded with `torch.save` & `torch.load` 

### Environment

```shell
Collecting environment information...
PyTorch version: 1.13.1+cu117
Is debug build: False
CUDA used to build PyTorch: 11.7
ROCM used to build PyTorch: N/A

OS: Ubuntu 20.04.5 LTS (x86_64)
GCC version: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Clang version: Could not collect
CMake version: version 3.16.3
Libc version: glibc-2.31

Python version: 3.8.10 (default, Nov 14 2022, 12:59:47)  [GCC 9.4.0] (64-bit runtime)
Python platform: Linux-5.15.0-56-generic-x86_64-with-glibc2.29
Is CUDA available: True
CUDA runtime version: Could not collect
CUDA_MODULE_LOADING set to: LAZY
GPU models and configuration: GPU 0: NVIDIA GeForce RTX 3080 Laptop GPU
Nvidia driver version: 470.141.03
cuDNN version: Could not collect
HIP runtime version: N/A
MIOpen runtime version: N/A
Is XNNPACK available: True

Versions of relevant libraries:
[pip3] numpy==1.24.1
[pip3] torch==1.13.1
[conda] Could not collect
```

### Additional context

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kornia/core/tensor_wrapper.py`
Content:
```
1 # pytorch tensor wrapper class
2 # insipired by:
3 # https://github.com/pytorch/pytorch/blob/591dfffa38848de54b7f5f4e49260847024c9281/test/test_overrides.py#L748
4 import collections
5 from typing import Any
6 
7 import torch
8 from torch import Tensor
9 
10 # wrap inputs if necessary
11 
12 # TODO: promote to KORNIA_WRAP
13 
14 
15 def wrap(v, cls):
16     if type(v) in {tuple, list}:
17         return type(v)(wrap(vi, cls) for vi in v)
18 
19     return cls(v) if isinstance(v, Tensor) else v
20 
21 
22 # TODO: promote to KORNIA_UNWRAP
23 
24 
25 def unwrap(v):
26     if type(v) in {tuple, list}:
27         return type(v)(unwrap(vi) for vi in v)
28 
29     return v._data if not isinstance(v, Tensor) else v
30 
31 
32 class TensorWrapper:
33     def __init__(self, data: Tensor) -> None:
34         self.__dict__["_data"] = data
35         self.__dict__["used_attrs"] = set()
36         self.__dict__["used_calls"] = set()
37 
38     @property
39     def data(self) -> Tensor:
40         return self._data
41 
42     def __repr__(self) -> str:
43         return f"{self.data}"
44 
45     def __getattr__(self, name):
46         if name in self.__dict__:
47             return self.__dict__[name]
48         self.used_attrs.add(name)
49 
50         val = getattr(self._data, name)
51 
52         # NOTE: not clear is really needed
53         # If it's a method
54         # if callable(val):
55         #    c = getattr(type(self._data), name)
56         #    # Don't append self to args if classmethod/staticmethod
57         #    if c is val:
58         #        return lambda *a, **kw: wrap(self.__torch_function__(c, (type(self),), args=a, kwargs=kw), type(self))
59         #    # Otherwise append self to args
60         #    return lambda *a, **kw: wrap(
61         #        #self.__torch_function__(c, (type(self),), args=(self,) + a, kwargs=kw), type(self)
62         #    )
63 
64         return wrap(val, type(self))
65 
66     def __setattr__(self, name, value):
67         if name in self.__dict__:
68             self.__dict__[name] = value
69 
70         self.used_attrs.add(name)
71         setattr(self._data, name, value)
72 
73     def __setitem__(self, key, value):
74         self._data[key] = value
75 
76     def __getitem__(self, key):
77         return wrap(self._data[key], type(self))
78 
79     @classmethod
80     def __torch_function__(cls, func, types, args=(), kwargs=None):
81         if kwargs is None:
82             kwargs = {}
83         # Find an instance of this class in the arguments
84         args_of_this_cls = []
85         for a in args:
86             if isinstance(a, cls):
87                 args_of_this_cls.append(a)
88             elif isinstance(a, collections.abc.Sequence):
89                 args_of_this_cls.extend(el for el in a if isinstance(el, cls))
90         # assert len(args_of_this_cls) > 0
91         for a in args_of_this_cls:
92             a.used_calls.add(func)
93         args = unwrap(tuple(args))
94         kwargs = {k: unwrap(v) for k, v in kwargs.items()}
95 
96         return wrap(func(*args, **kwargs), cls)
97 
98     # TODO: `def __add__(self, other) -> Self:` when mypy release >0.991
99     def __add__(self, other):
100         return self.__unary_op__(torch.add, other)
101 
102     def __mul__(self, other):
103         return self.__unary_op__(torch.mul, other)
104 
105     def __sub__(self, other):
106         return self.__unary_op__(torch.sub, other)
107 
108     def __truediv__(self, other):
109         return self.__unary_op__(torch.true_divide, other)
110 
111     def __floordiv__(self, other):
112         return self.__unary_op__(torch.floor_divide, other)
113 
114     def __ge__(self, other):
115         return self.__unary_op__(torch.ge, other)
116 
117     def __gt__(self, other):
118         return self.__unary_op__(torch.gt, other)
119 
120     def __lt__(self, other):
121         return self.__unary_op__(torch.lt, other)
122 
123     def __le__(self, other):
124         return self.__unary_op__(torch.le, other)
125 
126     def __eq__(self, other):
127         return self.__unary_op__(torch.eq, other)
128 
129     def __ne__(self, other):
130         return self.__unary_op__(torch.ne, other)
131 
132     def __bool__(self):
133         return self.__unary_op__(Tensor.__bool__)
134 
135     def __int__(self):
136         return self.__unary_op__(Tensor.__int__)
137 
138     def __neg__(self):
139         return self.__unary_op__(Tensor.negative)
140 
141     def __unary_op__(self, func: Any, other=None):
142         args = (self, other) if other is not None else (self,)
143         return self.__torch_function__(func, (type(self),), args)
144 
145     def __len__(self):
146         return len(self._data)
147
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kornia/core/tensor_wrapper.py b/kornia/core/tensor_wrapper.py
--- a/kornia/core/tensor_wrapper.py
+++ b/kornia/core/tensor_wrapper.py
@@ -35,15 +35,22 @@
         self.__dict__["used_attrs"] = set()
         self.__dict__["used_calls"] = set()
 
-    @property
-    def data(self) -> Tensor:
-        return self._data
+    def unwrap(self):
+        return unwrap(self)
+
+    def __getstate__(self):
+        return self.__dict__
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
 
     def __repr__(self) -> str:
-        return f"{self.data}"
+        return f"{self._data}"
 
     def __getattr__(self, name):
-        if name in self.__dict__:
+        if name == "data":
+            return self._data
+        elif name in self.__dict__:
             return self.__dict__[name]
         self.used_attrs.add(name)

{"golden_diff": "diff --git a/kornia/core/tensor_wrapper.py b/kornia/core/tensor_wrapper.py\n--- a/kornia/core/tensor_wrapper.py\n+++ b/kornia/core/tensor_wrapper.py\n@@ -35,15 +35,22 @@\n         self.__dict__[\"used_attrs\"] = set()\n         self.__dict__[\"used_calls\"] = set()\n \n-    @property\n-    def data(self) -> Tensor:\n-        return self._data\n+    def unwrap(self):\n+        return unwrap(self)\n+\n+    def __getstate__(self):\n+        return self.__dict__\n+\n+    def __setstate__(self, state):\n+        self.__dict__.update(state)\n \n     def __repr__(self) -> str:\n-        return f\"{self.data}\"\n+        return f\"{self._data}\"\n \n     def __getattr__(self, name):\n-        if name in self.__dict__:\n+        if name == \"data\":\n+            return self._data\n+        elif name in self.__dict__:\n             return self.__dict__[name]\n         self.used_attrs.add(name)\n", "issue": "Round trip `torch.save` / `torch.load` not working for `Hyperplane`\n### Describe the bug\r\n\r\nCrashing when trying to save and reload a `Hyperplane` using `torch`:\r\n\r\n```\r\nTraceback (most recent call last):\r\n  File \"save_hyperplane.py\", line 16, in <module>\r\n    plane = torch.load(\"./saved_plane.pt\")\r\n  File \"/home/kyle/venv/lib/python3.8/site-packages/torch/serialization.py\", line 789, in load\r\n    return _load(opened_zipfile, map_location, pickle_module, **pickle_load_args)\r\n  File \"/home/kyle/venv/lib/python3.8/site-packages/torch/serialization.py\", line 1131, in _load\r\n    result = unpickler.load()\r\n  File \"/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py\", line 48, in __getattr__\r\n    self.used_attrs.add(name)\r\n  File \"/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py\", line 48, in __getattr__\r\n    self.used_attrs.add(name)\r\n  File \"/home/kyle/venv/lib/python3.8/site-packages/kornia/core/tensor_wrapper.py\", line 48, in __getattr__\r\n    self.used_attrs.add(name)\r\n  [Previous line repeated 993 more times]\r\nRecursionError: maximum recursion depth exceeded\r\n```\r\n\r\n### Reproduction steps\r\n\r\n```bash\r\n$ python3 -m venv venv\r\n$ source venv/bin/activate\r\n(venv) $ pip install kornia\r\n(venv) $ pip install numpy\r\n(venv) $ python save_hyperplane.py\r\n```\r\n\r\nContent of save_hyperplane.py\r\n\r\n```\r\nfrom kornia.geometry.plane import Hyperplane\r\nfrom kornia.geometry.vector import Vec3, Scalar\r\nimport torch\r\n\r\nplane = Hyperplane(\r\n    Vec3(torch.tensor([0,0,1])), Scalar(torch.tensor(0.5))\r\n)\r\n\r\ntorch.save(plane, \"./saved_plane.pt\")\r\nplane = torch.load(\"./saved_plane.pt\")\r\n```\r\n\r\n### Expected behavior\r\n\r\nHyperplane is able to be saved and reloaded with `torch.save` & `torch.load` \r\n\r\n### Environment\r\n\r\n```shell\r\nCollecting environment information...\r\nPyTorch version: 1.13.1+cu117\r\nIs debug build: False\r\nCUDA used to build PyTorch: 11.7\r\nROCM used to build PyTorch: N/A\r\n\r\nOS: Ubuntu 20.04.5 LTS (x86_64)\r\nGCC version: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0\r\nClang version: Could not collect\r\nCMake version: version 3.16.3\r\nLibc version: glibc-2.31\r\n\r\nPython version: 3.8.10 (default, Nov 14 2022, 12:59:47)  [GCC 9.4.0] (64-bit runtime)\r\nPython platform: Linux-5.15.0-56-generic-x86_64-with-glibc2.29\r\nIs CUDA available: True\r\nCUDA runtime version: Could not collect\r\nCUDA_MODULE_LOADING set to: LAZY\r\nGPU models and configuration: GPU 0: NVIDIA GeForce RTX 3080 Laptop GPU\r\nNvidia driver version: 470.141.03\r\ncuDNN version: Could not collect\r\nHIP runtime version: N/A\r\nMIOpen runtime version: N/A\r\nIs XNNPACK available: True\r\n\r\nVersions of relevant libraries:\r\n[pip3] numpy==1.24.1\r\n[pip3] torch==1.13.1\r\n[conda] Could not collect\r\n```\r\n\r\n### Additional context\r\n\r\n_No response_\n", "before_files": [{"content": "# pytorch tensor wrapper class\n# insipired by:\n# https://github.com/pytorch/pytorch/blob/591dfffa38848de54b7f5f4e49260847024c9281/test/test_overrides.py#L748\nimport collections\nfrom typing import Any\n\nimport torch\nfrom torch import Tensor\n\n# wrap inputs if necessary\n\n# TODO: promote to KORNIA_WRAP\n\n\ndef wrap(v, cls):\n    if type(v) in {tuple, list}:\n        return type(v)(wrap(vi, cls) for vi in v)\n\n    return cls(v) if isinstance(v, Tensor) else v\n\n\n# TODO: promote to KORNIA_UNWRAP\n\n\ndef unwrap(v):\n    if type(v) in {tuple, list}:\n        return type(v)(unwrap(vi) for vi in v)\n\n    return v._data if not isinstance(v, Tensor) else v\n\n\nclass TensorWrapper:\n    def __init__(self, data: Tensor) -> None:\n        self.__dict__[\"_data\"] = data\n        self.__dict__[\"used_attrs\"] = set()\n        self.__dict__[\"used_calls\"] = set()\n\n    @property\n    def data(self) -> Tensor:\n        return self._data\n\n    def __repr__(self) -> str:\n        return f\"{self.data}\"\n\n    def __getattr__(self, name):\n        if name in self.__dict__:\n            return self.__dict__[name]\n        self.used_attrs.add(name)\n\n        val = getattr(self._data, name)\n\n        # NOTE: not clear is really needed\n        # If it's a method\n        # if callable(val):\n        #    c = getattr(type(self._data), name)\n        #    # Don't append self to args if classmethod/staticmethod\n        #    if c is val:\n        #        return lambda *a, **kw: wrap(self.__torch_function__(c, (type(self),), args=a, kwargs=kw), type(self))\n        #    # Otherwise append self to args\n        #    return lambda *a, **kw: wrap(\n        #        #self.__torch_function__(c, (type(self),), args=(self,) + a, kwargs=kw), type(self)\n        #    )\n\n        return wrap(val, type(self))\n\n    def __setattr__(self, name, value):\n        if name in self.__dict__:\n            self.__dict__[name] = value\n\n        self.used_attrs.add(name)\n        setattr(self._data, name, value)\n\n    def __setitem__(self, key, value):\n        self._data[key] = value\n\n    def __getitem__(self, key):\n        return wrap(self._data[key], type(self))\n\n    @classmethod\n    def __torch_function__(cls, func, types, args=(), kwargs=None):\n        if kwargs is None:\n            kwargs = {}\n        # Find an instance of this class in the arguments\n        args_of_this_cls = []\n        for a in args:\n            if isinstance(a, cls):\n                args_of_this_cls.append(a)\n            elif isinstance(a, collections.abc.Sequence):\n                args_of_this_cls.extend(el for el in a if isinstance(el, cls))\n        # assert len(args_of_this_cls) > 0\n        for a in args_of_this_cls:\n            a.used_calls.add(func)\n        args = unwrap(tuple(args))\n        kwargs = {k: unwrap(v) for k, v in kwargs.items()}\n\n        return wrap(func(*args, **kwargs), cls)\n\n    # TODO: `def __add__(self, other) -> Self:` when mypy release >0.991\n    def __add__(self, other):\n        return self.__unary_op__(torch.add, other)\n\n    def __mul__(self, other):\n        return self.__unary_op__(torch.mul, other)\n\n    def __sub__(self, other):\n        return self.__unary_op__(torch.sub, other)\n\n    def __truediv__(self, other):\n        return self.__unary_op__(torch.true_divide, other)\n\n    def __floordiv__(self, other):\n        return self.__unary_op__(torch.floor_divide, other)\n\n    def __ge__(self, other):\n        return self.__unary_op__(torch.ge, other)\n\n    def __gt__(self, other):\n        return self.__unary_op__(torch.gt, other)\n\n    def __lt__(self, other):\n        return self.__unary_op__(torch.lt, other)\n\n    def __le__(self, other):\n        return self.__unary_op__(torch.le, other)\n\n    def __eq__(self, other):\n        return self.__unary_op__(torch.eq, other)\n\n    def __ne__(self, other):\n        return self.__unary_op__(torch.ne, other)\n\n    def __bool__(self):\n        return self.__unary_op__(Tensor.__bool__)\n\n    def __int__(self):\n        return self.__unary_op__(Tensor.__int__)\n\n    def __neg__(self):\n        return self.__unary_op__(Tensor.negative)\n\n    def __unary_op__(self, func: Any, other=None):\n        args = (self, other) if other is not None else (self,)\n        return self.__torch_function__(func, (type(self),), args)\n\n    def __len__(self):\n        return len(self._data)\n", "path": "kornia/core/tensor_wrapper.py"}], "after_files": [{"content": "# pytorch tensor wrapper class\n# insipired by:\n# https://github.com/pytorch/pytorch/blob/591dfffa38848de54b7f5f4e49260847024c9281/test/test_overrides.py#L748\nimport collections\nfrom typing import Any\n\nimport torch\nfrom torch import Tensor\n\n# wrap inputs if necessary\n\n# TODO: promote to KORNIA_WRAP\n\n\ndef wrap(v, cls):\n    if type(v) in {tuple, list}:\n        return type(v)(wrap(vi, cls) for vi in v)\n\n    return cls(v) if isinstance(v, Tensor) else v\n\n\n# TODO: promote to KORNIA_UNWRAP\n\n\ndef unwrap(v):\n    if type(v) in {tuple, list}:\n        return type(v)(unwrap(vi) for vi in v)\n\n    return v._data if not isinstance(v, Tensor) else v\n\n\nclass TensorWrapper:\n    def __init__(self, data: Tensor) -> None:\n        self.__dict__[\"_data\"] = data\n        self.__dict__[\"used_attrs\"] = set()\n        self.__dict__[\"used_calls\"] = set()\n\n    def unwrap(self):\n        return unwrap(self)\n\n    def __getstate__(self):\n        return self.__dict__\n\n    def __setstate__(self, state):\n        self.__dict__.update(state)\n\n    def __repr__(self) -> str:\n        return f\"{self._data}\"\n\n    def __getattr__(self, name):\n        if name == \"data\":\n            return self._data\n        elif name in self.__dict__:\n            return self.__dict__[name]\n        self.used_attrs.add(name)\n\n        val = getattr(self._data, name)\n\n        # NOTE: not clear is really needed\n        # If it's a method\n        # if callable(val):\n        #    c = getattr(type(self._data), name)\n        #    # Don't append self to args if classmethod/staticmethod\n        #    if c is val:\n        #        return lambda *a, **kw: wrap(self.__torch_function__(c, (type(self),), args=a, kwargs=kw), type(self))\n        #    # Otherwise append self to args\n        #    return lambda *a, **kw: wrap(\n        #        #self.__torch_function__(c, (type(self),), args=(self,) + a, kwargs=kw), type(self)\n        #    )\n\n        return wrap(val, type(self))\n\n    def __setattr__(self, name, value):\n        if name in self.__dict__:\n            self.__dict__[name] = value\n\n        self.used_attrs.add(name)\n        setattr(self._data, name, value)\n\n    def __setitem__(self, key, value):\n        self._data[key] = value\n\n    def __getitem__(self, key):\n        return wrap(self._data[key], type(self))\n\n    @classmethod\n    def __torch_function__(cls, func, types, args=(), kwargs=None):\n        if kwargs is None:\n            kwargs = {}\n        # Find an instance of this class in the arguments\n        args_of_this_cls = []\n        for a in args:\n            if isinstance(a, cls):\n                args_of_this_cls.append(a)\n            elif isinstance(a, collections.abc.Sequence):\n                args_of_this_cls.extend(el for el in a if isinstance(el, cls))\n        # assert len(args_of_this_cls) > 0\n        for a in args_of_this_cls:\n            a.used_calls.add(func)\n        args = unwrap(tuple(args))\n        kwargs = {k: unwrap(v) for k, v in kwargs.items()}\n\n        return wrap(func(*args, **kwargs), cls)\n\n    # TODO: `def __add__(self, other) -> Self:` when mypy release >0.991\n    def __add__(self, other):\n        return self.__unary_op__(torch.add, other)\n\n    def __mul__(self, other):\n        return self.__unary_op__(torch.mul, other)\n\n    def __sub__(self, other):\n        return self.__unary_op__(torch.sub, other)\n\n    def __truediv__(self, other):\n        return self.__unary_op__(torch.true_divide, other)\n\n    def __floordiv__(self, other):\n        return self.__unary_op__(torch.floor_divide, other)\n\n    def __ge__(self, other):\n        return self.__unary_op__(torch.ge, other)\n\n    def __gt__(self, other):\n        return self.__unary_op__(torch.gt, other)\n\n    def __lt__(self, other):\n        return self.__unary_op__(torch.lt, other)\n\n    def __le__(self, other):\n        return self.__unary_op__(torch.le, other)\n\n    def __eq__(self, other):\n        return self.__unary_op__(torch.eq, other)\n\n    def __ne__(self, other):\n        return self.__unary_op__(torch.ne, other)\n\n    def __bool__(self):\n        return self.__unary_op__(Tensor.__bool__)\n\n    def __int__(self):\n        return self.__unary_op__(Tensor.__int__)\n\n    def __neg__(self):\n        return self.__unary_op__(Tensor.negative)\n\n    def __unary_op__(self, func: Any, other=None):\n        args = (self, other) if other is not None else (self,)\n        return self.__torch_function__(func, (type(self),), args)\n\n    def __len__(self):\n        return len(self._data)\n", "path": "kornia/core/tensor_wrapper.py"}]}