Skip to content

Commit fd36df6

Browse files
authored
Wandb integration (#6)
* readme change * wandb flag * using job_name for logging * add wandb reporting to all runners * fmt * added wandb dependency * trainer logging and ray init changes * training loop updates * logging adds * peft dir * deepspeed triton error fix * config changes * add wandb * add first logging step * wandb init * set wandb mode * readme * wandb settings * ray logging * force ray version * rm tempfile * clean up runners
1 parent 88b4f94 commit fd36df6

12 files changed

Lines changed: 1869 additions & 1088 deletions

File tree

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,32 @@ It uses Ray Train + Accelerate for distributed training.
5454

5555
Unless you overwrote `output_dir`, results will be stored in `outputs/training_type/job_name/`
5656

57-
### 3. Bundle Checkpoint for LEAP
57+
### 3. (Optional) Experiment Tracking with Weights & Biases
58+
59+
To enable experiment tracking (using [Weights & Biases](https://wandb.ai)):
60+
61+
- Set `wandb_logging=True` in `config.py` in your `user_config` overrides or default configs.
62+
- **Offline mode (default)**: If no `WANDB_API_KEY` is set, wandb logs locally to `./wandb/` directory. No API key needed!
63+
- **Online mode**: Set the `WANDB_API_KEY` environment variable to sync to wandb.ai dashboard:
64+
65+
```bash
66+
export WANDB_API_KEY=your_api_key # optional; for online syncing to wandb.ai
67+
```
68+
69+
You can also customize the project name (defaults to `"leap-finetune"`):
70+
71+
```bash
72+
export WANDB_PROJECT=my-custom-project # optional; defaults to "leap-finetune"
73+
```
74+
75+
After training, view your metrics:
76+
77+
- **Online mode**: View at `https://wandb.ai/<your-entity>/<project-name>/runs/<run-name>`
78+
- **Offline mode**: Sync later with `wandb sync ./wandb/offline-run-*` or view locally
79+
80+
Runs are named after your `job_name` and metrics are reported via TRL/Transformers. Training metrics (loss, learning rate, etc.) are logged every `logging_steps` (default: 10), and evaluation metrics are logged at the end of each epoch.
81+
82+
### 4. Bundle Checkpoint for LEAP
5883

5984
When training is done, you can bundle your output checkpoint with `leap-bundle` to use it directly within LEAP. Checkout our [Quick Start guide](https://leap.liquid.ai/docs/leap-bundle/quick-start?utm_source=github&utm_medium=link&utm_campaign=LEAP&utm_content=general).
6085

config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"per_device_train_batch_size": None,
6666
"gradient_accumulation_steps": None,
6767
"learning_rate": None,
68+
"wandb_logging": False, # If set to True, set your wandb API key via 'export WANDB_API_KEY=<your_api_key>'
6869
}
6970

7071

@@ -86,7 +87,7 @@
8687

8788
JOB_CONFIG = JobConfig(
8889
job_name="my_job_name",
89-
model_name="LFM2-8B-A1B",
90+
model_name="LFM2-1.2B",
9091
training_type="sft",
9192
dataset=example_sft_dataset,
9293
training_config=TrainingConfig.MOE_SFT.override(**user_config),

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ dependencies = [
1111
"accelerate>=1.7.0",
1212
"peft>=0.15.2",
1313
"deepspeed>=0.17.1",
14-
"ray[train]>=2.47.1",
1514
"torch>=2.7.1",
1615
"transformers @ git+https://github.com/huggingface/transformers.git@0c9a72e4576fe4c84077f066e585129c97bfd4e6",
1716
"trl>=0.18.2",
1817
"rich>=14.1.0",
1918
"pillow>=11.3.0",
2019
"mpi4py>=4.1.0",
2120
"liger-kernel>=0.6.2",
21+
"wandb>=0.22.3",
22+
"ray==2.48.0",
2223
]
2324

2425
[project.scripts]

src/leap_finetune/configs/sft_configs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"lr_scheduler_type": "linear",
8181
"warmup_steps": 100,
8282
"warmup_ratio": 0.2,
83-
"logging_steps": 10,
83+
"logging_steps": 10, # Log training metrics every 10 steps
84+
"logging_first_step": True, # Log at step 0 to see initial metrics
8485
"save_strategy": "epoch",
8586
"eval_strategy": "epoch",
8687
"load_best_model_at_end": True,

src/leap_finetune/configs/vlm_sft_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"warmup_steps": 100,
5151
"warmup_ratio": 0.2,
5252
"logging_steps": 10,
53+
"logging_first_step": True,
5354
"save_strategy": "epoch",
5455
"eval_strategy": "epoch",
5556
"load_best_model_at_end": True,

src/leap_finetune/trainer.py

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import ray
22
import os
3+
import logging
34

45
from accelerate.utils import set_seed
56
from ray.train import RunConfig, ScalingConfig
67
from ray.runtime_env import RuntimeEnv
78
from ray.train.torch import TorchTrainer, TorchConfig
8-
from rich.console import Console
9-
from rich.panel import Panel
109
from torch import cuda
1110

1211
from leap_finetune.utils.constants import RUNTIME_DIR
1312
from leap_finetune.training_loops.sft_run import sft_run
1413
from leap_finetune.training_loops.dpo_run import dpo_run
1514
from leap_finetune.training_loops.vlm_sft_run import vlm_sft_run
15+
from leap_finetune.utils.logging_utils import (
16+
get_ray_env_vars,
17+
print_next_steps_panel,
18+
select_ray_temp_dir,
19+
select_object_spilling_dir,
20+
)
1621

1722

1823
#################################
@@ -38,27 +43,27 @@ def ray_trainer(job_config: dict) -> None:
3843
os.makedirs(ray_temp_dir, exist_ok=True)
3944

4045
if not ray.is_initialized():
46+
# Force local init and avoid accidental cluster connects
47+
for key in ("RAY_ADDRESS", "RAY_HEAD_IP", "RAY_HEAD_NODE_ADDRESS", "RAY_PORT"):
48+
os.environ.pop(key, None)
49+
50+
ray_temp_dir = select_ray_temp_dir(os.path.expanduser("~/ray_temp"))
51+
spill_dir = select_object_spilling_dir(ray_temp_dir)
52+
53+
# Reduce Ray logging verbosity
54+
ray_logger = logging.getLogger("ray")
55+
ray_logger.setLevel(logging.ERROR) # Only show errors, not INFO/WARNING
56+
57+
runtime_env = RuntimeEnv(
58+
working_dir=str(RUNTIME_DIR),
59+
env_vars=get_ray_env_vars(ray_temp_dir),
60+
)
61+
4162
ray.init(
42-
runtime_env=RuntimeEnv(
43-
working_dir=str(RUNTIME_DIR),
44-
env_vars={
45-
"TMPDIR": ray_temp_dir,
46-
"TEMP": ray_temp_dir,
47-
"TMP": ray_temp_dir,
48-
"NCCL_IB_DISABLE": "1",
49-
"NCCL_P2P_DISABLE": "1", # Added to force CPU level communication during synchronization
50-
"TORCH_NCCL_ASYNC_ERROR_HANDLING": "1",
51-
"NCCL_SOCKET_IFNAME": "lo",
52-
"TORCH_NCCL_BLOCKING_WAIT": "1",
53-
"NCCL_TIMEOUT": "300", # 5 minute safe timeout
54-
"RAY_DISABLE_IMPORT_WARNING": "1",
55-
"RAY_memory_monitor_refresh_ms": "0",
56-
# Suppress Ray Data verbose logging
57-
"RAY_DATA_DISABLE_PROGRESS_BARS": "1",
58-
"RAY_IGNORE_UNHANDLED_ERRORS": "1",
59-
},
60-
),
63+
address="local",
64+
runtime_env=runtime_env,
6165
_temp_dir=ray_temp_dir,
66+
object_spilling_directory=spill_dir,
6267
)
6368

6469
if training_type == "sft":
@@ -72,6 +77,7 @@ def ray_trainer(job_config: dict) -> None:
7277

7378
train_loop_config = {
7479
"model_name": job_config["model_name"],
80+
"job_name": job_config.get("job_name", "leap-ft-run"),
7581
"train_config": job_config["training_config"],
7682
"peft_config": job_config["peft_config"],
7783
"dataset": job_config["dataset"],
@@ -98,24 +104,9 @@ def ray_trainer(job_config: dict) -> None:
98104

99105
trainer.fit()
100106

101-
console = Console()
102-
quick_start_url = (
103-
"https://leap.liquid.ai/docs/leap-bundle/quick-start?utm_source=github"
104-
"&utm_medium=link&utm_campaign=LEAP&utm_content=general"
105-
)
106-
107-
cta_message = (
108-
"[bold green]Training complete![/bold green]\n\n"
109-
f"[bold]Checkpoint directory:[/bold] [cyan]{output_dir}[/cyan]\n\n"
110-
"Bundle your output checkpoint with [bold]leap-bundle[/bold] to use it in LEAP:\n"
111-
f"[dim]leap-bundle create {output_dir}/[CHECKPOINT_NAME][/dim]\n\n"
112-
f"Quick Start: [link={quick_start_url}]{quick_start_url}[/link]"
113-
)
114-
115-
console.print(
116-
Panel.fit(
117-
cta_message,
118-
title="Next Step: Bundle for LEAP",
119-
border_style="green",
120-
)
121-
)
107+
print_next_steps_panel(output_dir)
108+
# Ensure Ray cleans up resources promptly to avoid post-training hangs
109+
try:
110+
ray.shutdown()
111+
except Exception:
112+
pass

src/leap_finetune/training_loops/dpo_run.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from transformers import PreTrainedTokenizerBase
55
from trl import DPOConfig, DPOTrainer
66
from ray.train.huggingface.transformers import prepare_trainer
7+
from ray.train import get_context
78

89
from leap_finetune.configs.distributed_configs import MOE_FSDP_CONFIG
910
from leap_finetune.utils.load_models import load_model
1011
from leap_finetune.utils.model_utils import is_moe_model_from_name
1112
from leap_finetune.utils.peft import apply_peft_to_model, merge_and_save_peft_model
13+
from leap_finetune.utils.logging_utils import init_wandb_if_enabled
1214

1315

1416
def dpo_run(training_config: dict) -> None:
@@ -18,29 +20,42 @@ def dpo_run(training_config: dict) -> None:
1820
tuple[Dataset, Dataset], training_config.get("dataset")
1921
)
2022

21-
train_config = training_config.get("train_config")
2223
peft_config = training_config.get("peft_config")
2324
model_name = training_config.get("model_name", "")
25+
job_name = training_config.get("job_name", "leap-ft-run")
2426

2527
# Check for MoE model
2628
is_moe = is_moe_model_from_name(model_name)
2729
use_fsdp = is_moe and peft_config is None
2830

29-
# Remove non-DPOConfig parameters
30-
train_config.pop("training_type", None)
31+
# Filter out non-DPOConfig parameters
32+
excluded_keys = {"training_type", "wandb_logging"}
33+
if use_fsdp:
34+
excluded_keys.add("deepspeed") # Remove deepspeed when using FSDP
35+
36+
train_config_filtered = {
37+
k: v
38+
for k, v in training_config.get("train_config").items()
39+
if k not in excluded_keys
40+
}
3141

32-
# Apply FSDP for MoE without PEFT
42+
# Configure wandb reporting if enabled via config
43+
wandb_logging = bool(
44+
training_config.get("train_config", {}).get("wandb_logging", False)
45+
)
46+
init_wandb_if_enabled(job_name, wandb_logging)
47+
48+
# Build training args
49+
config_kwargs = {
50+
"report_to": "wandb" if wandb_logging else "none",
51+
"run_name": job_name,
52+
**train_config_filtered,
53+
}
3354
if use_fsdp:
34-
train_config.pop("deepspeed", None)
35-
fsdp_config = MOE_FSDP_CONFIG["fsdp_config"].copy()
36-
training_args = DPOConfig(
37-
**train_config,
38-
fsdp=MOE_FSDP_CONFIG["fsdp"],
39-
fsdp_config=fsdp_config,
40-
)
41-
else:
42-
# MoE with PEFT or non-MoE: use DeepSpeed (already in config)
43-
training_args = DPOConfig(**train_config)
55+
config_kwargs["fsdp"] = MOE_FSDP_CONFIG["fsdp"]
56+
config_kwargs["fsdp_config"] = MOE_FSDP_CONFIG["fsdp_config"]
57+
58+
training_args = DPOConfig(**config_kwargs)
4459

4560
# Load model after config is created
4661
model, tokenizer = load_model(model_name)
@@ -59,8 +74,27 @@ def dpo_run(training_config: dict) -> None:
5974

6075
# Start training
6176
trainer = prepare_trainer(trainer)
62-
trainer.train()
77+
try:
78+
trainer.train()
79+
print("✅ Training completed successfully")
80+
except RuntimeError as e:
81+
error_msg = str(e)
82+
if any(
83+
keyword in error_msg.lower()
84+
for keyword in ["cuda error", "ecc error", "nccl", "collective", "timeout"]
85+
):
86+
print(
87+
f"⚠️ Training completed but hit distributed communication error during cleanup: {error_msg}"
88+
)
89+
print(
90+
"✅ Training was successful - error occurred in post-training synchronization"
91+
)
92+
else:
93+
raise e
6394

6495
# Save PEFT model if applicable
6596
if peft_config:
66-
merge_and_save_peft_model(model, tokenizer, training_args.output_dir)
97+
ctx = get_context()
98+
is_rank_zero = ctx is None or ctx.get_world_rank() == 0
99+
if is_rank_zero:
100+
merge_and_save_peft_model(model, tokenizer, training_args.output_dir)

src/leap_finetune/training_loops/sft_run.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from transformers import PreTrainedTokenizerBase
55
from trl import SFTConfig, SFTTrainer
66
from ray.train.huggingface.transformers import prepare_trainer
7+
from ray.train import get_context
78

89
from leap_finetune.configs.distributed_configs import MOE_FSDP_CONFIG
910
from leap_finetune.utils.load_models import load_model
1011
from leap_finetune.utils.model_utils import is_moe_model_from_name
1112
from leap_finetune.utils.peft import apply_peft_to_model, merge_and_save_peft_model
13+
from leap_finetune.utils.logging_utils import init_wandb_if_enabled
1214

1315

1416
def sft_run(training_config: dict) -> None:
@@ -18,29 +20,42 @@ def sft_run(training_config: dict) -> None:
1820
tuple[Dataset, Dataset], training_config.get("dataset")
1921
)
2022

21-
train_config = training_config.get("train_config")
2223
peft_config = training_config.get("peft_config")
2324
model_name = training_config.get("model_name", "")
25+
job_name = training_config.get("job_name", "leap-ft-run")
2426

2527
# Check for MoE model
2628
is_moe = is_moe_model_from_name(model_name)
2729
use_fsdp = is_moe and peft_config is None
2830

29-
# Remove non-SFTConfig parameters
30-
train_config.pop("training_type", None)
31+
# Filter out non-SFTConfig parameters
32+
excluded_keys = {"training_type", "wandb_logging"}
33+
if use_fsdp:
34+
excluded_keys.add("deepspeed") # Remove deepspeed when using FSDP
35+
36+
train_config_filtered = {
37+
k: v
38+
for k, v in training_config.get("train_config").items()
39+
if k not in excluded_keys
40+
}
3141

32-
# Apply FSDP for MoE without PEFT
42+
# Configure wandb reporting if enabled via config
43+
wandb_logging = bool(
44+
training_config.get("train_config", {}).get("wandb_logging", False)
45+
)
46+
init_wandb_if_enabled(job_name, wandb_logging)
47+
48+
# Build training args
49+
config_kwargs = {
50+
"report_to": "wandb" if wandb_logging else "none",
51+
"run_name": job_name,
52+
**train_config_filtered,
53+
}
3354
if use_fsdp:
34-
train_config.pop("deepspeed", None)
35-
fsdp_config = MOE_FSDP_CONFIG["fsdp_config"].copy()
36-
training_args = SFTConfig(
37-
**train_config,
38-
fsdp=MOE_FSDP_CONFIG["fsdp"],
39-
fsdp_config=fsdp_config,
40-
)
41-
else:
42-
# MoE with PEFT or non-MoE: use DeepSpeed (already in config)
43-
training_args = SFTConfig(**train_config)
55+
config_kwargs["fsdp"] = MOE_FSDP_CONFIG["fsdp"]
56+
config_kwargs["fsdp_config"] = MOE_FSDP_CONFIG["fsdp_config"]
57+
58+
training_args = SFTConfig(**config_kwargs)
4459

4560
# Load model after config is created
4661
model, tokenizer = load_model(model_name)
@@ -59,8 +74,27 @@ def sft_run(training_config: dict) -> None:
5974

6075
# Start training
6176
trainer = prepare_trainer(trainer)
62-
trainer.train()
77+
try:
78+
trainer.train()
79+
print("✅ Training completed successfully")
80+
except RuntimeError as e:
81+
error_msg = str(e)
82+
if any(
83+
keyword in error_msg.lower()
84+
for keyword in ["cuda error", "ecc error", "nccl", "collective", "timeout"]
85+
):
86+
print(
87+
f"⚠️ Training completed but hit distributed communication error during cleanup: {error_msg}"
88+
)
89+
print(
90+
"✅ Training was successful - error occurred in post-training synchronization"
91+
)
92+
else:
93+
raise e
6394

6495
# Save PEFT model if applicable
6596
if peft_config:
66-
merge_and_save_peft_model(model, tokenizer, training_args.output_dir)
97+
ctx = get_context()
98+
is_rank_zero = ctx is None or ctx.get_world_rank() == 0
99+
if is_rank_zero:
100+
merge_and_save_peft_model(model, tokenizer, training_args.output_dir)

0 commit comments

Comments
 (0)