Model Serving › Model Loading and Initialization
Language AI HandbookAll chapters Model Serving › Model Loading and InitializationOne section · Sections in this chapter2 / 9 ↓
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
Before any request can be served, the model must be loaded into memory and prepared for inference. This sounds straightforward but involves several design decisions that affect startup time, memory usage, and steady-state serving performance.
Weight Loading Strategies
The most basic approach is loading all model weights into GPU memory at startup. For a 7-billion-parameter model in 16-bit floating point, this requires approximately 14 GB of GPU memory just for weights, before accounting for activations and KV caches during inference. Larger models like 70B or 405B parameters require distributed loading across multiple GPUs.
The weight loading process involves several stages. First, the checkpoint files are read from disk. PyTorch checkpoints are typically stored as sharded files that are memory-mapped to avoid loading everything into CPU RAM simultaneously. Memory mapping lets the operating system handle the actual data movement on demand, paging in portions of the weight file only when the corresponding tensor is accessed. Then weights are transferred to GPU memory, which involves PCIe bandwidth if the GPU is discrete. For large models, this transfer alone can take tens of seconds.
Lazy loading defers this work, loading tensor shards only when they are first accessed. This reduces startup time but introduces latency spikes on first access, which is unacceptable in production where the first user request should not bear the cost of initializing the system. In practice, inference servers perform eager loading with warmup requests that pre-populate caches and ensure all code paths have been compiled before serving real traffic.
Why warmup requests? Many deep learning frameworks use just-in-time compilation: the first time a particular sequence length or batch size is encountered, the framework compiles a specialized CUDA kernel for that shape. Subsequent requests with the same shape reuse the compiled kernel and are much faster. Running warmup requests before opening the server to production traffic forces this compilation to happen in advance, preventing the first real users from experiencing artificially slow responses.
def simulate_model_loading(
num_params_billions: float, dtype_bytes: int = 2
) -> dict:
"""
Simulate the memory and time cost of loading model weights.
dtype_bytes=2 for float16, 4 for float32.
"""
params = num_params_billions * 1e9
weight_bytes = params * dtype_bytes
weight_gb = weight_bytes / (1024**3)
# Rough estimates based on NVMe SSD (~3 GB/s) and PCIe bandwidth (~25 GB/s)
disk_read_time_s = weight_gb / 3.0
pcie_transfer_time_s = weight_gb / 25.0
total_load_time_s = disk_read_time_s + pcie_transfer_time_s
return {
"model_size_gb": weight_gb,
"disk_read_time_s": disk_read_time_s,
"pcie_transfer_time_s": pcie_transfer_time_s,
"total_load_time_s": total_load_time_s,
}
model_sizes = [7, 13, 34, 70]
loading_stats = {size: simulate_model_loading(size) for size in model_sizes}Model | Weights (GB) | Disk Read | PCIe Transfer | Total Load ----------------------------------------------------------------- 7B params | 13.0 | 4.3s | 0.5s | 4.9s 13B params | 24.2 | 8.1s | 1.0s | 9.0s 34B params | 63.3 | 21.1s | 2.5s | 23.6s 70B params | 130.4 | 43.5s | 5.2s | 48.7s
These estimates reveal why model serving infrastructure is designed to avoid frequent restarts. A 70B model takes over a minute just to load from disk and transfer to GPU memory, not counting model initialization and warmup. Production serving systems aim for zero-downtime deployments that gradually shift traffic to new instances rather than restarting existing ones. A cold start causes seconds of unavailability and delays the point when a new instance can absorb its share of production traffic. Other instances must carry additional load during that window.
Safetensors is an increasingly common checkpoint format that addresses some of these loading bottlenecks. Unlike PyTorch's native format, safetensors files store tensor data with a simple binary layout and a JSON header describing tensor shapes and offsets. This allows memory-mapped loading without any deserialization overhead, and different tensors can be loaded by different threads simultaneously because their file offsets are independent. For large models, safetensors reduces load time by up to 40% compared to traditional PyTorch checkpoints.
Tensor Parallelism for Large Models
When a model is too large for a single GPU, tensor parallelism splits individual weight matrices across multiple GPUs. Each GPU holds a shard of every layer's weights, and all-reduce operations synchronize intermediate results between GPUs during each forward pass.
Consider a linear layer with weight matrix . With tensor parallelism across GPUs, each GPU holds a column shard . For an input , each GPU computes its partial output , then the results are concatenated or summed across GPUs using collective operations.
where:
- : the input dimension of the linear layer
- : the output dimension of the linear layer
- : the number of GPUs participating in tensor parallelism
- : the weight shard held on GPU , containing columns
- : the partial output computed on GPU
The trade-off is communication overhead. Every layer requires GPU-to-GPU communication, which adds latency proportional to the number of layers and the bandwidth between GPUs. NVLink (on server-class hardware) provides much higher bandwidth than PCIe, making tensor parallelism more viable on GPU clusters connected with high-speed interconnects. On PCIe-only systems, the communication overhead for 8-way tensor parallelism can add tens of milliseconds per forward pass, enough to substantially increase response latency for interactive applications.
Pipeline parallelism is an alternative that assigns different layers to different GPUs rather than splitting individual layers. This reduces inter-GPU communication to just the activations passed between stages, but introduces pipeline bubbles where GPUs wait idle while upstream stages complete. Practical systems often combine tensor and pipeline parallelism for very large models: tensor parallelism within a node (where GPUs share NVLink) and pipeline parallelism across nodes (where communication is limited to Ethernet or InfiniBand).
def estimate_tensor_parallel_overhead(
num_gpus: int, layer_hidden_dim: int, nvlink: bool = True
) -> dict:
"""
Estimate communication overhead per layer with tensor parallelism.
All-reduce communicates 2 * (N-1)/N * message_size bytes total.
"""
# Activation size in bytes (float16, batch=1, seq=512)
activation_bytes = 512 * layer_hidden_dim * 2
# All-reduce volume: 2 * (N-1)/N * size
allreduce_bytes = 2 * ((num_gpus - 1) / num_gpus) * activation_bytes
# Bandwidth: NVLink ~600 GB/s bidirectional, PCIe ~25 GB/s
bandwidth_gbs = 600 if nvlink else 25
allreduce_time_us = (allreduce_bytes / (bandwidth_gbs * 1e9)) * 1e6
return {
"num_gpus": num_gpus,
"activation_mb": activation_bytes / (1024**2),
"allreduce_bytes": allreduce_bytes,
"allreduce_time_us": allreduce_time_us,
"interconnect": "NVLink" if nvlink else "PCIe",
}
gpu_configs = [2, 4, 8]
dim = 8192 # typical hidden dim for a 70B model
results_nvlink = [
estimate_tensor_parallel_overhead(n, dim, nvlink=True) for n in gpu_configs
]
results_pcie = [
estimate_tensor_parallel_overhead(n, dim, nvlink=False) for n in gpu_configs
]Tensor Parallelism Communication Overhead per Layer Hidden dimension: 8192, Batch=1, Seq=512 tokens GPUs | NVLink (us) | PCIe (us) ----------------------------------- 2 | 14.0 | 335.5 4 | 21.0 | 503.3 8 | 24.5 | 587.2 For a 80-layer model, multiply per-layer overhead by 80 8-GPU NVLink total comm: 1957 us = 2.0 ms 8-GPU PCIe total comm: 46976 us = 47.0 ms
The difference between NVLink and PCIe interconnects becomes critical for large-scale tensor parallelism. On PCIe, the communication overhead for an 8-GPU configuration with 80 layers adds tens of milliseconds per forward pass, substantially increasing latency. NVLink reduces this to a few milliseconds, making 8-way tensor parallelism practical for serving. This is one reason that server-class GPU machines with NVLink (such as DGX systems) command a substantial price premium over workstation-class hardware with the same number of GPUs but only PCIe connectivity.
Memory Budgeting
Planning memory allocation before deployment prevents out-of-memory failures under load. GPU memory serves three main consumers during LLM inference.
Model weights occupy a fixed amount of memory determined by parameter count and data type. A 7B-parameter model in float16 uses 14 GB. Using int8 quantization (as discussed in Part XXXI) halves this to 7 GB, and int4 quantization halves it again to 3.5 GB, at the cost of some quality degradation.
Activations are the intermediate tensors computed during the forward pass. Their size depends on batch size and sequence length. For autoregressive decoding, activations are relatively small because only one token is processed per step. During prefill, however, the entire prompt is processed in parallel, and activation memory scales with sequence length. A system that accepts very long prompts must plan for the activation spike that occurs when a large prefill batch is processed.
The KV cache stores the key and value tensors for all tokens in currently active sequences. This is typically the most dynamic component: more concurrent requests means more KV cache consumption. Understanding the KV cache formula is essential for capacity planning. The KV cache size per request is:
where:
- : one tensor for keys, one for values
- : number of transformer layers in the model
- : number of attention heads per layer
- : dimensionality of each attention head
- : current sequence length for the request
- : 2 for float16, 1 for int8
Notice that , the model's hidden dimension. So the formula simplifies to , which makes intuitive sense: each of the layers stores a full-hidden-dimension key and value vector for every token position. As sequences grow longer or as more requests are served concurrently, KV cache consumption grows proportionally.
def compute_kv_cache_bytes(
n_layers: int,
n_heads: int,
d_head: int,
seq_len: int,
bytes_per_element: int = 2,
) -> int:
"""
Compute KV cache memory for a single sequence.
Factor of 2 accounts for both K and V tensors.
"""
return 2 * n_layers * n_heads * d_head * seq_len * bytes_per_element
# Model configs: (layers, heads, d_head)
model_configs = {
"Llama-2-7B": (32, 32, 128),
"Llama-2-13B": (40, 40, 128),
"Llama-2-70B": (80, 64, 128),
}
seq_lengths = [512, 2048, 4096]
kv_results = {}
for model_name, (layers, heads, d_head) in model_configs.items():
kv_results[model_name] = {}
for seq_len in seq_lengths:
kv_gb = compute_kv_cache_bytes(layers, heads, d_head, seq_len) / (
1024**3
)
kv_results[model_name][seq_len] = kv_gbKV Cache Memory per Request (GB, float16) Model | 512 tokens | 2048 tokens | 4096 tokens --------------------------------------------------------------------- Llama-2-7B | 0.250 | 1.000 | 2.000 Llama-2-13B | 0.391 | 1.562 | 3.125 Llama-2-70B | 1.250 | 5.000 | 10.000 With 100 concurrent requests at 2048 tokens: Llama-2-7B: 100.0 GB KV cache Llama-2-13B: 156.2 GB KV cache Llama-2-70B: 500.0 GB KV cache
These numbers show why KV cache management is so critical for serving. A system handling 100 concurrent requests at 2048 tokens with Llama-2-7B needs over 50 GB of KV cache alone, well exceeding the memory of a single A100 GPU. Effective serving requires careful memory budgeting and dynamic allocation strategies like PagedAttention to maximize the number of concurrent requests per GPU.
The interplay between these three memory consumers creates a planning problem. Weights are fixed, so that term is determined by the model choice. Activations are bounded by batch size and sequence length limits you configure. KV cache is the flexible term that fills remaining GPU memory and determines how many concurrent requests you can handle. The formula for maximum concurrent requests, given GPU memory, is approximately:
This directly motivates quantization: halving weight memory with int8 quantization doubles the headroom available for KV cache, allowing roughly twice as many concurrent requests. The trade-off is inference quality, which must be validated for each specific model and task.

KV Cache Management and PagedAttention
Traditional serving systems allocated a contiguous memory block for each request's KV cache at the start of the request, sized to the maximum allowed sequence length. This caused two problems. First, most sequences end long before reaching the maximum length, leaving most of the pre-allocated memory wasted. Second, memory fragmentation gradually reduced the effective memory available as many small gaps between allocations accumulated, making it impossible to serve new requests even when there seemed to be enough free memory in aggregate.
PagedAttention, introduced by the vLLM team in 2023, solved both problems by borrowing the virtual memory paging model from operating systems. Instead of allocating one contiguous block per request, PagedAttention divides KV cache memory into fixed-size blocks (pages), typically holding 16 or 32 token positions each. Each request is assigned pages on demand as new tokens are generated. When a request finishes, its pages are returned to a free pool and immediately available for new requests.
This design has three practical consequences. First, memory waste from over-allocation nearly disappears because pages are allocated incrementally. Second, fragmentation is eliminated because pages are interchangeable units of fixed size. Third, pages from different requests can share physical memory blocks if they have identical prefixes (prompt caching), because two requests starting with the same system prompt can point their early KV cache pages at the same physical memory rather than each maintaining their own copy. This prefix caching trick is particularly valuable for chat applications where most requests begin with the same assistant persona and instructions.
PagedAttention manages KV cache memory as fixed-size pages, similar to how an operating system manages virtual memory. Each request's key and value tensors are stored in non-contiguous pages that may be scattered across GPU memory. An attention lookup translates logical page numbers to physical page addresses at runtime. The resulting memory efficiency allows 2-4 times more concurrent requests per GPU compared to traditional contiguous allocation.