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