-
Notifications
You must be signed in to change notification settings - Fork 558
feat: CP-local loss and configurable CUDA memory profiling #1223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ | |
| from areal.engine.megatron_utils.megatron_lora import get_vllm_lora_target_modules | ||
| from areal.engine.megatron_utils.packed_context_parallel import ( | ||
| packed_context_parallel_forward, | ||
| split_packed_seqs_for_context_parallel, | ||
| ) | ||
| from areal.engine.megatron_utils.pipeline_parallel import ( | ||
| configure_pipeline_layer_splits, | ||
|
|
@@ -713,7 +714,14 @@ def forward_step(batch_iter, model): | |
| mb_input.padded_mb.update(tree_kwargs) | ||
| tree_attn_keys = list(tree_kwargs.keys()) | ||
|
|
||
| output = packed_context_parallel_forward(model, mb_input.padded_mb) | ||
| cp_size = mpu.get_context_parallel_world_size() | ||
| cp_local = cp_size > 1 | ||
|
|
||
| output = packed_context_parallel_forward( | ||
| model, | ||
| mb_input.padded_mb, | ||
| gather_cp_output=not cp_local, | ||
| ) | ||
|
|
||
| # Release tree attention metadata after forward pass | ||
| for key in tree_attn_keys: | ||
|
|
@@ -729,12 +737,30 @@ def _process_output(input_, output_): | |
| if mpu.is_pipeline_last_stage( | ||
| ignore_virtual=False, vp_stage=model_vp_stage | ||
| ): | ||
| output = unpad_logits( | ||
| output, | ||
| padding_length=mb_input.padding_length, | ||
| cu_seqlens=cu_seqlens, | ||
| old_cu_seqlens=mb_input.old_cu_seqlens, | ||
| ) | ||
| if cp_local and cu_seqlens is not None: | ||
| padded_cu_seqlens = mb_input.padded_mb["cu_seqlens"] | ||
| rolled_ids = torch.roll( | ||
| mb_input.padded_mb["input_ids"], shifts=-1, dims=-1 | ||
| ) | ||
| cp_labels = split_packed_seqs_for_context_parallel( | ||
| rolled_ids, padded_cu_seqlens | ||
| ) | ||
| cp_loss_mask = split_packed_seqs_for_context_parallel( | ||
| mb_input.padded_mb["loss_mask"], padded_cu_seqlens | ||
| ) | ||
| cp_cu_seqlens = padded_cu_seqlens // cp_size | ||
| cp_inputs = dict(mb_input.orig_mb) | ||
| cp_inputs["_cp_local_labels"] = cp_labels | ||
| cp_inputs["loss_mask"] = cp_loss_mask | ||
| cp_inputs["cu_seqlens"] = cp_cu_seqlens | ||
| return output, functools.partial(_process_output, cp_inputs) | ||
| else: | ||
| output = unpad_logits( | ||
| output, | ||
| padding_length=mb_input.padding_length, | ||
| cu_seqlens=cu_seqlens, | ||
| old_cu_seqlens=mb_input.old_cu_seqlens, | ||
| ) | ||
| return output, functools.partial(_process_output, mb_input.orig_mb) | ||
|
|
||
| forward_backward_func = get_forward_backward_func() | ||
|
|
@@ -789,7 +815,11 @@ def process_output( | |
| loss_multiplier=loss_multiplier, | ||
| ) | ||
|
|
||
| self.forward_backward_batch(mb_list, process_output, forward_only=False) | ||
| self.forward_backward_batch( | ||
| mb_list, | ||
| process_output, | ||
| forward_only=False, | ||
| ) | ||
|
Comment on lines
+818
to
+822
|
||
|
|
||
| # Step 4: Optimizer step | ||
| return self.optimizer_step() | ||
|
|
@@ -829,7 +859,9 @@ def process_output( | |
|
|
||
| # Step 4: Aggregate losses | ||
| if mpu.is_pipeline_last_stage(): | ||
| return aggregate_eval_losses(losses, mpu.get_data_parallel_group()) | ||
| return aggregate_eval_losses( | ||
| losses, mpu.get_data_parallel_group(with_context_parallel=True) | ||
| ) | ||
| return None | ||
|
|
||
| @torch.no_grad() | ||
|
|
@@ -1029,6 +1061,19 @@ def _validate_fp8_consistency(self): | |
| def get_device_stats(self) -> DeviceRuntimeInfo: | ||
| return DeviceRuntimeInfo.get_current() | ||
|
|
||
| def start_memory_profile(self, max_entries: int = 100000) -> None: | ||
| torch.cuda.memory._record_memory_history(max_entries=max_entries) | ||
|
|
||
| def stop_memory_profile(self, snapshot_dir: str) -> None: | ||
| pp = mpu.get_pipeline_model_parallel_rank() | ||
| dp = mpu.get_data_parallel_rank() | ||
| cp = mpu.get_context_parallel_rank() | ||
| tp = mpu.get_tensor_model_parallel_rank() | ||
| filename = f"snapshot_rank{self.rank:02d}_p{pp}d{dp}c{cp}t{tp}.pickle" | ||
| path = os.path.join(snapshot_dir, filename) | ||
| torch.cuda.memory._dump_snapshot(path) | ||
| torch.cuda.memory._record_memory_history(enabled=None) | ||
|
|
||
| def save_perf_tracer(self, step: int | None = None, force: bool = False) -> None: | ||
| perf_tracer.save(step=step, force=force) | ||
|
|
||
|
|
@@ -1741,7 +1786,11 @@ def _compute_logprobs_and_loss( | |
| else None, | ||
| ) | ||
| else: | ||
| labels = torch.roll(inputs["input_ids"], shifts=-1, dims=-1) | ||
| cp_local_labels = inputs.get("_cp_local_labels") | ||
| if cp_local_labels is not None: | ||
| labels = cp_local_labels | ||
| else: | ||
| labels = torch.roll(inputs["input_ids"], shifts=-1, dims=-1) | ||
| logprobs, entropy = gather_logprobs_entropy( | ||
| output, | ||
| labels, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,6 +180,12 @@ def train(self): | |
|
|
||
| self.saver.maybe_wait_for_staging() | ||
|
|
||
| if ( | ||
| config.memory_profiler is not None | ||
| and global_step in config.memory_profiler.profile_steps | ||
| ): | ||
| self.actor.start_memory_profile(config.memory_profiler.max_entries) | ||
|
|
||
|
Comment on lines
+183
to
+188
|
||
| with ( | ||
| stats_tracker.record_timing("train_step"), | ||
| perf_tracer.trace_scope( | ||
|
|
@@ -192,6 +198,18 @@ def train(self): | |
| self.actor.step_lr_scheduler() | ||
| self.actor.get_device_stats().log("after train step") | ||
|
|
||
| if ( | ||
| config.memory_profiler is not None | ||
| and global_step in config.memory_profiler.profile_steps | ||
| ): | ||
| log_dir = StatsLogger.get_log_path(config.stats_logger) | ||
| snapshot_dir = os.path.join( | ||
| log_dir, "memory_snapshots", f"step_{global_step}" | ||
| ) | ||
| os.makedirs(snapshot_dir, exist_ok=True) | ||
| self.actor.stop_memory_profile(snapshot_dir) | ||
| logger.info(f"Memory snapshots saved to {snapshot_dir}") | ||
|
|
||
| self.actor.set_version(global_step + 1) | ||
|
|
||
| with ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the CP-local branch,
cp_inputs = dict(mb_input.orig_mb)keeps the unsplit original microbatch tensors (and possibly unpadded lengths) but only replacesloss_mask/cu_seqlensand injects_cp_local_labels. Any other packed, token-aligned fields inorig_mb(e.g., PPO/RW training targets) will have mismatched shapes relative to the CP-shardedoutput, leading to incorrect loss computation or runtime shape errors. Consider buildingcp_inputsfrompadded_mband splitting every token-aligned tensor withsplit_packed_seqs_for_context_parallel, or restrict this path to the SFT loss that only consumes labels/loss_mask.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is by design.
cp_inputs = dict(mb_input.orig_mb)creates a shallow copy of the original microbatch dict, then we overwrite the fields that need CP-local values (_cp_local_labels,loss_mask,cu_seqlens). The SFT loss function (compute_packed_sft_loss) only consumes these replaced fields — it does not use any of the original unsplit tensors. The remaining fields inorig_mb(like metadata) are harmlessly carried through.