원격 KV 캐시가 GPU 재계산보다 느린 문제를 랭크 샤딩으로 푼다

When the KV Cache Is Slower Than the GPU: Scaling Remote KV Tiers for Context-Parallel Prefill

HF Daily2610.21500

Muhammad Ibrahim2026-10-15

무엇인가

이 논문은 프로덕션 환경에서 원격 KV 캐시 계층을 켰더니 오히려 느려진 사례를 다룬다. GLM-5.3(FP8 mixture-of-experts, DeepSeek식 multi-head latent attention)을 8×B300 노드에서 prefill context parallelism(PCP8)으로 서빙하고, LMCache가 호스트 메모리 L1 뒤에 Valkey 기반 L2 원격 저장소를 두는 구성이다. 9k~200k 토큰 엔터프라이즈 SLA 워크로드에서 L2를 켜자 중앙값 TTFT는 그대로였지만 16k 토큰 p99가 3.96초에서 17.3초로, 32k에서 6.06초에서 25.0초로 늘어 배포가 L1 전용으로 롤백됐다. 논문은 "KV를 불러오는 것이 다시 계산하는 것보다 싸다"는 원격 계층의 암묵적 전제가 깨진 조건을 재현하고, 그 원인을 비용 모델로 설명한 뒤 고친다.

어떻게 동작하나

핵심은 두 항으로 된 비용 모델이다. 재계산 시간은 n/R, 로드 시간은 T0 + ns/B다(n은 캐시에서 서빙되는 토큰 수, s는 토큰당 KV 바이트, R은 엔진의 초당 prefill 토큰 수, B는 캐시 계층이 한 프롬프트에 공급하는 대역폭). T0를 무시하면 히트가 이득인 조건은 B > B* = sR이고, 프롬프트 길이는 소거된다. GLM-5.3 FP8에서 s=48.4KB/token, R=52.2k tokens/s이므로 B* ≈ 2.53GB/s인데, 단일 Valkey 인스턴스가 8개 PCP 랭크에 제공한 최대 대역폭은 1.71GB/s로 이 문턱 아래였다. 즉 모든 L2 히트가 재계산보다 느렸다. 고정 비용 T0 ≈ 0.33초(8 인스턴스, B=10.76GB/s 조건의 중앙값 0.40초에서 0.79/10.76을 뺀 한 점 피팅) 때문에 약 17k 토큰 미만 프롬프트는 어떤 대역폭에서도 재계산을 이길 수 없다. 동기 로딩은 큐 안정성 문제도 만든다. 이용률 ρ = λns/B에서 λ=1.21 req/s, 중앙값 프롬프트 0.79GB, 단일 인스턴스 실측 0.78~0.89GB/s이므로 ρ≈1.13>1이 되어 정상 상태가 없고, 랭크별 8 인스턴스면 ρ≈0.09로 떨어진다.

무엇과 다른가

설계는 네 갈래다. 첫째, MLA KV 저장소를 PCP 랭크로 샤딩한다. AllGather 후 모든 랭크가 전체 프롬프트 KV를 갖지만 청크 c는 랭크 c mod W가 소유하고, 각 랭크는 자기 청크만 저장·조회·로드하며 L1도 1/W 슬라이스를 쓴다. 기존 스케줄러의 min-over-ranks 조회가 정확히 가장 긴 캐시 프리픽스를 반환한다는 것을 증명(Proposition 1)했고, 청크 목록 지문·첫 실패·메타데이터를 랭크마다 교환해 합의된 프리픽스를 계산하는 로딩 프로토콜로 어떤 랭크도 collective를 건너뛰지 못하게 했다. 둘째, 원격 계층을 랭크로 분할한다. 청크 소유권이 정적으로 정해져 있으므로 resp://valkey-{rank}:6379처럼 인스턴스마다 랭크 하나를 붙이면 클러스터 프로토콜이나 클라이언트 해싱 없이 GET 대역폭이 6.3배(10.76GB/s)로 오르고 엔진 간 재사용도 유지된다. 셋째, 샤딩 저장소에 비동기 로딩을 추가한다. 넷째, 무한 정지를 일으킨 클라이언트 버그 세 건을 고친다. 완료 이벤트가 제출과 future 등록 사이에서 유실되던 경쟁, 존재하지 않는 키에 대한 GET이 RESP의 $-1\r\n을 11바이트 고정 헤더로 읽어 영원히 반환되지 않던 문제, 비동기 GET이 worker-0 키로 재작성하지 않던 문제다.

어떻게 쓰나

실험은 프로덕션 라우터 로그로 16k 셀의 683개 요청 스트림을 재구성해, 프로덕션과 동일한 이미지·설정의 격리된 8×B300 노드에서 재생하는 방식이다. 히트가 많은 450개 요청 부분집합(중앙값 16,390 토큰, 범위 14,715~17,889)에서 98.5%의 프롬프트 토큰이 L2에서 서빙됐다. 단일 인스턴스 동기 구성(H1b)은 중앙값이 3.52초에서 52.30초로 늘고 엔진에 최대 59개 요청이 쌓였으며 GPU는 7%만 바빴다. 단일 인스턴스 비동기(H2b)는 지연을 3.2~3.8초로 묶었지만 49%의 요청이 3초 비동기 조회 타임아웃으로 재계산해 L2 서빙 비율이 50.6%에 그쳤다. 랭크별 8 인스턴스(H3~H5b)에서는 중앙값 0.41초, p99 0.80초가 됐고 모든 수정을 적용한 H5b는 p99 0.67초로 H1b 대비 124배 낮다. Valkey 샤드는 12.4MB GET당 평균 8.9ms의 서버 시간을 썼다. 다만 H5b의 중앙값은 같은 프롬프트를 재계산하는 것보다 0.09초 느린데, 10.76GB/s에서 전송이 차지하는 몫은 0.074초뿐이고 나머지는 엔진 안에서 쓰는 T0(스케줄링, 캐시되지 않은 꼬리 토큰의 forward, 청크별 교환과 브로드캐스트)다.

전제와 한계

처리량과 정확성도 측정했다. 8개의 동시 ~200k 토큰 프롬프트를 3라운드 돌린 결과 샤딩 저장소만으로 43.2k에서 49.4k tokens/s로 14% 올랐다. 같은 트레이스에서 별개의 병목이 드러났는데, FlashInfer의 fused SwiGLU + FP8 재양자화 커널이 호출당 1.57ms로 prefill GPU 시간의 17%를 차지했고(EP8에서 대부분의 스레드 블록이 다른 GPU로 라우팅된 슬롯을 처리하기 때문), SM103용으로 재작성한 업스트림 커널이 이를 0.23ms로 줄였다. 두 수정을 함께 적용하면 57.3~58.0k tokens/s로 34% 향상이며 LMCache를 끈 상태 대비 4~5% 이내다. 200k 토큰 두 개 구간의 Nsight 측정에서 AllGather 시간은 15.8초에서 5.8초로, rank 0의 device-to-host 트래픽은 19.1GB에서 2.4GB로 줄었다. 정확성은 12.8k~166.7k 토큰 needle-in-a-haystack 프롬프트에서 24/24 정답이었고, 동기 샤딩 저장소는 GPU 캐시 축출 후 3/3, 동시 버스트 36/36, 30분 소크 1,549 요청에서 오류가 없었다.

개발자 관점의 교훈은 캐시 계층을 "네트워크가 빠른가"가 아니라 "서빙하는 GPU의 prefill 속도를 따라잡는가"로 판단해야 한다는 것이다. B* = sR은 가속기가 빨라질수록 커지므로, 범용 키-값 서버로 만든 원격 계층은 엔진 병렬성에 맞춰 스케일아웃하거나 RDMA 전송으로 옮겨야 한다. PCP 같은 컨텍스트 병렬 환경에서는 랭크가 락스텝으로 돌기 때문에 한 랭크의 저장 지연이나 느린 GET, 유실된 완료가 전체를 멈추므로, 랭크별 작업을 대칭으로 유지하고 무한정 블록될 수 있는 연산을 스텝 안에 두지 않아야 한다. 또한 이 하드웨어에서 약 17k 토큰 미만 프롬프트에는 히트가 손해이므로, 요청별로 예측 로드 시간과 n/R을 비교하는 로드-또는-재계산 정책이 필요하다.

저자가 밝힌 한계는 분명하다. 각 조건을 한 번씩만 실행했고, 124배 p99 개선이나 6.3배 대역폭, 불안정 큐 같은 큰 효과는 실행 변동 범위 밖이지만 10% 미만 차이(예: H3 대 H4)는 입증되지 않았다. 재생은 prefill만 다루고 엔진 하나만 Valkey 클라이언트로 쓰기 때문에 여러 엔진이 공유 인스턴스에서 만드는 경합은 측정이 아니라 추정이다. 지연 결과는 14.7k~17.9k 토큰 한 구간에 국한되고, T0는 한 점에서 피팅했으며 그로부터 나오는 프롬프트 길이 임계값은 직접 측정하지 않은 예측이다. 히트가 많은 부분집합은 사설 인스턴스에 들어가도록 고른 것이라, 실제 프로덕션의 축출되는 인스턴스라면 미스와 EXISTS-GET 경쟁이 더 많을 것이다. 엔드투엔드 배포 비교는 캐시 계층 외에 병렬성과 prefill/decode 배치도 달라 순수한 절제 실험이 아니다.