The HuggingFace Stack

Loading models bigger than your GPU

device_map="auto" spreads a model's layers across GPU, RAM and disk so a model too big for any single one of them can still load and run.

On this page 5
  1. Why it exists
  2. How it works
  3. A real example you have seen
  4. Remember this
  5. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

device_map="auto" places a model's layers wherever they fit — fast GPU memory first, then computer RAM, then disk — so a too-big model can still run.

Think of cooking a wedding feast in a small kitchen. The dishes you are stirring right now sit on the counter. Ingredients for later wait in the fridge. The rarely-needed sacks live in the storeroom downstairs. Nothing is thrown away; things live at different distances from your hands.

A big model gets the same treatment. Its layers are the ingredients, and your GPU — the fast, small memory — is the counter.

Why it exists

A model's numbers must sit in memory to compute. A mid-range GPU holds 8 GB; popular models need 16, 40, or 140 GB. For years the result was a crash: the dreaded CUDA out of memory error, the "counter is full" of deep learning.

The fix is an automatic kitchen manager. Before loading, it measures every layer, checks every storage space, and writes a placement plan. Layers flow to the fastest home that has room.

How it works

model layers:   [1] [2] [3] [4] [5] [6]

GPU  (counter)   [1] [2] [3] [4]        ← fast, small
RAM  (fridge)            [5] [6]        ← bigger, slower
disk (storeroom)              ...       ← huge, very slow

computing walks layer by layer, wherever each one lives

The price is honest: every layer parked outside the GPU makes each answer slower, because data must travel to the counter when its turn comes. The storeroom keeps the feast possible, not fast.

A real example you have seen

Your phone's photo gallery. Recent photos live on the phone and open instantly. Older ones live in the cloud and take a second to stream in. You see one seamless gallery; a placement manager decides what lives where.

Remember this

  • Models must fit in memory somewhere; device_map="auto" writes the placement plan.
  • Order of preference: GPU, then RAM, then disk — each bigger and slower.
  • Offloading trades speed for possibility. Expect slower answers, not magic.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install transformers torch accelerate

Tested with transformers 5.6 and accelerate 1.13. The device_map machinery lives in accelerate. Model: distilgpt2, ~356 MB download.

Automatic placement, and reading the plan

device_map.py
import torch
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "distilgpt2",
    device_map="auto",          # let accelerate place the layers
    dtype=torch.float16,        # half precision: half the memory
)
print("device:", model.device)
print("footprint:", round(model.get_memory_footprint() / 1e6), "MB")
Output
device: cuda:0
footprint: 164 MB

This output is from a machine with one NVIDIA GPU. On a laptop without one you will see device: cpu — same code, different plan. Small model, roomy hardware: everything landed in one place.

Forcing a split, to watch the machinery

Real splits happen when the model outgrows the GPU. You can simulate that by lying about how much GPU memory exists:

forced_split.py
import torch
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "distilgpt2",
    device_map="auto",
    dtype=torch.float16,
    max_memory={0: "100MB", "cpu": "10GB"},   # pretend the GPU is tiny
)
for module, place in model.hf_device_map.items():
    print(f"{module:20} -> {place}")
Output
transformer.wte      -> 0
lm_head              -> 0
transformer.wpe      -> 0
transformer.drop     -> 0
transformer.ln_f     -> cpu
transformer.h        -> cpu

A 164 MB model against a fake 100 MB budget: embeddings went to GPU 0, the transformer blocks spilled to CPU RAM. A warning also appears — "Some parameters are on the meta device because they were offloaded to the cpu" — which is the split announcing itself. This snippet needs a CUDA GPU; without one there is nothing to split against.

The walkthrough

dtype=torch.float16 halved the bill before placement started. 82M parameters at 4 bytes would be ~328 MB; at 2 bytes, 164 MB. Half precision is the first move for inference, before any offloading. In transformers 5 this argument is named dtype — you will still see the older name torch_dtype in tutorials written against v4.

hf_device_map exists only when it has something to say. When everything fits on one device, transformers 5 skips the attribute — check model.device instead. After a real split, the map lists every module's home.

Send inputs to where the model starts. inputs.to(model.device) handles both worlds; hard-coding "cuda" breaks the code on CPU machines.

Disk is the third tier. Add offload_folder="offload" and layers beyond RAM spill to disk. Generation then crawls — weights stream from disk on every single token. It is a way to try a model, not to serve one. For actually shrinking the bill, see loading in 4-bit.

Common mistakes

Calling .to("cuda") after device_map. The placement plan already decided homes; yanking everything to one device either crashes on memory or defeats the offload. Pick one mechanism, not both.

Expecting offloaded speed. Layers on CPU compute at CPU speed, and travel costs are paid per forward pass. If generation feels 50x slower with a split, that is arithmetic, not a bug.

Forgetting dtype and loading fp32. Double the memory for no inference benefit — the difference between fitting and spilling for many models.

Using device_map for training. The mechanism targets inference. Training needs gradients and optimizer states placed too — a different problem, handled by Accelerate configs, DeepSpeed and friends.

Try it yourself

Tighten the fake budget to "50MB" and print the map — which extra modules moved out? Then time generate() for 20 tokens in the fits-on-GPU and forced-split versions, and put a number on what offloading costs you on your machine.

What to learn next

Researcher — Mathematics and papers.

How the plan is computed

device_map="auto" is accelerate's infer_auto_device_map: compute per-module weight sizes from the checkpoint index, take available memory per device (max_memory defaults derive from real free memory, minus a reserve), and assign modules greedily in model order — GPUs in index order, then CPU, then disk. Certain classes are marked unsplittable (_no_split_modules, e.g. a whole attention block) so no layer straddles devices. The greedy order explains the demo: early modules (embeddings) claimed the GPU budget, later blocks spilled.

Loading without materialising

The enabling trick is meta-device initialisation: the model skeleton is built with init_empty_weights(), allocating shapes but no storage, then safetensors shards are memory-mapped and copied tensor-by-tensor directly to each tensor's assigned device. Peak host memory stays near one shard's size rather than the model's. This is also why transformers 5 refuses legacy pickle checkpoints on older torch (CVE-2025-32434) — safetensors is both the safety and the mmap efficiency story.

Execution semantics

Accelerate attaches AlignDevicesHooks: before a module's forward, its inputs (and, for offloaded layers, its weights) are moved to the compute device; afterwards, weights are released. Execution is strictly sequential across devices — this is vertical partitioning without pipelining. With one request there is no parallelism to exploit; with micro-batching one could pipeline (GPipe, Huang et al., 2019), but inference offloading does not. Hence the honest framing: device_map buys capacity, never throughput.

Disk tier: weights stream from mmap'd files per forward; effective token latency becomes disk-bandwidth-bound. Quantisation dominates offloading on quality-per-latency for most single-GPU cases (Dettmers et al., 2022, LLM.int8; Dettmers et al., 2023, QLoRA) — see 4-bit loading.

The training-side counterparts

Offloading during training must also place gradients and optimizer states — 12+ bytes per parameter for Adam in mixed precision before activations. ZeRO-Offload (Ren et al., 2021) and ZeRO-Infinity (Rajbhandari et al., 2021) partition those states across CPU/NVMe with overlapped communication; DeepSpeed and FSDP implement the ideas. device_map shares only the philosophy: memory hierarchy as one address space.

What to learn next