주요 콘텐츠로 건너뛰기
AI 튜토리얼

EXL3와 vLLM으로 두 대의 DGX Spark에서 DeepSeek V4.1 Flash 로컬 실행

두 대의 NVIDIA GB10 DGX Spark 시스템에서 DeepSeek V4.1 Flash EXL3를 다운로드하고 구성, 실행, 테스트 및 최적화하는 방법을 알아보세요. 이 가이드에서는 자동 실행기, Engram 스토리지, DSpark 추론 시 추가 디코딩, 메모리 보호, 동기화 옵션, API 요청, 테스트 및 성능 튜닝을 다룹니다.

EXL3와 vLLM으로 두 대의 DGX Spark에서 DeepSeek V4.1 Flash 로컬 실행

개요

이 프로젝트는 두 대의 NVIDIA GB10 DGX Spark 시스템에서 DeepSeek V4.1 Flash를 로컬로 제공합니다. 2.9 bits-per-weight EXL3 체크포인트, 두 노드에 걸친 텐서 병렬 처리, 네이티브 sm_121a 커널, 포트 8888의 OpenAI 호환 vLLM API를 사용합니다.

제공되는 모델 식별자는 DeepSeek-v4.1-Flash-EXL3입니다. 이 배포는 텍스트 전용이며 CX7으로 연결된 2노드 GB10 키트를 위해 특별히 설계되었습니다.

이 컨테이너는 필요한 DeepseekV41 아키텍처가 포함된 ARM64 vLLM 이미지 vllm/vllm-openai:deepseekv41-flash-0909에 EXL3 지원을 추가합니다.

프로젝트가 제공하는 기능

  • 로컬 OpenAI 호환 서버: 애플리케이션에서 표준 /v1/chat/completions 엔드포인트를 호출할 수 있습니다.
  • 2노드 텐서 병렬 처리: 모델은 CX7 연결을 통해 텐서 병렬 크기 2로 실행됩니다.
  • EXL3 양자화: 체크포인트는 평균 2.9 bpw이며, 텐서별 양자화 설정과 함께 mul1 코드북을 사용합니다.
  • 네이티브 DSpark 추론: 초안 전문가가 체크포인트의 mtp.* 아래에 이미 저장되어 있으므로 별도의 초안 모델이 필요하지 않습니다.
  • 긴 컨텍스트 지원: 제공된 구성은 MAX_MODEL_LEN=600000으로 설정되어 있으며 2.5 GiB KV 캐시 풀을 사용합니다.
  • 파일 기반 Engram 테이블: 양자화되지 않은 Engram 데이터는 호스트 메모리에 고정되지 않고 원본 모델 샤드 47 및 48에서 읽습니다.
  • 자동 시작: start.sh는 가중치를 다운로드하고, 이미지를 가져오거나 빌드하며, 워커와 파일을 공유하고, GPU 검사를 실행한 뒤 API를 시작합니다.
  • 런타임 보호 기능: 런처는 부팅 여유 메모리를 확인하고, 프리필 단계 사이에서 할당자를 해제하며, 로딩 후 페이지 캐시를 삭제하고, 컨테이너의 OOM 점수를 조정합니다.

하드웨어 및 저장 공간 요구 사항

이 저장소는 정확히 두 대의 NVIDIA GB10 DGX Spark 노드를 대상으로 합니다. 기본 헤드 노드 주소는 10.0.0.1이며 노드는 CX7을 통해 통신합니다. README에는 첫 번째 노드에서 테스트한 인터페이스로 enp1s0f1np1/rocep1s0f1, 두 번째 노드에서 enp1s0f0np0/rocep1s0f0가 명시되어 있습니다.

약 387 GiB의 모델 데이터를 준비하십시오.

  • Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpw의 EXL3 샤드 39개에 약 197 GiB.
  • 원본 DeepSeek 샤드 47 및 48과 인덱스에 약 190 GiB. 여기에는 네이티브 Engram 테이블이 포함되어 있습니다.
  • 일치하는 이미지를 빌드하지 않고 가져올 수 있는 경우 게시된 컨테이너 이미지에 약 9 GiB.

완전한 DeepSeek-V4.1-Flash 디렉터리가 이미 있다면 ENGRAM_DIR을 해당 디렉터리로 지정하여 Engram 소스를 다시 다운로드하지 않도록 하십시오. 이 용도에서는 샤드 47 및 48과 인덱스만 읽습니다.

DGX Spark에서는 호스트 RAM도 GPU 메모리입니다. 모든 CUDA 할당은 공유 메모리를 즉시 사용하므로 저장 공간과 메모리 설정은 독립적으로 변경하고 긴 프롬프트로 테스트해야 합니다.

헤드 노드 준비

1. 저장소를 복제하고 환경 파일 만들기

헤드 Spark의 저장소 디렉터리에서 배포를 실행합니다.

cp .env.example .env

런처는 첫 실행 시 .env가 없으면 이 파일도 생성합니다. 시스템에 필요한 경우 WORKER_USER 및 GID와 같은 값을 검토하고 업데이트하십시오.

.env에 명시된 값은 명령줄 환경 변수 접두사보다 우선합니다. start.sh가 상속된 환경보다 파일을 먼저 소싱하기 때문입니다. 예를 들어 WEIGHT_SYNC=zfs ./start.sh를 실행하는 대신 .env 내부의 WEIGHT_SYNC를 변경하십시오.

2. Hugging Face 다운로드 도구 설치

자동 모델 다운로드에는 전송 기능이 포함된 Hugging Face CLI가 필요합니다.

pip install -U 'huggingface_hub[hf_transfer]'

다운로드는 재개할 수 있습니다. 중단된 실행으로 모델이 불완전하게 남은 경우 런처 또는 다운로더를 다시 실행하십시오.

3. 선택 사항: 서버를 시작하지 않고 다운로드

vLLM을 시작하기 전에 EXL3 및 Engram 파일을 준비하려면 다음을 실행하십시오.

./download.sh

자동 다운로드는 기본적으로 활성화되어 있습니다. 파일을 직접 관리하려면 구성에서 AUTO_DOWNLOAD=0으로 설정하십시오.

서버 시작

헤드 노드의 저장소에서 다음을 실행하십시오.

./start.sh

처음 실행할 때 런처는 다음과 같은 주요 작업을 수행합니다.

  1. 사용 가능한 메모리 여유 공간과 노드 구성을 확인합니다.
  2. 누락된 EXL3 및 Engram 파일을 다운로드합니다.
  3. 공개 이미지 ghcr.io/miaai-lab/deepseek-v4.1-flash-exl3-2x-dgx-sparks:2.9bpw를 가져옵니다.
  4. 레시피 스탬프가 변경되었거나 레지스트리를 사용할 수 없는 경우에만 로컬에서 빌드합니다.
  5. 워커에서 가중치를 사용할 수 있도록 합니다.
  6. EXL3 GEMM 및 Engram 역양자화 GPU 검사를 실행합니다.
  7. 두 텐서 병렬 랭크를 시작하고 포트 8888에서 API를 노출합니다.

깨끗하게 체크아웃한 저장소는 게시된 이미지의 레시피 스탬프와 일치해야 하므로 일반적으로 컴파일이 필요하지 않습니다.

이미지 빌드 및 전송 제어

  • BUILD=1 ./start.sh는 항상 이미지를 로컬에서 빌드합니다.
  • SKIP_BUILD=1 ./start.sh는 레시피 스탬프가 다르더라도 가져온 이미지를 유지합니다.
  • SKIP_PULL=1 ./start.sh는 컨테이너 레지스트리에 연결하지 않습니다.
  • PULL=1 ./start.sh는 로컬 스탬프가 일치하더라도 이미지를 다시 가져옵니다.
  • SKIP_SHIP=1 ./start.sh는 헤드가 이미지를 워커로 복사하지 못하게 합니다.

기본적으로 이미지 전송은 재개 가능한 rsync를 통해 스테이징된 tar 파일을 전송하는 방식으로 수행됩니다. IMAGE_SHIP=stream을 설정하면 docker save의 출력을 워커의 docker load로 파이프합니다. 스트리밍에는 임시 공간이 필요하지 않지만, 전송이 중단되면 처음부터 다시 시작합니다. auto 모드는 재개 가능한 전송을 우선 사용하며 워커 디스크 공간이 부족하면 스트리밍으로 전환합니다.

배포 확인 및 관리

두 컨테이너를 개별적으로 조작하지 말고 런처의 관리 명령을 사용하세요:

./start.sh status
./start.sh logs
./start.sh logs worker
./start.sh stop
./start.sh restart

워커에 이미 동기화된 가중치가 있다면 동기화 단계를 반복하지 않고 다시 시작하세요:

SKIP_SYNC=1 ./start.sh restart

시작 중에는 420초 동안 로그가 출력되지 않는지 감지하며 두 rank의 Python 스택 트레이스를 수집합니다. 진단 파일은 logs/ 아래에 기록됩니다.

첫 API 요청 보내기

서버가 정상 상태가 되면 헤드 노드로 채팅 완성 요청을 보냅니다. 이 스모크 요청은 직접적인 답변을 쉽게 확인할 수 있도록 사고 기능을 비활성화합니다:

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"DeepSeek-v4.1-Flash-EXL3","messages":[{"role":"user","content":"What is 17*19? Reply with the integer only."}],"max_tokens":32,"temperature":0,"chat_template_kwargs":{"enable_thinking":false}}'

예상 답변은 323입니다. 모든 OpenAI 호환 클라이언트에서 동일한 기본 URL과 모델 식별자를 사용할 수 있습니다.

권장 샘플링 설정 사용

일반적인 모델 사용 시 공식 샘플링 설정은 다음과 같습니다:

  • temperature=1.0
  • top_p=0.95
  • 사고 기능 활성화

채팅 템플릿의 기본값은 높은 추론 수준이며 75로 표시됩니다. 또한 low는 50, max는 100을 의미하며, 1부터 100 사이의 정수도 사용할 수 있습니다.

권장 샘플링 설정을 사용하는 요청은 다음과 같이 작성할 수 있습니다:

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"DeepSeek-v4.1-Flash-EXL3","messages":[{"role":"user","content":"Explain how tensor parallel inference works."}],"max_tokens":512,"temperature":1.0,"top_p":0.95,"reasoning_effort":"high"}'

DSpark 추측 디코딩 이해

이 배포는 체크포인트에 드래프트 전문가가 포함된 DSpark 추측 디코딩을 사용합니다. 별도의 드래프터를 다운로드하거나 로드하거나 서비스할 필요가 없습니다.

기반 vLLM 설정은 다음과 같습니다:

--speculative-config '{"method":"dspark","num_speculative_tokens":3}'

체크포인트의 드래프트 블록 크기 상한은 5이지만, 일반 텍스트에서 더 빠른 것으로 측정되었기 때문에 제공된 설정에서는 추측 토큰 3개를 사용합니다. 캡처 크기에는 1 2 3 4 6 8 12 18 24가 포함되며, 각 시퀀스가 추측 토큰 3개씩 생성하는 두 시퀀스를 위한 크기 6도 포함됩니다.

보고된 측정 결과에서 DSpark는 단일 스트림 디코딩을 개선합니다. 더 큰 배치에서는 SPEC_METHOD=none으로 비활성화하세요. 이렇게 하면 약 3.5 GiB도 확보됩니다. README에 따르면 추측을 사용하는 단일 스트림에서는 초당 약 31.6토큰을 처리했으며, 추측을 끈 서비스에서는 4개 스트림에서 더 높은 총 처리량을 기록했습니다.

메모리 안전하게 관리

노드당 대략적인 할당량에는 rank당 99.5 GiB의 EXL3 가중치, 2.5 GiB KV 풀, 컨텍스트·NCCL·워크스페이스·CUDA 그래프에 약 5~7 GiB, vLLM 프로세스·Docker·운영 체제·데스크톱에 약 9 GiB가 포함됩니다.

제공된 제한은 다음과 같습니다:

  • MAX_MODEL_LEN=600000
  • MAX_NUM_SEQS=2
  • MAX_NUM_BATCHED_TOKENS=1024
  • 2.5 GiB KV 캐시 풀

KV 레이아웃은 DeepSeek의 fp8_ds_mla로 자동 선택됩니다. --kv-cache-dtype를 전달하지 마세요. 측정된 풀은 컨텍스트 제한 614,400에서 774,400개의 토큰을 수용했으며, 토큰당 비용은 약 3.4 KiB였습니다.

이 체크포인트의 양자화를 2.9를 정수 2로 변환해 추정하지 마세요. 양자화 파라미터는 텐서별로 선택되며, 라우팅된 전문가, 공유 전문가, 어텐션 행렬, Engram 텐서, MTP 텐서, 언어 모델 헤드는 서로 다른 값을 사용합니다.

워밍업 후 헤드는 보고된 테스트에서 약 4.07~4.21 GiB의 가용 메모리를 유지했습니다. 601,000토큰 프리필을 수행하자 이 여유 공간은 약 2.1 GiB로 줄었습니다. 따라서 시작 직후가 아니라 긴 프리필 후에 변경 사항을 검증하세요.

메모리 관련 설정은 한 번에 하나만 늘리세요. README에서는 KV 풀에 0.5 GiB를 추가하면 프리필 여유 공간이 약 1 GiB 줄어들 수 있다고 경고합니다. 3 GiB 풀은 짧은 스모크 테스트를 통과했지만, 600,000토큰 프롬프트에서 약 470,000토큰 지점에 도달하자 실패했습니다.

선택적 메모리 가드 주의해서 사용

scripts/memguard.sh는 /proc/meminfo를 초당 한 번 확인하고, DSV41_MEM_GUARD_GIB의 기본 임계값인 1.5 GiB 미만인 샘플이 연속 두 번 나오면 로컬 서비스 컨테이너를 종료합니다.

이 가드는 어떤 프로세스가 메모리를 사용했는지 판단할 수 없으므로 기본적으로 비활성화되어 있습니다. 메모리 부족을 유발한 작업이 호스트의 무관한 워크로드인 경우에도 항상 자체 vLLM 컨테이너를 종료합니다. 명시적으로 활성화하려면 다음을 설정하세요:

DSV41_MEM_GUARD=1

호스트 측에서 메모리를 많이 사용하는 작업의 경우, README에서는 문제가 되는 작업 자체를 제한하는 방법을 대신 권장합니다:

systemd-run --scope -p MemoryMax=2G <job>

배포에서는 두 컨테이너 모두에 --oom-score-adj 1000을 계속 사용하며, 부팅 여유 공간 사전 점검, 프리필마다 수행하는 할당자 메모리 반환, 로드 후 페이지 캐시 삭제도 함께 적용합니다.

가중치 동기화 전략 선택

NFS: 기본값

NFS는 테스트된 구성에 적합한 설정입니다. vllm-fn-nfs 익스포터를 재사용하고 EXL3 및 slim Engram 디렉터리를 노출하며, 이를 작업자 컨테이너에 각각 /model과 /engram-src로 마운트합니다.

./start.sh share
./start.sh

start.sh가 공유 단계를 자동으로 수행하므로, 명시적인 share 명령은 주로 익스포트를 준비하거나 문제를 해결할 때 유용합니다.

ZFS: 노드 로컬 복제본

각 노드에서 로컬 데이터셋을 유지하려면 .env에 WEIGHT_SYNC=zfs를 설정하세요. ZFS는 복원력을 높이고 초기 복제본 생성 후 변경된 블록만 전송할 수 있습니다. 스냅샷 기반 롤백도 사용할 수 있습니다.

비용은 상당합니다. 각 노드에 약 385 GiB의 풀 여유 공간이 필요합니다. README에 따르면 테스트된 두 번째 Spark에는 약 41 GiB만 남아 있었으므로 해당 환경에서는 ZFS를 사용할 수 없었습니다.

각 노드에서의 대표적인 설정은 다음과 같습니다:

sudo apt-get install -y zfsutils-linux
sudo zpool create -f models <vdev>
sudo zfs create -o recordsize=1M -o atime=off -o compression=lz4 \
  -o mountpoint=/path/to/model models/dsv41-exl3
sudo zfs create -o recordsize=1M -o atime=off -o compression=lz4 \
  -o mountpoint=/path/to/engram models/dsv41-engram
sudo zfs allow -u "$WORKER_USER" create,destroy,mount,receive,snapshot,rollback models

샤드 47과 48 및 인덱스만 사용해 Engram 데이터셋을 채우세요. 476 GiB 전체 네이티브 모델 트리는 복사하지 마세요. 풀 또는 작업자의 zfs recv를 사용할 수 없으면 런처가 스트리밍 방식으로 대체합니다.

Rsync: 더 단순한 로컬 복사

WEIGHT_SYNC=rsync는 ZFS 풀이 없어도 일반적인 노드 로컬 복사본을 생성합니다. ZFS 블록 수준의 증분 업데이트는 제공하지 않습니다.

더 빠른 프리필을 위한 Engram 데이터 패키징

Engram 테이블은 네이티브 FP8 형식으로 유지되며 양자화된 EXL3 트리에 포함되지 않습니다. 기본적으로 파일 기반 임베딩 구현은 레이어당 약 47 GiB를 메모리에 고정하지 않고 행을 가져옵니다.

랭크별 Engram 행 파일을 각 노드의 로컬 NVMe로 옮기려면 다음을 실행하세요:

./start.sh pack

텐서 병렬 크기가 2인 경우 패키징 과정에서 노드당 약 94 GiB를 기록합니다. 선택 사항이며 부팅에 필요하지 않습니다. 보고된 테스트에서는 행 누락 시 NFS 대신 로컬 직접 I/O를 사용하므로, 로컬에 패키징된 Engram 파일을 사용하면 프리필 성능이 약 25~50% 향상되었습니다.

Engram 행을 역양자화 커널에 전달할 때는 fp8_e4m3fn 형식으로 유지해야 합니다. 이를 uint8로 처리하면 그럴듯하지만 잘못된 출력이 생성될 수 있습니다.

검증 테스트 실행

저장소에는 PyTorch 또는 vLLM이 필요하지 않은 소스 수준 검사가 포함되어 있습니다:

python3 tests/test_numeric_config.py
python3 tests/test_engram_src.py
python3 tests/test_engram_secondary.py
python3 tests/test_k_map.py
python3 tests/test_memory_log.py
python3 tests/test_exl3_lm_head.py
python3 tests/test_sm120_block64.py
python3 tests/test_h2d_stage.py
python3 scripts/weight_budget.py --tp 2

채팅 템플릿 일치성 테스트에는 transformers와 원본 모델의 encoding/encoding.py가 필요합니다:

python3 tests/test_chat_template.py --src /path/to/DeepSeek-V4.1-Flash

API가 실행된 후 엔드투엔드 스모크 테스트를 실행하세요:

bash tests/test_smoke.sh

스모크 테스트는 모델에 17*19을 계산하도록 요청하고 323을 예상합니다. 서비스를 시작하기 전에 start.sh는 임시 컨테이너에서도 GPU 일치성 및 Engram 역양자화 검사를 실행합니다. 결과는 logs/overlay-verify.log에 기록됩니다.

고급 운영 팁

  • 스트림이 하나 또는 두 개라면 DSpark를 우선 사용하세요: 보고된 단일 스트림 디코드 속도는 초당 31.6토큰이었으며, 두 스트림의 총 처리량은 초당 42.5토큰이었습니다.
  • 배치 폭이 더 넓다면 추측 실행을 비활성화하세요: SPEC_METHOD=none을 사용하면 단일 스트림 처리량은 감소하지만, 보고된 테스트에서 네 스트림의 총 처리량은 초당 53.7토큰에 도달했습니다.
  • 반복되는 긴 프롬프트에는 Engram을 패킹하세요: 로컬 NVMe 행 파일을 사용하면 헤드 노드의 NFS 내보내기에 대한 의존도가 줄고 프리필 속도가 크게 향상될 수 있습니다.
  • 배포를 텍스트 전용으로 유지하세요: SM120 커널 범위에는 모델의 비전 토큰 폭에 필요한 sparse-MLA 커널이 없습니다.
  • EXL3 보조 스트림을 신중하지 않게 다시 활성화하지 마세요: 서로 다른 CUDA 스트림에서 실행되는 두 커널이 장치당 런타임의 단일 잠금 버퍼와 교착 상태에 빠질 수 있으므로, 배포에서는 EXL3 스트림을 직렬화합니다.
  • 올바른 네트워크 인터페이스를 모니터링하세요: NCCL은 10.0.0.x 루프백 별칭을 사용할 수 없습니다. NIC별 GID를 잘못 선택하면 약 1분 후 ibv_modify_qp errno 61과 함께 실패할 수 있습니다.
  • 긴 프리필의 메모리를 점검하세요: 부팅 성공과 짧은 스모크 테스트만으로는 더 큰 KV 풀이나 컨텍스트 설정이 안전하다는 것을 입증할 수 없습니다.
  • 저장소의 런처를 사용하세요: 런처가 NFS 또는 로컬 동기화, 사전 점검, 이미지 검증, 2-랭크 시작 및 진단을 조정합니다.

예상 성능

공개된 측정 결과에 따르면 디코드 스트림 하나의 속도는 초당 31.6토큰이며, 두 스트림의 총 처리량은 초당 42.5토큰입니다. 패킹된 Engram 데이터와 그룹화된 전문가 커널을 사용한 짧은 프롬프트 프리필 속도는 약 초당 965~1,041토큰이었습니다.

긴 프롬프트의 프리필은 자연스럽게 더 느려집니다. 보고된 예로는 100,000토큰 프롬프트에서 약 초당 979토큰, 455,000토큰에서 초당 802토큰, 601,000토큰에서 초당 810토큰이 있습니다. 실제 결과는 프롬프트 형태, 저장소 구성, 추측 실행, 배치 폭 및 구성된 프리필 청크 크기에 따라 달라집니다.

결론

이 프로젝트는 두 대의 GB10 DGX Sparks에서 DeepSeek V4.1 Flash를 서비스하는 데 필요한 특수 커널, EXL3 로더, 파일 기반 Engram 구현, 네트워킹 구성, 메모리 제어 및 런처를 패키징합니다. 제공된 기본값으로 시작하고 스모크 테스트를 사용해 API를 검증한 다음, 한 번에 하나의 설정만 조정하세요. 가장 안전하게 운영하려면 실제와 유사한 긴 프리필 워크로드에서 처리량과 사용 가능한 메모리를 모두 평가하세요.