Implements EWoRA in PEFT - #3401
Conversation
BenjaminBossan
left a comment
There was a problem hiding this comment.
Thanks for reviving the EWoRA PR. For this review, I focused exclusively on the PEFT integration itself, keeping the rest for later.
From what I can tell, you probably started this PR based on a somewhat older version of PEFT and taking the LoRA code as a starting point. For this reason, numerous additions are not needed or are out of date. I marked the corresponding code. The good news is that we now require much less code, so overall this should lead to a simplification.
To get the majority of test cases covered, please start by adding EWoRA to the test matrix of our custom model tests. Since EWoRA does not allow merging, add it to this function too. Then run pytest tests/test_custom_models.py -k "ewora" -v and check if all tests pass.
Also, notice that we have reworked our contribution guideline, please check it out. For now, we're at the "Core integration" step, but to merge, we will need to cover the "Final PR" step too. From my perspective, it's, however, okay to finish step one first before proceeding.
| @@ -0,0 +1,70 @@ | |||
| <!--Copyright 2025 The HuggingFace Team. All rights reserved. | |||
There was a problem hiding this comment.
| <!--Copyright 2025 The HuggingFace Team. All rights reserved. | |
| <!--Copyright 2026 The HuggingFace Team. All rights reserved. |
Please use the current year here and throughout.
|
|
||
| Args: | ||
| r (`int`): | ||
| Ewora expert ranks. |
There was a problem hiding this comment.
Let's make it clear if it's the total rank of all experts (thus comparable to the LoRA rank) or the rank per expert (in which case r * num_experts is the correct comparison to LoRA). Also mention the defaults for all EWoRA specific parameters.
| tuner_layer_cls = EworaLayer | ||
| target_module_mapping = TRANSFORMERS_MODELS_TO_EWORA_TARGET_MODULES_MAPPING | ||
|
|
||
| def __init__(self, model, config, adapter_name) -> None: |
There was a problem hiding this comment.
Method not needed, pls remove
| def __init__(self, model, config, adapter_name) -> None: | ||
| super().__init__(model, config, adapter_name) | ||
|
|
||
| def _check_new_adapter_config(self, config: EworaConfig) -> None: |
There was a problem hiding this comment.
Method not needed, pls remove
| x = x.to(ewora_As.dtype) | ||
| x = x.unsqueeze(2).expand(-1, -1, num_experts, -1) | ||
|
|
||
| intermediate = torch.einsum("beid, idj -> beij", dropout(x), ewora_As) |
There was a problem hiding this comment.
Let's add a short comment to explain the indices b, d, e, i, j
| # scores = F.softmax(weighting(F.relu(intermediate.reshape(bs, seq_len, -1)))) | ||
| # scores = weighting(intermediate.reshape(bs, seq_len, -1)) |
| del intermediate | ||
|
|
||
| final = final * scores.unsqueeze(-1) | ||
| result.add_(final.sum(dim=2)) |
There was a problem hiding this comment.
Is this better than result = result + final.sum(dim=2)?
| final = final * scores.unsqueeze(-1) | ||
| result.add_(final.sum(dim=2)) | ||
|
|
||
| del final, scores |
There was a problem hiding this comment.
Regarding the manual deletions, did you measure that this has an effect? Normally, we don't have those calls in PEFT, it would only make sense if we find that they're required for proper memory management.
There was a problem hiding this comment.
This whole test file can be deleted. We already have an extensive test suite, and once EWoRA is added there, the test coverage will be pretty much complete. We don't have tests that check, for instance, if assert lin0.ewora_As["default"].shape == (num_experts, 10, r) , but that's really not needed, as this is basically checking for implementation details
|
@BenjaminBossan - thank you for reviewing my pull request and for the feedback. I have done a re-request to review - hope I have addressed most of your concerns. Please advise on next steps. |
BenjaminBossan
left a comment
There was a problem hiding this comment.
Thank you for the updates and for bringing the PR up-to-speed with the latest PEFT. In this review, I focused on the integration, which is not missing much; I haven't checked the docs or ran the experiments yet.
As the core integration is almost done, please consult our contribution guideline for the next steps.
| rank_pattern (`dict`): | ||
| The mapping from layer names or regexp expression to ranks which are different from the default rank | ||
| specified by `r`. | ||
| megatron_config (`Optional[dict]`): |
There was a problem hiding this comment.
megatron_config, megatron_core, use_dora, and layer_replication are not being used and are just leftovers from starting EWoRA based on LoRA. Please remove these arguments.
| The alpha parameter for Ewora scaling. | ||
| ewora_dropout (`float`): | ||
| The dropout probability for Ewora layers. | ||
| fan_in_fan_out (`bool`): |
| fan_in_fan_out (`bool`): | ||
| Set this to True if the layer to replace stores weight like (fan_in, fan_out). For example, gpt-2 uses | ||
| `Conv1D` which stores weights like (fan_in, fan_out) and hence this should be set to `True`. | ||
| bias (`str`): |
There was a problem hiding this comment.
This is not a very useful option to have, let's simplify our lives and just remove it.
| Args: | ||
| r (`int`): | ||
| The rank of each individual expert adapter (defaults to 256). EWoRA partitions the adaptation into | ||
| `num_experts` independent rank-`r` experts, so the LoRA-equivalent total rank is `r * num_experts`. |
There was a problem hiding this comment.
So with the default settings, we basically get 8 (num_experts) LoRA adapters with rank 256, do I see that right? That is quite a huge default, compared to LoRA, which is just rank 8 by default. Does it really make sense to pick such a high number?
| if isinstance(base_layer, nn.Linear): | ||
| in_features, out_features = base_layer.in_features, base_layer.out_features | ||
| elif isinstance(base_layer, nn.Conv2d): | ||
| in_features, out_features = base_layer.in_channels, base_layer.out_channels | ||
| elif isinstance(base_layer, nn.Embedding): | ||
| in_features, out_features = base_layer.num_embeddings, base_layer.embedding_dim | ||
| elif isinstance(base_layer, Conv1D): | ||
| in_features, out_features = ( | ||
| base_layer.weight.ds_shape if hasattr(base_layer.weight, "ds_shape") else base_layer.weight.shape | ||
| ) | ||
| elif hasattr(base_layer, "infeatures") and hasattr(base_layer, "outfeatures"): | ||
| # QuantLinear | ||
| in_features, out_features = base_layer.infeatures, base_layer.outfeatures | ||
| elif hasattr(base_layer, "input_size") and hasattr(base_layer, "output_size"): | ||
| # Megatron ColumnParallelLinear,RowParallelLinear | ||
| in_features, out_features = base_layer.input_size, base_layer.output_size | ||
| elif hasattr(base_layer, "codebooks") and base_layer.__class__.__name__ == "QuantizedLinear": | ||
| # AQLM QuantLinear | ||
| in_features, out_features = base_layer.in_features, base_layer.out_features | ||
| elif hasattr(base_layer, "w_bit") and base_layer.__class__.__name__ == "WQLinear_GEMM": | ||
| # Awq layers | ||
| in_features, out_features = base_layer.in_features, base_layer.out_features | ||
| elif base_layer.__class__.__name__ == "EetqLinear": | ||
| # Eetq layers | ||
| in_features, out_features = base_layer.in_features, base_layer.out_features | ||
| elif hasattr(base_layer, "W_q") and base_layer.__class__.__name__ == "HQQLinear": | ||
| # HQQ layers | ||
| in_features, out_features = base_layer.in_features, base_layer.out_features |
There was a problem hiding this comment.
We have a utility function now that can simplify the pattern: peft.tuners.utils._get_in_out_features.
| ewora_dropout_layer = nn.Dropout(p=config.ewora_dropout) | ||
| else: | ||
| ewora_dropout_layer = nn.Identity() | ||
| self.ewora_dropout.update(nn.ModuleDict({adapter_name: ewora_dropout_layer})) |
There was a problem hiding this comment.
Simpler:
| self.ewora_dropout.update(nn.ModuleDict({adapter_name: ewora_dropout_layer})) | |
| self.ewora_dropout[adapter_name] = ewora_dropout_layer |
| self.update_layer(adapter_name, r, config) | ||
| self.is_target_conv_1d_layer = is_target_conv_1d_layer | ||
|
|
||
| def forward(self, x: torch.Tensor, *args, **kwargs) -> torch.Tensor: |
There was a problem hiding this comment.
AFAICT, there is no scaling of the LoRA outputs, i.e. something that corresponds to alpha, is that intended? In the paper, you mention that "LoRA Scaling Parameter (α): 32" is "used in all our training
experiments" though it's not quite clear if that also applies to EWoRA in this case. But since it's an important hyper-parameter for LoRA, I would expect it to matter here too.
Resolves #3400