Skip to content

Commit 80a8070

Browse files
feat: Add first-class Comet ML experiment tracking (#2910)
Signed-off-by: Shane Moran <shane.moran@shopify.com> Signed-off-by: Chen Cui <chcui@nvidia.com> Co-authored-by: Shane Moran <shane.moran@shopify.com>
1 parent e828ea5 commit 80a8070

16 files changed

Lines changed: 1007 additions & 12 deletions

File tree

‎docs/training/logging.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,60 @@ When enabled, MLFlow receives:
197197

198198

199199

200+
### Comet ML
201+
202+
Megatron Bridge can log metrics and experiment metadata to Comet ML, following the same pattern as the W&B and MLFlow integrations.
203+
204+
#### What Gets Logged
205+
206+
When enabled, Comet ML receives:
207+
208+
- Training configuration as experiment parameters
209+
- Scalar metrics (losses, learning rate, batch size, throughput, timers, memory, runtime, norms, energy, etc.)
210+
- Validation loss and perplexity metrics
211+
- Checkpoint save/load metadata
212+
213+
#### Enable Comet ML Logging
214+
215+
1) Install Comet ML:
216+
217+
```bash
218+
pip install comet-ml
219+
```
220+
221+
2) Authenticate:
222+
- Either set `COMET_API_KEY` in the environment, or
223+
- Pass an explicit `comet_api_key` in the logger config.
224+
225+
3) Configure logging in your training setup.
226+
227+
```python
228+
from megatron.bridge.training.config import LoggerConfig
229+
230+
cfg.logger = LoggerConfig(
231+
tensorboard_dir="./runs/tensorboard",
232+
comet_project="my_project",
233+
comet_experiment_name="llama32_1b_pretrain_run",
234+
comet_workspace="my_workspace", # optional
235+
comet_tags=["pretrain", "llama32"], # optional
236+
)
237+
```
238+
239+
```{note}
240+
Comet ML is initialized lazily on the last rank when `comet_project` is set and `comet_experiment_name` is non-empty.
241+
```
242+
243+
#### Comet ML Configuration with NeMo Run Launching
244+
245+
For users launching training scripts with NeMo Run, Comet ML can be optionally configured using the {py:class}`bridge.recipes.run_plugins.CometPlugin`.
246+
247+
The plugin automatically forwards the `COMET_API_KEY` and by default injects CLI overrides for the following logger parameters:
248+
249+
- `logger.comet_project`
250+
- `logger.comet_workspace`
251+
- `logger.comet_experiment_name`
252+
253+
200254
#### Progress Log
201255

202256
When `logger.log_progress` is enabled, the framework generates a `progress.txt` file in the checkpoint save directory.

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ dependencies = [
9595
"timm",
9696
"open-clip-torch>=3.2.0",
9797
"mlflow>=3.5.0",
98+
"comet-ml>=3.50.0",
9899
"torch>=2.6.0",
99100
]
100101

‎src/megatron/bridge/recipes/run_plugins.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,75 @@ def setup(self, task: Union["run.Partial", "run.Script"], executor: "run.Executo
493493
)
494494

495495

496+
@dataclass
497+
class CometPluginScriptArgs:
498+
"""Arguments for CometPlugin to pass to run.Script."""
499+
500+
project: str
501+
workspace: Optional[str]
502+
name: Optional[str]
503+
504+
505+
def _default_comet_converter(args: CometPluginScriptArgs) -> List[str]:
506+
"""Default converter for CometPlugin that generates CLI overrides."""
507+
cli_overrides = [f"logger.comet_project={args.project}"]
508+
if args.workspace:
509+
cli_overrides.append(f"logger.comet_workspace={args.workspace}")
510+
if args.name:
511+
cli_overrides.append(f"logger.comet_experiment_name={args.name}")
512+
return cli_overrides
513+
514+
515+
@dataclass(kw_only=True)
516+
class CometPlugin(Plugin):
517+
"""
518+
A plugin for setting up Comet ML configuration.
519+
520+
This plugin sets up Comet ML logging configuration. The plugin is only activated
521+
if the ``COMET_API_KEY`` environment variable is set.
522+
The ``COMET_API_KEY`` environment variable will also be set in the executor's environment variables.
523+
Follow https://www.comet.com/docs/v2/guides/getting-started/quickstart/ to retrieve your ``COMET_API_KEY``.
524+
525+
Args:
526+
project (str): The Comet ML project name.
527+
name (Optional[str]): The name for the Comet ML experiment.
528+
workspace (Optional[str]): The Comet ML workspace.
529+
script_args_converter_fn (Optional[Callable]): A function that takes CometPluginScriptArgs
530+
and returns a list of CLI arguments.
531+
"""
532+
533+
project: str
534+
name: Optional[str] = None
535+
workspace: Optional[str] = None
536+
script_args_converter_fn: Optional[Callable[[CometPluginScriptArgs], List[str]]] = None
537+
538+
def setup(self, task: Union["run.Partial", "run.Script"], executor: "run.Executor"):
539+
if not HAVE_NEMO_RUN:
540+
raise ImportError(MISSING_NEMO_RUN_MSG)
541+
542+
if "COMET_API_KEY" in os.environ:
543+
executor.env_vars["COMET_API_KEY"] = os.environ["COMET_API_KEY"]
544+
545+
if isinstance(task, Script):
546+
script_args = CometPluginScriptArgs(
547+
project=self.project,
548+
workspace=self.workspace,
549+
name=self.name,
550+
)
551+
552+
converter = self.script_args_converter_fn or _default_comet_converter
553+
cli_overrides = converter(script_args)
554+
555+
task.args.extend(cli_overrides)
556+
logger.info(f"{self.__class__.__name__} added CLI overrides: {', '.join(cli_overrides)}")
557+
else:
558+
raise NotImplementedError("CometPlugin is only supported for run.Script tasks")
559+
else:
560+
logger.warning(
561+
f"Warning: The {self.__class__.__name__} will have no effect as COMET_API_KEY environment variable is not set."
562+
)
563+
564+
496565
@dataclass
497566
class PerfEnvPluginScriptArgs:
498567
"""Arguments for PerfEnvPlugin to pass to run.Script."""

‎src/megatron/bridge/training/checkpointing.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from megatron.bridge.training.state import GlobalState, TrainState
6464
from megatron.bridge.training.tokenizers.config import TokenizerConfig
6565
from megatron.bridge.training.tokenizers.tokenizer import MegatronTokenizer
66-
from megatron.bridge.training.utils import mlflow_utils, wandb_utils
66+
from megatron.bridge.training.utils import comet_utils, mlflow_utils, wandb_utils
6767
from megatron.bridge.training.utils.checkpoint_utils import (
6868
checkpoint_exists,
6969
ensure_directory_exists,
@@ -834,13 +834,23 @@ def mlflow_finalize_fn() -> None:
834834
mlflow_logger=state.mlflow_logger,
835835
)
836836

837+
def comet_finalize_fn() -> None:
838+
comet_utils.on_save_checkpoint_success(
839+
checkpoint_name,
840+
save_dir,
841+
train_state.step,
842+
comet_logger=state.comet_logger,
843+
)
844+
837845
if ckpt_cfg.async_save:
838846
assert async_save_request is not None
839847
async_save_request.add_finalize_fn(wandb_finalize_fn)
840848
async_save_request.add_finalize_fn(mlflow_finalize_fn)
849+
async_save_request.add_finalize_fn(comet_finalize_fn)
841850
else:
842851
wandb_finalize_fn()
843852
mlflow_finalize_fn()
853+
comet_finalize_fn()
844854

845855
if ckpt_cfg.async_save:
846856
schedule_async_save(state, async_save_request)
@@ -1851,6 +1861,7 @@ def _load_checkpoint_from_path(
18511861
if not torch.distributed.is_initialized() or is_last_rank():
18521862
wandb_utils.on_load_checkpoint_success(checkpoint_name, load_dir, state.wandb_logger)
18531863
mlflow_utils.on_load_checkpoint_success(checkpoint_name, load_dir, state.mlflow_logger)
1864+
comet_utils.on_load_checkpoint_success(checkpoint_name, load_dir, state.comet_logger)
18541865

18551866
torch.cuda.empty_cache()
18561867

‎src/megatron/bridge/training/config.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,21 @@ class LoggerConfig:
10841084
mlflow_tags: Optional[dict[str, str]] = None
10851085
"""Optional tags to apply to the MLFlow run."""
10861086

1087+
comet_project: Optional[str] = None
1088+
"""The Comet ML project name. Comet logging is disabled when this is None."""
1089+
1090+
comet_experiment_name: Optional[str] = None
1091+
"""The Comet ML experiment name."""
1092+
1093+
comet_workspace: Optional[str] = None
1094+
"""The Comet ML workspace. If not set, uses the default workspace for the API key."""
1095+
1096+
comet_api_key: Optional[str] = None
1097+
"""The Comet ML API key. Can also be set via COMET_API_KEY environment variable."""
1098+
1099+
comet_tags: Optional[list[str]] = None
1100+
"""Optional list of tags to apply to the Comet ML experiment."""
1101+
10871102
logging_level: int = logging.INFO
10881103
"""Set default logging level"""
10891104

@@ -1127,6 +1142,30 @@ def finalize(self) -> None:
11271142
"Install it via pip install mlflow or uv add mlflow"
11281143
) from exc
11291144

1145+
if self.comet_project and (self.comet_experiment_name is None or self.comet_experiment_name == ""):
1146+
raise ValueError("Set logger.comet_experiment_name when enabling Comet ML logging.")
1147+
1148+
using_comet = any(
1149+
[
1150+
self.comet_project,
1151+
self.comet_experiment_name,
1152+
self.comet_workspace,
1153+
self.comet_api_key,
1154+
self.comet_tags,
1155+
]
1156+
)
1157+
1158+
if using_comet:
1159+
try:
1160+
import importlib
1161+
1162+
importlib.import_module("comet_ml")
1163+
except ModuleNotFoundError as exc:
1164+
raise ModuleNotFoundError(
1165+
"Comet ML logging is configured, but the 'comet_ml' package is not installed. "
1166+
"Install it via pip install comet-ml or uv add comet-ml"
1167+
) from exc
1168+
11301169

11311170
@dataclass(kw_only=True)
11321171
class ProfilingConfig:

‎src/megatron/bridge/training/eval.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ def evaluate_and_print_results(
336336
writer = None
337337

338338
wandb_writer = state.wandb_logger
339+
comet_logger = state.comet_logger
339340

340341
if should_fire(callback_manager, start_event):
341342
callback_manager.fire(
@@ -386,6 +387,13 @@ def evaluate_and_print_results(
386387
if state.cfg.logger.log_validation_ppl_to_tensorboard:
387388
wandb_writer.log({"{} validation ppl".format(key): ppl}, state.train_state.step)
388389

390+
if comet_logger and is_last_rank():
391+
comet_logger.log_metrics(
392+
{"{} validation".format(key): total_loss_dict[key].item()}, step=state.train_state.step
393+
)
394+
if state.cfg.logger.log_validation_ppl_to_tensorboard:
395+
comet_logger.log_metrics({"{} validation ppl".format(key): ppl}, step=state.train_state.step)
396+
389397
if process_non_loss_data_func is not None and writer and is_last_rank():
390398
process_non_loss_data_func(collected_non_loss_data, state.train_state.step, writer)
391399

‎src/megatron/bridge/training/setup.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
)
5757

5858

59-
6059
class SetupOutput(NamedTuple):
6160
"""Represents the output of the main setup function.
6261
@@ -85,6 +84,7 @@ class SetupOutput(NamedTuple):
8584
checkpointing_context: dict[str, Any]
8685
pg_collection: ProcessGroupCollection
8786

87+
8888
def setup(
8989
state: GlobalState,
9090
train_valid_test_datasets_provider: Callable[..., tuple[Optional[Any], Optional[Any], Optional[Any]]],
@@ -251,7 +251,7 @@ def modelopt_pre_wrap_hook(model):
251251

252252
# For PEFT, the pretrained checkpoint is loaded in the pre-wrap hook
253253
if cfg.peft is not None:
254-
should_load_checkpoint = (cfg.checkpoint.load is not None and checkpoint_exists(cfg.checkpoint.load))
254+
should_load_checkpoint = cfg.checkpoint.load is not None and checkpoint_exists(cfg.checkpoint.load)
255255
if should_load_checkpoint:
256256
# The finetune toggle is explicitly set to True in order to avoid loading optimizer and RNG states
257257
# This is switched off here in order to load these states from the checkpoint
@@ -282,6 +282,7 @@ def modelopt_pre_wrap_hook(model):
282282
model,
283283
state.tensorboard_logger,
284284
state.wandb_logger,
285+
comet_logger=state.comet_logger,
285286
current_training_step=state.train_state.step,
286287
)
287288

@@ -299,9 +300,7 @@ def modelopt_pre_wrap_hook(model):
299300
if "tokenizer" in inspect.signature(train_valid_test_datasets_provider).parameters:
300301
train_valid_test_datasets_provider = partial(train_valid_test_datasets_provider, tokenizer=tokenizer)
301302
if "pg_collection" in inspect.signature(train_valid_test_datasets_provider).parameters:
302-
train_valid_test_datasets_provider = partial(
303-
train_valid_test_datasets_provider, pg_collection=pg_collection
304-
)
303+
train_valid_test_datasets_provider = partial(train_valid_test_datasets_provider, pg_collection=pg_collection)
305304

306305
train_data_iterator, valid_data_iterator, test_data_iterator = setup_data_iterators(
307306
cfg=cfg,
@@ -395,13 +394,13 @@ def _update_model_config_funcs(
395394
if len(model) == 1:
396395
model_config.param_sync_func = model_config.param_sync_func[0]
397396
if optimizer is not None:
398-
model_config.finalize_model_grads_func = partial(
399-
finalize_model_grads, pg_collection=pg_collection
400-
)
397+
model_config.finalize_model_grads_func = partial(finalize_model_grads, pg_collection=pg_collection)
401398
model_config.grad_scale_func = optimizer.scale_loss
402399

403400

404-
def _create_peft_pre_wrap_hook(cfg: ConfigContainer, state: GlobalState) -> Callable[[list[MegatronModule]], list[MegatronModule]]:
401+
def _create_peft_pre_wrap_hook(
402+
cfg: ConfigContainer, state: GlobalState
403+
) -> Callable[[list[MegatronModule]], list[MegatronModule]]:
405404
"""Create a pre-wrap hook that handles PEFT logic.
406405
407406
This hook is executed before the model is wrapped with DDP/FSDP and handles:
@@ -415,6 +414,7 @@ def _create_peft_pre_wrap_hook(cfg: ConfigContainer, state: GlobalState) -> Call
415414
Returns:
416415
A callable hook that can be registered with the model provider
417416
"""
417+
418418
def peft_pre_wrap_hook(model: list[MegatronModule]) -> list[MegatronModule]:
419419
"""Pre-wrap hook that handles PEFT transformation.
420420

0 commit comments

Comments
 (0)