Repository: pytorch/ignite

Issue:
TrainsSaver requires TrainsLogger but doesn't use it
## ❓ Questions/Help/Support

<!-- If you have a question, please checkout other issues labelled as "question" (https://github.com/pytorch/ignite/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion) -->
<!-- If your question is not listed above or you need a support or help, please provide a description-->
<!-- If you prefer, you can also reach out us on [Discussion Forum](https://discuss.pytorch.org/) adding tag "ignite". -->

Hi,

I'm opening this issue as a question, because I'm not sure if it's an artifact or it has an intended purpose that I don't understand.

The new trains support introduced `TrainsSaver` together with `TrainsLogger`.  It seems that to initialize the `TrainsSaver` object, you're required to pass a `TrainsLogger` logger (https://github.com/pytorch/ignite/blob/master/ignite/contrib/handlers/trains_logger.py#L610).
However, this logger is not used anywhere. Why is it required?

I would expect to be able to just do 
```
Checkpoint(to_save, TrainsSaver(output_uri=output_uri), 'epoch', n_saved=n_saved,
                       global_step_transform=global_step_from_engine(trainer))
```




Discussion:
@achigeor Thank you for this question.

That needs to be confirmed by @jkhenning but I think the `TrainLogger` is mandatory to have the stream opened to the cloud `trains` server. Indeed, `TrainLogger` not directly used but ensures that the `trains` session is created (and not closed). @achigeor Does it make sense ?

HTH
Hi @achigeor,

@sdesrozis is quite right 😄 

Just to add some details, In order to use Trains, you must create an experiment session and work with a remote Trains Server (your own installed server, for example). Since initializing a Trains experiment might require various parameters (such as name, project etc.) and since `TrainsLogger` already provides this functionality, duplicating it in the `TrainsSaver` seemed unnecessary. It also seems like a pattern used with other loggers, and serves the purpose of further enhancing the saver later on, if required.
Thanks for the clarifications @jkhenning and @sdesrozis!
Wouldn't the same functionality be achieved by checking if a task is running and if not throw an error (or warning and create a task ad hoc?)
We do use trains in our project! However, we take care of handling the tasks and logging ourselves, so in order to use the `DiskSaver` I end up just passing an empty logger object (trains is clever enough to not init a new task but use the current one that runs). 

It makes sense to me. @jkhenning your thoughts ?
@achigeor @sdesrozis What do you think about using a default value of `None` for the `logger` argument - this will still be a hint for non-experienced users that a logger is the quickest way to get Trains working in this context, but will allow more advanced users who already initialized Trains to skip it. Of course, if no logger is passed and Trains was not initialized, a UsageError will be raised.
Makes sense?
@jkhenning if we want to handle remote sessions opened by users outside ignite loggers (and be interoperable), we should do following your idea 👍🏻
@jkhenning yeap makes a lot of sense, this covers all usecases for advanced as well as new users as you said :+1: 
Anyone would like to send a PR with this minor fix ?

Resolving patch:
diff --git a/ignite/contrib/handlers/trains_logger.py b/ignite/contrib/handlers/trains_logger.py
--- a/ignite/contrib/handlers/trains_logger.py
+++ b/ignite/contrib/handlers/trains_logger.py
@@ -577,7 +577,17 @@ def _create_opt_params_handler(self, *args, **kwargs):
 
 
 class TrainsSaver(DiskSaver):
-    """Handler that saves input checkpoint as Trains artifacts
+    """
+    Handler that saves input checkpoint as Trains artifacts
+
+    Args:
+        logger (TrainsLogger, optional): An instance of :class:`~ignite.contrib.handlers.TrainsLogger`, ensuring a valid
+            Trains ``Task`` has been initialized. If not provided, and a Trains Task has not been manually initialized,
+            a runtime error will be raised.
+        output_uri (str, optional): The default location for output models and other artifacts uploaded by Trains. For
+            more information, see ``trains.Task.init``.
+        dirname (str, optional): Directory path where the checkpoint will be saved. If not provided, a temporary
+            directory will be created.
 
     Examples:
 
@@ -595,7 +605,7 @@ class TrainsSaver(DiskSaver):
 
             handler = Checkpoint(
                 to_save,
-                TrainsSaver(trains_logger),
+                TrainsSaver(),
                 n_saved=1,
                 score_function=lambda e: 123,
                 score_name="acc",
@@ -607,10 +617,7 @@ class TrainsSaver(DiskSaver):
 
     """
 
-    def __init__(self, logger: TrainsLogger, output_uri: str = None, dirname: str = None, *args, **kwargs):
-        if not isinstance(logger, TrainsLogger):
-            raise TypeError("logger must be an instance of TrainsLogger")
-
+    def __init__(self, logger: TrainsLogger = None, output_uri: str = None, dirname: str = None, *args, **kwargs):
         try:
             from trains import Task
         except ImportError:
@@ -619,6 +626,16 @@ def __init__(self, logger: TrainsLogger, output_uri: str = None, dirname: str =
                 "You may install trains using: \n pip install trains \n"
             )
 
+        if logger and not isinstance(logger, TrainsLogger):
+            raise TypeError("logger must be an instance of TrainsLogger")
+
+        self.task = Task.current_task()
+        if not self.task:
+            raise RuntimeError(
+                "TrainsSaver requires a Trains Task to be initialized."
+                "Please use the `logger` argument or call `trains.Task.init()`."
+            )
+
         if not dirname:
             dirname = tempfile.mkdtemp(
                 prefix="ignite_checkpoints_{}".format(datetime.now().strftime("%Y_%m_%d_%H_%M_%S_"))
@@ -627,9 +644,6 @@ def __init__(self, logger: TrainsLogger, output_uri: str = None, dirname: str =
 
         super(TrainsSaver, self).__init__(dirname=dirname, *args, **kwargs)
 
-        self.logger = logger
-        self.task = Task.current_task()
-
         if output_uri:
             self.task.output_uri = output_uri
 


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_trains_logger.py b/tests/ignite/contrib/handlers/test_trains_logger.py
--- a/tests/ignite/contrib/handlers/test_trains_logger.py
+++ b/tests/ignite/contrib/handlers/test_trains_logger.py
@@ -1,5 +1,5 @@
 import math
-from unittest.mock import ANY, MagicMock, call
+from unittest.mock import ANY, MagicMock, Mock, call
 
 import pytest
 import torch
@@ -623,6 +623,7 @@ def test_trains_disk_saver_integration():
     to_save_serializable = {"model": model}
 
     mock_logger = MagicMock(spec=TrainsLogger)
+    trains.Task.current_task = Mock(return_value=object())
     trains_saver = TrainsSaver(mock_logger)
     trains.binding.frameworks.WeightsFileHandler.create_output_model = MagicMock()
 
@@ -634,3 +635,22 @@ def test_trains_disk_saver_integration():
     trainer.state.iteration = 1
     checkpoint(trainer)
     assert trains.binding.frameworks.WeightsFileHandler.create_output_model.call_count == 2
+
+
+def test_trains_disk_saver_integration_no_logger():
+    model = torch.nn.Module()
+    to_save_serializable = {"model": model}
+
+    trains.Task.current_task = Mock(return_value=object())
+    trains_saver = TrainsSaver()
+
+    trains.binding.frameworks.WeightsFileHandler.create_output_model = MagicMock()
+
+    checkpoint = Checkpoint(to_save=to_save_serializable, save_handler=trains_saver, n_saved=1)
+
+    trainer = Engine(lambda e, b: None)
+    trainer.state = State(epoch=0, iteration=0)
+    checkpoint(trainer)
+    trainer.state.iteration = 1
+    checkpoint(trainer)
+    assert trains.binding.frameworks.WeightsFileHandler.create_output_model.call_count == 2