Repository: Lightning-AI/pytorch-lightning

Issue:
TensorBoardLogger not saving hparams without metrics
## 🐛 Bug

`log_hyperparams` for `TensorBoardLogger` saves no data with default `metrics=None`, only hparam entries/names show up in sidebar

### To Reproduce

Steps to reproduce the behavior:

```
import pytorch_lightning as pl
logger = pl.loggers.TensorBoardLogger("./test_logs")
test_dict = {"test":0}
logger.log_hyperparams(test_dict)                      ## no data saved
logger.log_hyperparams(test_dict, test_dict)           ## works
logger.log_metrics(test_dict)                          ## works
logger.experiment.add_hparams(test_dict, test_dict)    ## works but saves in a different events file
```

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

### Expected behavior
hparams data is saved and viewable via tensorboard with default args

### Proposed solution
For default hparams logging without metrics, add a placeholder metric? I can do a PR if this is appropriate.


Discussion:
```
logger.log_hyperparams({"hp_1":1,"hp_2":2}, {"test_m":0})
### training
logger.log_hyperparams({"hp_1":10,"hp_2":20}, {"test_m":1})
```

by calling log_hyperparams again at the end of training with the same metric name, the value can be updated... however it does mess up the graph for said metric (it sets the step to 0)... so there would have to be a dedicated metric for hparams
(calling `log_hyperparams` again cannot update the hparams)


code to reproduce the messed up graph:
```
import pytorch_lightning as pl
logger = pl.loggers.TensorBoardLogger("./test_logs")
logger.log_metrics({"metric":0.0}, step=0)
logger.log_metrics({"metric":0.5}, step=1)
logger.log_metrics({"metric":1.0}, step=2)
logger.log_hyperparams({"hp_1":10,"hp_2":20}, {"metric":1.0})
```
similar behavior can be seen in pytorch summarywriter:
```
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(log_dir="./test_logs")
writer.add_scalar("metric", 0.0, global_step=0)
writer.add_scalar("metric", 0.5, global_step=1)
writer.add_scalar("metric", 1.0, global_step=2)
writer.add_hparams({"hp_1":10,"hp_2":20}, {"metric": 1.0})
```
and the summarywriter docs states that the hparams metric key should be unique as well
This is duplicate of #2406 . The issue we have there, is that usually you log these params at the end, but we cannot guarantee them to be logged for crashes then.
My bad, will move discussions over there

Resolving patch:
diff --git a/pytorch_lightning/loggers/tensorboard.py b/pytorch_lightning/loggers/tensorboard.py
--- a/pytorch_lightning/loggers/tensorboard.py
+++ b/pytorch_lightning/loggers/tensorboard.py
@@ -65,6 +65,8 @@ class TensorBoardLogger(LightningLoggerBase):
         log_graph: Adds the computational graph to tensorboard. This requires that
             the user has defined the `self.example_input_array` attribute in their
             model.
+        default_hp_metric: Enables a placeholder metric with key `hp_metric` when `log_hyperparams` is
+            called without a metric (otherwise calls to log_hyperparams without a metric are ignored).
         \**kwargs: Other arguments are passed directly to the :class:`SummaryWriter` constructor.
 
     """
@@ -76,6 +78,7 @@ def __init__(
         name: Optional[str] = "default",
         version: Optional[Union[int, str]] = None,
         log_graph: bool = False,
+        default_hp_metric: bool = True,
         **kwargs
     ):
         super().__init__()
@@ -83,6 +86,7 @@ def __init__(
         self._name = name or ''
         self._version = version
         self._log_graph = log_graph
+        self._default_hp_metric = default_hp_metric
 
         self._experiment = None
         self.hparams = {}
@@ -162,16 +166,18 @@ def log_hyperparams(self, params: Union[Dict[str, Any], Namespace],
             from torch.utils.tensorboard.summary import hparams
 
             if metrics is None:
-                metrics = {}
-            exp, ssi, sei = hparams(params, metrics)
-            writer = self.experiment._get_file_writer()
-            writer.add_summary(exp)
-            writer.add_summary(ssi)
-            writer.add_summary(sei)
+                if self._default_hp_metric:
+                    metrics = {"hp_metric": -1}
+            elif not isinstance(metrics, dict):
+                metrics = {"hp_metric": metrics}
 
             if metrics:
-                # necessary for hparam comparison with metrics
-                self.log_metrics(metrics)
+                self.log_metrics(metrics, 0)
+                exp, ssi, sei = hparams(params, metrics)
+                writer = self.experiment._get_file_writer()
+                writer.add_summary(exp)
+                writer.add_summary(ssi)
+                writer.add_summary(sei)
 
     @rank_zero_only
     def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None) -> None:


Test patch:
diff --git a/tests/loggers/test_all.py b/tests/loggers/test_all.py
--- a/tests/loggers/test_all.py
+++ b/tests/loggers/test_all.py
@@ -79,9 +79,16 @@ def log_metrics(self, metrics, step):
     trainer.test()
 
     log_metric_names = [(s, sorted(m.keys())) for s, m in logger.history]
-    assert log_metric_names == [(0, ['epoch', 'val_acc', 'val_loss']),
-                                (0, ['epoch', 'train_some_val']),
-                                (1, ['epoch', 'test_acc', 'test_loss'])]
+    if logger_class == TensorBoardLogger:
+        assert log_metric_names == [(0, ['hp_metric']),
+                                    (0, ['epoch', 'val_acc', 'val_loss']),
+                                    (0, ['epoch', 'train_some_val']),
+                                    (0, ['hp_metric']),
+                                    (1, ['epoch', 'test_acc', 'test_loss'])]
+    else:
+        assert log_metric_names == [(0, ['epoch', 'val_acc', 'val_loss']),
+                                    (0, ['epoch', 'train_some_val']),
+                                    (1, ['epoch', 'test_acc', 'test_loss'])]
 
 
 @pytest.mark.parametrize("logger_class", [
diff --git a/tests/loggers/test_tensorboard.py b/tests/loggers/test_tensorboard.py
--- a/tests/loggers/test_tensorboard.py
+++ b/tests/loggers/test_tensorboard.py
@@ -125,7 +125,7 @@ def test_tensorboard_log_hyperparams(tmpdir):
 
 
 def test_tensorboard_log_hparams_and_metrics(tmpdir):
-    logger = TensorBoardLogger(tmpdir)
+    logger = TensorBoardLogger(tmpdir, default_hp_metric=False)
     hparams = {
         "float": 0.3,
         "int": 1,
@@ -141,7 +141,7 @@ def test_tensorboard_log_hparams_and_metrics(tmpdir):
 
 
 def test_tensorboard_log_omegaconf_hparams_and_metrics(tmpdir):
-    logger = TensorBoardLogger(tmpdir)
+    logger = TensorBoardLogger(tmpdir, default_hp_metric=False)
     hparams = {
         "float": 0.3,
         "int": 1,