웹 서버 추론
웹 서버 추론 (Web server inference)
웹 서버는 요청을 기다렸다가 도착하는 대로 처리하는 시스템입니다. 즉 Pipeline을 웹 서버의 추론 엔진으로 사용할 수 있어요. 들어오는 각 요청을 처리하기 위해 반복자(iterator, 데이터셋을 순회하는 방식과 비슷하게)를 사용할 수 있기 때문입니다.
출처: 문서
본문
하지만 Pipeline로 웹 서버를 설계하는 것은 근본적으로 다르기 때문에 독특합니다. 웹 서버는 여러 요청을 동시에 처리하기 위해 멀티플렉스(멀티스레드, 비동기 등)됩니다. 반면 Pipeline과 그 밑에 있는 모델은 메모리를 많이 차지하기 때문에 병렬 처리용으로 설계되지 않았어요. Pipeline을 실행할 때나 계산 집약적인 작업에서는 사용 가능한 자원을 모두 주는 것이 가장 좋습니다.
이 가이드는 웹 서버가 요청을 받고 보내는 가벼운 부하를 처리하고, 단일 스레드가 Pipeline 실행이라는 무거운 부하를 처리하도록 해서 이 차이를 우회하는 방법을 보여줍니다.
서버 만들기 (Create a server)
Starlette는 웹 서버를 만드는 가벼운 프레임워크입니다. 다른 프레임워크를 원하는 대로 써도 되지만, 아래 코드를 일부 수정해야 할 수도 있어요.
시작하기 전에 Starlette와 uvicorn이 설치되어 있는지 확인하세요.
!pip install starlette uvicorn
이제 server.py 파일에 간단한 웹 서버를 만들 수 있습니다. 핵심은 모델을 한 번만 로드해서 불필요한 복사본이 메모리를 차지하지 않게 하는 것이에요.
마스크된 토큰 [MASK]를 채우는 파이프라인을 만듭니다.
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from transformers import pipeline
import asyncio
async def homepage(request):
payload = await request.body()
string = payload.decode("utf-8")
response_q = asyncio.Queue()
await request.app.model_queue.put((string, response_q))
output = await response_q.get()
return JSONResponse(output)
async def server_loop(q):
pipe = pipeline(task="fill-mask",model="google-bert/bert-base-uncased")
while True:
(string, response_q) = await q.get()
out = pipe(string)
await response_q.put(out)
app = Starlette(
routes=[
Route("/", homepage, methods=["POST"]),
],
)
@app.on_event("startup")
async def startup_event():
q = asyncio.Queue()
app.model_queue = q
asyncio.create_task(server_loop(q))
아래 명령으로 서버를 시작합니다.
uvicorn server:app
POST 요청으로 서버에 질의합니다.
curl -X POST -d "Paris is the [MASK] of France." http://localhost:8000/
그러면 아래 출력이 반환되어야 합니다.
[{'score': 0.9969332218170166,
'token': 3007,
'token_str': 'capital',
'sequence': 'paris is the capital of france.'},
{'score': 0.0005914849461987615,
'token': 2540,
'token_str': 'heart',
'sequence': 'paris is the heart of france.'},
{'score': 0.00043787318281829357,
'token': 2415,
'token_str': 'center',
'sequence': 'paris is the center of france.'},
{'score': 0.0003378340043127537,
'token': 2803,
'token_str': 'centre',
'sequence': 'paris is the centre of france.'},
{'score': 0.00026995912776328623,
'token': 2103,
'token_str': 'city',
'sequence': 'paris is the city of france.'}]
요청 큐잉 (Queuing requests)
서버의 큐잉 메커니즘은 동적 배치(dynamic batching) 같은 흥미로운 애플리케이션에 사용할 수 있습니다. 동적 배치는 Pipeline으로 처리하기 전에 여러 요청을 먼저 모아 둡니다.
아래 예시는 성능보다 가독성을 위해 의사 코드(pseudocode)로 작성됐어요. 특히 다음 점을 눈여겨 보세요:
-
배치 크기 제한이 없습니다.
-
큐를 가져올 때마다 타임아웃이 리셋되어, 요청을 처리하기 전에
timeout값보다 훨씬 오래 기다리는 상황이 생길 수 있어요. 그러면 첫 추론 요청도 그만큼 지연됩니다. 웹 서버는 큐가 비어 있어도 항상 1ms를 기다리는데, 그 시간을 추론 시작에 쓸 수 있어 비효율적이에요. 다만 배칭이 내 사용 사례에 필수적이라면 그럴 만한 이유가 됩니다.아래처럼 매 fetch마다 리셋하지 말고 단일 1ms 데드라인을 두는 편이 더 좋아요.
async def server_loop(q):
pipe = pipeline(task="fill-mask", model="google-bert/bert-base-uncased")
while True:
(string, rq) = await q.get()
strings = []
queues = []
strings.append(string)
queues.append(rq)
while True:
try:
(string, rq) = await asyncio.wait_for(q.get(), timeout=1)
except asyncio.exceptions.TimeoutError:
break
strings.append(string)
queues.append(rq)
outs = pipe(strings, batch_size=len(strings))
for rq, out in zip(queues, outs):
await rq.put(out)
오류 확인 (Error checking)
프로덕션에서는 실패할 수 있는 일이 많습니다. 메모리 부족, 공간 부족, 모델 로드 실패, 잘못된 모델 설정, 잘못된 쿼리 등 다양해요.
try...except 문을 추가하면 이런 오류를 사용자에게 돌려줘 디버깅에 도움이 됩니다. 단, 특정 정보를 공개하면 안 되는 상황에서는 보안 위험이 될 수 있다는 점을 명심하세요.
회로 차단 (Circuit breaking)
서버가 과부하되면 사용자를 무한정 기다리게 만드는 대신 503 또는 504 오류를 반환하도록 시도하세요.
큐가 하나뿐이라면 이런 오류 유형을 구현하는 것은 비교적 간단합니다. 서버가 부하로 실패하기 전에 큐 크기를 확인해 언제 오류를 반환하기 시작할지 결정하세요.
메인 스레드 차단 (Block the main thread)
PyTorch는 비동기를 인지하지 못하므로(not async aware), 계산이 메인 스레드의 실행을 차단합니다.
따라서 PyTorch를 별도의 스레드나 프로세스에서 실행하는 것이 더 좋아요. 단일 요청의 추론이 특히 길 때(1초 이상)는 훨씬 더 중요합니다. 그런 경우 추론 중에는 모든 쿼리가 오류를 받기 전에 1초를 기다려야 하기 때문입니다.
동적 배치 (Dynamic batching)
동적 배치는 올바른 환경에서 사용하면 매우 효과적이지만, 한 번에 1개 요청만 전달할 때는 필요하지 않습니다 (자세한 내용은 배치 추론을 참고하세요).
더 알아보기 (Learn more)
- Starlette 문서: 웹 서버 프레임워크 가이드
- uvicorn 문서: ASGI 서버 실행 방법