์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

FastAPI์™€ PyTorch ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ๋น„๋™๊ธฐ AI ์„œ๋น™ ํ™˜๊ฒฝ์—์„œ ๋™์  ๋ฐ”์นญ ์‹œ ๋ฐœ์ƒํ•˜๋Š” VRAM ๋ˆ„์ˆ˜ ๋ฐ OOM ์žฅ์•  ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 8. 26. 01:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article provides an in-depth technical guide to diagnosing and resolving GPU VRAM memory leaks and Out-Of-Memory (OOM) errors during real-time asynchronous AI model serving using FastAPI and PyTorch with Dynamic Batching. It explores the interaction between Python's asyncio event loop, PyTorch's CUDA Caching Allocator, and reference counting mechanisms. Readers will learn actionable strategies, including custom memory profiling, torch.inference_mode() application, context manager optimization, and robust background worker architectural patterns to maintain high-throughput production stability.

FastAPI์™€ PyTorch๋ฅผ ๊ฒฐํ•ฉํ•œ ์‹ค์‹œ๊ฐ„(Real-time) ๋น„๋™๊ธฐ(Asynchronous) AI ๋ชจ๋ธ ์„œ๋น™(Model Serving) ์•„ํ‚คํ…์ฒ˜๋Š” ๊ณ ์„ฑ๋Šฅ ์›น ์„œ๋น„์Šค ๊ตฌํ˜„ ์‹œ ๋„๋ฆฌ ์ฑ„ํƒ๋˜๋Š” ์กฐํ•ฉ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ํŠธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ๋Ÿ‰(Throughput)์„ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ์š”์ฒญ์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด GPU์— ์ „์†กํ•˜๋Š” ๋™์  ๋ฐ”์นญ(Dynamic Batching) ๊ธฐ๋ฒ•์€ ํ•„์ˆ˜์ ์ธ ์ตœ์ ํ™” ์š”์†Œ๋กœ ์ž๋ฆฌ ์žก์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์„œ๋น„์Šค ์šด์˜ ์ค‘ ํŠน์ • ์‹œ์ ์— VRAM(Video Random Access Memory) ์‚ฌ์šฉ๋Ÿ‰์ด ๊พธ์ค€ํžˆ ์ฆ๊ฐ€ํ•˜๋‹ค ๊ฒฐ๊ตญ OOM(Out Of Memory) ์žฅ์• ๋ฅผ ์ผ์œผํ‚ค๋Š” ํ˜„์ƒ์„ ์ž์ฃผ ๋ชฉ๊ฒฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘ ์‹œ์ ์—๋Š” ์ •์ƒ ๋™์ž‘ํ•˜๋‹ค๊ฐ€ ๊ณ ํ•˜์ค‘ ์ƒํƒœ์—์„œ ์ ์ง„์ ์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ฆ๋ฐœํ•˜๋Š” ์ด ๋ฌธ์ œ๋Š” ์›์ธ ์ถ”์ ์ด ๋งค์šฐ ๊นŒ๋‹ค๋กญ์Šต๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” ์ด ๋ฌธ์ œ์˜ ๊ทผ๋ณธ์ ์ธ ์›์ธ์„ PyTorch ๋‚ด๋ถ€ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๋ฐ Python ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ๋ฃจํ”„(Event Loop) ์ˆ˜์ค€์—์„œ ๋ถ„์„ํ•˜๊ณ , ์™„๋ฒฝํ•œ ํ•ด๊ฒฐ์ฑ…๊ณผ ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ „๋žต์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.



1. ๋™์  ๋ฐ”์นญ๊ณผ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ๋ฃจํ”„์—์„œ์˜ ๋ฌธ์ œ ๋ฐœ์ƒ ์›์ธ

FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ `asyncio.Queue` ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์  ๋ฐ”์นญ์„ ๊ตฌํ˜„ํ•  ๋•Œ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์›์ธ์€ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ๋ณตํ•ฉ์ ์œผ๋กœ ์ž‘์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

1.1 ์—ฐ์‚ฐ ๊ทธ๋ž˜ํ”„(Computation Graph)์˜ ๋ฏธ์„ธํ•œ ์ฐธ์กฐ ์œ ์ง€

PyTorch๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๋ฏธ๋ถ„ ๊ณ„์ˆ˜ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์—ฐ์‚ฐ ๊ทธ๋ž˜ํ”„(Computation Graph)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ถ”๋ก (Inference) ๋‹จ๊ณ„์—์„œ `@torch.no_grad()` ๋˜๋Š” `@torch.inference_mode()`๋ฅผ ์—„๊ฒฉํ•˜๊ฒŒ ์ ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด, ํ…์„œ(Tensor)๊ฐ€ ๊ทธ๋ž˜ํ”„ ์ฐธ์กฐ๋ฅผ ๊ณ„์† ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋™์  ๋ฐ”์นญ ๊ตฌํ˜„๋ถ€์—์„œ ๋น„๋™๊ธฐ ํ“จ์ฒ˜(Future) ๊ฐ์ฒด์— ํ…์„œ ์ถœ๋ ฅ์„ ์ง์ ‘ ์ „๋‹ฌํ•  ๋•Œ ์ด ์ฐธ์กฐ๊ฐ€ ์ œ๊ฑฐ๋˜์ง€ ์•Š๊ณ  ์ด๋ฒคํŠธ ๋ฃจํ”„ ๋ฉ”๋ชจ๋ฆฌ์— ์ƒ์ฃผํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

1.2 PyTorch CUDA Caching Allocator์˜ ๋ฉ”๋ชจ๋ฆฌ ํŒŒํŽธํ™”(Memory Fragmentation)

PyTorch๋Š” GPU ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น ์†๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด ์ž์ฒด์ ์ธ **CUDA ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ ํ• ๋‹น์ž(CUDA Caching Allocator)**๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. `torch.cuda.memory_allocated()`๋กœ ์ธก์ •๋˜๋Š” ์‹ค์ œ ํ…์„œ ์ ์œ ๋Ÿ‰์€ ์ค„์–ด๋“ค๋”๋ผ๋„, `torch.cuda.memory_reserved()`๋กœ ํ‘œ์‹œ๋˜๋Š” PyTorch ์˜ˆ์•ฝ ๋ฉ”๋ชจ๋ฆฌ๋Š” ์šด์˜์ฒด์ œ(NVIDIA Driver)๋กœ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋™์  ๋ฐ”์นญ์œผ๋กœ ์ธํ•ด ๊ฐ€๋ณ€์ ์ธ ํฌ๊ธฐ์˜ ํ…์„œ๊ฐ€ ์—ฐ์†ํ•˜์—ฌ ํ• ๋‹น ๋ฐ ํ•ด์ œ๋  ๋•Œ ๋ฉ”๋ชจ๋ฆฌ ํŒŒํŽธํ™”(Memory Fragmentation)๊ฐ€ ์‹ฌํ™”๋˜์–ด ์‹ค์ œ ์‚ฌ์šฉ๋Ÿ‰๋ณด๋‹ค ๋” ์ผ์ฐ OOM์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

1.3 Python Garbage Collection๊ณผ ๋น„๋™๊ธฐ Task ๊ฐ„ ์ˆœํ™˜ ์ฐธ์กฐ

๋น„๋™๊ธฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ(Background Task)๊ฐ€ ๋ฐ”์น˜ ๋‹จ์œ„๋กœ ๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ํŒ(Pop)ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ๋•Œ, ์˜ˆ์™ธ(Exception) ์ฒ˜๋ฆฌ ๊ตฌ๋ฌธ์ด๋‚˜ ๋กœ๊น… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‚ด๋ถ€์—์„œ ํ…์„œ ์ฐธ์กฐ๋ฅผ `sys.exc_info()` ๋˜๋Š” ์บก์ฒ˜๋œ ์Šคํƒ ํ”„๋ ˆ์ž„์— ๋‚จ๊ฒจ๋‘๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. Python์˜ ์„ธ๋Œ€๋ณ„ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰ํ„ฐ(Garbage Collector)๊ฐ€ ์ด๋ฅผ ์ฆ‰์‹œ ์ •๋ฅ  ํ•ด์ œํ•˜์ง€ ๋ชปํ•˜๋ฉด GPU VRAM ์ฐธ์กฐ ์—ญ์‹œ ํ•จ๊ป˜ ๋ฌถ์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

2. VRAM ๋ˆ„์ˆ˜ ์ง„๋‹จ ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

์žฅ์•  ์›์ธ์„ ๊ทœ๋ช…ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹จ์ˆœ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๋ฒ•์„ ๋„˜์–ด PyTorch ๋‚ด๋ถ€ ๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ๋ฅผ ์ •๋ฐ€ํ•˜๊ฒŒ ์ถ”์ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2.1 Memory Snapshot์„ ํ™œ์šฉํ•œ ์ •๋ฐ€ ๋ถ„์„

PyTorch 2.0 ์ด์ƒ์—์„œ๋Š” VRAM ํ• ๋‹น ์Šค๋ƒ…์ƒท์„ HTML ํ˜•ํƒœ๋กœ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ํ”„๋กœํŒŒ์ผ๋ง ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„์ˆ˜๊ฐ€ ์˜์‹ฌ๋˜๋Š” ์‹œ์ ์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ…์„œ ํ• ๋‹น ๊ถค์ ์„ ์บก์ฒ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import torch

# ์Šค๋ƒ…์ƒท ๊ธฐ๋ก ์‹œ์ž‘
torch.cuda.memory._record_memory_history(max_entries=100000)

# ๋ฐ”์นญ ๋ฐ ์ถ”๋ก  ๋กœ์ง ์‹คํ–‰ ํ›„ ์Šค๋ƒ…์ƒท ์ €์žฅ
try:
    # ์˜ˆ: OOM ๋ฐœ์ƒ ์‹œ์  ๋˜๋Š” ํŠน์ • ์ฃผ๊ธฐ
    torch.cuda.memory._dump_snapshot("vram_leak_snapshot.pickle")
except Exception as e:
    print(f"Failed to dump snapshot: {e}")

์ƒ์„ฑ๋œ `.pickle` ํŒŒ์ผ์€ PyTorch ๊ณต์‹ Visualizer ์›นํŽ˜์ด์ง€(`pytorch.org/memory_viz`)์— ์—…๋กœ๋“œํ•˜์—ฌ ์–ด๋–ค ๋ ˆ์ด์–ด์™€ ์–ด๋–ค ๋ณ€์ˆ˜๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ  ์žˆ๋Š”์ง€ ์ •ํ™•ํžˆ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2.2 Allocated Memory vs Reserved Memory ์‹ค์‹œ๊ฐ„ ๋กœ๊น…

FastAPI ๋ฏธ๋“ค์›จ์–ด ๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด ํ—ฌ์Šค์ฒดํฌ ์—”๋“œํฌ์ธํŠธ์— ๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ ๋กœ๊น…์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํŒŒํŽธํ™” ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

from fastapi import FastAPI
import torch

app = FastAPI()

@app.get("/health/memory")
async def get_cuda_memory_status():
    if not torch.cuda.is_available():
        return {"cuda": False}
    
    allocated = torch.cuda.memory_allocated() / (1024 ** 2)  # MB ๋‹จ์œ„
    reserved = torch.cuda.memory_reserved() / (1024 ** 2)    # MB ๋‹จ์œ„
    max_allocated = torch.cuda.max_memory_allocated() / (1024 ** 2)
    
    return {
        "allocated_mb": round(allocated, 2),
        "reserved_mb": round(reserved, 2),
        "max_allocated_mb": round(max_allocated, 2),
        "fragmentation_ratio": round((reserved - allocated) / reserved, 2) if reserved > 0 else 0
    }


3. ๋™์  ๋ฐ”์นญ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐํ•จ ์ˆ˜์ • ๋ฐ ์ตœ์ ํ™” ์†”๋ฃจ์…˜

๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด FastAPI ๋น„๋™๊ธฐ ํ ์ฒ˜๋ฆฌ ๋กœ์ง๊ณผ PyTorch ์ถ”๋ก  ๋ฃจํ”„๋ฅผ ์•„๋ž˜ ํ‘œ์ค€ ํŒจํ„ด์œผ๋กœ ๊ฐœํŽธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

3.1 ์™„๋ฒฝํ•œ Inference Mode ๋ฐ Tensor Detach ์ฒ˜๋ฆฌ

์ถ”๋ก  ์ฝ”๋“œ ์ „๋ฐ˜์— `torch.inference_mode()`๋ฅผ ์ ์šฉํ•˜๊ณ , ํ…์„œ ๊ฒฐ๊ณผ๋ฅผ CPU๋กœ ์ด๊ด€ ์‹œ ๋ฐ˜๋“œ์‹œ `.detach().cpu()`๋ฅผ ์ ์šฉํ•˜์—ฌ ์—ฐ์‚ฐ ๊ทธ๋ž˜ํ”„์™€์˜ ์—ฐ๊ฒฐ์„ ์™„์ „ํžˆ ๋Š์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

import asyncio
import torch
from typing import List, Dict, Any

class AsyncBatchPredictor:
    def __init__(self, model: torch.nn.Module, max_batch_size: int = 16, max_delay: float = 0.05):
        self.model = model
        self.max_batch_size = max_batch_size
        self.max_delay = max_delay
        self.queue = asyncio.Queue()
        asyncio.create_task(self._batch_worker())

    async def predict(self, input_tensor: torch.Tensor) -> Any:
        loop = asyncio.get_running_loop()
        future = loop.create_future()
        await self.queue.put((input_tensor, future))
        return await future

    async def _batch_worker(self):
        while True:
            batch = []
            futures = []
            
            # ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ ๋Œ€๊ธฐ
            tensor, fut = await self.queue.get()
            batch.append(tensor)
            futures.append(fut)
            
            # Max Delay ๋™์•ˆ ์ถ”๊ฐ€ ์š”์ฒญ ์ˆ˜์ง‘ (Dynamic Batching)
            start_time = asyncio.get_event_loop().time()
            while len(batch) < self.max_batch_size:
                timeout = self.max_delay - (asyncio.get_event_loop().time() - start_time)
                if timeout <= 0:
                    break
                try:
                    tensor, fut = await asyncio.wait_for(self.queue.get(), timeout=timeout)
                    batch.append(tensor)
                    futures.append(fut)
                except asyncio.TimeoutError:
                    break

            # ์ถ”๋ก  ์ˆ˜ํ–‰
            await self._run_inference(batch, futures)

    async def _run_inference(self, batch: List[torch.Tensor], futures: List[asyncio.Future]):
        try:
            # 1. Tensor Stack ๋ฐ GPU ์ „์†ก
            input_batch = torch.stack(batch).cuda(non_blocking=True)
            
            # 2. Inference Mode ์ ์šฉ (no_grad ๋Œ€๋น„ ์—ฐ์‚ฐ overhead ๋‚ฎ๊ณ  ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „)
            with torch.inference_mode():
                outputs = self.model(input_batch)
                # CPU ์ด๊ด€ ๋ฐ numpy/python ๊ฐ์ฒด ๋ณ€ํ™˜์„ ํ†ตํ•ด GPU ์ฐธ์กฐ ์™„์ „ ์ œ๊ฑฐ
                outputs_cpu = outputs.detach().cpu()

            # 3. ๊ฐœ๋ณ„ Future ๊ฒฐ๊ณผ ํ• ๋‹น
            for i, fut in enumerate(futures):
                if not fut.cancelled():
                    fut.set_result(outputs_cpu[i])
                    
        except Exception as e:
            for fut in futures:
                if not fut.cancelled():
                    fut.set_exception(e)
        finally:
            # 4. ๋ช…์‹œ์  ๋ณ€์ˆ˜ ์ •๋ฆฌ (GC ์œ ๋„ ๋ฐ CUDA ์บ์‹œ ํŒŒํŽธํ™” ๋ฐฉ์ง€)
            del batch, input_batch, outputs, outputs_cpu
            # ํ•„์š”์‹œ ์ฃผ๊ธฐ์ ์ธ gc.collect() ์ˆ˜ํ–‰

3.2 PyTorch Memory Allocator ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ตœ์ ํ™”

๋™์  ๋ฐ”์นญ์ฒ˜๋Ÿผ ๊ฐ€๋ณ€์ ์ธ ํฌ๊ธฐ์˜ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต ํ• ๋‹น๋  ๋•Œ๋Š” PyTorch์˜ ์บ์‹œ ํ• ๋‹น ์ •์ฑ…์„ ์กฐ์ •ํ•˜์—ฌ ํŒŒํŽธํ™”๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ „ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • `PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128`: ํฐ ๋ฉ”๋ชจ๋ฆฌ ๋ธ”๋ก์ด ํŒŒํŽธํ™”๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ถ„ํ•  ๊ฐ€๋Šฅํ•œ ์ตœ๋Œ€ ๋ธ”๋ก ํฌ๊ธฐ๋ฅผ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. ์ด ์„ค์ •์„ ์ ์šฉํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ์žฌ์‚ฌ์šฉ ํšจ์œจ์ด ๊ธ‰๊ฒฉํžˆ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` (PyTorch 2.1 ์ด์ƒ): CUDA Virtual Memory Management๋ฅผ ํ™œ์šฉํ•ด ๋ฉ”๋ชจ๋ฆฌ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๊ฐ€๋ณ€์ ์œผ๋กœ ํ™•์žฅํ•จ์œผ๋กœ์จ ๋ฉ”๋ชจ๋ฆฌ ํŒŒํŽธํ™”๋ฅผ ๊ฑฐ์˜ ์™„๋ฒฝํ•˜๊ฒŒ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

3.3 ์ฃผ๊ธฐ์ ์ธ CUDA Cache Clear์˜ ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ๋ฒ•

`torch.cuda.empty_cache()`๋Š” VRAM์„ ๊ฐ•์ œ๋กœ OS์— ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ํ˜ธ์ถœ ์ž์ฒด๊ฐ€ GPU ๋™๊ธฐํ™”(Synchronization)๋ฅผ ์œ ๋ฐœํ•˜์—ฌ ์ปค๋„ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ๋Œ€ํญ ์ €ํ•˜์‹œํ‚ต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ชจ๋“  ์ถ”๋ก  ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰ํ•˜๋ฉด ์•ˆ ๋˜๋ฉฐ, ํŠน์ • ์กฐ๊ฑด(์˜ˆ: ์บ์‹œ ํŒŒํŽธํ™”์œจ์ด ์ผ์ • ์ˆ˜์ค€ ์ด์ƒ์ด๊ฑฐ๋‚˜ VRAM ์‚ฌ์šฉ๋Ÿ‰์ด 85%๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ) ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ œํ•œ์ ์œผ๋กœ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

# ๋งค ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰ ๊ธˆ์ง€ (Overhead ๊ทน์‹ฌ)
# ํŒŒํŽธํ™” ์ง€ํ‘œ๊ฐ€ ํŠน์ • ์ž„๊ณ„์น˜๋ฅผ ๋„˜์„ ๋•Œ๋งŒ ์„ ํƒ์  ํ˜ธ์ถœ
def smart_empty_cache():
    allocated = torch.cuda.memory_allocated()
    reserved = torch.cuda.memory_reserved()
    if reserved > 0 and (reserved - allocated) / reserved > 0.35: # ํŒŒํŽธํ™”์œจ 35% ์ดˆ๊ณผ ์‹œ
        torch.cuda.empty_cache()

4. ํ”„๋กœ๋•์…˜ ๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค ์„œ๋น™ ์•„ํ‚คํ…์ฒ˜ ๊ณ ๋ ค์‚ฌํ•ญ

FastAPI ๋ฉ”์ธ ๋‹จ์ผ ํ”„๋กœ์„ธ์Šค ๋‚ด๋ถ€์—์„œ GPU ์—ฐ์‚ฐ์„ ์ง์ ‘ ์ˆ˜ํ–‰ํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜๋Š” Python์˜ **GIL(Global Interpreter Lock)** ๋ฐ ์ด๋ฒคํŠธ ๋ฃจํ”„ ๋ธ”๋กœํ‚น ์œ„ํ—˜์ด ํ•ญ์ƒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์šด์˜ ํ™˜๊ฒฝ(Production)์—์„œ๋Š” ๋”์šฑ ๊ฐ•๋ ฅํ•œ ์•„ํ‚คํ…์ฒ˜ ๋ถ„๋ฆฌ๊ฐ€ ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

  • Process Isolation (IPC ํ†ต์‹ ): FastAPI ์›น ์„œ๋ฒ„ ํ”„๋กœ์„ธ์Šค์™€ PyTorch ์ถ”๋ก  ์ „์šฉ ์›Œ์ปค ํ”„๋กœ์„ธ์Šค๋ฅผ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•˜๊ณ  `SharedMemory` ๋˜๋Š” `gRPC`๋ฅผ ํ†ตํ•ด ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์ถ”๋ก  ์›Œ์ปค ํ”„๋กœ์„ธ์Šค์— ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ํŠน์ • ์‹œ์ ์— ์›Œ์ปค๋งŒ Graceful Restartํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Triton Inference Server ๋„์ž…: ๋™์  ๋ฐ”์นญ ๋ฐ GPU ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ Python ๋ ˆ์ด์–ด์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋Œ€์‹ , NVIDIA Triton Inference Server๋‚˜ vLLM ๋“ฑ์˜ C++ ๊ธฐ๋ฐ˜ ์ „์šฉ ์„œ๋น™ ์—”์ง„์œผ๋กœ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค. FastAPI๋Š” ์ˆœ์ˆ˜ ๊ฒŒ์ดํŠธ์›จ์ด(API Gateway) ์—ญํ• ๋งŒ ๋‹ด๋‹นํ•จ์œผ๋กœ์จ VRAM ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ๊ฒฉ๋ฆฌ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


5. ๊ฒฐ๋ก  ๋ฐ ์š”์•ฝ

FastAPI์™€ PyTorch ๊ธฐ๋ฐ˜ ๋™์  ๋ฐ”์นญ ์‹œ์Šคํ…œ์—์„œ VRAM ๋ˆ„์ˆ˜ ๋ฐ OOM ์žฅ์• ๋ฅผ ์˜ˆ๋ฐฉํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ์š”์•ฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. `torch.inference_mode()` ์‚ฌ์šฉ ํ•„์ˆ˜: ์—ฐ์‚ฐ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ์„ ์ฐจ๋‹จํ•˜์—ฌ ํ…์„œ ์ฐธ์กฐ๊ฐ€ ์œ ์ง€๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  2. ํ…์„œ ๋ฐ˜ํ™˜ ์‹œ ๋ช…์‹œ์  ์ด๊ด€ ๋ฐ ๋ณ€์ˆ˜ ์‚ญ์ œ: `detach().cpu()`๋ฅผ ์ ์šฉํ•ด GPU ์ ์œ ๋ฅผ ์ฆ‰์‹œ ํ•ด์ œํ•˜๊ณ  `del` ๊ตฌ๋ฌธ์œผ๋กœ ์ฐธ์กฐ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  3. `PYTORCH_CUDA_ALLOC_CONF` ์„ค์ •: `expandable_segments:True` ๋˜๋Š” `max_split_size_mb` ์„ค์ •์„ ํ†ตํ•ด ๋ฉ”๋ชจ๋ฆฌ ํŒŒํŽธํ™”๋ฅผ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค.
  4. ํ”„๋กœํŒŒ์ผ๋ง ๊ด€์ธก์„ฑ(Observability) ํ™•๋ณด: Memory Snapshot ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ์‹ค์ œ ๋ˆ„์ˆ˜ ํ…์„œ์˜ ํ• ๋‹น ์ง€์ ์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.

์œ„ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ํŒจํ„ด๊ณผ ์ตœ์ ํ™” ์„ค์ •์„ ์ ์šฉํ•˜๋ฉด ์‹ค์‹œ๊ฐ„ ๋น„๋™๊ธฐ AI ์„œ๋น™ ํ™˜๊ฒฝ์—์„œ๋„ OOM ์žฅ์•  ์—†์ด ์•ˆ์ •์ ์ด๊ณ  ๋†’์€ ํŠธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•