TT Lab
시작하기
배우기 러닝패스 코스

LLM 서빙

토큰 스트리밍 서버 직접 구현하기

TT Lab 에서 이어서 보기

목표

SSE 로 토큰을 흘려보내는 서버를 직접 만들고, TTFT 와 토큰 간 지연을 따로 측정하고, 클라이언트 중단 시 생성이 실제로 멈추는지 증명한다.

왜 중요한가

총 생성 시간이 6초로 같아도, 6초 뒤 한꺼번에 나오는 것과 200ms 뒤부터 흘러나오는 것은 완전히 다른 제품입니다. 그래서 LLM 서비스에서 스트리밍은 기본이고, 그 순간부터 측정 방식도 바뀝니다 — 응답 완료 시각 하나가 아니라 첫 청크까지의 시간과 청크 간 간격을 따로 재야 합니다. 이 실습에서 특히 놓치기 쉬운 것이 7번 스텝입니다. 사용자가 긴 응답 중간에 다른 질문으로 넘어가는 패턴은 매우 흔한데, 취소 처리가 없으면 그 요청들이 배치 슬롯을 계속 차지하며 아무도 안 보는 토큰을 만듭니다. 동시 처리량이 이유 없이 떨어지고 GPU 청구서만 늘어납니다. 조용하고 비싼 사고이고, 그래서 한 스텝을 통째로 배정했습니다.

단계

  1. /root/ls/gen.py 에 generate(prompt, n) 를 만든다. 같은 프롬프트와 n 이면 항상 같은 토큰 열을 돌려줘야 한다. /root/ls/gen1.txt 와 /root/ls/gen2.txt 를 같은 인자로 만들어 두 파일 내용이 같아야 한다.
  2. /root/ls/server.py 를 127.0.0.1:8170 에 띄운다. POST /generate 는 {"prompt":"hello","max_tokens":20} 을 받아 {"text":"...","usage":{"prompt_tokens":<n>,"completion_tokens":20}} 를 준다.
  3. GET /stream?prompt=hello&max_tokens=20 은 Content-Type: text/event-stream 으로 응답하고 각 토큰을 data: {"token":"..."} 와 빈 줄로 보낸다. 응답 본문에 data: 로 시작하는 줄이 20줄 이상이어야 한다.
  4. 스트림 마지막에 data: [DONE] 을 보낸다.
  5. /root/ls/ttft.py 로 첫 청크까지의 시간을 재어 /root/ls/ttft.txt 에 ttft_ms=<정수> 를 적는다. 0보다 크고 3000 미만이어야 한다.
  6. /root/ls/itl.txt 에 count=<n> p50_ms=<수> p99_ms=<수> 를 적는다. count 는 19 이상이어야 한다.
  7. max_tokens=100 으로 스트림을 열고 5청크만 받은 뒤 끊는다. GET /_debug/generated 가 직전 스트림 요청에서 서버가 실제로 만든 토큰 수를 알려 주게 하고(누적 합계도 함께 실어 두면 편하다), 그 값을 /root/ls/cancel.txt 에 requested=100 generated=<n> 으로 적는다. generated 는 30 이하여야 한다.
  8. /root/ls/report.txt 에 ttft_ms=<n>, itl_p50_ms=<수>, tokens=<n>, throughput_tps=<수> 네 줄을 적는다.

참고

결정적 토큰 생성기 만들기

/root/ls/gen.py 에 generate(prompt, n) 를 만든다. 같은 프롬프트와 n 이면 항상 같은 토큰 열을 돌려줘야 한다. /root/ls/gen1.txt 와 /root/ls/gen2.txt 를 같은 인자로 만들어 두 파일 내용이 같아야 한다.

같은 프롬프트에 항상 같은 토큰 열이 나와야 채점과 평가가 가능합니다. 해시를 시드로 쓰면 됩니다.

비스트리밍 엔드포인트 만들기

/root/ls/server.py 를 127.0.0.1:8170 에 띄운다. POST /generate 는 {"prompt":"hello","max_tokens":20} 을 받아 {"text":"...","usage":{"prompt_tokens":<n>,"completion_tokens":20}} 를 준다.

먼저 한 번에 돌려주는 형태로 만들어 기준선을 잡습니다. 사용 토큰 수도 함께 돌려주세요.

SSE 로 청크 전송하기

GET /stream?prompt=hello&max_tokens=20 은 Content-Type: text/event-stream 으로 응답하고 각 토큰을 data: {"token":"..."} 와 빈 줄로 보낸다. 응답 본문에 data: 로 시작하는 줄이 20줄 이상이어야 한다.

콘텐츠 타입과 이벤트 구분 규칙이 정해져 있습니다. 이벤트 사이의 빈 줄을 빠뜨리면 클라이언트가 인식하지 못합니다.

종료 신호 보내기

스트림 마지막에 data: [DONE] 을 보낸다.

스트림이 언제 끝났는지 알려 주는 관례적 마커가 있습니다.

첫 토큰까지의 시간 재기

/root/ls/ttft.py 로 첫 청크까지의 시간을 재어 /root/ls/ttft.txt 에 ttft_ms=<정수> 를 적는다. 0보다 크고 3000 미만이어야 한다.

요청 시작과 첫 청크 도착 사이입니다. 전체 완료 시각과 헷갈리지 마세요.

토큰 간 지연 분포 내기

/root/ls/itl.txt 에 count=<n> p50_ms=<수> p99_ms=<수> 를 적는다. count 는 19 이상이어야 한다.

청크마다 도착 시각을 기록해 차이를 냅니다. 평균만으로는 부족합니다.

클라이언트 중단 시 생성 멈추기

max_tokens=100 으로 스트림을 열고 5청크만 받은 뒤 끊는다. GET /_debug/generated 가 직전 스트림 요청에서 서버가 실제로 만든 토큰 수를 알려 주게 하고(누적 합계도 함께 실어 두면 편하다), 그 값을 /root/ls/cancel.txt 에 requested=100 generated=<n> 으로 적는다. generated 는 30 이하여야 한다.

끊긴 것을 감지해야 멈출 수 있습니다. 서버가 만든 토큰 수를 세어 증명하세요.

지표 보고서 만들기

/root/ls/report.txt 에 ttft_ms=<n>, itl_p50_ms=<수>, tokens=<n>, throughput_tps=<수> 네 줄을 적는다.

앞에서 잰 값들을 한 파일에 모읍니다. 처리량은 생성 토큰 수를 총 시간으로 나눈 값입니다.