llama.cpp가 재시작 후에도 기억하도록 만들었습니다: 54.4초의 prefill이 새 프로세스에서 3.5초로 단축 (무료 ARM 서버)
요약
llama.cpp의 KV 캐시를 디스크에 저장하여 프로세스 재시작 후에도 프리필(prefill) 속도를 획기적으로 단축하는 최적화 방법을 소개합니다. ARM 서버 환경에서 실험을 통해 TTFT를 개선하고, 추측 디코딩과의 충돌 문제 및 프롬프트 구조화의 중요성을 분석했습니다.
핵심 포인트
- KV 캐시 디스크 저장을 통해 프리필 시간을 최대 15~300배 단축 가능
- 추측 디코딩(Speculative Decoding) 활성화 시 캐시 상속 기능이 작동하지 않는 버그 확인
- 프롬프트를 'label: value' 형식으로 구조화하면 토큰 감소 및 정확도 향상
- MoE 모델에서 전문가 수를 줄여 프리필을 가속할 경우 캐시 손상 위험 존재
저는 아무도 선택하지 않을 하드웨어에서 LLM을 실행합니다: Oracle 무료 티어 ARM 박스, 4 코어, 월 0 EUR. 별도 언급이 없는 한 아래의 모든 수치는 여기서 측정되었습니다. CPU의 병목 현상은 디코딩 (decode)이 아니라 프리필 (prefill)입니다. 3356 토큰 분량의 문서는 모델이 단 하나의 토큰을 쓰기 전까지 54.4초가 소요됩니다. llama.cpp는 RAM에 KV 캐시를 저장하므로, 두 번째 동일한 요청은 빠릅니다. 하지만 프로세스가 재시작되면 다시 54초를 지불해야 합니다. 그래서 저는 KV 캐시를 디스크에 영구 저장하도록 만들었습니다. 새로운 프로세스는 디스크로부터 해당 프리필을 3.5초 만에 상속받으며, 블롭 (blob)이 여전히 페이지 캐시 (page cache)에 있다면 0.10초가 걸립니다. 읽어오는 위치에 따라 15~300배 차이가 납니다. 반복되는 워크로드의 엔드 투 엔드 (end-to-end) 기준으로는 4.8배입니다. 예측 가능한 접두사 (prefix)를 새벽 3시에 미리 처리하는 systemd 타이머를 사용하면, 2815 토큰 문서는 TTFT (Time To First Token)가 89.7초에서 16.7초로 줄어듭니다 (5.4배). 그리고 오전 9시에 도착하는 요청은 프리필 비용을 전혀 지불하지 않습니다.
발표할 가치가 있는 버그: Warm-ahead 기능은 추측 디코딩 (speculative decoding)이 켜져 있을 때(기본값) 조용히 작동하지 않았습니다. 추측 분기 (speculative branch)가 공유 접두사 캐시 (shared-prefix cache)를 참조하기 전에 반환되었기 때문에, 모든 웜업 (warm-up) 과정에서 생성된 스냅샷을 아무도 읽지 않았습니다. 운영 서버에서 측정한 결과: 동일한 캐시와 동일한 요청임에도 추측 디코딩을 켰을 때는 90.5초, 껐을 때는 16.7초가 걸렸습니다. 각각 작동하던 두 기능이 서로를 조용히 상쇄하고 있었던 것입니다.
작동하지 않은 것들:
서버의 이전 출력을 추측 초안 (speculative draft) 자료로 사용하는 것: 구조를 공유하는 서로 다른 요청들의 워크로드에서 수락률 (acceptance) +5%, 처리량 (throughput) -3.8%. 이 메커니즘은 말한 대로 작동은 하지만 그만큼의 이득을 가져다주지 못합니다.
프롬프트 조회 추측 (Prompt-lookup speculation): 동일한 워크로드에서 +3.9%. 이것이 유일한 성과입니다.
더 거친 양자화 (Coarser quantization): Q4_0는 프리필 속도가 37% 더 빠르지만, 제 추출 테스트에서 20개 중 5개의 사실을 놓쳤습니다. 기각되었습니다.
MoE (Mixture of Experts)에서 프리필 중 활성 전문가 (active experts)를 절반으로 줄이는 것: 44% 더 빠르지만, 캐시를 조용히 손상시킵니다. 4개의 전문가로 구축된 KV를 8개로 다시 읽었을 때, 대조군이 14/20점을 받은 것에 비해 11/20점을 기록했습니다. 손상은 단순히 출력뿐만 아니라 캐시된 표현 (cached representation) 자체에 발생합니다.
저를 놀라게 한 두 가지 사실이 있습니다. 첫째, 입력을 한 줄당 하나의 사실을 담은 "label: value" 형식으로 재작성했을 때: 토큰 수가 2137개에서 405개로 감소했고, TTFT (Time To First Token)는 40.5초에서 6.2초로 단축되었으며, 사실 관계 시험 점수가 19/20에서 20/20으로 올랐습니다. 토큰은 줄어들고 정확도는 높아졌습니다. 올바른 숫자에 대한 어텐션 (Attention) 비율이 잘못된 숫자에 대한 1.1:1 비율에서 7:1로 변했습니다. 산문 (prose)은 의미적 결합 (semantic binding)을 만들지만, "label: value"는 구조적 결합 (structural binding)을 만듭니다. 둘째, 어휘 사전 (vocabulary)을 151,936개에서 32k개로 줄였을 때: 디코딩 (decode) 속도가 17.8% 향상되었으며, 비트 단위로 손실이 없는 (bit-for-bit lossless) 방식입니다. 임베딩 (embedding)은 행이 전체 양자화 블록 (quantization blocks)에 걸쳐 있는 Q6_K 방식이므로, 블록을 나누지 않고 행 전체가 탈락합니다. 토크나이저 (tokenizer)는 바이트 수준 (byte-level)이며 256개 바이트 문자가 모두 유지되므로, 텍스트가 표현 불가능해지는 일은 없습니다. 최악의 경우 단어가 잘려 토큰이 하나 더 추가되는 정도입니다. 홀드아웃 텍스트 (held-out text)에서 측정한 비용은 토큰이 1.9% 더 추가되는 수준이었습니다.
이것이 아닌 것: 이 시스템은 llama.cpp를 기반으로 구축되었으며 해당 커널을 직접 호출하므로, 가공되지 않은 디코딩 속도는 동일합니다. 즉, 토큰당 오버헤드를 추가하지 않았습니다. 단일 콜드 요청 (cold request) 상황에서는 이것이 곧 llama.cpp입니다. 차이점은 반복되거나 캐시된 워크로드 (cached workloads)에서만 나타납니다. 사실 관계 시험은 제가 직접 만든 것입니다. 실제 이탈리아 비즈니스 페이지 하나를 대상으로 20개의 질문을 던졌고, 정규 표현식 (regex)으로 채점했습니다. 단 하나의 페이지, 하나의 언어, 하나의 도메인입니다. 이는 이 프로젝트에서 가장 취약한 부분이며, 솔직히 말씀드리고 싶습니다. 만약 소형 모델을 위한 공개적인 적대적 사실 추출 (adversarial fact-extraction) 데이터셋을 알고 계신다면 저에게 알려주세요. 실행 후 결과가 나쁘더라도 그대로 공개하겠습니다. MIT 라이선스입니다. 동일한 무료 ARM 서버에서 라이브 데모를 확인할 수 있습니다. 작은 인스턴스 하나이며 오토스케일링 (autoscaling)이 적용되지 않으므로, 속도가 느리다면 그것이 월 0유로 비용의 정직한 성능입니다.
데모: https://swellweb.github.io/reame/
코드: https://github.com/swellweb/reame
부정적인 결과가 포함된 벤치마크: https://github.com/swellweb/reame/blob/main/docs/BENCHMARKS.md
제출자: /u/Annual_Manner_5901 [link] [comments]
AI 자동 생성 콘텐츠
본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기