#!/usr/bin/env bash # ============================================================================= # setup_deepseek_v4_sm89.sh (stage 2: MTP speculative decoding) # # Model repos (byte-identical; either works): # https://huggingface.co/SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada # https://huggingface.co/canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP # (the original -- all credit for the quantization to canada-quant) # # Recreates, from scratch, the verified-working environment for serving # DeepSeek-V4-Flash (W4A16 INT4 experts + FP8 attention + BF16 MTP head, # 159 GB) on 4x RTX 6000 Ada (SM89) with vLLM, including MTP speculative # decoding. # # Measured on 2026-07-21 (4x RTX 6000 Ada, TP=4, fp8_ds_mla KV cache): # - plain decoding: ~80 tok/s single-stream # - MTP (num_spec_tokens=1): 115-133 tok/s sustained, draft acceptance # 96.6-100%, mean acceptance length ~1.97/2.00 # # Verified-working state this script reproduces: # - vLLM 0.23.1rc1.dev145+g8c631d45e.cu130 <- the ONLY release of the # yhfgyyf/vllm-deepseek-v4-sm89 fork whose SM89 path actually works. # (The newer dev1018 release regressed the SM89 indexer-logits fallback.) # - transformers 5.8.1, PINNED AND INSTALLED LAST. Newer releases fail # config validation on this checkpoint: their ALLOWED_LAYER_TYPES no # longer accepts the 'hash_moe' entries in mlp_layer_types. Installing # the vLLM wheel afterwards would let pip drag a newer transformers back # in, so the pin must be the final install step (and is asserted below). # - flashinfer_python 0.6.14+sm89 (installed, but idle on the dev145 # Triton path; kept to match the reference environment) # - flashinfer-cubin 0.6.13 # - torch 2.11.0+cu130 # - NO deep_gemm (its mere presence hijacks a presence-gated code path # and crashes SM89 -- it must NOT be installed) # - ONE source patch on top of the wheel (applied automatically below): # a BF16/unquantized-wo_a fallback in nvidia/ops/o_proj.py. The fused # o-projection assumes FP8 block scales; the checkpoint's BF16 MTP # drafter head has none and crashes without it. The FP8 target model's # path is untouched. # - Sparse MLA runs via the portable Triton path: VLLM_TRITON_MLA_SPARSE=1 # and NO --attention-backend flag. # - serve.sh resolves the model to a concrete snapshot DIRECTORY at launch # instead of trusting hub ref resolution: an online metadata check (e.g. # an interrupted huggingface-cli download after the upstream repo gained # a new commit) can leave refs/main pointing at a snapshot that was # never downloaded, which breaks HF_HUB_OFFLINE resolution. # # What this script does NOT do (on purpose): # - Download the 159 GB model (see the note printed at the end) # - Any system-level changes (apt, /usr/local/cuda symlinks). It only # VERIFIES that /usr/local/cuda-13.0 exists, because everything is # pinned to it at launch time. # ============================================================================= # ----------------------------- PARAMETERS ------------------------------------ PROJECT_DIR="${1:-$HOME/deepseek-v4-serve}" # folder to create (arg 1 overrides) MODEL="${2:-SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada}" # HF repo id OR local path (arg 2 overrides) # The default is the Ada mirror (ships these scripts alongside the weights). # The original repo works identically -- the checkpoints are byte-identical: # ./setup_deepseek_v4_sm89.sh canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP SERVED_NAME="DeepSeek-V4-Flash" # name exposed on the OpenAI-compatible API PORT=8002 GPUS="0,1,2,3" TP_SIZE=4 # MEMORY/THROUGHPUT: validated corner from a day of SWE-bench Verified pilots # (2026-07-23). KV is the thin slice on this model -- weights + activations + # cudagraph pools eat almost the whole 48 GB card. The values below trade # against each other; see the generated serve.sh header for the full story. # Short version: # * util 0.97 leaves ~1.68 GiB/GPU free at idle, ~0.5-0.8 GiB under a long # trajectory -- THIN. It is above the Triton JIT-OOM line only because # warmup.sh pre-compiles the kernel shapes at startup and the systemd unit # auto-restarts+re-warms. For an UNATTENDED or shared deployment, prefer # GPU_MEM_UTIL=0.95 (restores ~2.4 GiB headroom; identical throughput at # seq=1). See serve.sh header. # * max-model-len 98304 fits KV and covers agentic trajectories (~30-100k); # 262144 will NOT boot (KV-fit ValueError). Prompt + max_tokens must be # <= this, so keep the harness's requested output tokens modest. # * max-num-seqs 1: at seq>1, multiple 30-100k trajectories don't fit KV at # once and vLLM preempts + RE-PREFILLS from scratch (prefix-cache hit # collapses <40%, ~10x wall-clock blowup). One resident stream (prefix hit # ~85-91%) wins decisively here. # * batched-tokens 4096: raising it toggles chunked prefill, whose buffers # come out of the KV pool -- not free on this card. # Pilot results (50-instance SWE-bench Verified, error_instances=0): # 64k/8192-out=56% | 96k/8192-out=60% (best) | 96k/high-effort=60% (no gain) MAX_MODEL_LEN=98304 GPU_MEM_UTIL=0.97 MAX_NUM_SEQS=1 MAX_NUM_BATCHED_TOKENS=4096 # 4096 tested; raising toggles chunked prefill (eats KV) NUM_SPEC_TOKENS=1 # MTP draft depth (1 = validated config) REASONING_EFFORT="medium" # default chat-template reasoning effort. # high effort showed NO resolved-rate gain over # medium on the pilots (60% both) at ~40% more # wall-clock and more step-limit failures. WARMUP_WAIT_MAX=300 # warmup.sh: seconds to wait for the API before giving up SERVICE_USER="$(id -un)" # user baked into the generated systemd unit PYTHON_BIN="python3.12" # venv interpreter (wheel is cp312) CUDA_HOME_DIR="/usr/local/cuda-13.0" # MUST be 13.0 to match cu130 wheels LOCAL_WHEEL_DIR="$HOME/wheels" # checked first, before downloading # Pinned artifacts (do not change casually -- this exact combination works) GH_REPO="yhfgyyf/vllm-deepseek-v4-sm89" VLLM_TAG="v0.23.1rc1.dev145-g8c631d45e-cu130-sm89" VLLM_WHEEL="vllm-0.23.1rc1.dev145+g8c631d45e.cu130-cp312-cp312-linux_x86_64.whl" FI_TAG="v0.23.1rc1.dev1018-g8aba6ae7e-cu130-sm89" # flashinfer wheel only exists in this release FI_WHEEL="flashinfer_python-0.6.14+sm89-py3-none-any.whl" TORCH_SPEC="torch==2.11.0" TORCH_INDEX="https://download.pytorch.org/whl/cu130" FI_CUBIN_SPEC="flashinfer-cubin==0.6.13" TRANSFORMERS_SPEC="transformers==5.8.1" # MUST be installed LAST (see header) # ----------------------------------------------------------------------------- set -euo pipefail log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; } warn() { printf '\n\033[1;33m!! %s\033[0m\n' "$*"; } die() { printf '\n\033[1;31mXX %s\033[0m\n' "$*" >&2; exit 1; } # ----------------------------- SANITY CHECKS --------------------------------- log "Sanity checks" command -v "$PYTHON_BIN" >/dev/null 2>&1 \ || die "$PYTHON_BIN not found (the pinned wheels are cp312)." command -v nvidia-smi >/dev/null 2>&1 \ || die "nvidia-smi not found -- NVIDIA driver missing?" [ -x "$CUDA_HOME_DIR/bin/nvcc" ] \ || die "$CUDA_HOME_DIR/bin/nvcc not found. cu130 wheels need CUDA 13.0 here." "$CUDA_HOME_DIR/bin/nvcc" --version | grep -q "cuda_13\.0" \ || die "$CUDA_HOME_DIR/bin/nvcc is not CUDA 13.0. Mixed toolchains break JIT builds (one-arg __cudaLaunch vs. 13.x headers)." [ -e "$PROJECT_DIR" ] && die "$PROJECT_DIR already exists -- refusing to clobber it." # ----------------------------- FOLDER + VENV --------------------------------- log "Creating $PROJECT_DIR and virtual environment" mkdir -p "$PROJECT_DIR/wheels" "$PYTHON_BIN" -m venv "$PROJECT_DIR/.venv" VPY="$PROJECT_DIR/.venv/bin/python" "$VPY" -m pip install --upgrade pip >/dev/null # ----------------------------- WHEEL RETRIEVAL ------------------------------- # Lessons encoded here: # * pip REFUSES wheels whose filename was changed -- keep canonical names. # * GitHub release asset URLs contain '+', which must be sent as %2B. # * gh CLI needs auth; the plain REST API does not. fetch_asset() { # fetch_asset local tag="$1" name="$2" dest="$PROJECT_DIR/wheels/$2" url if [ -f "$LOCAL_WHEEL_DIR/$name" ]; then log "Using local copy of $name from $LOCAL_WHEEL_DIR" cp "$LOCAL_WHEEL_DIR/$name" "$dest" return fi log "Downloading $name from release $tag" url=$(curl -fsS "https://api.github.com/repos/$GH_REPO/releases/tags/$tag" \ | "$VPY" -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if a['name']=='$name'][0])") \ || die "Could not resolve asset $name in release $tag" curl -fL -o "$dest" "${url//+/%2B}" || die "Download failed: $name" } fetch_asset "$FI_TAG" "$FI_WHEEL" fetch_asset "$VLLM_TAG" "$VLLM_WHEEL" # ----------------------------- INSTALL ORDER --------------------------------- # Order matters: torch cu130 first so nothing drags in a CPU/other-CUDA torch, # then the SM89 flashinfer wheel so pip never fetches the official one, then # the vLLM wheel (with deps), and transformers==5.8.1 LAST so that nothing # can override the pin afterwards. log "Installing torch 2.11.0 (cu130)" "$VPY" -m pip install "$TORCH_SPEC" --index-url "$TORCH_INDEX" log "Installing flashinfer-cubin" "$VPY" -m pip install "$FI_CUBIN_SPEC" log "Installing SM89 FlashInfer wheel" "$VPY" -m pip install "$PROJECT_DIR/wheels/$FI_WHEEL" log "Installing vLLM dev145 (SM89 golden release) + dependencies" "$VPY" -m pip install "$PROJECT_DIR/wheels/$VLLM_WHEEL" log "Ensuring deep_gemm is ABSENT (presence breaks SM89)" "$VPY" -m pip uninstall -y deep-gemm deep_gemm deepgemm >/dev/null 2>&1 || true log "Pinning transformers 5.8.1 (LAST install -- newer releases reject the checkpoint's hash_moe layer types)" "$VPY" -m pip install "$TRANSFORMERS_SPEC" # ----------------------------- SOURCE PATCH ---------------------------------- # BF16/unquantized-wo_a fallback for the fused o-projection. Required for the # BF16 MTP drafter head; harmless otherwise (guarded, target path unchanged). log "Writing and applying the BF16 o_proj patch" cat > "$PROJECT_DIR/patch_dev145_bf16_oproj.py" <<'PATCH_EOF' #!/usr/bin/env python3 """ patch_dev145_bf16_oproj.py Fixes: AttributeError: 'ColumnParallelLinear' object has no attribute 'weight_scale' in vllm/models/deepseek_v4/nvidia/ops/o_proj.py:68 when running --speculative-config '{"method":"mtp",...}' on canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP with the dev145 SM89 wheel. Root cause: deep_gemm_fp8_o_proj() unconditionally assumes an FP8-quantized wo_a (reads weight_scale_inv / weight_scale). The target model's attention is FP8 (compressed-tensors) and sails through; the checkpoint's MTP drafter head is BF16/unquantized, so its wo_a is a plain ColumnParallelLinear with no scale attribute. Fix: insert an early branch for scale-less wo_a that replicates the fused op's math in plain torch: inverse RoPE (interleaved even/odd pairs on the LAST rope_dim dims of each head, rotation by -theta, mirroring _fused_inv_rope_fp8_quant_per_head), grouped bf16 einsum against wo_a.weight viewed [n_groups, o_lora_rank, heads_per_group*head_dim], then wo_b. Idempotent (marker-guarded); writes .bak_bf16 once before modifying. """ import pathlib import py_compile import sys MARKER = "SM89 patch: BF16/unquantized wo_a" OLD = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` / ``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``. """ o_fp8, o_scale = fused_inv_rope_fp8_quant( ''' NEW = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` / ``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``. """ if ( getattr(wo_a, "weight_scale_inv", None) is None and getattr(wo_a, "weight_scale", None) is None ): # ---- SM89 patch: BF16/unquantized wo_a (e.g. the BF16 MTP drafter # head in canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP). The fused # FP8 path below requires block scales; replicate its math in plain # torch: inverse RoPE (interleaved even/odd pairs on the LAST # ``rope_dim`` dims, rotation by -theta, mirroring # _fused_inv_rope_fp8_quant_per_head), grouped bf16 einsum, wo_b. num_tokens, num_heads, head_dim = o.shape of = o.to(torch.float32) cs = cos_sin_cache[positions.to(torch.long)] half = rope_dim // 2 cos = cs[:, :half].unsqueeze(1) sin = cs[:, half:].unsqueeze(1) rope = of[..., nope_dim:] x1 = rope[..., 0::2] x2 = rope[..., 1::2] rope_inv = torch.stack( (x1 * cos + x2 * sin, x2 * cos - x1 * sin), dim=-1 ).flatten(-2) o_inv = torch.cat((of[..., :nope_dim], rope_inv), dim=-1) o_grouped = o_inv.to(torch.bfloat16).reshape( num_tokens, n_groups, heads_per_group * head_dim ) w = wo_a.weight.to(torch.bfloat16).view( n_groups, o_lora_rank, heads_per_group * head_dim ) z_bf16 = torch.einsum("bhr,hdr->bhd", o_grouped, w) return wo_b(z_bf16.flatten(1)) o_fp8, o_scale = fused_inv_rope_fp8_quant( ''' def resolve_installed_target() -> pathlib.Path: import vllm # noqa: PLC0415 return ( pathlib.Path(vllm.__file__).parent / "models" / "deepseek_v4" / "nvidia" / "ops" / "o_proj.py" ) def main() -> int: if len(sys.argv) > 1: target = pathlib.Path(sys.argv[1]) else: target = resolve_installed_target() if not target.is_file(): print(f"XX target not found: {target}", file=sys.stderr) return 2 src = target.read_text() if MARKER in src: print(f"OK already patched, nothing to do: {target}") return 0 if OLD not in src: print( "XX expected code block not found -- file differs from the " "dev145 (g8c631d45e) layout this patch targets. Refusing to " f"guess. File: {target}", file=sys.stderr, ) return 2 backup = target.with_suffix(target.suffix + ".bak_bf16") if not backup.exists(): backup.write_text(src) print(f"OK backup written: {backup}") target.write_text(src.replace(OLD, NEW, 1)) py_compile.compile(str(target), doraise=True) print(f"OK patched + compiled: {target}") return 0 if __name__ == "__main__": raise SystemExit(main()) PATCH_EOF "$VPY" "$PROJECT_DIR/patch_dev145_bf16_oproj.py" # ----------------------------- VERIFICATION ---------------------------------- log "Verifying installed environment" "$VPY" - <<'PYEOF' import pathlib import sys import torch assert torch.__version__.startswith("2.11.0"), f"torch is {torch.__version__}, expected 2.11.0 (a vLLM dependency probably replaced it -- reinstall torch with the cu130 index)" assert torch.version.cuda == "13.0", f"torch CUDA is {torch.version.cuda}, expected 13.0" import transformers assert transformers.__version__ == "5.8.1", f"transformers is {transformers.__version__}, expected 5.8.1 (newer releases reject this checkpoint's hash_moe layer types) -- something installed after the pin overrode it" import vllm assert vllm.__version__.startswith("0.23.1rc1.dev145"), f"vLLM is {vllm.__version__}, expected dev145 (dev1018 is the broken release)" from vllm.models.deepseek_v4.nvidia.ops import sm12x_deep_gemm_fallbacks as m assert hasattr(m, "_fp8_paged_mqa_logits_sm12x"), "SM89 indexer fallbacks missing from this vLLM build" o_proj = pathlib.Path(vllm.__file__).parent / "models/deepseek_v4/nvidia/ops/o_proj.py" assert "SM89 patch: BF16/unquantized wo_a" in o_proj.read_text(), "BF16 o_proj patch not applied" import flashinfer try: import deep_gemm # noqa: F401 sys.exit("deep_gemm is importable -- it MUST NOT be installed on SM89") except ImportError: pass print(f"OK torch {torch.__version__} | transformers {transformers.__version__} | vLLM {vllm.__version__} | flashinfer {getattr(flashinfer, '__version__', '?')} | deep_gemm absent | sm12x fallbacks present | bf16 o_proj patch applied") if torch.cuda.is_available(): for i in range(torch.cuda.device_count()): print(f"OK GPU {i}: {torch.cuda.get_device_name(i)} capability {torch.cuda.get_device_capability(i)}") PYEOF # ----------------------------- GENERATE serve.sh ----------------------------- log "Writing $PROJECT_DIR/serve.sh" cat > "$PROJECT_DIR/serve.sh" < NO BOOT). # Current values: util 0.97, max-model-len 98304, max-num-seqs 1. MEASURED # ~1.68 GiB/GPU free at idle, ~0.5-0.8 GiB under a long trajectory -- thin. # For UNATTENDED/shared serving, drop util to 0.95 (restores ~2.4 GiB # headroom, same throughput at seq=1). Do NOT raise max-model-len toward # 262k without lowering util -- that reopens the JIT-OOM hole. # * max-num-seqs 1: at seq>1 the KV pool can't hold multiple 30-100k agentic # trajectories, so vLLM preempts and RE-PREFILLS evicted sequences (prefix # hit <40%, ~10x wall-clock blowup). seq=1 keeps one context resident # (prefix hit ~85-91%) decoding continuously; it wins decisively here. # * PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True keeps the torch pool from # overgrowing past its profiled peak on long prefills and eating the JIT # headroom. (If graph capture ever rejects it on a future wheel, drop it.) # * MTP measured at 115-133 tok/s (~1.5-1.65x over the ~80 tok/s baseline), # 96.6-100% draft acceptance. To run WITHOUT speculative decoding, delete # the --speculative-config line (and optionally --max-num-batched-tokens). # * First requests JIT-compile Triton kernels per shape (one-time latency # spikes). Run ./warmup.sh once after every server start so the shapes # compile at minute 0, not minute 100 of an unattended run. set -euo pipefail cd "\$(dirname "\$0")" MODEL="$MODEL" # Resolve a repo id to its local snapshot directory. Prefer refs/main when it # points at a snapshot that actually exists; otherwise fall back to the # newest snapshot directory present. resolve_model() { local m="\$1" if [ -d "\$m" ]; then printf '%s\n' "\$m"; return; fi local hub="\${HF_HOME:-\$HOME/.cache/huggingface}/hub" local repo_dir="\$hub/models--\${m//\//--}" local snaps="\$repo_dir/snapshots" if [ -d "\$snaps" ]; then local ref="" [ -f "\$repo_dir/refs/main" ] && ref="\$(cat "\$repo_dir/refs/main" 2>/dev/null || true)" if [ -n "\$ref" ] && [ -d "\$snaps/\$ref" ]; then printf '%s\n' "\$snaps/\$ref"; return fi local newest newest="\$(ls -1t "\$snaps" 2>/dev/null | head -1)" if [ -n "\$newest" ]; then printf '%s\n' "\$snaps/\$newest"; return; fi fi printf '\n' } MODEL_PATH="\$(resolve_model "\$MODEL")" if [ -z "\$MODEL_PATH" ]; then echo "XX model '\$MODEL' not found locally. Download it once with:" >&2 echo " \$(dirname "\$0")/.venv/bin/huggingface-cli download \$MODEL" >&2 exit 1 fi echo "==> serving model from: \$MODEL_PATH" exec env -u VLLM_TRITON_MLA_SPARSE_HEAD_BLOCK_SIZE \\ -u VLLM_TRITON_MLA_SPARSE_TOPK_CHUNK_SIZE \\ -u VLLM_TRITON_MLA_SPARSE_QUERY_CHUNK_SIZE \\ -u VLLM_TRITON_MLA_SPARSE_MATMUL_DECODE \\ -u VLLM_VERSION_OVERRIDE \\ -u VLLM_PYTHON_EXECUTABLE \\ VLLM_TRITON_MLA_SPARSE=1 \\ CUDA_HOME=$CUDA_HOME_DIR \\ CUDA_PATH=$CUDA_HOME_DIR \\ PATH="$CUDA_HOME_DIR/bin:\$PATH" \\ FLASHINFER_DISABLE_VERSION_CHECK=1 \\ VLLM_USE_FLASHINFER_SAMPLER=0 \\ PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \\ HF_HUB_OFFLINE=1 \\ CUDA_VISIBLE_DEVICES=$GPUS \\ "\$(dirname "\$0")/.venv/bin/vllm" serve "\$MODEL_PATH" \\ --served-model-name "$SERVED_NAME" \\ --host 0.0.0.0 --port $PORT \\ --tensor-parallel-size $TP_SIZE \\ --kv-cache-dtype fp8_ds_mla \\ --block-size 256 \\ --max-model-len $MAX_MODEL_LEN \\ --gpu-memory-utilization $GPU_MEM_UTIL \\ --max-num-seqs $MAX_NUM_SEQS \\ --max-num-batched-tokens $MAX_NUM_BATCHED_TOKENS \\ --tokenizer-mode deepseek_v4 \\ --reasoning-parser deepseek_v4 \\ --enable-auto-tool-choice --tool-call-parser deepseek_v4 \\ --default-chat-template-kwargs '{"reasoning_effort": "$REASONING_EFFORT"}' \\ --speculative-config '{"method":"mtp","num_speculative_tokens":$NUM_SPEC_TOKENS}' \\ --trust-remote-code SERVE_EOF chmod +x "$PROJECT_DIR/serve.sh" # ----------------------------- GENERATE canary.sh ---------------------------- log "Writing $PROJECT_DIR/canary.sh" cat > "$PROJECT_DIR/canary.sh" < "$PROJECT_DIR/warmup.sh" < overrides the server-wait bound. set -euo pipefail HOST="\${1:-127.0.0.1}" PORT="\${2:-$PORT}" URL="http://\$HOST:\$PORT/v1/chat/completions" MODEL="$SERVED_NAME" WAIT_MAX="\${WAIT_MAX:-$WARMUP_WAIT_MAX}" MAXLEN="$MAX_MODEL_LEN" echo "==> waiting for server at \$HOST:\$PORT (up to \${WAIT_MAX}s) ..." deadline=\$(( \$(date +%s) + WAIT_MAX )) until curl -sf "http://\$HOST:\$PORT/v1/models" >/dev/null 2>&1; do if [ "\$(date +%s)" -ge "\$deadline" ]; then echo "XX server not reachable after \${WAIT_MAX}s -- engine likely died during init; giving up" >&2 exit 1 fi sleep 5 done echo "==> server is up" req() { # req local n="\$1" local prompt payload prompt="\$(yes 'the' | head -n "\$n" | tr '\n' ' ')" # Build JSON via printf to avoid nested-quote escaping hazards. payload="\$(printf '{"model":"%s","messages":[{"role":"user","content":"%s"}],"max_tokens":16,"temperature":0}' "\$MODEL" "\$prompt")" curl -s -o /dev/null -w " HTTP %{http_code} in %{time_total}s (n=\$n)\n" \\ "\$URL" -H 'Content-Type: application/json' --data "\$payload" } # Sweep shapes up to (but not over) --max-model-len; an over-cap request just # 400s and compiles nothing useful. Top entry sits just under MAXLEN. echo "==> sequential shape sweep (long ones take a while; that is the point)" top=\$(( MAXLEN > 4096 ? MAXLEN - 4096 : MAXLEN / 2 )) for n in 16 100 511 1024 2000 4096 8192 16384 32768 65536 "\$top"; do [ "\$n" -le "\$MAXLEN" ] && req "\$n" done echo "==> warmup done. Driver-level headroom per GPU (want >= ~1 GiB free):" if command -v nvidia-smi >/dev/null 2>&1; then nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv else echo " (nvidia-smi not on PATH here; check on the host)" fi WARMUP_EOF chmod +x "$PROJECT_DIR/warmup.sh" # ----------------------------- GENERATE systemd unit ------------------------- log "Writing $PROJECT_DIR/deepseek-v4-flash.service" cat > "$PROJECT_DIR/deepseek-v4-flash.service" <= ~1 GiB). Runs automatically via ExecStartPost under the systemd unit. 4. Smoke test: $PROJECT_DIR/canary.sh A correct one-sentence answer naming Muenchen = numerics are sound. Watch the SpecDecoding metrics lines: draft acceptance should sit around 95-100% with mean acceptance length near 2.00. 5. Run as a service: see header of $PROJECT_DIR/deepseek-v4-flash.service NEXT_EOF