본문으로 건너뛰기

© 2026 Molayo

r/LocalLLaMA분석2026. 06. 29. 09:59

4x DGX Spark에서 실행되는 고품질 GLM-5.2 양자화(Quant) - 가이드, 결과 및 비교

요약

4대의 DGX Spark를 사용하여 GLM-5.2 모델을 NVFP4 방식으로 양자화하고 128K 컨텍스트에서 구동하는 기술 가이드입니다. DCP4와 MTP1 기술을 결합하여 메모리 제약을 극복하고 실질적인 서빙이 가능한 수준의 성능을 구현했습니다.

핵심 포인트

  • NVFP4 양자화를 통해 체크포인트 크기를 1.5TB에서 410GB로 약 3.7배 축소
  • DCP4(디코딩-컨텍스트 병렬성)를 활용하여 128K의 긴 컨텍스트 지원
  • GSM8K 정확도를 BF16 대비 2포인트 이내로 유지하며 효율성 확보
  • vLLM 포크와 희소 MLA 패치를 적용하여 최적화된 추론 성능 달성

저는 128K 컨텍스트(context)에서 4대의 DGX Spark를 사용하여 GLM-5.2 NVFP4를 구동했습니다. 이는 여전히 틈새적이고 해킹에 가까운 설정이지만, 이제는 단순한 동작 증명을 넘어 실제 서빙(serving)이 가능한 지점에 도달했습니다.

목표: 4x Spark에서 실행되는 고품질 4비트 양자화(quant). 모델: https://huggingface.co/Mapika/GLM-5.2-NVFP4

요약(TL;DR): fp8_ds_mla에서 128k 컨텍스트를 지원하며, c0 디코딩(decode) 시 약 15-16 tps를 기록하고, 긴 컨텍스트에서는 약 13 tps로 떨어집니다 (이는 매우 잘 유지되는 편입니다).

또 다른 요약(TL;DR): 또는 m3ultra 512GB의 경우, unsloth Q4_K_S 양자화(quant)를 "그냥 실행"할 수 있습니다. 자세한 내용은 하단에 있지만, MLA 커널(kernel) 지원 부족으로 인해 Mac은 c=0에서 아주 미세한 디코딩 우위를 점하며 시작하지만, 컨텍스트(ctx)가 증가함에 따라 성능이 매우 급격히 무너집니다.

하드웨어: 4x 표준 nVidia 브랜드 GB10 DGX Spark 및 Microtik RoCE 스위치.

카드(card)의 내용을 인용하자면:

MoE 전문가 FFN(routed + shared)은 NVFP4로 양자화(quantized)되었습니다. 어텐션(attention) (MLA + DeepSeek 스타일의 DSA lightning indexer), 라우터(router), 그리고 LM 헤드(LM head)는 BF16으로 유지됩니다. 이를 통해 체크포인트(checkpoint) 크기를 1.5 TB에서 410 GB로(~3.7배) 줄이면서도, GSM8K 정확도를 BF16 대비 약 2포인트 이내로 유지합니다.

이것이 흥미로운 이유: 모델이 너무 크고 메모리가 너무 타이트하여 Spark를 일반적인 개별 GPU(discrete-GPU) 하드웨어처럼 다룰 수 없습니다. 핵심적인 승인은 디코딩-컨텍스트 병렬성(decode-context parallelism)과 공격적인 시스템/Ray 메모리 트리밍(trimming)을 결합한 것입니다. DCP4는 디코딩 컨텍스트를 4개의 TP 랭크(ranks)에 걸쳐 샤딩(shards)하며, 이것이 128K를 가능하게 만드는 요소입니다. 그 후 MTP1이 사용 가능한 수준의 생성 속도를 충분히 회복시킵니다.

주요 결과:
4x DGX Spark / GB10, 노드당 GPU 1개
GLM-5.2 NVFP4 MTP 하이브리드 체크포인트 (hybrid checkpoint)
DCP + B12X 희소 MLA (sparse MLA) 패치가 적용된 vLLM 포크 (fork)
TP4 / PP1 / DCP4 / MTP1
fp8 KV 캐시 (KV cache), 랭크(rank)당 명시적 1.81 GB
최대 모델 길이 (max model len) 131,072
적합된 KV 토큰 (fitted KV tokens) 132,096
프리필 (prefill) 512 tokens/s
짧은 프롬프트 코드 생성 (codegen) 시 약 14.5-15.2 output tok/s
약간의 불일치가 발생할 수 있음, 예: 캐시되지 않은 112k 프롬프트의 경우:
(APIServer pid=736) INFO 06-29 00:12:03 [loggers.py:277] Engine 000: Avg prompt throughput: 511.6 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 7.2%, Prefix cache hit rate: 0.0%
(APIServer pid=736) INFO 06-29 00:12:13 [loggers.py:277] Engine 000: Avg prompt throughput: 512.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 11.1%, Prefix cache hit rate: 0.0%
(APIServer pid=736) INFO 06-29 00:12:23 [loggers.py:277] Engine 000: Avg prompt throughput: 511.9 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 15.0%, Prefix cache hit rate: 0.0%
(APIServer pid=736) INFO 06-29 00:12:33 [loggers.py:277] Engine 000: Avg prompt throughput: 511.9 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 18.8%, Prefix cache hit rate: 0.0%
(APIServer pid=736) INFO 06-29 00:12:43 [loggers.py:277] Engine 000: Avg prompt throughput: 512.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 22.7%, Prefix cache hit rate: 0.0%
(APIServer pid=736) INFO 06-29 00:12:53 [loggers.py:277] Engine 000: Avg prompt throughput: 409.6 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 25.8%, Prefix cache hit rate: 0.0%

(APIServer pid=736) INFO 06-29 00:13:03 [loggers.py:277] Engine 000: 평균 프롬프트 처리량 (Avg prompt throughput): 512.0 tokens/s, 평균 생성 처리량 (Avg generation throughput): 0.0 tokens/s, 실행 중 (Running): 1 reqs, 대기 중 (Waiting): 0 reqs, GPU KV 캐시 사용률 (GPU KV cache usage): 29.7%, 프리픽스 캐시 적중률 (Prefix cache hit rate): 0.0%
(APIServer pid=736) INFO 06-29 00:13:13 [loggers.py:277] Engine 000: 평균 프롬프트 처리량 (Avg prompt throughput): 511.9 tokens/s, 평균 생성 처리량 (Avg generation throughput): 0.0 tokens/s, 실행 중 (Running): 1 reqs, 대기 중 (Waiting): 0 reqs, GPU KV 캐시 사용률 (GPU KV cache usage): 33.6%, 프리픽스 캐시 적중률 (Prefix cache hit rate): 0.0%
(APIServer pid=736) INFO 06-29 00:13:23 [loggers.py:277] Engine 000: 평균 프롬프트 처리량 (Avg prompt throughput): 512.0 tokens/s, 평균 생성 처리량 (Avg generation throughput): 0.0 tokens/s, 실행 중 (Running): 1 reqs, 대기 중 (Waiting): 0 reqs, GPU KV 캐시 사용률 (GPU KV cache usage): 37.5%, 프리픽스 캐시 적중률 (Prefix cache hit rate): 0.0%
(APIServer pid=736) INFO 06-29 00:13:33 [loggers.py:277] Engine 000: 평균 프롬프트 처리량 (Avg prompt throughput): 409.5 tokens/s, 평균 생성 처리량 (Avg generation throughput): 0.0 tokens/s, 실행 중 (Running): 1 reqs, 대기 중 (Waiting): 0 reqs, GPU KV 캐시 사용률 (GPU KV cache usage): 40.6%, 프리픽스 캐시 적중률 (Prefix cache hit rate): 0.0%
(APIServer pid=736) INFO 06-29 00:13:43 [loggers.py:277] Engine 000: 평균 프롬프트 처리량 (Avg prompt throughput): 512.0 tokens/s, 평균 생성 처리량 (Avg generation throughput): 0.0 tokens/s, 실행 중 (Running): 1 reqs, 대기 중 (Waiting): 0 reqs, GPU KV 캐시 사용률 (GPU KV cache usage): 44.5%, 프리픽스 캐시 적중률 (Prefix cache hit rate): 0.0%
왜 409로 떨어지며, 왜 이렇게 일관되게 나타날까요? 확실하지 않습니다. 그리고 409와 512 사이의 수치는 이상할 정도로 일관되게 불일치합니다.
여기서 약간의 주의사항(asterisk)을 덧붙이자면, 저는 보통 fp8 KV 캐시가 품질 측면에서 좋지 않은 계획이라고 생각하지만, 제 견해로는 B12X_MLA_SPARSE를 사용하는 fp8_ds_mla 포맷은 단순한 일반적인 텐서 스케일링(tensor-scaled) fp8이 아닙니다. 이것은 별도의 포스팅 주제가 될 수 있습니다.
이 설정은 단순히 순정(stock) vLLM이 아닙니다. dark-devotion DCP 작업, B12X sparse MLA 구성 요소, FlashInfer/CUTLASS MoE, 그리고 멀티 노드 Ray 시작 시 멈춤 현상이 발생하던 TP/DCP 메시지 큐 브로드캐스터(message-queue broadcaster) 경로를 비활성화하기 위한 작은 Spark 전용 수정 사항이 포함된 패치된 vLLM 브랜치를 사용합니다. Spark 패브릭(fabric) 상에서 NCCL/RDMA는 활성화된 상태로 유지됩니다.
따라서 이를 실행할 기회라도 얻으려면, 가장 먼저 해야 할 일은 가지치기(prune)이며, 정말로 철저한 가지치기를 해야 한다는 뜻입니다.

Ray 설정은 의도적으로 매우 작게 구성되었습니다:
Dashboard 비활성화
Log monitor 비활성화
Usage stats 비활성화
Object store 128 MiB
Object spilling to /var/tmp/ray-spill
노드당 1 CPU 및 1 GPU 할당
host networking 및 host IPC 사용
OS 또한 중요합니다. 저는 cups, avahi, bluetooth, ModemManager, colord, fwupd, packagekit, desktop portal/pipewire 관련 요소 등 불필요한 headless-node 서비스를 비활성화했습니다. 중요: 이 작업은 데스크톱 GUI를 비활성화하므로, 반드시 headless 추론 노드에서만 수행하십시오. Spark 통합 메모리(unified memory) 환경에서는 몇 GB의 무작위 Linux/userland 오버헤드가 모델을 올릴 수 있느냐 실패하느냐를 결정짓는 차이가 될 수 있습니다.

이를 통해 무엇을 얻었을까요?
다음과 같이 읽어야 할 방식으로 분류된 몇 가지 측정 수치입니다:
짧은 코드 생성(codegen) 디코딩, MTP1: 약 14.5-15.2 tok/s
긴 프롬프트 프리필(prefill): 16K-112K 테스트에서 약 450-500 input tok/s
Post-TTFT 디코딩: 32K-112K 프롬프트 크기에서 약 13 tok/s
동시성(concurrency)에 대한 중요한 주의사항: 128K 프로필은 MAX_NUM_SEQS=1이므로, 동시 요청은 대기열(queue)에 쌓입니다. 이는 단일 긴 문맥(single-long-context)을 위한 레시피이지, 배치 서빙(batch-serving)을 위한 레시피가 아닙니다. 배치 지향적인 변형을 만들려면 MAX_NUM_SEQS를 높이고, 아마도 최대 문맥(max context)을 낮춤으로써 KV 예산을 다시 맞추어야 합니다. 이 부분은 독자의 몫으로 남겨둡니다. 하지만 제가 수정한 커스텀 부분들을 고려할 때, 저는 여기서 결코 자동으로 정확하다고 가정하지 않을 것입니다.

작동하지 않았던 것들:
128K에서의 BF16 KV: 충분한 여유 공간(headroom) 없이 모델이 들어가지 않았습니다.
DCP4/MTP3: 이후의 투기적 위치(speculative positions)가 수락(acceptance) 단계에서 붕괴되었습니다.
DCP4/MTP2: 때때로 경쟁력은 있었으나, 기본값으로 설정할 만큼 안정적이지 않았습니다.
NCCL_IB_DISABLE=1: LLM을 활용해 튜닝을 도와달라고 하면 여전히 이를 해제하는 경향이 있습니다(점점 나아지고는 있습니다). 그냥 안 된다고 하십시오. Spark에는 InfiniBand가 없지만 상호 연결(interconnect)은 작동합니다.
기본 컨테이너(Stock container) 가정: 이 스택을 구동하기에는 충분하지 않습니다.

만약 문맥(context)을 32k로 줄인다면 DCP=1을 실행할 수 있으며, 이 경우 약 27 tps를 얻을 수 있었습니다. 따라서 DGX Spark에서는 매우 실질적이고 고통스러운 트레이드오프(tradeoff)가 존재합니다.

제 미션의 일부는 여기서 곧바로 REAP 모델로 넘어가지 않는 것이었습니다.

한 가지 중요한 실무적 세부 사항: 실제로 model.layers.78.*를 포함하고 있는 하이브리드 체크포인트(hybrid checkpoint)를 사용하십시오. 베이스 GLM 체크포인트는 실제 MTP 레이어 없이 MTP 메타데이터만 표시할 수 있습니다. 이 설정은 정확히 하나의 MTP 레이어를 가지고 있으므로, MTP1이 깔끔한 프로덕션(production) 지점입니다. MTP2/MTP3는 동일한 단일 단계 예측기(one-step predictor)를 재귀적으로 재사용하며, 이는 연구 영역에 속합니다.

여기서 한 가지 언급하자면, 정말 버그처럼 보이는 무언가가 있습니다. 이를 파악하기 위해 30시간 동안 시도해 보았지만 근본적인 원인을 찾아낼 수는 없었습니다. 하지만 제가 관찰한 것은 수락 붕괴(acceptance collapse) 현상으로, MTP 수락(acceptance)이 다음과 같이 작동하는 대신
0.9, 0.75, 0.6
이런 식으로 보이는 것입니다.
0.9, (0.75^4), (0.6^4)
즉, MTP 자체는 잘 작동하며, 2/3 단계의 코드에서 발생하는 문제는 DCP=4, 극심한 메모리 부족, sm121의 특이점 등 가능한 여러 방해 요소 중 하나에 의해 간섭을 받고 있을 가능성이 높습니다.
MTP2는 한때 일부 파라미터에서 MTP1보다 아주 미세하게 더 나은 성능을 보이기도 했습니다.

전체 가이드와 스크립트는 리포지토리 레시피(repo recipe)에 있습니다:
https://github.com/m9e/blackwell-llm-docker/tree/main/recipes/4x-spark-cluster/glm52-b12x-spark

vLLM 패치 브랜치는 다음과 같습니다:
https://github.com/m9e/vllm/tree/codex/glm52-spark-dcp-mtp-patches

리포지토리와 문서를 계속 수정할 수도 있겠지만, 베이스라인은 간단합니다:

  • DCP4 / 128K / MTP1
  • B12X sparse MLA
  • flashinfer_cutlass MoE
  • fp8_ds_mla KV
  • Ray 경량화(slimmed down)
  • IB/RDMA 활성화

각주: 제가 테스트에 사용했던 것과 동일한 112k 프롬프트를 8x RTX 6000 Pro Blackwell(PCI 스위치 뒤에 4+4 배치)로 보냈을 때, 프리필(prefill)에서 약 2800 tps를 달성할 수 있었습니다... 그리고 521 토큰의 출력을 약 13 tps로 디코딩했는데, 이 점이 흥미로웠습니다. 비록 동일한 하드웨어가 아무런 제약이 없는 ~c=0 코드 생성(codegen) 프롬프트를 받았을 경우, 짧은 컨텍스트에서 bs=1일 때 약 106 tps, bs=8일 때 약 420 tps의 디코딩 속도를 보여주기도 했습니다.

여기서 확실한 교훈은 긴 컨텍스트(long context) 처리가 엄청난 영향을 미치지는 않는다는 점입니다.
따라서, 제가 m3ultra에서도 이를 구축했기 때문에, 사람들이 비교 데이터를 좋아할 것이라고 생각했습니다.

빠르고 간편하지만, 평소와 마찬가지로 MLA (Multi-head Latent Attention) 계열의 커널(kernels)은 Mac의 성능에 우호적이지 않습니다:
여기 m3에서 실행되는 112k 프리필(prefill) 결과입니다:
1595.18.238.494 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 2068, progress = 0.02, t = 11.10 s / 186.27 tokens per second
1595.34.922.187 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 4116, progress = 0.04, t = 27.79 s / 148.13 tokens per second
1595.54.466.076 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 6164, progress = 0.06, t = 47.33 s / 130.24 tokens per second
1596.16.986.492 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 8212, progress = 0.08, t = 69.85 s / 117.57 tokens per second
1596.42.441.365 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 10260, progress = 0.10, t = 95.30 s / 107.65 tokens per second
1597.10.845.585 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 12308, progress = 0.13, t = 123.71 s / 99.49 tokens per second
1597.42.137.959 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 14356, progress = 0.15, t = 155.00 s / 92.62 tokens per second
1598.16.412.189 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 16404, progress = 0.17, t = 189.28 s / 86.67 tokens per second
1598.53.580.124 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 18452, progress = 0.19, t = 226.44 s / 81.49 tokens per second
1599.33.747.589 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 20500, progress = 0.21, t = 266.61 s / 76.89 tokens per second
1599.51.681.576 I srv operator(): Chat format: peg-native
1599.54.704.562 W srv stop: cancel task, id_task = 19752
1600.16.803.340 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 22548, progress = 0.23, t = 309.67 s / 72.81 tokens per second

1601.03.058.509 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 24596, progress = 0.25, t = 355.92 s / 69.11 tokens per second
1601.51.985.931 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 26644, progress = 0.27, t = 404.85 s / 65.81 tokens per second
1602.43.802.548 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 28692, progress = 0.29, t = 456.67 s / 62.83 tokens per second
1603.38.953.203 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 30740, progress = 0.31, t = 511.82 s / 60.06 tokens per second
1604.36.616.922 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 32788, progress = 0.33, t = 569.48 s / 57.58 tokens per second
detcode for a ~c=0 prompt:
14.29.814.210 I slot print_timing: id 3 | task 0 | prompt eval time = 878.17 ms / 34 tokens ( 25.83 ms per token, 38.72 tokens per second)
14.29.814.213 I slot print_timing: id 3 | task 0 | eval time = 754184.12 ms / 10368 tokens ( 72.74 ms per token, 13.75 tokens per second)
실제로 약 16.36 t/s에서 디코딩이 시작됩니다. 약 2k에서는 15 tps에 도달하고, 4600에서는 14 tps에 도달합니다. 아직 mac으로 많이 탐색해보지 못했습니다. 설정하기는 쉽지만, 강력한 MLA 커널의 부족함이 Mac 성능을 떨어뜨리는 것 같습니다. 따라서 컨텍스트가 짧은 작업은 안정적입니다.
제가 타이핑하는 동안 112k 입력을 처리할 수 있는지 확인하기 위해 청크 단위로 지켜보고 있습니다:
1611.24.410.290 I slot print_timing: id 0 | task 19739 | prompt processing, n_tokens = 45076, progress = 0.46, t = 9

AI 자동 생성 콘텐츠

본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0