Memory management#
Use this page when you care about how much RAM/VRAM a dataset takes, which GPU runs the work, and how to give the memory back when you are done. For opening files and choosing a loader, start with IO/GPU.
Dtype in plain language#
The dtype is how each number is stored.
dtype |
range |
size |
use it for |
|---|---|---|---|
|
0 to 255 |
1 byte |
fast preview copies |
|
0 to 65535 |
2 bytes |
raw detector counts |
|
decimals |
4 bytes |
processed maps |
For raw electron detector counts, start with uint16. It keeps the measured
counts exactly and is still much smaller than float32.
Use uint8 only when you want a lightweight preview or tutorial copy. It is
fast and small, but it can saturate real counts above 255.
If you request dtype="u8" and any counts exceed 255, load warns you:
Warning: dtype='u8' saturated 1,482,913 pixels (0.0124%) above 255 to 255.
Pass dtype='u16' or 'auto' to keep full counts.
That warning is intentional. It means the browsing copy is no longer exact, so
use dtype="u16" or the default load for quantitative work.
Size estimates#
A 4096 x 4096 image is about:
dtype |
size |
|---|---|
|
16 MB |
|
32 MB |
|
64 MB |
A common 512 x 512 x 192 x 192 4D-STEM scan is much larger:
load mode |
approximate size |
|---|---|
full detector, |
18-20 GB |
|
4.5-5 GB |
|
1.1-1.3 GB |
|
about 0.6 GB |
Leave a few GB free for the viewer, browser, and downstream processing.
NVIDIA GPU workflow#
Most lab workflows should run Python on the NVIDIA workstation and open JupyterLab from a laptop. The workstation holds the data and runs the GPU work; the laptop is the frontend.
from quantem.widget import load, Show4DSTEM
data = load("scan_master.h5") # CUDA is selected automatically when available
Show4DSTEM(data)
Good first choices:
GPU memory |
first try |
|---|---|
96 GB |
|
48 GB |
|
24 GB |
|
16 GB or less |
|
Check the GPU before and after a large load with quantem.widget.io.memory():
from quantem.widget import load, Show4DSTEM
from quantem.widget.io import memory
memory() # check VRAM before loading
data = load("scan_001_master.h5", verbose=True)
print(data.data.shape, data.data.dtype, f"{data.data.nbytes / 1e9:.1f} GB")
memory() # confirm VRAM after loading
Typical output:
VRAM GPU0 12.6 / 95.0 GB used (82.4 free) [torch 0.0, cupy 0.0] NVIDIA RTX PRO 6000
RAM 84.1 / 540.0 GB used (447.2 free)
Loaded 1,048,576 frames (19.3 GB) in 6.42s (3.0 GB/s)
(1024, 1024, 96, 96) uint16 19.3 GB
VRAM GPU0 32.1 / 95.0 GB used (62.9 free) [torch 0.0, cupy 19.3] NVIDIA RTX PRO 6000
RAM 84.4 / 540.0 GB used (446.8 free)
Read this as: the full detector-count stack is now on the NVIDIA GPU as
uint16; no browser copy has been quantized.
Can I choose the NVIDIA GPU inside the notebook?#
Yes. Put this in the first notebook cell, before importing torch, cupy,
quantem.widget, or any other GPU package:
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # use physical NVIDIA GPU 0
Then import and load normally:
import torch
from quantem.widget import load, Show4DSTEM
print(torch.cuda.get_device_name(0))
data = load("scan_001_master.h5")
Show4DSTEM(data)
Example output on a Linux workstation with NVIDIA GPUs:
cuda available: True
visible device count: 1
notebook device 0: NVIDIA RTX PRO 6000 Blackwell Workstation Edition
free 80.3 GiB / total 94.9 GiB
Inside that notebook, the selected GPU is called cuda:0. CUDA renumbers the
visible device, so physical GPU 1 also appears as cuda:0 if you selected it
with CUDA_VISIBLE_DEVICES="1".
How do I switch from GPU 0 to GPU 1?#
Change the first cell, restart the kernel, then run from the top:
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "1" # switch to physical NVIDIA GPU 1
Example output after restarting the Python process with GPU 1 selected:
cuda available: True
visible device count: 1
notebook device 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition
free 94.4 GiB / total 95.0 GiB
Restarting matters. Once CUDA is initialized in a Python process, changing
CUDA_VISIBLE_DEVICES later in the notebook is not a reliable way to move the
work to another GPU.
How do I use two NVIDIA GPUs at the same time?#
Run one Jupyter process per GPU. Start each server with a different
CUDA_VISIBLE_DEVICES value:
# terminal 1: GPU 0
CUDA_VISIBLE_DEVICES=0 jupyter lab --no-browser --ip=0.0.0.0 --port=8888
# terminal 2: GPU 1
CUDA_VISIBLE_DEVICES=1 jupyter lab --no-browser --ip=0.0.0.0 --port=8889
Then open the printed URLs from your laptop. Each notebook sees its assigned
GPU as cuda:0.
Check what the notebook sees:
import torch
print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0))
print(torch.cuda.mem_get_info()) # free bytes, total bytes
Check and free GPU memory#
Check the GPU before and after a large load:
import torch
free, total = torch.cuda.mem_get_info()
print(f"free {free / 1e9:.1f} GB / total {total / 1e9:.1f} GB")
Keep a handle to the viewer if you plan to release memory later:
from quantem.widget import load, Show4DSTEM
data = load("scan_001_master.h5")
viewer = Show4DSTEM(data)
viewer
When you are done with that dataset, free the viewer and delete the loaded data:
viewer.free() # releases widget tensor/backend caches
viewer.close() # closes the ipywidget comm/model
del viewer
del data
import gc
import torch
gc.collect()
torch.cuda.empty_cache()
try:
import cupy as cp
except Exception:
pass
else:
cp.get_default_memory_pool().free_all_blocks()
cp.get_default_pinned_memory_pool().free_all_blocks()
If viewer or data still exists anywhere in the notebook, the memory is still
owned by the live Python process. That is correct behavior. A small residual
allocation can remain after cleanup because CUDA keeps a runtime context and
small caches alive until the kernel exits.
NVIDIA GPU cleanup check, using a real Au_TiO2_030_master.h5 4D-STEM scan
loaded as det_bin=4, dtype="u8":
GPU 0: NVIDIA RTX PRO 6000 Blackwell Workstation Edition
before: free 74.12 GiB / total 94.95 GiB
after load: free 71.72 GiB cupy used 2.25 GiB
after Show4DSTEM: free 70.64 GiB torch reserved 0.98 GiB
after cleanup: free 73.74 GiB cupy used 0.00 GiB
residual: 0.38 GiB CUDA runtime/cache overhead
GPU 1: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition
before: free 80.95 GiB / total 94.97 GiB
after load: free 78.68 GiB cupy used 2.25 GiB
after Show4DSTEM: free 77.60 GiB torch reserved 0.98 GiB
after cleanup: free 80.70 GiB cupy used 0.00 GiB
residual: 0.25 GiB CUDA runtime/cache overhead
If memory is still occupied after this pattern, another variable, notebook, or kernel still owns it. Shut down old kernels from JupyterLab before assuming the GPU is stuck.
Moving image data to Torch or CuPy#
Most viewers accept NumPy arrays or quantem datasets directly, so you usually do not need to move a PNG, TIFF, or EMD survey image to Torch just to view it. Move data to the GPU when you are about to run your own GPU computation.
For Torch:
import numpy as np
import torch
from quantem.widget import read_image
ds = read_image("haadf.emd")
image = np.ascontiguousarray(ds.array, dtype=np.float32)
device = "cuda" if torch.cuda.is_available() else "cpu"
image_t = torch.as_tensor(image, device=device)
For a large CPU array that you will reuse many times on an NVIDIA GPU:
if torch.cuda.is_available():
image_t = torch.from_numpy(image).pin_memory().to("cuda", non_blocking=True)
torch.cuda.synchronize()
For CuPy:
import cupy as cp
image_gpu = cp.asarray(image)
Keep raw detector counts as uint16 until you need decimal math. Convert to
float32 for filtering, fitting, normalization, neural networks, or display
processing. Avoid accidental float64; it doubles memory with no benefit for
normal interactive viewing.
For large .npy files, memory-map first so Python does not copy the whole file
before you decide what to view:
import numpy as np
stack = np.load("stack.npy", mmap_mode="r")
preview = np.asarray(stack[::8], dtype=np.float32) # explicit preview reduction
Apple Silicon workflow#
On a MacBook, the same API works:
from quantem.widget import load, Show4DSTEM
data = load("scan_master.h5")
Show4DSTEM(data)
Mac unified memory is shared by the operating system, browser, Python, and GPU. If the machine feels tight, start with:
data = load("scan_master.h5", det_bin=2)
For a small preview or teaching copy:
data = load("scan_master.h5", det_bin=4, dtype="u8")