1차 기록에서 GB10 2노드에 DeepSeek V4 Flash 0731 원본 FP8을 올리고 코딩 에이전트로 쓰는 상태까지 만들었다. 남은 질문은 하나였다. 추론(thinking)을 켠 실사용 조건에서 더 빠르게 만들 수 있나.
설정과 코드를 바꿔 가며 서버를 40번 넘게 다시 띄웠다. 얻은 것은 두 줄이고, 답을 찾은 질문이 하나 있으며, 기각한 가설이 열 개쯤 된다.
얻은 것. max_num_seqs를 레시피 기본값 6에서 8로 올리면 동시 8건 처리량이 121.7 → 163.8 tok/s (+35%)다. 대화형 응답에는 손해가 없다. 기동 직후 워밍업을 흘리면 첫 토큰까지가 3.51초에서 0.38초로 줄어든다.
답을 찾은 질문. 커뮤니티의 80 tok/s대 수치와 우리 56의 격차는 추론을 켰는지 여부였다. 추론을 끄면 우리도 73.0이 나온다. 원인은 추측디코딩 수락률이다.
얻지 못한 것. 추론을 켠 단일 스트림 속도는 어떤 설정으로도 개선되지 않았다. NCCL 프로토콜, 컴파일 융합 패스, draft 샘플링 방식, 하이브리드 양자화 — 전부 시험했고 전부 기각이다.
부산물. 같은 설정으로 서버를 다시 띄우기만 해도 성능이 최대 15% 달라진다. 이 스택에서 단일 실행으로 발표된 tok/s는 그만큼 믿을 수 없다. 그리고 발열을 쫓다가 vLLM의 대기 루프 문제를 찾았다 — 별도 기록.
본론에 들어가기 전에 두 가지를 고쳐야 했다.
런타임이 로그에 이렇게 남긴다.
CuTeDSL JIT compilation during inference: W4A16FusedMoeKernel.
This causes a latency spike; consider extending warmup to cover this shape/config.
추론 도중에 MoE 커널을 컴파일한다는 뜻이다. 원인을 이미지 안에서 추적하니 b12x MoE 커널이 워밍업 등록 대상이 아니었다. 워밍업 프로바이더를 등록하는 곳은 MLA 프리필 백엔드 하나뿐이고 그마저 FA4 경로일 때만인데, 이 모델은 sparse MLA 경로라 아무것도 등록되지 않는다.
프레임워크 안에 손잡이가 없으므로 밖에서 메웠다. 기동 직후 실제로 쓰는 동시성과 프롬프트 길이 조합을 한 번씩 흘려 컴파일을 서비스 시간에서 기동 시간으로 옮긴다.
| 첫 토큰까지 (동시성 1) | TTFT |
|---|---|
| 워밍업 없음 | 3.51초 |
| 기동 직후 워밍업 | 0.38초 |
같은 설정, 같은 프롬프트. 차이는 기동 뒤 더미 요청을 흘렸는지뿐이다. 처리량은 바뀌지 않는다 — 일회성 비용이라 긴 벤치에서는 희석되고, 체감이 달라지는 것은 세션의 첫 응답이다.
초기 스크리닝에서 효과가 없는 설정값 세 개를 연달아 측정한 적이 있다. 결과적으로 같은 구성을 네 번 측정한 셈이 됐는데, 그 네 값이 동시성 1에서 44.4·49.3·53.8·58.1 tok/s로 흩어졌다. ±13%다.
중요한 건 이 변동이 부팅 안에서가 아니라 부팅 사이에서 생긴다는 점이다. 한 번 띄운 뒤 여러 번 재면 편차가 2~5%로 작다. 그래서 최종 실험은 각 구성을 세 번 따로 띄워서 측정하고, 라운드마다 구성 순서를 회전시켜 시간에 따른 표류가 특정 구성에 몰리지 않게 했다.
그림 1. 같은 설정으로 서버를 다시 띄웠을 때의 최대 편차. 동시성 1에서 최대 15.5%까지 벌어진다. 동시성 8에서는 3~7%로 줄어든다 — 포화 상태에서는 메모리 대역폭이 병목이라 부팅마다 달라지는 요소가 씻겨 나간다.
판정 기준도 여기서 정해진다. 두 구성의 재기동 간 [최소~최대] 범위가 겹치면 구분하지 못한 것으로 적는다. 표본이 세 개뿐이라 정규성을 가정하는 검정보다 이쪽이 정직하다.
1차 기록을 올린 뒤 가장 많이 받은 질문이 이것이었다. 커뮤니티에 도는 수치는 동시성 1에서 82.4나 95.9인데 우리는 왜 56인가.
답은 추론(thinking)이다. 이 모델은 추론 모델이고, 추론을 켜면 디코드가 느려진다. 그런데 공개된 수치 대부분은 추론 상태를 밝히지 않는다.
같은 부팅 안에서 요청 파라미터(chat_template_kwargs)만 바꿔 max ↔ off를 세 세트 교대 측정했다 — 재기동 변동이 개입하지 않는 비교다.
그림 2. 추론 단계별 스트림당 디코드 속도. 같은 부팅·같은 커널에서 요청 파라미터만 바꿔 잰 값이다.
| 추론 | 동시성 1 | 동시성 8 | 추론 토큰 비중 |
|---|---|---|---|
| 끔 | 73.0 +30% | 32.2 +46% | 0% |
high | 58.0 구분 못 함 | 25.6 +16% | 60% |
max · 우리 실사용 | 56.3 | 22.1 | 86~100% |
스트림당 tok/s. 각 3세트(high는 1세트)의 중앙값. 범위가 겹치지 않는 것만 백분율을 적었다.
서버가 남기는 위치별 draft 수락률을 추론 모드별로 갈라 보면 이렇다.
| 추론 | 1번째 | 2번째 | 3번째 | 4번째 | 5번째 | 실효 수락 길이 |
|---|---|---|---|---|---|---|
| 끔 | 0.963 | 0.866 | 0.781 | 0.511 | 0.473 | 4.67 |
high | 0.884 | 0.766 | 0.605 | 0.455 | 0.393 | 4.14 |
max | 0.870 | 0.736 | 0.543 | 0.398 | 0.315 | 3.87 |
추론 문장은 draft 모델이 맞히기 어렵다. 추론을 끄면 첫 자리 적중이 0.870에서 0.963으로 오르고, 검증 한 스텝에서 건지는 토큰이 3.87개에서 4.67개로 늘어난다. 우리 추론-끔 수치 73.0은 커뮤니티의 82.4와 12% 차이인데, 그쪽은 "peak" 표기다. 격차의 정체는 대부분 추론이었다.
유보를 하나 달아야 한다. 이 벤치의 프롬프트는 번호 붙인 단어 목록을 요구한다. 추론을 끄면 출력이 매우 규칙적이어서 draft가 유난히 잘 맞는다. 실제 코딩 작업에서는 격차가 이보다 작을 수 있다.
실용적으로는 reasoning_effort를 max에서 high로만 낮춰도 동시성 구간에서 16%를 얻는다. 동시성 1에서는 차이가 없다. 다만 이건 추론 깊이를 줄이는 품질 거래라 기본 권고에는 넣지 않는다.
그림 3. 동시 요청 8건일 때의 집계 처리량. 점이 재기동 3회의 중앙값, 가로 막대가 범위다. 진한 것이 권고 구성.
| 구성 | 동시 1건 | 4건 | 8건 | 기동 |
|---|---|---|---|---|
| seqs 6 · k 5 레시피 기본값 | 58.1 58~59 | 117.1 115~118 | 121.7 120~126 | 3 |
| seqs 8 · k 5 권고 | 58.2 52~59 | 114.3 114~116 | 163.8 158~169 | 3 |
| seqs 12 · k 5 | 54.6 52~59 | 116.1 115~117 | 165.8 156~167 | 3 |
| seqs 8 · k 3 | 51.4 51~52 | 112.9 108~114 | 160.9 157~165 | 3 |
| seqs 8 · k 7 | 52.6 51~58 | 106.4 106~108 | 139.3 138~142 | 3 |
집계 처리량 tok/s. 위 칸은 재기동 3회 중앙값, 아래 작은 글씨는 최소~최대. 추론은 reasoning_effort=max 고정.
동시 8건에서 max_num_seqs 6과 8의 범위는 전혀 겹치지 않는다 (120~126 대 158~169). 당연한 결과이기도 하다. 6이면 8건 중 2건이 큐에서 기다린다. 요점은 레시피 기본값이 동시 6건까지만 감당한다는 것이고, 코딩 에이전트를 여러 개 띄우면 그 지점에서 막힌다.
중요한 건 공짜라는 점이다. 동시성 1과 4에서는 6·8·12가 서로 구분되지 않는다. 다만 8에서 12로 더 올리는 것은 의미가 없고(범위 겹침), 동시 8건에서 스트림당 속도는 seqs 6이 25.3 tok/s로 가장 높다 — 집계와 개별 응답 속도는 맞바꿈 관계다. 여러 에이전트를 돌리면 8, 한 사람이 붙어 쓰면 6이 낫다.
DSpark 추측디코딩의 깊이(MTP_NUM_TOKENS, k)는 레시피 기본이 5다. 위아래로 흔들어 봤다.
위치별 수락률이 이유를 보여준다. 깊이를 늘리면 뒤쪽 자리의 기여가 0.2 수준으로 떨어지는데, 그 자리를 위해 draft 연산과 메모리 트래픽은 그대로 늘어난다. 이 모델은 층마다 전문가 256개 중 6개만 쓰는 희소 MoE라, 한 스텝에 여러 토큰을 검증하면 읽어야 할 전문가의 합집합이 커진다. 깊이를 더하면 이 합집합이 계속 자라는데 수락 기여는 급감한다.
그림 4. 요청 1건일 때. 다섯 구성이 모두 51~59 tok/s에 몰려 있고 범위가 서로 겹친다.
동시성 1에서 유의한 차이가 난 쌍은 열다섯 번의 기동을 통틀어 하나뿐이었다. 재기동 간 변동이 설정 차이보다 크기 때문이다. 이건 "튜닝 여지가 없다"는 뜻이면서 동시에 "이 스택의 단일 스트림 수치는 단일 실행으로 비교할 수 없다"는 뜻이다. 커뮤니티에 돌아다니는 값들 — 82.4, 95.9 — 도 같은 조건에서 측정된 것이다.
메모리는 LPDDR5X 8533 MT/s × 256bit = 273 GB/s다. 가중치 155.4 GiB의 구성을 safetensors 헤더에서 직접 세면 이렇다.
| 구성요소 | GiB | 비중 |
|---|---|---|
| 라우팅 전문가 (43층 × 256개 × 12.8 MiB) | 137.1 | 88.2% |
| MTP draft (추측디코딩용) | 10.1 | 6.5% |
| 어텐션 MLA | 5.1 | 3.3% |
| 임베딩·출력 | 2.0 | 1.3% |
| 공유 전문가·dense FFN | 1.0 | 0.6% |
스텝 시간 ~68ms(57 tok/s ÷ 실효 수락 3.9)를 분해하면 가중치 읽기가 27~39ms, draft ~2ms, 노드 간 all-reduce 5~9ms, 그리고 어디에 쓰이는지 잡히지 않는 시간이 20~30ms다. 대역폭이 포화조차 안 돼 있을 가능성이 높다는 뜻이고, 실제로 통신·설정 쪽 손잡이를 아무리 돌려도 안 움직인 이유가 여기 있다 — NCCL을 전부 없애도 110 tok/s에는 못 간다.
수락 3.9 고정이면 스텝 45ms에 87, 35ms에 111 tok/s다. 수락을 추론-끔 수준(4.67)까지 올리면 같은 스텝에서 104~130이 된다. 110~130이 이 하드웨어의 상한이고, 스텝 단축과 수락 개선을 둘 다 이겨야 닿는다.
기각도 결과다. 각각 최소 한 번의 재기동 측정으로 판정했다.
FORCE_BLOCKS_MAX_M 16→48·64, FORCE_BLOCKS_PER_SM — 전부 노이즈 범위.fuse_allreduce_rms(이 빌드에 존재, 기본 꺼짐)를 켜도 c1 51.1 / c8 146.1 — 패턴 미매칭이거나 무이득.ENABLE_VLLM_GB10_PATCH=1). 전문가 가중치가 어차피 로드 때 MXFP4로 변환돼 있어 이득 없음 (c1 53.2 / c8 154.9).온도를 쫓다가 이 기록에서 가장 큰 발견이 나왔다. 부하 중 SoC가 96°C까지 가는데 열원이 GPU가 아니라 대기 루프를 도는 CPU 성능코어였고, 원인은 vLLM의 기본값 한 줄이었다. 수정 후 CPU 낭비는 12분의 1, SoC 최고는 96→68°C가 됐다 — 성능 손실 없이.
추적 과정(팬 A/B, 코어별 계측, py-spy, 클럭 실험)은 분량이 있어 별도 기록으로 뺐다.
설정 스크리닝이 전멸한 지금, 남은 큰 레버는 하나다. 추론 문장에서 3.87로 떨어지는 실효 수락 길이를 끌어올리는 것 — 즉 drafter를 thinking 텍스트로 미세조정하는 것이다. 4.4~4.7에 도달하면 디코드 +15~25%다.
조사해 보니 길이 있다. DeepSeek이 공개한 DeepSpec은 DSpark 계열 추측디코딩의 학습·평가 코드베이스이고, 공개 drafter들은 non-thinking 출력으로 학습됐으며 문서가 도메인·thinking 모드에서는 재미세조정을 권한다. 우리 관측 — 추론을 끄면 수락이 4.67로 뛴다 — 과 정확히 맞는 그림이다.
drafter는 전체 모델이 아니라 1층짜리 MTP 모듈(10.1 GiB)이라, 데이터 생성(타깃 모델이 thinking으로 재생성한 트레이스를 teacher-forcing 프리필로 덤프)과 학습 모두 이 2노드와 보조 장비로 감당된다. 착수 전에 싼 진단이 하나 있다 — draft가 쓰는 축소 어휘의 miss rate가 추론 텍스트에서 얼마나 되는지. 그게 천장이면 학습으로도 못 넘는다.
# .env.dspark — 레시피 기본값에서 한 줄만 바꾼다
MAX_NUM_SEQS=8 # 6 → 8. 동시 8건에서 +35%, 대화형 손해 없음
MTP_NUM_TOKENS=5 # 그대로 두는 게 맞다 (7은 −15~18%, 3은 이득 없음)
DEFAULT_THINKING=max # 실사용 조건
# 기동 직후 워밍업 — 실제로 쓰는 동시성 × 프롬프트 길이 조합을 한 번씩
# 첫 토큰까지 3.51초 → 0.38초 (기동 스크립트에 통합)
# 발열 처방 (vLLM 스핀 수정 + CPU 클럭 상한)은 별도 기록 참고
# → 부하 중 SoC 96°C → 68°C, 성능 손실 없음
max_num_seqs 하나뿐이었다. 나머지 상한은 수락률(추론 텍스트)과 스텝 오버헤드에 있다.DeepSeek V4 Flash-0731, 2× DGX Spark, 추측디코딩 켬, 동시성별 3회 평균(총 27회). 추론 off / high / max의 동시성 1 지속 처리량은 각각 64.1 / 53.9 / 56.5 tok/s.
x.com/LocalInference/status/20862927986771722822× DGX Spark용 DeepSeek V4 Flash 대규모 업데이트. 확인해 보니 권장 설정값과 이미지 태그는 그대로였고, 핵심은 긴 컨텍스트 디코드의 커널 디스패치 회귀 수정(Issue #22)이었다.
x.com/MiaAI_lab/status/20871168950905080222 Spark, 1M 컨텍스트에서 디코드 95.9 tok/s. 이 수치는 일반 CUDA 그래프(VLLM_USE_BREAKABLE_CUDAGRAPH=0) 덕이 크다 — breakable 대비 +28.6%. 우리 스택도 같은 설정이다.
2× Spark, 1M 컨텍스트에서 40~74 tok/s. "추론 모드는 속도를 거의 안 깎는다"고 했지만, 같은 부팅 안에서 교대 측정한 우리 결과는 30~46% 차이였다.
x.com/krunkosaurus/status/20870713731434867122노드 GB10(128GB LPDDR5X 각), ConnectX-7 200GbE 직결, vLLM TP=2 mp 백엔드(0.25.2 계열 커뮤니티 빌드), KV 캐시 nvfp4_ds_mla, MoE 백엔드 flashinfer_b12x(전문가 MXFP4 적재), GPU 클럭 상한 2000MHz, VLLM_USE_BREAKABLE_CUDAGRAPH=0. 모델은 원본 FP8 체크포인트(revision 고정, 해시 검증).
벤치는 짧은 프롬프트(약 350토큰)에 긴 생성으로 디코드에 맞췄다. 에이전트 사용에서는 컨텍스트가 누적되며 프리픽스 캐시로 흡수되므로 프리필은 측정 대상에서 뺐다. 추론을 max로 고정했기 때문에 생성 토큰은 사실상 전부 추론 토큰이다.
본실험은 구성 5개 × 재기동 3회(총 15회 기동), 부팅마다 워밍업 후 2라운드 중앙값, 라운드마다 구성 순서 회전. 추론 on/off와 후반 스크리닝은 각각 별도 부팅에서 측정. 모든 측정은 재기동 뒤 컨테이너의 실제 기동 인자를 파싱해 의도한 구성과 일치하는지 확인했고, 어긋난 측정은 버렸다.
전력은 GPU 레일 값만 노출된다(모듈 전력·hwmon 없음). 벽면 소비전력은 측정하지 못했다.