아래는 Streaming ASR 서버를 운영(production)한다는 관점에서의 latency 설계 프레임워크입니다. 목표는 단순히 “빠르게”가 아니라, p95 SLA(당신의 경우 500ms)를 안정적으로 지키는 시스템을 만드는 것입니다. A10, 동시 10명, client 20ms 업로드를 전제로 작성합니다.
1) Latency를 “예산”으로 쪼개서 설계한다
Streaming ASR의 end-to-end latency는 보통 아래 합으로 모델링합니다.
[
L_{e2e} = L_{buffer} + L_{batch} + L_{queue} + L_{gpu} + L_{post} + L_{net}
]
각 항목의 의미:
- (L_{buffer}): hop을 만들기 위한 입력 버퍼링(서버 집계)
- (L_{batch}): micro-batching window로 인한 추가 대기
- (L_{queue}): GPU가 바쁠 때 작업이 큐에서 기다린 시간 (tail의 주범)
- (L_{gpu}): 실제 모델 inference 시간
- (L_{post}): 후처리(텍스트 정리, punctuation, endpointing)
- (L_{net}): 전송 및 프레임 처리(WebSocket/gRPC)
p95=500ms 예산 예시(권장)
운영에서 통제 가능한 항목과 통제 불가능(환경/네트워크) 항목을 분리합니다.
(L_{buffer}): 160ms (hop=160ms로 고정)
(L_{batch}): ≤ 20ms (micro-batching)
(L_{net}+L_{post}): 50ms (여유 포함)
그러면 남는 예산:
- (500 - (160 + 20 + 50) = 270ms)
즉, p95에서 (L_{queue}+L_{gpu})를 270ms 이내로 유지해야 합니다.
2) p95를 망치는 1순위: Queueing(혼잡)
현장에서 p95가 튀는 가장 흔한 원인은 GPU 자체가 느린 게 아니라,
- “GPU가 잠깐 밀릴 때”
- “작업이 큐에서 쌓이며”
- tail이 폭발하는 현상입니다.
따라서 운영 설계의 핵심은:
GPU compute 최적화도 중요하지만, queueing을 제어하는 정책이 더 중요하다.
3) 설계 레버 1: Hop과 Window를 명확히 분리
추천(동시 10명, A10, p95 500ms)
- hop = 160ms (client 20ms × 8)
- rolling window = 0.96~1.28s
왜 이렇게 분리하나:
- hop은 사용자 업데이트 cadence(UX)를 결정
- window는 GPU compute량을 결정
혼잡할 때는 hop을 건드리기보다 window를 줄이는 것이 p95 방어에 유리합니다.
# overload 시 window 축소 예시
if queue_wait_p95_ms > 150:
window_secs = 0.96
else:
window_secs = 1.28
4) 설계 레버 2: Micro-batching은 “p95 보호형”으로
동시 10명에서는 평균 batch size가 크게 안 커집니다.
그렇다면 micro-batching의 목표는 throughput이 아니라:
- GPU 호출 수를 줄이되
- 추가 대기 시간을 상한으로 묶는 것
권장:
max_latency_ms = 15~20msmax_batch_size = 4~8in_flight = 1(단일 GPU stream)
batcher = DynamicBatcher(
max_latency_ms=20, # p95에 들어가는 상한
max_batch_size=8,
flush_interval_ms=2,
)
5) 설계 레버 3: “2단계 타이밍”을 운영 규칙으로 고정
Streaming에서 운영 규칙을 단순화하는 게 중요합니다.
Stage A: ingress (20ms)
- 클라이언트가 보내는 단위
- 서버는 여기서 “수신”만 한다
Stage B: inference trigger (160ms)
- 서버가 GPU를 부르는 단위
- hop마다 1회 job 생성
이 2단계 타이밍이 무너지면:
- CPU도 불안정해지고
- batching도 깨지고
- tail이 흔들립니다.
6) p95 방어의 핵심: Overload Control(Degradation Policy)
운영에서 반드시 넣어야 하는 것은 “품질을 조금 희생해서 SLA를 지키는 장치”입니다.
추천 degradation 순서(실전)
Emit throttling: partial 업데이트 빈도 줄이기
- 예: 160ms → 320ms로 partial emit만 줄임(호출 자체는 유지 가능)
Window 축소: 1.28s → 0.96s
Decoder 비용 축소: beam 4 → 2 → greedy
(최후) 세션 admission control: 신규 세션 거절/대기
if queue_wait_p95_ms > 200:
emit_interval_ms = 320
window_secs = 0.96
beam = 2
elif queue_wait_p95_ms > 120:
window_secs = 0.96
이 정책의 핵심은:
- p95가 깨지기 전에 미리 조치 (proactive)
- “어떤 값이 기준인지”를 명확히 로그로 남김
7) “측정” 없이는 설계가 아니다: 운영 메트릭 정의
p95를 지키려면 최소 다음 지표는 분리해서 봐야 합니다.
Required metrics
hop_buffer_ms(고정에 가까움)batch_wait_ms(micro-batching 대기)queue_wait_ms(GPU 앞에서의 대기)gpu_infer_ms(실행 시간)e2e_latency_ms(총합)batch_size분포
그리고 p50/p95를 각각 기록합니다.
@dataclass
class LatencyBreakdown:
hop_buffer_ms: int
batch_wait_ms: int
queue_wait_ms: int
gpu_infer_ms: int
post_ms: int
net_ms: int
운영에서 중요한 건 “총 p95”가 아니라,
- 어느 항목이 p95를 만드는지를 매일 확인하는 것입니다.
8) 동시 10명 기준, 현실적인 기대치
잘 설계된 경우의 감각적 범위:
- p50 e2e: ~220–320ms
- p95 e2e: ~350–500ms
- batch size 평균: 1–2 (가끔 3–4)
p95가 튀기 시작하면 대부분:
queue_wait_ms가 먼저 오릅니다.
이걸 감지해서 window/beam을 줄이는 게 운영의 핵심입니다.
9) 운영 관점 설계 요약
- p95 예산을 분해하고, 각 항목에 상한을 둔다
- hop과 window를 분리하고, 혼잡 시 window부터 줄인다
- micro-batching은 throughput보다 p95 보호형 상한이 우선
- queueing이 tail의 주범이므로 degradation 정책을 필수로 둔다
- latency breakdown을 항목별로 계측하고, p95 원인을 추적한다
원하시면, 위 설계를 바로 코드로 연결해서:
- FastAPI(WebSocket) 기준으로 Latency breakdown 로깅 + p95 계산 + 자동 degradation까지 포함한 “운영용 샘플 서버” 코드를 작성해 드리겠습니다.