|
#!/usr/bin/env bash |
|
# Install and run the DeepSeek V4 Flash 0731 Q2 stack on an NVIDIA DGX Spark. |
|
# |
|
# This is a standalone, shareable alternative to piping a remote installer into |
|
# Bash. It pins the ds4 source release and both GGUF revisions used by the |
|
# StressingLLMs benchmark, verifies every download, builds for GB10/sm_121, and |
|
# starts an OpenAI-compatible server. The source and model artifacts are MIT |
|
# licensed; DeepSeek retains the base-model copyright and release terms. |
|
# |
|
# Quick start: |
|
# bash run_deepseek_v4_flash_dgx_spark.sh |
|
# |
|
# The first run needs roughly 95 GB of downloads and 110 GiB of free disk. |
|
# Later runs reuse the verified source, binary, and model files. |
|
set -euo pipefail |
|
|
|
readonly DS4_VERSION_DEFAULT="v0.6.2" |
|
readonly DS4_SOURCE_URL_DEFAULT="https://github.com/Entrpi/ds4/archive/refs/tags/v0.6.2.tar.gz" |
|
readonly DS4_SOURCE_SHA256_DEFAULT="cd714cb89522b3b19fe26b01b1718787e00368007d10232aca23bc7889fc3771" |
|
|
|
readonly BASE_NAME_DEFAULT="DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf" |
|
readonly BASE_REVISION_DEFAULT="86bb38ce2ba7a98ab0e550359fec5f48859dc723" |
|
readonly BASE_REPOSITORY_DEFAULT="antirez/deepseek-v4-gguf" |
|
readonly BASE_SIZE_DEFAULT="86720111488" |
|
readonly BASE_SHA256_DEFAULT="ca22ae2f838e14077c22bc1c1417b71b45b5e5a3687bd96c2ac6e17fdb6261c0" |
|
|
|
readonly DRAFTER_NAME_DEFAULT="DSpark-drafter-Q2K-Q8-0731.gguf" |
|
readonly DRAFTER_REVISION_DEFAULT="81c6fdd38f9582da45ba27f0ed7b63bcd3ea3b62" |
|
readonly DRAFTER_REPOSITORY_DEFAULT="bleysg/DeepSeek-V4-Flash-DSpark-drafter-GGUF" |
|
readonly DRAFTER_SIZE_DEFAULT="6971241504" |
|
readonly DRAFTER_SHA256_DEFAULT="8fa269560dc76fd73e4233ad9b1938b5f65dd363381fd9b1a5c6183f7d12d686" |
|
|
|
DS4_VERSION="${DS4_VERSION:-$DS4_VERSION_DEFAULT}" |
|
DS4_SOURCE_URL="${DS4_SOURCE_URL:-$DS4_SOURCE_URL_DEFAULT}" |
|
DS4_SOURCE_SHA256="${DS4_SOURCE_SHA256:-$DS4_SOURCE_SHA256_DEFAULT}" |
|
DS4_SRC_DIR="${DS4_SRC_DIR:-$HOME/code/ds4-$DS4_VERSION}" |
|
DS4_GGUF_DIR="${DS4_GGUF_DIR:-$HOME/gguf}" |
|
|
|
BASE_NAME="${GGUF_FILE:-$BASE_NAME_DEFAULT}" |
|
BASE_URL="${DS4_BASE_URL:-https://huggingface.co/$BASE_REPOSITORY_DEFAULT/resolve/$BASE_REVISION_DEFAULT/$BASE_NAME}" |
|
BASE_SIZE="${DS4_BASE_SIZE:-$BASE_SIZE_DEFAULT}" |
|
BASE_SHA256="${DS4_BASE_SHA256:-$BASE_SHA256_DEFAULT}" |
|
DRAFTER_NAME="${DSPARK_FILE:-$DRAFTER_NAME_DEFAULT}" |
|
DRAFTER_URL="${DS4_DRAFTER_URL:-https://huggingface.co/$DRAFTER_REPOSITORY_DEFAULT/resolve/$DRAFTER_REVISION_DEFAULT/$DRAFTER_NAME}" |
|
DRAFTER_SIZE="${DS4_DRAFTER_SIZE:-$DRAFTER_SIZE_DEFAULT}" |
|
DRAFTER_SHA256="${DS4_DRAFTER_SHA256:-$DRAFTER_SHA256_DEFAULT}" |
|
|
|
CONTEXT_SIZE=262144 |
|
HOST_ADDRESS="127.0.0.1" |
|
PORT=8888 |
|
MEMORY_FLOOR_GB=8 |
|
JOBS="$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 8)" |
|
INSTALL_ONLY=0 |
|
DRY_RUN=0 |
|
SKIP_HOST_CHECK=0 |
|
SKIP_SMOKE_TEST=0 |
|
USE_DSPARK=1 |
|
REBUILD=0 |
|
SERVER_ARGS=() |
|
TEMP_DIR="" |
|
SERVER_PID="" |
|
|
|
usage() { |
|
cat <<'EOF' |
|
Install and run DeepSeek V4 Flash on an NVIDIA DGX Spark. |
|
|
|
Usage: |
|
run_deepseek_v4_flash_dgx_spark.sh [options] [-- ds4-server options] |
|
|
|
Options: |
|
--ctx TOKENS Context window (4096..524288; default: 262144) |
|
--host ADDRESS Listen address (default: 127.0.0.1) |
|
--port PORT Listen port (default: 8888) |
|
--mem-floor-gb N Preserve at least N GiB for the OS (default: 8) |
|
--src-dir PATH ds4 source/build directory |
|
--gguf-dir PATH model download directory |
|
--jobs N parallel build jobs |
|
--no-dspark skip the 6.5 GB drafter and use plain decoding |
|
--rebuild rebuild even when the pinned CUDA build is already present |
|
--install-only download, verify, and build without starting the server |
|
--skip-host-check allow non-GB10/non-aarch64 hosts (unsupported) |
|
--skip-smoke-test do not send the post-start "capital of France" request |
|
--dry-run print the resolved setup without downloading or running |
|
-h, --help show this help |
|
|
|
Environment overrides for mirrors/offline copies: |
|
DS4_SOURCE_URL, DS4_SOURCE_SHA256, DS4_SRC_DIR, DS4_GGUF_DIR |
|
DS4_BASE_URL, DS4_BASE_SIZE, DS4_BASE_SHA256, GGUF_FILE |
|
DS4_DRAFTER_URL, DS4_DRAFTER_SIZE, DS4_DRAFTER_SHA256, DSPARK_FILE |
|
|
|
API after startup: |
|
http://127.0.0.1:8888/v1 |
|
model: deepseek-v4-flash |
|
|
|
The server runs in the foreground. Press Ctrl-C to stop it. |
|
EOF |
|
} |
|
|
|
die() { |
|
echo "error: $*" >&2 |
|
exit 1 |
|
} |
|
|
|
note() { |
|
echo "==> $*" >&2 |
|
} |
|
|
|
require_value() { |
|
local option="$1" |
|
local value="${2:-}" |
|
[[ -n "$value" ]] || die "$option requires a value" |
|
} |
|
|
|
is_integer() { |
|
[[ "$1" =~ ^[0-9]+$ ]] |
|
} |
|
|
|
while (($#)); do |
|
case "$1" in |
|
--ctx) |
|
require_value "$1" "${2:-}" |
|
CONTEXT_SIZE="$2" |
|
shift 2 |
|
;; |
|
--host) |
|
require_value "$1" "${2:-}" |
|
HOST_ADDRESS="$2" |
|
shift 2 |
|
;; |
|
--port) |
|
require_value "$1" "${2:-}" |
|
PORT="$2" |
|
shift 2 |
|
;; |
|
--mem-floor-gb) |
|
require_value "$1" "${2:-}" |
|
MEMORY_FLOOR_GB="$2" |
|
shift 2 |
|
;; |
|
--src-dir) |
|
require_value "$1" "${2:-}" |
|
DS4_SRC_DIR="$2" |
|
shift 2 |
|
;; |
|
--gguf-dir) |
|
require_value "$1" "${2:-}" |
|
DS4_GGUF_DIR="$2" |
|
shift 2 |
|
;; |
|
--jobs) |
|
require_value "$1" "${2:-}" |
|
JOBS="$2" |
|
shift 2 |
|
;; |
|
--no-dspark) |
|
USE_DSPARK=0 |
|
shift |
|
;; |
|
--rebuild) |
|
REBUILD=1 |
|
shift |
|
;; |
|
--install-only) |
|
INSTALL_ONLY=1 |
|
shift |
|
;; |
|
--skip-host-check) |
|
SKIP_HOST_CHECK=1 |
|
shift |
|
;; |
|
--skip-smoke-test) |
|
SKIP_SMOKE_TEST=1 |
|
shift |
|
;; |
|
--dry-run) |
|
DRY_RUN=1 |
|
shift |
|
;; |
|
-h|--help) |
|
usage |
|
exit 0 |
|
;; |
|
--) |
|
shift |
|
SERVER_ARGS=("$@") |
|
break |
|
;; |
|
*) |
|
die "unknown option: $1 (use --help)" |
|
;; |
|
esac |
|
done |
|
|
|
is_integer "$CONTEXT_SIZE" || die "--ctx must be an integer" |
|
((CONTEXT_SIZE >= 4096 && CONTEXT_SIZE <= 524288)) || \ |
|
die "--ctx must be between 4096 and 524288; 1,000,000 is unsafe on a 128 GB Spark" |
|
is_integer "$PORT" || die "--port must be an integer" |
|
((PORT >= 1 && PORT <= 65535)) || die "--port must be between 1 and 65535" |
|
is_integer "$MEMORY_FLOOR_GB" || die "--mem-floor-gb must be an integer" |
|
((MEMORY_FLOOR_GB >= 4 && MEMORY_FLOOR_GB <= 32)) || \ |
|
die "--mem-floor-gb must be between 4 and 32" |
|
is_integer "$JOBS" || die "--jobs must be an integer" |
|
((JOBS >= 1)) || die "--jobs must be at least 1" |
|
is_integer "$BASE_SIZE" || die "DS4_BASE_SIZE must be an integer" |
|
is_integer "$DRAFTER_SIZE" || die "DS4_DRAFTER_SIZE must be an integer" |
|
[[ "$DS4_SOURCE_SHA256" =~ ^[0-9a-f]{64}$ ]] || die "DS4_SOURCE_SHA256 must be a lowercase SHA-256 digest" |
|
[[ "$BASE_SHA256" =~ ^[0-9a-f]{64}$ ]] || die "DS4_BASE_SHA256 must be a lowercase SHA-256 digest" |
|
[[ "$DRAFTER_SHA256" =~ ^[0-9a-f]{64}$ ]] || die "DS4_DRAFTER_SHA256 must be a lowercase SHA-256 digest" |
|
for argument in "${SERVER_ARGS[@]}"; do |
|
case "$argument" in |
|
-c|--ctx|--ctx=*|-m|--model|--model=*|--host|--host=*|--port|--port=*|\ |
|
--mem-floor-gb|--mem-floor-gb=*|--mtp|--mtp=*|--dspark|--dspark=*|\ |
|
--no-mtp|--no-dspark|--no-spec) |
|
die "server option $argument is managed by this launcher; use its named option instead" |
|
;; |
|
esac |
|
done |
|
|
|
DS4_SRC_DIR="$(realpath -m "$DS4_SRC_DIR")" |
|
DS4_GGUF_DIR="$(realpath -m "$DS4_GGUF_DIR")" |
|
BASE_PATH="$DS4_GGUF_DIR/$BASE_NAME" |
|
DRAFTER_PATH="$DS4_GGUF_DIR/$DRAFTER_NAME" |
|
SERVER_PATH="$DS4_SRC_DIR/ds4-server" |
|
|
|
if ((DRY_RUN)); then |
|
cat <<EOF |
|
DeepSeek V4 Flash DGX Spark dry run |
|
ds4 version: $DS4_VERSION |
|
source URL: $DS4_SOURCE_URL |
|
source dir: $DS4_SRC_DIR |
|
base URL: $BASE_URL |
|
base model: $BASE_PATH |
|
DSpark: $([[ "$USE_DSPARK" == 1 ]] && echo enabled || echo disabled) |
|
drafter URL: $DRAFTER_URL |
|
drafter model: $DRAFTER_PATH |
|
endpoint: http://$HOST_ADDRESS:$PORT/v1 |
|
context: $CONTEXT_SIZE |
|
memory floor: $MEMORY_FLOOR_GB GiB |
|
install only: $INSTALL_ONLY |
|
EOF |
|
exit 0 |
|
fi |
|
|
|
for command in curl make tar sha256sum stat df awk realpath nvidia-smi nvcc find; do |
|
command -v "$command" >/dev/null 2>&1 || die "required command not found: $command" |
|
done |
|
|
|
check_host() { |
|
local architecture |
|
local gpu_data |
|
local cuda_release |
|
local memory_kib |
|
|
|
architecture="$(uname -m)" |
|
gpu_data="$(nvidia-smi --query-gpu=name,compute_cap --format=csv,noheader 2>/dev/null || true)" |
|
cuda_release="$(nvcc --version 2>/dev/null | awk '/release/{gsub(/,/, "", $5); print $5; exit}')" |
|
memory_kib="$(awk '/^MemTotal:/{print $2}' /proc/meminfo)" |
|
|
|
note "host: architecture=$architecture gpu=${gpu_data:-unknown} CUDA=${cuda_release:-unknown}" |
|
if ((SKIP_HOST_CHECK)); then |
|
note "host compatibility checks skipped by request" |
|
return |
|
fi |
|
[[ "$architecture" == "aarch64" ]] || die "DGX Spark requires aarch64 (found $architecture)" |
|
[[ "$gpu_data" == *"GB10"* && "$gpu_data" == *"12.1"* ]] || \ |
|
die "expected NVIDIA GB10 compute capability 12.1 (found ${gpu_data:-none})" |
|
[[ "$cuda_release" == 13.* ]] || die "CUDA toolkit 13.x is required (found ${cuda_release:-none})" |
|
[[ -n "$memory_kib" && "$memory_kib" -ge 120000000 ]] || \ |
|
die "at least 120,000,000 KiB system/unified memory is required" |
|
} |
|
|
|
check_disk_space() { |
|
local needed=10737418240 |
|
local available_kib |
|
local available |
|
|
|
mkdir -p "$DS4_GGUF_DIR" |
|
[[ -f "$BASE_PATH" ]] || needed=$((needed + BASE_SIZE)) |
|
if ((USE_DSPARK)) && [[ ! -f "$DRAFTER_PATH" ]]; then |
|
needed=$((needed + DRAFTER_SIZE)) |
|
fi |
|
available_kib="$(df -Pk "$DS4_GGUF_DIR" | awk 'NR==2 {print $4}')" |
|
available=$((available_kib * 1024)) |
|
((available >= needed)) || \ |
|
die "insufficient disk space in $DS4_GGUF_DIR: need $((needed / 1073741824)) GiB including safety margin, have $((available / 1073741824)) GiB" |
|
} |
|
|
|
download_file() { |
|
local url="$1" |
|
local destination="$2" |
|
local partial="$destination.part" |
|
|
|
if [[ -e "$destination" ]]; then |
|
return 0 |
|
fi |
|
note "downloading $(basename "$destination") (resumable)" |
|
curl -fL --progress-meter --retry 5 --retry-all-errors -C - -o "$partial" "$url" |
|
mv "$partial" "$destination" |
|
} |
|
|
|
verify_file() { |
|
local path="$1" |
|
local expected_size="$2" |
|
local expected_hash="$3" |
|
local marker="$path.sha256-ok" |
|
local actual_size |
|
local actual_hash |
|
local fingerprint |
|
local expected_marker |
|
|
|
[[ -f "$path" ]] || die "missing downloaded file: $path" |
|
actual_size="$(stat -Lc %s "$path")" |
|
[[ "$actual_size" == "$expected_size" ]] || \ |
|
die "wrong size for $path: expected $expected_size bytes, found $actual_size; move it aside and rerun" |
|
fingerprint="$(stat -Lc '%s:%Y:%i' "$path")" |
|
expected_marker="$expected_hash $fingerprint" |
|
if [[ -f "$marker" ]] && [[ "$(<"$marker")" == "$expected_marker" ]]; then |
|
note "verified marker: $(basename "$path")" |
|
return |
|
fi |
|
note "verifying SHA-256: $(basename "$path")" |
|
actual_hash="$(sha256sum "$path" | awk '{print $1}')" |
|
[[ "$actual_hash" == "$expected_hash" ]] || \ |
|
die "checksum mismatch for $path: expected $expected_hash, found $actual_hash" |
|
printf '%s\n' "$expected_marker" >"$marker" |
|
} |
|
|
|
install_source() { |
|
local source_marker="$DS4_SRC_DIR/.source-archive-sha256" |
|
local archive |
|
|
|
if [[ -f "$source_marker" && "$(<"$source_marker")" == "$DS4_SOURCE_SHA256" ]]; then |
|
note "reusing ds4 $DS4_VERSION source at $DS4_SRC_DIR" |
|
return |
|
fi |
|
if [[ -d "$DS4_SRC_DIR" && -n "$(find "$DS4_SRC_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then |
|
die "$DS4_SRC_DIR is non-empty but is not a complete ds4 $DS4_VERSION build; choose --src-dir" |
|
fi |
|
|
|
TEMP_DIR="$(mktemp -d)" |
|
archive="$TEMP_DIR/ds4.tar.gz" |
|
note "downloading pinned ds4 source $DS4_VERSION" |
|
curl -fL --progress-meter --retry 5 --retry-all-errors -o "$archive" "$DS4_SOURCE_URL" |
|
echo "$DS4_SOURCE_SHA256 $archive" | sha256sum --check --status || \ |
|
die "ds4 source archive checksum mismatch" |
|
mkdir -p "$DS4_SRC_DIR" |
|
tar -xzf "$archive" -C "$DS4_SRC_DIR" --strip-components=1 |
|
printf '%s\n' "$DS4_SOURCE_SHA256" >"$source_marker" |
|
} |
|
|
|
build_server() { |
|
local build_marker="$DS4_SRC_DIR/.cuda-spark-build-identity" |
|
local compiler_identity |
|
local expected_identity |
|
|
|
compiler_identity="$(nvcc --version | sha256sum | awk '{print $1}')" |
|
expected_identity="$DS4_SOURCE_SHA256:$compiler_identity" |
|
if ((!REBUILD)) && [[ -x "$SERVER_PATH" && -f "$build_marker" ]] && \ |
|
[[ "$(<"$build_marker")" == "$expected_identity" ]]; then |
|
note "reusing verified CUDA sm_121 build at $SERVER_PATH" |
|
return |
|
fi |
|
note "building ds4 for DGX Spark (CUDA sm_121)" |
|
make -C "$DS4_SRC_DIR" -j"$JOBS" cuda-spark |
|
[[ -x "$SERVER_PATH" ]] || die "build completed without creating $SERVER_PATH" |
|
printf '%s\n' "$expected_identity" >"$build_marker" |
|
} |
|
|
|
cleanup() { |
|
local exit_status=$? |
|
trap - EXIT INT TERM |
|
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then |
|
kill "$SERVER_PID" 2>/dev/null || true |
|
wait "$SERVER_PID" 2>/dev/null || true |
|
fi |
|
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" && "$TEMP_DIR" == /tmp/* ]]; then |
|
rm -rf -- "$TEMP_DIR" |
|
fi |
|
exit "$exit_status" |
|
} |
|
trap cleanup EXIT |
|
trap 'exit 130' INT |
|
trap 'exit 143' TERM |
|
|
|
check_host |
|
check_disk_space |
|
install_source |
|
build_server |
|
|
|
download_file "$BASE_URL" "$BASE_PATH" |
|
verify_file "$BASE_PATH" "$BASE_SIZE" "$BASE_SHA256" |
|
if ((USE_DSPARK)); then |
|
download_file "$DRAFTER_URL" "$DRAFTER_PATH" |
|
verify_file "$DRAFTER_PATH" "$DRAFTER_SIZE" "$DRAFTER_SHA256" |
|
fi |
|
|
|
if ((INSTALL_ONLY)); then |
|
note "installation complete" |
|
echo "Run this script again without --install-only to start the server." |
|
exit 0 |
|
fi |
|
|
|
launch=( |
|
"$SERVER_PATH" |
|
--cuda |
|
-m "$BASE_PATH" |
|
-c "$CONTEXT_SIZE" |
|
--host "$HOST_ADDRESS" |
|
--port "$PORT" |
|
--mem-floor-gb "$MEMORY_FLOOR_GB" |
|
) |
|
if ((USE_DSPARK)); then |
|
launch=( |
|
env |
|
DS4_CONT_MTP_MODE=2 |
|
DS4_CONT_DSPARK=1 |
|
"DS4_DSPARK_MODEL=$DRAFTER_PATH" |
|
"${launch[@]}" |
|
) |
|
else |
|
launch+=(--no-spec) |
|
fi |
|
launch+=("${SERVER_ARGS[@]}") |
|
|
|
if [[ "$HOST_ADDRESS" == "0.0.0.0" || "$HOST_ADDRESS" == "::" ]]; then |
|
note "warning: ds4 has no API authentication; firewall port $PORT before binding publicly" |
|
fi |
|
note "starting DeepSeek V4 Flash at http://$HOST_ADDRESS:$PORT/v1 with ctx=$CONTEXT_SIZE" |
|
"${launch[@]}" & |
|
SERVER_PID=$! |
|
|
|
if ((SKIP_SMOKE_TEST)); then |
|
wait "$SERVER_PID" |
|
exit $? |
|
fi |
|
|
|
HEALTH_HOST="$HOST_ADDRESS" |
|
if [[ "$HEALTH_HOST" == "0.0.0.0" || "$HEALTH_HOST" == "::" ]]; then |
|
HEALTH_HOST="127.0.0.1" |
|
fi |
|
HEALTH_URL="http://$HEALTH_HOST:$PORT/v1/models" |
|
CHAT_URL="http://$HEALTH_HOST:$PORT/v1/chat/completions" |
|
for ((attempt = 1; attempt <= 180; attempt++)); do |
|
kill -0 "$SERVER_PID" 2>/dev/null || die "ds4-server exited during startup" |
|
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then |
|
break |
|
fi |
|
if ((attempt == 180)); then |
|
die "server did not become ready within 15 minutes" |
|
fi |
|
sleep 5 |
|
done |
|
|
|
note "server is ready; running smoke test" |
|
SMOKE_RESPONSE="$(curl -fsS --max-time 300 \ |
|
-H 'Content-Type: application/json' \ |
|
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"What is the capital of France? Answer with the city only."}],"thinking":{"type":"disabled"},"max_tokens":32,"temperature":0}' \ |
|
"$CHAT_URL")" |
|
[[ "$SMOKE_RESPONSE" == *"Paris"* ]] || \ |
|
die "server responded, but the smoke test did not contain Paris: $SMOKE_RESPONSE" |
|
note "smoke test passed; server PID $SERVER_PID is running (Ctrl-C to stop)" |
|
wait "$SERVER_PID" |