Efficiency Frontiers › Code Implementation

Michael BrenndoerferMarch 16, 202615 min read
Language AI HandbookAll chapters
Part 67 of 67Frontier Outlook from 2025
Chapter 509Efficiency Frontiers › Code Implementation
Reading section 6 of 10 · Code Implementation · 15 minSection 6 of 10 · 15 min

Book contents

Section 6 of 10 · 15 min read

Efficiency Frontiers › Code ImplementationOne section · Sections in this chapter6 / 10 ↓
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

This section implements several key efficiency techniques: quantization, KV-cache management, and a simple demonstration of speculative decoding concepts.

Setting Up

In[3]:
Code
import numpy as np

# Set random seed for reproducibility
np.random.seed(42)

Quantization: From Float to Integer

Let's implement a simple uniform quantization scheme to see how bit-width affects representational precision.

In[4]:
Code
def quantize(x: np.ndarray, bits: int) -> tuple:
    """Uniform symmetric quantization."""
    x_max = np.max(np.abs(x))
    num_levels = 2 ** (bits - 1) - 1  # symmetric around 0
    scale = x_max / num_levels
    x_int = np.round(x / scale).astype(np.int32)
    x_int = np.clip(x_int, -num_levels, num_levels)
    return x_int, scale


def dequantize(x_int: np.ndarray, scale: float) -> np.ndarray:
    """Reconstruct float values from quantized representation."""
    return x_int.astype(np.float32) * scale


# Generate a sample weight matrix (mimicking a small transformer weight)
W = np.random.randn(64, 64).astype(np.float32)

# Quantize at different bit widths
results = {}
for bits in [8, 4, 2]:
    W_int, scale = quantize(W, bits)
    W_reconstructed = dequantize(W_int, scale)
    error = np.mean((W - W_reconstructed) ** 2)
    # Account for packed storage at the target bit-width. The demonstration
    # array remains int32 so that dequantization is straightforward.
    memory_bytes = W.size * bits // 8
    float_bytes = W.nbytes
    results[bits] = {
        "mse": error,
        "memory_bytes": memory_bytes,
        "float_bytes": float_bytes,
        "compression": float_bytes / memory_bytes,
    }
Out[5]:
Console
Quantization Comparison (64x64 weight matrix)
Bits     MSE             Memory (bytes)     Compression
-------------------------------------------------------
8        0.000080        4096               4.0x
4        0.026122        2048               8.0x
2        0.850858        1024               16.0x

Original FP32 memory: 16384 bytes

The table shows how quantization error increases as bit-width decreases. At 8 bits, the mean squared error is tiny, and the weight values are nearly indistinguishable from the original. At 4 bits, the error grows but remains small enough for most practical purposes. At 2 bits, reconstruction quality degrades substantially, which is why 2-bit quantization is rarely used in practice without special techniques. The compression ratio grows inversely with bit-width: 8-bit weights occupy 4x less memory than FP32, 4-bit weights occupy 8x less.

Out[6]:
Visualization
Bar chart showing quantization MSE rising from near-zero at 8 bits to very high at 2 bits.
Reconstruction error (MSE) versus quantization bit-width for a sample 64x64 weight matrix. Error rises sharply below 4 bits, explaining why INT4 is the practical lower bound for most deployments without specialized techniques.
Bar chart showing memory compression ratio doubling as bit-width halves.
Memory compression ratio versus bit-width relative to FP32. INT8 provides 4x compression, INT4 provides 8x, and INT2 provides 16x, but the quality cost of INT2 typically makes it impractical.

KV-Cache Simulation

Let's simulate the KV-cache to understand how its size grows with context length and how Grouped Query Attention reduces it.

In[7]:
Code
def kv_cache_memory_mb(
    n_tokens: int,
    n_layers: int,
    n_kv_heads: int,
    head_dim: int,
    bytes_per_value: int = 2,  # BF16
) -> float:
    """Compute KV-cache memory in MB."""
    # K and V each: n_layers * n_kv_heads * head_dim * n_tokens
    kv_elements = 2 * n_layers * n_kv_heads * head_dim * n_tokens
    return (kv_elements * bytes_per_value) / (1024**2)


# Model configurations
configs = {
    "7B MHA (32 heads)": {"n_layers": 32, "n_kv_heads": 32, "head_dim": 128},
    "7B GQA (8 KV heads)": {"n_layers": 32, "n_kv_heads": 8, "head_dim": 128},
    "7B MQA (1 KV head)": {"n_layers": 32, "n_kv_heads": 1, "head_dim": 128},
}

context_lengths = [512, 1024, 2048, 4096, 8192, 16384, 32768]

# Compute memory for each config and context length
kv_memory = {}
for name, cfg in configs.items():
    kv_memory[name] = [
        kv_cache_memory_mb(
            n, cfg["n_layers"], cfg["n_kv_heads"], cfg["head_dim"]
        )
        for n in context_lengths
    ]
Out[8]:
Console
KV-Cache Memory (MB) by Context Length and Attention Variant

Config                         512    1024    2048    4096    8192   16384   32768
------------------------------------------------------------------------------------------
7B MHA (32 heads)            256.0   512.0  1024.0  2048.0  4096.0  8192.0  16384.0
7B GQA (8 KV heads)           64.0   128.0   256.0   512.0  1024.0  2048.0  4096.0
7B MQA (1 KV head)             8.0    16.0    32.0    64.0   128.0   256.0   512.0

The table shows how dramatically context length affects KV-cache requirements. At 32,768 tokens, a standard Multi-Head Attention (MHA) model requires several gigabytes just for the KV-cache of a single request. With Grouped Query Attention (GQA, 8 KV heads), this is reduced by 4x. Multi-Query Attention (MQA, 1 KV head) reduces it by 32x but at a higher quality cost. These tradeoffs explain why Llama 3 and Mistral models use GQA by default.

Out[9]:
Visualization
Line chart showing KV-cache memory in MB versus context length for MHA, GQA, and MQA variants.
KV-cache memory requirements for three attention variants across context lengths from 512 to 32,768 tokens. Multi-Head Attention (MHA) grows linearly and reaches several gigabytes at long contexts, while Grouped Query Attention (GQA) and Multi-Query Attention (MQA) offer 4x and 32x reductions respectively. This comparison explains why GQA has become the default for modern open-weight models like Llama 3.

Measuring Arithmetic Intensity

Arithmetic intensity determines whether an operation is compute-bound or memory-bound. Let's compute it for key transformer operations.

In[10]:
Code
def arithmetic_intensity(flops: float, bytes_accessed: float) -> float:
    """Compute arithmetic intensity in FLOPs/byte."""
    return flops / bytes_accessed


# For a matrix multiplication C = A @ B with shapes (m, k) and (k, n)
# FLOPs: 2 * m * k * n (multiply and add for each output element)
# Bytes: (m*k + k*n + m*n) * bytes_per_element
def matmul_intensity(
    m: int, k: int, n: int, bytes_per_element: int = 2
) -> float:
    flops = 2 * m * k * n
    bytes_acc = (m * k + k * n + m * n) * bytes_per_element
    return arithmetic_intensity(flops, bytes_acc)


# For attention softmax on a row of n elements
# FLOPs: ~5n (max, subtract, exp, sum, divide)
# Bytes: n * bytes_per_element * 2 (read + write)
def softmax_intensity(n: int, bytes_per_element: int = 2) -> float:
    flops = 5 * n
    bytes_acc = n * bytes_per_element * 2
    return arithmetic_intensity(flops, bytes_acc)


# Common sizes
seq_len = 2048
d_model = 4096
d_ff = 16384
batch_size = 1

ops = {
    "MatMul (QK^T), seq=2048, d=128": matmul_intensity(seq_len, 128, seq_len),
    "MatMul (FF up), d=4096->16384": matmul_intensity(
        batch_size * seq_len, d_model, d_ff
    ),
    "MatMul (FF down), d=16384->4096": matmul_intensity(
        batch_size * seq_len, d_ff, d_model
    ),
    "Softmax row, n=2048": softmax_intensity(seq_len),
    "Softmax row, n=256": softmax_intensity(256),
}

# H100 hardware roofline (approximate)
peak_flops_per_byte = 300  # H100 ~1000 TFLOPS BF16, ~3 TB/s HBM bandwidth
Out[11]:
Console
Arithmetic Intensity Analysis
Operation                                     FLOPs/byte     Bottleneck
---------------------------------------------------------------------------
MatMul (QK^T), seq=2048, d=128                113.8          Memory
MatMul (FF up), d=4096->16384                 1260.3         Compute
MatMul (FF down), d=16384->4096               1260.3         Compute
Softmax row, n=2048                           1.2            Memory
Softmax row, n=256                            1.2            Memory

H100 roofline crossover: ~300 FLOPs/byte
Operations above crossover are compute-bound; below are memory-bound.

This analysis reveals why FlashAttention matters so much. The QKTQK^T matrix multiplication and softmax operations in attention are memory-bound on modern GPUs, while the feed-forward layer matrix multiplications are compute-bound. FlashAttention specifically targets the memory-bound attention operations by reducing HBM accesses, which is why it delivers such substantial speedups.

Out[12]:
Visualization
Horizontal dot plot on a logarithmic scale showing arithmetic intensity of transformer operations with a dashed H100 roofline threshold.
Arithmetic intensity (FLOPs per byte of memory accessed) for key transformer operations compared to the H100 roofline threshold. Operations below the dashed line are memory-bound and benefit from bandwidth-reducing optimizations like FlashAttention. Operations above are compute-bound and are already well-utilized on modern hardware.

Speculative Decoding: A Simple Demonstration

Let's implement a simplified speculative decoding simulation to illustrate the acceptance-rejection mechanism.

In[13]:
Code
def simulate_speculative_decoding(
    n_tokens: int,
    k_draft: int,
    p_accept: float,
    draft_latency_ms: float,
    verifier_latency_ms: float,
) -> dict:
    """
    Simulate speculative decoding throughput.

    Parameters
    ----------
    n_tokens : int
        Total tokens to generate.
    k_draft : int
        Draft tokens per round.
    p_accept : float
        Per-token acceptance probability.
    draft_latency_ms : float
        Latency of draft model per token (ms).
    verifier_latency_ms : float
        Latency of verifier forward pass (ms), regardless of k.
    """
    rng = np.random.RandomState(42)

    tokens_generated = 0
    total_time_ms = 0.0
    rounds = 0

    while tokens_generated < n_tokens:
        # Draft phase: k_draft autoregressive steps
        draft_time = k_draft * draft_latency_ms

        # Verifier phase: one forward pass evaluating all k draft tokens
        verifier_time = verifier_latency_ms

        # Acceptance is sequential: after the first rejection, later draft
        # tokens are discarded. The verifier supplies the replacement token,
        # or one bonus token when the whole draft prefix is accepted.
        accepted_prefix = 0
        for draw in rng.random(k_draft):
            if draw >= p_accept:
                break
            accepted_prefix += 1
        generated_this_round = accepted_prefix + 1

        tokens_generated += min(
            generated_this_round, n_tokens - tokens_generated
        )
        total_time_ms += draft_time + verifier_time
        rounds += 1

    tokens_per_second = tokens_generated / (total_time_ms / 1000)
    return {
        "tokens_per_second": tokens_per_second,
        "total_time_ms": total_time_ms,
        "rounds": rounds,
        "avg_tokens_per_round": tokens_generated / rounds,
    }


# Baseline: verifier only (no speculation)
def baseline_generation(n_tokens: int, verifier_latency_ms: float) -> dict:
    total_time_ms = n_tokens * verifier_latency_ms
    return {
        "tokens_per_second": n_tokens / (total_time_ms / 1000),
        "total_time_ms": total_time_ms,
    }


# Parameters
n_tokens = 1000
k_draft = 4
draft_latency = 5.0  # ms per token (small 7B model)
verifier_latency = 50.0  # ms per forward pass (70B model)

baseline = baseline_generation(n_tokens, verifier_latency)
scenarios = {
    "p_accept=0.5": simulate_speculative_decoding(
        n_tokens, k_draft, 0.5, draft_latency, verifier_latency
    ),
    "p_accept=0.7": simulate_speculative_decoding(
        n_tokens, k_draft, 0.7, draft_latency, verifier_latency
    ),
    "p_accept=0.85": simulate_speculative_decoding(
        n_tokens, k_draft, 0.85, draft_latency, verifier_latency
    ),
    "p_accept=0.95": simulate_speculative_decoding(
        n_tokens, k_draft, 0.95, draft_latency, verifier_latency
    ),
}
Out[14]:
Console
Speculative Decoding Simulation (4 draft tokens, 1000 total tokens)
Draft latency: 5.0 ms/token, Verifier latency: 50.0 ms/pass

Scenario           tok/s        Speedup vs baseline    Avg tokens/round
----------------------------------------------------------------------
Baseline (no spec) 20.0         1.0x                   1.0
p_accept=0.5       26.9         1.34                 x 1.88
p_accept=0.7       39.1         1.96                 x 2.74
p_accept=0.85      52.1         2.61                 x 3.65
p_accept=0.95      64.6         3.23                 x 4.52

The simulation shows how speculative decoding speedup scales with acceptance rate. At 50% acceptance, the expected speedup is about 1.4x because draft-model overhead offsets part of the gain. At 85-95% acceptance (typical for a well-matched draft-verifier pair on natural text), the idealized speedup is roughly 2.6-3.2x. The key insight is that the draft model cost is small relative to the verifier, so even moderate acceptance rates produce meaningful gains.

Out[15]:
Visualization
Line chart showing idealized speculative decoding speedup increasing with acceptance probability from below 1x to above 3x.
Idealized speculative decoding throughput speedup versus per-token acceptance probability with 4 draft tokens and a 10:1 verifier-to-draft latency ratio. The speedup rises from about 1.4x at 50% acceptance to over 3x at 95% acceptance, illustrating why a well-matched draft model is critical for realizing the technique's potential.

Comparing Architecture Efficiency

Let's compute theoretical compute requirements for standard transformer versus MoE architectures.

In[16]:
Code
def transformer_flops_per_token(
    n_layers: int, d_model: int, d_ff: int, n_heads: int, seq_len: int
) -> dict:
    """
    Estimate FLOPs per forward pass token for a dense transformer.
    Counts multiply-adds as 2 FLOPs.
    """
    d_head = d_model // n_heads

    # Attention: QKV projections + attention + output projection
    attn_qkv = 2 * 3 * d_model * d_model  # QKV linear projections
    attn_scores = 2 * n_heads * seq_len * d_head  # Q @ K^T
    attn_values = 2 * n_heads * seq_len * d_head  # attn @ V
    attn_out = 2 * d_model * d_model  # output projection

    # Feed-forward: up projection + activation + down projection
    ff_up = 2 * d_model * d_ff
    ff_down = 2 * d_ff * d_model

    per_layer = (
        attn_qkv + attn_scores + attn_values + attn_out + ff_up + ff_down
    )
    total = n_layers * per_layer

    return {
        "total_flops": total,
        "attn_flops_per_layer": attn_qkv + attn_scores + attn_values + attn_out,
        "ff_flops_per_layer": ff_up + ff_down,
    }


def moe_flops_per_token(
    n_layers: int,
    d_model: int,
    d_ff: int,
    n_heads: int,
    seq_len: int,
    n_experts: int,
    top_k: int,
) -> dict:
    """
    Estimate active FLOPs per token for a MoE transformer.
    Only top_k experts fire per token.
    """
    d_head = d_model // n_heads

    # Attention is the same as dense
    attn_qkv = 2 * 3 * d_model * d_model
    attn_scores = 2 * n_heads * seq_len * d_head
    attn_values = 2 * n_heads * seq_len * d_head
    attn_out = 2 * d_model * d_model

    # Only top_k of n_experts are active
    ff_up = 2 * top_k * d_model * d_ff
    ff_down = 2 * top_k * d_ff * d_model

    # Gating network
    gating = 2 * d_model * n_experts

    per_layer = (
        attn_qkv
        + attn_scores
        + attn_values
        + attn_out
        + ff_up
        + ff_down
        + gating
    )
    total = n_layers * per_layer

    total_params_ff = n_layers * n_experts * (d_model * d_ff + d_ff * d_model)
    active_params_ff = n_layers * top_k * (d_model * d_ff + d_ff * d_model)

    return {
        "total_flops": total,
        "total_ff_params": total_params_ff,
        "active_ff_params": active_params_ff,
    }


# Model configurations: 7B dense vs Mixtral-style 8x7B MoE
dense_cfg = {
    "n_layers": 32,
    "d_model": 4096,
    "d_ff": 14336,
    "n_heads": 32,
    "seq_len": 512,
}
moe_cfg = {
    "n_layers": 32,
    "d_model": 4096,
    "d_ff": 14336,
    "n_heads": 32,
    "seq_len": 512,
    "n_experts": 8,
    "top_k": 2,
}

dense_result = transformer_flops_per_token(**dense_cfg)
moe_result = moe_flops_per_token(**moe_cfg)
Out[17]:
Console
Dense Transformer vs Mixtral-style MoE (per token)

Metric                              Dense 7B        MoE 8x7B
-----------------------------------------------------------------
Active FLOPs per token (GFLOPs)     12.08           19.60
FF param count (M)                  32              -
Total FF params (M)                 -               30064M
Active FF params (M)                -               7516M

MoE uses 1.6x the FLOPs of dense for this seq_len,
but has ~4x more total FF parameters with top-2 routing.
The quality improvement from larger capacity justifies the parameter overhead.

The computation confirms the MoE efficiency story. With top-2 routing across 8 experts, each MoE layer uses roughly 2x the feed-forward FLOPs of a dense layer (because 2 of 8 experts are active), but the model has 8x more total feed-forward capacity. The practical result is that Mixtral 8x7B competes with Llama 2 70B on many benchmarks while being much faster at inference.