architecture2025-02-20·10 min·186/348

Vercel에서 AI/ML 모델 서빙

Vercel에서 AI/ML 모델을 서빙하는 방법과 최적화 전략을 알아봅니다.

Vercel에서 AI/ML 모델 서빙

Introduction

AI/ML 모델을 웹 애플리케이션에서 서빙하는 것은 점점 더 중요해지고 있습니다. Vercel 환경에서 AI/ML 모델을 효율적으로 서빙하는 방법을 알아보겠습니다.

Environment

  • Vercel 프로젝트 (Next.js 14+)
  • TensorFlow.js
  • ONNX Runtime
  • Python 백엔드 (FastAPI)

Problem

Vercel에서 직접 ML 모델을 서빙할 때 발생하는 문제:

// 모델 로딩 시간이 너무 김
import * as tf from '@tensorflow/tfjs';

const model = await tf.loadLayersModel('model.json');
// Cold start 시 모델 로딩에 수십 초 소요

Analysis

Vercel 환경에서 ML 모델 서빙의 도전:

  1. 메모리 제한: 서버리스 함수 메모리 제한
  2. Cold Start: 모델 로딩 시간
  3. CPU/GPU 제한: GPU 가속 불가
  4. 파일 시스템 접근 제한: 로컬 모델 파일 로딩 어려움

Solution

1. 사전 훈련된 모델 사용

// lib/model-loader.js
import * as tf from '@tensorflow/tfjs';

let model = null;

export async function loadModel() {
  if (model) return model;
  
  // 모델 로딩 (캐싱 활용)
  model = await tf.loadLayersModel(
    `${process.env.MODEL_URL}/model.json`
  );
  
  return model;
}

export async function predict(input) {
  const loadedModel = await loadModel();
  
  const tensor = tf.tensor2d([input]);
  const prediction = loadedModel.predict(tensor);
  
  const result = await prediction.data();
  
  tensor.dispose();
  prediction.dispose();
  
  return Array.from(result);
}

2. 외부 ML API 통합

// lib/ml-api.js
export async function callMLAPI(input, options = {}) {
  const {
    endpoint = process.env.ML_API_ENDPOINT,
    timeout = 10000,
  } = options;
  
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.ML_API_KEY}`,
      },
      body: JSON.stringify({ input }),
      signal: controller.signal,
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`ML API error: ${response.statusText}`);
    }
    
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

3. Python 백엔드 서빙

# ml_service/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import numpy as np
import joblib

app = FastAPI()

# 모델 로딩
model = joblib.load('model.pkl')
scaler = joblib.load('scaler.pkl')

class PredictionRequest(BaseModel):
    features: list[float]

class PredictionResponse(BaseModel):
    prediction: float
    confidence: float

@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
    try:
        # 전처리
        features = np.array(request.features).reshape(1, -1)
        features_scaled = scaler.transform(features)
        
        # 예측
        prediction = model.predict(features_scaled)[0]
        confidence = model.predict_proba(features_scaled).max()
        
        return PredictionResponse(
            prediction=float(prediction),
            confidence=float(confidence)
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health():
    return {"status": "healthy"}

4. Vercel에서 Python 백엔드 호출

// app/api/predict/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { features } = await request.json();
  
  try {
    const response = await fetch(`${process.env.ML_SERVICE_URL}/predict`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ features }),
    });
    
    const result = await response.json();
    
    return NextResponse.json(result);
  } catch (error) {
    console.error('Prediction failed:', error);
    return NextResponse.json(
      { error: 'Prediction failed' },
      { status: 500 }
    );
  }
}

5. 모델 캐싱

// lib/model-cache.js
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

export async function getCachedPrediction(input, predictFn) {
  const cacheKey = `prediction:${JSON.stringify(input)}`;
  const cached = await redis.get(cacheKey);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  const result = await predictFn(input);
  await redis.setex(cacheKey, 3600, JSON.stringify(result));
  
  return result;
}

6. 스트리밍 응답

// app/api/stream-predict/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { input } = await request.json();
  
  const encoder = new TextEncoder();
  
  const stream = new ReadableStream({
    async start(controller) {
      try {
        // 청크 단위로 예측 결과 전송
        for (let i = 0; i < input.length; i += 100) {
          const chunk = input.slice(i, i + 100);
          const prediction = await predict(chunk);
          
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify(prediction)}\n\n`)
          );
        }
        
        controller.enqueue(encoder.encode('data: [DONE]\n\n'));
        controller.close();
      } catch (error) {
        controller.error(error);
      }
    },
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    },
  });
}

Lessons Learned

  1. 외부 API 활용: 복잡한 ML 모델은 외부 API를 통해 서빙하는 것이 효과적입니다.
  2. 캐싱 전략: 동일한 입력에 대한 예측은 캐싱하여 성능을 향상시킬 수 있습니다.
  3. 스트리밍: 대용량 예측은 스트리밍으로 처리하면 사용자 경험을 개선할 수 있습니다.
  4. 에러 핸들링: ML 서비스 실패 시 대체 로직이 필요합니다.
  5. 비용 고려: ML API 사용량에 따른 비용을 고려해야 합니다.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.