-
Notifications
You must be signed in to change notification settings - Fork 34.1k
[Trainer] accelerate contextparallel support in trainer #40205
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 27 commits
ca8e366
29b22cf
d70fef4
dd58dd0
ecc2366
a629ff0
66d4273
ffa4699
361f122
3d426c1
3efe69b
eca52ac
951527b
412c15e
be60c40
2c357aa
71e082f
37e6fdf
4f6fe15
485d7fa
3d16def
25a308e
6d41365
d82022c
ae9f878
531924e
64d7336
52cb3bc
6e9fb30
bf187b2
33817f3
2294506
f956348
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 |
|---|---|---|
|
|
@@ -3813,6 +3813,123 @@ def _prepare_inputs(self, inputs: dict[str, Union[torch.Tensor, Any]]) -> dict[s | |
|
|
||
| return inputs | ||
|
|
||
| def _is_attention_mask_causal(self, attention_mask): | ||
| """ | ||
| Check if an attention mask is causal (compatible with causal attention). | ||
| Context parallelism only supports causal attention patterns. This function | ||
| checks if the provided attention mask is compatible. | ||
|
|
||
| Args: | ||
| attention_mask (torch.Tensor): The attention mask to check | ||
|
|
||
| Returns: | ||
| bool: True if the mask is causal or compatible with causal attention | ||
| """ | ||
| if attention_mask is None: | ||
| return True # No mask is considered causal (model uses default causal masking) | ||
|
|
||
| # Handle different mask dimensions | ||
| if attention_mask.dim() == 2: | ||
| # (batch_size, seq_len) - standard padding mask, compatible with causal attention | ||
| return True | ||
| elif attention_mask.dim() in [3, 4]: | ||
| # (batch_size, seq_len, seq_len) or (batch_size, num_heads, seq_len, seq_len) | ||
| # Check if it's lower triangular (causal) | ||
| seq_len = attention_mask.shape[-1] | ||
| if seq_len <= 1: | ||
| return True # Single token or empty is always causal | ||
|
|
||
| # Take first batch and head (if 4D) for checking pattern | ||
| if attention_mask.dim() == 4: | ||
| mask = attention_mask[0, 0] # First batch, first head | ||
| else: | ||
| mask = attention_mask[0] # First batch | ||
|
|
||
| # Check if upper triangular part is masked (should be 0 or very negative for causal) | ||
| upper_triangular = torch.triu(mask, diagonal=1) | ||
|
|
||
| # For causal masks, upper triangular should be 0 or very negative (like -inf) | ||
| # Use a reasonable threshold to handle float precision issues | ||
| is_causal = torch.all(upper_triangular <= 1e-6) or torch.all(upper_triangular < -1e4) | ||
| return is_causal.item() if isinstance(is_causal, torch.Tensor) else is_causal | ||
|
|
||
| # For unknown dimensions, be conservative and reject | ||
| return False | ||
|
|
||
| def _prepare_context_parallel_inputs(self, model, inputs: dict[str, Union[torch.Tensor, Any]]): | ||
| """ | ||
| Prepare inputs for context parallelism by setting up buffers and validation. | ||
|
|
||
| Args: | ||
| model: The model being trained | ||
| inputs: Input tensors to prepare | ||
|
|
||
| Returns: | ||
| tuple: (context_manager, prepared_inputs) where context_manager is either | ||
| the context parallelism wrapper or a no-op context | ||
| """ | ||
| if ( | ||
| getattr(self.accelerator, "parallelism_config") is not None | ||
| and self.accelerator.parallelism_config.cp_enabled | ||
| ): | ||
|
Comment on lines
+3862
to
+3884
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be breaking for axolotl as we don't require fsdp here (in accelerate yes but we have a variable to bypass that check
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. still need to fix that potentially but we can do that in a follow up otherwise |
||
| if hasattr(model, "config"): | ||
| if model.config._attn_implementation != "sdpa": | ||
| raise ValueError( | ||
| f"Context parallelism is supported only with SDPA attention, you are using {model.config._attn_implementation}." | ||
| ) | ||
|
|
||
| if "position_ids" not in inputs: | ||
| logger.warning_once("Position IDs not found in the inputs, generating manually") | ||
| inputs["position_ids"] = torch.arange( | ||
| inputs["input_ids"].size(1), device=inputs["input_ids"].device | ||
| ).expand(inputs["input_ids"].size(0), -1) | ||
| if "shift_labels" not in inputs: | ||
| logger.warning_once("Shift labels not found in the inputs, shifting manually") | ||
| if "labels" in inputs: | ||
| _ignore_index = -100 | ||
| labels = nn.functional.pad(inputs["labels"], (0, 1), value=_ignore_index) | ||
| inputs["shift_labels"] = labels[:, 1:].contiguous() | ||
|
|
||
| buffers = [] | ||
| buffer_seq_dims = [] | ||
|
|
||
| if "input_ids" in inputs: | ||
| buffers.append(inputs["input_ids"]) | ||
| buffer_seq_dims.append(1) # Sequence dimension | ||
| if "labels" in inputs: | ||
| buffers.append(inputs["labels"]) | ||
| buffer_seq_dims.append(1) | ||
| if "shift_labels" in inputs: | ||
| buffers.append(inputs["shift_labels"]) | ||
| buffer_seq_dims.append(1) | ||
| if "attention_mask" in inputs and not getattr(self, "_attn_mask_causal_checked", False): | ||
| # Context parallel currently doesn't support other masks than causal | ||
| # Accelerate applies hooks to replace mask with is_causal arg in SDPA | ||
| # Check if the mask is really causal and if not throw an error | ||
| # TODO: check this only once or always, with speed being the cost | ||
| attention_mask = inputs["attention_mask"] | ||
| if not self._is_attention_mask_causal(attention_mask): | ||
| raise ValueError( | ||
| "Context parallelism only supports causal attention masks. " | ||
| "The provided attention_mask is not causal. " | ||
| "Please ensure your data uses causal masking (lower triangular) " | ||
| "or remove the attention_mask to use the model's default causal masking." | ||
| ) | ||
| self._attn_mask_causal_checked = True | ||
| # Include position_ids in context parallelism splitting | ||
| if "position_ids" in inputs and inputs["position_ids"] is not None: | ||
| buffers.append(inputs["position_ids"]) | ||
| buffer_seq_dims.append(1) | ||
|
|
||
| return partial( | ||
| self.accelerator.maybe_context_parallel, | ||
| buffers=buffers, | ||
| buffer_seq_dims=buffer_seq_dims, | ||
| no_restore_buffers=set(buffers), | ||
| ), inputs | ||
|
|
||
| return contextlib.nullcontext, inputs | ||
|
|
||
| def compute_loss_context_manager(self): | ||
| """ | ||
| A helper wrapper to group together context managers. | ||
|
|
@@ -3863,66 +3980,74 @@ def training_step( | |
| Return: | ||
| `torch.Tensor`: The tensor with training loss on this batch. | ||
| """ | ||
| model.train() | ||
| if hasattr(self.optimizer, "train") and callable(self.optimizer.train): | ||
| self.optimizer.train() | ||
| # Prepare buffers for context parallelism | ||
|
|
||
| inputs = self._prepare_inputs(inputs) | ||
| if is_sagemaker_mp_enabled(): | ||
| loss_mb = smp_forward_backward(model, inputs, self.args.gradient_accumulation_steps) | ||
| return loss_mb.reduce_mean().detach().to(self.args.device) | ||
| cp_context, inputs = self._prepare_context_parallel_inputs(model, inputs) | ||
|
|
||
| with self.compute_loss_context_manager(): | ||
| loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) | ||
| # Context manager is no-op if CP isn't enabled | ||
| with cp_context(): | ||
| model.train() | ||
| if hasattr(self.optimizer, "train") and callable(self.optimizer.train): | ||
| self.optimizer.train() | ||
|
|
||
| del inputs | ||
| if ( | ||
| self.args.torch_empty_cache_steps is not None | ||
| and self.state.global_step % self.args.torch_empty_cache_steps == 0 | ||
| ): | ||
| if is_torch_xpu_available(): | ||
| torch.xpu.empty_cache() | ||
| elif is_torch_mlu_available(): | ||
| torch.mlu.empty_cache() | ||
| elif is_torch_musa_available(): | ||
| torch.musa.empty_cache() | ||
| elif is_torch_npu_available(): | ||
| torch.npu.empty_cache() | ||
| elif is_torch_mps_available(): | ||
| torch.mps.empty_cache() | ||
| elif is_torch_hpu_available(): | ||
| logger.warning( | ||
| "`torch_empty_cache_steps` is set but HPU device/backend does not support empty_cache()." | ||
| ) | ||
| else: | ||
| torch.cuda.empty_cache() | ||
| inputs = self._prepare_inputs(inputs) | ||
| if is_sagemaker_mp_enabled(): | ||
| loss_mb = smp_forward_backward(model, inputs, self.args.gradient_accumulation_steps) | ||
| return loss_mb.reduce_mean().detach().to(self.args.device) | ||
|
|
||
| with self.compute_loss_context_manager(): | ||
| loss = self.compute_loss(model, inputs, num_items_in_batch=num_items_in_batch) | ||
|
|
||
| del inputs | ||
| if ( | ||
| self.args.torch_empty_cache_steps is not None | ||
| and self.state.global_step % self.args.torch_empty_cache_steps == 0 | ||
| ): | ||
| if is_torch_xpu_available(): | ||
| torch.xpu.empty_cache() | ||
| elif is_torch_mlu_available(): | ||
| torch.mlu.empty_cache() | ||
| elif is_torch_musa_available(): | ||
| torch.musa.empty_cache() | ||
| elif is_torch_npu_available(): | ||
| torch.npu.empty_cache() | ||
| elif is_torch_mps_available(): | ||
| torch.mps.empty_cache() | ||
| elif is_torch_hpu_available(): | ||
| logger.warning( | ||
| "`torch_empty_cache_steps` is set but HPU device/backend does not support empty_cache()." | ||
| ) | ||
| else: | ||
| torch.cuda.empty_cache() | ||
|
|
||
| kwargs = {} | ||
| kwargs = {} | ||
|
|
||
| # For LOMO optimizers you need to explicitly use the learning rate | ||
| if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: | ||
| kwargs["learning_rate"] = self._get_learning_rate() | ||
| # For LOMO optimizers you need to explicitly use the learning rate | ||
| if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: | ||
| kwargs["learning_rate"] = self._get_learning_rate() | ||
|
|
||
| if self.args.n_gpu > 1: | ||
| loss = loss.mean() # mean() to average on multi-gpu parallel training | ||
| if self.args.n_gpu > 1: | ||
| loss = loss.mean() # mean() to average on multi-gpu parallel training | ||
|
|
||
| if self.use_apex: | ||
| from apex import amp | ||
| if self.use_apex: | ||
| from apex import amp | ||
|
|
||
| with amp.scale_loss(loss, self.optimizer) as scaled_loss: | ||
| scaled_loss.backward() | ||
| else: | ||
| # Finally we need to normalize the loss for reporting if GA loss bug is not fixed during compute loss | ||
| if (not self.model_accepts_loss_kwargs or num_items_in_batch is None) and self.compute_loss_func is None: | ||
| # If the model does not accept loss kwargs, we need to normalize the loss by the number of gradient accumulation steps | ||
| loss = loss / self.current_gradient_accumulation_steps | ||
| with amp.scale_loss(loss, self.optimizer) as scaled_loss: | ||
| scaled_loss.backward() | ||
| else: | ||
| # Finally we need to normalize the loss for reporting if GA loss bug is not fixed during compute loss | ||
| if ( | ||
| not self.model_accepts_loss_kwargs or num_items_in_batch is None | ||
| ) and self.compute_loss_func is None: | ||
| # If the model does not accept loss kwargs, we need to normalize the loss by the number of gradient accumulation steps | ||
| loss = loss / self.current_gradient_accumulation_steps | ||
|
|
||
| # Turning off loss scaling w.r.t. gradient accumulation when DeepSpeed is enabled | ||
| # https://github.com/huggingface/transformers/pull/35808 | ||
| if self.accelerator.distributed_type == DistributedType.DEEPSPEED: | ||
| kwargs["scale_wrt_gas"] = False | ||
| # Turning off loss scaling w.r.t. gradient accumulation when DeepSpeed is enabled | ||
| # https://github.com/huggingface/transformers/pull/35808 | ||
| if self.accelerator.distributed_type == DistributedType.DEEPSPEED: | ||
| kwargs["scale_wrt_gas"] = False | ||
|
|
||
| self.accelerator.backward(loss, **kwargs) | ||
| self.accelerator.backward(loss, **kwargs) | ||
|
|
||
| return loss.detach() | ||
|
|
||
|
|
@@ -4157,6 +4282,9 @@ def _save(self, output_dir: Optional[str] = None, state_dict=None): | |
| # Save a trained model and configuration using `save_pretrained()`. | ||
| # They can then be reloaded using `from_pretrained()` | ||
| if not isinstance(self.model, supported_classes): | ||
| # Defer to accelerate's get_state_dict when using distributed setups that require special state dict handling | ||
| if state_dict is None and (getattr(self.accelerator, "is_fsdp2", False) or self.is_deepspeed_enabled): | ||
| state_dict = self.accelerator.get_state_dict(self.model) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there an issue with how things are currently handled ? just to better understand
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would just silently fail at this point, but it's with custom models which is a rather rare use-case. |
||
| if state_dict is None: | ||
| state_dict = self.model.state_dict() | ||
|
|
||
|
|
@@ -5327,6 +5455,16 @@ def create_accelerator_and_postprocess(self): | |
| args = { | ||
| "deepspeed_plugin": self.args.deepspeed_plugin, | ||
| } | ||
|
|
||
| # We defer compatibility checks to accelerator | ||
| if self.args.parallelism_config is not None: | ||
| if not is_accelerate_available("1.10.0"): | ||
| raise ImportError( | ||
| "ParallelismConfig requires accelerate v1.10.0 and above. Please upgrade accelerate to use this feature." | ||
| ) | ||
|
|
||
|
kashif marked this conversation as resolved.
|
||
| args["parallelism_config"] = self.args.parallelism_config | ||
|
|
||
| if is_accelerate_available("0.28.0"): | ||
| args["dataloader_config"] = dataloader_config | ||
| else: | ||
|
|
@@ -5469,6 +5607,9 @@ def get_batch_samples( | |
| if self.args.n_gpu > 1 and num_items_in_batch.dim() == 0: | ||
| # In the DataParallel case, convert the scalar tensor into a 1-dim tensor | ||
| num_items_in_batch = num_items_in_batch.unsqueeze(0) | ||
| # Divide by number of devices with the same batch | ||
| if pc := self.accelerator.parallelism_config: | ||
| num_items_in_batch = num_items_in_batch // pc.non_data_parallel_size | ||
|
|
||
| return batch_samples, num_items_in_batch | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.