Generate: legacy mode is only triggered when `generation_config` is untouched
Author: gante
Pull request: yes

This is a long description, but bear with me -- it is important to understand the whole context here!

# Background

Long ago, in ancient times (2022), `model.config` held all model-related configurations, including generative configuration. This means it would be possible to modify the model config to parameterize `.generate()`, e.g.:

```py
# (Load model, prepare input)
model.generate(**model_inputs)   # generates up to 20 tokens, the default
model.config.max_length = 100
model.generate(**model_inputs)   # now generates up to 100 tokens
```

By the end of last year, since a given model could have its generation parameterized differently for separate purposes, we carved out the generation parameterization into `generate_config`. To facilitate the transition process, in the absence of a manually set `generate_config`, its parameters are pulled from `model.config` and `_from_model_config` is set to `True` to flag it.

*Keeping retrocompatibility is paramount*, so when `_from_model_config` is `True`, we revert to the legacy mode and `model.config` takes precedence.

# The problem

Currenty, here's what happens:
✅ The user never touches `model.generation_config` and does all parameterization through `.generate()` and/or through `model.config` (i.e. legacy mode)
✅ The user sets a new generation config in the model (`model.generation_config = generation_config`) and/or passes a new `generation_config` to `.generate()`, where the former takes precedence.
❌  Hybrid situations: the user modifies a `model.generation_config` while `_from_model_config` is set to `True`. This fails because `model.config` takes precedence, and thus changes are ignored (*with a warning!*). #25917 is an example of this issue, but there were others in the past.

# The solution (this PR)

My proposed solution is based on the following assumption: if the user touches `generation_config`, then they are aware of the best practices to configure `.generate()`. Therefore, when it happens, NEVER enter the legacy mode, and `model.generation_config` is always in charge.

In practice, this is done through hashing: at `generation_config` creation time, we store its hash. If the hash changes, then the user has touched the object, rejecting the legacy mode.


Comments:
_The documentation is not available anymore as the PR was closed or merged._

The first thing I can think of is 

https://docs.python.org/3/reference/datamodel.html#object.__hash__

> ... Although they remain constant within an individual Python process, they are not predictable between repeated invocations of Python. ...

Therefore, using the built-in `hash` is not guaranteed to give the same value even on the same machine (across different python run).

I would suggest to use the standard hash library like `hashlib` and use something like `sha256` on the string.

I will take a deeper look on the generation config side later today.

Also, I would create a new dict with sorted keys before `json.dumps`. The dict is ordered (with python >=3.7), but it's in insertion order. So if a user doing some non-standard operation like below, the dict is the same, but the string represention would be different and hence different hash.

(I am not sure if the following could be done with `GenerationConfig` though.)

```python3
import json


d = {"a": 1, "b": 2}
print(json.dumps(d, indent=2) + "\n")

d.pop("a")
print(json.dumps(d, indent=2) + "\n")

d["a"] = 1
print(json.dumps(d, indent=2) + "\n")
```
gives

```bash
{
  "a": 1,
  "b": 2
}

{
  "b": 2
}

{
  "b": 2,
  "a": 1
}
```


@ydshieh good points! 

The hash is computed from a sorted json string (i.e. a `json.dumps` call with `sort_keys=True`), so the ordering part is already sorted 🙌 

As for the hashing library, I had no idea. Going to update it to a better one. Thank you for your input 💛 

> @ydshieh good points!
> 
> The hash is computed from a sorted json string (i.e. a `json.dumps` call with `sort_keys=True`), so the ordering part is already sorted 🙌

I looked the wrong place: `class InputExample.to_json_string`, sorry 😅 

After a second, it seems `_original_object_hash` is never saved and reloaded. So its lifetime is only inside the python run when it is created, and therefore, we don't need to worry what I mentioned previously.