File size: 6,970 Bytes
5337457 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | # file: memory_utils.py
"""Memory management utilities for NVIDIA RTX 4050 (6GB VRAM)."""
from __future__ import annotations
import gc
import logging
from typing import Any, Callable, Dict, Tuple
import torch
import torch.nn as nn
from torch.optim import Optimizer
logger = logging.getLogger(__name__)
# ===================================================================
# MemoryManager
# ===================================================================
class MemoryManager:
"""CUDA memory management for constrained VRAM (6GB)."""
# ------------------------------------------------------------------
# cache clearing
# ------------------------------------------------------------------
@staticmethod
def clear_memory() -> None:
"""Clear CUDA cache and run garbage collection."""
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
gc.collect()
# ------------------------------------------------------------------
# diagnostics
# ------------------------------------------------------------------
@staticmethod
def get_memory_usage() -> Dict[str, Any]:
"""Return CUDA memory stats."""
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated() / 1024**2
reserved = torch.cuda.memory_reserved() / 1024**2
max_allocated = torch.cuda.max_memory_allocated() / 1024**2
return {
"allocated_mb": round(allocated, 1),
"reserved_mb": round(reserved, 1),
"max_allocated_mb": round(max_allocated, 1),
"device": torch.cuda.get_device_name(0),
}
return {"status": "cuda_unavailable"}
# ------------------------------------------------------------------
# gradient checkpointing
# ------------------------------------------------------------------
@staticmethod
def enable_gradient_checkpointing(model: nn.Module) -> nn.Module:
"""Model already uses checkpoint in forward — no-op here."""
return model
# ------------------------------------------------------------------
# mixed precision
# ------------------------------------------------------------------
@staticmethod
def enable_mixed_precision() -> torch.amp.autocast:
"""Return bfloat16 autocast for RTX 40-series (native bfloat16 support)."""
return torch.autocast(device_type='cuda', dtype=torch.bfloat16)
# ------------------------------------------------------------------
# model memory estimation
# ------------------------------------------------------------------
@staticmethod
def estimate_model_memory(model: nn.Module) -> float:
"""Estimate model memory in MiB (params only, bfloat16)."""
total = sum(p.numel() * 2 for p in model.parameters()) # bfloat16 = 2 bytes
total += sum(p.numel() * 2 for p in model.buffers())
return total / 1024**2
# ===================================================================
# Model optimisation helper
# ===================================================================
def optimize_for_rtx4050(model: nn.Module, _config: Any = None) -> nn.Module:
"""Apply RTX 40-series optimizations.
- torch.compile for Ada Lovelace speedup
- Log model size and VRAM estimate
- Verify forward pass fits in 6GB
Note: torch.compile won't compile on first call (requires CUDA warmup).
If it fails, falls back to eager mode gracefully.
"""
param_count = sum(p.numel() for p in model.parameters())
logger.info("Model params: %.2fM", param_count / 1e6)
mem_mb = MemoryManager.estimate_model_memory(model)
logger.info("Model memory (bf16): %.1f MiB", mem_mb)
if torch.cuda.is_available():
free, total = torch.cuda.mem_get_info()
logger.info("VRAM: %.1f GiB free / %.1f GiB total", free / 1024**3, total / 1024**3)
if mem_mb > free / 1024**2 * 0.8:
logger.warning("Model may exceed 80%% of free VRAM — consider reducing batch size")
# torch.compile handled by model's create_small_model factory
return model
def verify_cuda_compatibility(
model: nn.Module,
batch_size: int = 2,
seq_len: int = 128,
vocab_size: int = 5000,
) -> bool:
"""Run dummy forward+backward to verify CUDA compatibility and VRAM fit.
Returns True if the model fits and gradients flow correctly.
"""
if not torch.cuda.is_available():
logger.warning("CUDA not available — skipping verification")
return True
try:
model = model.cuda()
model.train()
x = torch.randint(0, vocab_size, (batch_size, seq_len), device='cuda')
with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
logits = model(x)
loss = logits.mean()
loss.backward()
MemoryManager.clear_memory()
logger.info("CUDA verification passed — forward+backward OK")
return True
except torch.cuda.OutOfMemoryError:
logger.error("OOM during verification — reduce batch size or model size")
return False
except Exception as e:
logger.error("CUDA verification failed: %s", e)
return False
# ===================================================================
# Logging helper
# ===================================================================
def log_memory_usage(logger_instance: logging.Logger) -> None:
"""Log current CUDA memory usage."""
if torch.cuda.is_available():
stats = MemoryManager.get_memory_usage()
logger_instance.debug(
"VRAM: allocated=%(allocated_mb).1fMB reserved=%(reserved_mb).1fMB max=%(max_allocated_mb).1fMB",
stats,
)
# ===================================================================
# Gradient accumulation
# ===================================================================
def create_gradient_accumulator(
accumulation_steps: int,
) -> Tuple[Callable[[Optimizer], None], Callable[[int], bool]]:
"""Return (step_fn, should_step_fn) for gradient accumulation.
Usage::
step_fn, should_step = create_gradient_accumulator(4)
for i, (x, y) in enumerate(dataloader):
loss = model(x, y)
loss.backward()
if should_step(i):
step_fn(optimizer) # optimizer.step() + zero_grad()
"""
def step_fn(optimizer: Optimizer) -> None:
optimizer.step()
optimizer.zero_grad()
MemoryManager.clear_memory()
def should_step_fn(batch_idx: int) -> bool:
return (batch_idx + 1) % accumulation_steps == 0
return step_fn, should_step_fn
# ===================================================================
# Backward compat aliases
# ===================================================================
optimize_for_m2 = optimize_for_rtx4050
|