Repository: huggingface/transformers
Pull request: [configuration] allow to overwrite kwargs from subconfigs

# What does this PR do?

Fixes https://github.com/huggingface/transformers/issues/38258 and allows users to overwrite kwargs from subconfigs in the same way as we allow to overwrite in the main config. See below code for what has changed


```python
# Create a config that is same as the LLaVa-NeXT Mistral with smaller vocab size

# Before
config = AutoConfig.from_pretrained("llava-hf/llava-v1.6-mistral-7b-hf", text_config={"vocab_size": 900})
config.text_config.sliding_window 
>>> 4096 # default value for sliding_window in Mistral config
config.text_config.vocab_size
>>> 900  # Vocab size was changed to user-defined kwarg

# After
config = AutoConfig.from_pretrained("llava-hf/llava-v1.6-mistral-7b-hf", text_config={"vocab_size": 900})
config.text_config.sliding_window
>>> None # sliding_window from loaded checkpoint
config.text_config.vocab_size
>>> 900  # Vocab size was changed to user-defined kwarg
```

Changed files:
filename: src/transformers/configuration_utils.py
patch: @@ -825,8 +825,11 @@ def from_dict(
             if hasattr(config, key):
                 current_attr = getattr(config, key)
                 # To authorize passing a custom subconfig as kwarg in models that have nested configs.
+                # We need to update only custom kwarg values instead and keep other attributes in subconfig.
                 if isinstance(current_attr, PretrainedConfig) and isinstance(value, dict):
-                    value = current_attr.__class__(**value)
+                    current_attr_updated = current_attr.to_dict()
+                    current_attr_updated.update(value)
+                    value = current_attr.__class__(**current_attr_updated)
                 setattr(config, key, value)
                 if key != "dtype":
                     to_remove.append(key)

filename: tests/test_configuration_common.py
patch: @@ -154,6 +154,41 @@ def create_and_test_config_from_and_save_pretrained_composite(self):
                     sub_config_loaded_2 = sub_class.from_pretrained(tmpdirname2)
                     self.parent.assertEqual(sub_config_loaded.to_dict(), sub_config_loaded_2.to_dict())
 
+    def create_and_test_config_from_pretrained_custom_kwargs(self):
+        """
+        Tests that passing custom kwargs to the `from_pretrained` will overwrite model's saved config values.
+        for composite configs. We should overwrite only the requested keys, keeping all values of the
+        subconfig that are loaded from the checkpoint.
+        """
+        # Check only composite configs. We can't know which attributes each type fo config has so check
+        # only text config because we are sure that all text configs have a `vocab_size`
+        config = self.config_class(**self.inputs_dict)
+        if config.get_text_config() is config or not hasattr(self.parent.model_tester, "get_config"):
+            return
+
+        # First create a config with non-default values and save it. The reload it back with a new
+        # `vocab_size` and check that all values are loaded from checkpoint and not init from defaults
+        non_default_inputs = self.parent.model_tester.get_config().to_dict()
+        config = self.config_class(**non_default_inputs)
+        original_text_config = config.get_text_config()
+        text_config_key = [key for key in config if getattr(config, key) is original_text_config]
+
+        # The heuristic is a bit brittle so let's just skip the test
+        if len(text_config_key) != 1:
+            return
+
+        text_config_key = text_config_key[0]
+        with tempfile.TemporaryDirectory() as tmpdirname:
+            config.save_pretrained(tmpdirname)
+
+            # Set vocab size to 20 tokens and reload from checkpoint and check if all keys/values are identical except for `vocab_size`
+            config_reloaded = self.config_class.from_pretrained(tmpdirname, **{text_config_key: {"vocab_size": 20}})
+            original_text_config_dict = original_text_config.to_dict()
+            original_text_config_dict["vocab_size"] = 20
+
+            text_config_reloaded_dict = config_reloaded.get_text_config().to_dict()
+            self.parent.assertDictEqual(text_config_reloaded_dict, original_text_config_dict)
+
     def create_and_test_config_with_num_labels(self):
         config = self.config_class(**self.inputs_dict, num_labels=5)
         self.parent.assertEqual(len(config.id2label), 5)
@@ -204,3 +239,4 @@ def run_common_tests(self):
         self.create_and_test_config_with_num_labels()
         self.check_config_can_be_init_without_params()
         self.check_config_arguments_init()
+        self.create_and_test_config_from_pretrained_custom_kwargs()