Qwen3.8-Flash-Next 2x3090 + DDR4 환경, 4부: 전문가 캐시를 GPU에서 밀어내어 프리필 속도 2.2~2.5배 향상
Qwen3.8-Flash-Next on 2x3090 + DDR4, part 4: 2.2-2.5x faster prefill by kicking the expert cache off the GPU while the prompt runs
핵심 요약
프리필 실행 중 전문가 캐시를 일시적으로 비워 VRAM을 확보함으로써 프리필 속도를 2배 이상 개선했습니다.
- 프리필 최적화 — 프리필 실행 시 캐시 슬롯을 비워 마이크로 배치 크기를 2048로 늘려 성능을 개선함
- 성능 향상 — 8k 프롬프트 기준 프리필 속도 2.24배, 119k 프롬프트 기준 2.54배 향상됨
- 트랜잭션 방식 — 캐시 스왑 실패 시 서버가 오작동하지 않도록 올-오어-낫싱(all-or-nothing) 방식을 적용함
- 하드웨어 구성 — 2x3090 GPU와 6x32GB DDR4 ECC 메모리를 사용하여 전문가 레이어를 호스트 RAM에 고정함
같은 박스에서 진행한 4번째 테스트다. 1편은 expert cache PR 적용 후 17 -> 25-29 t/s, 2편은 UD-Q4_K_XL로 바꾸고 캐시에 MTP를 쌓은 뒤 37-41 t/s, 3편은 필요 이상으로 정렬을 해대던 top-k fallback 문제였다. 이번엔 프리필(prefill)이 핵심인데, 솔직히 그동안 이게 제일 문제였다. 8k 프롬프트에서 첫 토큰 나오기까지 80초 넘게 걸리고, 119k는 24분이나 걸렸으니... 말 다 했지 ㅋㅋ.
박스 사양은 여전히 2x 3090, 듀얼 Broadwell Xeon, llama.cpp, 두 번째 카드에 Q8 MTP 헤드를 올린 UD-Q4_K_XL, 모든 expert 레이어는 호스트 RAM에 고정, 150슬롯 캐시, 261k 컨텍스트, f16 KV다. 2편 이후로 하드웨어 변경이 딱 하나 있는데, LRDIMM을 6x32 GB DDR4-2133 ECC로 바꿨다. 어떤 수치가 4-DIMM이고 6-DIMM인지 구분해서 적을 테니 섞였다고 오해하지 마라.
2편에서 제대로 설명 못한 부분
내가 -ub 512를 썼던 건 캐시를 위한 타협안이었기 때문이다. 2048 토큰 마이크로 배치에는 GPU당 약 7.3 GiB의 연산 버퍼가 필요한데, 512는 1.9 GiB면 된다. 그 차이가 디코딩용으로 쓰고 싶었던 캐시 슬롯 50개 정도랑 맞먹거든. 그래서 슬롯을 지키는 대신 프리필 속도를 3배 정도 손해 보고 있었다.
왜 3배나 손해였냐면, 마이크로 배치마다 expert가 호스트에서 GPU0으로 스트리밍되는데, 배치가 512 토큰이든 2048 토큰이든 업로드 비용은 똑같거든. 그래서 프리필 속도가 사실상 마이크로 배치 크기에 비례한다. ub 512일 땐 8k 프롬프트가 PCIe를 통해 expert 세트 전체를 16번이나 끌어오지만, ub 2048에선 4번이면 끝난다.
바꾼 점
캐시는 8 토큰 이하의 배치(디코딩 및 MTP 검증 배치)만 처리한다. 프롬프트 처리 중에는 VRAM만 차지하고 놀고 있길래, 필요 없을 때 그 공간을 뺏어 쓰기로 했다. 이제 프롬프트가 들어오면 서버가 캐시 슬롯, 디코딩 연산 버퍼, CUDA 풀을 싹 비운다. 그리고 ub 2048 크기의 연산 버퍼를 잡아서 프롬프트 전체를 2048로 돌린 다음, 첫 토큰이 나오기 전에 다시 원래대로 복구한다. 디코딩은 이 과정에서 전혀 건드리지 않으니 이전이랑 똑같이 돌아간다. 환경 변수 두 개(LLAMA_PHASE_PREFILL_UBATCH=2048, LLAMA_PHASE_PREFILL_MODE=transaction)만 설정하면 되고, 서버 시작은 여전히 -ub 512로 한다. 참고로 "transaction"은 스왑이 올-오어-낫싱(all-or-nothing)이라는 뜻이다. 복구가 안 되면 서버가 어정쩡하게 돌아가는 게 아니라 에러를 뱉을 거다. 확실히 해두려고 말한다.
수치 (6 DIMMs, 같은 날, 실행마다 서버 새로 시작)
| what | before (ub 512 + cache) | now | change |
|---|---|---|---|
| 8k fresh prompt, greedy: prefill | 99.9 t/s | 223.7 t/s | 2.24x |
| 8k: time to first token | 82 s | 37 s | 0.45x |
| 8k: decode over the next 2048 tokens | 33.4 t/s | 34.3 t/s | +2% |
| ~37k context, my normal sampling: prefill |


