Repository: pytorch/ignite

Issue:
ClearML support for configuration
## ❓ 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". -->

Hello! I'd like to know how to transfer the ClearML **configuration** logs from pure PyTorch to PyTorch-Ignite, that is:
```
task = Task.init("project-name", task_name="task-name")
task.connect_configuration(config) # or task.connect({k: v for k, v in config.items()})
```
In official docs, I only found `ClearMLLogger` class which implemented `Task.init()` inside, but with no support for `Task.connect()`. Is there any good solution to this problem in current verison of release? 

Thanks!


Discussion:
@pyb0924 thanks for asking! At the moment we do not expose `task` publicly from `ClearMLLogger`. There is a private argument `ClearMLLogger._task` but it is not advised to use it... In our examples, we just import `Task` from clearml package directly: 
https://github.com/pytorch/ignite/blob/ad37a17004ceaf73606f04125f77bdc3705a935c/examples/contrib/cifar10/main.py#L49-L68

I agree that it could be helpful to expose the task from `ClearMLLogger`. Actually, we could do 2 things:
1) Like TensorboardLogger https://github.com/pytorch/ignite/blob/ad37a17004ceaf73606f04125f77bdc3705a935c/ignite/contrib/handlers/tensorboard_logger.py#L163-L164 such that we can pass through directly to [`ClearMLLogger.clearml_logger`](https://github.com/pytorch/ignite/blob/ad37a17004ceaf73606f04125f77bdc3705a935c/ignite/contrib/handlers/clearml_logger.py#L152)
2) Provide new `ClearMLLogger.get_task()` method to return `ClearMLLogger._task`.

I'll mark this topic as "help wanted" and "good first issue" if someone would like to help with it.

@vfdev-5 Thanks for your clear answer!
But there is still a slight question: I can directly import `Task` from clearml package directly, but suppose I want to simultaneously use `ClearMLLogger` during my training , I have to do like this:
```Python
task = Task.init(project_name=project_name, task_name=task_name)
task.connect_configuration(config)
clearml_logger = ClearMLLogger(project_name=project_name, task_name=task_name) 
clearml_logger.attach_output_handler(
    trainer,
    event_name=Events.ITERATION_COMPLETED(every=log_every),
    tag="training",
    output_transform=lambda loss: {"batchloss": loss},
)
...
```
So, is there any potential conflict produced by this kind of duplicated code?

Thanks! 
By the way, I've tried in one of my own project but raise no conflict. It's just a supposed question to check the answer in principle. Thanks!
> So, is there any potential conflict produced by this kind of duplicated code?

yeah, if `task` is already created in the user code, then we fetch current task inside `ClearMLLogger` and use it if it exists. So, basically, there is no conflict.


Really appreciate to your answering!

Resolving patch:
diff --git a/ignite/contrib/handlers/clearml_logger.py b/ignite/contrib/handlers/clearml_logger.py
--- a/ignite/contrib/handlers/clearml_logger.py
+++ b/ignite/contrib/handlers/clearml_logger.py
@@ -125,29 +125,16 @@ def __init__(self, **kwargs: Any):
         if self.bypass_mode():
             warnings.warn("ClearMLSaver: running in bypass mode")
 
-            class _Stub(object):
-                def __call__(self, *_: Any, **__: Any) -> "_Stub":
-                    return self
-
-                def __getattr__(self, attr: str) -> "_Stub":
-                    if attr in ("name", "id"):
-                        return ""  # type: ignore[return-value]
-                    return self
-
-                def __setattr__(self, attr: str, val: Any) -> None:
-                    pass
-
-            self._task = _Stub()
-        else:
-            # Try to retrieve current the ClearML Task before trying to create a new one
-            self._task = Task.current_task()
-            if self._task is None:
-                self._task = Task.init(
-                    project_name=kwargs.get("project_name"),
-                    task_name=kwargs.get("task_name"),
-                    task_type=kwargs.get("task_type", Task.TaskTypes.training),
-                    **experiment_kwargs,
-                )
+        # Try to retrieve current the ClearML Task before trying to create a new one
+        self._task = Task.current_task()
+
+        if self._task is None:
+            self._task = Task.init(
+                project_name=kwargs.get("project_name"),
+                task_name=kwargs.get("task_name"),
+                task_type=kwargs.get("task_type", Task.TaskTypes.training),
+                **experiment_kwargs,
+            )
 
         self.clearml_logger = self._task.get_logger()
 
@@ -156,13 +143,20 @@ def __setattr__(self, attr: str, val: Any) -> None:
     @classmethod
     def set_bypass_mode(cls, bypass: bool) -> None:
         """
-        Will bypass all outside communication, and will drop all logs.
+        Set ``clearml.Task`` to offline mode.
+        Will bypass all outside communication, and will save all data and logs to a local session folder.
         Should only be used in "standalone mode", when there is no access to the *clearml-server*.
 
         Args:
             bypass: If ``True``, all outside communication is skipped.
+                Data and logs will be stored in a local session folder.
+                For more information, please refer to `ClearML docs
+                <https://clear.ml/docs/latest/docs/clearml_sdk/task_sdk/#offline-mode>`_.
         """
+        from clearml import Task
+
         setattr(cls, "_bypass", bypass)
+        Task.set_offline(offline_mode=bypass)
 
     @classmethod
     def bypass_mode(cls) -> bool:
@@ -172,12 +166,32 @@ def bypass_mode(cls) -> bool:
         Note:
             `GITHUB_ACTIONS` env will automatically set bypass_mode to ``True``
             unless overridden specifically with ``ClearMLLogger.set_bypass_mode(False)``.
+            For more information, please refer to `ClearML docs
+            <https://clear.ml/docs/latest/docs/clearml_sdk/task_sdk/#offline-mode>`_.
 
         Return:
-            If True, all outside communication is skipped.
+            If True, ``clearml.Task`` is on offline mode, and all outside communication is skipped.
         """
         return getattr(cls, "_bypass", bool(os.environ.get("CI")))
 
+    def __getattr__(self, attr: Any) -> Any:
+        """
+        Calls the corresponding method of ``clearml.Logger``.
+
+        Args:
+            attr: methods of the ``clearml.Logger`` class.
+        """
+        return getattr(self.clearml_logger, attr)
+
+    def get_task(self) -> Any:
+        """
+        Returns the task context that the logger is reporting.
+
+        Return:
+            Returns the current task, equivalent to ``clearml.Task.current_task()``.
+        """
+        return self._task
+
     def close(self) -> None:
         self.clearml_logger.flush()
 


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_clearml_logger.py b/tests/ignite/contrib/handlers/test_clearml_logger.py
--- a/tests/ignite/contrib/handlers/test_clearml_logger.py
+++ b/tests/ignite/contrib/handlers/test_clearml_logger.py
@@ -1,7 +1,7 @@
 import math
 import os
 from collections import defaultdict
-from unittest.mock import ANY, call, MagicMock, Mock, patch
+from unittest.mock import ANY, call, MagicMock, patch
 
 import clearml
 import pytest
@@ -718,7 +718,8 @@ def update_fn(engine, batch):
 
         def dummy_handler(engine, logger, event_name):
             global_step = engine.state.get_event_attrib_value(event_name)
-            logger.clearml_logger.report_scalar(title="", series="", value="test_value", iteration=global_step)
+            test_value = 0.3  # example
+            logger.clearml_logger.report_scalar(title="", series="", value=test_value, iteration=global_step)
 
         logger.attach(trainer, log_handler=dummy_handler, event_name=Events.EPOCH_COMPLETED)
 
@@ -745,19 +746,54 @@ def update_fn(engine, batch):
 
             def dummy_handler(engine, logger, event_name):
                 global_step = engine.state.get_event_attrib_value(event_name)
-                logger.clearml_logger.report_scalar(title="", series="", value="test_value", iteration=global_step)
+                test_value = 0.3  # example
+                logger.clearml_logger.report_scalar(title="", series="", value=test_value, iteration=global_step)
 
             clearml_logger.attach(trainer, log_handler=dummy_handler, event_name=Events.EPOCH_COMPLETED)
 
             trainer.run(data, max_epochs=n_epochs)
 
 
+def test_clearml_logger_getattr_method(dirname):
+
+    with pytest.warns(UserWarning, match="ClearMLSaver: running in bypass mode"):
+        ClearMLLogger.set_bypass_mode(True)
+
+        logger = ClearMLLogger(output_uri=dirname)
+
+        # Create a mock clearml.Logger() object
+        mock_logger = MagicMock()
+        logger.clearml_logger = mock_logger
+
+        # Test a method called by __getattr__ calls the corresponding method of the mock project.
+        logger.report_single_value("accuracy", 0.72)
+        mock_logger.report_single_value.assert_called_once_with("accuracy", 0.72)
+
+        # Test a method called by __getattr__ calls the corresponding classmethod of the mock project's class.
+        logger.current_logger()
+        mock_logger.current_logger.assert_called_once()
+
+        logger.close()
+
+
+def test_clearml_logger_get_task_bypass(dirname):
+
+    with pytest.warns(UserWarning, match="ClearMLSaver: running in bypass mode"):
+        ClearMLLogger.set_bypass_mode(True)
+
+        with ClearMLLogger(output_uri=dirname) as clearml_logger:
+            task = clearml_logger.get_task()
+            assert isinstance(task, clearml.Task)
+            assert task == clearml.Task.current_task()
+            task.close()
+
+
 def test_clearml_disk_saver_integration():
     model = torch.nn.Module()
     to_save_serializable = {"model": model}
     with pytest.warns(UserWarning, match="ClearMLSaver created a temporary checkpoints directory"):
         mock_logger = MagicMock(spec=ClearMLLogger)
-        clearml.Task.current_task = Mock(return_value=object())
+        clearml.Task.current_task = MagicMock(spec=clearml.Task)
         clearml_saver = ClearMLSaver(mock_logger)
         clearml.binding.frameworks.WeightsFileHandler.create_output_model = MagicMock()
 
@@ -781,7 +817,7 @@ def test_clearml_disk_saver_integration_no_logger():
     to_save_serializable = {"model": model}
 
     with pytest.warns(UserWarning, match="ClearMLSaver created a temporary checkpoints directory"):
-        clearml.Task.current_task = Mock(return_value=object())
+        clearml.Task.current_task = MagicMock(spec=clearml.Task)
         clearml.binding.frameworks.WeightsFileHandler.create_output_model = MagicMock()
         clearml_saver = ClearMLSaver()
         checkpoint = Checkpoint(to_save=to_save_serializable, save_handler=clearml_saver, n_saved=1)
@@ -893,7 +929,7 @@ def forward(self, x):
 def _test_save_model_optimizer_lr_scheduler_with_state_dict(device, on_zero_rank=False):
 
     if idist.get_rank() == 0:
-        clearml.Task.current_task = Mock(return_value=object())
+        clearml.Task.current_task = MagicMock(spec=clearml.Task)
         clearml.binding.frameworks.WeightsFileHandler.create_output_model = MagicMock()
 
     torch.manual_seed(23)