
Apple Neural Engine에서 LLM을 출력 변화 없이 가속화하기 — Core ML 투기적 디코딩(Speculative
요약
Apple Neural Engine(ANE)에서 Gemma 4 E2B 모델을 가속화하기 위한 로스리스 투기적 디코딩(Speculative Decoding) 구현 사례를 소개합니다. 드래프트 모델 없이 프롬프트 룩업을 활용하며, KV 캐시의 디스크 영속화를 통해 출력 변화 없이 1.6~2.0배의 속도 향상을 달성했습니다.
핵심 포인트
- 드래프트 모델 없이 프롬프트 룩업을 이용한 투기적 디코딩 구현
- 출력값이 1바이트도 변하지 않는 로스리스(Lossless) 가속화 검증
- KV 캐시의 디스크 영속화를 통한 프로세스 간 문맥 복원 가능
- iPhone 17 Pro 기준 약 1.6~2.0배의 실측 가속 성능 확인

Gemma 4 E2B를 Apple Neural Engine(이하 ANE)에서 실행하는 Core ML 번들에, 드래프트 모델(Draft Model)이 필요 없는 로스리스 투기적 디코딩(Speculative Decoding, 출력을 1바이트도 바꾸지 않는 가속화)과 KV 캐시의 디스크 영속화를 구현하여 공개했습니다. multifunction 함수 구성, 호스트 측 KV, 폭(width) 4 검증이라는 구현의 요점과 실측값에 더해
- ANE에 배치한 검증 헤드(Verification Head)가 출력을 변경함
- 6GB 기기에서 매 라운드마다 3초간 정지
- 앱의 장부에는 보이지 않는 2GB
0 byte— 투기적 디코딩 ON/OFF 시의 출력 차이(iPhone 실기 테스트 결과, 138 bytes 완전 일치)
×1.6〜2.0— 실기(17 Pro)의 투기적 디코딩 배율 — 어구(逐語)·채택률 0.86(취약한 조건은 동속~패배까지 모두 공개)
3 초 ×124— 6GB 기기에서 투기적 디코딩 라운드마다 발생한 정지(합계 383초)
~2 GB— 앱 장부에는 400MB 미만으로 보이지만 단말기 전체의 총 소비
등을 포함하여 전반적으로 정리한 구현 기록입니다.
1. 공개한 것
공개물은 2가지입니다. 모델은 Hugging Face에, 그것을 구동하는 Swift 런타임(Runtime)+데모 앱은 GitHub에 있습니다.
- 모델: okayuji/Gemma-4-E2B-it-coreml-speculative(Apache 2.0, 약 4.9GB, context 2048)
- 런타임+데모 앱: oka-yuji/coreml-llm-samples(iOS / macOS, 앱 내에서 HF 다운로드 가능)
내용물은 Google의 Gemma 4 E2B(2B급 온디바이스용 모델)를 3개 청크(Chunk)의 multifunction Core ML 그래프로 변환한 것으로, 다음 3가지 점이 "제가 아는 한 공개물로서는 최초"인 조합입니다(한정적인 표현을 사용했습니다. 이유는 솔직히 한계가 있기 때문입니다).
로스리스 투기적 디코딩 (Lossless Speculative Decoding): 투기적 디코딩 ON과 OFF 시 출력이 바이트 단위로 동일함을 iPhone 실기에서 검증 완료 -
드래프트 모델 불필요: 초안(Draft)은 prompt-lookup(프롬프트 내의 반복)으로부터 생성. 두 번째 모델을 상주시키지 않음 -
KV 캐시의 디스크 영속화: 긴 문맥(Context)을 프로세스를 넘어, 나아가 Mac → iPhone을 넘어 복원 가능
순수 속도는 iPhone 17 Pro에서 per-token 중앙값 6064ms(약 16 tok/s), iPhone 15에서 8688ms(약 11.5 tok/s)입니다. 투기적 디코딩에 의한 가속화는, 출력을 1바이트도 바꾸지 않은 채, 초안이 적중하는 워크로드의 실기 실측에서 ×1.6~2.0(17 Pro·통제된 A/B 테스트). 다만 효과가 있는 조건과 없는 조건이 명확히 나뉩니다 — 어디서 효과가 있는지, "바꾸지 않음"을 어떻게 증명했는지, 그리고 ANE의 어디에 함정이 있었는지. 구현 순서대로 모두 작성하겠습니다.
2. Core ML에 투기적 디코딩이 (거의) 없었던 이유
투기적 디코딩(Speculative Decoding)은 "먼저 저렴하게 수 토큰을 초안으로 작성하고, 본래 모델로 일괄 검증하여, 맞은 만큼만 한꺼번에 진행하는" 가속화 방식입니다. 출력 분포를 바꾸지 않는 구현이라면 결과는 통상적인 디코딩과 완전히 일치해야 하며 — 이것이 "로스리스(Lossless)"의 의미입니다. llama.cpp나 MLX에서는 당연하게 존재하지만, Core ML의 세계에는 거의 없었습니다. 이유는 구조적입니다.
KV 캐시에 접근할 수 없음: Core ML 표준의 MLState에 KV를 두면, 호스트 측에서 내용을 읽을 수 없어 되돌리기(Rollback)가 불가능합니다. 검증에 실패한 초안 부분을 "없었던 일"로 만드는 조작(Rollback)이 성립되지 않습니다 -
폭(Width)이 있는 Forward가 필요함: 초안 S 토큰의 일괄 검증을 위해서는 [S, H]의 배치 포워드(Batch Forward)와 그 argmax를 반환하는 헤드가 필요합니다. 1 토큰씩의 decode 함수만 가진 변환으로는 검증할 수 없습니다.
따라서 이 번들(bundle)은 KV를 호스트 측(Swift 측)의 텐서로 보유하며, 모델에는 decode / 각 너비(width)의 offset-prefill / 너비 4의 검증용 함수를 multifunction으로서 갖추었습니다. KV가 호스트 측에 있으면, 되돌리기(rollback)는 "쓰기 위치를 되돌리기만 하면 되고" (causal mask가 미확정 슬롯을 가림), 디스크 영속화(persistence)는 "텐서를 그대로 써내기만 하면" 됩니다. 투기(speculation)와 KV 영속화가 동일한 하나의 설계 결정에서 비롯된다는 점이 이 구성의 매력적인 부분입니다.
3. 드래프트 모델 없는 투기 — PLD와 verify(4)
초안(draft)의 공급원으로 prompt-lookup decoding (PLD)를 선택했습니다. 생성 중인 문맥에 이미 등장한 토큰열이 있다면, 그 뒤를 초안으로 제안하는 방식입니다. 드래프트 모델이 필요하지 않기 때문에, 6GB 기기에 두 번째 모델을 상주 시킬 여유가 없다는 제약 조건과 그대로 맞아떨어집니다. 어구의 반복, 인용, JSON 추출과 같이 "문맥 복사가 많은" 워크로드에서 강력하며, 자유 대화에서는 거의 발화하지 않습니다 (발화하지 않으면 통상적인 디코딩으로 넘어갈 뿐이며, 해는 제로입니다 — 이는 나중에 실측 데이터로 확인됩니다).
검증 측은 너비 4로 고정했습니다 (prefill4 + 배치 argmax 헤드 lmhead_v4). 채택(acceptance)은 "전방 일치 + 일치가 끊긴 위치의 본래 토큰 1개 보너스"이며, 실패 시의 되돌리기는 위치 되돌리기뿐입니다. ANE에서의 검증 1회 비용은 거의 바닥(고정비)에 붙어 있으므로, S=4 전용 함수를 통해 바닥 비용을 1회로 압축합니다.
번들의 함수 구성과 투기 1라운드의 흐름입니다. 구현의 전체 상은 이것으로 파악할 수 있을 것입니다.
# 번들의 함수 구성 (3 청크 × multifunction + 헤드 2개)
chunk0..2 : decode(1tok) / offset-prefill 512・256・32・8・4
lmhead : 순차 decode용 argmax 헤드 (int8)
...
4. ANE에 배치한 검증 헤드가 argmax를 전부 0으로 만든다 — v8의 함정
첫 번째 이상 징후는 "투기를 ON 하면 출력이 깨진다"였습니다. 로스리스(lossless)를 표방하는 구현으로서 최악의 증상입니다. 원인을 분리해 보니, 너비 8의 int8 배치 헤드(lmhead_v8)를 ANE에서 실행했을 때만, argmax가 모든 토큰에서 0을 반환한다는 것을 알게 되었습니다. 동일한 모델이 cpuOnly에서는 올바르게 작동합니다. 수치 근사(approximation)의 문제가 아니라, 특정 형태와 양자화(quantization)의 조합이 ANE에서 깨지는 계열의 문제입니다.
대응책은 "forward는 ANE, 검증 헤드만 cpuOnly"로 하는 CU 분리였습니다. 이를 통해 투기 ON/OFF의 출력 일치가 회복되었고, iPhone 15 실기에서 어구 프롬프트를 흘려보낸 게이트는 ON과 OFF 모두 138 바이트로 완전 일치했으며, 5 라운드 중 35개 중 26개 토큰을 채택(채택률 0.775)했습니다. 채택 궤적은 Mac의 cpuOnly 참조와 비트 단위로 일치했습니다. 여기서 "로스리스 투기, 실기에서 성립"에 도달합니다.
이때 저는 원인 분리의 부수적인 결과로 "너비 4의 헤드(v4)는 ANE에서도 정상"이라는 메모를 남겼습니다. 이 메모가 나중에 함정이 됩니다.
5. 6GB 기기에서 투기 라운드마다 3초간 멈춤 — 수정하니 이번에는 jetsam
다음 이상 징후는 실제 사용 로그에서 나왔습니다. iPhone 15(6GB)에서 장문을 생성하게 하면, 1430 토큰 생성 중에 1초 이상의 정지가 124회, 총 383초 발생했습니다. 정지 1회는 약 3초였으며, 투기 라운드 수(131)와 거의 1:1로 대응했습니다. 12GB인 17 Pro에서는 동일 조건에서 제로였습니다.
범인은 §4의 대응책 그 자체였습니다. 검증 헤드를 cpuOnly에 둠으로써, 385MB의 가중치가 앱 측의 메모리 장부에 올라가게 됩니다. 12GB 기기에서는 페이지가 상주하여 수 ms 내에 돌아가지만, 6GB 기기에서는 라운드 사이에 OS가 헤드의 페이지를 밀어내고(evict), 매 라운드마다 NAND에서 385MB를 다시 읽어 들입니다 — 그것이 3초의 정체였습니다.
그렇다면 "헤드를 ANE로 돌려보내면, 가중치는 OS 측의 wired 메모리가 되어 페이지 밀어내기(evict)도 사라질 것이다" — §4의 분리 메모에는 "v4는 ANE에서 정상"이라고 적혀 있었다. 실행했다. 결과는 생성 도중의 jetsam(OS에 의한 프로세스 강제 종료). wired 메모리가 +385MB 추가되면서 6GB 기기의 천장을 넘어버린 것이다. 정지(stall)를 감수할 것인가, 강제 종료를 감수할 것인가라는 괴로운 이지선다에 놓이게 되었다.
6. 경량화해도 ANE 직행 헤드는 비손실(non-lossless)이 아니다 — batched와 sequential의 near-tie
딜레마의 해결책으로 "헤드를 pal6(6bit 팔레트 양자화)로 가볍게 만들어 ANE로 보낸다"를 시도했다. 크기는 385MB → 291MB로 줄었다. argmax의 동일성은 실제 데이터의 hidden 56 위치에서 int8과 완전히 일치(flip zero)했으며, ANE 상에서도 문제의 전원 제로(all-zero) 현상이 재발하지 않았다. 여기까지는 순조로웠다.
하지만 end-to-end 투기적 디코딩(Speculative Decoding)의 ON/OFF 게이트를 ANE 직행 헤드로 돌리면 **FAIL(4번째 토큰부터 다른 출력)**이 발생한다. 게다가 원인을 분석해 보니 pal6 때문이 아니라, int8 버전의 v4 헤드를 ANE에 두어도 동일한 위치에서 FAIL이 발생했다. cpuOnly 헤드로 되돌리면 둘 다 PASS한다.
최종 형태는 다음과 같이 결정되었다: 헤드는 pal6로 양자화한 후 cpuOnly에 배치한다(291MB로 줄어든 만큼 재폴트(re-fault)량이 약 24% 감소), 그리고 6GB 기기에서는 투기적 디코딩을 기본적으로 OFF 설정한다(검증 자산은 토글 ON 시에만 로드). 이 구성으로 iPhone 15는 1237 토큰을 정지 없이, 크래시 없이 완주했다(실효 10.8 tok/s, per-token 중앙값 93ms, footprint 262MB). 정지의 근절(cpuOnly 헤드의 pin/wire, 또는 near-tie 내성이 있는 ANE 검증)은 미해결 설계 과제로 남아 있다.
최종적인 배치 정리이다. ANE에서 LLM을 구동하려는 사람이 결국 알고 싶은 것은 이것일 것이다:
| 부품 | 배치 위치 | 이유 |
|---|---|---|
| 3 청크 forward (decode / prefill / verify) | ANE | 속도의 주전장. 이 부분은 일관되게 문제 없음 |
순차 디코딩 헤드 (lmhead · int8) | engine 설정 그대로 (ANE 가능) | 순차 경로가 "정답"을 정의하는 측 |
검증 헤드 (lmhead_v4 · pal6 291MB) | cpuOnly 고정 | ANE 직행은 batched near-tie 상황에서 비손실(int8 및 pal6 모두) |
| 투기적 디코딩 기본값 | 6GB 기기 OFF / 8GB 이상 ON | 6GB는 cpuOnly 헤드의 재폴트 정지가 남아 있기 때문 (ON은 수동 토글) |
7. 보이지 않는 메모리 — 앱 장부 400MB 뒤에 숨은 2GB
이 일련의 소동을 관통하는 것은 메모리의 3가지 장부 문제이다. 앱이 스스로 측정할 수 있는 phys_footprint는 본 번들 실행 중 260500MB 정도이다. 하지만 ANE에서 돌아가는 가중치는 OS 측의 wired 메모리로서 이 수치 외부에 약 1.51.7GB가 상주하며, int8 사이드카(약 2.6GB 파일)는 mmap의 클린 페이지(clean page)로서 접근한 만큼만 실제 RAM을 사용한다. 이를 합치면 짧은 채팅 시 단말 전체 약 2GB, 넓은 prefill 작업 세트가 올라가면 3GB 초과가 된다.
"footprint는 400MB인데 6GB 기기에서는 jetsam으로 종료된다", "cpuOnly에 둔 385MB만이 페이지 밀어내기를 겪는다"라는 얼핏 불가해한 현상은 모두 이 장부의 분리로 설명이 가능하다. 온디바이스 LLM의 메모리 논의는 어떤 장부에 대한 이야기인지를 먼저 확정하지 않으면 대화가 통하지 않는다.
8. 긴 문장을 쓸 수 없다면, KV를 운반하면 된다
6GB 기기의 또 다른 천장은 prefill이다. 긴 프롬프트를 넓은 prefill로 흘려보내면 작업 세트가 천장을 뚫게 되며, 이는 정지가 아니라 프로세스 강제 종료(SIGKILL)로 이어진다. 즉, 6GB 기기는 "긴 문맥을 만드는" 것이 불가능하다.
그것이 바로 KV 디스크 영속화(KV disk persistence)입니다. 호스트 측 KV는 단순한 텐서(Tensor)이므로, 유효한 프리픽스(Prefix) 분량(1 토큰당 18,432 바이트)을 그대로 써 내려가면 다른 프로세스나 다른 디바이스에서 복원할 수 있습니다. 복원은 decode 함수만으로 동작하기 때문에(광범위한 prefill을 전혀 로드하지 않음), 6GB 기기에서도 prefill의 한계를 구조적으로 우회하여 긴 문맥에 도달할 수 있습니다. 실측 결과, Mac(cpuOnly)에서 생성한 중문맥(Mid-context) KV(6.54MB)를 iPhone의 ANE 상에서 0.044~0.057초 만에 임포트했으며, 이어지는 생성 결과가 Mac과 바이트 단위로 일치했습니다. 이는 수식상의 호환성이 아니라, 실제 바이트가 일치하는 것입니다.
멀티 턴(Multi-turn) 대화에서도 동일한 메커니즘이 작동하며, 실제 사용 로그에서는 3번째 턴에서 323 토큰 문맥의 96%를 재사용하여 TTFT(Time To First Token) 1.28초를 기록한 사례가 있습니다(재사용이 없다면 A16에서는 수십 초 단위의 피딩(Feeding)이 필요함).
9. 실측치 요약
조건: 동일 앱(공개 데모 앱) · 동일 번들 · greedy. iPhone 측은 실제 사용 로그, Mac은 1 프로세스 1 조건의 통제된 실행(Controlled run).
| 지표 | iPhone 15(A16 · 6GB) | iPhone 17 Pro(A19 Pro · 12GB) |
|---|---|---|
| per-token 중앙값(단문~중문) | 86~88ms(≈11.5 tok/s) | 60~64ms(≈16 tok/s) |
| ... |
투기적 디코딩(Speculative Decoding)의 배율은 실제 기기의 통제된 ABBA 8 런(2개 워크로드 × ON/OFF 각 2회 · greedy) + Mac 통제 A/B + 실제 기기 단문 쌍으로 실측했습니다. 결론: 배율은 채택률(Acceptance rate)이 모든 것을 결정한다. 특화 영역에서는 실제 기기에서 ×1.6~2.0, 중간 채택률에서는 대등(cold는 역전패하기도 함), 취약 영역에서는 동일 속도 = 해가 없음. 일반 decode의 OFF 기준이 8개 런 모두 15.27~15.43 tok/s(±0.1, 발열 심화 시에도 불변)로 수렴하고 있어, 비교 기반은 견고합니다.
| 워크로드(장소) | 채택률 | OFF | ON(cold / warm) | 배율 |
|---|---|---|---|---|
| 축어 반복(실제 17 Pro) | 0.857 | 15.4 tok/s | 24.8 / 30.9 | ×1.61 / ×2.01 |
| JSON 추출(실제 17 Pro) | 0.544 | 15.3 tok/s | 12.5 / 17.3 | ×0.82 / ×1.13 |
| JSON 추출(Mac cpuOnly · 통제) | 0.54 | 19.0 tok/s | 23.1 | ×1.22 |
| 단문 채팅(실제 · warm 쌍) | 0.5(채택 18/219) | 15.1 tok/s | 15.5 | ≈×1.0(실질적 동일 속도) |
솔직한 주석 3가지. ① 실제 기기의 각 조건은 n=2이며, ON 측은 cold/warm의 구조적 쌍(통계적 샘플은 아님). 다만 OFF 기준의 안정성과, 순서가 채택률 + cold/warm으로 기계적으로 설명 가능하다는 점에서 방향성(축어 반복=승리 · 추출=대등 · 대화=동일 속도)은 견고하다고 판단합니다. ② 추출의 cold 런은 투기가 오히려 느림(×0.82) — decode 구간으로의 첫 특수화 스톨(Specialization stall) 혼입과 더불어, prompt 72 토큰이 폭(width) 32의 prefill 버킷에 들어가 첫 특수화 시 TTFT 48.5초가 발생했습니다. "첫 번째만 극단적으로 느려지는 현상"은 투기적 디코딩에서도 예외 없이 발생합니다. ③ 로스리스(Lossless)는 8개 런 모두에서 유지됨(동일 워크로드의 생성 토큰 수가 ON/OFF 시 완전히 일치). 초안(Draft)이 맞지 않는 상황에서는 1바이트도 1ms도 손해 보지 않는다는 발화 게이트(Gating) 설계는 단문 채팅 쌍(중앙값 64.476 vs 64.472ms)에서도 입증되었습니다. 패배한 조건을 포함하여 측정된 모든 조건을 그대로 게재했습니다.
투기 ON(왼쪽 · 30.9 tok/s)과 OFF(오른쪽 · 15.4 tok/s). 동일한 축어 프롬프트 · 동일 출력에서 속도가 약 2배 차이 남(iPhone 17 Pro · 모두 warm). 화면 녹화에서 추출되었으며, 통계 표시는 metrics 로그 기록(30.92 / 15.43 tok/s)과 일치함.
| 투기 ON 실제 기기 스크린샷(30.92 tok/s) | 투기 OFF 실제 기기 스크린샷(15.43 tok/s) |
|---|---|
![]() | [IMG:2] |
*투기(Speculative) ON(왼쪽·15.5 tok/s)과 OFF(오른쪽·15.1 tok/s)의 실제 기기 화면(iPhone 17 Pro·warm·동일 프롬프트). 출력은 greedy 결정론(deterministic)으로 완전히 일치하며, 속도도 실질적으로 동일함 — 화면의 통계 표시는 metrics 로그 기록과 일치함(화면 녹화에서 발췌).
| 투기 ON 실제 기기 스크린샷(15.5 tok/s) | 투기 OFF 실제 기기 스크린샷(15.1 tok/s) |
|---|---|
| [IMG:3] | [IMG:4] |
10. 빗나간 예상
시계열로 6가지. 전부 측정 기록에 남겨두었습니다.
- 「v4 헤드는 ANE에서 정상」→ p=0 한정의 진실이었다. 단일 argmax의 정상성으로부터 end-to-end의 무손실(lossless)을 추정한 것이 오류였다. 배치 forward의 near-tie가 p>0에서 argmax를 반전시키며, int8에서도 pal6에서도 ANE 직행 헤드는 비무손실(non-lossless)이었다 (§6).
- 「pal6화로 헤드는 절반이 된다」→ 0.756× 수준에 그침. 절반(~180MB)은 pal4 상당의 계산이며, 6bit는 6/8이다. 양자화 축소율은 bit 수의 비율 이상이 될 수 없다는 당연한 사실을 기대가 덮어쓰고 있었다.
- 「투기는 대화도 빠르게 만든다」→ 자유 대화에서는 거의 발화하지 않는다. 실제 사용 로그에서는 fallback 1507회 대비 투기 라운드(speculative round) 131회였다. PLD의 특기 영역(축어·인용·추출)이 대화에 포함되지 않는다면, 올바른 기대치는 "빨라지지는 않지만 절대 느려지거나 망가지지도 않는다"이다. 후일 warm 상태가 맞춰진 실제 기기 쌍(ON 15.5 / OFF 15.1 tok/s·중앙값 거의 동일)이 이를 통제 조건으로 뒷받침했다.
- 「footprint ≒ 소비 메모리」→ 3개의 장부였다. 앱 장부 400MB 뒤에는 wired 1.5~1.7GB와 mmap이 있다. 6GB 기기의 동작은 앱 장부만 봐서는 설명할 수 없다 (§7).
- 「6GB에서의 정지는 헤드를 ANE로 돌려주면 사라진다」→ jetsam으로 변했다. 정지(페이지 퇴출)와 강제 종료(wired 초과)는 트레이드오프 관계이며, 돌파구라고 생각했던 ANE 직행은 비무손실 발견(1.)으로 이어졌다. 결과적으로 가장 가치 있는 실패였다.
- 「채택률(acceptance rate)이 0.5만 되어도 실제 기기에서 이길 수 있을 것이다」→ cold 상태에서는 역전패(×0.82). 중간 정도의 채택률에서는 버려진 드래프트(discarded draft)의 계산 비용이 이득을 거의 상쇄하며, 여기에 첫 회 특수화 스톨(specialization stall)이 더해지면 OFF보다 느려진다. 투기의 손익분기점이 채택률에 밀착된다는 llama.cpp 진영의 지식과 같은 구조가 Core ML 실제 기기에서도 성립하고 있었다.
11. 교훈
- CU를 가로지르는 무손실은 추정하지 말고, 경로마다 end-to-end로 검증할 것. "cpuOnly에서 무손실", "단일 argmax가 ANE에서 정상"으로부터 "ANE에서도 무손실"을 도출할 수 없다. ON==OFF의 바이트 일치 게이트를 CU 구성마다 돌리는 것이 유일한 증명이다.
- p=0(확정 문맥)의 검증은 p>0의 동작을 보장하지 않는다. near-tie는 margin이 작은 위치에만 나타난다. 게이트에는 실제 문맥·실제 hidden을 사용한다.
- 메모리는 장부를 가로질러 계산할 것. app footprint / OS 측 wired / mmap clean page의 3가지를 나누어 보고 나서야 정지(퇴출)와 강제 종료(jetsam)가 별개의 문제임을 알 수 있다.
- 「빠르다」를 말하기 전에 「절대 망가지지 않는다」를 증명할 것. 배율은 워크로드에 따라 부풀릴 수 있지만, 바이트 일치는 부풀릴 수 없다. 무손실을 먼저 증명해 두었기에, ×2.0으로 이기는 조건도 ×0.82로 지는 조건도 동일한 표에 나란히 제시할 수 있다.
- 수치는 「실재하는가」가 아니라 「기록에 출처가 있는가」로 채택 여부를 결정할 것. 공개 문서의 수치를 전부 기록 파일과 대조한 결과, 실재하지만 미기록된 실측값이 하나 발견되어 출처가 있는 값으로 교체했다. 기록하는 습관은 공개 시의 보험이 된다.
12. 솔직한 한계
- 일단 이 구현은 제가 확인한 범위 내에서는 아직 어디에도 공개된 적이 없으며, 실제로 실무 프로덕트에서 로컬 모델을 통합하고자 할 경우 사양(Specification)이 매우 까다로워지기 때문에, 이 모델과 구현은 하나의 기반(Foundation)이 될 것이라는 인상을 받았습니다. 따라서 iPhone의 ANE (Apple Neural Engine) 상에서 동작하는 채팅 LLM (Large Language Model) 중, 공식 Core ML API를 이용한 로스리스 (Lossless) 투기적 디코딩 (Speculative Decoding) 공개 구현으로서는 제가 아는 한 세계적으로도 최초라고 인식하고 있습니다.
- 다만 같은 주에 12B 규모의 Core ML 투기 번들 (MTP Drafter + 폭 4 검증, 보고값 0.2~0.5 tok/s)이 공개되었으므로, 무제한적인 "Core ML × 투기적 디코딩의 최초"는 더 이상 존재하지 않습니다. 모델 카드 (Model Card)에도 관련 연구 (Related work)로 명시하였습니다.
- 6GB 기기는 긴 프롬프트의 prefill (프리필)을 수행할 수 없습니다. KV restore (KV 복구)는 우회로일 뿐 해결책은 아닙 (새로운 장문은 12GB 기기나 Mac에서 생성해야 합니다).
- context (컨텍스트)는 2048입니다. 멀티 턴 (Multi-turn) 대화에서 KV를 쌓아 올리면 반드시 한계에 도달합니다 (실측 시 위치 2040에서 결정론적으로 정지). 데모 앱은 상한에 도달했을 때 UI로 안내하도록 구현했으나, 상한 자체는 변환 시의 상수입니다.
- 실기기 배율은 n=2 방향의 확인 단계까지입니다. ABBA 8회 실행 결과,逐語 (Word-by-word) 방식은 ×1.61
2.01, 추출 (Extraction) 방식은 ×0.821.13을 얻었으나, ON 측은 cold/warm 구조 쌍으로 구성되어 통계적 샘플이라고 보기 어렵습니다 (OFF 기준은 ±0.1로 안정적). 매 실행 시 Reset을 통일하고 n≥3의 반복 실험을 수행하는 것이 향후 과제입니다. - ANE 직행 검증 헤드 (Verification Head)는 미해결 상태입니다. 현재 구성은 로스리스를 우선시하여 cpuOnly에 배치해 두었으며, 6GB 기기에서 투기적 디코딩을 수동으로 ON 하면 라운드당 최대 수 초의 정지가 남습니다 (pal6화로 완화되었으나, 근절은 향후 과제입니다).
13. 재현 방법 및 참조
iPhone에서의 재현은 리포지토리를 클론(Clone)하여 DemoApp을 빌드하면, 앱 내에서 모델을 다운로드하여 즉시 테스트할 수 있습니다 (자세한 내용은 device guide 참조). Mac의 CLI에서의 투기적 디코딩 A/B 테스트는 다음과 같습니다:
git clone https://github.com/oka-yuji/coreml-llm-samples && cd coreml-llm-samples
swift build -c release
hf download okayuji/Gemma-4-E2B-it-coreml-speculative --local-dir models/gemma-4-e2b-speculative-pal6
...
검증값의 1차 정보는 공개된 자료들에 정리되어 있습니다:
- 모델 카드의 Verification 섹션 — 실기기 byte 일치 게이트, KV 크로스 디바이스 복구, 실측값 총람
- device guide — 빌드부터 실행까지의 절차, 메모리 읽는 법 (3가지 장부), 트러블슈팅
- DemoApp이 생성하는
metrics.jsonl
— 본문의 실기기 수치는 모두 여기서 가져왔습니다 (per-token 전 시리즈, 투기 통계, 메모리, thermal 포함. 독자의 단말기에서도 동일한 로그를 추출할 수 있습니다).
본문의 수치는 모두 실측값이며, 제가 보유한 측정 기록과 대조를 마쳤습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기