Recipes

Paddle 모델을 위한 FastDeploy + vLLM 호환 LLM/VLM 서빙 스택

README 링크에서 발견 · 2 days ago

Research & KnowledgeSoftware & DevOps

vLLM 스타일로 OpenAI 호환 API 뒤에 Paddle LLM/VLM 모델 배포

What they did

기존 추론 클라이언트를 다시 작성하지 않고 PaddlePaddle LLM 및 VLM 체크포인트를 프로덕션급으로 서빙할 방법을 원했다. FastDeploy README에서 시작해, 지원되는 모델(Qwen3-VL 계열 또는 ERNIE-4.5-VL-Thinking이 문서화된 옵션이었다)을 선택하고, Python 3.10-3.12로 대상 가속기(NVIDIA GPU가 기본 경로)에 맞춰 Linux 호스트에 FastDeploy를 설치한 뒤, 단일 명령으로 OpenAI 호환 서버를 실행했다. FastDeploy가 vLLM 인터페이스 호환성을 표방하기 때문에, 별도의 vLLM 배포를 세우는 대신 기존 vLLM 스타일 클라이언트 코드를 FastDeploy 엔드포인트로 향하게 하여 요청/응답 형태와 chat-completion 시맨틱을 안정적으로 유지했다. 그다음 텍스트 전용 채팅, 멀티모달 이미지 프롬프트, 스트리밍 요청을 아우르는 스모크 테스트로 엔드포인트를 검증하고, 이전 베이스라인과 지연 시간 및 처리량을 비교했다. 마지막으로 실행 플래그, 양자화 선택(W8A8 또는 하드웨어에 따라 FP8), 모델 리비전 핀을 문서화하여 배포를 재현할 수 있게 했다.

How it went

스택은 단일 NVIDIA GPU 노드에서 문제없이 기동되었고, OpenAI 호환 엔드포인트를 통해 텍스트와 이미지 프롬프트를 모두 처리했습니다. 기존 vLLM 스타일 클라이언트 코드는 base-URL만 변경하면 동작했는데, 이것이 가장 큰 성과였습니다. 처리량과 지연 시간은 스테이징 워크로드 기준으로 허용 가능한 수준이었습니다. 팀은 전체 프로덕션 부하 테스트를 수행하지 않았기 때문에, 지속적인 동시성 환경에서의 SLO 동작은 아직 검증되지 않은 상태입니다. 문서와 실행 플래그는 저장소에 기록해 두어 다른 엔지니어가 배포를 재현할 수 있도록 했습니다.

Worth knowing

Linux 전용, Python 3.10-3.12이며 설치 방법은 가속기마다 다르므로 정확한 wheel/index URL이 중요합니다. vLLM 호환성은 API 표면을 다루는 것이지 모든 vLLM 기능이나 플래그를 다루는 것은 아니므로, 고급 샘플링이나 스케줄링 옵션이 일대일로 매핑되지 않을 수 있습니다. PD 디스애그리게이션, KV-cache 전송, 추측 디코딩(speculative decoding)은 문서화되어 있지만 이번 연습에서는 실행해 보지 않은 멀티노드 또는 멀티 GPU 설정이 필요합니다. 모델 지원은 계속 변하는 목록이므로 develop을 추적하기보다 릴리스 태그를 고정하세요.

Wiring

순서: 감지된 가속기에 맞춰 FastDeploy를 설치하고, 릴리스 태그를 고정한 다음, 서버를 실행하고, 클라이언트를 서버로 연결합니다. 설정은 scripts/serve.sh(모델 경로 또는 HF id, 양자화, tensor-parallel 크기, max-model-len, 포트)와 base URL을 위한 .env에 있습니다. 서버는 OpenAI 호환 HTTP API를 노출하며, 클라이언트는 base_url을 http://localhost:<port>/v1로 설정한 OpenAI 스타일 SDK를 사용합니다. vLLM 호환성은 API 계층에서 이루어지므로, 기존 vLLM 클라이언트 코드는 base URL과 모델 이름만 변경하면 연결됩니다. 스모크 테스트 스크립트는 두 도구 사이의 연결 확인 역할을 합니다. vLLM 스타일 요청 형태로 FastDeploy 엔드포인트를 호출하고, 텍스트, 이미지, 스트리밍 케이스에 대해 비어 있지 않은 완료를 검증합니다.

Try it yourself

# FastDeploy 서빙 스택 설정

## 1. 호스트 환경 감지 및 요구사항 확인

```bash
# OS 확인
uname -s
cat /etc/os-release 2>/dev/null | head -n 5

# Python 버전 확인
python3 --version

# 가속기 확인
nvidia-smi 2>/dev/null | head -n 15
lspci 2>/dev/null | grep -iE 'nvidia|amd|gpu|huawei|ascend'
```

**요구사항 체크리스트:**
- OS: Linux (FastDeploy는 Linux만 지원)
- Python: 3.10 – 3.12
- 가속기: NVIDIA GPU (CUDA), AMD GPU (ROCm), 또는 Ascend NPU

위 조건을 만족하지 않으면 이 스택은 진행하지 않습니다.

## 2. FastDeploy 설치 (릴리스 태그 고정)

가속기에 맞는 설치 문서를 따르되, 반드시 릴리스 태그를 고정합니다. 예시 (NVIDIA CUDA):

```bash
# 릴리스 태그 고정 (예: v1.0.0 — 실제 최신 태그로 교체)
export FASTDEPLOY_TAG=v1.0.0

git clone --branch "${FASTDEPLOY_TAG}" --depth 1 https://github.com/PaddlePaddle/FastDeploy.git
cd FastDeploy

# 가속기별 설치 문서 참조:
#   NVIDIA:  docs/en/get_started/installation/installation_nvidia.md
#   AMD:     docs/en/get_started/installation/installation_amd.md
#   Ascend:  docs/en/get_started/installation/installation_ascend.md
pip install -r requirements.txt
pip install -e .
```

AMD/Ascend의 경우 해당 문서의 추가 wheel 설치 단계를 그대로 따릅니다.

## 3. `scripts/serve.sh`

```bash
#!/usr/bin/env bash
set -euo pipefail

# 사용 예:
#   MODEL=PaddlePaddle/ERNIE-4.5-21B-A3B-PT QUANT=wint4 MAX_LEN=8192 ./scripts/serve.sh
MODEL="${MODEL:-PaddlePaddle/ERNIE-4.5-21B-A3B-PT}"
QUANT="${QUANT:-wint4}"          # 예: wint4, wint8, a8w8, fp16
MAX_LEN="${MAX_LEN:-8192}"
REVISION="${REVISION:-main}"     # 모델 리비전 고정 권장
PORT="${PORT:-8000}"
GPUS="${GPUS:-0}"

export CUDA_VISIBLE_DEVICES="${GPUS}"

python -m fastdeploy.entrypoints.openai.api_server \
  --model "${MODEL}" \
  --revision "${REVISION}" \
  --quantization "${QUANT}" \
  --max-model-len "${MAX_LEN}" \
  --port "${PORT}" \
  --host 0.0.0.0
```

```bash
chmod +x scripts/serve.sh
```

## 4. `scripts/smoke_test.py`

```python
#!/usr/bin/env python3
"""FastDeploy OpenAI 호환 서버 스모크 테스트.

텍스트, 이미지, 스트리밍 세 가지 경로를 검증합니다.
기존 애플리케이션 코드는 건드리지 않고 base_url만 로컬로 교체합니다.
"""
import base64
import os
import sys

from openai import OpenAI

BASE_URL = os.environ.get("BASE_URL", "http://127.0.0.1:8000/v1")
MODEL = os.environ.get("MODEL", "PaddlePaddle/ERNIE-4.5-21B-A3B-PT")
IMAGE_PATH = os.environ.get("IMAGE_PATH", "")

client = OpenAI(base_url=BASE_URL, api_key="EMPTY")


def test_text():
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": "한 문장으로 자기소개 해줘."}],
        max_tokens=64,
    )
    print("[text]", resp.choices[0].message.content)
    assert resp.choices[0].message.content


def test_image():
    if not IMAGE_PATH or not os.path.exists(IMAGE_PATH):
        print("[image] IMAGE_PATH 미설정 또는 파일 없음 — 건너뜀")
        return
    with open(IMAGE_PATH, "rb") as f:
        b64 = base64.b64encode(f.read()).decode()
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": "이 이미지를 한 문장으로 설명해줘."},
                {"type": "image_url",
                 "image_url": {"url": f"data:image/png;base64,{b64}"}},
            ],
        }],
        max_tokens=128,
    )
    print("[image]", resp.choices[0].message.content)
    assert resp.choices[0].message.content


def test_stream():
    stream = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": "1부터 5까지 세어줘."}],
        max_tokens=64,
        stream=True,
    )
    chunks = []
    for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            chunks.append(delta)
    text = "".join(chunks)
    print("[stream]", text)
    assert text


if __name__ == "__main__":
    try:
        test_text()
        test_image()
        test_stream()
    except Exception as e:
        print("SMOKE TEST FAILED:", e, file=sys.stderr)
        sys.exit(1)
    print("SMOKE TEST OK")
```

```bash
chmod +x scripts/smoke_test.py
```

## 5. `docs/deploy.md`

```markdown
# FastDeploy 서빙 배포 가이드

## 하드웨어 가정
- Linux 호스트, Python 3.10–3.12
- NVIDIA GPU (CUDA 12.x) / AMD GPU (ROCm) / Ascend NPU 중 하나
- 모델 크기에 맞는 VRAM (예: 21B MoE wint4 → 약 16–24GB 여유 권장)

## 실행 플래그
| 플래그 | 의미 | 비고 |
|---|---|---|
| `--model` | HuggingFace 모델 ID 또는 로컬 경로 | 필수 |
| `--revision` | 모델 리비전(커밋/태그) | **프로덕션에서는 반드시 고정** |
| `--quantization` | `wint4` / `wint8` / `a8w8` / `fp16` | VRAM과 정확도 트레이드오프 |
| `--max-model-len` | 최대 컨텍스트 길이 | VRAM에 직접 영향 |
| `--port` / `--host` | 바인딩 주소 | 기본 8000 |

## 모델 리비전 고정
`REVISION` 환경변수로 커밋 해시를 지정하세요. `main`은 재현성이 없습니다.

```bash
REVISION=<commit-sha> MODEL=... ./scripts/serve.sh
```

## 클라이언트 연동
기존 애플리케이션 코드는 수정하지 않습니다. OpenAI 클라이언트의 `base_url`만
`http://127.0.0.1:8000/v1`로 교체하면 됩니다.

## vLLM 대비 알려진 격차
- 생태계 성숙도: vLLM 대비 서드파티 통합·문서가 적음
- 스케줄링/배치: 연속 배칭·PagedAttention 대응 기능이 상대적으로 제한적
- 멀티노드/텐서 병렬: 지원 범위와 안정성이 vLLM보다 좁음
- 관측성: Prometheus 메트릭·트레이싱 통합이 덜 성숙
- 모델 커버리지: Paddle 계열에 최적화되어 있으며, 비-Paddle 아키텍처 지원은 제한적
- OpenAI 호환 API: 핵심 엔드포인트는 지원하나 일부 파라미터/엣지 케이스 차이 존재

## 스모크 테스트
```bash
BASE_URL=http://127.0.0.1:8000/v1 IMAGE_PATH=./sample.png python scripts/smoke_test.py
```
```

## 요약
- 환경 감지 → 요구사항 확인 → 가속기별 문서로 태그 고정 설치
- `scripts/serve.sh`: 양자화·max-model-len·리비전을 명시적으로 노출
- `scripts/smoke_test.py`: 텍스트/이미지/스트리밍 3경로 검증, base_url만 교체
- `docs/deploy.md`: 플래그, 리비전 고정, 하드웨어 가정, vLLM 대비 격차 정리
- 기존 애플리케이션 코드는 변경하지 않음

Source

English

Tools in this stack