파리는 [M] [M] 도시다. [M]은 아직 정하지 못해 가려 둔 자리다. 이 두 칸을 어떻게 채우느냐에 따라 문장은 "유명한 관광 도시"도 되고 "아름다운 역사 도시"도 된다.
확산형 언어모델을 빠르게 돌리는 한 방법은 완성된 문장들을 비교하는 게 아니다. 빈칸을 한 자리씩 더 채운 중간 후보 여럿을 만들어, 그걸 한꺼번에 검사하는 것이다. 파리는 유명한 [M] 도시다처럼 한 칸만 더 열린 후보들을 나란히 놓고 "모델이 정말 이렇게 채울까"를 함께 묻는다.
이렇게 묶어서 검사하면 검사 "횟수"는 준다. 대신 후보가 늘수록 검사 한 번이 무거워진다. 디코딩 비용은 거칠게 "검사 횟수 × 검사 한 번의 비용"으로 나뉘는데, 횟수를 줄인 만큼 회당 비용이 불어나는 맞교환인 셈이다.
SpecFold가 손대는 쪽은 횟수가 아니라 검사 한 번의 무게다. 출발점은 단순한 사실이다. 함께 검사받는 중간 후보들은 한 칸 차이뿐이라 내부 계산이 대부분 겹친다. SpecFold는 그 겹치는 계산을 접어서(fold) 재사용하고, 실제로 달라진 자리만 다시 계산한다. 말은 간단하지만, 어디까지가 "달라진 자리"인지 가려내는 게 이 기법의 거의 전부다. 거기서부터 풀어 보자.
먼저 용어 셋만 손에 쥐자. 토큰은 모델이 글을 처리하는 작은 단위, 블록은 그 토큰을 여러 개 묶은 구간, 숨은 상태는 각 자리의 의미를 담은 모델 내부 표현이다.
요즘 흔한 언어모델은 왼쪽에서 오른쪽으로 토큰을 하나씩 뱉는다. 확산형 언어모델은 다르다. 글을 블록으로 나누고, 한 블록을 통째로 가린([M]) 채 시작한다. 여러 자리를 동시에 예측하고, 그중 확신이 높은 몇 개만 공개한다([M]을 벗긴다). 남은 가린 자리를 다시 채우는 일을 블록이 완성될 때까지 되풀이한다.
한 번에 여러 자리를 채우는 이 병렬성이 속도의 여지를 연다. 문제는 품질이다. 쓸 만한 문장을 얻으려면 같은 블록을 두고 모델을 여러 번 돌려야 하고, 이 반복이 추론 비용의 대부분을 먹는다. 그래서 "반복을 어떻게 줄이나"가 오래된 숙제였다.
추측 디코딩은 미래에 나올 법한 후보를 미리 만들어 한꺼번에 검증해 두는 기법이다. 검증을 통과한 후보가 나중에 실제로 쓰이면, 미리 해 둔 계산을 재사용해 앞으로 할 검사를 건너뛴다.
다시 파리는 [M] [M] 도시다로 가자. 이걸 부모 후보라 하자. 한 자리를 더 공개한 파리는 유명한 [M] 도시다는 자식 후보다. 둘은 새로 열린 "유명한" 한 자리만 다르다. 확산형 모델의 어텐션은 앞뒤를 모두 참고하므로(양방향), 후보 하나는 토큰 하나가 아니라 블록 전체의 상태를 이룬다.
그러니 자식이 부모와 겹치는 부분은 부모의 계산을 물려받으면 될 것 같다. 바로 이 지점이 함정이다. 그 이야기는 조금 뒤에 하고, 먼저 왜 회당 비용이 문제인지부터 보자.
여러 후보 가지를 한 번에 검증하는 방식(멀티브랜치)은 검사 횟수를 잘 줄인다. 그런데 한 번 검사할 때마다 현재 가지와 모든 후보 가지를 하나도 건너뛰지 않고 전부 계산한다. 가지가 늘수록 회당 비용이 그대로 불어난다. 논문은 이 방식의 대표 격인 Spiffy(보정을 쓰는 추측 디코딩 기준선)를 직접 구현해 재 봤는데, 초안 예산이 커질수록 검사 한 번의 평균 지연이 계속 올라갔다(Fig. 3, Nemotron-Labs-Diffusion-3B, H100). SpecFold가 겨냥하는 게 바로 이 회당 비용이다.
관찰에서 출발한다. 자식은 부모와 거의 같고 새로 공개한 소수의 자리에서만 다르다. 저자들이 Fast-dLLM-v2-7B로 GSM8K 샘플을 풀며 후보·층·위치·단계별로 부모와 자식의 숨은 상태 차이(같은 층 입력에서 잰 자식과 부모의 상대 차이 ‖H자식−H부모‖/‖H부모‖)를 들여다보니(Fig. 2), 부모와 자식의 숨은 상태는 대부분 비슷했고 큰 차이는 새로 공개된 위치 근처에만 몰려 있었다.
그럼 겹치는 걸 그대로 물려받으면 끝일까. 아니다. 한 자리의 변화는 그 자리에만 머물지 않는다. 어텐션이 앞뒤를 보니까, 새로 열린 한 자리의 변화가 어텐션을 타고 옆자리로 번진다. 논문은 이 번짐을 "잔차 파문(residual ripple)"이라 부른다(4.4절). 그래서 "부모 계산을 재사용해도 되나"는 가지마다, 자리마다, 층마다 따로 판단해야 한다. 기존 방식은 이 판단을 아예 안 하고 전부 다시 계산했다. SpecFold는 이 판단을 자기 몫으로 삼는다.
문단 교정에 빗대 보자. 한 문단만 고쳤다고 책 전체를 다시 조판하는 건 낭비다. 바뀐 문단만 다시 짜고 나머지는 둔다. 다만 고친 문단이 길어지면 뒤 배치가 밀리듯, 수정의 영향은 주변으로 번진다. 어디까지 밀렸는지를 짚어 딱 그만큼만 다시 손보는 것 — 그게 SpecFold다.
구체적으로 한 자리는 차이 측정 → 부모 결과 재사용 → 자기 차이 보존 → 필요하면 재계산의 길을 따라 움직인다.
‖H자식−H부모‖/‖H부모‖)만 잰다. 이 측정은 값싸다. 무거운 층 계산을 돌려 자식의 출력을 구한 뒤 비교하는 게 아니라, 층 입력에서 두 가지의 숨은 상태 차이를 볼 뿐이다. 비교에 쓰는 값과 생략하는 계산이 여기서 갈린다.파리는 유명한 [M] 도시다의 "유명한"이 만든 작은 차이가 그렇게 보존된다.접기로 아끼려면 고르는 비용 자체가 싸야 한다. 그래서 τ를 넘어 다시 계산할 자리들을 모든 가지에서 긁어모아 하나의 묶음으로 압축한 뒤, 무거운 층을 그 묶음에 대해 한 번만 돌린다. 흩어진 채 여러 번 부르지 않는다. 또 가지들이 공유하는 앞부분 계산 캐시는 가지 수만큼 복사하지 않고 하나를 참조만 바꿔 쓴다. 복사 자체가 비싼 데이터 이동이기 때문이다. 생략해서 아낀 시간이 이렇게 고르고 옮기는 비용을 넘어설 때에야 실제로 빨라진다.
한 가지는 분명히 해 두자. 차이 보존과 자동 해제는 후보 다양성과 출력 품질을 지키려는 장치다. 장치가 있다는 것과 최종 품질이 실제로 유지됐다는 것은 다른 문제다. 실제 정확도는 아래 수치에서 따로 본다.
구현 메모 (구현자용) 각 층은 GPU 커널을 직접 짜는 언어 Triton으로 구현했다. 게이트 커널이 가지 간 숨은 상태 차이를 재고 접힌 자리의 출처를 기록하면, 모든 가지의 재계산 위치를 하나의 묶음(packed batch)으로 압축한다. 출처 해석 커널은 접힘 사슬을 따라가 각 접힌 자리를 실제 부모 상태에 잇는다. 어텐션은 FlashAttention식 스트리밍 커널로 처리하고, 접힌 자리의 어텐션 출력은 그 가지 자신의 키·값으로 다시 맞춘다(보정). 공유 캐시는 복사 없이 참조만 바꾸는 방식(zero-copy strided view)으로 펼치며, 키·값을 여러 쿼리 헤드가 공유하는 어텐션 변형(GQA)도 지원한다.
어떤 재사용인가 트랜스포머 추론에서 중복을 더는 길은 보통 둘이었다. 하나는 시간축 재사용 — 이어지는 단계가 앞 단계 계산을 물려받는다(KV 캐싱이 대표). 다른 하나는 한 번의 실행 안에서 불필요한 토큰·층을 건너뛰는 것이다. SpecFold가 노리는 건 결이 다르다. 같은 검증 단계에서 나란히 평가되는 후보 가지들 "사이"의 가로 방향 중복이다. 대상이 다르니 초안을 만드는 방식은 건드리지 않고 Spiffy 같은 기법 위에 그대로 얹힌다. 경쟁이 아니라 보완이다.
표를 읽기 전에 두 가지만. 속도는 TPS(초당 생성 토큰 수, 높을수록 빠름)로 잰다. 비교 상대는 둘이다. 추측 초안을 아예 안 쓰는 기본 블록 디코딩 Vanilla, 그리고 앞서 나온 Spiffy. 과제는 성격별로 코드 생성(HumanEval, MBPP), 수학 추론(GSM8K, MATH-500), 지시 따르기(IFEval)로 나뉜다.
실험 설계의 핵심은 하나다. Spiffy와 SpecFold에 똑같은 초안 그래프를 물린다. 다만 같은 초안 그래프를 써도 실행 경로나 검사 횟수(NFE)가 완전히 똑같아지지는 않는다. 실제로 표 1의 Fast-dLLM-v2-7B·HumanEval에서 NFE는 Spiffy 117, SpecFold 108로 다르고 정확도도 갈린다. 그래도 둘이 건너뛰는 검사 횟수는 비슷한 수준으로 맞춰지니, 속도 차이의 상당 부분을 회당 비용 절감으로 설명할 수 있다. 회당 비용을 낮추는 것이 SpecFold의 핵심 기여지만, 전체 속도 차이를 그것만으로 완전히 분리했다고 단정할 수는 없다.
먼저 최대 사례. 가장 작은 모델에서 가장 큰 이득이 났다.
| 모델·과제 | Vanilla | Spiffy | SpecFold |
|---|---|---|---|
| Fast-dLLM-v2-1.5B · MBPP | 121 TPS | 148 TPS | 242 TPS |
SpecFold는 Vanilla의 1.99배, Spiffy의 1.64배다. 헤드라인 숫자가 여기서 나온다. 단 이건 전체 최대치지 기대치가 아니다.
전체 경향은 따로 읽어야 한다. 같은 1.5B 모델의 다섯 벤치마크에서 Vanilla 대비 +54~99%로 폭이 넓었다. 모델이 커지면 이득은 빠르게 줄어, NLD(Nemotron-Labs-Diffusion 계열)-14B의 두 과제에서는 Vanilla 대비 +10~24%에 그쳤고 Spiffy와는 98 대 102, 110 대 115 TPS로 사실상 대등했다. 작은 모델일수록, 그리고 더 느린 Vanilla와 견줄수록 배수가 커진다는 뜻이다.
정확도는 성격이 다르니 따로 본다. 각 행이 어떤 기준선과 비교한 값인지 함께 적었다.
| 모델·과제 | 기준선 → SpecFold | 방향 |
|---|---|---|
| NLD-14B · HumanEval | 71.34 → 74.39 | 상승 |
| NLD-14B · MATH | 84.00 → 85.60 | 상승 |
| Fast-dLLM-v2-7B · HumanEval | Vanilla 65.85 · Spiffy 64.63 → 61.59 | 하락 |
| NLD-3B · HumanEval | Spiffy 75.61 → 70.12 | 하락 |
평균으로는 기준선과 비슷하다. 하지만 과제별로 양쪽으로 흔들린다. NLD-14B처럼 오히려 오르는 과제가 있는가 하면, 코드 생성(HumanEval)에서는 7B·3B 모두 눈에 띄게 내려갔다. 평균이 아니라 내 과제가 어느 쪽에 걸리는지가 중요하다.
평가 조건. 두 계열 다섯 모델(Fast-dLLM-v2 1.5B·7B, NLD 3B·8B·14B). 모두 제로샷, 별도 언급 없으면 H100 80GB 한 장 기준. 코드 과제는 EvalPlus, 나머지는 LM-Eval로 채점. Spiffy의 보정 샘플은 MBPP·MATH 훈련 분할에서 가져와 평가 데이터와 분리했다.
결론부터. 생략해서 아낀 시간이 고르고(선별) 맞추고(보정) 옮기는(데이터 이동) 비용을 넘어설 때만 실제로 빨라진다.
관측된 사실은 둘이다. 하나, 모델이 커질수록(NLD-14B) 향상폭이 수 퍼센트로 수렴했다. 둘, 코드 생성 일부 과제에서 정확도가 내려갔다.
이유는 가설로 둔다. 중요한 건 둘 다 측정으로 가를 수 있다는 점이다.
곧 실행 중 재계산 비율과 선별·보정 시간을 따로 재면, 느린 이유가 "아끼는 쪽이 작아서"인지 "비용 쪽이 커서"인지가 그 측정에서 갈린다.
도입을 고려한다면 조건이 붙는다. 이득이 모델 규모에 민감하고 코드 생성에서 실제로 정확도가 내려간 과제가 있으니, 과제별 벤치마크는 선택이 아니다. 재계산 임계값 τ가 속도와 정확도의 맞교환을 쥐고 있다는 점도 기억해야 한다. τ를 낮게 잡으면 덜 접어 안전하지만 느리고, 높이면 그 반대다. 운영 과제마다 이 곡선을 그려 안전 지점을 찾아야 한다. 끝으로 이 가속은 커스텀 커널에 묶여 있어, 평가된 두 계열(Fast-dLLM-v2, NLD) 밖의 모델이나 다른 어텐션 변형이면 커널 호환부터 확인해야 한다.
온틀 스택은 확산형 모델이 아니고, SpecFold의 커널을 가져다 쓸 수도 없다. 이 논문은 적용할 코드가 아니라 던질 질문 하나로 읽는 게 맞다. 우리가 후보를 여러 개 만들 때, 그 후보들 사이에 실제로 겹치는 작업이 얼마나 되나.
한 장면으로 보자(지금 이렇게 돈다는 뜻이 아니라, 구조를 보이려는 가상의 예다). 온틀 PM 에이전트가 주간 계획을 세운다. 지난주 진행·밀린 task·팀 리소스라는 공통 맥락을 깔고, 그 위에서 공격적 안·안전한 안·리소스 최소 안 셋을 뽑아 비교한 뒤 하나를 고른다. 세 안은 같은 맥락을 똑같이 참조한다. 하지만 순진하게 짜면 안마다 그 맥락을 처음부터 다시 모델에 태운다. 참조는 공유하는데 계산은 공유하지 못하는 것이다. 후보가 늘수록 토큰과 지연이 그만큼 분다.
SpecFold가 주는 힌트는 순서다. 공유하는 계산을 한 번만 치르고 나머지 안이 그 결과를 물려받게 한다. 그리고 안마다 달라지는 작은 부분이 결과에 제대로 반영됐는지 확인한다. 이걸 빠뜨리면 세 안이 서로 닮아, 애초에 여러 안을 만든 이유가 사라진다. 잔차 파문을 가지별로 들고 가던 그 고민과 정확히 같다.
효과가 있었는지는 같은 조건에서 넷을 재면 갈린다. 안 하나당 지연, 안 하나당 비용, 후보 다양성, 그리고 최종 선택안의 품질. 공유 계산을 접어 앞의 둘이 줄면서 뒤의 둘이 유지되면, 그 팬아웃(한 입력에서 여러 갈래로 뻗는 구조)은 접을 가치가 있다. 특히 넷째, 최종 품질은 이 논문에서도 과제에 따라 흔들린 지표다. 속도만 보고 품질 측정을 건너뛰면 안 된다는 교훈이 여기서도 그대로다.
저자들은 직렬 실행(serialized-execution)으로 점검해 구현 자체는 정확하다고 밝힌다(부록 C). 흔들림의 일부는 계산 순서에서 생기는 미세한 수치 차이, 곧 부동소수점 잡음으로 설명한다. 다만 어디까지나 "일부"라, 코드 생성 과제의 하락을 전부 잡음으로 돌릴 수는 없다. 과제별 실측이 필요한 이유다.
논문은 샘플 하나를 풀며 모델을 통과시키는 횟수(NFE)도 함께 잰다. 같은 초안 그래프를 물려도 NFE가 완전히 같아지지는 않지만(표 1의 7B·HumanEval은 Spiffy 117 대 SpecFold 108) 비슷한 수준으로 맞춰지는데, 그런데도 TPS가 벌어진다면 그 차이의 상당 부분은 검사 한 번의 비용에서 왔다고 읽을 수 있다. 회당 비용을 떼어 보려던 설계와 맞물린다.
초안 예산과 드래프팅 전략을 바꿔 가며 재계산 비율과 처리량이 어떻게 움직이는지는 부록 D에서 더 따라갈 수 있다. 접을 거리가 줄면 재계산이 늘어 이득이 깎인다는 본문의 가설을 가늠해 볼 대목이다.