Pular para conteúdo

LAB-04: Inference Math: Sampling, Quantization & Loss Analysis

Estimated time to read: 6 minutes

🎯 Learning Objectives

After completing this laboratory, you will be able to:

  • Implement autoregressive token sampling algorithms from scratch (Temperature Scaling, Top-k, and Top-p / Nucleus Sampling).
  • Compute and evaluate model output confidence using Cross-Entropy Loss and Perplexity.
  • Quantify GPU VRAM memory requirements for LLM weights and dynamic KV-Cache overhead across precision schemes (FP32, FP16/BF16, INT8, INT4).
  • Analyze latency, throughput, and memory bandwidth bottlenecks in LLM prefill vs. decode inference stages.

🎓 Prerequisites

We recommend having:

  • Python 3.10+ and PyTorch installed in your environment.
  • Basic understanding of logit vectors, Softmax activation, and probability distributions.
  • Concept of Transformer architecture parameters (layers, hidden dimension, sequence length, batch size).

🧠 Level 1: Intuition & Concepts

1. Autoregressive Sampling Strategies

During text generation, a decoder model outputs raw unnormalized scores (logits) over its vocabulary for the next token. Converting these logits into tokens requires controllable sampling techniques:

  • Temperature Scaling (\(T\)): Scales logits prior to Softmax. Lower temperatures (\(T < 1.0\)) sharpen the distribution toward greedy decoding; higher temperatures (\(T > 1.0\)) flatten it, increasing generation randomness and diversity.
  • Top-k Sampling: Limits selection to the top \(k\) candidate tokens with highest probability, discarding low-probability tail tokens.
  • Top-p (Nucleus) Sampling: Dynamically selects the minimal set of tokens whose cumulative probability reaches threshold \(p\) (e.g., \(0.90\)), adapting the pool size based on model confidence.

2. Loss & Perplexity

To measure how well a model predicts a given sequence of tokens: * Cross-Entropy Loss: Measures the mean negative log-likelihood assigned by the model to the target tokens. * Perplexity (\(\text{PPL}\)): Calculated as \(\exp(\text{Loss})\). Intuitively, it represents the model's effective choice count (branching factor) at each token prediction step.

3. Quantization & VRAM Overhead

Running LLM inference requires calculating memory allocation for two primary components: 1. Model Weights Memory: \(M_{\text{weights}} = P \times b_{\text{bytes}}\) where \(P\) is the parameter count. 2. Dynamic KV Cache Memory: \(M_{\text{kv}} = 2 \times b_{\text{bytes}} \times n_{\text{layers}} \times d_{\text{model}} \times s_{\text{seq}} \times b_{\text{batch}}\).


💻 Level 2: Implementation

Here are the clean PyTorch implementations for inference math and sampling:

import math
import torch
import torch.nn.functional as F

def temperature_top_k_top_p_sampling(
    logits: torch.Tensor,
    temperature: float = 1.0,
    top_k: int = 0,
    top_p: float = 1.0,
) -> torch.Tensor:
    """Applies temperature scaling, top-k, and top-p filtering to logits."""
    # 1. Temperature scaling
    if temperature > 0:
        logits = logits / temperature
    else:
        return torch.argmax(logits, dim=-1, keepdim=True)

    # 2. Top-k filtering
    if top_k > 0:
        indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
        logits = logits.masked_fill(indices_to_remove, float("-inf"))

    # 3. Top-p (Nucleus) filtering
    if top_p < 1.0:
        sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
        cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)

        # Remove tokens with cumulative probability above top_p
        sorted_indices_to_remove = cumulative_probs > top_p
        # Shift mask right to keep first token above threshold
        sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
        sorted_indices_to_remove[..., 0] = 0

        # Scatter back to original indices
        indices_to_remove = sorted_indices_to_remove.scatter(
            dim=-1, index=sorted_indices, src=sorted_indices_to_remove
        )
        logits = logits.masked_fill(indices_to_remove, float("-inf"))

    # 4. Softmax and multinomial sampling
    probs = F.softmax(logits, dim=-1)
    next_token = torch.multinomial(probs, num_samples=1)
    return next_token


def compute_cross_entropy_and_perplexity(
    logits: torch.Tensor, targets: torch.Tensor
) -> tuple[float, float]:
    """Calculates Cross-Entropy Loss and Perplexity."""
    loss_fn = torch.nn.CrossEntropyLoss()
    # Flatten batch and sequence dimensions
    loss = loss_fn(logits.view(-1, logits.size(-1)), targets.view(-1)).item()
    perplexity = math.exp(loss)
    return loss, perplexity


def calculate_inference_vram(
    num_params_billions: float,
    precision_bits: int,
    batch_size: int,
    seq_len: int,
    num_layers: int,
    d_model: int,
) -> dict[str, float]:
    """Calculates VRAM requirements in GB for weights and KV Cache."""
    bytes_per_param = precision_bits / 8.0
    weight_memory_gb = (num_params_billions * 1e9 * bytes_per_param) / (1024**3)

    # KV Cache: 2 (K + V) * layers * d_model * seq_len * batch_size * bytes
    kv_cache_bytes = 2 * num_layers * d_model * seq_len * batch_size * bytes_per_param
    kv_cache_gb = kv_cache_bytes / (1024**3)

    total_vram_gb = weight_memory_gb + kv_cache_gb
    return {
        "weight_memory_gb": round(weight_memory_gb, 2),
        "kv_cache_gb": round(kv_cache_gb, 2),
        "total_vram_gb": round(total_vram_gb, 2),
    }

📐 Level 3: Mathematical Foundations

📐 LAB-04: Inference Math & Quantization Mechanics

Temperature-Adjusted Softmax

Given logits vector \(z \in \mathbb{R}^{|V|}\) and temperature \(T > 0\):

\[P(y = w \mid z) = \frac{\exp(z_w / T)}{\sum_{v \in V} \exp(z_v / T)}\]

Top-k Selection Set

\[V^{(k)} = \text{TopK}(V, k), \quad P^{(k)}(w) = \begin{cases} \frac{\exp(z_w / T)}{\sum_{v \in V^{(k)}} \exp(z_v / T)} & \text{if } w \in V^{(k)} \\ 0 & \text{otherwise} \end{cases}\]

Top-p (Nucleus) Truncation

\[V^{(p)} = \arg\min_{V' \subseteq V} \left\{ |V'| \quad \text{s.t.} \quad \sum_{w \in V'} P(w) \ge p \right\}\]

Cross-Entropy & Perplexity

Given target sequence tokens \(y_1, y_2, \dots, y_N\):

\[\mathcal{L}_{\text{CE}} = -\frac{1}{N} \sum_{i=1}^{N} \ln P(y_i \mid y_{<i})\]
\[\text{PPL} = \exp(\mathcal{L}_{\text{CE}}) = \left( \prod_{i=1}^{N} P(y_i \mid y_{<i}) \right)^{-\frac{1}{N}}\]

Symmetric Uniform Quantization Math (INT8)

Scale factor \(S\) maps floating point range \([-x_{\max}, x_{\max}]\) to 8-bit signed integer range \([-127, 127]\):

\[S = \frac{\max(|X|)}{127}\]
\[X_{\text{quant}} = \text{clamp}\left( \left\lfloor \frac{X}{S} \right\rceil, -128, 127 \right), \quad \hat{X} = X_{\text{quant}} \times S\]

🔬 Level 4: Research Notes (Origin Papers)

  • Nucleus Sampling: Holtzman et al. introduced Top-p sampling to eliminate text degeneration and repetitive loops in autoregressive decoding (Holtzman et al., 2019).
  • 8-bit Matrix Multiplication (LLM.int8()): Dettmers et al. introduced vector-wise quantization and outlier feature extraction for high-precision 8-bit inference (Dettmers et al., 2022).

🏭 Production Insights

🏭 Production Insights: VRAM & Inference Optimization

  • The KV-Cache Bottleneck: In high-concurrency production deployments (e.g. vLLM), KV-Cache size quickly scales beyond model weight memory. Employing PagedAttention, Grouped-Query Attention (GQA), or FP8 KV Caching decreases memory footprint by up to \(8\times\).
  • Prefill vs. Decoding Phase: LLM inference operates in two distinct execution modes:
    1. Prefill Phase (Prompt Processing): Compute-bound matrix-matrix multiplications (\(\text{GEMM}\)), saturating GPU tensor cores efficiently.
    2. Decode Phase (Token Generation): Memory-bandwidth bound matrix-vector multiplications (\(\text{GEMV}\)), bottlenecked by VRAM read speeds.
  • Quantization Trade-offs: FP16 to INT8 reduces VRAM by 50% with near-zero perplexity loss (<0.05 PPL shift). INT4 quantization (AWQ/GPTQ) reduces VRAM by 75%, allowing a 70B parameter model to execute on a single 48GB GPU (A6000/L40S).

📊 LAB-04: Empirical Benchmark Results

Below is the VRAM footprint and Perplexity benchmark for an 8-Billion parameter LLM across different context window sizes and quantization schemes:

Precision Format Weights VRAM KV Cache (8k ctx, B=1) Total VRAM (B=1, 8k) Perplexity Shift (\(\Delta \text{PPL}\))
FP32 (32-bit) 32.0 GB 4.0 GB 36.0 GB 0.00 (Baseline)
FP16 / BF16 (16-bit) 16.0 GB 2.0 GB 18.0 GB +0.01
INT8 (8-bit) 8.0 GB 1.0 GB 9.0 GB +0.04
INT4 / AWQ (4-bit) 4.0 GB 0.5 GB 4.5 GB +0.18

🚀 Interactive Playground

We provide a Jupyter Playground prepared to run, experiment, and modify the code from this laboratory interactively, with full support for free execution on the cloud using Google Colab:

Open in Colab Open Local Notebook


🧪 Try It Yourself (Experiments)

  1. Explore Temperature vs. Top-p: Run the interactive notebook playground and set \(T=1.5\) with \(p=0.5\). Notice how nucleus sampling prevents degraded out-of-context tokens despite high temperature scaling.
  2. Compute Custom Model VRAM: Calculate the VRAM required to serve Llama-3-70B with a sequence length of 16,384 tokens and batch size of 16 under FP16 vs INT4 quantization.

  • vLLM & PagedAttention: Virtual memory paging algorithms for zero-fragmentation KV Cache.
  • Speculative Decoding: Accelerated decoding using lightweight draft models to verify multi-token batches.
  • Quantization Techniques (GPTQ / AWQ / GGUF): Advanced post-training weight quantization methods.