9 min read

The Billion-Token Bottleneck: Architecting High-Throughput MoE Inference with Tiered Memory and Predictive Prefetching

High-Throughput MoE Inference via Tiered Memory and Prefetching

The industry is currently obsessed with a singular paradox: we want models that know everything, but we want them to run on hardware that can’t fit them.

When Mixtral 8x7B dropped, followed by the behemoth Grok-1 and DBRX, the narrative shifted overnight. We moved from the era of "Dense" transformers—where every parameter works for every token—to the era of Mixture-of-Experts (MoE). On paper, MoE is a cheat code. It gives you the reasoning capabilities of a trillion-parameter model with the compute cost of a much smaller one.

But here is the engineering reality that keeps infrastructure leads up at night: MoE models are memory-bound nightmares.

While you only compute with a fraction of the parameters (the "active" experts), you still have to store the entire model somewhere. When you factor in the massive KV-cache (Key-Value cache) required for long-context windows—128k, 256k, or even a million tokens—you hit the VRAM Wall.

In this deep dive, we’re going to look under the hood at how we’re re-engineering the inference stack. We’ll explore how to manage KV-caches that fluctuate like a volatile stock market and how to use tiered memory prefetching to trick the GPU into thinking it has ten times more HBM than it actually does.


The Geometry of the Problem: Why MoE is Different

In a standard dense model (like Llama 3), every token passes through every layer and every weight. In an MoE model, each layer contains multiple "experts" (usually feed-forward networks). A router decides which 1 or 2 experts are best suited to handle a specific token.

This creates a massive discrepancy between Compute (FLOPs) and Memory (Parameters).

  1. Dense Model: 70B parameters $\rightarrow$ 70B parameters active per token.
  2. MoE Model: 1.8T parameters $\rightarrow$ Only 12B parameters active per token.

From a compute perspective, the MoE is "cheap." But from a memory perspective, it is "expensive." If your 1.8T parameter model takes up 3.6TB of space (at FP16), you can't just shove that into a single H100 (80GB). You need a cluster. And once you have a cluster, your biggest enemy isn't the GPU speed—it's the interconnect bottleneck and the KV-cache fragmentation.


The KV-Cache: The Silent Memory Killer

Before we solve the MoE parameter problem, we have to address the KV-cache. In autoregressive generation, we store the Keys and Values of previous tokens to avoid re-computing them.

As context windows grow, the KV-cache grows linearly. For a massive MoE model serving thousands of concurrent users, the KV-cache can easily exceed the size of the model weights themselves.

PagedAttention and Dynamic Allocation

The first step in our optimization is moving away from contiguous memory allocation. Standard deep learning frameworks allocate a fixed block of memory for the KV-cache based on the maximum sequence length. If a user only asks for 10 tokens but you've allocated for 32,000, you are wasting 99% of that memory.

We utilize PagedAttention (pioneered by vLLM). Think of it like Virtual Memory in an OS. We divide the KV-cache into blocks.

# Conceptual PagedAttention Block Mapping
class KVCacheManager:
    def __init__(self, num_blocks, block_size):
        self.free_blocks = list(range(num_blocks))
        self.block_table = {} # Maps Request ID to list of physical blocks

    def allocate(self, request_id, num_tokens):
        needed_blocks = (num_tokens + self.block_size - 1) // self.block_size
        allocated = [self.free_blocks.pop() for _ in range(needed_blocks)]
        self.block_table[request_id] = allocated
        return allocated

By using non-contiguous blocks, we reduce internal fragmentation to near zero. But in a large-scale MoE pipeline, even PagedAttention isn't enough. We need to move blocks between HBM (High Bandwidth Memory) and System RAM.


Tiered Memory: The Hierarchy of Truth

To run a trillion-parameter MoE without buying a data center's worth of H100s, we implement a Tiered Memory Architecture. We treat the GPU, the CPU, and the NVMe drive as a single, unified memory pool.

Level 1: GPU HBM (The Hot Zone)

This is where the active experts for the current token and the most recent KV-cache blocks reside. Access is measured in terabytes per second.

Level 2: System RAM / DDR5 (The Warm Zone)

This is where the "inactive" experts and the older KV-cache blocks live. Using PCIe Gen 5 or CXL (Compute Express Link), we can stream these into the GPU at 64GB/s to 128GB/s.

Level 3: NVMe (The Cold Zone)

For massive context (million-token histories), we offload KV-cache blocks to NVMe. While slow (7-14GB/s), it allows for "infinite" context capacity.

The Engineering Challenge: Hiding the Latency

The problem with tiered memory is latency. If the router decides Token A needs Expert #42, but Expert #42 is sitting in System RAM, the GPU will idle while the weights are fetched over PCIe. This kills throughput.

The solution? Predictive Prefetching.


Predictive Prefetching: Looking into the Future

In a standard MoE, you don't know which expert you need until the router processes the token. By then, it's too late to fetch weights from System RAM.

To solve this, we implement Speculative Routing.

1. The Shadow Router

We run a much smaller, "draft" version of the router (a tiny MLP) a few tokens ahead of the actual model. This shadow router predicts which experts are likely to be triggered for the next 5-10 tokens.

2. Async Weight Streaming

Once the shadow router makes a prediction, we initiate an asynchronous cudaMemcpyAsync to pull those expert weights from DDR to HBM before the main model reaches that layer.

// Pseudocode for Asynchronous Expert Prefetching
void forward_pass_with_prefetch(Token current_token, int lookahead_depth) {
    // 1. Run shadow router for future tokens
    auto predicted_experts = shadow_router.predict(current_token, lookahead_depth);

    for (int i = 0; i < lookahead_depth; i++) {
        // 2. Start async transfer of predicted weights from CPU to GPU
        if (!hbm_cache.contains(predicted_experts[i])) {
            stream_weights_async(predicted_experts[i], cpu_memory, gpu_hbm);
        }
    }

    // 3. Compute current token with already-resident weights
    compute_layer(current_token, current_layer_experts);
}

3. Double Buffering the KV-Cache

We apply the same logic to the KV-cache. Since LLM generation is sequential, we know exactly which blocks we will need for the next token's attention mechanism. We prefetch the KV-blocks for the next sequence chunk from System RAM while the GPU is busy doing the matrix multiplication for the current chunk.


The "Hype" vs. The Substance: Why This Matters Now

There’s a lot of noise about "Zero-GPU" inference or "Local LLMs on MacBooks." While Unified Memory on Apple Silicon is impressive, it’s not designed for high-concurrency production workloads.

The real breakthrough isn't just "running" the model; it's running it at a Tokens Per Second per Dollar (TPS/$) that makes sense for a business.

The hype around MoE (like GPT-4's rumored architecture) suggests that sparsity is a free lunch. It’s not. The "substance" behind the hype is that MoE trades compute-bound problems for IO-bound problems.

If you are a developer using an MoE API, you don't see this. But if you are an engineer building the infra, you realize that your job has shifted from optimizing CUDA kernels to optimizing Data Orchestration.


Infrastructure Scale: Networking is the New Memory

When we scale these MoE models across multiple nodes (e.g., an 8-node cluster of H100s), the Tiered Memory concept extends to the network.

Expert Parallelism (EP)

Instead of replicating the entire MoE on every GPU, we shard the experts. GPU 1 holds Experts 1-8, GPU 2 holds Experts 9-16, and so on.

When a token on GPU 1 needs Expert 9, we don't move the weights. We move the token. This is called an All-to-All communication pattern.

RDMA and InfiniBand

To prevent the network from becoming the bottleneck, we use RDMA (Remote Direct Memory Access). This allows GPU 1 to write the token data directly into the memory of GPU 2 without involving the CPU. At 400Gbps (InfiniBand NDR), the latency of sending a token to another node is often lower than the latency of fetching a weight from local System RAM.


Advanced KV-Cache Compression: GQA and Quantization

Even with tiered memory, we want to keep as much KV-cache in HBM as possible. We employ two critical techniques:

  1. Grouped-Query Attention (GQA): Instead of having a unique Key and Value head for every Query head, we share KV heads across groups. This reduces the KV-cache size by a factor of 4x or 8x with negligible loss in accuracy.
  2. KV-Cache Quantization (INT8/FP8): We don't store KV values in FP16. Recent research shows we can quantize the KV-cache to 8-bit or even 4-bit integers.

The Math of Savings:

  • Model: 100B params.
  • Context: 100k tokens.
  • FP16 KV-cache: ~200GB.
  • INT4 KV-cache: ~50GB.

By moving to INT4, we suddenly fit 4x more concurrent users on the same hardware.


The Orchestration Layer: Putting it all Together

How does this look in a production environment? At scale, we use a custom orchestration layer that sits between the Load Balancer and the GPU Workers.

  1. Request Batching: We use Continuous Batching (iteration-level scheduling). Instead of waiting for a whole batch to finish, we insert new requests as soon as one token is generated.
  2. Priority-Based Eviction: If the HBM fills up, we don't just kill a request. We use an LRU (Least Recently Used) policy to swap its KV-cache blocks to System RAM. If the user starts typing again, we swap them back.
  3. Cross-Layer Prefetching: We don't just prefetch within a single request. We look at the entire batch. If 5 different users are all likely to need "Expert #7" in the next 3 steps, we prioritize that weight fetch above all else.

Code Snippet: The Scheduler Logic

class MoEScheduler:
    def step(self):
        # 1. Check HBM pressure
        if self.get_hbm_usage() > 0.95:
            self.evict_to_cpu(self.find_lru_request())

        # 2. Look ahead for expert needs
        needed_experts = self.shadow_router.get_batch_predictions(self.active_batch)

        # 3. Trigger async transfers
        for expert_id in needed_experts:
            if not self.gpu_registry.is_resident(expert_id):
                self.memory_engine.prefetch(expert_id)

        # 4. Execute the iteration
        return self.executor.run_iteration(self.active_batch)

Final Thoughts: The Future of Sparse Inference

Optimizing MoE inference is move toward Software-Defined Hardware. We are no longer treating the GPU as a black box that runs kernels. We are treating the entire server—its NVMe lanes, its PCIe topology, its NUMA nodes, and its NICs—as a single, fluid compute engine.

The companies that win the MoE era won't necessarily be the ones with the best models, but the ones with the best Inference Orchestration.

When you can serve a trillion-parameter model with the latency of a 10B model and the cost-profile of a 50B model, you've cracked the code. You’ve successfully navigated the billion-token bottleneck, turning a memory-bound nightmare into a high-throughput reality.

As we look toward the next generation of hardware—H200s with 141GB of HBM3e and the eventual rise of CXL 3.0—the strategies we’ve discussed here will become the standard architecture for the AI-native backbone. The wall is high, but the ladders we’re building are faster.


More to explore

Keep diving in