Repository: deepset-ai/haystack
Pull request: first version of save_to_remote for HF from FarmReader
Fixes #2416 

This draft PR introduces the `save_to_remote()` function to the `FarmReader` 
It converts a model to the transformer models format using the `convert_to_transformer` method from the `AdaptiveModel` class.
I have tested this by fine-tuning a model and then calling `save_to_remote` - then loading that same model back into `FarmReader` and using it in an `ExtractiveQuestionAnswering` pipeline

(PS: sentence-transformers implements this in their own repo and I was able to reuse their method of tracking files for lfs)

**To do and test**
- I am unsure how this would behave in the case the repo already exists on HF. The `create_repo` method from huggingface_hub has an `exists_ok` argument which I've set to True but this still needs testing
- The draft/default model card
- Testing that this structure of model is ok and can generalise to other types of models(?) that could be trained with FarmReader
- Once we're happy with this, we would need the same for the other two classes: `DensePassageRetriever` and `TableTextRetriever`

**Status (please check what you already did)**:
- [x] First draft (up for discussions & feedback)
- [ ] Final code
- [ ] [Enable actions on your fork](https://github.com/deepset-ai/haystack/blob/master/CONTRIBUTING.md#forks)
- [ ] Added tests
- [ ] Updated documentation


Problem statement:
Autoupload models to huggingface hub after training
**Is your feature request related to a problem? Please describe.**
When training with FARMReader finishes, there should be an option to upload it automatically to HF hub.

**Describe the solution you'd like**
Add option and template to auto upload trained / fine-tuned model to HF.

**Describe alternatives you've considered**
Implement helper function to upload model to HF manually. Maybe we go for that if the selection of the model snapshot involves human interaction.

**Tasks**:
- [ ] add `hf_token`, `hf_repo`, `hf_branch`, `hf_model_card` params to `FARMReader.train()`, `DensePassageRetriever.train()` and `TableTextRetriever.train()`
- [ ] create repo if not exists
- [ ] convert model into huggingface format
- [ ] upload model files and model card
- [ ] provide model card template
- [ ] write documentation about how to use it

Hints from issue discussion:
Using [huggingface_hub lib](https://github.com/huggingface/huggingface_hub) we can [create a repo](https://huggingface.co/docs/hub/how-to-upstream#%60Repository%60) and upload them model files. To add a model card, we simply have to upload a README.md according to [this](https://huggingface.co/docs/hub/model-repos#what-are-model-cards-and-why-are-they-useful) structure.
Additionally we might want to enable users to specify a version of the model. This could be achieved by using [branches](https://huggingface.co/docs/hub/how-to-upstream#branch) in the model repo.
Sentencetranformers lib also offers this kind of functionality. Maybe we can get some inspiration from there, too.
Additionally, it might make sense to make this available for distilled models too. Something that would allow you to do:

```
student.distil_intermediate_layers_from(teacher, data_dir=".", train_filename="augmented_dataset.json", use_gpu=True)
student.distil_prediction_layer_from(teacher, data_dir="data/squad20", train_filename="dev-v2.0.json", use_gpu=True)
student.push_to_hf()
```

In this case, a helper function might make more sense though as there won't be a .train() to provide the HF options to.

Commit message: Update Documentation & Code Style

Patch to review:
diff --git a/haystack/nodes/reader/farm.py b/haystack/nodes/reader/farm.py
index 260528d0a9..70ed5a1cc0 100644
--- a/haystack/nodes/reader/farm.py
+++ b/haystack/nodes/reader/farm.py
@@ -4,6 +4,8 @@
 import multiprocessing
 from pathlib import Path
 from collections import defaultdict
+import os
+import tempfile
 from time import perf_counter
 import torch
 
@@ -24,6 +26,7 @@
 from haystack.document_stores.base import BaseDocumentStore
 from haystack.nodes.reader.base import BaseReader
 
+from huggingface_hub import create_repo, HfFolder, Repository
 
 logger = logging.getLogger(__name__)
 
@@ -688,6 +691,50 @@ def save(self, directory: Path):
         self.inferencer.model.save(directory)
         self.inferencer.processor.save(directory)
 
+    def save_to_remote(
+        self,
+        model_name: str,
+        hf_organization: Optional[str] = None,
+        private: Optional[bool] = None,
+        commit_message: str = "Add new model to Hugging Face.",
+    ):
+
+        token = HfFolder.get_token()
+        if token is None:
+            raise ValueError(
+                "You must login to the Hugging Face hub on this computer by typing `transformers-cli login`."
+            )
+
+        repo_url = create_repo(
+            token, model_name, organization=hf_organization, private=private, repo_type=None, exist_ok=True
+        )
+
+        transformer_models = self.inferencer.model.convert_to_transformers()
+
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            repo = Repository(tmp_dir, clone_from=repo_url)
+
+            self.save(tmp_dir)
+            transformer_models[0].save_pretrained(tmp_dir)
+
+            large_files = []
+            for root, dirs, files in os.walk(tmp_dir):
+                for filename in files:
+                    file_path = os.path.join(root, filename)
+                    rel_path = os.path.relpath(file_path, tmp_dir)
+
+                    if os.path.getsize(file_path) > (5 * 1024 * 1024):
+                        large_files.append(rel_path)
+
+            if len(large_files) > 0:
+                logger.info("Track files with git lfs: {}".format(", ".join(large_files)))
+                repo.lfs_track(large_files)
+
+            logger.info("Push model to the hub. This might take a while")
+            commit_url = repo.push_to_hub(commit_message=commit_message)
+
+        return commit_url
+
     def predict_batch(
         self,
         queries: List[str],


Code review comments:
diff_hunk: @@ -688,6 +691,50 @@ def save(self, directory: Path):
         self.inferencer.model.save(directory)
         self.inferencer.processor.save(directory)
 
+    def save_to_remote(
+        self,
+        model_name: str,
+        hf_organization: Optional[str] = None,
+        private: Optional[bool] = None,
+        commit_message: str = "Add new model to Hugging Face.",
+    ):
+
+        token = HfFolder.get_token()
+        if token is None:
+            raise ValueError(
+                "You must login to the Hugging Face hub on this computer by typing `transformers-cli login`."
+            )
+
+        repo_url = create_repo(
+            token, model_name, organization=hf_organization, private=private, repo_type=None, exist_ok=True
+        )
+
+        transformer_models = self.inferencer.model.convert_to_transformers()
+
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            repo = Repository(tmp_dir, clone_from=repo_url)
+
+            self.save(tmp_dir)
path: haystack/nodes/reader/farm.py
text: @user1:
This is not necessary, as it saves FARM configs too. Let's just save the HF tokenizer:
```suggestion
            self.inferencer.processor.tokenizer.save_pretrained(tmp_dir)
```