다중 독립 질문: 하나의 요청으로 배치할까, 여러 개로 나눌까? — LLM 동시 처리 분석
Source: Dev.to
위에 제공된 Source 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.
어느 것이 더 빠른가?
짧은 답변:
질문을 여러 독립적인 병렬 요청으로 나누는 것이 거의 항상 더 빠릅니다.
왜? – LLM이 “글을 쓰는” 방식을 첫 원리에서 바라보기
| 단계 | 일어나는 일 | 지연에 미치는 영향 |
|---|---|---|
| Autoregressive generation | 모델이 한 번에 하나의 토큰을 생성하고, 이를 프롬프트에 추가한 뒤 다음 토큰을 생성합니다. | N 토큰 → N 번의 포워드 패스 |
| Prefill | 전체 입력 프롬프트를 한 번 처리하여 KV‑cache를 구축합니다. | 입력 길이에 비례하는 선형 시간 |
| Decode | 토큰이 순차적으로 (하나씩) 생성됩니다. | 전체 지연을 주도함 |
결과
- 100‑토큰 답변 ≈ 100번의 추론 단계.
- 500‑토큰 답변 ≈ 500번의 추론 단계.
- 전체 출력 길이가 직접적으로 전체 지연을 결정합니다.
Source: …
시나리오: 5개의 독립 질문, 각각 ~200 토큰
접근법 A – 모든 질문을 하나의 요청으로 결합
Please answer the following questions separately:
1. …
2. …
3. …
4. …
5. …
모델이 해야 할 일
- Prefill: 긴 연결 프롬프트(5개 질문 전체)를 처리합니다.
- Decode: ≈ 5 × 200 = 1000 토큰을 순차적으로 생성합니다.
- 추가 오버헤드:
- 컨텍스트 전환(“이제 질문 3에 답변합니다”).
- KV‑cache가 커져서 단계당 더 많은 어텐션 연산이 필요합니다.
- 포맷팅/전환 텍스트가 토큰 수를 1000을 초과하게 만들기도 합니다.
예상 지연 시간 ≈ 1000 × (토큰당 생성 시간).
접근법 B – 5개의 독립 요청을 병렬로 전송
각 요청은 하나의 질문만 포함하고 ≈ 200 토큰을 생성합니다.
서버가 수행하는 일
- Prefill: 각 요청마다 짧은 프롬프트를 사용합니다.
- Decode: 5개의 별도 디코드 스트림이 동시에 실행됩니다(또는 함께 배치됩니다).
- 최신 추론 엔진(vLLM, TensorRT‑LLM, TGI 등)은 연속 배치를 사용합니다: 하나의 GPU 포워드 패스가 5개의 요청 각각에 대해 한 토큰씩 동시에 출력할 수 있습니다.
예상 지연 시간 ≈ max(개별 요청 지연 시간) ≈ 200 × (토큰당 생성 시간).
직접 비교
| 접근법 | 총 출력 토큰 수 | 예상 지연 시간 (비교) |
|---|---|---|
| 결합 요청 | ~1000+ | ~1000 디코드 단계(순차) |
| 5개 병렬 요청 | 각각 ~200 | ~200 디코드 단계(병렬) |
이론적 속도 향상: 약 5배(질문 수와 동일).
서버 측에서 병렬 요청이 더 빠른 이유
-
Continuous Batching – GPU는 병렬 행렬 연산에 강점이 있다.
- 5개의 짧은 요청 → 5‑way batched 전방 패스, 단계당 5개의 토큰을 생성.
- 1개의 긴 요청 → 단일 시퀀스 전방 패스, 단계당 1개의 토큰만 생성.
-
Higher GPU Utilization – 많은 짧은 시퀀스를 배치하면 GPU를 지속적으로 활용할 수 있지만, 단일 긴 시퀀스는 병렬 처리 능력을 낭비한다.
-
Prefill vs. Decode –
- Combined: 프리필이 길어지고 디코드도 길어진다.
- Split: 각 요청마다 짧은 프리필; 모든 프리필을 파이프라인하거나 동시에 실행할 수 있으며, 각 디코드도 짧다.
속도 그 이상의 품질 고려사항
| 이슈 | 결합 프롬프트 | 분할 요청 |
|---|---|---|
| 주의 집중 희석 | 관련 없는 맥락이 답변 품질을 낮출 수 있음 (“중간에 빠짐”). | 단일 질문에 전념. |
| 포맷 오류 | 번호 매기기/누락 실수가 더 자주 발생. | 독립된 출력 → 더 깔끔한 포맷. |
| 오류 전파 | Q2의 실수가 Q3‑Q5에 영향을 줄 수 있음(자동 회귀 관성). | 오류가 해당 요청에만 제한됨. |
결합이 여전히 합리적일 수 있는 경우
| 상황 | 이유 |
|---|---|
| 숨겨진 상관관계 | 질문들이 관련되어 있는 경우(예: 같은 보고서의 일부), 공유된 컨텍스트가 일관성을 향상시킬 수 있다. |
| 엄격한 API 호출 제한 | 초당 3회 호출만 가능하다면 묶어야 할 수도 있다. |
| 네트워크 지연이 지배적 | 왕복 지연이 매우 높을 경우(예: > 2 초) 5개의 별도 호출보다 하나의 결합 호출이 더 빠를 수 있다. 최신 API는 보통 100‑300 ms이므로 드물다. |
| 극히 짧은 답변 | 각 답변이 한두 단어에 불과할 때, 프리필 오버헤드가 지배적이다; 단일 요청으로 중복 프리필을 줄일 수 있다. |
빠른 실증 벤치마크 (Async Python)
import asyncio
import time
import aiohttp
API_URL = "https://api.your-llm.com/v1/completions"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
async def ask_single(session, prompt):
start = time.time()
async with session.post(
API_URL,
json={"model": "gpt-4o-mini", "prompt": prompt, "max_tokens": 300},
headers=HEADERS,
) as resp:
await resp.json() # ignore content, just wait for response
return time.time() - start
async def benchmark():
questions = [
"Question 1: …",
"Question 2: …",
"Question 3: …",
"Question 4: …",
"Question 5: …",
]
async with aiohttp.ClientSession() as session:
# ---- Approach A: Combined -------------------------------------------------
combined_prompt = "Please answer each question separately:\n" + "\n".join(questions)
t_combined = await ask_single(session, combined_prompt)
# ---- Approach B: Parallel -------------------------------------------------
tasks = [ask_single(session, q) for q in questions]
t_parallel = max(await asyncio.gather(*tasks))
print(f"Combined request latency : {t_combined:.2f}s")
print(f"Parallel requests latency: {t_parallel:.2f}s")
print(f"Speed‑up factor : {t_combined / t_parallel:.2f}×")
if __name__ == "__main__":
asyncio.run(benchmark())
스크립트를 몇 번 실행하면 일반적으로 독립적인 중간 길이 답변에 대해 병렬 버전이 약 4‑5× 빠른 것을 확인할 수 있습니다.
TL;DR
- Speed: 5개의 병렬 요청은 하나의 결합된 요청보다 약 5배 빠릅니다 (서비스가 이를 배치할 수 있다고 가정할 때).
- Quality: 병렬 요청은 모델의 주의를 집중시켜 질문 간 오염을 방지합니다.
- Exceptions: 질문이 실제로 상호 의존적이거나, 엄격한 속도 제한에 의해 제한을 받거나, 네트워크 지연 시간이 생성 시간을 압도할 때만 결합을 고려하세요.
Bottom line: 질문이 서로 관련이 없을 때는 별도의 동시 요청을 보내세요. 🚀
병렬 vs. 결합 요청
# Parallel execution
start = time.time()
await asyncio.gather(*[ask_single(session, q) for q in questions])
time_parallel = time.time() - start
print(f"Combined: {time_combined:.2f}s")
print(f"Parallel: {time_parallel:.2f}s")
print(f"Speedup: {time_combined / time_parallel:.1f}x")
실제로, 5개의 중간 복잡도 독립 질문은 일반적으로 병렬 요청으로 3–5배 속도 향상을 달성합니다.
비교
| Dimension | Combined request | Split parallel requests |
|---|---|---|
| Generation speed | Slow (sequential output of all answers) | Fast (parallel generation, latency = slowest) |
| GPU utilization | Low (single‑sequence inference) | High (batched parallel inference) |
| Answer quality | May degrade (attention dilution) | Better (isolated context) |
| API calls | 1 | N (one per question) |
| Best for | Rate‑limited / extremely short answers | Independent questions needing detailed answers |
핵심 원리 (한 문장)
LLM의 자기회귀 메커니즘은 출력이 순차적임을 의미한다; 요청을 결합하면 모든 출력을 하나의 직렬 스트림으로 강제하고, 요청을 분할하면 서버 측 병렬성을 활용해 여러 출력을 동시에 생성한다—더 많은 동시 슬롯(공간)을 사용해 시간을 절약하는 고전적인 트레이드오프이다.