LLM / 서빙 / MoE / Strata / 실측
RTX 3090 한 장에 125B MoE를 올리며 배운 오프로드 서빙 설계
집 서버의 로컬 LLM을 27B 밀집 모델에서 125B MoE 모델로 바꾸고 열흘 동안 조정했습니다. 오프로드 서빙에서 프리필과 디코드의 병목이 어디로 옮겨가는지, 서빙 바깥의 어떤 구성이 성능을 정하는지 정리합니다.

최근 대형 모델은 전문가 혼합(Mixture of Experts, MoE) 구조를 많이 씁니다. Qwen3.8 Flash-Next는 125B 파라미터 중 토큰 하나에 6B만 활성화합니다. 가중치는 크지만 한 토큰의 계산은 작은 모델과 비슷하다는 뜻이고, 이 성질을 이용해 가중치 대부분을 램에 두고 GPU 한 장으로 서빙하는 엔진도 나왔습니다.
이 글은 RTX 3090 24GB 한 장과 램 128GB를 가진 집 서버에서, Ollama 위의 27B 밀집 모델을 Strata 엔진 위의 125B MoE 모델로 바꾸고 열흘 동안 조정한 기록입니다. 배운 것을 오프로드 서빙의 구성 요소별로 정리하고, 서빙 바깥의 구성이 성능에 어떻게 개입하는지 살펴보겠습니다.
오프로드 서빙
오프로드는 보통 “GPU 밖으로 내보낸다”는 뜻이지만, 이 글에서는 MoE 모델의 전문가 가중치를 램에 두고 필요할 때 GPU로 옮기거나 CPU가 그 자리에서 계산하는 서빙 방식을 뜻합니다. Strata는 llama.cpp나 Ollama와 무관한 자체 엔진으로, 이 방식에 맞춰 만들어졌습니다.
- 전문가 캐시: Flash-Next의 전문가 2만 4천 개 중 자주 불리는 8천 개쯤을 VRAM에 둡니다. 토큰 하나에 쓰이는 전문가는 10개 남짓입니다.
- 램: 나머지 1만 6천 개와 KV 캐시가 램에 있습니다. 컨텍스트를 262K로 잡아도 VRAM이 늘지 않습니다.
- PCIe: 캐시에 없는 전문가를 GPU로 퍼 나르는 길입니다. 프리필은 이 길의 대역폭이 정합니다.
- CPU: 캐시에 없는 전문가를 램에 있는 그대로 계산하는 쪽입니다. 디코드는 이 계산과 램 대역폭이 정합니다.
밀집 모델의 “속도 = 메모리 대역폭 ÷ 토큰당 읽는 바이트” 식이 여기서는 둘로 갈립니다. 디코드는 VRAM 캐시 적중률과 램 대역폭, 프리필은 PCIe 대역폭입니다. 밖에서 보면 OpenAI 호환 /v1을 주는 서버 하나라서, 앞단 게이트웨이의 별칭이 가리키는 백엔드만 바꿨고 앱 코드 변경은 0줄이었습니다.
측정 환경은 다음과 같습니다.
| 항목 | 값 |
|---|---|
| CPU | Intel i9-12900K (16코어 24스레드) |
| RAM | DDR4 128GB |
| GPU | RTX 3090 24GB |
| 모델 | Qwen3.8 Flash-Next, IQ3_XXS 양자화 GGUF, 약 80GB |
램 128GB는 전제 조건입니다. 모델 파일 80GB 중 50GB가 램에 상주합니다. 모델 선택에도 변경 이력이 있습니다.
- 초기에는 보류했습니다. 10월 2일 검토 때 llama.cpp 미병합, Ollama 미지원, Q4 파일 111GB가 램을 넘는다는 세 가지 이유였습니다.
- 자체 엔진에는 앞의 두 이유가 해당하지 않았습니다. ISTA-DASLab의 GSQ-RCO IQ3_XXS 양자화가 BF16 대비 과제 평균 99.4%를 내서 Q4도 필요 없었습니다.
- llama.cpp 미병합은 8월 27일에 이미 병합된 사실(PR #27742)을 2차 자료만 보고 잘못 적은 것이었습니다.
프리필: 전송 경로가 정한다
Strata는 프롬프트를 청크로 나눠 처리하고, 청크마다 VRAM에 없는 전문가 32GiB를 전부 GPU로 다시 퍼 나릅니다. 프리필 속도는 이 전송을 몇 번 하는지와 한 번에 얼마나 걸리는지로 정해집니다.
- 청크 크기: 전송 횟수를 정합니다. 기본값은 8,192입니다.
- fused 프리필: 청크 안의 연산을 합쳐 한 번에 처리하는 옵션입니다.
- PCIe 링크: 한 번의 전송 시간을 정합니다. 슬롯의 세대와 폭이 곧 대역폭입니다.
- CPU 분담 상한: 이 길이 이하의 프롬프트는 전송 없이 CPU가 프리필을 맡습니다.
초기에는 카드가 칩셋 쪽 PCIe 3.0 x4 슬롯에 있었습니다. 9월에 GPU 오류를 격리하느라 옮겨둔 것입니다.
- 링크 3.3GB/s, 프리필 중 PCIe 수신 3.45GB/s로 내내 포화였습니다.
- GPU 사용률은 100%인데 전력은 160~200W였습니다. 계산이 아니라 데이터를 기다리는 상태입니다.
- 4.7만 토큰 프롬프트가 601 tok/s로 78초, 수백 토큰 프롬프트도 첫 글자까지 12초였습니다. 27B 시절에는 1초 안팎이던 값입니다.
슬롯을 옮기기 전에 청크 크기부터 조정했습니다.
| 청크 크기 | 4.7만 토큰 프리필 | 시간 |
|---|---|---|
| 8,192 (기본) | 601 tok/s | 77.9초 |
| 16,384 | 1,035 tok/s | 45.2초 |
| 32,768 | 1,262 tok/s | 37.1초 |
| 32,768 + fused 프리필 | 1,372 tok/s | 34.1초 |
| 65,536 | 1,359 tok/s | 같음 |
- 설정 한 줄로 2.3배입니다. 65,536은 32,768과 같았는데, 청크가 1개가 돼도 전체 스트리밍 한 번은 남기 때문입니다.
- VRAM은 23.5GB 그대로이고, 문서상 품질 변화는 KL 0.02~0.04 범위 안입니다.
- 19만 토큰 프롬프트는 325초에서 115초가 됐습니다.
같은 날 저녁에 카드를 CPU 직결 PCIe 4.0 x16 슬롯으로 옮겼습니다. 설정은 바꾸지 않았습니다.
| 프롬프트 | x4 Gen3 (청크 조정 뒤) | x16 Gen4 | 변화 |
|---|---|---|---|
| 446 토큰, 첫 글자까지 | 약 12초 | 1.05초 | 11배 |
| 4.7만 토큰 | 1,378 tok/s (34초) | 2,639 tok/s (17.7초) | 1.9배 |
| 9.7만 토큰 | 1,787 tok/s (54초) | 2,765 tok/s (35초) | 1.5배 |
| 19만 토큰 | 1,708 tok/s (115초) | 2,623 tok/s (73.5초) | 1.5배 |
| 디코드 | 59~90 tok/s | 93~121 tok/s | 약 1.4배 |
- 전문가 32GiB를 퍼 나르는 시간이 10~12초에서 1.8초가 됐습니다.
- 병목이 PCIe에서 GPU 연산으로 옮겨갔습니다. 프리필 중 GPU가 328W로 전력 캡 330W에 닿습니다.
- 병목이 옮겨가면 버린 설정을 다시 봐야 합니다. CPU 분담 상한은 x4에서 손해라 1,024에 두었던 값인데, x16에서 3,072로 올리니 1
3천 토큰 프롬프트가 3.3초에서 1.11.7초로 줄었습니다.
하드웨어가 바뀌면 튜닝 표는 처음부터 다시 그려야 합니다.
디코드: 캐시 밖 전문가를 누가 계산하는가
디코드에서는 토큰마다 필요한 전문가 중 VRAM 캐시에 없는 것을 처리해야 합니다. Strata에는 이 몫을 GPU로 보낼지 CPU가 계산할지 정하는 비율이 있고, 보정 도구가 이 값을 기계에 맞춰 잡아 줍니다.
- GPU 전송 비율: 캐시 밖 전문가 중 GPU로 보내는 비율입니다. 기본값 0.55입니다.
- CPU 작업자 수: CPU가 계산할 때 쓰는 스레드 수입니다.
- MTP 드래프트: 다음 토큰을 미리 뽑는 투기적 디코딩용 보조 모델입니다.
- 엔진 버전: 디코드 기본값이 버전마다 바뀝니다.
다음은 x4 링크에서 보정 도구를 돌린 결과입니다. 13분이 걸렸고 그동안 서비스는 멈췄습니다.
| 캐시 밖 전문가를 GPU로 보내는 비율 | 디코드 |
|---|---|
| 0.55 (기본) | 36.6 tok/s |
| 0.09 (엔진 자동) | 63.3 tok/s |
| 0.00 | 64.0 tok/s |
- x4에서는 전문가를 GPU로 보내느니 CPU가 램에서 제자리 계산하는 쪽이 빨랐습니다. 0으로 두었습니다.
- CPU 작업자 수(4~15)는 차이가 없었고, “적응 티어” 옵션은 오히려 20% 느렸습니다.
- x16으로 옮긴 뒤 다시 돌려도 비율을 0에서 1.0까지 올릴수록 단조 감소였습니다(98.1 → 65.9 tok/s). 링크가 네 배 넘게 빨라져도 CPU 계산이 빨랐습니다.
- 보정 도구는 프리필에 효과가 없습니다. 문서 첫머리에 적혀 있는데 끝까지 읽지 않고 돌린 13분이었습니다.
- MTP 드래프트는 교체 뒤 수락률 82%였고 한국어에서 10p 올랐습니다.
엔진 버전은 사흘 간격으로 두 번 비교했습니다.
- v0.1.45: 디코드가 2~13% 느려서 채택하지 않았습니다.
- v0.2.2: RTX 30 전용 디코드 기본값이 들어가 98.3에서 105.4 tok/s로 7.3% 빨라져 채택했습니다. 6시간 소크에서 오류 0입니다.
- 개발자 측정은 3060 +10.2%, A4000 +13.0%였고 3090 수치는 없었습니다. 직접 재지 않았으면 기존 옵션 하나가 새 기본값과 충돌해 꺼진다는 것도 몰랐을 것입니다.
CPU: 뜨거운 것과 느린 것은 다르다
27B 밀집 모델에서는 놀던 CPU가 디코드마다 물리 코어 16개를 전부 쓰기 시작했습니다. 패키지 온도 87~94°C에 스로틀링이 걸려 80W부터 241W까지 전력 캡을 스윕했습니다.
| CPU 전력 캡 | 디코드 | 패키지 최고 / 중앙 온도 | 실제 전력 |
|---|---|---|---|
| 241W (BIOS 기본) | 93.1 tok/s | 88 / 78.5°C | 158W |
| 130W | 95.2 tok/s | 87 / 75°C | 129W |
| 100W (채택) | 92.2~92.8 tok/s | 70~73 / 65°C | 99W |
- 100W로 묶어도 디코드는 1% 안에서 그대로였고, 4.8만 토큰 프리필도 16.14초에서 16.10초였습니다.
- 디코드에서 CPU는 코어 전부를 잠깐씩 쓰는 모양이라, 클럭이 조금 내려가도 GPU를 기다리는 시간 안에 끝납니다.
- 프리필은 CPU를 60W밖에 쓰지 않아 캡과 무관합니다.
thermald는 94°C에서 아무것도 하지 않으면서 설정한 캡만 241W로 되돌리고 있어서 껐습니다.
서빙 바깥의 구성
열흘 동안 바꾼 것 중 절반은 엔진 설정이 아니었습니다. 게이트웨이, 자동화, 동시 처리 세 가지입니다.
게이트웨이
앱은 게이트웨이의 별칭만 부르므로, 게이트웨이가 요청을 어떻게 손대는지가 그대로 모델 동작으로 보입니다.
- 파라미터 전달: 모르는 파라미터를 조용히 버리는 설정이 켜져 있었고 그 목록에
reasoning_effort가 있었습니다. 직접 호출은 사고가 73자 나오는데 게이트웨이 경유는 0자였습니다. 9월 30일부터 한 번도 엔진에 도착한 적이 없던 파라미터입니다. - 샘플링 기본값: 서버 기본값이 탐욕 디코딩이어서 에이전트가 같은 명령을 370번 반복했습니다. temperature 1.0으로 바꾼 뒤 멈췄습니다.
- 출력 언어: 한국어 답에 한자가 섞였습니다. 한자 토큰 55,659개에 logit_bias −3을 걸어 60회 중 0회가 됐습니다. −100은 과했습니다.
- 버전 고정: 게이트웨이 이미지를 태그 대신 digest로 고정했습니다. 전날 상류 안정 태그가 오염된 공급망 사고가 있었고, 마이그레이션은 67건이었습니다.
모델을 바꾸고 가장 많이 한 일은 벤치마크가 아니라, 바꾸기 전부터 조용히 고장 나 있던 것을 찾는 일이었습니다. 직접 호출과 게이트웨이 경유를 같은 요청으로 비교하는 것이 가장 빠른 확인 방법이었습니다.
자동화
물리 구성을 바꾸면 그 구성을 전제로 짠 자동화가 천장이 됩니다.
- 9월에 “PCIe 링크 목표 속도가 Gen1로 떨어지면 Gen3로 올려라”라는 2분짜리 타이머를 만들어 두었습니다. 당시 슬롯은 Gen3가 최고였으므로 바닥값이 3이었습니다.
- 카드를 Gen4 슬롯으로 옮긴 날 밤 타이머가 Gen3로 되돌렸습니다. 1시간 반 동안 디코드 93이 75~77로, 4,500토큰 프리필 3.5초가 7.5초로 됐습니다.
- 전력 캡 실험 결과가 전부 이상하게 나와서야 찾았습니다. 9월의 바닥이 10월의 천장이 된 것입니다. 이후로는 측정 전에
nvidia-smi로 링크 세대부터 확인합니다. - 재시작 상한도 같은 종류입니다. 20분에 8번 넘게 재기동하면 systemd 상한에 걸려 서비스가 비는데, 같은 날 두 번 걸렸습니다. 둘 다 직접 넣어 둔 상한이었습니다.
- 되돌릴 길은 먼저 깔았습니다. 옛 모델 블롭 33GB를 지우지 않고 한 명령으로 돌아갈 수 있게 두었고, 새 엔진 버전은 실행 스크립트를 따로 두고 심링크만 바꿉니다. 롤백은 40초입니다.
동시 처리
서브에이전트를 여럿 띄우는 일이 잦아서 동시 처리 슬롯을 2~4로 열어 봤습니다.
- 슬롯 하나가 전문가 캐시 500칸을 차지합니다. 캐시가 줄면 CPU가 계산할 몫이 늘어 혼자 쓸 때도 21~26% 느려졌습니다.
- 4명 합산 처리량도 32~41% 줄었습니다. 이 기계에서 MoE 오프로드는 1슬롯이 맞습니다.
- VRAM 여유가 441MiB뿐이라 다른 모델을 같은 카드에 올릴 수 없습니다. 비전 인코더는 CPU에 붙여 켰습니다. 그림당 1,024토큰으로 640×480 사진은 2.7초, 1080p 스크린샷은 11.6초입니다. 300토큰으로 줄이면 없는 글자를 지어내서 1,024를 유지했습니다.
설계 원칙
프리필 조정에서 소프트웨어는 2.3배, 슬롯 이동은 1.9배였습니다. 하나는 설정 한 줄이고 하나는 드라이버를 들고 10분인데, 비싼 순서와 효과 순서가 맞지 않았습니다. 두 경우의 차이는 병목이 어디에 있는지에 있습니다. 병목이 버스에 있으면 설정은 전송 횟수만 줄일 수 있고, 한 번의 전송 시간은 배선만이 줄입니다. 즉, 오프로드 서빙에서 프리필 튜닝은 설정이 아니라 슬롯에서 시작합니다.
보정 도구와 타이머는 다른 종류의 교훈입니다. 보정 도구는 문서가 프리필에 효과 없다고 적어 두었고, 타이머는 직접 만든 것이었습니다. 둘 다 새 구성이 전제를 바꿨는데 전제를 다시 읽지 않아서 생겼습니다. 즉, 구성을 바꿀 때는 그 구성을 전제로 쓴 문서와 자동화를 같이 봐야 합니다.
결과
열흘 전후의 값입니다.
| 전 (Ollama · Qwen3.8-27B) | 후 (Strata · Qwen3.8 Flash-Next 125B MoE) | |
|---|---|---|
| 모델 크기 | 27B 밀집 | 125B, 토큰당 6B 활성 |
| 디코드 (실사용 중앙값) | 68 tok/s | 88~94 tok/s |
| 디코드 (벤치 최고) | 54 tok/s (262K 채움) | 105 tok/s |
| 프리필, 4.7만 토큰 | 900~1,200 tok/s | 2,639 tok/s |
| 첫 글자까지 (수백 토큰 프롬프트) | 1초 안팎 | 1.05초 |
| 컨텍스트 | 245,760 | 262,144 |
| VRAM / RAM | 23.5GB / 9GB | 23.5GB / 56GB |
| 지능 지수 (Artificial Analysis) | 33.7 | 39.8 |
| 동시 처리 | 1슬롯 | 1슬롯 |
- 모델은 네 배 넘게 커졌고 디코드는 1.4배, 긴 프롬프트의 프리필은 2배 남짓 빨라졌습니다.
- 같은 모델을 공식 API로 부르면 52 tok/s 정도이므로 3090 한 장이 1.8배 빠릅니다.
- 지능은 Artificial Analysis 지수로 39.8입니다. DeepSeek V4.1 Flash(39.5)·GPT-5.5(38.4)와 같은 급이고 Opus 5.5(57.6)의 70%쯤입니다. 비용은 전기이고 동시 사용자는 한 명입니다.
지금 상주 상태에서 잰 값입니다.
| 항목 | 값 |
|---|---|
| 디코드, 벤치 (200~300토큰) | 105 tok/s |
| 디코드, 실사용 짧은 답 중앙값 (6시간 36회) | 88.4 tok/s (58.8~107.1) |
| 프리필 1천 / 8천 / 3.2만 토큰 | 816 / 1,727 / 2,790 tok/s |
| 프리필 9.7만 / 19.3만 토큰 | 2,784 / 2,646 tok/s |
| 첫 글자까지 (수백 토큰) | 약 1초 |
| 같은 문맥 이어 부르기 (4.7만 토큰 재호출) | 0.1초 (접두 32,768토큰 캐시) |
| 그림 1장 | 첫 장 15초, 같은 그림 재요청 3.3~3.6초 |
| VRAM | 23.5 / 24GB (여유 441MiB) |
| RAM | Strata 56GB (전문가 49.8GiB 고정), 상시 여유 약 40GB |
| CPU | 디코드 중 99W, 65~73°C |
| GPU | 프리필 중 328W, 최고 83°C |
| 적재 | 80~105초, 새 버전은 36초 |
한계
이 구성이 전제로 하는 조건과 아직 해결하지 못한 것입니다.
- 동시 처리: 1슬롯이므로 사람 하나와 에이전트 하나가 겹치면 뒤 요청이 줄을 섭니다. 벤치 중에 10만 토큰짜리 턴을 보내던 날 “이어 묻기 2초”가 38초로 찍혔습니다.
- 사고 모드 high: 정답이 하나인 긴 독해 객관식에서 문항당 209초, 176초를 생각만 하다 출력 상한 1만 6천 토큰에 걸려 답을 내지 못했습니다. 코딩 382턴에서는 최대 6,416토큰이라 코딩에서는 나지 않습니다. 벤치 설계는 medium 이하로 잡습니다.
- 긴 지문 독해: 사고를 끄고 푼 수능형 객관식 50문항에서 27B의 37개가 31개로 줄었습니다. 토큰마다 6B만 쓰는 모델의 한계로 읽습니다.
- 출력 언어: logit_bias는 모델을 바꾸면 한자 토큰 목록을 다시 만들어야 합니다.
- VRAM: 여유가 0이라 임베딩 모델 같은 것을 같은 카드에 올릴 수 없습니다.
- 램: 전문가 50GiB가 상주하고 캐시 설정에 16GiB를 더 쓰므로 64GB로는 시작할 수 없습니다.
마무리
24GB GPU 한 장에 125B MoE 모델을 오프로드 서빙으로 올리면서 조정한 내용을 구성 요소별로 정리했습니다. 프리필은 PCIe 전송이 정하므로 청크 크기와 슬롯을 먼저 봤고, 디코드는 캐시 밖 전문가를 CPU가 계산하는 쪽이 빨라서 GPU 전송 비율을 0으로 두었습니다. CPU는 전력 캡 100W로 묶어도 속도가 그대로였습니다.
서빙 바깥에서는 게이트웨이가 파라미터를 버리던 것, 옛 슬롯을 전제로 한 타이머, 동시 처리 슬롯이 성능을 정했습니다. 열흘 동안 효과가 큰 순서는 슬롯 이동(프리필 1.9배, 첫 글자 11배), 청크 크기(2.3배), 엔진 새 버전(7.3%)이었고, 비싼 순서와는 맞지 않았습니다.
오프로드 서빙을 구성할 때는 프리필의 병목이 설정에 있는지 버스에 있는지, 그리고 새 구성이 기존 문서와 자동화의 전제를 바꾸지 않았는지 확인해보시길 바랍니다.
참고자료
- Artificial Analysis, Intelligence Index (모델 지능 지수, 2026-10-13 조회)
- Niko1221, Strata — docs/HOW_IT_WORKS.md(전문가 캐시·오프로드 구조), docs/DETAILS.md(프리필 청크·보정 도구)
- ISTA-DASLab, Qwen3.8-Flash-Next-GSQ-RCO-GGUF (IQ3_XXS 양자화 품질 평가)
- ggml-org, llama.cpp PR #27742 (Flash-Next 지원 병합)