C.W.K.
Stream
Lesson 02 of 04 · published

동시에 받고, 한 번에 하나씩 돌려

~12 min · semaphore, serialization, gpu, concurrency, memory

Level 0툴 임차인
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"API 는 백 개 요청을 저글링할 수 있어. GPU 는 정확히 하나 돌릴 수 있고. 그건 다른 두 숫자고, 설계가 둘 다 존중해야 해."

자주 헷갈리는 두 종류의 동시성

async 서버는 많은 요청을 한꺼번에 받게 지어졌어 — 그게 반응형으로 유지하는 거야. 함정은 '많이 받기' 가 '많이 실행' 을 뜻해야 한다고 가정하는 거야. 대부분의 웹 작업엔 그래. GPU inference 엔 절대 안 돼: API 레이어는 행복하게 동시성 유지하고 실제 GPU 작업은 엄격히 한 번에 한 job 씩 돌아야 해. 이 둘을 뭉뚱그리는 게 반응형 서버가 load 하에 크래시난 서버로 변하는 길이야.

왜 두 inference 동시가 다시 OOM 인지

앞 lesson 을 떠올려: 단일 diffusion inference 는 진짜 peak 메모리 비용을 가져. 둘을 동시에 돌리면 그 peak 을 두 배 해. 유한한 디바이스에선, peak 메모리 두 배가 방금 고친 out-of-memory 크래시로 도로 가는 길이야. inference_mode 해법이 한 job 의 메모리를 경계 지었는데; 두 job 동시 실행이 그걸 다시 경계 풀어. GPU 의 동시성이 다른 모든 데의 메모리 규율을 무효화해.

admission 을 execution 에서 분리해. job 받기랑 job 돌리기는 다른 한계를 가진 다른 관심사야. 후하게 받아(반응형 유지); 자원의 진짜 capacity 안에서 실행해(살아있기). 둘 사이의 queue 가 각자 자기 올바른 한계를 따르게 하는 거야.

하나의 semaphore

메커니즘은 작아: 한 번에 정확히 하나의 holder 를 허용하는 동시성 게이트, adapter 의 GPU 섹션 둘레에 감싸짐. job queue 는 여전히 모든 걸 받고 모든 비-GPU 작업을 동시에 돌려 — 검증, 큐잉, 응답 스트리밍. 근데 job 이 실제 inference 에 닿으면, 단일 permit 을 획득해야 해. 둘째 job 은 첫째가 풀 때까지 그 게이트에서 기다려. API 는 절대 안 막히고; GPU 섹션만 직렬화돼.

게이트는 전체 요청이 아니라 희소 자원 둘레에 속해. 전체 요청 핸들러를 직렬화하면, 서버를 단일 스레드로 만들고 반응성을 죽인 거야. semaphore 를 GPU-bound 섹션 둘레에만 감싸. 그 섹션 전후의 모든 건 동시성 유지 — 게이트는 그게 보호하는 희소성만큼 좁아.

그냥 더 큰 머신은 왜 안 돼?

메모리 더 사서 더 많은 동시 inference 를 돌릴 수 있어 — 근데 그건 천장을 옮길 뿐, 제거 안 해. 메모리가 아무리 많아도, 어떤 수의 동시 inference 가 그걸 초과하고, 게이트 없이는 그 수를 어렵게, 프로덕션에서 찾아. semaphore 가 엔진을 어떤 메모리 크기에서든 올바르게 만들어: 디바이스가 얼마나 크든, 디바이스가 담을 수 있는 것보다 많은 GPU 작업을 절대 안 돌리려 해. 올바름이 capacity 를 이겨.

더 많은 capacity 는 버그를 숨기지; 안 고쳐. 경계 없는-동시성 문제에 하드웨어 던지는 건 크래시를 제거 안 하고 크래시 임계값을 올려. 시스템은 여전히 원칙 있는 한계가 없어 — 그냥 더 늦게, 더 헷갈리게 실패해. 설계에서 경계를 고치고, 그다음 여분 capacity 를 해법 대체가 아니라 headroom 으로 즐겨.

피파의 고백

내 async 본능이 '느린 부분을 병렬화해!' 라고 비명 질렀어 — GPU 작업이 가장 느리니까, 분명 더 많이 동시에 돌려. 아빠가 내가 메모리 두 배 없이 물리적으로 두 배 못 하는 그 하나를 병렬화하고 있다고 짚었어. 느린 부분은 느리고 그리고 희소해; 그건 반대 처리가 필요해. 난 '이거 느려?' 만 아니라 '이거 희소해?' 도 묻는 법을 배웠어 — 그리고 async-나의 모든 섬유가 fan out 하고 싶어도 희소한 걸 직렬화하는 법을.

Code

게이트는 희소한 섹션만 감싸·python
import asyncio

class LocalAdapter(Adapter):
    def __init__(self):
        # 정확히 permit 하나. GPU inference 가 한 번에 하나만 돌아.
        self._gpu = asyncio.Semaphore(1)

    async def run(self, request):
        # 여기까지 모든 게 동시성: 여러 요청이 in flight.
        prepared = await self.prepare(request)      # 동시성 OK

        async with self._gpu:                        # 좁은 게이트
            # GPU-bound 섹션만 직렬화. job 2 가 여기서 기다려
            # job 1 이 풀 때까지 — peak 디바이스 메모리 절대 두 배 안 함.
            result = await self._infer(prepared)

        return await self.finalize(result)           # 다시 동시성

External links

Exercise

네 시스템 하나에서 느린 게 아니라 희소한 자원을 찾아 (GPU 하나, 크기 N 의 connection pool, rate-limited 외부 호출). 네 코드가 작업 받기랑 작업 실행을 같은 동시성 레벨로 하고 있어? semaphore 게이트가 둘을 분리하러 어디 갈지 스케치하고 — 게이트가 들 동시성 숫자가 뭔지도.
Hint
게이트의 숫자는 희소 자원의 진짜 capacity 야: GPU 하나 메모리엔 1, 연결 N 개 풀엔 N. admission 은 경계 없이(또는 queue 로만 경계) 유지; execution 은 게이트로 경계.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.