[LLM] LLM 서빙과 최적화: 단일 모델 서빙 시스템 - 3.2. 배치 요청: 정적 배칭의 효과와 한계
서종호(가시다)님의 Hands-On LLM Serving and Optimization Study (LLMSO) 2주차 학습 내용을 기반으로 합니다.
TL;DR
- 배치는 학습·추론 어디서나 “독립적인 여러 입력을 한 번의 연산 호출로 묶는 단위”지만, 학습에서는 gradient 업데이트의 통계적 단위이고 LLM 서빙에서는 요청·토큰·KV 캐시를 스케줄링하는 실행 단위다 — 같은 단어, 다른 관심사
/generate로 프롬프트 5개를 보내면 batch_size=4 제한에 따라 4개 + 1개의 두 번 실행으로 나뉜다. 로그의torch.Size([4, 5])와 request_id 매핑으로 배칭의 실체를 확인했다- 프롬프트 처리량은 기본 생성 대비 약 2배(0.9 → 1.7~1.9 prompts/s)로 올랐다. 그러나 서로 다른 HTTP 요청 사이의 배칭은 일어나지 않아, 동시 요청을 늘리면 처리량은 정체되고 대기 시간만 폭증한다 — 정적 배칭의 한계
- 프롬프트 개수를 바꿔 가며 총 시간의 계단을 관찰하면 배치 크기를 역산할 수 있다. 실측 결과
1.05 × ceil(N/4)에 들어맞아 B=4로 동작함을 확인했다. 측정 노이즈는 항상 시간을 늘리는 방향으로만 작용하므로, 이런 실험에서는 중앙값보다 최소값이 좋은 추정치다
전제: 배치란 무엇인가
지난 글에서 요청당 프롬프트 하나만 처리하는 병목을 재현했고, 이번 글에서 배칭으로 그 병목을 공략한다. 그 전에 짚고 갈 것이 있다 — 배치 사이즈라는 말이 학습할 때와 서빙할 때 미묘하게 다른 의미로 쓰인다. 기본 의미는 같은데, 맥락마다 연관되는 개념과 주의점이 다르다.
학습·추론·서빙에서의 배치
공통의 기본 의미는 하나다. 배치(batch)는 서로 독립적인 여러 입력을 하나의 텐서로 묶어, 모델의 한 번의 연산 호출에서 함께 처리하는 단위다.
입력 A ┐
입력 B ├─ Batch → model(...) 한 번 호출
입력 C ┘
다른 것은 왜 묶는지, 배치가 끝난 뒤 무엇을 하는지, 어떤 기준으로 크기를 정하는지다.
| 구분 | 학습의 배치 | 추론의 배치 | 온라인 LLM 서빙의 배치 |
|---|---|---|---|
| 묶는 대상 | 학습 샘플 | 추론 입력 | 여러 사용자의 활성 요청·시퀀스 |
| 목적 | gradient를 안정적·효율적으로 계산 | 처리량과 하드웨어 활용률 향상 | 동시 요청의 처리량·비용 최적화 |
| 배치 처리 후 | Loss → 역전파 → 파라미터 업데이트 | 예측 결과 반환 | 토큰을 요청별로 분배하고 다음 디코딩 스텝 진행 |
| 주요 고려사항 | 수렴, 일반화, 학습률 | 처리량, 지연, 메모리 | TTFT·TPOT, 큐 대기, KV 캐시, 가변 길이 |
| 배치 구성 | 대체로 학습 코드가 고정 | 고정 또는 동적 | 요청 도착·종료에 따라 계속 변할 수 있음 |
학습의 batch_size=32는 “32개 샘플로 gradient 한 번을 계산해 파라미터를 한 번 업데이트한다”는 뜻이고, 그래서 수렴·일반화·학습률과 얽힌다. 추론에는 역전파가 없으므로 배칭의 목적이 여러 입력을 한 번의 큰 행렬 연산으로 처리해 처리량을 높이는 것으로 단순해진다. 다만 배치가 커지면 메모리와 대기 시간이 늘고 개별 사용자의 지연이 오히려 나빠질 수 있어서, 추론에서도 배치가 클수록 항상 좋은 것은 아니다.
LLM 서빙의 배치가 더 복잡한 이유
LLM 요청은 입력·출력 길이가 제각각이다.
A: 입력 10토큰 + 출력 20토큰
B: 입력 2,000토큰 + 출력 5토큰
C: 입력 100토큰 + 출력 500토큰
그래서 “요청 몇 개”만으로는 메모리와 연산량을 판단하기 어렵고, 실제 서빙 엔진들은 동시 활성 시퀀스 수(vLLM의 max_num_seqs)와 한 스케줄링 스텝의 전체 토큰 수(max_num_batched_tokens)를 함께 제한한다. 프롬프트 전체를 한 번에 처리하는 prefill과 토큰을 하나씩 생성하는 decode의 성격이 다르다는 점(1주차에 본 비대칭)도 여기에 겹친다. 지금은 “서빙 엔진의 배치는 요청 수가 아니라 토큰 수와 KV 캐시 여유 기준으로 동적으로 구성된다”는 방향만 잡아 두면 충분하고, 상세는 vLLM 편에서 다룬다.
정적 배칭과 continuous 배칭
이번 /generate 실습은 정적 배칭(static batching)에 가깝다. 배치를 구성해 시작하면, 그 배치가 다 끝나야 다음 배치가 시작된다.
[A, B, C, D] 시작
→ 네 요청이 모두 끝날 때까지 대기
→ 그다음 [E] 실행
반면 vLLM의 continuous batching은 디코딩 스텝마다 배치 구성을 바꿀 수 있다.
T0: [A, B, C]
T1: A 완료
T2: [B, C, D] <- 빈자리에 D 즉시 투입
그래서 LLM 서빙에서 말하는 배치 사이즈는 종종 고정된 텐서 크기가 아니라 “현재 디코딩 스텝에서 함께 처리 중인 활성 시퀀스 수”를 뜻한다. 이번 글에서는 정적 배칭의 효과와 한계를 측정하고, continuous batching이 왜 필요한지까지의 빌드업으로 삼는다.
실습: 배치 생성 요청
다섯 프롬프트 한 번에 보내기
/generate는 한 HTTP 요청에 프롬프트 배열을 받는다. 2편에서 봤듯 WorkloadManager의 batch_size가 4이므로, 5개를 보내면 내부적으로 4개 + 1개로 나뉘어 실행될 것이다.
~$ curl -s -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{
"prompts": [
"Hello, I am",
"The weather is",
"I want to",
"The best way to",
"The most efficient way to"
]
}' | jq
{
"generated_texts": [
"Hello, I am a student at the University of California, Berkeley. I am a graduate student in the Department of Psychology. ...",
"The weather is, of course, a factor in the weather.\n\nThe weather is a factor in the weather. ...",
"I want toand I want to be a part of this.\nI want to be a part of this. ...",
"The best way to get a job is to get a job. ",
"The most efficient way to get a job is to get a job. "
]
}
결과 5개가 요청 순서대로 돌아온다. 출력이 책 예제와 또 완전히 같은데, 이유는 지난 글에서 확인한 greedy decoding의 결정론 그대로다. 일부 응답 뒤에 붙은 공백 꼬리는 작은 모델이 문장을 끝낸 뒤에도 생성 길이 상한까지 공백 토큰을 반복 생성한 결과로, 모델의 생성 품질·종료 처리 문제이지 배칭 오류가 아니다. 입력 배치를 맞추기 위한 padding과도 별개이며, 표시용으로는 strip() 한 번이면 정리된다.
로그로 보는 4+1 분할
응답만 봐서는 배칭이 됐는지 알 수 없다. 서버 로그가 증거다.
~$ tail -f server_run.log
# 실행 결과 (핵심 발췌)
# 1단계 — 첫 배치: batch_size 상한만큼 4개
llm.model_executor - DEBUG - Sending batch to worker: [Sequence, Sequence, Sequence, Sequence]
llm.model_worker - DEBUG - Batch input shape: torch.Size([4, 5])
# 2단계 — 결과에 request_id가 딸려 나옴
llm.model_executor - DEBUG - Received results from worker: ('complete',
[{'request_id': '8f5c53ca-...', 'generated_text': 'Hello, I am a student...'},
{'request_id': '22bd35ea-...', 'generated_text': 'The weather is, of course...'},
{'request_id': '25a1f857-...', 'generated_text': 'I want toand I want to...'},
{'request_id': 'c4ff937b-...', 'generated_text': 'The best way to get a job...'}])
# 3단계 — 두 번째 배치: 남은 1개
llm.model_executor - DEBUG - Sending batch to worker: [Sequence]
llm.model_worker - DEBUG - Batch input shape: torch.Size([1, 6])
읽을 지점이 세 곳이다.
torch.Size([4, 5])— 4는 한 번에 처리하는 시퀀스 수, 5는 배치 내 최장 프롬프트 기준으로 padding해 맞춘 토큰 길이다. 토크나이저가padding=True로 프롬프트 4개를 텐서 하나에 묶은 결과이고, 이것이 배칭의 물리적 실체다 — 서로 다른 요청이 하나의 forward pass에 섞여 들어간다. 두 번째 배치가[1, 6]인 것도 같은 원리다(남은 프롬프트 1개, 그 길이 기준 6토큰)- request_id 매핑 — 워커가 생성 결과에 원래 Sequence의 id를 다시 붙여 돌려준다. 배치로 섞여 들어가도 각 결과가 어느 프롬프트 것인지 잃지 않는 이유이고, 엔진은 이 id로 각 Sequence에 결과를 채워 넣는다. API 요청의 프롬프트 개수와 실제 모델 실행의 배치 크기는 같지 않을 수 있고, 원칙적으로는 서로 다른 사용자의 요청도 하나의 실행 배치로 다시 묶일 수 있는 구조다
- 4개가 끝난 뒤에야 1개가 실행된다 — 첫 배치가 모두 끝나 활성 슬롯이 비워진 다음에야 대기 중이던 5번째 프롬프트가 단독 배치로 실행됐다. 앞서 말한 정적 배칭(FIFO + 고정 크기)의 동작이 로그로 증명된 셈이다
흐름을 그림으로 정리하면 다음과 같다.
flowchart LR
subgraph req["HTTP 요청 1건"]
A["prompt A"] & B["prompt B"] & C["prompt C"] & D["prompt D"] & E["prompt E"]
end
req --> Q["incoming_queue<br/>(Sequence로 포장, id 발급)"]
Q -->|"batch_size=4 상한"| B1["배치 1: A B C D<br/>torch.Size([4, 5])"]
Q -->|"배치 1 완료 후"| B2["배치 2: E<br/>torch.Size([1, 6])"]
B1 --> W["ModelWorker<br/>한 번의 forward로 4개 생성"]
B2 --> W
W --> M["request_id로 결과 매핑<br/>→ 요청 순서대로 응답 조립"]
배치는 기다려 주지 않는다
글감을 정리하다 생긴 의문 하나 — 프롬프트를 1개만 보내면, 4개가 찰 때까지 기다리느라 오히려 손해를 보는 것 아닌가.
결론은 기다리지 않는다. get_next_batch()는 큐에 쌓여 있는 것만 최대 4개까지 즉시 꺼내 반환할 뿐, 배치를 채우려고 대기하는 로직이 없다. 프롬프트 1개면 크기 1짜리 배치로 바로 실행된다. 뒤의 배치 경계 실험에서 N=1이 약 1.03초 — 단일 실행 1회와 같은 시간 — 로 끝나는 것으로도 확인된다.
다만 이것은 이 구현의 선택일 뿐이다. 실제 서빙 시스템에는 “최대 N ms까지 기다렸다가 모인 만큼 배치로 실행”하는 대기 기반(dynamic) 배칭도 흔하고, 그 대기 시간이 지연과 처리량을 맞바꾸는 튜닝 노브가 된다. 기다리지 않으면 첫 요청의 지연은 최소가 되지만 배치가 덜 차서 처리량 기회를 잃고, 기다리면 그 반대다.
검증 1: 처리량 비교
배치 요청 부하 테스트
지난 글과 같은 방식으로 hey를 쓰되, 이번에는 요청 하나에 프롬프트 5개를 담아 /generate로 보낸다. 주의할 점 — /basic_generate는 요청 1개 = 프롬프트 1개지만 /generate는 요청 1개 = 프롬프트 5개이므로, Requests/sec를 그대로 비교하면 안 되고 프롬프트 처리량으로 환산해야 한다.
배치 프롬프트 처리량 = Requests/sec × 요청당 프롬프트 수
총 10개 요청(= 프롬프트 50개)을 동시성 1, 2, 5, 10으로 보낸 결과다.
| 동시 요청 | Total | Average | p50 | p90 | Requests/sec | Prompts/sec |
|---|---|---|---|---|---|---|
| 1 | 26.99초 | 2.70초 | 2.69초 | 2.97초 | 0.371 | 1.85 |
| 2 | 28.88초 | 5.46초 | 5.81초 | 6.13초 | 0.346 | 1.73 |
| 5 | 27.49초 | 11.20초 | 13.49초 | 14.51초 | 0.364 | 1.82 |
| 10 | 26.04초 | 14.36초 | 15.70초 | 26.04초 | 0.384 | 1.92 |
모든 요청이 200으로 성공했고, 요청 유실은 없다.
기본 생성 대비: 처리량 약 2배
지난 글의 /basic_generate 결과(요청 1개 = 프롬프트 1개라 Requests/sec가 곧 Prompts/sec)와 나란히 놓으면 배칭의 효과가 보인다.
| 동시 요청 | 기본 생성 Prompts/sec | 배치 Prompts/sec | 향상 |
|---|---|---|---|
| 1 | 0.888 | 1.853 | 2.09배 |
| 2 | 0.919 | 1.732 | 1.88배 |
| 5 | 0.958 | 1.819 | 1.90배 |
| 10 | 0.892 | 1.920 | 2.15배 |
c=1 기준으로 계산을 펼치면 이렇다. 기본 생성은 프롬프트당 평균 1.13초였으므로 5개를 순차 실행하면 약 5.6초가 예상되는데, 배치 요청 하나는 평균 2.70초에 끝났다. 프롬프트 5개가 4 + 1의 모델 실행 2회로 처리되면서, 개별 실행 5회가 2회로 줄어든 만큼(약 2.1배)의 개선이 나온 것이다. 프롬프트 하나를 처리하는 평균 비용으로 보면 약 1.1초 → 0.52~0.58초로 절반이 됐다.
주의할 점 하나 — 이 “프롬프트당 비용”은 처리량 계산용 평균이다. 실제 사용자는 배치 전체가 끝나야 응답을 받으므로, 사용자 관점의 지연은 Average 값(2.70초)을 그대로 봐야 한다. 배칭은 처리량을 사는 대신 개별 응답 지연을 지불하는 거래다.
정적 배칭의 한계
효과를 확인했으니 한계를 보자. 동시 요청 수를 늘렸을 때의 패턴이 지난 글과 판박이다.
- 처리량이 늘지 않는다. 동시성을 1 → 10으로 10배 올렸는데 프롬프트 처리량은 1.85 → 1.92 prompts/s로 약 4% 변화, 측정 편차 수준이다. 총 처리 시간도 약 26~29초로 일정하다
- 지연만 폭증한다. 평균 응답은 2.70초 → 14.36초(5.3배), p90은 2.97초 → 26.04초(8.8배). c=10 히스토그램에서는 요청이 약 2~3초 간격으로 하나씩 완료되는 계단이 다시 나타난다
- 네트워크 병목이 아니다. c=10 기준 연결 시간은 4.1ms, resp wait가 14.4초다
즉 요청 내부의 프롬프트들은 배칭되지만, 서로 다른 HTTP 요청을 묶는 배칭은 일어나지 않는다. 각 배치 요청이 단일 ModelWorker 앞에서 줄을 서고, 워커는 한 번에 한 요청의 배치만 순차 처리한다. 정리하면 다음과 같다.
정적 배칭으로 프롬프트 처리량은 약 2배 향상됐지만, 동시 HTTP 요청 간 동적 배칭은 이루어지지 않아 부하가 증가하면 처리량은 정체되고 응답 대기 시간만 크게 증가했다.
앞의 로그 관찰에서 “원칙적으로는 다른 사용자의 요청도 하나의 배치로 묶일 수 있는 구조”라고 했는데, 그 가능성이 실현되려면 요청 경계를 넘어 디코딩 스텝 단위로 배치를 재구성하는 스케줄링이 필요하다 — continuous batching이 하는 일이고, 이 시스템에서는 스트리밍 경로(3.3 예정)가 그 방향의 첫걸음이다.
검증 2: 배치 경계 관찰
아이디어: ceil(N/B)의 계단
처리량 비교와 별개로, 배치 크기 4가 정말 경계로 동작하는지 직접 확인하고 싶었다. hey는 같은 요청을 반복하는 부하 도구라 “프롬프트 개수를 바꿔 가며 경계를 본다”는 목적에는 맞지 않는다. 대신 이런 아이디어를 쓴다 — 배치 크기가 B라면 프롬프트 N개는 ceil(N/B)번의 모델 실행으로 처리되므로, 총 시간은 N에 비례하지 않고 계단식으로 증가해야 한다.
배치 크기 4 가정 시 예상:
N=1 → 실행 1회 N=5 → 실행 2회 (4+1)
N=4 → 실행 1회 N=8 → 실행 2회 (4+4)
N=9 → 실행 3회 (4+4+1)
배칭이 동작한다면 N=1과 N=4의 시간이 4배 차이 나지 않아야 하고, 4 → 5와 8 → 9에서 시간이 계단식으로 뛰어야 한다. 이 로직을 측정 스크립트로 만들었다. N을 바꿔 가며 워밍업 후 여러 번 측정하고, 총시간 ≈ a + b·ceil(N/B) 모델을 B 후보 전부에 대해 최소제곱 적합해 결정계수가 가장 높은 B를 고르는 방식이다.
1차 측정: B=4로 보인다
~$ EXTRA_JSON='"max_tokens": 32' ./test_batch.sh
# 실행 결과 (핵심 발췌)
N 중앙값(s) 최소(s) 프롬프트당(s) 처리량(p/s) vs N=1 직전 대비
─────────────────────────────────────────────────────────────────────────
1 1.064 1.030 1.0641 0.94 1.00x —
2 1.396 1.298 0.6981 1.43 1.31x +31% ← 계단
3 1.335 1.248 0.4451 2.25 1.25x -4%
4 1.220 1.152 0.3049 3.28 1.15x -9%
5 2.268 2.196 0.4537 2.20 2.13x +86% ← 계단
8 2.438 2.337 0.3047 3.28 2.29x +7%
9 4.687 3.329 0.5208 1.92 4.40x +92% ← 계단
── 배치 크기 추정 (모델: 총시간 ≈ a + b·ceil(N/B)) ──
1위 B=4 R²=0.944 실행 1회당 1.574s
── 판정 ──
[O] N=1→4 시간이 1.15배 (4배보다 훨씬 작음) → 한 번에 묶여 처리됨
[O] N=4→5 에서 +86% 계단 상승 → 두 번째 배치가 생김 (경계 = 4)
[O] N=8→9 에서 +92% 계단 상승 → 세 번째 배치가 생김
세 가지 시그니처가 모두 나왔다. 특히 결정적인 두 지점 —
- N=4와 N=8의 프롬프트당 시간이 0.3049초 / 0.3047초로 사실상 같다. 꽉 찬 배치는 몇 번을 돌리든 효율이 동일하다는 뜻이다
- N=1 → 4는 일이 4배인데 시간은 1.15배다. 처리량은 0.94 → 3.28 prompts/s로 3.5배 뛰었다
- 반대로 N=5와 N=9의 “꼬리 배치”는 비싸다. 5번째 프롬프트 하나 때문에 실행 1회분(약 1.1초)이 통째로 추가된다 — 4개짜리 배치를 돌리는 비용과 거의 같다. 배칭의 핵심 트레이드오프가 숫자로 보인다
측정 노이즈와 통계 선택
다만 표를 그대로 믿으면 안 되는 지점이 있다. N=2의 +31% ← 계단은 오탐이다 — N=2 중앙값(1.396초)이 N=4(1.220초)보다 큰데, 실행 횟수가 같은 두 지점에서 이 순서는 물리적으로 어색하다. 3회 측정의 중앙값이 노이즈에 끌려간 것이다.
범위를 넓힌 2차 측정(SIZES="4 8 12 13 16")에서는 이 문제가 추정 자체를 틀리게 만들었다. 중앙값 기준으로는 N=12(5.29초)가 N=13(4.08초)보다 커지는 역전이 생겼고 — 12는 3회, 13은 4회 실행이니 불가능한 순서다 — 추정기는 이 오염된 순서를 설명하려고 B≈5라는 답을 내놨다. 그런데 최소값 기준으로 보면 그림이 완벽하게 맞는다.
| N | 최소(s) | ceil(N/4) | 실행 1회당 |
|---|---|---|---|
| 4 | 1.016 | 1 | 1.02초 |
| 8 | 2.144 | 2 | 1.07초 |
| 12 | 3.166 | 3 | 1.06초 |
| 13 | 4.077 | 4 | 1.02초 |
| 16 | 4.599 | 4 | 1.15초 |
총 시간이 1.05 × ceil(N/4)에 거의 정확히 붙는다. 12 → 13에서 계단(3회 → 4회)이 예측대로 나타나고, 13과 16이 같은 층(4회)인 것도 맞다. 배치 크기 4가 의도대로 동작하고 있다.
여기서 얻은 측정 방법론 하나 — 로컬 머신의 측정 노이즈(다른 프로세스와의 경합, 스로틀링)는 언제나 시간을 늘리는 방향으로만 작용한다. 다른 프로세스가 CPU를 보태 줘서 요청이 빨라지는 일은 없다. 이런 단방향 오염에서는 대칭 노이즈를 가정하는 중앙값보다 “방해 없는 순수 실행 시간”의 추정치인 최소값이 낫다. 스크립트도 기본 통계를 min으로 바꾸고, N이 커졌는데 시간이 줄어드는 물리적으로 불가능한 구간을 경고하도록 고쳤다.
남은 한계
이 실험에서 정리하지 못한 것도 명시해 둔다. 출력 길이를 고정하려고 요청 본문에 "max_tokens": 32를 넣었지만, 서버의 요청 모델(BatchGenerateRequest)에는 prompts 필드만 정의되어 있어 모르는 필드는 조용히 무시된 것으로 보인다(응답 길이가 줄지 않았다). pydantic은 기본적으로 스키마에 없는 필드를 에러 없이 버리기 때문에, 클라이언트는 파라미터가 적용됐다고 착각하기 쉽다. 생성 길이가 실행마다 흔들리면 시간 차이가 배칭이 아니라 출력 길이 탓일 수 있으므로, 위 계단 판정은 min 통계와 ceil(N/B) 적합의 일관성에 근거한 것으로 한정한다. 또 이 실험은 요청 하나 안의 프롬프트 리스트에 대한 정적 배칭을 잰 것이고, 동시 요청을 묶는 continuous batching의 검증은 별도 실험이 필요하다.
정리
- 배치의 공통 정의는 “독립 입력들을 한 번의 연산 호출로 묶는 단위”이고, 학습·추론·서빙은 묶는 목적과 사후 처리, 크기 결정 기준이 다르다. LLM 서빙에서는 가변 길이와 KV 캐시 때문에 요청 수가 아니라 토큰 수 기준의 동적 구성이 필요해진다
/generate의 프롬프트 5개는 batch_size=4 제한에 따라 4+1 두 번의 실행으로 나뉘었고,torch.Size([4, 5])(시퀀스 4개 × padding 5토큰)와 request_id 매핑으로 배칭의 실체를 확인했다- 이 구현의 배칭은 큐에 있는 만큼만 즉시 실행하며 배치를 채우려 기다리지 않는다. 대기 기반 배칭은 지연과 처리량을 맞바꾸는 별도의 설계 선택이다
- 정적 배칭으로 프롬프트 처리량이 약 2배(0.9 → 1.7~1.9 prompts/s) 올랐지만, HTTP 요청 사이의 배칭은 없어서 동시 부하에서는 처리량이 정체되고 지연만 누적된다
- 프롬프트 개수를 바꿔 총 시간의 계단을 관찰하는 방법으로 배치 크기 B=4를 역산해 확인했다. 단방향 측정 노이즈 아래에서는 중앙값보다 최소값이 좋은 추정치라는 방법론도 얻었다
- 남은 문제 — 배치 전체가 끝나야 응답이 돌아오는 구조에서는 사용자가 첫 토큰을 보기까지의 시간도 배치에 묶인다. 토큰을 생성되는 대로 흘려보내는 스트리밍(3.3 예정)이 다음 주제다
참고 링크
- Hands-On LLM Serving and Optimization (O’Reilly)
- 실습 코드: orca3/llm-model-inference — ch03/single_model_llm_serving
- vLLM: Optimization and Tuning (max_num_seqs, max_num_batched_tokens)
- 서빙 시스템 직접 만들기 3.1. 기본 생성 요청
- 서빙 시스템 직접 만들기 2. 코드 구조
댓글남기기