나노바나나(NanoBanana) 실전 배포: ONNX 변환과 API 서버 구축
들어가는 글
나노바나나(NanoBanana) 시리즈의 마지막 날이다. 그동안 우리는 나노바나나의 아키텍처 이해부터 추론, 파인튜닝, 제어(Control)까지 모델의 핵심 기능을 샅샅이 파헤쳤다.
이제 남은 과제는 ‘어떻게 사용자에게 전달할 것인가’이다. 로컬 터미널에서 혼자 python inference.py를 실행하는 것은 실험 단계일 뿐이다. 실제 서비스를 위해서는 모델을 더 가볍게 압축하고, 안정적인 API 형태로 제공해야 한다.
이번 글에서는 나노바나나를 ONNX(Open Neural Network Exchange) 포맷으로 변환하여 성능을 극대화하고, FastAPI를 이용해 간단한 서빙 시스템을 구축하는 방법을 다룬다.
ONNX 변환: 왜 필요한가?
PyTorch 모델은 유연하지만 무겁다. ONNX로 변환하면 다음과 같은 이점을 얻을 수 있다.
- 플랫폼 독립성: Python뿐만 아니라 C++, C#, Java 등 다양한 언어에서 모델을 실행할 수 있다.
- 하드웨어 가속: NVIDIA TensorRT, Intel OpenVINO 등 하드웨어별 최적화 런타임을 사용할 수 있어 추론 속도가 2~3배 빨라진다.
- 모바일 최적화: iOS(CoreML)나 Android(NCNN)로 포팅하기 위한 중간 단계로 활용된다.
변환 스크립트
optimum 라이브러리를 사용하면 복잡한 변환 과정을 자동화할 수 있다.
1
pip install optimum[onnxruntime-gpu]
1
2
3
4
5
6
# 나노바나나 모델을 ONNX로 변환
optimum-cli export onnx \
--model banana-ai/nano-banana-v1-base \
--task text-to-image \
--device cuda \
./onnx-banana/
변환이 완료되면 ./onnx-banana 폴더에 unet.onnx, text_encoder.onnx, vae_decoder.onnx 등의 파일이 분리되어 저장된다.
ONNX Runtime을 이용한 고속 추론
이제 PyTorch가 아닌 ORTModelForTextToImage 클래스를 사용하여 추론을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from optimum.onnxruntime import ORTModelForTextToImage
from transformers import AutoTokenizer
# ONNX 모델 로드 (Provider를 CUDA로 설정)
model = ORTModelForTextToImage.from_pretrained(
"./onnx-banana",
provider="CUDAExecutionProvider"
)
tokenizer = AutoTokenizer.from_pretrained("./onnx-banana")
prompt = "A golden statue of a banana in a museum"
inputs = tokenizer(prompt, return_tensors="np")
# 추론 실행
outputs = model(**inputs)
# 이후 VAE 디코딩 과정을 거쳐 이미지로 변환...
테스트 결과, RTX 3060 환경에서 PyTorch 대비 약 40% 이상의 속도 향상이 확인되었다.
FastAPI를 활용한 모델 서빙
웹 서비스나 앱과 연동하려면 HTTP API가 필요하다. Python의 비동기 프레임워크인 FastAPI는 머신러닝 모델 서빙에 최적화되어 있다.
server.py 작성
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
from fastapi import FastAPI, UploadFile
from pydantic import BaseModel
from nano_banana import BananaPipeline
import torch
import io
import base64
app = FastAPI(title="NanoBanana API")
# 모델은 서버 시작 시 한 번만 로드하여 메모리에 상주
print("Loading Model...")
pipe = BananaPipeline.from_pretrained(
"banana-ai/nano-banana-v1-base",
torch_dtype=torch.float16
).to("cuda")
print("Model Loaded!")
class GenRequest(BaseModel):
prompt: str
negative_prompt: str = ""
steps: int = 25
@app.post("/generate")
async def generate_image(req: GenRequest):
# 이미지 생성
image = pipe(
prompt=req.prompt,
negative_prompt=req.negative_prompt,
num_inference_steps=req.steps
).images[0]
# 이미지를 Base64 문자열로 인코딩하여 반환
buffered = io.BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
return {"status": "success", "image_base64": img_str}
# 실행: uvicorn server:app --host 0.0.0.0 --port 8000
API 호출 테스트
서버를 실행한 후, 다른 터미널에서 curl을 이용해 요청을 보낸다.
1
2
3
curl -X POST "http://localhost:8000/generate" \
-H "Content-Type: application/json" \
-d '{"prompt": "Cute cat in space suit"}'
응답으로 받은 Base64 문자열을 디코딩하면 이미지를 얻을 수 있다.