Danh sách bài viết

Bài 14: GPU và CUDA — chuyển tensor giữa CPU / GPU

GPU và CUDA trong PyTorch: vì sao DL cần GPU, kiểm tra torch.cuda.is_available(), torch.device, chuyển tensor và model giữa CPU / GPU. Pattern device-agnostic, tạo tensor trực tiếp trên GPU, tensor + model phải cùng device (RuntimeError nếu mismatch). Mixed precision (FP16 / BF16) với autocast, memory management, xử lý CUDA Out of Memory. MPS cho Apple Silicon, TPU, multi-GPU preview với DataParallel và DistributedDataParallel.

24/05/2026
13 phút đọc
0 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Hiểu vì sao DL (CNN, Transformer, LLM) cần GPU và GPU nhanh hơn CPU bao nhiêu trong phép tensor.
  • Biết CUDA là gì, mối quan hệ PyTorch → CUDA → GPU NVIDIA.
  • Phân biệt consumer GPU (RTX), datacenter (A100, H100), Apple Silicon (MPS), TPU.
  • Viết code device-agnostic: device = torch.device("cuda" if torch.cuda.is_available() else "cpu").
  • Chuyển tensor và model qua lại CPU ↔ GPU, hiểu khác biệt tensor.to() (trả tensor mới) và model.to() (in-place).
  • Dùng mixed precision (autocast + GradScaler) để giảm RAM và tăng tốc.
  • Đọc torch.cuda.memory_allocated, xử lý CUDA Out of Memory (OOM) bằng giảm batch / gradient accumulation / checkpointing / FP16.
  • Biết multi-GPU ở mức preview: DataParallel, DistributedDataParallel, Lightning, Accelerate.
2

Vì sao DL cần GPU

Phép cốt lõi của neural network là matrix multiplication giữa hai tensor lớn. Một forward pass của ResNet-50 cho batch 32 ảnh đã có vài tỷ phép nhân-cộng (FLOPs); một bước train Transformer base với sequence dài 512 lên đến hàng chục tỷ FLOPs.

CPU thiết kế cho ít core mạnh, latency thấp, branch prediction tốt — phù hợp logic phức tạp tuần tự. GPU thiết kế ngược lại: hàng nghìn core đơn giản, throughput cao, song song hóa hàng nghìn phép tính cùng dạng. Matrix multiply là bài toán "embarrassingly parallel" — mỗi phần tử output là một dot product độc lập — đúng "sở trường" của GPU.

Hệ quả thực tế:

  • Train ResNet-50 trên ImageNet với CPU mất nhiều ngày đến vài tuần; trên 1 GPU consumer (RTX 4090) mất nhiều giờ; trên 8x A100 mất khoảng 1 giờ.
  • Train một Transformer base trên CPU gần như không khả thi nếu corpus đủ lớn.
  • LLM (GPT, LLaMA…) cần multi-GPU, thậm chí multi-node — model parameter vượt RAM của một GPU đơn lẻ.

Khoảng cách tốc độ matmul giữa CPU và một GPU consumer rơi vào tầm 10–100x với tensor đủ lớn (xem section 16). Với tensor nhỏ, overhead launch kernel + copy memory có thể khiến GPU chậm hơn CPU — GPU chỉ "đáng" khi workload đủ lớn.

3

CUDA là gì

CUDA = Compute Unified Device Architecture — platform tính toán song song của NVIDIA, ra mắt 2007. CUDA gồm:

  • Driver + runtime cho GPU NVIDIA.
  • Toolkit (compiler nvcc, profiler, libraries).
  • Thư viện chuyên dụng: cuBLAS (BLAS), cuDNN (deep learning primitives), NCCL (multi-GPU collective).

Stack thực tế khi bạn gọi a @ b trên GPU trong PyTorch:

Python (a @ b)
  → PyTorch ATen dispatcher (chọn kernel theo device + dtype)
    → cuBLAS / cuDNN
      → CUDA runtime
        → GPU driver
          → SM (Streaming Multiprocessor) chạy kernel

Bạn không cần viết kernel CUDA tay; PyTorch wrap sẵn. Quan trọng: CUDA chỉ chạy trên GPU NVIDIA. AMD GPU dùng ROCm (có PyTorch ROCm build), Intel GPU dùng XPU backend, Apple Silicon dùng MPS — mỗi backend là một dispatch path riêng trong PyTorch, nhưng API tensor (cho đa số trường hợp) thì giống nhau.

4

Hardware: consumer, datacenter, MPS, TPU

Consumer GPU (NVIDIA, dùng cho học và project cá nhân)

  • RTX 3060 / 4060: 8–12GB VRAM. Đủ chạy hầu hết model dạy học (ResNet, BERT-base inference, một số fine-tune small LLM với LoRA).
  • RTX 4070 / 4080: 12–16GB.
  • RTX 4090: 24GB. Đủ fine-tune model 7B với LoRA / QLoRA, train CNN cỡ trung.

Datacenter GPU (NVIDIA, dùng cho production / large-scale)

  • A100: 40GB hoặc 80GB HBM2e. Standard cho train model lớn 2020–2023.
  • H100: 80GB HBM3, hỗ trợ FP8, Transformer Engine. Standard cho LLM training hiện tại.
  • H200 / B200 (Blackwell): generation mới hơn, RAM lớn hơn.

Apple Silicon — MPS (Metal Performance Shaders)

  • M1 / M2 / M3 / M4 — GPU tích hợp dùng unified memory (CPU + GPU dùng chung RAM).
  • PyTorch hỗ trợ backend "mps" từ v1.12 (2022). Không phải tất cả op đều có MPS kernel — một số op vẫn fallback về CPU.
  • Đủ cho học và prototype; chậm hơn RTX cùng phân khúc giá ở workload heavy.

TPU (Google)

  • ASIC chuyên cho tensor op. Mạnh với TensorFlow / JAX.
  • PyTorch chạy TPU qua PyTorch-XLA (compile graph qua XLA). Code phải viết hơi khác (XLA device, lazy execution).

Free cloud — học không cần mua hardware

  • Google Colab free tier: GPU T4 (16GB), session ngắn, ngắt nhanh khi idle. Pro / Pro+ trả phí có A100, L4.
  • Kaggle Notebooks: P100 (16GB) hoặc T4 x2, 30h/tuần GPU free, có session dài hơn Colab free.
  • Lightning AI Studios, SageMaker Studio Lab: có free tier với GPU.
5

Kiểm tra GPU available

import torch

print(torch.__version__)              # vd: 2.4.0+cu121
print(torch.cuda.is_available())      # True nếu có GPU NVIDIA + CUDA driver
print(torch.cuda.device_count())      # số GPU thấy được
if torch.cuda.is_available():
    print(torch.cuda.get_device_name(0))         # vd: NVIDIA GeForce RTX 4090
    print(torch.cuda.get_device_capability(0))   # vd: (8, 9) — compute capability
    print(torch.cuda.get_device_properties(0))   # name, total_memory, multi_processor_count, ...

Trên Apple Silicon, kiểm tra MPS:

print(torch.backends.mps.is_available())  # True trên M1/M2/M3/M4 với macOS 12.3+
print(torch.backends.mps.is_built())      # True nếu PyTorch build có MPS

is_available() trả False thường vì: chưa cài bản PyTorch + CUDA (CPU-only build), driver NVIDIA chưa cài / cũ, hoặc đang chạy trong container không expose GPU. Vào pytorch.org/get-started để lấy lệnh cài đúng cho hệ CUDA của máy.

6

torch.device

torch.device là object đại diện cho nơi tensor sống. Các giá trị phổ biến:

torch.device("cpu")        # CPU
torch.device("cuda")       # GPU mặc định (cuda:0)
torch.device("cuda:0")     # GPU thứ 0
torch.device("cuda:1")     # GPU thứ 1 (nếu có)
torch.device("mps")        # Apple Silicon GPU

Mỗi tensor có thuộc tính .device:

x = torch.tensor([1.0, 2.0, 3.0])
print(x.device)            # cpu

y = x.to("cuda")
print(y.device)            # cuda:0

Lưu ý: "cuda" luôn map về cuda:0 trừ khi bạn set torch.cuda.set_device(i) hoặc dùng env var CUDA_VISIBLE_DEVICES.

7

Pattern device-agnostic

Code chạy được ở mọi máy — Colab T4, máy local có / không GPU, MacBook M-series — chỉ cần một dòng:

import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

Phiên bản hỗ trợ cả MPS:

if torch.cuda.is_available():
    device = torch.device("cuda")
elif torch.backends.mps.is_available():
    device = torch.device("mps")
else:
    device = torch.device("cpu")

Sau đó mọi nơi cần biết device thì dùng biến device, không hard-code "cuda". Đây là pattern chuẩn trong mọi tutorial / template PyTorch.

8

Move tensor sang GPU

Ba cách phổ biến:

x = torch.randn(3, 4)        # CPU tensor

# 1) .to(device) — recommended, flexible (CPU/CUDA/MPS đều dùng được)
x_gpu = x.to(device)

# 2) .cuda() — shortcut, CHỈ work với CUDA
x_cuda = x.cuda()            # tương đương x.to("cuda")

# 3) .cpu() — quay về CPU
x_back = x_gpu.cpu()

Quan trọng: tensor.to() không in-place — nó trả về tensor mới ở device đích. Phải gán lại:

x.to(device)         # SAI — kết quả bị bỏ
x = x.to(device)     # ĐÚNG

Nếu tensor đã ở đúng device, .to() trả lại chính tensor đó (no-op, không copy) — viết phòng hờ không tốn gì.

to() cũng nhận dtype: x.to(device, dtype=torch.float16) chuyển cả device và dtype trong một call.

9

Tạo tensor trực tiếp trên GPU

Mọi factory function (zeros, ones, randn, arange, empty…) nhận tham số device:

x = torch.zeros(3, 4, device="cuda")
y = torch.randn(3, 4, device=device)
z = torch.arange(10, device=device)

Cách này nhanh hơn tạo trên CPU rồi .to(device) vì tránh được bước copy host → device. Với tensor lớn (vài chục MB trở lên) hoặc trong hot loop, khác biệt rõ rệt.

Reuse device từ tensor / model có sẵn:

noise = torch.randn_like(x)              # cùng shape, dtype, device với x
mask  = torch.zeros(x.size(0), device=x.device)
10

Tensor + model phải cùng device

Đây là bug phổ biến nhất khi mới làm việc với GPU. Mọi tensor tham gia một phép tính phải ở cùng device (và thường cùng dtype). Nếu mismatch:

RuntimeError: Expected all tensors to be on the same device,
but found at least two devices, cuda:0 and cpu!

Tình huống điển hình: model đã .to("cuda") nhưng input batch quên move:

model  = MyModel().to(device)
inputs = torch.randn(32, 784)              # vẫn ở CPU
out = model(inputs)                        # RuntimeError

Fix:

for inputs, targets in dataloader:
    inputs  = inputs.to(device)            # bắt buộc gán lại
    targets = targets.to(device)
    out  = model(inputs)
    loss = loss_fn(out, targets)
    ...

Khác biệt then chốt cần nhớ:

  • model.to(device)in-place: move toàn bộ parameter và buffer của module. Không cần gán lại, nhưng viết model = model.to(device) cũng đúng.
  • tensor.to(device)không in-place: trả tensor mới, phải gán lại.

Lý do: nn.Module.to() override để di chuyển param và đăng ký device cho module; còn Tensor.to() giữ semantic functional (như NumPy).

11

Move model

import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        return self.fc2(torch.relu(self.fc1(x)))

model = MyModel().to(device)        # move parameter sang device
print(next(model.parameters()).device)

Lưu checkpoint thì save từ CPU để file portable:

torch.save(model.state_dict(), "model.pt")

# Load: tạo model rỗng, load state, rồi mới to(device)
model = MyModel()
model.load_state_dict(torch.load("model.pt", map_location="cpu"))
model.to(device)

Tham số map_location trong torch.load quan trọng: nếu checkpoint lưu trên cuda:0 và bạn load trên máy CPU-only, không có map_location="cpu" sẽ lỗi.

12

Mixed precision (FP16 / BF16)

Tensor mặc định float32 (FP32, 4 byte / phần tử). GPU hiện đại (Tensor Core từ Volta trở đi) chạy FP16 / BF16 nhanh hơn nhiều, RAM giảm gần một nửa.

  • FP16: 16 bit, range hẹp — dễ overflow / underflow trong loss và gradient.
  • BF16 (bfloat16): 16 bit, cùng range với FP32 (8 bit exponent), precision thấp hơn FP16 nhưng stable hơn khi train. Yêu cầu Ampere (A100, RTX 30xx) trở lên.

Pattern chuẩn: autocast cho forward, GradScaler cho backward (chỉ cần cho FP16, BF16 không cần):

from torch.amp import autocast, GradScaler

scaler = GradScaler("cuda")     # PyTorch 2.0+ API

for inputs, targets in dataloader:
    inputs  = inputs.to(device)
    targets = targets.to(device)
    optimizer.zero_grad()

    # autocast: ops chạy FP16/BF16 khi an toàn, FP32 cho ops cần precision
    with autocast(device_type="cuda", dtype=torch.float16):
        out  = model(inputs)
        loss = loss_fn(out, targets)

    # GradScaler: scale loss để gradient FP16 không bị underflow
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

API cũ torch.cuda.amp.autocast() vẫn chạy nhưng đã deprecated; PyTorch 2.0+ thống nhất sang torch.amp.autocast(device_type=...) để hỗ trợ CPU autocast, MPS, v.v.

Với BF16, bỏ GradScaler:

with autocast(device_type="cuda", dtype=torch.bfloat16):
    out  = model(inputs)
    loss = loss_fn(out, targets)
loss.backward()
optimizer.step()

Tham khảo PyTorch AMP recipes cho chi tiết edge case (gradient clipping, multi-GPU, multiple loss).

13

Memory management

PyTorch dùng caching allocator: khi free một tensor, RAM không trả về hệ điều hành mà giữ trong cache để reuse cho tensor sau (tránh overhead cudaMalloc).

print(torch.cuda.memory_allocated() / 1e9, "GB")   # đang dùng (sống)
print(torch.cuda.memory_reserved()  / 1e9, "GB")   # tổng cache PyTorch giữ
print(torch.cuda.max_memory_allocated() / 1e9, "GB")  # peak từ đầu run

torch.cuda.reset_peak_memory_stats()              # reset peak counter
print(torch.cuda.memory_summary(abbreviated=True))  # bảng chi tiết

Giải phóng cache (KHÔNG giúp giảm RAM của model / tensor đang sống):

del big_tensor              # bỏ reference
torch.cuda.empty_cache()    # trả cache về OS / driver

Hiểu lầm thường gặp: empty_cache() KHÔNG "fix" OOM của model lớn. Nó chỉ trả phần cache chưa được PyTorch tái sử dụng. Nếu model + activation đang chiếm chỗ thật, gọi empty_cache() bao nhiêu lần cũng vô ích.

nvidia-smi báo VRAM bao gồm cả cache PyTorch — đừng panic nếu số cao hơn memory_allocated().

14

CUDA Out of Memory

RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB
(GPU 0; 24.00 GiB total capacity; 22.10 GiB already allocated;
 1.50 GiB free; 22.50 GiB reserved in total by PyTorch)

Nguyên nhân điển hình:

  1. Batch size quá lớn — phổ biến nhất. Activation cho mỗi sample nhân với batch size.
  2. Model quá lớn cho VRAM — tổng parameter + gradient + optimizer state (Adam giữ 2 buffer / param) đã vượt RAM.
  3. Quên detach tensor không cần gradloss_total += loss sẽ giữ cả computation graph qua nhiều iteration; phải loss.item() hoặc loss.detach().
  4. Giữ reference dư — biến trỏ vào tensor trên GPU không bị free.
  5. Memory fragmentation — cache có đủ tổng RAM nhưng không có block liên tục đủ lớn.

Cách giải quyết, từ rẻ đến đắt:

  • Giảm batch size — thử half rồi quarter.
  • Gradient accumulation — chia batch lớn thành nhiều micro-batch, cộng dồn gradient trước khi optimizer.step(). Hiệu quả như batch lớn nhưng RAM bằng micro-batch.
  • Mixed precision (FP16 / BF16) — giảm ~50% RAM cho activation và một phần parameter.
  • Gradient checkpointing (torch.utils.checkpoint) — không lưu activation, tính lại trong backward. Đổi compute lấy RAM.
  • Model parallel / sharding — chia model qua nhiều GPU (FSDP, DeepSpeed ZeRO). Dành cho khi 1 GPU không chứa nổi.
  • Hạ optimizer — Adam → SGD (giảm 2/3 optimizer state), hoặc 8-bit Adam.

Snippet gradient accumulation:

accum_steps = 4
optimizer.zero_grad()
for i, (x, y) in enumerate(loader):
    x, y = x.to(device), y.to(device)
    loss = loss_fn(model(x), y) / accum_steps
    loss.backward()
    if (i + 1) % accum_steps == 0:
        optimizer.step()
        optimizer.zero_grad()
15

NumPy không support GPU

NumPy ndarray sống trên CPU RAM. Không có cách convert tensor CUDA → ndarray trực tiếp:

x = torch.randn(3, 4, device="cuda")

x.numpy()                # TypeError: can't convert cuda:0 device type tensor to numpy
x.cpu().numpy()          # OK — copy về CPU trước

Ngược lại từ ndarray sang tensor GPU phải qua hai bước (PyTorch tự lo):

import numpy as np
arr = np.array([1.0, 2.0, 3.0])
t   = torch.from_numpy(arr).to(device)

Thư viện tương đương NumPy nhưng chạy GPU: CuPy (API gần giống NumPy, chạy CUDA). PyTorch tensor cũng đã đủ dùng cho hầu hết workflow DL.

16

Speed test CPU vs GPU

import time
import torch

N = 10000
a = torch.randn(N, N)
b = torch.randn(N, N)

# CPU
start = time.time()
c = a @ b
print("CPU:", time.time() - start, "s")

# GPU
a = a.cuda()
b = b.cuda()

# Warm-up — lần matmul đầu tiên tốn extra do cuBLAS load + autotune
_ = a @ b
torch.cuda.synchronize()

start = time.time()
c = a @ b
torch.cuda.synchronize()
print("GPU:", time.time() - start, "s")

Vì sao cần torch.cuda.synchronize(): GPU op chạy bất đồng bộ — Python a @ b chỉ enqueue kernel rồi trả về ngay, kernel có thể chưa chạy xong. Không có synchronize() thì time.time() đo thời gian enqueue, không phải thời gian tính. Số đo sẽ vô nghĩa nhỏ.

Quy luật:

  • N = 100: GPU thường chậm hơn CPU (overhead kernel launch).
  • N = 1000: bắt đầu hòa.
  • N = 10000: GPU nhanh hơn 20–100x tùy hardware và dtype.

Đo nghiêm túc dùng torch.utils.benchmark.Timer (handle warm-up, sync, repeat) thay vì time.time().

17

Multi-GPU preview

Đây là preview — đi sâu vào các bài training nâng cao sau.

  • nn.DataParallel (DP): wrapper đơn giản, 1 process điều khiển N GPU. Có bottleneck ở GPU 0 (gather output), không khuyến nghị cho production. PyTorch docs khuyên dùng DDP thay thế.
  • nn.parallel.DistributedDataParallel (DDP): N process, mỗi process 1 GPU; sync gradient qua NCCL. Standard cho multi-GPU và multi-node. Phức tạp hơn (cần torchrun, init process group), nhưng scale gần linear.
  • PyTorch Lightning: wrap training loop, một flag strategy="ddp" là chạy được multi-GPU.
  • Hugging Face Accelerate: vài dòng config CLI (accelerate config) để chạy cùng code trên CPU / 1 GPU / multi-GPU / TPU.
  • FSDP (Fully Sharded Data Parallel) trong PyTorch và DeepSpeed ZeRO: shard parameter + gradient + optimizer state qua nhiều GPU — cho phép train model lớn hơn RAM một GPU.

Skeleton DDP đơn giản:

# train.py — chạy bằng: torchrun --nproc_per_node=2 train.py
import os, torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

dist.init_process_group("nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
device = torch.device("cuda", local_rank)

model = MyModel().to(device)
model = DDP(model, device_ids=[local_rank])
# ... training loop bình thường

dist.destroy_process_group()
18

Code Python — đầy đủ một flow

import time
import torch
import torch.nn as nn

# 1) Chọn device
if torch.cuda.is_available():
    device = torch.device("cuda")
elif torch.backends.mps.is_available():
    device = torch.device("mps")
else:
    device = torch.device("cpu")
print("Using device:", device)

# 2) Tạo tensor trực tiếp trên device
x = torch.randn(1024, 784, device=device)
y = torch.randint(0, 10, (1024,), device=device)

# 3) Model trên device
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(784, 256), nn.ReLU(),
            nn.Linear(256, 10),
        )
    def forward(self, x):
        return self.net(x)

model = MLP().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn   = nn.CrossEntropyLoss()

# 4) Một bước train ngắn
out  = model(x)
loss = loss_fn(out, y)
loss.backward()
optimizer.step()
print("Loss:", loss.item())

# 5) Speed test matmul (chỉ chạy nếu CUDA)
if device.type == "cuda":
    N = 5000
    a = torch.randn(N, N, device=device)
    b = torch.randn(N, N, device=device)

    # warm-up
    _ = a @ b
    torch.cuda.synchronize()

    start = time.time()
    for _ in range(5):
        _ = a @ b
    torch.cuda.synchronize()
    gpu_t = (time.time() - start) / 5
    print(f"GPU matmul {N}x{N}: {gpu_t*1000:.2f} ms")

    a_cpu, b_cpu = a.cpu(), b.cpu()
    start = time.time()
    _ = a_cpu @ b_cpu
    cpu_t = time.time() - start
    print(f"CPU matmul {N}x{N}: {cpu_t*1000:.2f} ms")
    print(f"Speedup: {cpu_t / gpu_t:.1f}x")

# 6) Memory snapshot (chỉ CUDA)
if device.type == "cuda":
    print(f"Allocated: {torch.cuda.memory_allocated()/1e9:.2f} GB")
    print(f"Reserved : {torch.cuda.memory_reserved()/1e9:.2f} GB")

# 7) Demo OOM-safe pattern: try/except, dọn rồi giảm batch
def train_step(batch):
    try:
        out  = model(batch.to(device))
        loss = loss_fn(out, torch.zeros(batch.size(0), dtype=torch.long, device=device))
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        return loss.item()
    except torch.cuda.OutOfMemoryError:
        optimizer.zero_grad()
        torch.cuda.empty_cache()
        raise   # caller giảm batch và retry
19

Bài tập

  1. Mở Google Colab, chọn Runtime → Change runtime type → T4 GPU. In torch.cuda.is_available(), torch.cuda.get_device_name(0), torch.cuda.get_device_properties(0).
  2. Speed test matmul 5000×5000 CPU vs GPU. Nhớ torch.cuda.synchronize() trước và sau khi đo. So sánh speedup; thử thêm N=500 và quan sát: với N nhỏ, GPU có thể chậm hơn — giải thích.
  3. Tạo tensor x = torch.arange(10, device="cpu"), move sang GPU bằng .to(device), thực hiện x = x * 2 + 1, move về CPU, verify giá trị bằng print hoặc so sánh với tính tay.
  4. Tự kích hoạt CUDA Out of Memory: tạo tensor lớn lặp lại trong loop (vd tensors.append(torch.randn(10000, 10000, device="cuda"))) cho đến khi torch.cuda.OutOfMemoryError. In memory_allocated trước lúc lỗi. Sau đó dọn (del tensors, torch.cuda.empty_cache()) và xác nhận RAM giảm.
  5. Viết một training loop ngắn với autocast(device_type="cuda", dtype=torch.float16) + GradScaler cho MLP trên dữ liệu giả. So sánh memory_allocated peak với bản FP32.
Gợi ý câu 2 — vì sao GPU chậm với N nhỏ

Mỗi kernel CUDA có overhead launch cố định (~vài μs đến vài chục μs) và overhead copy / dispatch trong PyTorch. Với N=500, matmul thực chỉ tốn vài μs trên GPU — overhead launch ngang bằng hoặc lớn hơn phần tính. CPU làm thẳng trong cache L1/L2 không có overhead này. GPU chỉ "thắng" rõ rệt khi compute đủ lớn để che overhead.

20

Tóm tắt

  • GPU song song hóa matrix multiplication — train DL nhanh hơn CPU 10–100x với workload đủ lớn. LLM bắt buộc multi-GPU.
  • CUDA là stack tính toán song song của NVIDIA; PyTorch dispatch qua cuBLAS / cuDNN xuống CUDA kernel. AMD dùng ROCm, Apple dùng MPS, Google dùng TPU (qua XLA).
  • Kiểm tra GPU: torch.cuda.is_available(), torch.cuda.device_count(), torch.cuda.get_device_name().
  • Pattern device-agnostic: device = torch.device("cuda" if torch.cuda.is_available() else "cpu").
  • Move tensor: x = x.to(device) (phải gán lại). Move model: model.to(device) (in-place). Tạo trực tiếp trên GPU: torch.zeros(..., device=device) — nhanh hơn tạo CPU rồi .to().
  • Tensor + model phải cùng device — mismatch raise RuntimeError. Trong train loop, luôn inputs = inputs.to(device), targets = targets.to(device).
  • Mixed precision với torch.amp.autocast(device_type="cuda", dtype=torch.float16/bfloat16); FP16 kèm GradScaler, BF16 không cần.
  • Memory: memory_allocated (sống), memory_reserved (cache). empty_cache() trả cache, KHÔNG giảm RAM của tensor đang sống.
  • CUDA OOM fix theo thứ tự: giảm batch → gradient accumulation → mixed precision → gradient checkpointing → model sharding (FSDP / DeepSpeed).
  • NumPy chỉ ở CPU — phải .cpu().numpy() để convert tensor CUDA.
  • Speed test GPU phải torch.cuda.synchronize() trước và sau khi đo, vì kernel async.
  • Multi-GPU production dùng DDP (qua torchrun) hoặc wrapper như Lightning / Accelerate. DataParallel đơn giản nhưng có bottleneck.