후속 보고: 4개의 DGX Spark에서 구동하는 GLM-5.2 NVFP4 — MTP 미스터리 해결, 이제 128K 컨텍스트에서 초당 약 24토큰 달성
Follow-up: GLM-5.2 NVFP4 on four DGX Sparks — the MTP mystery is solved, and it's now ~24 tok/s at 128K context
핵심 요약
vLLM 설정 오류를 수정하여 4개의 DGX Spark 환경에서 128K 컨텍스트로 초당 24토큰의 추론 성능을 확보했습니다.
- 성능 최적화 — vLLM의 SpeculativeConfig 설정 오류를 수정하여 추론 속도를 초당 15토큰에서 24토큰으로 개선함
- 버그 원인 — draft 모델의 병렬 설정이 타겟 설정과 동기화되지 않아 발생한 KV 캐시 및 어텐션 연산 오류를 해결함
- 하드웨어 효율 — 120W 전력 소모로 744B급 모델을 128K 컨텍스트에서 구동하는 효율적인 로컬 추론 환경을 구축함
- 추가 개선 — NCCL 채널 최적화 및 최신 업스트림 브랜치 적용으로 메모리 사용 효율과 안정성을 높임
후속 글: DGX Spark 4대에서 돌리는 GLM-5.2 NVFP4 — MTP 미스터리 해결, 이제 128K 컨텍스트에서 ~24 tok/s 나옴
이 글은 DGX Spark 4대에서 128K 컨텍스트로 GLM-5.2 NVFP4를 돌렸던 이전 글의 후속편이다. 이전 글 요약: MTP1을 쓰면 128K에서 ~15 tok/s가 나오긴 하는데, 128K 컨텍스트를 쓰거나 아니면 DCP1에서 32K로 ~23 tok/s를 뽑거나, 둘 중 하나만 골라야 하는 뼈아픈 트레이드오프가 있었다. 그리고 DCP4에서 MTP2/MTP3의 수락률(acceptance)이 박살 나는 게 "진짜 버그 같다"고 했는데, 30시간을 파고들어도 해결을 못 했었지.
근데 버그 맞았다. 해결했다. 트레이드오프는 이제 없다. 결과는 다음과 같다.
TL;DR
| old post (DCP4/128K/MTP1) | now (DCP4/128K/MTP3) | now (DCP4/128K/MTP4) |
|---|---|---|
| decode, short codegen (hot) | 14.5-15.2 tok/s | 22-23 tok/s |
| MTP acceptance per position | 0.74 (MTP1 only) | 0.90 / 0.79 / 0.67 |
| context | 131,072 | 131,072 |
| hardware | 4x GB10 Spark + MikroTik RoCE | unchanged |
수정: 프리필(prefill)은 여전히 ~475 tps, bs=3 디코드는 ~48 tps 정도 나옴.
맞다, MTP4다. 재귀적으로 재사용되는 단일 MTP 레이어는 4번째 토큰 위치에서 여전히 ~0.84 정도로 조건부 수락이 된다. 이건 내 RTX 6000 Pro 장비에서 MTP4가 피크를 찍는 거랑 똑같다. 설정 팁 하나 주자면, MAX_CUDAGRAPH_CAPTURE_SIZE는 num_speculative_tokens + 1보다 여유 공간이 좀 있어야 한다(드래프트 모델이 타겟보다 작은 캡을 가져오는데, 정확히 N+1이면 "No valid cudagraph sizes" 에러 뜨면서 시작부터 터짐). 난 MTP4에 10을 쓴다. 호스트 페이징이 튀면 가끔 성능이 출렁거릴 때가 있는데, 안전하게 가려면 MTP3, 피크 성능을 뽑으려면 MTP4를 쓰면 된다.
똑같은 장비, 똑같은 스위치, 똑같은 체크포인트, 똑같은 1.81 GB/rank KV 버짓이다. 성능 향상은 vLLM 설정 파일에서 빠져있던 코드 한 줄이랑 최신 업스트림 브랜치로 리베이스한 게 전부다. DCP1/32K 타협 설정은 이제 아무 의미 없다. 풀 컨텍스트에서 DCP4가 그냥 압살한다.
대체 버그가 뭐였나
이전 글에서 수락률이 0.9, 0.75^4, 0.6^4 정도로 나오는 것 같다고 썼고, 랭크 간 교차 효과(rank-intersection effect) 때문인가 싶었다. 지수 쪽 직관은 맞았다(실제로 DCP 월드 사이즈가 커질수록 데미지가 커지니까). 근데 메커니즘이 생각보다 훨씬 깊숙이 숨어있었다. 30시간 넘게 삽질해도 안 잡혔던 이유가 진짜 악질인데:
SpeculativeConfig.create_draft_parallel_config() 함수가 타겟 설정에서 필드를 복사해 드래프트 모델의 병렬 설정을 만드는데, decode_context_parallel_size 이 필드는 복사가 안 된다. 그냥 조용히 기본값인 1로 박혀버리는 거다. 내 스택이 사용하는 코드 경로에서는 이 값이 그대로 쓰인다.
그래서 TP4/DCP4 환경에서 MTP 드래프트 레이어의 되어 있는데(라이터 쪽은 타겟 설정을 따르니까), 정작 드래프트의 . 쿼리 올게더(all-gather)도 없고, LSE 머지도 없고, 글로벌 top-k 인덱스는 로컬 쿼터 캐시가 전체 캐시인 것처럼 처리됐다. 텐서 덤프를 떠보니 드래프트 포워드 과정에서 4개 랭크 중 3개가 아무것도 선택을 안 하고, 64개 헤드 중 48개에 대해 0으로만 가득 찬 어텐션을 뱉고 있더라.


