SinclairSchneider commited on
Commit
a72139f
·
verified ·
1 Parent(s): 88ad0c8

Upload 3 files

Browse files
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