GPT-5.2

GPT-5.2

#1 / 5 50.9s 4469
8.6

OpenAI의 최신 플래그십 모델

AI 평가

명확성9.0
완성도8.8
관련성8.0
창의성8.5

교차 평가 종합 점수

전체 응답

스테이징 환경의 단일 A100에서 초당 약 45토큰(tokens/sec)으로 실행되던 미세 조정된 Llama 모델을 배포했는데, 프로덕션 트래픽이 몰리자 처리량이 초당 18토큰으로 급감하고 p95 지연 시간(latency)이 두 배로 늘어납니다. 모델은 바뀌지 않았습니다. 바뀐 것은 파이프라인입니다. 콜드 스타트, 컨테이너 이미지 비대화, 노이지 네이버(noisy neighbors), 그리고 네트워크를 통해 텐서를 불필요하게 한 번 더 이동시키는 데이터 경로가 원인입니다.

지금 이 문제가 중요한 이유는 AI 워크로드가 "모델 중심"에서 점차 "시스템 중심"으로 변하고 있기 때문입니다. PyTorch 2.x(torch.compile 포함), vLLM, Ray와 같은 프레임워크는 런타임 스케줄링, 커널 선택, 메모리 레이아웃에 더 많은 성능 결정권을 부여합니다. 동시에 GPU 공급은 부족하고, 팀들은 배포 마찰을 최소화하면서 달러당 토큰 처리량을 극대화해야 하는 압박을 받고 있습니다.

이런 상황에서 CoreWeave는 GPU 우선 인프라와 쿠버네티스 네이티브 기본 객체(primitives)를 중심으로 구축되었다는 점에서 흥미롭습니다. 개발자에게 이는 "GPU 플릿 + 빠른 스토리지 + 컨테이너 스케줄링"을 코드로 취급할 수 있음을 의미하며, 스택의 다른 부분과 마찬가지로 성능을 반복적으로 개선할 수 있게 해줍니다. 즉, 측정하고, 변수를 하나 바꾸고, 다시 측정하는 과정이 가능해집니다.

1) 관점: GPU 스케줄링을 위한 코드형 인프라(Kubernetes + CoreWeave)

예측 가능한 배치를 통해 적절한 GPU 클래스에 안정적으로 도달할 수 없다면, 다른 모든 최적화는 소음에 불과합니다. 오토스케일링 중에 포드(pod)가 더 작은 GPU로 밀려나거나, 리소스 요청/제한(requests/limits)이 잘못 설정되어 스케줄러가 워크로드를 너무 빽빽하게 배치하는 바람에 처리량이 20~35% 저하되는 팀들을 많이 보았습니다.

CoreWeave에서는 일반적으로 쿠버네티스 리소스 요청을 통해 GPU 요구 사항을 표현하며, 적절한 노드 풀을 타겟팅하기 위해 노드 어피니티(node affinity)와 톨러레이션(tolerations)을 사용합니다. 요청 사항을 명시적으로 유지하세요. "최선형(best effort)" GPU 스케줄링은 예기치 않은 지연 시간 급증의 원인이 됩니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-inference
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vllm-inference
  template:
    metadata:
      labels:
        app: vllm-inference
    spec:
      # 타겟 GPU 노드 (라벨 이름은 클러스터에 따라 다를 수 있으므로 CoreWeave 설정에 맞게 조정하세요)
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node.kubernetes.io/instance-type
                    operator: In
                    values: ["gpu-a100-80gb"]
      containers:
        - name: server
          image: ghcr.io/vllm-project/vllm-openai:v0.5.4
          args:
            - "--model"
            - "meta-llama/Meta-Llama-3-8B-Instruct"
            - "--tensor-parallel-size"
            - "1"
            - "--gpu-memory-utilization"
            - "0.90"
          ports:
            - containerPort: 8000
          resources:
            requests:
              nvidia.com/gpu: "1"
              cpu: "4"
              memory: "16Gi"
            limits:
              nvidia.com/gpu: "1"
              cpu: "8"
              memory: "24Gi"

팁과 주의사항

  • CPU와 메모리에 대해 요청(requests)과 제한(limits)을 모두 설정하세요. 토큰화, HTTP 처리, 샘플링 로직은 여전히 CPU에서 실행되므로 CPU 자원이 부족하면 초당 토큰 수가 현저히 떨어질 수 있습니다.
  • 모델 캐시 스토리지를 컴퓨팅 노드 근처에 고정하세요. 부하가 걸린 상황에서 모델 가중치나 KV 캐시가 네트워크 스토리지로 넘어가게 되면 p95 지연 시간이 급격히 나빠집니다.
  • 콜드 스타트를 정상 상태(steady-state)와 분리하여 측정하세요. 제로 스케일링(scale to zero)을 사용하는 경우, 2~5분이 소요되는 이미지 풀(pull) 및 모델 로드 시간이 사용자 경험을 좌우할 수 있습니다.

2) 관점: 컨테이너 및 빌드 관리(Python wheels, CUDA 라이브러리, 이미지 크기)

제가 디버깅했던 "클라우드 AI가 느리다"는 사례의 대부분은 결국 "컨테이너가 너무 크고 시작 경로에서 불필요한 작업이 많다"는 것이 원인이었습니다. 전체 CUDA 툴킷, 빌드 종속성, 캐시를 프로덕션 이미지에 모두 복사하면 12~18GB 크기의 이미지가 만들어지는 일은 흔합니다. 붐비는 클러스터에서 이는 롤아웃과 오토스케일링 이벤트에 몇 분의 시간을 추가하게 됩니다.

더 깔끔한 접근 방식은 멀티 스테이지 빌드를 사용하고, wheel 파일을 캐싱하며, 런타임에 필요한 CUDA 라이브러리만 설치하는 것입니다. 이를 통해 정상 상태의 초당 토큰 수가 10배 빨라지지는 않겠지만, 기준점에 따라 콜드 스타트 시간을 30~70%까지 단축할 수 있습니다.

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS builder
WORKDIR /w

# wheel 빌드 (설치 속도 향상 및 최종 레이어 변경 최소화)
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential git && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml uv.lock ./
# uv는 빠르고 재현 가능합니다. 원한다면 pip를 사용해도 됩니다.
RUN pip install --no-cache-dir uv==0.4.20
RUN uv sync --frozen --no-install-project --python-preference=only-system

# 앱 wheel 빌드 (런타임을 깔끔하게 유지)
COPY . .
RUN uv build

FROM python:3.12-slim AS runtime
WORKDIR /app

# 위에서 빌드한 wheel에서 런타임 종속성만 설치
COPY --from=builder /w/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl && rm -rf /tmp/*.whl

# 읽기 전용 컨테이너에서 Python이 .pyc 파일을 생성하지 않도록 설정
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

EXPOSE 8000
CMD ["python", "-m", "my_service.api"]

실무적 고려사항

  • 런타임 이미지에 컴파일러를 포함하지 마세요. CUDA 확장을 컴파일해야 한다면 빌더 스테이지에서 수행하고 결과물만 복사하세요.
  • CUDA/PyTorch 호환성을 고정(pin)하세요. 일치하지 않는 wheel을 사용하면 조용히 CPU 연산으로 돌아가거나 느린 커널이 실행될 수 있습니다. 시작 시 셀프 체크를 통해 이를 검증하세요.
  • 이미지 크기와 풀(pull) 시간을 주요 지표로 추적하세요. 이미지 크기를 줄이는 것만으로 배포 롤백 시간을 약 9분에서 3분으로 단축한 팀들을 보았습니다.

3) 관점: 배칭 및 스트리밍을 통한 런타임 처리량 개선(vLLM + OpenAI 호환 API)

LLM 추론에서 가장 큰 성능 이득은 대개 더 스마트한 배칭(batching)과 KV 캐시 관리에서 나옵니다. vLLM의 페이징 어텐션(paged attention)과 지속적 배칭(continuous batching)은 동시 부하 상황에서 실질적인 처리량을 획기적으로 높여주며, 이는 단순한 "GPU당 요청 하나" 방식의 서버와 비교할 때 특히 두드러집니다.

CoreWeave는 인프라를 제공하지만, 서버를 올바르게 구동하는 것은 사용자의 몫입니다. 서버가 여러 요청을 배칭하는 동안 클라이언트는 사용자에게 토큰을 스트리밍해야 합니다. 이것이 GPU 사이클을 놀리지 않으면서 p95 지연 시간을 합리적으로 유지하는 방법입니다.

import os
from openai import OpenAI

# vLLM OpenAI 호환 서버 엔드포인트
client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY", "not-needed-for-local"),
    base_url=os.environ.get("VLLM_BASE_URL", "http://vllm-inference:8000/v1"),
)

def stream_chat(prompt: str) -> None:
    # 스트리밍은 체감 지연 시간을 줄이고 연결을 활성 상태로 유지합니다.
    stream = client.chat.completions.create(
        model="meta-llama/Meta-Llama-3-8B-Instruct",
        messages=[
            {"role": "system", "content": "Be concise and accurate."},
            {"role": "user", "content": prompt},
        ],
        temperature=0.2,
        stream=True,
    )

    for event in stream:
        delta = event.choices[0].delta
        if delta and delta.content:
            print(delta.content, end="", flush=True)
    print()

if __name__ == "__main__":
    stream_chat("사용자 테이블에서 중복된 이메일을 찾는 SQL 쿼리를 작성해 주세요.")

성능 참고 사항

  • 배칭 트레이드오프: 과도하게 배칭하면 처리량은 높아지지만 첫 번째 토큰 생성 시간(TTFT)이 늘어날 수 있습니다. TTFT와 초당 토큰 수를 모두 모니터링하세요.
  • 적절한 최대 컨텍스트(max context) 설정: 컨텍스트 길이를 너무 크게 할당하면 KV 캐시 압박이 커져 캐시 축출이 발생하거나 GPU 메모리 활용 목표치가 낮아질 수 있습니다.
  • 동시성 벤치마크 수행: 단일 요청 벤치마크는 실제와 다릅니다. 8~64개의 동시 클라이언트를 실행하여 지속적 배칭이 효과를 발휘하는지 확인하세요.

4) 관점: 분산 학습과 데이터 로컬리티(Ray + PyTorch DDP)

데이터 파이프라인이 GPU에 데이터를 충분히 공급하지 못하면 프로덕션 환경의 학습 워크로드는 실패합니다. 데이터 로더가 네트워크 읽기, 작은 파일 I/O 또는 CPU 변환 작업에서 병목 현상을 일으켜 멀티 GPU 작업의 활용률이 40~55%에 머무는 경우를 보았습니다. 해결책은 "더 많은 GPU"가 아니라 "기존 GPU를 굶기지 않는 것"이었습니다.

Ray는 실용적인 중간 계층입니다. 워커를 조정하고 전처리를 컴퓨팅 노드에 더 가깝게 배치하며 작업 정의를 코드로 유지할 수 있습니다. CoreWeave 스타일의 쿠버네티스 클러스터에서 Ray의 오퍼레이터 패턴은 GPU 노드 풀과 잘 어우러집니다.

# train_ray_ddp.py
import os
import ray
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

def setup_ddp(rank: int, world_size: int) -> None:
    os.environ["MASTER_ADDR"] = os.environ.get("MASTER_ADDR", "127.0.0.1")
    os.environ["MASTER_PORT"] = os.environ.get("MASTER_PORT", "29500")
    dist.init_process_group("nccl", rank=rank, world_size=world_size)
    torch.cuda.set_device(rank)

def cleanup_ddp() -> None:
    dist.destroy_process_group()

@ray.remote(num_gpus=1)
def train_worker(rank: int, world_size: int) -> float:
    setup_ddp(rank, world_size)

    model = torch.nn.Linear(4096, 4096, bias=False).cuda()
    ddp = DDP(model, device_ids=[rank])

    opt = torch.optim.AdamW(ddp.parameters(), lr=1e-3)
    loss_fn = torch.nn.MSELoss()

    # 가상 배치; 로컬/캐시 스토리지에 고정된 실제 DataLoader로 교체하세요.
    for _ in range(50):
        x = torch.randn(32, 4096, device="cuda")
        y = torch.randn(32, 4096, device="cuda")
        opt.zero_grad(set_to_none=True)
        loss = loss_fn(ddp(x), y)
        loss.backward()
        opt.step()

    cleanup_ddp()
    return float(loss.detach().cpu().item())

if __name__ == "__main__":
    ray.init(address=os.environ.get("RAY_ADDRESS", "auto"))

    world_size = int(os.environ.get("WORLD_SIZE", "2"))
    losses = ray.get([train_worker.remote(r, world_size) for r in range(world_size)])
    print({"final_losses": losses})

데이터 경로 팁

  • 수백만 개의 작은 객체보다는 더 적고 더 큰 파일(예: WebDataset shards)을 선호하세요. 작은 파일 오버헤드는 GPU 활용률을 10~30%까지 떨어뜨릴 수 있습니다.
  • 에포크마다 발생하는 포크/스폰 오버헤드를 피하기 위해 데이터 로더 워커를 고정하고 persistent_workers를 사용하세요.
  • NCCL 시간을 모니터링하세요. all-reduce 작업이 지배적이라면 네트워크 대역폭이 병목인 상태입니다. 노드 간 확장(scale out)을 하기 전에 노드당 GPU 수를 먼저 늘리세요(scale up).

5) 관점: 비용 및 성능 가드레일(부하 테스트 + 오토스케일링 신호)

AI 워크로드 최적화는 단순히 최대 처리량을 얻는 것만이 아니라, 트래픽 변화에 따라 성능을 안정적으로 유지하는 것입니다. 실제 제약 조건은 GPU 메모리인데 CPU 사용량을 기준으로 확장하다가 오토스케일러가 진동하고 간헐적인 OOM(Out Of Memory)이 발생하는 사례를 보았습니다.

가드레일을 코드로 구현하세요. 반복 가능한 부하 테스트를 실행하고, 지연 시간과 처리량을 캡처하며, 적절한 신호(GPU 활용률, 큐 깊이 또는 처리 중인 요청 수)를 오토스케일링에 전달하세요. 달러당 토큰 처리량을 측정하지 않는다면 엉뚱한 것을 최적화하게 될 것입니다.

// loadtest.js (Node.js 20+)
import http from "node:http";
import { setTimeout as sleep } from "node:timers/promises";

const BASE = process.env.VLLM_BASE_URL ?? "http://localhost:8000/v1";
const CONCURRENCY = Number(process.env.CONCURRENCY ?? 16);
const REQUESTS_PER_WORKER = Number(process.env.REQS ?? 20);

function postJson(path, body) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify(body);
    const req = http.request(
      `${BASE}${path}`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Content-Length": Buffer.byteLength(data),
          // vLLM은 인증을 무시할 수 있지만 호환성을 위해 헤더를 유지합니다.
          Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? "x"}`,
        },
      },
      (res) => {
        let buf = "";
        res.setEncoding("utf8");
        res.on("data", (c) => (buf += c));
        res.on("end", () => resolve({ status: res.statusCode, body: buf }));
      }
    );
    req.on("error", reject);
    req.write(data);
    req.end();
  });
}

async function worker(id) {
  const latencies = [];
  for (let i = 0; i < REQUESTS_PER_WORKER; i++) {
    const t0 = performance.now();
    const resp = await postJson("/chat/completions", {
      model: "meta-llama/Meta-Llama-3-8B-Instruct",
      messages: [{ role: "user", content: "PyTorch 추론 속도를 높이는 3가지 팁을 알려줘." }],
      temperature: 0.2,
      max_tokens: 128,
      stream: false,
    });
    const t1 = performance.now();

    if (resp.status !== 200) throw new Error(`HTTP ${resp.status}: ${resp.body}`);
    latencies.push(t1 - t0);

    // 테스트 시 고정된 배칭 아티팩트를 방지하기 위해 약간의 지터를 추가합니다.
    await sleep(10 + Math.random() * 30);
  }
  return latencies;
}

function percentile(xs, p) {
  const s = [...xs].sort((a, b) => a - b);
  const idx = Math.floor((p / 100) * (s.length - 1));
  return s[idx];
}

const all = (await Promise.all([...Array(CONCURRENCY)].map((_, i) => worker(i)))).flat();
console.log({
  n: all.length,
  p50_ms: percentile(all, 50).toFixed(1),
  p95_ms: percentile(all, 95).toFixed(1),
  p99_ms: percentile(all, 99).toFixed(1),
});

확장 기준(What to scale on)

  • 추론: CPU가 아닌 복제본당 큐 깊이 또는 처리 중인 요청 수를 기준으로 확장하세요. GPU가 포화 상태일 때도 CPU 사용량은 낮은 경우가 많습니다.
  • 학습: 스텝 시간(step time)과 GPU 활용률을 기준으로 확장하세요. 활용률이 70% 미만이라면 노드를 추가하기 전에 입력 파이프라인부터 수정하세요.
  • 비용 제어: 최대 복제본 수를 제한하고 카나리 롤아웃을 사용하세요. 설정 오류 하나로 몇 시간 만에 GPU 예산을 모두 써버릴 수 있습니다.

GPU 배치에 대해 쿠버네티스 네이티브 수준의 제어가 필요하고 성능을 코드 문제(명시적 스케줄링, 가벼운 이미지, 배칭 인식 추론 서버(vLLM), 데이터를 컴퓨팅에 가깝게 유지하는 분산 학습)로 다룰 준비가 되었다면 CoreWeave를 사용해 보세요. GPU 요청 및 어피니티를 고정하고 콜드 스타트를 줄이는 것부터 시작한 다음, 동시성을 벤치마킹하고 배칭을 튜닝하세요. 이 순서를 따르면 지연 시간을 불안정하게 만들지 않으면서도 달러당 토큰 처리량에서 가장 크고 재현 가능한 이득을 얻을 수 있습니다.

comparison.translatedBy

다른 AI의 평가

Grok 4
8.8
명확성
9.0
완성도
9.0
관련성
8.0
창의성
9.0
Overall
8.8

문단이 간결하고 실용적인 코드 예시가 논리적으로 흐르며, 숙련된 블로거가 쓴 것처럼 가독성이 높고 자연스럽습니다. 실제 사례와 데이터 수치가 신뢰도와 몰입감을 더합니다. 다만 다섯 가지 관점을 약속했음에도 중간에 갑자기 끊겨 전체적인 구조를 해치는 점은 아쉽습니다.

번역 보기
Claude Sonnet 4.6
8.6
명확성
9.0
완성도
8.0
관련성
8.0
창의성
9.0
Overall
8.6

이번 세트 중 가장 뛰어난 글임. 구체적인 성능 비교(45 vs 18 tokens/sec)로 시작해 즉각적인 신뢰를 확보했고, 코드 샘플도 실무급임. "다른 최적화는 다 소음일 뿐" 같은 표현은 실제 엔지니어의 통찰이 느껴짐. 섹션 3에서 문장이 끊긴 점만 아니었다면 만점에 가까웠을 것임.

번역 보기
Gemini 3 Flash
8.6
명확성
9.0
완성도
9.0
관련성
8.0
창의성
8.0
Overall
8.6

현대 AI의 시스템 엔지니어링적 특성을 잘 파악한 고퀄리티 기술 문서입니다. 컨테이너 위생과 K8s 리소스 요청에 집중한 분석은 실제 운영 환경에서 매우 실용적입니다.

번역 보기
DeepSeek V3.2
8.6
명확성
9.0
완성도
9.0
관련성
8.0
창의성
8.0
Overall
8.6

자신감 넘치는 어조와 깊이 있는 기술적 통찰이 돋보이는 훌륭한 글입니다. 도입부 시나리오가 독자를 끌어들이기에 완벽하며, 조언은 구체적이고 실용적입니다. 구성은 논리적이나, 문장 중간에 글이 끊겨 완성도를 온전히 평가하기 어렵

번역 보기