s₁Problem — 무엇이 문제인가
"GPU 8장을 한 사용자 요청 하나에 몰아줘도 빨라지지 않는다." 그 이유는 ① speculative decoding의 draft 단계가 항상 critical path(가장 긴 실행 경로) 위에 있고, ② 작은 batch에서는 tensor parallelism을 늘려도 GPU 간 통신이 발목을 잡기 때문이다.
"the draft model remains on the critical path, and communication overhead limits scaling across GPUs"— Abstract
"draft and target models have very different GPU-scaling curves"— §1, Table 1 설명
먼저 용어부터 — 이 논문을 읽기 위한 최소 배경
[논문에 없음] Decoding (디코딩): LLM이 답을 만들 때 토큰(단어 조각)을 한 번에 하나씩 생성하는 과정. 70B 모델이면 토큰 하나마다 수백 GB 무게를 읽어야 해서 느리다. 단일 요청에서는 "초당 몇 토큰"이 곧 체감 속도다.
[논문에 없음] Speculative decoding (추측 디코딩): 작은 draft 모델이 먼저 "다음에 올 것 같은 토큰"을 여러 개 추측하고, 큰 target 모델이 그것들을 한 번에 묶어서 맞는지 검증(verify)하는 기법. 큰 모델을 한 번 돌려서 여러 토큰을 얻으니 빨라진다. (논문 §2.3에서 정의)
[논문에 없음] Tensor parallelism (TP, 텐서 병렬화): 모델의 큰 행렬(가중치)을 GPU 여러 장에 쪼개 놓고 각자 부분 계산 후, all-reduce(모든 GPU의 부분합을 합치는 통신)로 합치는 방식. GPU가 많을수록 계산은 빨라지지만 통신 횟수는 그대로라서 작은 모델·작은 batch에서는 오히려 통신이 병목이 된다.
[논문에 없음] KV cache: Transformer가 앞에서 본 토큰들의 중간 계산 결과(Key/Value)를 저장해 두는 메모리. 이게 있어야 매 토큰마다 앞 문맥을 다시 계산하지 않는다. 추측이 틀려서 버려지는 토큰의 KV는 낭비가 된다.
문제가 드러나는 근거 — Table 1
s₂기존 접근법 — 무엇을 풀었고 무엇을 못 풀었나
논문 §2.4와 Table 2를 기준으로 정리. 세 가지 축(독립 확장성 / KV 일관성 / 작은 batch kernel)을 동시에 만족하는 것은 SwiftSpec뿐이라는 것이 저자의 포지셔닝이다.
| 접근법 | 무엇을 쪼갰나 / 해결했나 | 대가 · 못 푼 것 |
|---|---|---|
| SpecInfer / EAGLE / SpecExec tree-based spec | draft가 트리 형태로 여러 후보를 제안 → acceptance length(한 번 검증에서 받아들여지는 토큰 수)↑ §2.3 | draft→verify가 직렬. 그래서 draft/target 둘 다 모든 GPU에 펼쳐야 하고, 작은 draft는 TP로 얻는 게 없음 §2.4 |
| PipeInfer | draft/target을 다른 GPU에 두고 pipeline으로 비동기 실행, 트리 사용 Table 2 | target이 트리 일부를 기각하면 이후 draft 트리 전부 폐기 → KV 낭비, 실효 이용률 낮음. GPU 배분 유연성 없음 §2.4 |
| PEARL | draft/target 분리 compute + KV 재사용, 오예측 시 re-rooting Table 2 | 시퀀스 기반(트리 아님) → 추측 품질 낮음. GPU 배분 고정 Table 2 |
| vLLM / SGLang / TRT-LLM SOTA 엔진 | NCCL-LL all-reduce로 소량 통신 비용↓, 일부 kernel fusion(SwiGLU 부분) §2.2, §2.4 | 통신을 계산과 fuse하지 않음(별도 kernel, 별도 barrier). draft/target이 같은 GPU에 co-located Table 2, §4.6 |
| FlashAttention / FlashInfer | attention kernel 자체 최적화 | batch ≥ 32 등 큰 batch 기준 설계. 작은 head 수·batch 8에서 비효율 §2.4, §4.4 |
| SwiftSpec (본 논문) | ① draft/target을 서로 다른 GPU 그룹에 두고 비동기 트리 생성, GPU 수 임의 배분 ② 오예측이 나도 유효 subtree KV 100% 재사용(evolving tree cache) ③ GEMM+all-reduce, attention을 NCCL-LL과 하나의 kernel로 fuse §3 | acceptance length 약 9% 감소(draft가 검증 결과를 모르고 앞서가므로), 큰 모델 SwiGLU는 기존 kernel보다 느림, 큰 batch에는 부적합 §4.3.1, §4.4, §5 |
s₃논문의 해결 방식 — 세 가지 설계 원칙
P1 Disaggregate(쪼개라): draft와 target을 다른 GPU에. P2 Evolving tree cache: 틀린 추측이 있어도 KV를 버리지 마라. P3 Fuse(합쳐라): 통신을 계산 kernel 안에 집어넣어라. §3 P1~P3
3.1 Disaggregated tree generation — 추측과 검증을 동시에
핵심 파라미터 §3.1.1: bs = target이 한 번에 검증하는 토큰 수(= target batch), w = 한 번 확장에서 펼치는 잎 노드 수(= draft batch), d = 한 iteration 안의 트리 확장 횟수. 논문은 bs=8, w=8, d = ⌊t_target / t_draft⌋ 또는 +1로 설정(draft 확장 시간 총합 ≈ target 검증 시간이 되도록) §3.1.3.
Draft worker의 한 iteration Algorithm 1, §3.1.1:
- Expand: 가장 확률 높은 잎
w개를d번 확장 (target이 검증하는 동안) - Synchronize: target에서 검증된 토큰 받기
- Re-root: 검증된 경로를 따라 내려가 마지막 검증 토큰을 새 root로 삼고 KV cache 정리
- Select: 노드가 부족하면 더 키운 뒤, 크기
bs의 가장 유망한 subtree 선택 - Send: target에 전송
t1,t2,t3,t5를 보내고, target이 검증하는 동안 draft는 t7~t12 6개를 더 키운다. target 답이 (t1,t3,t6) → draft는 t6을 새 root로 하고 이미 키워둔 t9,t10,t11을 바로 다음 입력으로 보낸다. Iter 2에서 target이 t16을 냈는데 트리에 없으면 → t16에서 새 트리 시작.
메시지: "검증을 기다리는 동안 키워둔 노드"가 다음 iteration의 입력이 되므로 draft 시간이 critical path에서 사라진다. 단, 트리 밖 토큰이 나오면 키워둔 것이 무용지물이 되는 위험도 그림에 포함돼 있다.
이 그림이 말하는 것: 직렬에서는 "dft 3칸"이 매 iteration 비용으로 쌓이지만, SwiftSpec은 그 3칸을 target 검증 시간 뒤에 숨긴다. 그래서 토큰 생성 주기 = verify 시간 하나로 줄어든다.
기술적 디테일 §3.1.3
- Maximum-likelihood 확장: 각 노드 값 = log softmax 확률, root부터의 합을 가중치로 priority queue에서 상위 k개 잎을
O(k log s)로 뽑음. - GPU 배분: k장 중 target에 x장, draft에 k−x장. 서빙 전 프로파일링으로 가장 빠른 x 선택. target이 강할수록 최적 x는 작아진다(draft에 더 줘야 함).
- Non-square mask: draft 쪽 attention은 "새로 펼칠 잎 4개 × 기존 트리+prefix 10개" 같은 정사각형이 아닌 mask가 필요(Figure 3). 기존 kernel(FlashAttention 등)은 정사각형만 지원해서 직접 구현.
3.2 Evolving Tree Cache — 틀려도 버리지 않는 KV cache
불변식(Invariant) §3.2: KV cache = [prefix cache: 검증된 토큰들, 연속 배치] + [tree cache: 남은 draft 트리 노드들, 그 뒤에 연속 배치]. 이 배치 덕분에 re-root 후에도 재계산이 없다.
갱신 절차: ① 검증 토큰 경로로 트리를 걸어 내려가 마지막 토큰에서 re-root ② 그 노드가 트리에 있으면 유효 subtree의 KV만 남기고 재배치, 없으면 새 트리 시작.
"even when some of the predicted tokens ... are wrong, we can still reuse all the computed KV states"— §3.2
t1,t3,t7,t10(초록 prefix) 뒤에 트리 t11~t18(노랑). target이 t12,t15를 추가 확정 → t12,t15를 prefix로 승격하고, t15의 자식 t17,t18은 tree cache 앞으로 당김. t11,t13,t14,t16은 폐기.
메시지: 오예측된 가지(t11 쪽)가 있어도 살아남은 subtree(t17,t18)의 KV는 그대로 재사용된다 = "zero waste" 주장의 실체.
이 그림이 말하는 것: re-root는 "새로 계산"이 아니라 "KV 블록을 앞으로 당기는 정리"다. 그래서 draft가 앞서 달려도 낭비 없이 따라잡는다.
수치 워크스루 — 정상 케이스 (bs=4, w=2, d=3) Figure 2 기반 재구성
| step | 이벤트 | target 입력 (bs=4) | target 출력 | root | tree 크기(보낼 때) | 재사용된 노드 |
|---|---|---|---|---|---|---|
| 0 | 초기 트리 t1~t6 | t1,t2,t3,t5 | — | t1 | 6 | — |
| 1 | target 검증 中, draft 6개 확장(t7~t12) | (검증 중) | t1,t3,t6 | t1→t6 | 12 | t9,t10,t11,t12 (t6 subtree) |
| 2 | re-root t6, 즉시 다음 전송 | t6,t9,t10,t11 | t6,t9,t16 | t6→t16 | 10 | 0 — t16이 트리에 없음 → 새 트리 |
| 3 | t16에서 t17~t21 확장 | t16,t17,t18,t20 | t16,t18,t21 | t16→t21 | 11 | t28,t29 등 t21 subtree |
| 4 | …반복 | t21,… | … | … | … | … |
step 2처럼 target이 트리 밖 토큰을 내면 키워둔 노드가 전부 낭비된다. 이것이 §4.3.1에서 acceptance length가 직렬 대비 9% 낮아지는 원인이다(붕괴 케이스는 s₈에서).
3.3 Latency-optimized Kernels — 통신을 계산 안에 숨기기
[논문에 없음] Kernel: GPU에서 실행되는 함수 한 덩어리. kernel을 하나 띄울 때마다 launch 비용과 앞 kernel이 끝나길 기다리는 barrier(동기화 장벽)가 든다. 큰 batch에선 무시되지만 batch 8에선 이 고정비가 지배적이다(Table 3).
[논문에 없음] GEMM = 행렬곱, SwiGLU = Llama 계열 MLP의 활성화 블록(σ(xW+b)⊙(xV+c), 행렬곱 2개 + sigmoid + 곱).
NCCL LL / LL128 프로토콜 §3.3: 데이터와 flag를 한 묶음으로 쓰고(storeLL), 읽는 쪽은 flag가 기대값(예: 지난번 x → 이번 x+1)이 될 때까지 polling(loadLL). flag 자체가 "준비됨" 신호이므로 별도 barrier가 필요 없다. LL: 4B 데이터+4B flag(대역폭 50% 낭비, 유연), LL128: 120B+8B(5% 낭비, 128B 연속 메모리 필요).
세 가지 fused kernel §3.3:
- Fused GEMM + all-reduce: layer당 2개 all-reduce(attention 뒤, MLP 뒤)를 각각 앞 GEMM에 fuse.
- Masked attention: position embedding(RoPE) fuse + 한 head를 여러 thread block으로 쪼갠 뒤 NCCL LL로 합산(FlashAttention의 split-kv + combine 두 kernel을 하나로).
- Fused SwiGLU: 같은 tile을 두 행렬곱이 공유(입력을 HBM에서 한 번만 읽음) → 바로 sigmoid·곱 → 저장.
구현 §3.4: CUDA/C++ ~3K LOC(kernel) + C++/Python ~4K LOC(runtime). CUTLASS 기반. CUDAGraph용으로 tree mask를 (w, maxlen)로 padding. GPU 2:6 같은 비(非) 2의 거듭제곱 분할은 행렬 차원·head 수를 zero-pad.
s₄Figure별 주장과 근거
| Figure | 설득하려는 주장 | 근거로 쓰는 것 |
|---|---|---|
| Fig 1 | 기존 방식은 draft가 critical path에 있고 TP 통신에 묶인다; SwiftSpec은 둘을 분리·비동기화 | GPU별 타임라인 개념도 3종 비교 |
| Table 1 | draft와 target은 GPU scaling 곡선이 다르다 → 분리해야 | vLLM TP 1/2/4/8 inference 시간 (1B는 2장 이후 악화, 70B는 계속 개선) |
| Table 2 | 3가지 요구(독립 확장·KV 일관성·작은 batch kernel)를 다 만족하는 건 SwiftSpec뿐 | PipeInfer/PEARL/SOTA 엔진 feature 체크표 |
| Table 3 | batch 8에서 GPU는 계산/대역폭을 거의 못 쓰고 대기 중 | 70B layer kernel별 util (all-reduce·attn 대역폭 <10%) |
| Fig 2 | 검증 중 키운 노드를 re-root 후 바로 쓸 수 있다 | 3 iteration 트리 진화 예시(bs=4,d=3,w=2) |
| Fig 3 | 트리를 이어 키우려면 비정사각 mask가 필요 → 자체 attention kernel 정당화 | 4×10 mask 예시 |
| Fig 4 | 오예측이 있어도 유효 subtree KV는 100% 재사용 | prefix/tree cache 재배치 예시 |
| Fig 5 | GEMM과 all-reduce를 한 kernel에서 barrier 없이 처리 가능 | thread block 3단계 데이터 흐름 |
| Fig 6 | 5개 모델군 전부에서 최고 baseline 대비 평균 1.75× | end-to-end tokens/s 막대그래프 |
| Fig 7 | 평균만이 아니라 tail(p95)까지 빠르다 | Llama-70B 480 query CDF |
| Fig 8 | 분리(1.43~1.50×)와 kernel(1.16~1.21×) 둘 다 기여 | ablation 4종 비교 |
| Table 7 | 분리의 대가는 acceptance length −9%뿐, 대신 draft GPU 79% 절약 | Qwen2-72B 병렬/직렬 수치 |
| Table 8 | 작은 모델 kernel에서 vLLM/TRT/FA/FI보다 빠름(큰 모델 SwiGLU 제외) | kernel 단위 μs 비교 |
| Fig 9 / Table 9 | 최적 GPU 분할은 모델쌍마다 다르다(4:4 또는 6:2) | target TP 2/4/6별 tokens/s |
| Table 10 | 더 약한 H800으로 NVIDIA Medusa(H200)보다 1.3× 빠르다 | 347 vs 268 tok/s |
| Table 11 | 특수 훈련된 EAGLE3보다 범용 draft + 분리가 더 빠르다 | 369 vs 256 tok/s(평균), 268 vs 186(p95) |
s₅실험 결과 분석
셋업 §4.1: 8×H800 80GB NVLink 1노드. 모델쌍 5개(Llama3-70B/3.2-3B, DS-Coder-33B/1.3B, Qwen2-72B/1.5B, R1-Qwen-32B/1.5B, R1-Llama-70B/8B), 6개 데이터셋 × 80 query = 480개. 모든 시스템에 동일한 int4 AWQ 양자화, greedy(temperature 0). Baseline: vLLM 0.7.2, SGLang 0.4.4, TRT-LLM 0.17, SpecExec(저자 재구현).
s₆Conclusion
논문의 결론 §6: 세 아이디어 — (1) draft/target GPU 그룹 분리, (2) KV 일관성을 보장하며 재사용을 극대화하는 evolving tree cache, (3) NCCL-LL을 GEMM·attention에 fuse하고 SwiGLU를 단일 kernel로 만든 latency-optimized kernel — 를 함께 적용하면 단일 요청 LLM 지연을 크게 줄일 수 있다. 5개 모델군에서 최강 오픈소스 대비 1.75×, Llama-3-70B 347 tok/s(8×H800)로 H200의 Medusa를 넘었다. 각 아이디어는 정밀도·모델 구조와 직교하므로 다른 precision·모델·더 많은 GPU로 확장될 것이라 기대.
Future work §5: 저사양 GPU(PCIe 연결)에서 TP 없이 분리만 적용, EAGLE3 draft 추가 비교, prefill/decode 분리·compute 분리와의 결합, 큰 batch 경로와 자동 전환하는 runtime. 코드는 공개 예정(github.com/ByteDance-Seed/SwiftSpec).
⏱ 30초 요약
| Problem | 단일 요청에 GPU 8장을 줘도 draft 단계가 직렬로 끼고, 작은 batch에서 TP 통신·kernel 고정비가 지배해 빨라지지 않음. |
|---|---|
| Idea | draft와 target을 다른 GPU 그룹에서 동시에 돌리고, 틀린 추측이 나도 subtree KV를 재배치해 재사용하며, 통신을 계산 kernel 안에 flag 기반으로 fuse한다. |
| Cost | acceptance length ≈ −9%, 7K LOC 자체 kernel/runtime, 서빙 전 GPU 분할 프로파일링, 큰 모델 SwiGLU는 기존보다 느림. |
| 전제조건 | batch 1 단일 요청 · NVLink 단일 노드 · int4 AWQ · 범용(out-of-box) draft 모델 존재 · greedy decoding. |
| 가장 강한 근거 | Table 7 — draft GPU 8→2장으로 줄여도 draft가 더 빨라지고(3.73→3.25ms), target 8→6장은 거의 손해 없음(10.34→10.48ms). 분리의 논리가 수치로 닫힌다. |
s₇Appendix급 주요 내용 — 놓치기 쉬운 부분
이 논문은 별도 Appendix가 없다. 대신 본문 곳곳에 흩어진 설계 선택의 근거(§4.5)와 Limitation(§5)을 모았다.
Limitations §5
| 항목 | 내용 | 저자의 입장 |
|---|---|---|
| 저사양 GPU | 평가는 8×H800 NVLink뿐. PCIe 연결 소비자 GPU에서는 TP 자체가 비효율. | 분리+tree cache는 TP 없이도 적용 가능하다고 주장(미검증). |
| EAGLE 계열 draft | EAGLE2/3는 target 내부 feature를 입력으로 써서 분리 불가. 별도 훈련도 필요. | 범용 draft + 분리가 현재 테스트에선 더 빠름(Table 11). 더 많은 EAGLE3 모델 공개 후 재비교. |
| 다른 disaggregation과의 결합 | prefill/decode 분리(DistServe 등), 모델 내부 compute 분리(MegaScale-Infer)와는 직교. | 결합 시 추가 이득 기대(미실험). |
| High-throughput 서빙 | 큰 batch에선 통신·launch 비용이 분할상환되어 FlashInfer 같은 throughput kernel이 더 빠름(Table 8 70B SwiGLU). | batch 크기에 따라 경로를 전환하는 runtime을 future work로. |
설계 선택 근거 FAQ §4.5, §3.1.3
r = ⌊t_target / t_draft⌋를 프로파일링으로 구하고 d=r과 d=r+1 둘 다 돌려 빠른 쪽 채택. 목적은 draft 확장 총 시간 ≈ target 검증 시간(둘이 동시에 끝나야 어느 쪽도 놀지 않음). §3.1.3, §4.5s₈비판 / 한계 분석
아래는 논문이 명시적으로 다루지 않은 층위를 중심으로 정리했다. 별도 표기 없는 항목은 모두 [논문에 없음](요약자 분석)이다.
1) 분리가 깨지는 조건 — acceptance가 낮을 때
분리의 이득은 "target이 검증하는 동안 draft가 유용한 노드를 키운다"에 달려 있다. target이 자주 트리 밖 토큰을 내면(Figure 2의 t16 상황) 키운 노드는 전부 버려지고, draft는 사실상 검증을 기다린 뒤 새 트리를 처음부터 키워야 한다 → 직렬과 같아진다.
이 그림이 말하는 것: 우측 상황이 반복되면 분리의 이득은 사라지고 GPU 2장을 draft에 빼앗긴 만큼 target만 느려진다. 논문의 6개 데이터셋은 모두 예측이 비교적 쉬운 도메인이다.
수치 워크스루 — 붕괴 케이스 (bs=4, w=2, d=3, 트리 밖 토큰 연속)
| step | 이벤트 | target 출력 | 재사용 노드 | draft가 추가로 해야 할 일 | target 대기 |
|---|---|---|---|---|---|
| 1 | t1 트리(6노드) 전송, 검증 중 6개 확장 | t1, t50(트리 밖) | 0 | t50 root로 bs=4까지 확장(2회) | ≈2·t_draft |
| 2 | t50 트리 전송, 검증 중 6개 확장 | t50, t77(트리 밖) | 0 | t77 root로 재확장(2회) | ≈2·t_draft |
| 3 | 반복 | … | 0 | … | ≈2·t_draft |
| 결과: 검증당 토큰 1개, 주기 ≈ t_target + 2·t_draft. 직렬보다 나을 것이 없고 GPU 2장은 target에서 빠져 있다. 정상 케이스(s₃ 표)에서는 같은 3 step 동안 토큰 3·3=9개, 주기 ≈ t_target. | |||||
2) 비교 조건의 공정성
- Baseline 재구현: 가장 강한 비교 대상 SpecExec는 저자 구현이며 저자의 attention kernel을 쓴다(§4.3). 원저자 구현 대비 어떤지는 알 수 없다.
- SGLang 3/5 모델군 auto-regressive(§4.1): 2.23×의 상당 부분은 "spec decoding 유무" 차이.
- Medusa 1.3×는 NVIDIA 블로그 수치와의 교차 비교 — 데이터셋·양자화·프롬프트 길이 미통제.
- greedy만(temperature 0, §4.1). sampling 시 acceptance가 낮아져 분리 이득이 줄 가능성.
3) 다루지 않은 층위
- Prefill: 논문은 decode 전용. 긴 prompt의 첫 토큰 지연(TTFT)과 draft/target KV 초기화 비용은 측정 없음.
- Multi-turn / 긴 context: attention 벤치는 context 500·1000만(Table 8). 수만 토큰 context에서 tree cache 재배치(메모리 복사) 비용 미평가.
- 멀티 노드: "NVLink/cross-network" 언급(§3.1)만 있고 실험은 단일 노드.
- Batch>1로의 degrade: 2~4개 요청이 동시에 오면 어떤 경로로 가는지 없음(§5에서 future work).
- Kernel 이식성: int4 AWQ + Hopper + CUTLASS 특화 3K LOC. 다른 precision·아키텍처는 "적용 가능"이라 말만 함.
유효 적용 범위
| 조건 | SwiftSpec 이득 | 근거 |
|---|---|---|
| 단일 요청, NVLink 8-GPU 노드, 예측 쉬운 도메인(코드·수학·요약) | 매우 큼 (1.5~2.5×) | Fig 6 DS-Coder 470 tok/s, Table 7 |
| 단일 요청, 일반 대화·QA | 큼 (≈1.5~1.7×) | Fig 7 p95 1.7×, Table 7 ALP/QA |
| draft가 target 대비 매우 약함 (acceptance <2) | 불확실 | s₈-1 붕괴 분석; 논문 미실험 [논문에 없음] |
| EAGLE3 등 target-feature 의존 draft를 쓰고 싶을 때 | 적용 불가 | §5, §4.7 |
| PCIe 소비자 GPU | 이론상 가능, 미검증 | §5 |
| batch ≥ 32 throughput 서빙 | 부적합 (throughput kernel이 더 빠름) | §5, Table 8 70B SwiGLU |
| sampling(temperature>0) | 미검증 | §4.1 greedy만 [논문에 없음] |