Autoregressive Generation › Complete Generation Implementation

Michael BrenndoerferUpdated July 30, 20255 min read
Language AI HandbookAll chapters
Part 28 of 67GPT Architecture
Chapter 201Autoregressive Generation › Complete Generation Implementation
Reading section 8 of 12 · Complete Generation Implementation · 5 minSection 8 of 12 · 5 min

Book contents

Section 8 of 12 · 5 min read

Autoregressive Generation › Complete Generation ImplementationOne section · Sections in this chapter8 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's put everything together into a complete, production-quality generation function:

In[36]:
Code
from dataclasses import dataclass


@dataclass
class GenerationConfig:
    """Configuration for text generation."""

    max_new_tokens: int = 50
    temperature: float = 1.0
    top_k: int | None = None
    top_p: float | None = None
    repetition_penalty: float = 1.0
    stop_sequences: list[str] | None = None


def generate_with_config(
    model,
    tokenizer,
    prompt: str,
    config: GenerationConfig,
) -> str:
    """
    Complete generation function with configurable sampling strategies.

    Supports temperature scaling, top-k sampling, nucleus sampling,
    repetition penalty, and stop sequences.
    """
    input_ids = tokenizer.encode(prompt, return_tensors="pt")
    generated_ids = input_ids.clone()

    for _ in range(config.max_new_tokens):
        with torch.no_grad():
            outputs = model(generated_ids)
            logits = outputs.logits[:, -1, :]

            # Apply repetition penalty
            if config.repetition_penalty != 1.0:
                for token_id in generated_ids[0].unique():
                    logits[0, token_id] /= config.repetition_penalty

            # Apply temperature
            if config.temperature != 1.0:
                logits = logits / config.temperature

            # Apply top-k filtering
            if config.top_k is not None:
                top_k_logits, top_k_indices = torch.topk(logits, config.top_k)
                logits = torch.full_like(logits, float("-inf"))
                logits.scatter_(1, top_k_indices, top_k_logits)

            # Apply nucleus (top-p) filtering
            if config.top_p is not None:
                sorted_logits, sorted_indices = torch.sort(
                    logits, descending=True
                )
                cumulative_probs = torch.cumsum(
                    F.softmax(sorted_logits, dim=-1), dim=-1
                )
                sorted_indices_to_remove = cumulative_probs > config.top_p
                sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[
                    :, :-1
                ].clone()
                sorted_indices_to_remove[:, 0] = False
                indices_to_remove = sorted_indices_to_remove.scatter(
                    1, sorted_indices, sorted_indices_to_remove
                )
                logits[indices_to_remove] = float("-inf")

            # Greedy selection of highest-probability token
            next_token = torch.argmax(logits, dim=-1, keepdim=True)

        generated_ids = torch.cat([generated_ids, next_token], dim=1)

        # Check EOS
        if next_token.item() == tokenizer.eos_token_id:
            break

        # Check stop sequences
        if config.stop_sequences:
            current_text = tokenizer.decode(
                generated_ids[0], skip_special_tokens=True
            )
            for stop_seq in config.stop_sequences:
                if stop_seq in current_text[len(prompt) :]:
                    idx = current_text.find(stop_seq, len(prompt))
                    return current_text[:idx]

    return tokenizer.decode(generated_ids[0], skip_special_tokens=True)
Out[37]:
Console
Prompt: The meaning of life is

----------------------------------------------------------------------
Greedy (temp=0.01)  : not the same as the meaning of death.
Creative (temp=1.2) : not the same as the meaning of death.
Top-k=50            : not the same as the meaning of death.
Nucleus p=0.9       : not the same as the meaning of death.

The different configurations produce noticeably different outputs. Near-greedy decoding (low temperature) gives focused, predictable completions. Higher temperature introduces more variety but can become less coherent. Top-k and nucleus sampling offer different ways to balance diversity and quality. The right choice depends on your application: creative writing benefits from higher diversity, while factual Q&A typically works better with lower temperature.

Out[38]:
Visualization
Bar chart showing the original token probability distribution with most mass in the top few tokens and a long tail.
Original probability distribution from the model. Most probability mass concentrates in the top tokens, with a long tail of low-probability options.
Bar chart showing a sharpened probability distribution after temperature = 0.5 scaling, with the top token more dominant.
Temperature = 0.5 sharpens the distribution, making the top token even more dominant. Lower temperature reduces randomness.
Bar chart showing top-k = 5 filtering where only the five highest-probability tokens remain and others are zeroed out.
Top-k = 5 truncates to only the 5 most likely tokens (orange), zeroing out the rest (gray). Remaining probabilities are renormalized.
Bar chart showing nucleus sampling where tokens accumulating to 90% cumulative probability are retained and the rest discarded.
Nucleus (top-p = 0.9) keeps the smallest set of tokens whose cumulative probability exceeds 90% (red). This adapts to the distribution shape.