Generate: Add assisted generation
# What does this PR do?

Here it is, the PR for assisted generation 🙌 In a nutshell, it uses an assistant model (which should be a smaller model with the same tokenizer) to speed up generation, taking advantage of the reduced need for memory transfers in the main model forward pass. It leverages the same property that makes batched inference faster per token.

Since it is meant to be a reference implementation, the code is meant to be clear and well-commented. If you come across any non-obvious steps, let me know so I can clarify them!

Follow-up steps after this PR:
1. Add support for a `sample` version of assisted generation (many cool apps rely on sampling, including chatbots/assistants)
2. Write a blog post a prepare strong communications about the feature

_________________________________________________________________
To process the potential speedup visually, consider the following script and the two videos. They correspond to greedy search using a 6.9B GPTNeoX model on an nvidia 3090 🚀 

<details>
  <summary>Script</summary>

  ```py
  from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
  import torch
  import time
  
  model_id = "EleutherAI/pythia-6.9b-deduped"
  assistant_id = "EleutherAI/pythia-160m-deduped"
  
  tokenizer = AutoTokenizer.from_pretrained(model_id)
  
  assistant_model = AutoModelForCausalLM.from_pretrained(assistant_id)
  assistant_model = assistant_model.to("cuda")
  
  model_kwargs = {
      "pretrained_model_name_or_path": model_id,
      "device_map": "auto",
      "max_memory": {0: "20GiB", "cpu": "50GiB"},
      "torch_dtype": torch.float16,
  }
  model = AutoModelForCausalLM.from_pretrained(**model_kwargs)
  
  inputs = tokenizer("Here's how to cook a good ramen:", return_tensors="pt").to("cuda")
  
  streamer = TextStreamer(tokenizer=tokenizer)
  
  print("Without assistance:")
  start = time.time()
  model.generate(**inputs, streamer=streamer, max_new_tokens=128)
  print(f"Elapsed time: {time.time() - start:.2f} seconds")
  
  print("With assistance:")
  start = time.time()
  model.generate(**inputs, assistant_model=assistant_model, streamer=streamer, max_new_tokens=128)
  print(f"Elapsed time: {time.time() - start:.2f} seconds")

  ```
</details>

Without assistant             |  With assistant
:-------------------------:|:-------------------------:
<img src="https://user-images.githubusercontent.com/12240844/232580502-19965b8d-0f9e-45d8-b57b-86fad2d4681b.gif"/> |  <img src="https://user-images.githubusercontent.com/12240844/232580535-30a27fd2-1338-4c71-a0ba-68055a825605.gif"/>

(focus on the speed and the fact that the output is the same, not on the output itself)
_The documentation is not available anymore as the PR was closed or merged._<|||||>@amyeroberts @sgugger -- since this PR is a bit more complex than most, I've decided to request a review from you two 🤗 <|||||>@amyeroberts regarding splitting up, I totally agree! And not only on this method but on most parts of `GenerationMixin`.  Not only are the functions long, but they reuse a significant part of the logic. I want to address that in the near future, by designing a `.generate()` that can be somehow composed of a sequence of smaller functional blocks. I haven't figured out the deets, but I'd expect that a good implementation would get us better readability, less code duplication, and higher flexibility for HW/model/decoding-specific implementations! 💅 

Before merging, I'm going to double-check that the current code keeps the performance numbers I got a few weeks ago. If everything goes well, it will be merged today 🙏 