Skip to content

Commit f4c0ffe

Browse files
committed
fix: improve Megatron SFT runtime startup
1 parent ea34524 commit f4c0ffe

1 file changed

Lines changed: 58 additions & 5 deletions

File tree

src/art/megatron/service.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import subprocess
1111
import sys
1212
from typing import Any, AsyncIterator, Literal, TypedDict, cast
13+
import warnings
1314

1415
from peft.tuners.lora.config import LoraConfig
1516
import torch
@@ -115,7 +116,7 @@ def create_identity_lora(
115116
model_config = handler.identity_lora_model_config(base_config)
116117
with init_empty_weights():
117118
model = AutoModelForCausalLM.from_config(
118-
model_config, torch_dtype=torch.bfloat16, trust_remote_code=True
119+
model_config, dtype=torch.bfloat16, trust_remote_code=True
119120
)
120121
model.name_or_path = base_model
121122

@@ -142,8 +143,19 @@ def _skip_meta_to(
142143
return module
143144
return orig_to(module, *args, **kwargs)
144145

145-
with patch.object(torch.nn.Module, "to", _skip_meta_to):
146-
peft_model = get_peft_model(model, peft_lora_config)
146+
with warnings.catch_warnings():
147+
if bool(getattr(handler, "is_moe", False)):
148+
warnings.filterwarnings(
149+
"ignore",
150+
message=(
151+
r"Unsupported layer type '.*MoeExperts.*' encountered, "
152+
r"proceed at your own risk\."
153+
),
154+
category=UserWarning,
155+
module=r"peft\.tuners\.tuners_utils",
156+
)
157+
with patch.object(torch.nn.Module, "to", _skip_meta_to):
158+
peft_model = get_peft_model(model, peft_lora_config)
147159

148160
os.makedirs(lora_path, exist_ok=True)
149161
peft_model.save_pretrained(lora_path)
@@ -209,6 +221,13 @@ def _on_child_process_exit(self, _error: RuntimeError) -> None:
209221
def _raise_if_child_failed(self) -> None:
210222
self._child_processes.raise_if_failed()
211223

224+
def _status(self, message: str) -> None:
225+
print(f"[ART Megatron] {message}", flush=True)
226+
227+
@staticmethod
228+
def _display_path(path: str | os.PathLike[str]) -> str:
229+
return str(Path(path).resolve())
230+
212231
@property
213232
def is_dedicated(self) -> bool:
214233
return is_dedicated_mode(self.config)
@@ -410,6 +429,10 @@ def _adapter_exists_and_loads(self, lora_path: str) -> bool:
410429
return True
411430

412431
def _create_identity_lora(self, lora_path: str) -> None:
432+
self._status(
433+
"Preparing initial LoRA adapter "
434+
f"for {self.base_model} at {self._display_path(lora_path)}"
435+
)
413436
create_identity_lora(
414437
self.base_model,
415438
lora_path,
@@ -531,6 +554,10 @@ async def _start_vllm_subprocess(
531554
os.makedirs(log_dir, exist_ok=True)
532555
self._vllm_log_path = os.path.join(log_dir, "vllm-runtime.log")
533556
self._vllm_log_file = open(self._vllm_log_path, "w", buffering=1)
557+
self._status(
558+
"Starting vLLM runtime "
559+
f"for {self.base_model}. Logs: {self._display_path(self._vllm_log_path)}"
560+
)
534561
self._vllm_process = subprocess.Popen(
535562
managed_process_cmd(cmd),
536563
cwd=str(get_vllm_runtime_working_dir()),
@@ -581,6 +608,7 @@ async def _start_vllm_subprocess(
581608
) from exc
582609
assert self._vllm_process is not None
583610
assert self._vllm_log_path is not None
611+
self._status(f"vLLM runtime is ready at {self._vllm_base_url}")
584612
self._child_processes.watch_popen(
585613
"vLLM runtime",
586614
self._vllm_process,
@@ -663,6 +691,7 @@ async def _sleep_runtime(self) -> None:
663691
import httpx
664692

665693
self._raise_if_child_failed()
694+
self._status("Sleeping vLLM runtime to free GPU memory for training")
666695
async with httpx.AsyncClient() as client:
667696
response = await client.post(
668697
f"{self._vllm_base_url}/sleep",
@@ -672,11 +701,13 @@ async def _sleep_runtime(self) -> None:
672701
)
673702
response.raise_for_status()
674703
self._is_sleeping = True
704+
self._status("vLLM runtime is sleeping")
675705

676706
async def _wake_runtime(self) -> None:
677707
import httpx
678708

679709
self._raise_if_child_failed()
710+
self._status("Waking vLLM runtime")
680711
async with httpx.AsyncClient() as client:
681712
response = await client.post(
682713
f"{self._vllm_base_url}/wake_up",
@@ -685,6 +716,7 @@ async def _wake_runtime(self) -> None:
685716
)
686717
response.raise_for_status()
687718
self._is_sleeping = False
719+
self._status("vLLM runtime is awake")
688720

689721
async def register_lora_for_step(self, step: int, checkpoint_dir: str) -> None:
690722
self._raise_if_child_failed()
@@ -764,6 +796,10 @@ async def _ensure_megatron_running(self) -> None:
764796
"w",
765797
buffering=1,
766798
)
799+
self._status(
800+
f"Starting Megatron worker on {num_gpus} GPU(s). "
801+
f"Logs: {self._display_path(megatron_log_path)}"
802+
)
767803
self._megatron_process = await asyncio.create_subprocess_exec(
768804
*managed_process_cmd(command),
769805
cwd=str(project_root),
@@ -778,6 +814,7 @@ async def _ensure_megatron_running(self) -> None:
778814
self._megatron_process,
779815
log_path=megatron_log_path,
780816
)
817+
self._status("Megatron worker is initializing")
781818

782819
def _clear_pending_jobs(self) -> None:
783820
jobs_dir, _training_log_dir, _wake_lock_path = self._megatron_runtime_paths()
@@ -805,9 +842,10 @@ def _resolve_training_lora_path(self) -> str:
805842
async def _prepare_for_training(self) -> str:
806843
self._raise_if_child_failed()
807844
self._validate_megatron_dependencies()
808-
await self._ensure_megatron_running()
845+
# Shared-GPU Megatron must start after vLLM has released GPU memory.
809846
await self._sleep_runtime()
810847
gc_and_empty_cuda_cache()
848+
await self._ensure_megatron_running()
811849

812850
lora_path = self._resolve_training_lora_path()
813851
self._clear_pending_jobs()
@@ -820,6 +858,10 @@ async def _publish_training_checkpoint(
820858
) -> None:
821859
next_step = self._latest_step + 1
822860
new_checkpoint_dir = get_step_checkpoint_dir(self.output_dir, next_step)
861+
self._status(
862+
f"Publishing training checkpoint {next_step} "
863+
f"to {self._display_path(new_checkpoint_dir)}"
864+
)
823865
os.makedirs(new_checkpoint_dir, exist_ok=True)
824866
shutil.copy(
825867
f"{lora_path}/adapter_model.safetensors",
@@ -837,6 +879,7 @@ async def _publish_training_checkpoint(
837879
os.remove(wake_lock_path)
838880

839881
await self._reload_adapter(new_checkpoint_dir, next_step)
882+
self._status(f"Loaded checkpoint {next_step} into vLLM")
840883

841884
async def start_openai_server(
842885
self, config: dev.OpenAIServerConfig | None
@@ -1015,18 +1058,28 @@ async def train_sft(
10151058
log_path=log_path,
10161059
)
10171060
write_megatron_job(job, job_path=job_path)
1061+
self._status(
1062+
f"Starting Megatron SFT job with {serialized_batches.num_batches} "
1063+
f"batch(es). First batch may take a few minutes while kernels compile. "
1064+
f"Training log: {self._display_path(log_path)}"
1065+
)
10181066

10191067
async for result in stream_megatron_job(
10201068
job,
10211069
job_path=job_path,
10221070
process=self._megatron_process,
10231071
process_log_path=self._megatron_log_path,
10241072
):
1025-
yield {
1073+
metrics = {
10261074
"loss/train": float(result["loss"]),
10271075
"loss/learning_rate": float(result["learning_rate"]),
10281076
"loss/grad_norm": float(result["grad_norm"]),
10291077
}
1078+
if "tokens_per_second" in result:
1079+
metrics["throughput/step_trainer_tok_per_s"] = float(
1080+
result["tokens_per_second"]
1081+
)
1082+
yield metrics
10301083

10311084
await self._publish_training_checkpoint(lora_path=lora_path)
10321085
except BaseException:

0 commit comments

Comments
 (0)