Repository: huggingface/peft

Issue:
ModulesToSaveWrapper not working with ModulesDict dictionary methods
### System Info

transformers 4.37.2
peft 0.8.2
accelerate 0.27.2
torch  2.2.0

        

### Who can help?

_No response_

### Information

- [ ] The official example scripts
- [X] My own modified scripts

### Tasks

- [ ] An officially supported task in the `examples` folder
- [ ] My own task or dataset (give details below)

### Reproduction


Error:
```
Exception has occurred: TypeError
'ModulesToSaveWrapper' object is not subscriptable
  File "/home/sam_work/code/tag_classification/test.py", line 14, in forward
    return self.foo_dict["foo"](torch.rand(1,10))
           ~~~~~~~~~~~~~^^^^^^^
  File "/home/sam_work/code/tag_classification/test.py", line 25, in <module>
    lora_model()
TypeError: 'ModulesToSaveWrapper' object is not subscriptable
```

```python
from peft import *
from torch import nn, Tensor
import torch


class ModelFoo(nn.Module):
    def __init__(self) -> None:
        super().__init__()
        self.other_layer = nn.Linear(10, 10)
        self.foo_dict = nn.ModuleDict({"foo": nn.Linear(10, 10)})

    def forward(self) -> Tensor:
        return self.foo_dict["foo"](torch.rand(1, 10))


model = ModelFoo()

peft_config = LoraConfig(
    target_modules=["other_layer"],
    modules_to_save=["foo_dict"],
)
model()
lora_model = get_peft_model(model=model, peft_config=peft_config)

lora_model()
```

### Expected behavior

The dictionary methods are promoted to wrapper - can use modulesdict without modifying code.

Seems like only forward is getting promoted to the wrapper and treating the modulesdict as a dictionary doesn't work. . Similar errors for items keys values indexing with key etc. Also same issues with iterating over ModulesList. Can look into working on it myself if there isn't some reason this hasn't been implemented,


Discussion:
`ModulesToSaveWrapper` is not really set up to work with `ModuleDict` at all at the moment. Even if `__getitem__` etc. were exposed, I'm not sure if it would work correctly, e.g. `forward` would also fail IIUC. Maybe we should just not allow `ModuleDict` at all here.
Ok, would be nice to add a warning when initializing or at least documentation for ModuleDicta and ModuleList

Resolving patch:
diff --git a/src/peft/utils/other.py b/src/peft/utils/other.py
--- a/src/peft/utils/other.py
+++ b/src/peft/utils/other.py
@@ -178,6 +178,17 @@ def __init__(self, module_to_save, adapter_name):
         self._active_adapter = adapter_name
         self._disable_adapters = False
         self.update(adapter_name)
+        self.check_module()
+
+    def check_module(self):
+        """Perform some sanity checks on the module to ensure that it works"""
+        # Try to anticipate some modules that users could try to target that would not work.
+        # Note: It's not possible to check hasattr(module, "forward"), since that returns True for ModuleDict and
+        # ModuleList, even though their forward methods cannot be called
+        forbidden_classes = (torch.nn.ModuleDict, torch.nn.ModuleList, torch.nn.ParameterDict, torch.nn.ParameterList)
+        if isinstance(self.original_module, forbidden_classes):
+            cls_name = self.original_module.__class__.__name__
+            raise TypeError(f"modules_to_save cannot be applied to modules of type {cls_name}")
 
     @property
     def disable_adapters(self) -> bool:


Test patch:
diff --git a/tests/test_other.py b/tests/test_other.py
new file mode 100644
--- /dev/null
+++ b/tests/test_other.py
@@ -0,0 +1,75 @@
+# Copyright 2023-present the HuggingFace Inc. team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import torch
+from torch import nn
+
+from peft import LoraConfig, get_peft_model
+
+
+class ModelWithModuleDict(nn.Module):
+    def __init__(self):
+        super().__init__()
+        self.other_layer = nn.Linear(10, 10)
+        self.module = nn.ModuleDict({"foo": nn.Linear(10, 10)})
+
+    def forward(self):
+        return self.module["foo"](torch.rand(1, 10))
+
+
+class ModelWithModuleList(nn.Module):
+    def __init__(self):
+        super().__init__()
+        self.other_layer = nn.Linear(10, 10)
+        self.module = nn.ModuleList([nn.Linear(10, 10)])
+
+    def forward(self):
+        return self.module[0](torch.rand(1, 10))
+
+
+class ModelWithParameterDict(nn.Module):
+    def __init__(self):
+        super().__init__()
+        self.other_layer = nn.Linear(10, 10)
+        self.module = nn.ParameterDict({"foo": nn.Parameter(torch.rand(10, 10))})
+
+    def forward(self):
+        return self.module["foo"]
+
+
+class ModelWithParameterList(nn.Module):
+    def __init__(self):
+        super().__init__()
+        self.other_layer = nn.Linear(10, 10)
+        self.module = nn.ParameterList([nn.Parameter(torch.rand(10, 10))])
+
+    def forward(self):
+        return self.module[0]
+
+
+@pytest.mark.parametrize(
+    "cls", [ModelWithModuleDict, ModelWithModuleList, ModelWithParameterDict, ModelWithParameterList]
+)
+def test_modules_to_save_targets_module_dict_raises(cls):
+    model = cls()
+    peft_config = LoraConfig(
+        target_modules=["other_layer"],
+        modules_to_save=["module"],
+    )
+    model()  # sanity check that the model would normally work
+
+    msg = "modules_to_save cannot be applied to modules of type"
+    with pytest.raises(TypeError, match=msg):
+        get_peft_model(model=model, peft_config=peft_config)