Repository: bentoml/BentoML

Issue:
BentoML cloudpicke went wrong when checking __module__ attribute from torch.qscheme
**Describe the bug**
BentoML `cloudpicke` went wrong when checking `__module__` attribute from `torch.qscheme`

When saving BentoML service by using the `save` function, I caught an Exception with the following message `AttributeError: 'torch.qscheme' object has no attribute '__module__'`

**To Reproduce**
Steps to reproduce the issue:
1. Create Service class:

```python
import bentoml
from bentoml.adapters import ImageInput
from bentoml.frameworks.pytorch import PytorchModelArtifact


@bentoml.env(infer_pip_packages=True)
@bentoml.artifacts([PytorchModelArtifact("net")])
class PytorchModelService(bentoml.BentoService):
    @bentoml.api(input=ImageInput(), batch=True)
    def predict(self, imgs):
        outputs = self.artifacts.net(imgs)
        return outputs
```
2. Create `DynamicQuantizedLinear` instance:
```python
import torch.nn as nn
import torch

net = nn.quantized.dynamic.Linear(20, 30)
input = torch.randn(128, 20)
output = net(input)
print(output.size())
```

3. Pack `net` artifact and save this service
```python
from service import PytorchModelService
from bentoml.configuration import set_debug_mode
set_debug_mode(True)

svc = PytorchModelService()
svc.pack("net", net)
svc.save()
```
4. See error

**Expected behavior**
Service was saved successfully and show some information like:

```bash
[2022-01-13 12:14:33,080] WARNING - BentoML by default does not include spacy and torchvision package when using PytorchModelArtifact. To make sure BentoML bundle those packages if they are required for your model, either import those packages in BentoService definition file or manually add them via `@env(pip_packages=['torchvision'])` when defining a BentoService
[2022-01-13 12:14:34,362] INFO - BentoService bundle 'PytorchModelService:20220113121433_74DCF1' saved to: /home/niits/bentoml/repository/PytorchModelService/20220113121433_74DCF1
'/home/niits/bentoml/repository/PytorchModelService/20220113121433_74DCF1'
```

**Screenshots/Logs**
```bash
[2022-01-13 12:18:16,175] DEBUG - pip package requirement 'imageio' already exist
[2022-01-13 12:18:16,177] WARNING - BentoML by default does not include spacy and torchvision package when using PytorchModelArtifact. To make sure BentoML bundle those packages if they are required for your model, either import those packages in BentoService definition file or manually add them via `@env(pip_packages=['torchvision'])` when defining a BentoService
[2022-01-13 12:18:16,177] DEBUG - pip package requirement 'torch' already exist
[2022-01-13 12:18:16,178] DEBUG - Creating local YataiService instance
[2022-01-13 12:18:16,182] DEBUG - Upgrading tables to the latest revision
[2022-01-13 12:18:16,186] DEBUG - Created temporary directory: /tmp/bentoml-temp-j1_nauf1
[2022-01-13 12:18:17,154] DEBUG - BentoML in debug mode, keeping temp directory "/tmp/bentoml-temp-j1_nauf1"

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_59812/3996843059.py in <module>
      1 svc = PytorchModelService()
      2 svc.pack("net", net)
----> 3 svc.save()

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/service/__init__.py in save(self, yatai_url, version, labels)
    747         yc = get_yatai_client(yatai_url)
    748 
--> 749         return yc.repository.upload(self, version, labels)
    750 
    751     def save_to_dir(self, path, version=None):

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/yatai/client/bento_repository_api.py in upload(self, bento_service, version, labels)
    194         """
    195         with TempDirectory() as tmpdir:
--> 196             save_to_dir(bento_service, tmpdir, version, silent=True)
    197             return self.upload_from_dir(tmpdir, labels)
    198 

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/saved_bundle/bundler.py in save_to_dir(bento_service, path, version, silent)
    244                 _upload_file_to_remote_path(path, tarfile_path, tarfile_name)
    245     else:
--> 246         _write_bento_content_to_dir(bento_service, path)
    247 
    248     copy_zip_import_archives(

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/saved_bundle/bundler.py in _write_bento_content_to_dir(bento_service, path)
     94 
     95     # save all model artifacts to 'base_path/name/artifacts/' directory
---> 96     bento_service.artifacts.save(module_base_path)
     97 
     98     # write conda environment, requirement.txt

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/service/artifacts/__init__.py in save(self, dst)
    261         for artifact in self.get_artifact_list():
    262             if artifact.is_ready:
--> 263                 artifact.save(save_path)
    264             else:
    265                 logger.warning(

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/service/artifacts/__init__.py in wrapped_save(*args, **kwargs)
    196 
    197                 original = object.__getattribute__(self, item)
--> 198                 return original(*args, **kwargs)
    199 
    200             return wrapped_save

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/frameworks/pytorch.py in save(self, dst)
    160             return torch.jit.save(self._model, self._file_path(dst))
    161 
--> 162         return cloudpickle.dump(self._model, open(self._file_path(dst), "wb"))
    163 
    164 

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/utils/cloudpickle.py in dump(obj, file, protocol)
   1089     compatibility with older versions of Python.
   1090     """
-> 1091     CloudPickler(file, protocol=protocol).dump(obj)
   1092 
   1093 

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/utils/cloudpickle.py in dump(self, obj)
    365         self.inject_addons()
    366         try:
--> 367             return Pickler.dump(self, obj)
    368         except RuntimeError as e:
    369             if 'recursion' in e.args[0]:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in dump(self, obj)
    483         if self.proto >= 4:
    484             self.framer.start_framing()
--> 485         self.save(obj)
    486         self.write(STOP)
    487         self.framer.end_framing()

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    599 
    600         # Save the reduce() output and finally memoize the object
--> 601         self.save_reduce(obj=obj, *rv)
    602 
    603     def persistent_id(self, obj):

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    713         if state is not None:
    714             if state_setter is None:
--> 715                 save(state)
    716                 write(BUILD)
    717             else:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    556             f = self.dispatch.get(t)
    557             if f is not None:
--> 558                 f(self, obj)  # Call unbound method with explicit self
    559                 return
    560 

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_dict(self, obj)
    967 
    968         self.memoize(obj)
--> 969         self._batch_setitems(obj.items())
    970 
    971     dispatch[dict] = save_dict

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in _batch_setitems(self, items)
    993                 for k, v in tmp:
    994                     save(k)
--> 995                     save(v)
    996                 write(SETITEMS)
    997             elif n:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    599 
    600         # Save the reduce() output and finally memoize the object
--> 601         self.save_reduce(obj=obj, *rv)
    602 
    603     def persistent_id(self, obj):

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    709 
    710         if dictitems is not None:
--> 711             self._batch_setitems(dictitems)
    712 
    713         if state is not None:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in _batch_setitems(self, items)
    998                 k, v = tmp[0]
    999                 save(k)
-> 1000                 save(v)
   1001                 write(SETITEM)
   1002             # else tmp is empty, and we're done

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    599 
    600         # Save the reduce() output and finally memoize the object
--> 601         self.save_reduce(obj=obj, *rv)
    602 
    603     def persistent_id(self, obj):

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    713         if state is not None:
    714             if state_setter is None:
--> 715                 save(state)
    716                 write(BUILD)
    717             else:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    556             f = self.dispatch.get(t)
    557             if f is not None:
--> 558                 f(self, obj)  # Call unbound method with explicit self
    559                 return
    560 

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_tuple(self, obj)
    897         write(MARK)
    898         for element in obj:
--> 899             save(element)
    900 
    901         if id(obj) in memo:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    599 
    600         # Save the reduce() output and finally memoize the object
--> 601         self.save_reduce(obj=obj, *rv)
    602 
    603     def persistent_id(self, obj):

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    688         else:
    689             save(func)
--> 690             save(args)
    691             write(REDUCE)
    692 

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    556             f = self.dispatch.get(t)
    557             if f is not None:
--> 558                 f(self, obj)  # Call unbound method with explicit self
    559                 return
    560 

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_tuple(self, obj)
    897         write(MARK)
    898         for element in obj:
--> 899             save(element)
    900 
    901         if id(obj) in memo:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    556             f = self.dispatch.get(t)
    557             if f is not None:
--> 558                 f(self, obj)  # Call unbound method with explicit self
    559                 return
    560 

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save_tuple(self, obj)
    882         if n <= 3 and self.proto >= 2:
    883             for element in obj:
--> 884                 save(element)
    885             # Subtle.  Same as in the big comment below.
    886             if id(obj) in memo:

~/anaconda3/envs/test_env/lib/python3.8/pickle.py in save(self, obj, save_persistent_id)
    585         # Check for string returned by reduce(), meaning "save as global"
    586         if isinstance(rv, str):
--> 587             self.save_global(obj, rv)
    588             return
    589 

~/anaconda3/envs/test_env/lib/python3.8/site-packages/bentoml/utils/cloudpickle.py in save_global(self, obj, name, pack)
    845             return self.save_reduce(type, (NotImplemented,), obj=obj)
    846 
--> 847         if obj.__module__ == "__main__":
    848             return self.save_dynamic_class(obj)
    849 

AttributeError: 'torch.qscheme' object has no attribute '__module__'
```

**Environment:**
 - OS: Ubuntu 20.04.3 LTS
 - Python Version  Python 3.8.0
 - BentoML Version BentoML-0.13.1
 - Torch version 1.10.0
 
**Additional context**

After hot-fixed by checking if `obj` has  `__module__` attribute, saving process worked well and Docker image built from this service worked well without any Exceptions

https://github.com/bentoml/BentoML/blob/3954f36762e10f5df15af7e0ae6dd71f5f214261/bentoml/utils/cloudpickle.py#L847-L848

```python
    if hasattr(obj, "__module__") and obj.__module__ == "__main__":
        return self.save_dynamic_class(obj)
```


Discussion:

Resolving patch:
diff --git a/bentoml/utils/cloudpickle.py b/bentoml/utils/cloudpickle.py
--- a/bentoml/utils/cloudpickle.py
+++ b/bentoml/utils/cloudpickle.py
@@ -844,7 +844,7 @@ def save_global(self, obj, name=None, pack=struct.pack):
         elif obj is type(NotImplemented):
             return self.save_reduce(type, (NotImplemented,), obj=obj)
 
-        if obj.__module__ == "__main__":
+        if hasattr(obj, "__module__") and obj.__module__ == "__main__":
             return self.save_dynamic_class(obj)
 
         try:


Test patch: