1. 요청 하나가 지나는 프로세스와 경계
LLM 서빙 프레임워크를 처음 열면 대개 스케줄러부터 찾는다. 하지만 스케줄러 코드를 아무리 읽어도 "이 함수는 누가 부르고, 저 큐에는 누가 넣는가"가 풀리지 않는다. Mini-SGLang 은 한 프로세스짜리 프로그램이 아니기 때문이다. HTTP 요청 하나가 답으로 돌아오기까지 최소 네 종류의 프로세스…
작은 추론 엔진 저장소를 하나 고르고, 커밋 하나에 고정한 뒤 처음부터 끝까지 읽어 나가는 연재입니다. 인용한 코드는 모두 그 커밋의 permalink 로 연결되므로 저장소가 바뀌어도 글과 코드가 어긋나지 않습니다.
지금까지 다룬 저장소:
각 편은 앞 편을 읽었다고 가정하므로 1편부터 순서대로 읽는 편이 낫습니다.
LLM 서빙 프레임워크를 처음 열면 대개 스케줄러부터 찾는다. 하지만 스케줄러 코드를 아무리 읽어도 "이 함수는 누가 부르고, 저 큐에는 누가 넣는가"가 풀리지 않는다. Mini-SGLang 은 한 프로세스짜리 프로그램이 아니기 때문이다. HTTP 요청 하나가 답으로 돌아오기까지 최소 네 종류의 프로세스…
1편에서 스케줄러가 받은 것은 CPU 위의 1차원 int32 텐서였다. 그 토큰열이 답으로 자라는 동안, 시스템은 "이 요청이 어디까지 갔는가"를 어딘가에 적어 두어야 한다. 그 장부가 Req 다. 장부라고 부르는 이유가 있다. 필드가 많아서가 아니라, 길이 세 개로 진행 상태를 표현하고 그 셋의 대소…
2편에서 요청 하나는 세 길이로 표현되는 장부가 됐다. 이제 질문은 하나다. 대기 중인 요청이 여럿일 때, 이번 스텝에 무엇을 얼마나 넣을 것인가. 답은 예산 두 개와 조건 다섯 개다. 그리고 예산을 넘는 프롬프트를 어떻게 처리하는지에 이 저장소의 깔끔한 선택이 하나 더 있다. 예산은 둘이다 배치를 짜는…
3편에서 요청 하나가 "자리를 잡았다"고 했다. 그 자리의 정체가 이 편의 주제다. KV cache 는 페이지 단위로 관리된다. 그런데 요청마다 하나씩 있는 pagetable 을 열어 보면 페이지 번호가 아니라 토큰 하나하나의 위치가 적혀 있다. 페이지로 잡으면서 왜 표에는 토큰 위치를 적는가. 저장소가…
4편에서 요청 하나가 자기 자리를 잡는 법을 봤다. 그런데 같은 시스템 프롬프트로 들어오는 요청 백 개가 각자 같은 계산을 반복하면 그 자리는 금방 동난다. 그래서 이미 계산한 접두사를 다시 쓴다. 다시 쓰기 시작하면 두 가지 질문이 새로 생긴다. 어디까지 같은가를 어떻게 빠르게 찾는가, 그리고 공간이…
지금까지 다섯 편에 걸쳐 스케줄러가 하는 일을 봤다. 메시지를 받고, 예산을 계산하고, 트리를 걷고, 페이지를 잡고, 표를 채운다. 전부 CPU 일이다. 그동안 GPU 는 무엇을 하고 있는가. 아무것도 안 하고 있다면, 모델을 아무리 빨리 돌려도 그 사이사이가 비어 있다는 뜻이다. 이 편은 그 빈틈을 메…
6편 마지막에 preparebatch 의 일곱 줄을 열거하면서 마지막 두 줄을 남겨 뒀다. padbatch 와 preparemetadata 다. 이 편이 그 둘을 연다. 지금까지 세운 것은 전부 파이썬 객체였다. Req 의 길이 세 개, 슬롯 번호, page table 의 한 행. 커널은 그런 것을 모른…
1편에서 스케줄러가 TP 크기만큼 뜬다는 것을 보고, 그 뒤 여섯 편은 하나의 스케줄러 안에서 일어나는 일을 읽었다. 이제 그 전부를 rank 수만큼 복제한다. 여기서 문제가 생긴다. 스케줄러는 매 스텝 결정을 내린다. 어떤 요청을 배치에 넣을지, 어느 페이지를 잡을지, 언제 무엇을 버릴지. rank 들…
jmaczan/tiny-vllm은 Llama 3.2 1B Instruct 를 GPU 에서 돌리는 추론 엔진을 C++ 와 CUDA 로 처음부터 짠 저장소다. PyTorch 도, Hugging Face 도, 심지어 토크나이저도 쓰지 않는다. safetensors 파일을 직접 열어 가중치를 GPU 로 올리고,…
이 엔진에서 main() 은 cuBLAS 핸들을 만든 뒤 곧바로 두 가지를 한다(src/main.cpp:557-569). model.safetensors 파일에서 가중치를 꺼내 GPU 메모리에 올리는 일, 그리고 앞으로 모든 계산이 쓸 버퍼를 미리 잡아 두는 일. 이번 편은 그 둘을 읽는다. 둘 다 "딱…
1편에서 prefill() 은 프롬프트 전체를 한 번에 처리하는 함수라고 했다. 17개 토큰이 한 호출 안에서 임베딩부터 다음 토큰 선택까지 통과한다. 이번 편은 그 함수의 안을 본다. 토큰들이 레이어 하나를 지나는 동안 어떤 커널을, 어떤 순서로 거치는지, 그리고 그 순서가 왜 그 모양인지를 각 커널…
3편에서 Q/K/V 투영을 볼 때 이런 문장을 적어 두고 넘어갔다. "활성과 가중치는 행 우선인데 cuBLAS 는 열 우선을 가정하므로, CUBLASOPT 전치 플래그와 lda/ldb/ldc 만으로 호출한다. 왜 이렇게 부르는지는 4편이 다룬다." 이 편이 그 "왜"다. 3편이 커널이 어떤 순서로 흐르는…
prefill 이 프롬프트 전체를 한 번에 흘려 보내면, 그다음부터는 지금까지 만든 토큰을 바탕으로 토큰을 하나씩 만들어야 한다. 이 단계가 decode 다. 1편에서 본 대로 prefill 과 decode 는 연산의 모양이 달라서 — 전자는 행렬 × 행렬, 후자는 벡터 × 행렬 — 이 저장소는 커널을…
decode 가 토큰을 하나 만들 때마다 어텐션은 그 시퀀스가 지금까지 쌓은 모든 K/V 를 다시 읽는다. 이 K/V 를 시퀀스마다 연속된 큰 버퍼 하나로 잡으면, 시퀀스들이 저마다 다른 시점에 끝나면서 안 쓰는 구간이 생겨 메모리가 낭비된다. vLLM 을 유명하게 만든 PagedAttention 의 답…
GPU 는 한 번에 한 시퀀스만 처리하면 그 시퀀스가 prefill 을 마치고 decode 로 넘어가는 사이에서 쉬게 된다. continuous batching 은 처리 중이던 요청이 끝나는 대로 그 자리에 큐에서 기다리던 다음 요청을 넣어 GPU 를 놀리지 않는 방식이다. 이 저장소의 README 도…