Upload 3 files
Browse files- deepseek-v4-flash.service +60 -0
- patch_dev145_bf16_oproj.py +129 -0
- setup_deepseek_v4_sm89.sh +503 -0
deepseek-v4-flash.service
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# deepseek-v4-flash.service (stage 2: MTP speculative decoding)
|
| 3 |
+
#
|
| 4 |
+
# systemd unit for the DeepSeek-V4-Flash vLLM server on 4x RTX 6000 Ada
|
| 5 |
+
# (SM89). serve.sh (generated by setup_deepseek_v4_sm89.sh) is the single
|
| 6 |
+
# source of truth for environment variables, model-snapshot resolution, and
|
| 7 |
+
# launch flags; this unit only supervises it. systemd's clean service
|
| 8 |
+
# environment additionally guarantees no stale shell-profile exports can
|
| 9 |
+
# leak in.
|
| 10 |
+
#
|
| 11 |
+
# Measured 2026-07-21: 115-133 tok/s sustained generation (~1.5-1.65x over
|
| 12 |
+
# the ~80 tok/s plain-decoding baseline), 96.6-100% draft acceptance.
|
| 13 |
+
#
|
| 14 |
+
# NOTE: the setup script also generates a copy of this unit inside the
|
| 15 |
+
# project folder with User/paths already baked in. EDIT the three
|
| 16 |
+
# /home/sinclair/deepseek-v4-serve references and User/Group below if your
|
| 17 |
+
# layout differs.
|
| 18 |
+
#
|
| 19 |
+
# Install:
|
| 20 |
+
# sudo cp deepseek-v4-flash.service /etc/systemd/system/
|
| 21 |
+
# sudo systemctl daemon-reload
|
| 22 |
+
# sudo systemctl enable --now deepseek-v4-flash
|
| 23 |
+
# Logs:
|
| 24 |
+
# journalctl -fu deepseek-v4-flash
|
| 25 |
+
# =============================================================================
|
| 26 |
+
|
| 27 |
+
[Unit]
|
| 28 |
+
Description=vLLM OpenAI API server - DeepSeek-V4-Flash + MTP on 4x RTX 6000 Ada (SM89)
|
| 29 |
+
After=network-online.target
|
| 30 |
+
Wants=network-online.target
|
| 31 |
+
# A boot loop on a 159 GB model is expensive: allow 3 failed starts per 10 min,
|
| 32 |
+
# then stay down until 'systemctl reset-failed deepseek-v4-flash'.
|
| 33 |
+
StartLimitIntervalSec=600
|
| 34 |
+
StartLimitBurst=3
|
| 35 |
+
|
| 36 |
+
[Service]
|
| 37 |
+
Type=exec
|
| 38 |
+
User=sinclair
|
| 39 |
+
Group=sinclair
|
| 40 |
+
WorkingDirectory=/home/sinclair/deepseek-v4-serve
|
| 41 |
+
|
| 42 |
+
# Fail fast (and retry via Restart=) if the NVIDIA driver is not up yet,
|
| 43 |
+
# e.g. when racing device initialization right after boot.
|
| 44 |
+
ExecStartPre=/usr/bin/nvidia-smi
|
| 45 |
+
|
| 46 |
+
ExecStart=/home/sinclair/deepseek-v4-serve/serve.sh
|
| 47 |
+
|
| 48 |
+
Restart=on-failure
|
| 49 |
+
# Weight load alone is ~80 s (+ ~5 s MTP drafter); don't hammer restarts.
|
| 50 |
+
RestartSec=15
|
| 51 |
+
# Give the engine time to tear down 4 TP workers and NCCL cleanly on stop.
|
| 52 |
+
TimeoutStopSec=90
|
| 53 |
+
# SIGTERM to the API server first, then SIGKILL the whole cgroup
|
| 54 |
+
# (EngineCore + 4 worker processes) if anything lingers.
|
| 55 |
+
KillMode=mixed
|
| 56 |
+
LimitNOFILE=65535
|
| 57 |
+
LimitMEMLOCK=infinity
|
| 58 |
+
|
| 59 |
+
[Install]
|
| 60 |
+
WantedBy=multi-user.target
|
patch_dev145_bf16_oproj.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
patch_dev145_bf16_oproj.py
|
| 4 |
+
|
| 5 |
+
Fixes: AttributeError: 'ColumnParallelLinear' object has no attribute
|
| 6 |
+
'weight_scale' in vllm/models/deepseek_v4/nvidia/ops/o_proj.py:68 when
|
| 7 |
+
running --speculative-config '{"method":"mtp",...}' on
|
| 8 |
+
canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP with the dev145 SM89 wheel.
|
| 9 |
+
|
| 10 |
+
Root cause: deep_gemm_fp8_o_proj() unconditionally assumes an FP8-quantized
|
| 11 |
+
wo_a (reads weight_scale_inv / weight_scale). The target model's attention is
|
| 12 |
+
FP8 (compressed-tensors) and sails through; the checkpoint's MTP drafter head
|
| 13 |
+
is BF16/unquantized, so its wo_a is a plain ColumnParallelLinear with no
|
| 14 |
+
scale attribute. The fork author's spec-decode validation used the separate
|
| 15 |
+
DSpark checkpoint, never method=mtp with a BF16 head.
|
| 16 |
+
|
| 17 |
+
Fix: insert an early branch for scale-less wo_a that replicates the fused
|
| 18 |
+
op's math in plain torch: inverse RoPE (interleaved even/odd pairs on the
|
| 19 |
+
LAST rope_dim dims of each head, rotation by -theta, mirroring
|
| 20 |
+
_fused_inv_rope_fp8_quant_per_head in
|
| 21 |
+
common/ops/fused_inv_rope_fp8_quant.py), grouped bf16 einsum against
|
| 22 |
+
wo_a.weight viewed [n_groups, o_lora_rank, heads_per_group*head_dim], then
|
| 23 |
+
wo_b. Unfused and bf16, but the drafter is a single tiny layer, so the cost
|
| 24 |
+
is noise.
|
| 25 |
+
|
| 26 |
+
Usage:
|
| 27 |
+
# inside the serving venv
|
| 28 |
+
python patch_dev145_bf16_oproj.py # patches installed vllm
|
| 29 |
+
python patch_dev145_bf16_oproj.py <path> # patches an explicit file
|
| 30 |
+
|
| 31 |
+
Idempotent (marker-guarded); writes <file>.bak_bf16 once before modifying.
|
| 32 |
+
"""
|
| 33 |
+
import pathlib
|
| 34 |
+
import py_compile
|
| 35 |
+
import sys
|
| 36 |
+
|
| 37 |
+
MARKER = "SM89 patch: BF16/unquantized wo_a"
|
| 38 |
+
|
| 39 |
+
OLD = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` /
|
| 40 |
+
``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``.
|
| 41 |
+
"""
|
| 42 |
+
o_fp8, o_scale = fused_inv_rope_fp8_quant(
|
| 43 |
+
'''
|
| 44 |
+
|
| 45 |
+
NEW = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` /
|
| 46 |
+
``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``.
|
| 47 |
+
"""
|
| 48 |
+
if (
|
| 49 |
+
getattr(wo_a, "weight_scale_inv", None) is None
|
| 50 |
+
and getattr(wo_a, "weight_scale", None) is None
|
| 51 |
+
):
|
| 52 |
+
# ---- SM89 patch: BF16/unquantized wo_a (e.g. the BF16 MTP drafter
|
| 53 |
+
# head in canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP). The fused
|
| 54 |
+
# FP8 path below requires block scales; replicate its math in plain
|
| 55 |
+
# torch: inverse RoPE (interleaved even/odd pairs on the LAST
|
| 56 |
+
# ``rope_dim`` dims, rotation by -theta, mirroring
|
| 57 |
+
# _fused_inv_rope_fp8_quant_per_head), grouped bf16 einsum, wo_b.
|
| 58 |
+
num_tokens, num_heads, head_dim = o.shape
|
| 59 |
+
of = o.to(torch.float32)
|
| 60 |
+
cs = cos_sin_cache[positions.to(torch.long)]
|
| 61 |
+
half = rope_dim // 2
|
| 62 |
+
cos = cs[:, :half].unsqueeze(1)
|
| 63 |
+
sin = cs[:, half:].unsqueeze(1)
|
| 64 |
+
rope = of[..., nope_dim:]
|
| 65 |
+
x1 = rope[..., 0::2]
|
| 66 |
+
x2 = rope[..., 1::2]
|
| 67 |
+
rope_inv = torch.stack(
|
| 68 |
+
(x1 * cos + x2 * sin, x2 * cos - x1 * sin), dim=-1
|
| 69 |
+
).flatten(-2)
|
| 70 |
+
o_inv = torch.cat((of[..., :nope_dim], rope_inv), dim=-1)
|
| 71 |
+
o_grouped = o_inv.to(torch.bfloat16).reshape(
|
| 72 |
+
num_tokens, n_groups, heads_per_group * head_dim
|
| 73 |
+
)
|
| 74 |
+
w = wo_a.weight.to(torch.bfloat16).view(
|
| 75 |
+
n_groups, o_lora_rank, heads_per_group * head_dim
|
| 76 |
+
)
|
| 77 |
+
z_bf16 = torch.einsum("bhr,hdr->bhd", o_grouped, w)
|
| 78 |
+
return wo_b(z_bf16.flatten(1))
|
| 79 |
+
o_fp8, o_scale = fused_inv_rope_fp8_quant(
|
| 80 |
+
'''
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def resolve_installed_target() -> pathlib.Path:
|
| 84 |
+
import vllm # noqa: PLC0415
|
| 85 |
+
|
| 86 |
+
return (
|
| 87 |
+
pathlib.Path(vllm.__file__).parent
|
| 88 |
+
/ "models" / "deepseek_v4" / "nvidia" / "ops" / "o_proj.py"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def main() -> int:
|
| 93 |
+
if len(sys.argv) > 1:
|
| 94 |
+
target = pathlib.Path(sys.argv[1])
|
| 95 |
+
else:
|
| 96 |
+
target = resolve_installed_target()
|
| 97 |
+
|
| 98 |
+
if not target.is_file():
|
| 99 |
+
print(f"XX target not found: {target}", file=sys.stderr)
|
| 100 |
+
return 2
|
| 101 |
+
|
| 102 |
+
src = target.read_text()
|
| 103 |
+
|
| 104 |
+
if MARKER in src:
|
| 105 |
+
print(f"OK already patched, nothing to do: {target}")
|
| 106 |
+
return 0
|
| 107 |
+
|
| 108 |
+
if OLD not in src:
|
| 109 |
+
print(
|
| 110 |
+
"XX expected code block not found -- file differs from the "
|
| 111 |
+
"dev145 (g8c631d45e) layout this patch targets. Refusing to "
|
| 112 |
+
f"guess. File: {target}",
|
| 113 |
+
file=sys.stderr,
|
| 114 |
+
)
|
| 115 |
+
return 2
|
| 116 |
+
|
| 117 |
+
backup = target.with_suffix(target.suffix + ".bak_bf16")
|
| 118 |
+
if not backup.exists():
|
| 119 |
+
backup.write_text(src)
|
| 120 |
+
print(f"OK backup written: {backup}")
|
| 121 |
+
|
| 122 |
+
target.write_text(src.replace(OLD, NEW, 1))
|
| 123 |
+
py_compile.compile(str(target), doraise=True)
|
| 124 |
+
print(f"OK patched + compiled: {target}")
|
| 125 |
+
return 0
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
if __name__ == "__main__":
|
| 129 |
+
raise SystemExit(main())
|
setup_deepseek_v4_sm89.sh
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# setup_deepseek_v4_sm89.sh (stage 2: MTP speculative decoding)
|
| 4 |
+
#
|
| 5 |
+
# Model repos (byte-identical; either works):
|
| 6 |
+
# https://huggingface.co/SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada
|
| 7 |
+
# https://huggingface.co/canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP
|
| 8 |
+
# (the original -- all credit for the quantization to canada-quant)
|
| 9 |
+
#
|
| 10 |
+
# Recreates, from scratch, the verified-working environment for serving
|
| 11 |
+
# DeepSeek-V4-Flash (W4A16 INT4 experts + FP8 attention + BF16 MTP head,
|
| 12 |
+
# 159 GB) on 4x RTX 6000 Ada (SM89) with vLLM, including MTP speculative
|
| 13 |
+
# decoding.
|
| 14 |
+
#
|
| 15 |
+
# Measured on 2026-07-21 (4x RTX 6000 Ada, TP=4, fp8_ds_mla KV cache):
|
| 16 |
+
# - plain decoding: ~80 tok/s single-stream
|
| 17 |
+
# - MTP (num_spec_tokens=1): 115-133 tok/s sustained, draft acceptance
|
| 18 |
+
# 96.6-100%, mean acceptance length ~1.97/2.00
|
| 19 |
+
#
|
| 20 |
+
# Verified-working state this script reproduces:
|
| 21 |
+
# - vLLM 0.23.1rc1.dev145+g8c631d45e.cu130 <- the ONLY release of the
|
| 22 |
+
# yhfgyyf/vllm-deepseek-v4-sm89 fork whose SM89 path actually works.
|
| 23 |
+
# (The newer dev1018 release regressed the SM89 indexer-logits fallback.)
|
| 24 |
+
# - transformers 5.8.1, PINNED AND INSTALLED LAST. Newer releases fail
|
| 25 |
+
# config validation on this checkpoint: their ALLOWED_LAYER_TYPES no
|
| 26 |
+
# longer accepts the 'hash_moe' entries in mlp_layer_types. Installing
|
| 27 |
+
# the vLLM wheel afterwards would let pip drag a newer transformers back
|
| 28 |
+
# in, so the pin must be the final install step (and is asserted below).
|
| 29 |
+
# - flashinfer_python 0.6.14+sm89 (installed, but idle on the dev145
|
| 30 |
+
# Triton path; kept to match the reference environment)
|
| 31 |
+
# - flashinfer-cubin 0.6.13
|
| 32 |
+
# - torch 2.11.0+cu130
|
| 33 |
+
# - NO deep_gemm (its mere presence hijacks a presence-gated code path
|
| 34 |
+
# and crashes SM89 -- it must NOT be installed)
|
| 35 |
+
# - ONE source patch on top of the wheel (applied automatically below):
|
| 36 |
+
# a BF16/unquantized-wo_a fallback in nvidia/ops/o_proj.py. The fused
|
| 37 |
+
# o-projection assumes FP8 block scales; the checkpoint's BF16 MTP
|
| 38 |
+
# drafter head has none and crashes without it. The FP8 target model's
|
| 39 |
+
# path is untouched.
|
| 40 |
+
# - Sparse MLA runs via the portable Triton path: VLLM_TRITON_MLA_SPARSE=1
|
| 41 |
+
# and NO --attention-backend flag.
|
| 42 |
+
# - serve.sh resolves the model to a concrete snapshot DIRECTORY at launch
|
| 43 |
+
# instead of trusting hub ref resolution: an online metadata check (e.g.
|
| 44 |
+
# an interrupted huggingface-cli download after the upstream repo gained
|
| 45 |
+
# a new commit) can leave refs/main pointing at a snapshot that was
|
| 46 |
+
# never downloaded, which breaks HF_HUB_OFFLINE resolution.
|
| 47 |
+
#
|
| 48 |
+
# What this script does NOT do (on purpose):
|
| 49 |
+
# - Download the 159 GB model (see the note printed at the end)
|
| 50 |
+
# - Any system-level changes (apt, /usr/local/cuda symlinks). It only
|
| 51 |
+
# VERIFIES that /usr/local/cuda-13.0 exists, because everything is
|
| 52 |
+
# pinned to it at launch time.
|
| 53 |
+
# =============================================================================
|
| 54 |
+
|
| 55 |
+
# ----------------------------- PARAMETERS ------------------------------------
|
| 56 |
+
PROJECT_DIR="${1:-$HOME/deepseek-v4-serve}" # folder to create (arg 1 overrides)
|
| 57 |
+
MODEL="${2:-SinclairSchneider/DeepSeek-V4-Flash-W4A16-FP8-MTP-Ada}" # HF repo id OR local path (arg 2 overrides)
|
| 58 |
+
# The default is the Ada mirror (ships these scripts alongside the weights).
|
| 59 |
+
# The original repo works identically -- the checkpoints are byte-identical:
|
| 60 |
+
# ./setup_deepseek_v4_sm89.sh <dir> canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP
|
| 61 |
+
|
| 62 |
+
SERVED_NAME="DeepSeek-V4-Flash" # name exposed on the OpenAI-compatible API
|
| 63 |
+
PORT=8002
|
| 64 |
+
GPUS="0,1,2,3"
|
| 65 |
+
TP_SIZE=4
|
| 66 |
+
MAX_MODEL_LEN=262144
|
| 67 |
+
GPU_MEM_UTIL=0.96
|
| 68 |
+
MAX_NUM_SEQS=4
|
| 69 |
+
MAX_NUM_BATCHED_TOKENS=2048 # author's spec-decode recommendation
|
| 70 |
+
NUM_SPEC_TOKENS=1 # MTP draft depth (1 = validated config)
|
| 71 |
+
REASONING_EFFORT="high" # default chat-template reasoning effort
|
| 72 |
+
SERVICE_USER="$(id -un)" # user baked into the generated systemd unit
|
| 73 |
+
|
| 74 |
+
PYTHON_BIN="python3.12" # venv interpreter (wheel is cp312)
|
| 75 |
+
CUDA_HOME_DIR="/usr/local/cuda-13.0" # MUST be 13.0 to match cu130 wheels
|
| 76 |
+
LOCAL_WHEEL_DIR="$HOME/wheels" # checked first, before downloading
|
| 77 |
+
|
| 78 |
+
# Pinned artifacts (do not change casually -- this exact combination works)
|
| 79 |
+
GH_REPO="yhfgyyf/vllm-deepseek-v4-sm89"
|
| 80 |
+
VLLM_TAG="v0.23.1rc1.dev145-g8c631d45e-cu130-sm89"
|
| 81 |
+
VLLM_WHEEL="vllm-0.23.1rc1.dev145+g8c631d45e.cu130-cp312-cp312-linux_x86_64.whl"
|
| 82 |
+
FI_TAG="v0.23.1rc1.dev1018-g8aba6ae7e-cu130-sm89" # flashinfer wheel only exists in this release
|
| 83 |
+
FI_WHEEL="flashinfer_python-0.6.14+sm89-py3-none-any.whl"
|
| 84 |
+
TORCH_SPEC="torch==2.11.0"
|
| 85 |
+
TORCH_INDEX="https://download.pytorch.org/whl/cu130"
|
| 86 |
+
FI_CUBIN_SPEC="flashinfer-cubin==0.6.13"
|
| 87 |
+
TRANSFORMERS_SPEC="transformers==5.8.1" # MUST be installed LAST (see header)
|
| 88 |
+
# -----------------------------------------------------------------------------
|
| 89 |
+
|
| 90 |
+
set -euo pipefail
|
| 91 |
+
|
| 92 |
+
log() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; }
|
| 93 |
+
warn() { printf '\n\033[1;33m!! %s\033[0m\n' "$*"; }
|
| 94 |
+
die() { printf '\n\033[1;31mXX %s\033[0m\n' "$*" >&2; exit 1; }
|
| 95 |
+
|
| 96 |
+
# ----------------------------- SANITY CHECKS ---------------------------------
|
| 97 |
+
log "Sanity checks"
|
| 98 |
+
|
| 99 |
+
command -v "$PYTHON_BIN" >/dev/null 2>&1 \
|
| 100 |
+
|| die "$PYTHON_BIN not found (the pinned wheels are cp312)."
|
| 101 |
+
|
| 102 |
+
command -v nvidia-smi >/dev/null 2>&1 \
|
| 103 |
+
|| die "nvidia-smi not found -- NVIDIA driver missing?"
|
| 104 |
+
|
| 105 |
+
[ -x "$CUDA_HOME_DIR/bin/nvcc" ] \
|
| 106 |
+
|| die "$CUDA_HOME_DIR/bin/nvcc not found. cu130 wheels need CUDA 13.0 here."
|
| 107 |
+
|
| 108 |
+
"$CUDA_HOME_DIR/bin/nvcc" --version | grep -q "cuda_13\.0" \
|
| 109 |
+
|| die "$CUDA_HOME_DIR/bin/nvcc is not CUDA 13.0. Mixed toolchains break JIT builds (one-arg __cudaLaunch vs. 13.x headers)."
|
| 110 |
+
|
| 111 |
+
[ -e "$PROJECT_DIR" ] && die "$PROJECT_DIR already exists -- refusing to clobber it."
|
| 112 |
+
|
| 113 |
+
# ----------------------------- FOLDER + VENV ---------------------------------
|
| 114 |
+
log "Creating $PROJECT_DIR and virtual environment"
|
| 115 |
+
mkdir -p "$PROJECT_DIR/wheels"
|
| 116 |
+
"$PYTHON_BIN" -m venv "$PROJECT_DIR/.venv"
|
| 117 |
+
VPY="$PROJECT_DIR/.venv/bin/python"
|
| 118 |
+
"$VPY" -m pip install --upgrade pip >/dev/null
|
| 119 |
+
|
| 120 |
+
# ----------------------------- WHEEL RETRIEVAL -------------------------------
|
| 121 |
+
# Lessons encoded here:
|
| 122 |
+
# * pip REFUSES wheels whose filename was changed -- keep canonical names.
|
| 123 |
+
# * GitHub release asset URLs contain '+', which must be sent as %2B.
|
| 124 |
+
# * gh CLI needs auth; the plain REST API does not.
|
| 125 |
+
fetch_asset() { # fetch_asset <tag> <asset-filename>
|
| 126 |
+
local tag="$1" name="$2" dest="$PROJECT_DIR/wheels/$2" url
|
| 127 |
+
if [ -f "$LOCAL_WHEEL_DIR/$name" ]; then
|
| 128 |
+
log "Using local copy of $name from $LOCAL_WHEEL_DIR"
|
| 129 |
+
cp "$LOCAL_WHEEL_DIR/$name" "$dest"
|
| 130 |
+
return
|
| 131 |
+
fi
|
| 132 |
+
log "Downloading $name from release $tag"
|
| 133 |
+
url=$(curl -fsS "https://api.github.com/repos/$GH_REPO/releases/tags/$tag" \
|
| 134 |
+
| "$VPY" -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if a['name']=='$name'][0])") \
|
| 135 |
+
|| die "Could not resolve asset $name in release $tag"
|
| 136 |
+
curl -fL -o "$dest" "${url//+/%2B}" || die "Download failed: $name"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
fetch_asset "$FI_TAG" "$FI_WHEEL"
|
| 140 |
+
fetch_asset "$VLLM_TAG" "$VLLM_WHEEL"
|
| 141 |
+
|
| 142 |
+
# ----------------------------- INSTALL ORDER ---------------------------------
|
| 143 |
+
# Order matters: torch cu130 first so nothing drags in a CPU/other-CUDA torch,
|
| 144 |
+
# then the SM89 flashinfer wheel so pip never fetches the official one, then
|
| 145 |
+
# the vLLM wheel (with deps), and transformers==5.8.1 LAST so that nothing
|
| 146 |
+
# can override the pin afterwards.
|
| 147 |
+
log "Installing torch 2.11.0 (cu130)"
|
| 148 |
+
"$VPY" -m pip install "$TORCH_SPEC" --index-url "$TORCH_INDEX"
|
| 149 |
+
|
| 150 |
+
log "Installing flashinfer-cubin"
|
| 151 |
+
"$VPY" -m pip install "$FI_CUBIN_SPEC"
|
| 152 |
+
|
| 153 |
+
log "Installing SM89 FlashInfer wheel"
|
| 154 |
+
"$VPY" -m pip install "$PROJECT_DIR/wheels/$FI_WHEEL"
|
| 155 |
+
|
| 156 |
+
log "Installing vLLM dev145 (SM89 golden release) + dependencies"
|
| 157 |
+
"$VPY" -m pip install "$PROJECT_DIR/wheels/$VLLM_WHEEL"
|
| 158 |
+
|
| 159 |
+
log "Ensuring deep_gemm is ABSENT (presence breaks SM89)"
|
| 160 |
+
"$VPY" -m pip uninstall -y deep-gemm deep_gemm deepgemm >/dev/null 2>&1 || true
|
| 161 |
+
|
| 162 |
+
log "Pinning transformers 5.8.1 (LAST install -- newer releases reject the checkpoint's hash_moe layer types)"
|
| 163 |
+
"$VPY" -m pip install "$TRANSFORMERS_SPEC"
|
| 164 |
+
|
| 165 |
+
# ----------------------------- SOURCE PATCH ----------------------------------
|
| 166 |
+
# BF16/unquantized-wo_a fallback for the fused o-projection. Required for the
|
| 167 |
+
# BF16 MTP drafter head; harmless otherwise (guarded, target path unchanged).
|
| 168 |
+
log "Writing and applying the BF16 o_proj patch"
|
| 169 |
+
cat > "$PROJECT_DIR/patch_dev145_bf16_oproj.py" <<'PATCH_EOF'
|
| 170 |
+
#!/usr/bin/env python3
|
| 171 |
+
"""
|
| 172 |
+
patch_dev145_bf16_oproj.py
|
| 173 |
+
|
| 174 |
+
Fixes: AttributeError: 'ColumnParallelLinear' object has no attribute
|
| 175 |
+
'weight_scale' in vllm/models/deepseek_v4/nvidia/ops/o_proj.py:68 when
|
| 176 |
+
running --speculative-config '{"method":"mtp",...}' on
|
| 177 |
+
canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP with the dev145 SM89 wheel.
|
| 178 |
+
|
| 179 |
+
Root cause: deep_gemm_fp8_o_proj() unconditionally assumes an FP8-quantized
|
| 180 |
+
wo_a (reads weight_scale_inv / weight_scale). The target model's attention is
|
| 181 |
+
FP8 (compressed-tensors) and sails through; the checkpoint's MTP drafter head
|
| 182 |
+
is BF16/unquantized, so its wo_a is a plain ColumnParallelLinear with no
|
| 183 |
+
scale attribute.
|
| 184 |
+
|
| 185 |
+
Fix: insert an early branch for scale-less wo_a that replicates the fused
|
| 186 |
+
op's math in plain torch: inverse RoPE (interleaved even/odd pairs on the
|
| 187 |
+
LAST rope_dim dims of each head, rotation by -theta, mirroring
|
| 188 |
+
_fused_inv_rope_fp8_quant_per_head), grouped bf16 einsum against
|
| 189 |
+
wo_a.weight viewed [n_groups, o_lora_rank, heads_per_group*head_dim], then
|
| 190 |
+
wo_b.
|
| 191 |
+
|
| 192 |
+
Idempotent (marker-guarded); writes <file>.bak_bf16 once before modifying.
|
| 193 |
+
"""
|
| 194 |
+
import pathlib
|
| 195 |
+
import py_compile
|
| 196 |
+
import sys
|
| 197 |
+
|
| 198 |
+
MARKER = "SM89 patch: BF16/unquantized wo_a"
|
| 199 |
+
|
| 200 |
+
OLD = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` /
|
| 201 |
+
``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``.
|
| 202 |
+
"""
|
| 203 |
+
o_fp8, o_scale = fused_inv_rope_fp8_quant(
|
| 204 |
+
'''
|
| 205 |
+
|
| 206 |
+
NEW = ''' Shared by the FlashMLA and FlashInfer CUDA backends. ``einsum_recipe`` /
|
| 207 |
+
``tma_aligned_scales`` come from ``compute_fp8_einsum_recipe``.
|
| 208 |
+
"""
|
| 209 |
+
if (
|
| 210 |
+
getattr(wo_a, "weight_scale_inv", None) is None
|
| 211 |
+
and getattr(wo_a, "weight_scale", None) is None
|
| 212 |
+
):
|
| 213 |
+
# ---- SM89 patch: BF16/unquantized wo_a (e.g. the BF16 MTP drafter
|
| 214 |
+
# head in canada-quant/DeepSeek-V4-Flash-W4A16-FP8-MTP). The fused
|
| 215 |
+
# FP8 path below requires block scales; replicate its math in plain
|
| 216 |
+
# torch: inverse RoPE (interleaved even/odd pairs on the LAST
|
| 217 |
+
# ``rope_dim`` dims, rotation by -theta, mirroring
|
| 218 |
+
# _fused_inv_rope_fp8_quant_per_head), grouped bf16 einsum, wo_b.
|
| 219 |
+
num_tokens, num_heads, head_dim = o.shape
|
| 220 |
+
of = o.to(torch.float32)
|
| 221 |
+
cs = cos_sin_cache[positions.to(torch.long)]
|
| 222 |
+
half = rope_dim // 2
|
| 223 |
+
cos = cs[:, :half].unsqueeze(1)
|
| 224 |
+
sin = cs[:, half:].unsqueeze(1)
|
| 225 |
+
rope = of[..., nope_dim:]
|
| 226 |
+
x1 = rope[..., 0::2]
|
| 227 |
+
x2 = rope[..., 1::2]
|
| 228 |
+
rope_inv = torch.stack(
|
| 229 |
+
(x1 * cos + x2 * sin, x2 * cos - x1 * sin), dim=-1
|
| 230 |
+
).flatten(-2)
|
| 231 |
+
o_inv = torch.cat((of[..., :nope_dim], rope_inv), dim=-1)
|
| 232 |
+
o_grouped = o_inv.to(torch.bfloat16).reshape(
|
| 233 |
+
num_tokens, n_groups, heads_per_group * head_dim
|
| 234 |
+
)
|
| 235 |
+
w = wo_a.weight.to(torch.bfloat16).view(
|
| 236 |
+
n_groups, o_lora_rank, heads_per_group * head_dim
|
| 237 |
+
)
|
| 238 |
+
z_bf16 = torch.einsum("bhr,hdr->bhd", o_grouped, w)
|
| 239 |
+
return wo_b(z_bf16.flatten(1))
|
| 240 |
+
o_fp8, o_scale = fused_inv_rope_fp8_quant(
|
| 241 |
+
'''
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def resolve_installed_target() -> pathlib.Path:
|
| 245 |
+
import vllm # noqa: PLC0415
|
| 246 |
+
|
| 247 |
+
return (
|
| 248 |
+
pathlib.Path(vllm.__file__).parent
|
| 249 |
+
/ "models" / "deepseek_v4" / "nvidia" / "ops" / "o_proj.py"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def main() -> int:
|
| 254 |
+
if len(sys.argv) > 1:
|
| 255 |
+
target = pathlib.Path(sys.argv[1])
|
| 256 |
+
else:
|
| 257 |
+
target = resolve_installed_target()
|
| 258 |
+
|
| 259 |
+
if not target.is_file():
|
| 260 |
+
print(f"XX target not found: {target}", file=sys.stderr)
|
| 261 |
+
return 2
|
| 262 |
+
|
| 263 |
+
src = target.read_text()
|
| 264 |
+
|
| 265 |
+
if MARKER in src:
|
| 266 |
+
print(f"OK already patched, nothing to do: {target}")
|
| 267 |
+
return 0
|
| 268 |
+
|
| 269 |
+
if OLD not in src:
|
| 270 |
+
print(
|
| 271 |
+
"XX expected code block not found -- file differs from the "
|
| 272 |
+
"dev145 (g8c631d45e) layout this patch targets. Refusing to "
|
| 273 |
+
f"guess. File: {target}",
|
| 274 |
+
file=sys.stderr,
|
| 275 |
+
)
|
| 276 |
+
return 2
|
| 277 |
+
|
| 278 |
+
backup = target.with_suffix(target.suffix + ".bak_bf16")
|
| 279 |
+
if not backup.exists():
|
| 280 |
+
backup.write_text(src)
|
| 281 |
+
print(f"OK backup written: {backup}")
|
| 282 |
+
|
| 283 |
+
target.write_text(src.replace(OLD, NEW, 1))
|
| 284 |
+
py_compile.compile(str(target), doraise=True)
|
| 285 |
+
print(f"OK patched + compiled: {target}")
|
| 286 |
+
return 0
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
if __name__ == "__main__":
|
| 290 |
+
raise SystemExit(main())
|
| 291 |
+
PATCH_EOF
|
| 292 |
+
"$VPY" "$PROJECT_DIR/patch_dev145_bf16_oproj.py"
|
| 293 |
+
|
| 294 |
+
# ----------------------------- VERIFICATION ----------------------------------
|
| 295 |
+
log "Verifying installed environment"
|
| 296 |
+
"$VPY" - <<'PYEOF'
|
| 297 |
+
import pathlib
|
| 298 |
+
import sys
|
| 299 |
+
|
| 300 |
+
import torch
|
| 301 |
+
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)"
|
| 302 |
+
assert torch.version.cuda == "13.0", f"torch CUDA is {torch.version.cuda}, expected 13.0"
|
| 303 |
+
|
| 304 |
+
import transformers
|
| 305 |
+
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"
|
| 306 |
+
|
| 307 |
+
import vllm
|
| 308 |
+
assert vllm.__version__.startswith("0.23.1rc1.dev145"), f"vLLM is {vllm.__version__}, expected dev145 (dev1018 is the broken release)"
|
| 309 |
+
|
| 310 |
+
from vllm.models.deepseek_v4.nvidia.ops import sm12x_deep_gemm_fallbacks as m
|
| 311 |
+
assert hasattr(m, "_fp8_paged_mqa_logits_sm12x"), "SM89 indexer fallbacks missing from this vLLM build"
|
| 312 |
+
|
| 313 |
+
o_proj = pathlib.Path(vllm.__file__).parent / "models/deepseek_v4/nvidia/ops/o_proj.py"
|
| 314 |
+
assert "SM89 patch: BF16/unquantized wo_a" in o_proj.read_text(), "BF16 o_proj patch not applied"
|
| 315 |
+
|
| 316 |
+
import flashinfer
|
| 317 |
+
try:
|
| 318 |
+
import deep_gemm # noqa: F401
|
| 319 |
+
sys.exit("deep_gemm is importable -- it MUST NOT be installed on SM89")
|
| 320 |
+
except ImportError:
|
| 321 |
+
pass
|
| 322 |
+
|
| 323 |
+
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")
|
| 324 |
+
if torch.cuda.is_available():
|
| 325 |
+
for i in range(torch.cuda.device_count()):
|
| 326 |
+
print(f"OK GPU {i}: {torch.cuda.get_device_name(i)} capability {torch.cuda.get_device_capability(i)}")
|
| 327 |
+
PYEOF
|
| 328 |
+
|
| 329 |
+
# ----------------------------- GENERATE serve.sh -----------------------------
|
| 330 |
+
log "Writing $PROJECT_DIR/serve.sh"
|
| 331 |
+
cat > "$PROJECT_DIR/serve.sh" <<SERVE_EOF
|
| 332 |
+
#!/usr/bin/env bash
|
| 333 |
+
# Launch DeepSeek-V4-Flash with MTP speculative decoding on 4x SM89
|
| 334 |
+
# -- generated by setup_deepseek_v4_sm89.sh
|
| 335 |
+
#
|
| 336 |
+
# * VLLM_TRITON_MLA_SPARSE=1 is LOAD-BEARING: it selects the portable Triton
|
| 337 |
+
# sparse-MLA path, the only one validated on Ada. Do NOT add an
|
| 338 |
+
# --attention-backend flag; the FlashInfer path crashes on SM89 (SWA cache
|
| 339 |
+
# shape mismatch).
|
| 340 |
+
# * 'env -u' scrubs stale profile exports that dev145 would otherwise read.
|
| 341 |
+
# * CUDA is pinned to 13.0 because this machine has six toolkits and an apt
|
| 342 |
+
# CUDA 12.0 nvcc in /usr/bin; unpinned JIT builds pick the wrong one.
|
| 343 |
+
# * The model is resolved to a concrete snapshot DIRECTORY below instead of
|
| 344 |
+
# letting hub ref resolution run under HF_HUB_OFFLINE: an interrupted
|
| 345 |
+
# online check can leave refs/main pointing at a snapshot that was never
|
| 346 |
+
# downloaded (LocalEntryNotFoundError at boot).
|
| 347 |
+
# * MTP measured at 115-133 tok/s (~1.5-1.65x over the ~80 tok/s baseline),
|
| 348 |
+
# 96.6-100% draft acceptance. To run WITHOUT speculative decoding, delete
|
| 349 |
+
# the --speculative-config line (and optionally --max-num-batched-tokens).
|
| 350 |
+
# * First requests JIT-compile ~a dozen Triton kernels (one-time latency
|
| 351 |
+
# spikes); throughput settles after the warm-up traffic.
|
| 352 |
+
set -euo pipefail
|
| 353 |
+
cd "\$(dirname "\$0")"
|
| 354 |
+
|
| 355 |
+
MODEL="$MODEL"
|
| 356 |
+
|
| 357 |
+
# Resolve a repo id to its local snapshot directory. Prefer refs/main when it
|
| 358 |
+
# points at a snapshot that actually exists; otherwise fall back to the
|
| 359 |
+
# newest snapshot directory present.
|
| 360 |
+
resolve_model() {
|
| 361 |
+
local m="\$1"
|
| 362 |
+
if [ -d "\$m" ]; then printf '%s\n' "\$m"; return; fi
|
| 363 |
+
local hub="\${HF_HOME:-\$HOME/.cache/huggingface}/hub"
|
| 364 |
+
local repo_dir="\$hub/models--\${m//\//--}"
|
| 365 |
+
local snaps="\$repo_dir/snapshots"
|
| 366 |
+
if [ -d "\$snaps" ]; then
|
| 367 |
+
local ref=""
|
| 368 |
+
[ -f "\$repo_dir/refs/main" ] && ref="\$(cat "\$repo_dir/refs/main" 2>/dev/null || true)"
|
| 369 |
+
if [ -n "\$ref" ] && [ -d "\$snaps/\$ref" ]; then
|
| 370 |
+
printf '%s\n' "\$snaps/\$ref"; return
|
| 371 |
+
fi
|
| 372 |
+
local newest
|
| 373 |
+
newest="\$(ls -1t "\$snaps" 2>/dev/null | head -1)"
|
| 374 |
+
if [ -n "\$newest" ]; then printf '%s\n' "\$snaps/\$newest"; return; fi
|
| 375 |
+
fi
|
| 376 |
+
printf '\n'
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
MODEL_PATH="\$(resolve_model "\$MODEL")"
|
| 380 |
+
if [ -z "\$MODEL_PATH" ]; then
|
| 381 |
+
echo "XX model '\$MODEL' not found locally. Download it once with:" >&2
|
| 382 |
+
echo " \$(dirname "\$0")/.venv/bin/huggingface-cli download \$MODEL" >&2
|
| 383 |
+
exit 1
|
| 384 |
+
fi
|
| 385 |
+
echo "==> serving model from: \$MODEL_PATH"
|
| 386 |
+
|
| 387 |
+
exec env -u VLLM_TRITON_MLA_SPARSE_HEAD_BLOCK_SIZE \\
|
| 388 |
+
-u VLLM_TRITON_MLA_SPARSE_TOPK_CHUNK_SIZE \\
|
| 389 |
+
-u VLLM_TRITON_MLA_SPARSE_QUERY_CHUNK_SIZE \\
|
| 390 |
+
-u VLLM_TRITON_MLA_SPARSE_MATMUL_DECODE \\
|
| 391 |
+
-u VLLM_VERSION_OVERRIDE \\
|
| 392 |
+
-u VLLM_PYTHON_EXECUTABLE \\
|
| 393 |
+
VLLM_TRITON_MLA_SPARSE=1 \\
|
| 394 |
+
CUDA_HOME=$CUDA_HOME_DIR \\
|
| 395 |
+
CUDA_PATH=$CUDA_HOME_DIR \\
|
| 396 |
+
PATH="$CUDA_HOME_DIR/bin:\$PATH" \\
|
| 397 |
+
FLASHINFER_DISABLE_VERSION_CHECK=1 \\
|
| 398 |
+
VLLM_USE_FLASHINFER_SAMPLER=0 \\
|
| 399 |
+
HF_HUB_OFFLINE=1 \\
|
| 400 |
+
CUDA_VISIBLE_DEVICES=$GPUS \\
|
| 401 |
+
"\$(dirname "\$0")/.venv/bin/vllm" serve "\$MODEL_PATH" \\
|
| 402 |
+
--served-model-name "$SERVED_NAME" \\
|
| 403 |
+
--host 0.0.0.0 --port $PORT \\
|
| 404 |
+
--tensor-parallel-size $TP_SIZE \\
|
| 405 |
+
--kv-cache-dtype fp8_ds_mla \\
|
| 406 |
+
--block-size 256 \\
|
| 407 |
+
--max-model-len $MAX_MODEL_LEN \\
|
| 408 |
+
--gpu-memory-utilization $GPU_MEM_UTIL \\
|
| 409 |
+
--max-num-seqs $MAX_NUM_SEQS \\
|
| 410 |
+
--max-num-batched-tokens $MAX_NUM_BATCHED_TOKENS \\
|
| 411 |
+
--tokenizer-mode deepseek_v4 \\
|
| 412 |
+
--reasoning-parser deepseek_v4 \\
|
| 413 |
+
--enable-auto-tool-choice --tool-call-parser deepseek_v4 \\
|
| 414 |
+
--default-chat-template-kwargs '{"reasoning_effort": "$REASONING_EFFORT"}' \\
|
| 415 |
+
--speculative-config '{"method":"mtp","num_speculative_tokens":$NUM_SPEC_TOKENS}' \\
|
| 416 |
+
--trust-remote-code
|
| 417 |
+
SERVE_EOF
|
| 418 |
+
chmod +x "$PROJECT_DIR/serve.sh"
|
| 419 |
+
|
| 420 |
+
# ----------------------------- GENERATE canary.sh ----------------------------
|
| 421 |
+
log "Writing $PROJECT_DIR/canary.sh"
|
| 422 |
+
cat > "$PROJECT_DIR/canary.sh" <<CANARY_EOF
|
| 423 |
+
#!/usr/bin/env bash
|
| 424 |
+
# German coherence smoke test -- token salad here means broken numerics.
|
| 425 |
+
curl -s http://localhost:$PORT/v1/chat/completions \\
|
| 426 |
+
-H 'Content-Type: application/json' \\
|
| 427 |
+
-d '{"model":"$SERVED_NAME","messages":[{"role":"user","content":"Antworte in einem Satz: Was ist die Hauptstadt von Bayern?"}],"max_tokens":128}'
|
| 428 |
+
echo
|
| 429 |
+
CANARY_EOF
|
| 430 |
+
chmod +x "$PROJECT_DIR/canary.sh"
|
| 431 |
+
|
| 432 |
+
# ----------------------------- GENERATE systemd unit -------------------------
|
| 433 |
+
log "Writing $PROJECT_DIR/deepseek-v4-flash.service"
|
| 434 |
+
cat > "$PROJECT_DIR/deepseek-v4-flash.service" <<UNIT_EOF
|
| 435 |
+
# systemd unit for the DeepSeek-V4-Flash vLLM server (MTP speculative
|
| 436 |
+
# decoding) -- generated by setup_deepseek_v4_sm89.sh. serve.sh is the single
|
| 437 |
+
# source of truth for environment variables and launch flags; this unit only
|
| 438 |
+
# supervises it. systemd's clean service environment additionally guarantees
|
| 439 |
+
# no stale shell-profile exports can leak in.
|
| 440 |
+
#
|
| 441 |
+
# Install:
|
| 442 |
+
# sudo cp $PROJECT_DIR/deepseek-v4-flash.service /etc/systemd/system/
|
| 443 |
+
# sudo systemctl daemon-reload
|
| 444 |
+
# sudo systemctl enable --now deepseek-v4-flash
|
| 445 |
+
# Logs:
|
| 446 |
+
# journalctl -fu deepseek-v4-flash
|
| 447 |
+
|
| 448 |
+
[Unit]
|
| 449 |
+
Description=vLLM OpenAI API server - DeepSeek-V4-Flash + MTP on 4x RTX 6000 Ada (SM89)
|
| 450 |
+
After=network-online.target
|
| 451 |
+
Wants=network-online.target
|
| 452 |
+
# A boot loop on a 159 GB model is expensive: allow 3 failed starts per 10 min,
|
| 453 |
+
# then stay down until 'systemctl reset-failed deepseek-v4-flash'.
|
| 454 |
+
StartLimitIntervalSec=600
|
| 455 |
+
StartLimitBurst=3
|
| 456 |
+
|
| 457 |
+
[Service]
|
| 458 |
+
Type=exec
|
| 459 |
+
User=$SERVICE_USER
|
| 460 |
+
Group=$SERVICE_USER
|
| 461 |
+
WorkingDirectory=$PROJECT_DIR
|
| 462 |
+
|
| 463 |
+
# Fail fast (and retry via Restart=) if the NVIDIA driver is not up yet,
|
| 464 |
+
# e.g. when racing device initialization right after boot.
|
| 465 |
+
ExecStartPre=/usr/bin/nvidia-smi
|
| 466 |
+
|
| 467 |
+
ExecStart=$PROJECT_DIR/serve.sh
|
| 468 |
+
|
| 469 |
+
Restart=on-failure
|
| 470 |
+
# Weight load alone is ~80 s (+ ~5 s MTP drafter); don't hammer restarts.
|
| 471 |
+
RestartSec=15
|
| 472 |
+
# Give the engine time to tear down 4 TP workers and NCCL cleanly on stop.
|
| 473 |
+
TimeoutStopSec=90
|
| 474 |
+
# SIGTERM to the API server first, then SIGKILL the whole cgroup
|
| 475 |
+
# (EngineCore + 4 worker processes) if anything lingers.
|
| 476 |
+
KillMode=mixed
|
| 477 |
+
LimitNOFILE=65535
|
| 478 |
+
LimitMEMLOCK=infinity
|
| 479 |
+
|
| 480 |
+
[Install]
|
| 481 |
+
WantedBy=multi-user.target
|
| 482 |
+
UNIT_EOF
|
| 483 |
+
|
| 484 |
+
# ----------------------------- DONE ------------------------------------------
|
| 485 |
+
log "Environment ready in $PROJECT_DIR"
|
| 486 |
+
cat <<NEXT_EOF
|
| 487 |
+
|
| 488 |
+
Next steps:
|
| 489 |
+
1. Model weights: serve.sh resolves "$MODEL" from the local HF
|
| 490 |
+
cache (or uses it directly if it is a path). To fetch it (~159 GB),
|
| 491 |
+
run once:
|
| 492 |
+
$PROJECT_DIR/.venv/bin/huggingface-cli download "$MODEL"
|
| 493 |
+
2. Start the server: $PROJECT_DIR/serve.sh
|
| 494 |
+
Expect ~80 s of weight loading (+ ~5 s for the MTP drafter), brief
|
| 495 |
+
Triton JIT compiles during warmup and the first requests, then
|
| 496 |
+
'Application startup complete'.
|
| 497 |
+
3. Smoke test: $PROJECT_DIR/canary.sh
|
| 498 |
+
A correct one-sentence answer naming Muenchen = numerics are sound.
|
| 499 |
+
Watch the SpecDecoding metrics lines: draft acceptance should sit
|
| 500 |
+
around 95-100% with mean acceptance length near 2.00.
|
| 501 |
+
4. Run as a service: see header of
|
| 502 |
+
$PROJECT_DIR/deepseek-v4-flash.service
|
| 503 |
+
NEXT_EOF
|