r37: fix PP+MTP verify CUDA graph (pre-planned path missing pp_proxy fill); beats A16 by 40% on killer, now serving on 60.8

This commit is contained in:
yy-fighting 2026-09-09 01:24:29 +08:00
parent ffda226f5a
commit a3b8f1b2ab
11 changed files with 2810 additions and 1 deletions

View File

@ -14,7 +14,7 @@
| 60.5 | `glm53-nvfp4`Up 29h | **NVFP4 团队生产**deploy_glm53_605.shmd5 fcd9109b。生产机铁律不实验、不重启、不覆盖脚本 | `profiles/pro6000/glm53_nvfp4_pro6000_sglang_tp8eagle.env`(方案 A 口径) |
| 60.6 | 无容器,但 8 卡被外部裸金属实验占用(`/data/hzy/sparse-opd-*`09-08 晚实测) | 外部任务,勿动(此前台账漏记) | — |
| 60.7 | 基本空4 卡仍有 `/home/user/dirA_exp` 外部小任务09-08 晚实测) | 09-08 已拆除清空(方案 F 前身单机实验 + 场景一深优资产留盘),不再恢复 | — |
| 60.8 | `glm53-nvfp4`Up:30000restart=unless-stopped | **A16 口径在役**09-08 i8k/o1k/c16 压测收官保留tp8eagle.env 场景二变体 = A + MRR32 + graph bs 4/8/12/16池 276,864质量门 7/7。同场景实测最优为 BTP4PP2out 265 vs A16 219 tok/s如转纯吞吐用途可切 B。压测前经授权清理了 GPU3/6 的 dirA_ext 外部 eval 进程。**09-08 晚 PP+MTP r36 实验后已恢复本口径**(实验判决:去 GLOO 修复成立且稳定,但 i8k 场景 PP+MTP 111-116s 差 B' 61.5s 1.81×,图模式 r36g 为正确性灾难——详见 `experiments/pro6000/glm53_ppmtp_r36_degloo/`;语料消耗至 21,235,008/21,296,780 无复测余量) | `profiles/pro6000/glm53_nvfp4_pro6000_sglang_tp8eagle.env`+注释中场景二高并发变体);压测记录 `experiments/pro6000/glm53_nvfp4_i8k_o1k_c16_bench/``experiments/pro6000/glm53_ppmtp_r36_degloo/` |
| 60.8 | `glm53-nvfp4`Up:30000restart=unless-stopped | **r37 PP+MTP verify-graph 口径在役**09-08 深夜取代 A16TP4PP2 + EAGLE 3/1/4 + verify CUDA graph + draft eager + de-GLOO + SYNC_MASK=1279 挂载文件,启动 `bash /root/deploy_ppmtp_r37.sh '--tp 4 --pp-size 2 --disable-overlap-schedule --max-prefill-tokens 16384' mtp 8192 0.88 1 1 0 0`)。质量门 7/7 ×2、conc_test×3、killer×6 零崩溃killer cc16 双种子 **63.8/64.7s vs A16 105.6/96.0s-40%**、TPOT 41.5/39.3ms、TTFT 33s vs 58-61s近乎减半。修复根因=pre-planned 早退路径缺 pp_proxy 补拷(详见 `experiments/pro6000/glm53_ppmtp_r37_verify_graph/`。A16 切回脚本 deploy_glm53_605.sh 留盘sglang_patch2/eagle_worker_common.py 已升级 bisect 版("1"/"0" 语义兼容) | `experiments/pro6000/glm53_ppmtp_r37_verify_graph/`patches+scripts+results 全量);前史 `experiments/pro6000/glm53_ppmtp_r36_degloo/` |
## 方案 A-F 一览GLM-5.3-NVFP4 @ pro60002026-09-08 双场景报告口径)

View File

@ -0,0 +1,66 @@
# GLM-5.3-NVFP4 PP+MTP verify CUDA graph 修复r37— 2026-09-08 @ 174.1.60.8
上一个实验glm53_ppmtp_r36_degloo判定PP+MTP eager 模式正确但 MTP 机器开销吃光收益step 239ms@cc16 vs
净胜拐点 148ms图模式r36g则正确性崩溃accept 2.85→1.05 + 数字乱码)。本实验继续修复图捕获并兑现
MTP+PP 双优势。**结果修复成功killer e2e -40%、TPOT -40%、TTFT 近乎减半,全面胜过在役 A16 配置并留役生产。**
## 环境
- 镜像 lmsysorg/sglang:nightly-dev-cu13-20260901-07c8f729GLM-5.3-NVFP48×RTX 6000DSM120
- TP4PP2 + EAGLE3 steps / topk1 / 4 draft tokensDSAhicache 3memfrac 0.88chunk 8192
--disable-overlap-scheduleSYNC_MASK=127de-GLOO 中继r36 遗产)
- r36 的 8 个挂载文件全部沿用 + 本实验新增第 9 挂载decode_cuda_graph_runner
## 排查过程(按时间序)
1. **:1515 切片修复**r33 注释的根因猜测decode runner `execute()` 对非末 stage 的 PPProxyTensors
`self.bs`(请求数)切片,截断 TARGET_VERIFY 树行bs×4→ 改按 `raw_num_token`。**必要但不充分**
r37 实测 accept 仍 1.02-1.27 + 乱码bs=1 零 padding 下确定性复现 → 排除竞态/padding
2. **按 stage 二分**:把 /root/sglang_patch2/eagle_worker_common.py 的 `_PP_FORCE_EAGER_VERIFY` 门扩展为
1/2/3 模式(全 eager / 仅首 stage eager / 仅末 stage eager。判决
- 模式 2PP0 eager + PP1 图)→ 仍乱码;模式 3PP0 图 + PP1 eager→ 完全正确。
- **缺陷唯一在末 stagePP1的 verify 图**PP0 图(含 :1515 修复)被实证正确。
3. **遥测**runner 内置 VRFY-IN/BUF/G/SLOT + xcheckSGLANG_PP_SPEC_DEBUG=2/3
- 入口 relayed hidden 每轮新鲜正常;返回 hidden_states 新鲜logits argmax 全是高频英文 token' of'/' the'/':'
tokenizer 解码实证)→ "模型没看到上下文"signature。
- VRFY-SLOTpp 槽位注册健康enabled=True copy_from_fb=True buffer=静态张量 data_ptr 一致)。
- **VRFY-BUFfill_from 之后静态 pp 缓冲仍全零VRFY-IN needs_init=False** → 真凶。
## 根因(真·根因,取代 r33 的 :1515 猜测为唯一根因)
spec worker 的 plan-stream 在 prepare 阶段预跑 `load_batch`eagle_prepare_for_verify 的 overlap 设计)——彼时
**PP 中继尚未到达**`pp_proxy_tensors=None``fill_from` 跳过全部 pp 槽位,仅完成常规槽位 + 注意力元数据,
batch 被标记 pre-plannedneeds_forward_metadata_init()=False。前向流 `execute()` 于是走 pre-planned 早退路径,
**该路径只补拷 input_ids/positions 两个 spec 必需字段** → pp_proxyhidden_states/residual/topk_indices
静态缓冲永远是分配时的全零。捕获图内的 clone kernelrun_once 体内,:1212从静态缓冲拷零 → 末 stage 在零激活上
计算 → logits 为"无上下文"垃圾 → 全拒 + 乱码。
一切旁证自洽:
- PP0首 stage不吃 pp 输入input_ids/positions 恰好是早退路径补拷的两个)→ 模式 3 正确;
- B'nomtp+图+PP无 spec worker plan-stream → 走完整 fill_from 路径 → 正确;
- TP8 A16 无 PP → 正确;
- 图 logits ≈ eager 参考滞后一轮xcheck 实测):零 token 嵌入 + 上一轮 KV 历史的注意力产出。
## 修复patches/decode_cuda_graph_runner_fix.py第 9 挂载文件)
两处改动:
1. `execute()` :1515PPProxyTensors 抽取改按 `self.raw_num_token` 切(原 :bs 截断树行decode width=1 时二者等价于
raw/padded 行数之差,树行场景必须 raw_num_token
2. `load_batch()` pre-planned 早退路径:新增 pp_proxy 补拷——`pp_proxy_tensors` 各 key 拷入静态缓冲
`[:src.shape[0]]`pad 尾(`self.bs*captured_req_width` 之外)清零保确定性。这是本修复的核心。
生产运行时遥测代码VRFY-* / xcheck随文件留存由 SGLANG_PP_SPEC_DEBUG 门控0=关闭,无开销)。
## 验证阶梯(全部通过)
- 质量门 7/7 ×2 bootGSM8K×5/中文推理/tool call
- accept 恢复 3.59-3.75eager 档);
- conc_test ×3 ALL-OK、killer ×6 零崩溃(三个 boot 累计);
- 性能见 results/bench_results.mdvs eager -40% e2evs A16 在役配置 -40% e2e、TTFT 33s vs 58-61s。
## 生产终态
60.8 glm53-nvfp4:30000 = r37 胜者配置verify 图 + draft eager + de-GLOO + mask127
`--restart unless-stopped`,已取代 A16 留役A16 脚本 deploy_glm53_605.sh 留盘可切回)。
draft 图DRAFTEAGER=0实测无增益噪声带内互有胜负保持 eager 以少一个变量。
## 遗产与提醒
- /root/sglang_patch2/eagle_worker_common.py 已被本实验覆盖为 bisect 版md5 babb6461…env "1"/"0" 语义与
r36 版完全兼容,新增 "2"/"3" 单侧模式r36 档案中的旧 md5 以本目录为准。
- 语料池 61,772 token 剩余(< 一次 i8k 262,144i8k 永久无重跑余量killer random-ids 为标准口径
- killer 的 raw bench JSON/data/hf_models/bs_results/san_cc16_s*.json被 A16 复测覆盖,
胜者数字以 results/bench_results.md 记录为准。

View File

@ -0,0 +1,796 @@
from __future__ import annotations
import logging
import os
from typing import TYPE_CHECKING, Any, Optional
import torch
from sglang.kernels.ops.speculative.cache_locs import (
assign_draft_cache_locs_contiguous,
)
from sglang.kernels.ops.speculative.eagle import fill_bonus_tokens_func
from sglang.srt.distributed import get_pp_group
from sglang.srt.layers.logprob_processor import compute_spec_logprobs
from sglang.srt.managers.utils import GenerationBatchResult
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
)
from sglang.srt.speculative.eagle_info import EagleDraftInput, EagleVerifyInput
from sglang.srt.speculative.eagle_utils import (
TreeMaskMode,
build_tree_kernel_efficient,
eagle_prepare_for_verify,
eagle_sample,
)
from sglang.srt.speculative.spec_utils import (
GrammarTree,
build_grammar_vocab_mask,
commit_mamba_states_after_verify,
move_accept_tokens_to_target_kvcache,
record_stream_each,
record_stream_for_v2_verify,
)
from sglang.srt.utils import is_cpu
from sglang.srt.utils.async_probe import (
maybe_detect_inf,
maybe_detect_nan,
maybe_detect_oob,
)
from sglang.srt.utils.common import is_npu
_is_cpu = is_cpu()
_is_npu = is_npu()
_PP_SPEC_DEBUG = os.getenv("SGLANG_PP_SPEC_DEBUG", "0") == "1"
# "0"=verify graphs on all stages; "1"=eager on all stages;
# "2"=eager on the FIRST PP stage only; "3"=eager on the LAST PP stage only.
_PP_FORCE_EAGER_VERIFY = os.getenv("SGLANG_PP_FORCE_EAGER_VERIFY", "0")
logger = logging.getLogger(__name__)
def _pp_spec_proxy_desc(proxies) -> str:
"""Per-row absmax summary of a PPProxyTensors payload (dict-backed)."""
if proxies is None:
return "None"
tensors = getattr(proxies, "tensors", None)
if tensors is None:
return f"{type(proxies).__name__}(no-tensors)"
if isinstance(tensors, dict):
items = list(tensors.items())
elif isinstance(tensors, (list, tuple)):
items = [(str(i), t) for i, t in enumerate(tensors)]
else:
items = [("0", tensors)]
parts = []
for name, t in items:
if torch.is_tensor(t):
desc = f"{name}:{tuple(t.shape)}:{str(t.dtype).replace('torch.', '')}"
if t.numel():
try:
desc += f":absmax={t.abs().max().item():.3e}"
if t.dim() == 2 and t.shape[0] <= 8:
desc += ":rows=" + ",".join(
f"{t[i].abs().max().item():.2e}"
for i in range(t.shape[0])
)
except Exception as e: # lazy proxies may not be readable yet
desc += f":unreadable({type(e).__name__})"
parts.append(desc)
else:
parts.append(f"{name}:{type(t).__name__}")
return ";".join(parts)
if _is_cpu:
from sgl_kernel import assign_draft_cache_locs_contiguous_cpu
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.managers.tp_worker import TpModelWorker
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.speculative.eagle_draft_cuda_graph_runner import (
EAGLEDraftCudaGraphRunner,
)
from sglang.srt.speculative.eagle_info import EagleDraftExtendInput
def duplicate_prefix_tail_to_draft_branches(
token_to_kv_pool,
rows: torch.Tensor,
prefix_base: torch.Tensor,
last_page: torch.Tensor,
num_new_pages: torch.Tensor,
topk: int,
page_size: int,
) -> None:
"""Copy the prefix partial-tail page into each branch's first-page holes (page>1 + topk>1).
The draft-decode expand pass reads each branch's own draft page by block id
(cache_loc // page_size), so branch b>=1's hole slots [0, last_page) must hold the
real prefix tail (branch 0's first page already is it). Mirrors V1 #7725.
"""
if topk <= 1:
return
bs = rows.shape[0]
page_off = torch.arange(page_size, device=rows.device, dtype=torch.int64)
branches = torch.arange(1, topk, device=rows.device, dtype=torch.int64).view(
1, topk - 1, 1
)
# Source: the prefix tail page [prefix_base, prefix_base + page_size), one per branch.
src_pos = (prefix_base.view(bs, 1, 1) + page_off.view(1, 1, page_size)).expand(
bs, topk - 1, page_size
)
# Target: branch b's first page [prefix_base + b*num_new_pages*page, + page_size).
tgt_pos = (
prefix_base.view(bs, 1, 1)
+ branches * (num_new_pages.view(bs, 1, 1) * page_size)
+ page_off.view(1, 1, page_size)
)
# Only [0, last_page) holds real prefix KV; [last_page, page_size) are the branch's
# own draft slots and must not be overwritten.
vmask = (page_off.view(1, 1, page_size) < last_page.view(bs, 1, 1)).expand(
bs, topk - 1, page_size
)
src_slots = torch.gather(rows, 1, src_pos.reshape(bs, -1)).reshape(
bs, topk - 1, page_size
)[vmask]
tgt_slots = torch.gather(rows, 1, tgt_pos.reshape(bs, -1)).reshape(
bs, topk - 1, page_size
)[vmask]
if src_slots.numel() > 0:
token_to_kv_pool.move_kv_cache(tgt_slots, src_slots)
def prepare_for_draft_extend(
draft_extend_input: EagleDraftExtendInput,
batch: ScheduleBatch,
predict: torch.Tensor,
num_draft_tokens: int,
draft_model_runner: Any,
cuda_graph_runner: Any,
*,
return_hidden_states_before_norm: bool,
widened_out_cache_loc: Optional[torch.Tensor] = None,
widened_positions: Optional[torch.Tensor] = None,
):
bs = len(batch.seq_lens)
# Optional window widening (num_front_tokens=0 -> off): prepend that many
# rows below the boundary. Locs/positions arrive precomputed; token/hidden
# buffers are zeroed placeholders the caller fills after the plan-stream join.
num_front_tokens = draft_extend_input.num_front_tokens
widen = num_front_tokens > 0 and not batch.forward_mode.is_idle()
front_offset = num_front_tokens if widen else 0
num_window_tokens = num_draft_tokens + front_offset
extend_num_tokens = bs * num_window_tokens
# When seq_lens_cpu is absent, stay on GPU-only path -- no .tolist()/.cpu().
gpu_only = batch.seq_lens_cpu is None
batch.spec_info = draft_extend_input
# Do NOT cast predict dtype here. The caller (e.g., _draft_extend_for_decode)
# may run this under a plan stream; casting inside the plan stream creates a
# cross-stream dependency that can lead to data races and break MTP acceptance.
# The caller should cast to int64 before entering the plan stream context.
if widen:
assert widened_out_cache_loc is not None and widened_positions is not None
batch.input_ids = predict.new_zeros((extend_num_tokens,))
batch.out_cache_loc = widened_out_cache_loc
# init_new adopts spec_info.positions when present.
draft_extend_input.positions = widened_positions
# Placeholder for the widened hidden window, filled by the worker.
if draft_extend_input.hidden_states is not None:
draft_extend_input.hidden_states = (
draft_extend_input.hidden_states.new_empty(
(extend_num_tokens, draft_extend_input.hidden_states.shape[1])
)
)
else:
batch.input_ids = predict
maybe_detect_oob(
batch.input_ids,
0,
batch.model_config.vocab_size,
"v2 prepare_for_draft_extend input_ids",
)
# init_new requires both list or both Tensor;
# gpu_only emits device tensors to skip H2D.
if gpu_only:
batch.prefix_lens = (batch.seq_lens - front_offset).clamp(min=0).to(torch.int32)
batch.extend_lens = torch.full(
(bs,), num_window_tokens, dtype=torch.int32, device=batch.seq_lens.device
)
else:
batch.prefix_lens = [
max(int(x) - front_offset, 0) for x in batch.seq_lens_cpu.tolist()
]
batch.extend_lens = [num_window_tokens] * bs
batch.extend_num_tokens = extend_num_tokens
capture_mode = (
CaptureHiddenMode.NULL
if draft_model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.FULL
)
batch.forward_mode = (
ForwardMode.IDLE
if batch.forward_mode.is_idle()
else ForwardMode.DRAFT_EXTEND_V2
)
forward_batch = ForwardBatch.init_new(
batch,
draft_model_runner,
capture_hidden_mode=capture_mode,
return_hidden_states_before_norm=return_hidden_states_before_norm,
)
# Forward sees post-write length (draft extend writes num_draft_tokens
# slots); mutation stays on forward_batch to preserve SB.seq_lens.
forward_batch.seq_lens = forward_batch.seq_lens + num_draft_tokens
if not gpu_only:
forward_batch.seq_lens_cpu = forward_batch.seq_lens_cpu + num_draft_tokens
forward_batch.seq_lens_sum = int(forward_batch.seq_lens_cpu.sum())
else:
# Supply CPU mirror (extend_seq_lens are all num_window_tokens) so
# backend max() reads from list without a per-iter D2H sync.
forward_batch.extend_seq_lens_cpu = [num_window_tokens] * bs
can_run_decode_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
if not batch.forward_mode.is_idle() and not can_run_decode_cuda_graph:
draft_model_runner.attn_backend.init_forward_metadata(forward_batch)
# Planned pre-pad; do NOT opt into post-pad re-plan. DSA's indexer
# cannot rebuild its deep_gemm schedule_meta on a DP-padded batch
# (the `_batch_size == batch_size` assertion, see #27091); the
# marked pre-pad metadata is used as-is, matching the proven
# skip_attn_backend_init=True behavior.
# On NPU with --disable-cuda-graph, block_table shape won't match
# after prepare_mlp_sync_batch padding; defer re-init to
# forward_extend (post-pad) instead.
if not is_npu() or can_run_decode_cuda_graph:
forward_batch.mark_forward_metadata_ready()
return forward_batch
def prepare_for_draft(
draft_input: EagleDraftInput,
req_to_token_pool: ReqToTokenPool,
batch: ScheduleBatch,
cuda_graph_runner: EAGLEDraftCudaGraphRunner,
draft_model_runner: ModelRunner,
topk: int,
num_steps: int,
):
if not batch.forward_mode.is_idle():
bs = len(batch.seq_lens)
# Assign cache locations (draft-write targets).
page_size = batch.token_to_kv_pool_allocator.page_size
if page_size == 1 or topk == 1:
batch.out_cache_loc = torch.empty(
(bs * topk * num_steps,),
dtype=torch.int64,
device=batch.device,
)
if _is_cpu:
assign_draft_cache_locs_contiguous_cpu(
batch.req_pool_indices,
req_to_token_pool.req_to_token,
batch.seq_lens,
batch.out_cache_loc,
req_to_token_pool.req_to_token.shape[1],
topk,
num_steps,
)
else:
# FIXME(lsyin): align with the default code path
assign_draft_cache_locs_contiguous[(bs,)](
batch.req_pool_indices,
req_to_token_pool.req_to_token,
batch.seq_lens,
batch.out_cache_loc,
req_to_token_pool.req_to_token.shape[1],
topk,
num_steps,
)
else:
# page_size > 1 + topk > 1: per-branch page-aligned draft pages.
# Reduce out_cache_loc from the page-aligned tree region down to the
# dense draft slots (skip each branch's duplicated prefix-tail slots
# and trailing padding), matching generate_draft_decode_kv_indices'
# paged read formula: prefix_base + t*num_new_pages*page + last_page + s.
# base is batch.seq_lens (== KV-ready committed prefix at draft time;
# the bonus is the tree root written by verify, not part of [0:seq_lens]).
rows = req_to_token_pool.req_to_token[batch.req_pool_indices.long()]
seq_lens = batch.seq_lens.to(torch.int64)
last_page = seq_lens % page_size
prefix_base = seq_lens - last_page
num_new_pages = (last_page + num_steps + page_size - 1) // page_size
topk_ids = torch.arange(topk, device=rows.device, dtype=torch.int64).view(
1, topk
)
starts = (
prefix_base.view(bs, 1)
+ topk_ids * (num_new_pages.view(bs, 1) * page_size)
+ last_page.view(bs, 1)
)
steps = torch.arange(num_steps, device=rows.device, dtype=torch.int64).view(
1, 1, num_steps
)
pos = (starts.view(bs, topk, 1) + steps).reshape(bs, topk * num_steps)
batch.out_cache_loc = torch.gather(rows, 1, pos).reshape(-1).contiguous()
# Each branch's page-aligned region starts with `last_page` hole slots
# overlapping the prefix tail page; duplicate the real prefix-tail KV
# into them so whole-page reads stay coherent (see helper docstring).
duplicate_prefix_tail_to_draft_branches(
draft_model_runner.token_to_kv_pool,
rows,
prefix_base,
last_page,
num_new_pages,
topk,
page_size,
)
# Get a forward batch
# Actual width of the next draft-decode forward: topk tokens per req.
draft_input.num_tokens_per_req = topk
draft_input.num_tokens_for_logprob_per_req = topk
capture_mode = (
CaptureHiddenMode.NULL
if draft_model_runner.spec_algorithm.is_standalone()
else CaptureHiddenMode.LAST
)
draft_input.positions = batch.seq_lens.repeat_interleave(topk, dim=0)
forward_batch = ForwardBatch.init_new(
batch,
draft_model_runner,
capture_hidden_mode=capture_mode,
return_hidden_states_before_norm=False,
)
can_run_decode_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
return forward_batch, can_run_decode_cuda_graph
def build_eagle_verify_input(
batch: ScheduleBatch,
draft_input: EagleDraftInput,
parent_list: torch.Tensor,
top_scores_index: torch.Tensor,
draft_tokens: torch.Tensor,
draft_probs: Optional[torch.Tensor],
*,
target_worker: TpModelWorker,
topk: int,
num_steps: int,
num_draft_tokens: int,
tree_mask_mode: TreeMaskMode,
device: str,
) -> EagleVerifyInput:
"""Shared draft() tail: idle input, tree-mask build, EagleVerifyInput assembly.
``draft_probs`` is the caller's source of draft distributions (single-layer
eagle: this round's draft_forward output; multi-layer eagle: the ones the
draft input carried).
"""
if batch.forward_mode.is_idle():
return EagleVerifyInput.create_idle_input(
topk,
num_steps,
num_draft_tokens,
device,
)
# Write straight into the backend's buffer when it owns one and this batch
# fits; an eager batch past the captured max_bs falls back to allocating.
bs = batch.seq_lens.shape[0]
target_attn_backend = target_worker.model_runner.attn_backend
verify_mask = target_attn_backend.verify_mask
if verify_mask is None:
tree_mask_buf, mask_mode, fill_mask = None, tree_mask_mode, True
else:
mask_mode, fill_mask = verify_mask.mode, verify_mask.is_read
tree_mask_buf = verify_mask.buffer if verify_mask.fits(bs) else None
# build_tree_kernel uses seq_lens_sum only to size the (non-preallocated)
# FULL_MASK tree mask; over-size is safe. Skip per-iter .sum().item() D2H via UB.
seq_lens_sum = batch.seq_lens_sum
if seq_lens_sum is None:
if tree_mask_buf is not None or mask_mode == TreeMaskMode.QLEN_ONLY:
# Preallocated, or a QLEN_ONLY allocation sized off bs alone.
seq_lens_sum = 0
else:
seq_lens_sum = bs * target_attn_backend.max_context_len
(
tree_mask,
position,
retrieve_index,
retrieve_next_token,
retrieve_next_sibling,
draft_tokens,
) = build_tree_kernel_efficient(
draft_input.bonus_tokens,
parent_list,
top_scores_index,
draft_tokens,
batch.seq_lens,
seq_lens_sum,
topk,
num_steps,
num_draft_tokens,
mask_mode,
tree_mask_buf,
fill_prefix_mask=fill_mask,
)
return EagleVerifyInput(
draft_token=draft_tokens,
custom_mask=tree_mask,
positions=position,
retrieve_index=retrieve_index,
retrieve_next_token=retrieve_next_token,
retrieve_next_sibling=retrieve_next_sibling,
retrieve_cum_len=None,
spec_steps=num_steps,
topk=topk,
draft_token_num=num_draft_tokens,
capture_hidden_mode=None,
seq_lens_sum=None,
seq_lens_cpu=None,
draft_probs=draft_probs,
)
def _finalize_accept_tree_path(
batch: ScheduleBatch,
accept_index: torch.Tensor,
accept_lens: torch.Tensor,
predict: torch.Tensor,
logits_output: Any,
bs: int,
*,
token_to_kv_pool_allocator: Any,
num_draft_tokens: int,
) -> torch.Tensor:
"""Tree drafting (topk > 1): move the accepted path -- KV slots, predict,
hidden_states -- to the contiguous front of each per-req block, which the
downstream chain-layout code (draft-extend select_index, committed-KV reads)
assumes. Returns compacted predict; mutates logits_output.hidden_states
(moved only when present)."""
move_accept_tokens_to_target_kvcache(
batch, accept_index, accept_lens - 1, token_to_kv_pool_allocator
)
predict = _compact_accept_to_front(
predict, accept_index, bs, num_draft_tokens=num_draft_tokens
)
if logits_output.hidden_states is not None:
logits_output.hidden_states = _compact_accept_to_front(
logits_output.hidden_states,
accept_index,
bs,
num_draft_tokens=num_draft_tokens,
)
return predict
def _compact_accept_to_front(
x: torch.Tensor,
accept_index: torch.Tensor,
bs: int,
*,
num_draft_tokens: int,
) -> torch.Tensor:
"""Gather the accepted tree path to the front of each per-req block.
``x`` is node-indexed over the whole tree (``[bs * num_draft_tokens, ...]``),
``accept_index`` is ``[bs, spec_steps + 1]`` global node indices (-1 padded).
Padded entries clamp to node 0 but land past accept_lens (never read);
trailing unaccepted slots stay and are freed as overshoot.
"""
nd = num_draft_tokens
s1 = accept_index.shape[1] # spec_steps + 1
safe = accept_index.to(torch.int64).clamp(min=0).reshape(-1)
gathered = x[safe]
out = x.clone()
out.view(bs, nd, *x.shape[1:])[:, :s1] = gathered.view(bs, s1, *x.shape[1:])
return out
def run_eagle_verify(
batch: ScheduleBatch,
*,
target_worker: TpModelWorker,
req_to_token_pool: ReqToTokenPool,
token_to_kv_pool_allocator: Any,
plan_stream: Any,
plan_stream_ctx: Any,
topk: int,
num_draft_tokens: int,
device: str,
metadata_ready_pre_pad: bool,
finalize_tree_path: bool,
grammar_barrier=None,
pp_proxy_tensors=None,
) -> GenerationBatchResult:
"""Shared verify step: target-verify forward, sampling, acceptance bookkeeping.
The single-layer eagle verify body is the source of truth (superset). Two
switches encode the multi-layer worker's preserved-verbatim differences:
- ``metadata_ready_pre_pad``: multi-layer marks forward metadata ready
pre-pad unconditionally; single-layer relies on eagle_prepare_for_verify
marking it only when the cuda-graph path ran.
- ``finalize_tree_path``: single-layer compacts the accepted tree path to
the front of each per-req block for topk > 1; multi-layer has never run
this compaction.
"""
fwd_stream = torch.get_device_module(device).current_stream()
verify_input: EagleVerifyInput = batch.spec_info
record_stream_for_v2_verify(batch, verify_input, fwd_stream)
bs = len(batch.seq_lens)
# [EXPERIMENT] PP+spec: the decode-graph runner's PP payload extraction
# sizes the pipeline proxy output by bs (decode convention, 1 token/req),
# truncating the TARGET_VERIFY tree rows (bs * num_draft_tokens) to a
# single row per request — tree children arrive as zeros on the next
# stage and every draft proposal is rejected. Force the eager verify
# path on PP stages until the graph runner's PP payload sizing handles
# the verify token layout.
_verify_graph_runner_bak = None
_verify_prefill_runner_bak = None
_gate_stage = None
if getattr(get_pp_group(), "world_size", 1) > 1:
if _PP_FORCE_EAGER_VERIFY == "1":
_gate_stage = "all"
elif _PP_FORCE_EAGER_VERIFY == "2":
_gate_stage = "first" if get_pp_group().is_first_rank else None
elif _PP_FORCE_EAGER_VERIFY == "3":
_gate_stage = "last" if get_pp_group().is_last_rank else None
if _gate_stage is not None:
_mr = target_worker.model_runner
if _mr.decode_cuda_graph_runner is not None:
_verify_graph_runner_bak = _mr.decode_cuda_graph_runner
_mr.decode_cuda_graph_runner = None
# TARGET_VERIFY is extend-family: with only the decode runner
# disabled, the verify batch reroutes to the PREFILL piecewise
# graph — whose captured PP input/output buffers are decode-shaped
# (bs rows), the same one-row-per-request relay truncation. Keep
# BOTH runners disabled for the whole verify window (prepare AND
# forward). Restoring the decode runner right after the prepare
# left the forward itself re-electing the graph via
# can_run_graph(forward_batch), silently defeating the gate: the
# graph's bs-sized PP payload then relayed only the root row, and
# the next stage read rows 1..nd-1 as stale buffer content —
# garbage bonus tokens on every accept>1 round.
if _mr.prefill_cuda_graph_runner is not None:
_verify_prefill_runner_bak = _mr.prefill_cuda_graph_runner
_mr.prefill_cuda_graph_runner = None
# Batch 1: Target verify
# Prepare for target verify in a separate stream
with plan_stream_ctx:
verify_forward_batch, can_run_cuda_graph = eagle_prepare_for_verify(
verify_input,
req_to_token_pool,
batch,
target_worker,
)
# Cover post-prepare rebinds: draft_token, plan_stream-allocated out_cache_loc.
record_stream_each((batch.input_ids, batch.out_cache_loc), fwd_stream)
# Correct some buffers due to the overlap plan
if plan_stream:
torch.get_device_module(device).current_stream().wait_stream(plan_stream)
if (
_is_npu
and target_worker.model_runner.model_config.model_is_mrope
and batch.spec_info is not None
and getattr(batch.spec_info, "positions", None) is not None
and not batch.forward_mode.is_idle()
):
# mrope_position depends on draft output in default stream and is computed in plan stream,
# causing errors. Compute it here for correct values.
verify_forward_batch.compute_spec_mrope_positions(
target_worker.model_runner, batch
)
# Some values such as custom_mask and position depend on the output of draft,
# so the previous plan step used the wrong values. Here, we need to run the related
# computation again to update them to the correct values.
target_worker.model_runner.attn_backend.update_verify_buffers_to_fill_after_draft(
verify_input,
(
target_worker.model_runner.decode_cuda_graph_runner.bs
if can_run_cuda_graph
else None
),
)
# Must stay ahead of the target verify launch below.
grammar_tree = (
GrammarTree.from_device(
verify_input.retrieve_next_token,
verify_input.retrieve_next_sibling,
verify_input.draft_token.view(verify_input.retrieve_next_token.shape),
)
if batch.has_grammar
else None
)
if metadata_ready_pre_pad:
# Multi-layer eagle preserved-verbatim behavior: metadata init is
# skipped here unconditionally, although eagle_prepare_for_verify
# only plans when cuda-graph load_batch ran. Single-layer eagle
# re-inits the non-graph path instead (post-pad); multi-layer has
# not adopted that fix. On NPU with --disable-cuda-graph, non-graph
# verify needs metadata init in forward_extend (post-pad); only
# mark ready for the cuda-graph path.
if not _is_npu or can_run_cuda_graph:
verify_forward_batch.mark_forward_metadata_ready()
if _PP_SPEC_DEBUG:
logger.info(
f"[PPSPEC] verify-recv pp_rank={get_pp_group().rank_in_group} "
f"bs={len(batch.seq_lens)} "
f"tree_head={verify_input.draft_token[: min(8, verify_input.draft_token.numel())].tolist()} "
f"pos_head={verify_input.positions[: min(8, verify_input.positions.numel())].tolist()} "
f"proxies={_pp_spec_proxy_desc(pp_proxy_tensors)}"
)
# Run target verify batch in the main compute stream (GPU compute).
# Metadata init is skipped iff cuda-graph already ran load_batch —
# eagle_prepare_for_verify marked the batch in exactly that case; the
# non-cuda-graph path stays unmarked and gets forward_extend's init
# (post-pad).
try:
forward_batch_output = target_worker.forward_batch_generation(
batch=None,
forward_batch=verify_forward_batch,
is_verify=True,
pp_proxy_tensors=pp_proxy_tensors,
)
finally:
# [EXPERIMENT] PP+spec: restore the runners disabled for the verify
# window — draft and prefill rounds still use them.
if _verify_graph_runner_bak is not None:
target_worker.model_runner.decode_cuda_graph_runner = (
_verify_graph_runner_bak
)
if _verify_prefill_runner_bak is not None:
target_worker.model_runner.prefill_cuda_graph_runner = (
_verify_prefill_runner_bak
)
if not get_pp_group().is_last_rank:
if _PP_SPEC_DEBUG:
logger.info(
f"[PPSPEC] verify-send pp_rank={get_pp_group().rank_in_group} "
f"in_ids={tuple(verify_forward_batch.input_ids.shape)} "
f"in_pos={verify_forward_batch.positions[: min(8, verify_forward_batch.positions.numel())].tolist()} "
f"proxies={_pp_spec_proxy_desc(forward_batch_output.pp_hidden_states_proxy_tensors)}"
)
# [EXPERIMENT] PP+spec replica: a non-last stage owns only its target
# segment of the verify forward. Sampling/acceptance live on the last
# stage; this rank's scheduler advances its draft replica from the
# ring-relayed result instead. Return the segment proxies only.
return GenerationBatchResult(
pp_hidden_states_proxy_tensors=(
forward_batch_output.pp_hidden_states_proxy_tensors
),
can_run_cuda_graph=getattr(
forward_batch_output, "can_run_cuda_graph", False
),
extra_keep_alive_refs=[verify_forward_batch],
)
logits_output = forward_batch_output.logits_output
# Generate vocab mask for constrained decoding
grammar_mask = None
if batch.has_grammar:
grammar_mask = build_grammar_vocab_mask(
reqs=batch.reqs,
tree=grammar_tree,
sampling_info=batch.sampling_info,
device=verify_input.retrieve_next_token.device,
barrier=grammar_barrier,
)
# Sample
maybe_detect_nan(logits_output.next_token_logits, "verify: target model logits")
maybe_detect_inf(logits_output.next_token_logits, "verify: target model logits")
(
predict,
accept_lens,
accept_index,
) = eagle_sample(verify_input, batch, logits_output, grammar_mask)
if _PP_SPEC_DEBUG:
logger.info(
f"[PPSPEC] verify-sample pp_rank={get_pp_group().rank_in_group} "
f"graph={can_run_cuda_graph} "
f"in_ids={tuple(verify_forward_batch.input_ids.shape)} "
f"logits={tuple(logits_output.next_token_logits.shape)} "
f"vh={None if logits_output.hidden_states is None else tuple(logits_output.hidden_states.shape)} "
f"tree={verify_input.draft_token[: min(8, verify_input.draft_token.numel())].tolist()} "
f"predict={predict[: min(8, predict.numel())].tolist()} "
f"accept={accept_lens[: min(8, accept_lens.numel())].tolist()} "
f"vh_max={None if logits_output.hidden_states is None else f'{logits_output.hidden_states.abs().max().item():.3e}'}"
)
new_seq_lens = batch.seq_lens + accept_lens
clear_unaccepted_c128 = getattr(
token_to_kv_pool_allocator.get_kvcache(),
"clear_unaccepted_c128_draft_states",
None,
)
if clear_unaccepted_c128 is not None and not batch.forward_mode.is_idle():
clear_unaccepted_c128(
batch.req_pool_indices,
batch.seq_lens,
accept_lens,
num_draft_tokens,
)
# Update mamba state for hybrid GDN models after verification
commit_mamba_states_after_verify(
target_worker,
batch,
accept_lens,
accept_index,
num_draft_tokens,
)
if not batch.forward_mode.is_idle():
accept_tokens = predict[accept_index]
bonus_tokens = torch.empty_like(accept_lens, dtype=torch.int32)
# stride = accept_tokens per-req width = accept_index.shape[1]
# (spec_steps + 1); NOT num_draft_tokens, wrong for topk > 1 trees.
fill_bonus_tokens_func(
accept_tokens,
accept_lens,
bonus_tokens,
accept_index.shape[1],
bs,
)
else:
bonus_tokens = torch.empty((0,), device=device, dtype=torch.int32)
if batch.return_logprob and not batch.forward_mode.is_idle():
compute_spec_logprobs(batch, logits_output, predict, accept_index=accept_index)
if finalize_tree_path and not batch.forward_mode.is_idle() and topk > 1:
# topk == 1 needs nothing here: the accepted path is already the front
# chain, so the whole compaction is an identity transform.
predict = _finalize_accept_tree_path(
batch,
accept_index,
accept_lens,
predict,
logits_output,
bs,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
num_draft_tokens=num_draft_tokens,
)
next_draft_input = EagleDraftInput(bonus_tokens=bonus_tokens)
# verify_forward_batch transitively holds verify-time GPU tensors
# (draft_token / out_cache_loc / ...) that must outlive the imminent
# batch.input_ids rebind in prepare_for_draft_extend.
# Scheduler pins it in batch_record_buf for the 2-iter window.
return GenerationBatchResult(
logits_output=logits_output,
next_token_ids=predict,
can_run_cuda_graph=can_run_cuda_graph,
speculative_num_draft_tokens=num_draft_tokens,
next_draft_input=next_draft_input,
accept_lens=accept_lens,
new_seq_lens=new_seq_lens,
routed_experts_output=forward_batch_output.routed_experts_output,
indexer_topk_output=forward_batch_output.indexer_topk_output,
extra_keep_alive_refs=[verify_forward_batch],
)

View File

@ -0,0 +1,42 @@
# r37 verify-graph 修复 — 全部压测数字2026-09-08174.1.60.8
Killer profile`bench_serving --dataset-name random-ids 16×16384→512 cc16 --flush-cache --temperature 0`seed 6402/6403
即 09-06/07 档案中的竞态触发负载。语料池已耗尽21,235,008/21,296,780无 i8k 重跑余量),
killer random-ids 是唯一免费对比口径。
## 主判决表killer cc16两种子
| 配置 | e2e s6402/s6403 | 解码吞吐 (tok/s) | Mean TPOT (ms) | accept | Mean TTFT (s) |
|---|---|---|---|---|---|
| eager PP+MTPr36 基线) | 105.32 / 106.00 | ~77.5 / ~77.1 | 72.46 / 73.10 | 3.76 / 3.69 | — |
| **r37 胜者verify 图 + draft eagerSPECDEBUG=0 终验)** | **63.83 / 64.69** | **128.34 / 126.64** | **41.51 / 39.29** | 3.72 / 3.61 | 32.99 / 32.33 |
| 同上SPECDEBUG=2 首测) | 65.68 / 59.90 | 124.73 / 136.75 | 33.98 / 39.55 | 3.59 / 3.70 | 35.15 / 32.49 |
| 全图(+draft 图,无增益) | 69.73 / 58.83 | 117.47 / 139.24 | 42.39 / 38.25 | 3.50 / 3.75 | 33.84 / 32.42 |
| A16在役 TP8+EAGLE 4/1/5@0.90,同 killer | 105.62 / 95.99 | 77.56 / 85.34 | 69.13 / 64.98 | 4.31 / 4.58 | 60.76 / 58.33 |
- vs eager 基线e2e **-39~-40%**,解码吞吐 **+61~77%**TPOT **-46~-53%**accept 持平。
- vs A16 在役配置e2e **-40%**,解码吞吐 **+52~65%**TPOT **-40%****TTFT 近乎减半**33s vs 58-61sTP4PP2 prefill 优势)。
- 净胜拐点复核step = TPOT × accept ≈ 122-151ms…… 取终验两种子 41.5×3.72=154ms / 39.3×3.61=142ms
均值 ~148ms ≈ 拐点nomtp 52ms × accept配合 e2e/TPOT 全面领先,判定 MTP 机器开销已被图摊销、净收益成立。
## 稳定性
- conc_test ×38x64 / 8x2048 / 8x16384 递进ALL-OK ×3容器零重启见 results/conc_r37fix.txt
- killer ×2verify-graph 配置boot 1+ killer ×2全图 boot+ killer ×2终验 boot容器全程存活。
- 质量门 7/7GSM8K×5 + 中文推理 + tool callPASS=7 FAIL=0 ×2 次SPECDEBUG=2 boot 与 SPECDEBUG=0 终验 boot
## 定位实验记录bisect + 遥测)
- 模式 1/2/3 二分SGLANG_PP_FORCE_EAGER_VERIFY=1/2/3模式 2PP0 eager+PP1 图)乱码;模式 3PP0 图+PP1 eager完全正确
→ 缺陷唯一在末 stage verify 图。
- VRFY-IN/VRFY-G/VRFY-BUF/VRFY-SLOT 遥测SPECDEBUG=2decode_cuda_graph_runner_fix.py 内置,仅 24 轮):
- 入口 relayed hidden 正常((4,6144)absmax 0.3-2.1),返回 hs 新鲜但 logits argmax 全为高频英文垃圾(' of'/' the'/':',解码实证)
- **VRFY-BUF 全零 + needs_init=False + 槽位健康enabled/copy_fb/static_match 全 True**
→ 根因spec worker plan-stream 预跑 load_batch 时中继未到pp=Nonepp 槽位被跳过;前向流走 pre-planned 早退路径,
该路径只补拷 input_ids/positions → pp 静态缓冲永远全零 → 图内 clone 读零。
- xcheckSPECDEBUG=3同 batch eager 前向参考 logits 与图 logits 呈一轮滞后关系(零输入 + 上轮 KV与根因自洽。
- 修复后VRFY-BUF ≡ VRFY-IN 逐行相等,输出正确。
## 生产部署(终态)
- 容器 glm53-nvfp4 @ 60.8:30000`--restart unless-stopped`health 200。
- 启动命令:`bash /root/deploy_ppmtp_r37.sh '--tp 4 --pp-size 2 --disable-overlap-schedule --max-prefill-tokens 16384' mtp 8192 0.88 1 1 0 0`
degloo=1, drafteager=1, verifyeager=0, specdebug=0
- 与 A16 对比后胜出留役A16 恢复脚本 deploy_glm53_605.sh 留盘可随时切回。

View File

@ -0,0 +1,15 @@
=== conc_test round 1 ===
req6: ok=True 23.8s text="\n\t\t{{\n\t\t}}</arg_value></think>I notice you've pasted what ap"
req7: ok=True 23.0s text='\n\t\t#-name\n\t\t}\n\t}\n}\n</arg_value>`\n\nThe provided text appears '
stage 8x16384: OK total=24.1s
FINAL: ALL-OK
=== conc_test round 2 ===
req6: ok=True 5.7s text='\n</think># Java 8 Stream API: Complete Guide\n\n## Introductio'
req7: ok=True 6.5s text='languagelanguage=s\n\t\t);\n\t}\n}\n</arg_value>`\n\n</think># Analy'
stage 8x16384: OK total=6.6s
FINAL: ALL-OK
=== conc_test round 3 ===
req6: ok=True 5.7s text='\n\t\t{{\n\t\t}\n\t}\n}\n</arg_value>`\n\nThis is a corrupted/garbled te'
req7: ok=True 5.5s text='\n</think>I need to stop here and clarify something important'
stage 8x16384: OK total=5.7s
FINAL: ALL-OK

View File

@ -0,0 +1,12 @@
killer: waiting for health (up to 3600s)...
healthy at 01:06:33
bench pid=2608113 started 01:06:33
BENCH-FINISHED rc=1 01:09:09 —— 竞态未触发(负载全程存活)
---------------Inter-Token Latency----------------
Mean ITL (ms): 69.25
Median ITL (ms): 48.34
P90 ITL (ms): 55.08
P95 ITL (ms): 119.36
P99 ITL (ms): 248.22
Max ITL (ms): 42175.49
==================================================

View File

@ -0,0 +1,12 @@
killer: waiting for health (up to 3600s)...
healthy at 01:09:10
bench pid=2609200 started 01:09:10
BENCH-FINISHED rc=1 01:11:21 —— 竞态未触发(负载全程存活)
---------------Inter-Token Latency----------------
Mean ITL (ms): 65.08
Median ITL (ms): 48.77
P90 ITL (ms): 51.07
P95 ITL (ms): 61.05
P99 ITL (ms): 246.47
Max ITL (ms): 51519.76
==================================================

View File

@ -0,0 +1,3 @@
a4be812885f664bc5e7b7c24142f967c *patches/decode_cuda_graph_runner_fix.py
babb646172942b680cb73d8eb45e84e3 *patches/eagle_worker_common_bisect.py
b1a50238d9bc424e2052fbdbaae93ada *scripts/deploy_ppmtp_r37.sh

View File

@ -0,0 +1,12 @@
===== GSM8K x5 (temperature=0) =====
[PASS] GSM8K-1 (含 72)
[PASS] GSM8K-2 (含 3)
[PASS] GSM8K-3 (含 60)
[PASS] GSM8K-4 (含 63)
[PASS] GSM8K-5 (含 10)
===== 中文推理 (鸡兔同笼) =====
[PASS] 中文推理-鸡数(兔应为12) (含 23)
(兔数检查: 上面输出应同时含 12)
===== Tool Call =====
[PASS] tool call: get_weather {"city": "北京"}
===== 结果: PASS=7 FAIL=0 =====

View File

@ -0,0 +1,81 @@
#!/bin/bash
# PP+MTP r37 部署60.8, nightly-dev-cu13-20260901-07c8f729
# r36g 图模式正确性灾难的修复版decode_cuda_graph_runner.execute() 对非末
# stage 的 PPProxyTensors 按 self.bs请求数decode 口径)切片,把
# TARGET_VERIFY 的树节点行bs*num_draft_tokens截成每请求 1 行 → 子节点
# 全零中继 → accept 崩塌 + accept>1 轮出乱码 bonus token。修复 = 改按
# self.raw_num_tokenraw_bs*captured_req_width与 eager 中继契约一致)。
# 本版仅开 verify 图FORCE_EAGER_VERIFY=0draft 仍 eager 隔离变量。
# 用法: bash deploy_ppmtp_r37.sh "<并行参数>" [mtp|nomtp] [chunk] [memfrac] [degloo]
# 例: bash deploy_ppmtp_r37.sh "--tp 4 --pp-size 2 --disable-overlap-schedule --max-prefill-tokens 16384" mtp 8192 0.88 1
PAR=${1:?usage: deploy_ppmtp_r37.sh "<flags>" [mtp|nomtp] [chunk] [memfrac] [degloo] [drafteager] [verifyeager] [specdebug]}
MTPMODE=${2:-nomtp}
CHUNK=${3:-8192}
MEMFRAC=${4:-0.88}
DEGLOO=${5:-1}
DRAFTEAGER=${6:-1}
VERIFYEAGER=${7:-0}
SPECDEBUG=${8:-0}
MTPARGS=""
if [ "$MTPMODE" = "mtp" ]; then
MTPARGS="--speculative-algorithm EAGLE --speculative-num-steps 3 --speculative-eagle-topk 1 --speculative-num-draft-tokens 4"
fi
docker update --restart=no glm53-nvfp4 >/dev/null 2>&1
for i in 1 2 3 4 5; do
docker rm -f glm53-nvfp4 >/dev/null 2>&1
sleep 2
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^glm53-nvfp4$' || break
done
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^glm53-nvfp4$'; then
echo "ERROR: old container cannot be removed"; exit 1
fi
for i in $(seq 1 15); do ss -ltn 2>/dev/null | grep -q ":30000 " || break; sleep 2; done
docker run -d --name glm53-nvfp4 --gpus all --shm-size 64g --ipc=host --cap-add SYS_PTRACE \
-v /root/sglang_patch2/layer_setup.py:/sgl-workspace/sglang/python/sglang/srt/model_executor/model_runner_components/layer_setup.py:ro \
-v /root/sglang_patch2/validation_hook.py:/sgl-workspace/sglang/python/sglang/srt/arg_groups/validation_hook.py:ro \
-v /root/eagle_worker_v2_mask.py:/sgl-workspace/sglang/python/sglang/srt/speculative/eagle_worker_v2.py:ro \
-v /root/sglang_patch2/eagle_worker_common.py:/sgl-workspace/sglang/python/sglang/srt/speculative/eagle_worker_common.py:ro \
-v /root/sglang_patch2/deepseek_nextn.py:/sgl-workspace/sglang/python/sglang/srt/models/deepseek_nextn.py:ro \
-v /root/scheduler_pp_mixin_r35.py:/sgl-workspace/sglang/python/sglang/srt/managers/scheduler_pp_mixin.py:ro \
-v /root/request_receiver_degloo.py:/sgl-workspace/sglang/python/sglang/srt/managers/scheduler_components/request_receiver.py:ro \
-v /root/sglang_patch2/deepseek_v2.py:/sgl-workspace/sglang/python/sglang/srt/models/deepseek_v2.py:ro \
-v /root/decode_cuda_graph_runner_fix.py:/sgl-workspace/sglang/python/sglang/srt/model_executor/runner/decode_cuda_graph_runner.py:ro \
-e SGLANG_PP_SPEC_DEBUG=${SPECDEBUG} \
-e SGLANG_PP_DEGLOO=${DEGLOO} \
-e SGLANG_PP_SPEC_FORCE_EAGER_DRAFT=${DRAFTEAGER} \
-e SGLANG_PP_FORCE_EAGER_VERIFY=${VERIFYEAGER} \
--restart no -p 30000:30000 \
-v /data/hf_models:/data/hf_models \
lmsysorg/sglang:nightly-dev-cu13-20260901-07c8f729 \
python3 -m sglang.launch_server \
--model-path /data/hf_models/GLM-5.3-NVFP4 \
--tp 8 \
--mem-fraction-static ${MEMFRAC} \
--max-running-requests 16 \
--chunked-prefill-size ${CHUNK} \
--disable-shared-experts-fusion \
--moe-runner-backend flashinfer_cutlass \
--disable-flashinfer-autotune \
--reasoning-parser glm45 --tool-call-parser glm47 \
--enable-hierarchical-cache --hicache-ratio 3 \
${MTPARGS} \
${PAR} \
--host 0.0.0.0 --port 30000
echo "deployed r37 GRAPH-FIX (DEGLOO=${DEGLOO} DRAFTEAGER=${DRAFTEAGER}): par=[${PAR}] mtp=${MTPMODE}; waiting for health..."
for i in $(seq 10 10 1800); do
code=$(curl -s -o /dev/null -m3 -w '%{http_code}' http://127.0.0.1:30000/health 2>/dev/null)
if [ "$code" = "200" ]; then
echo "healthy after ${i}s"
docker logs glm53-nvfp4 2>&1 | grep -oE "max_total_num_tokens = [0-9]+" | head -1
exit 0
fi
if ! docker ps --format '{{.Names}}' | grep -q '^glm53-nvfp4$'; then
echo "CONTAINER DIED after ${i}s"; docker logs --tail 60 glm53-nvfp4 2>&1 | grep -iE "error|assert|not support|incompatible" | tail -8; exit 1
fi
sleep 10
done
echo "TIMEOUT waiting for health"; exit 1