在8×A800上把 DeepSeek-V4-Flash 榨到极限:从5倍提速到TP+EP双实例的完整实测

在 8× A800 上把 DeepSeek-V4-Flash 榨到极限:从 5 倍提速到 TP+EP 双实例的完整实测

单机 8 卡,双实例,一路从 50 tok/s 到 479 tok/s------我把每一步的决策、测量和踩坑都写在这里。

前言

  • 8 张 A800 80GB(PCIe 版,仅 GPU 两两成对经 NVLink Bridge 相连),目标是把开源的 DeepSeek-V4-Flash-0731(稀疏 MoE 模型)在本地跑起来,既要应付偶尔的超长上下文,又要扛住 60K 量级的高并发批量对话。

这篇文章不是配置清单,而是一次完整的实测记录:从最差的方案一路演进到最终形态,每一步都带真实数字。你可以直接照着部署,也能从里面的瓶颈分析学到 MoE 服务化该往哪个方向使劲。

先说结论:

  • 8 卡双实例:GPU0-3 跑 1M 上下文专机,GPU4-7 跑 512K + TP+EP + 30 并发批量。
  • TP+EP 在 24+ 并发时比纯 TP 快 15~19%,而且白拿更大的 KV 缓存;16 并发以下两者持平,没有任何退步。
  • GPU 平均利用率只有 ~49%(峰值 98%)------瓶颈在 MoE 的跨卡集合通信走 PCIe(NVLink 仅成对,跨对仍是 PCIe),软件层面已经压到硬件天花板。
  • 衡量口径提示:正文所称"5 倍提速"来自早期纯 decode 单流 50 → 235 tok/s 的取整 (~4.7 倍),与后文 TP+EP 阶段的端到端口径不同,详见 §九 演进表。

一 为什么混合推理在这台机器上是浪费

最初的方案来自模型仓库的官方 README:让 CPU 和 GPU 协同,GPU 只负责"常住显存的层",MoE 专家计算放 CPU。这套思路本来是为"显存装不下模型"设计的(比如 2 张 3090 塞 155GB 模型)。但我有 4 张 80GB = 320GB,装得下 155GB 的模型。于是出现了一个非常反直觉的观测:

  • 混合模式(NUMA=1) :GPU 利用率 0%,CPU 冲到 ~3000%(30 个线程),解码只有 ~100 tok/s。为什么?MoE 的解码计算全在 CPU 上跑,GPU 只是"放权重的架子",4 张卡全程闲置。
  • 纯 GPU 模式(NUMA=0):GPU 利用率 61-95%,解码 111-235 tok/s。

结论很干脆:显存够的情况下,混合推理是纯浪费。 我用纯 GPU 模式,把 4 张卡的算力真正用起来。


二 dspark 是单实例的"杀手锏"

DeepSeek-V4-Flash-0731 模型自带的投机解码(dspark,基于 MTP 草稿模型)是第二个决定性优化。它每次不是只走一步,而是用草稿模型批量猜一串候选 token,再让主模型一次验收。

实测对比(纯 GPU 4 卡):

指标 无 dspark 有 dspark(=3)
单流解码 111-128 tok/s 221-235 tok/s
20 并发聚合 ~207 tok/s 829 tok/s
20 并发延迟 P50 48s 11.8s

单流提升约 2 倍,20 并发聚合直接翻 4 倍。dspark 的接受率约 34-54%,平均接受 2.7-3.7 个 token------参数量从线上值 3 开始调,我没用到仓库默认的 5,因为 5 的 4、5 位接受率极低(0.40、0.34),大部分被浪费,3 更平滑。这一步让我从 50 到 235 tok/s,是量级最大的跳跃 ------ 这一跳正是标题"5 倍提速"(≈4.7 倍取整)的出处


三 8 卡双实例:Tensor Parallelism + Expert Parallelism

单实例 4 卡已经摸到单机 4×80GB 的天花板了,下一步是扩容。我选了双实例而不是 8 卡 TP=8:

  • 本机 GPU0-3 在 NUMA node0,GPU4-7 在 NUMA node1,跨节点全是慢速 SYS 链路。TP=8 的每层 all-reduce 都要走这条慢链路,预填充很可能比 4 卡还慢。
  • 所以拆成 两个 TP=4 实例(GPU0-3 + GPU4-7),实例内互联优于跨 NUMA(成对 NVLink + 组内 PIX,无 SYS),总吞吐和 KV 池各乘 2。

3.1 第二个实例的"最佳实践"配置(GPU4-7,端口 8002)

本文所称脚本名为对外通称,完整脱敏脚本见**§十一附录**。

bash 复制代码
bash scripts/deploy_ds4_tp_ep.sh
# 默认 = 512K 上下文 / TP+EP / 30 并发 / 25600 预填充
TP_SIZE / EP_SIZE 4 / 4(--enable-expert-parallel)
MAX_MODEL_LEN 524288(512K)
MAX_NUM_SEQS 30
MAX_BATCHED_TOKENS 25600
KV cache / dtype fp8_ds_mla / bfloat16
dspark / watermark / 显存 3 / 0.1 / 0.9
功能验证 functional_test 4/4 通过,512K 上限正常受理

值得注意的是 512K 上下文是"白赚"的 ------KV 池大小由显存决定,不随 MAX_MODEL_LEN 上限改变。把上限从 256K 抬到 512K,64K 并发的吞吐完全不受影响,却让单请求能带上 512K 的长文档。这是我权衡后认为"无负优化 + 有长上下文能力"的最佳折中。


四 重头戏:TP+EP vs 纯 TP,到底差多少?

这是本文最有价值的部分------一台仅成对 NVLink(NVLink Bridge 只在 4-5、6-7 之间)的 A800 上,专家并行究竟值不值?我做了严格的对比分析:同一台 GPU4-7、同一负载脚本、同一 512-token 生成任务,只切换 ENABLE_EP(1=TP+EP, 0=纯 TP),其余所有参数完全一致。

4.1 分档并发实测(tok/s)

并发 TP+EP 纯 TP 差异 判定
1 101.5 97.6 +4% 噪声内
4 205.2 213.5 −4% 噪声内
8 341.8* 196.4 +74%* *纯噪声(EP 重测仅 180.8)
16 315.3 / 326.9 327.1 ≈0 持平(两次)
24 392.4 / 386.2 330.6 +15~19% EP 胜(可复现)
32 380.1 443.0 −14% ⚠️ 超 30 席位封顶,失真

先说测量噪声 :这模型默认开思考模式,每次实际输出的 token 数不可控,所以 tok/s 天生波动大(同一并发测两次能差近 2 倍,比如并发 8 那次)。我只采纳可复现的差异:16 并发持平、24 并发 EP 胜

4.2 为什么 EP 在低并发看不到好处?

我用一句话解释核心机制:

  • 纯 TP :每个被命中的专家,4 张卡都要算自己的分片,然后 all-reduce 合并成完整输出。每层跨卡流量大,而且不随稀疏度下降而减少
  • EP :专家按整卡驻留(每卡 64 个),每层只做一次 all-gather,只交换被激活的那一小撮专家的输出。稀疏度(每 token 只激活 6/256 ≈ 2.3%)直接转化为通信节省。

但 EP 的 all-gather 有个固定开销 (每层一次同步点)。batch 小的时候,这个固定开销把省下来的通信量给抵消了------所以 16 并发以下两者持平;当 batch 大到能把固定开销摊薄,EP 的优势就显现出来了。这台机器的收益拐点出现在 ~24 并发。

4.3 附带收益:更大的 KV 池

配置 KV 池
纯 TP ~81 万 token
TP+EP 928,878 token

EP 让专家权重/激活按卡驻留,省下的显存直接让给 KV cache。这是纯白赚的。

4.4 结论:选 EP,决策风险为零

  • 所有并发档无系统退步
  • 24+ 并发有可复现的 15~19% 增益
  • 白拿更大 KV 池

所以我保留 TP+EP 作为默认。对 MoE + PCIe(NVLink 仅成对)这套组合,EP 是把稀疏度转化为实实在在吞吐的正确手段。


五 其他旋钮我也试过:别在里面找奇迹

为了"榨干"最后一点,我还做了另外两组实验,结论都是别改:

实验 A:把并发上限从 30 抬到 64 + 关掉节能

配置 30 并发 tok/s GPU 均载 16 并发 tok/s
默认(30/25600/节能开) 479 49% 508
调优(64/25600/节能关) 448 45% 237

并发上限 64 并没能增大单轮 decode 的 batch(30 负载下席位本就有余),反而在 16 并发时明显退步;节能开关也不是瓶颈(GPU 本来就只跑 77W/300W,非功耗受限)。

实验 B:max-num-batched-tokens 不要提到 32768

预填充 chunk 与 KV 池是直接权衡:32768 会把 KV 池从 282 万 token 塌缩到 35 万,并发从 10.7× 掉到 1.34×。25600 是我实测后确认能兼顾预填充速度与KV的值。

六 瓶颈到底在哪:GPU 为什么只有 ~49%?

这是最常被追问的问题------"GPU 没跑满是不是哪里没配好?"

观测
GPU 均载(30 并发持续) ~45-49%
GPU 峰值 98%
SM 时钟 1410/1410 MHz(P0 满频,无降频)
功耗 ~77W/300W(无功耗墙)
互联(本机) GPU 两两成对 NVLink(4-5、6-7 等各 NV8), 跨对走 PCIe(PIX, Gen4×16 52GB/s)
CPU 每 worker ~5 核,不饱和

这是典型的"计算爆发-等待交替 "曲线:GPU 以接近满速(98%)算完一轮,然后就卡在等跨卡数据搬运(EP 的 all-gather 走 PCIe)。因为 MoE 每 token 只激活 6/256 个专家,真正的计算量很小,于是通信(搬运)的相对占比被放大了。需要强调的是:NVLink 只成对桥接(4-5、6-7),而 TP=4/EP=4 的集合通信必然跨对走 PCIe,所以瓶颈本质是被跨对的 PCIe 链路卡住,不是"完全没有 NVLink"。

想在这台机器上把 GPU 均载推到 80%+,只有三条路,都不是调参:

  1. 上 NVLink 全互联/NVSwitch------A800 PCIe 版只有成对桥接,没有全连通;
  2. 减 EP 组规模(TP=2+EP=2)------通信量级降,但每卡算更大的分片,收益需另测;
  3. 接受现状------对 64K 批量业务,479 tok/s 已经够用。

我选择的是第 3 条:认清楚这是硬件形态决定的边界,不是配置失误,然后针对业务做出最优解。


七 踩过的坑

给后来人三个最痛的:

坑 1:KV 池被前缀缓存占满 → 并发排队爆炸

运行一段时间后,系统提示词/工具定义/历史前缀会被永久缓存进 KV 池,实测累积到池的 80%。新请求一来没有头寸,就开始驱逐+重算,7 个 token 的小请求 TTFT 能飙到 151 秒。

解法:--watermark 0.1(永久保留 10% KV 空闲)+ 上下文封顶 + 依赖 vLLM 自带的按需驱逐缓存。不用重启清缓存,vLLM 空闲时会自动把缓存堆满,需要时再驱逐,这是最优缓存行为。

坑 2:预填充 chunk 与 KV 池是此消彼长

MAX_BATCHED_TOKENS KV 池(token) 满载并发
8192 ~177 万 ~6.8×
16384 89 万 3.4×
32768 35 万 1.34×

还有老生常谈的 libnuma 损坏(NUMA not available! abort.)、dspark 图捕获 OOM(0.98 显存)等,属于部署期常见问题的通用经验。

坑 3(反直觉判例):自愈脚本的启动宽限不足会把"正常冷启动"误判成宕机 → 无限重建

这是部署 DeepSeek 系列后踩到的最隐蔽一坑,与性能无关、却直接决定"服务看起来稳不稳"。在给 GPU4-7 加自愈 supervisor 时,最初把"重建后冷却/宽限"定成 180s。但 vLLM 完整启动实测要 ~6-7 分钟(模型加载 + 大量 TileLang JIT 编译 + CUDA graph 捕获 ~95s + API 启动)。

于是发生了这样的事:容器每次启动还没跑完 → supervisor 的 3×30s = 90s 连续失败判定 → 判死重建 → 又等 6 分钟 → 又超 90s → 再重建...... 在没有任何真实流量、服务完全正常的情况下,self-heal 已累计重建 940 次。表面看"GPU4-7 很不稳",其实是被自愈脚本自己反复杀掉正在正常启动的进程。对照同为 TP+EP 的 GPU0-3(未配 supervisor)稳定运行 8 天,根因一目了然。

解法:把冷却改为基于容器实际启动时间的宽限 STARTUP_GRACE(默认 600s) ,StartedAt 距今 <600s 一律跳过健康探测。修复后健康服务零误杀(实测重建次数 0)。这条的教训:"加自愈"不等于"更稳",自愈参数若不匹配服务启动时长,反而制造故障。量体裁衣,先量启动时间,再设宽限。

八 稳定性:一次 48 小时后的宕机复盘

同期发布的《长时运行宕机复盘 + 稳定性加固》一文有完整证据链,这里只讲结论。

长跑 48h 后,GPU4-7 实例突然 EngineCore fatal,API 永久挂死,容器进程却还活着(vLLM 不自愈),只能手工重建。表象很像"前缀缓存满了",但证据链指向完全不同的根因。

根因不是前缀缓存 ------崩溃时 KV 池占用仅 39.6%,LRU 淘汰机制正常。真正的元凶是单个超长上下文请求:

  • 一个 512K 级请求独霸池的 98.4% 历史(Running:1);
  • 上下文越长,decode 单步的稀疏 attention 越重 → 单步耗时超线性增长,生成掉到 1.6 tok/s;
  • 单步是全 TP 组同步的,一步卡住所有 worker 一起等;
  • EngineCore 等结果超时(VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS,默认 300)抛 fatal;
  • vLLM 不会自愈,API 永久僵死,残留 worker 占着显存。

三件事把它兜住(已落地):

  1. 加大模型执行硬超时到 600s (EXEC_MODEL_TIMEOUT):给"慢但能恢复"的 512K 单步留足窗口,减少误杀。
  2. 外部自愈 supervisor (scripts/supervisor.sh):探测到僵死(HTTP 失败 ×3 或日志命中 EngineCore fatal)→ docker rm -f + 自动重建,把恢复时间从"手工发现"压到 1-2 分钟。
  3. 生成吞吐冻结探测 (scripts/monitor.sh watch):Running>0 但生成 <1 tok/s 持续 30s → 告警 + 自动联动 supervisor 重建。

三个都可用一键托管:bash scripts/start_trio.sh(幂等 start/status/stop)。

取舍 :要么降 MAX_MODEL_LEN 到 256K(单请求 decode 上界减半,池并发度 1.59→3.1),要么保留 512K 但必须开 supervisor 兜底。真怕卡死就上 KV offload(KV_OFFLOAD_SIZE=256,KV 放 CPU,decode 变慢换池自由)。我的选择是保留 512K + 三件套。

这条比性能调参更值得记住:长上下文是天然的并发/时延风险,服务必须可自愈。


九 演进总结(带数字的完整脉络)

阶段 1-4 的"单流/聚合"是纯 decode(只算生成,不含 prefill/思考)的基准;阶段 5 起是端到端完整生成(含 prefill 与思考)的基准,两者测量基准不同,数字不可直接相减。阶段 5 是单个 4 卡 TP+EP 实例(gpu4-7),阶段 6 是它在双实例中的实际角色。

阶段 配置 实例范围 单流 tok/s 并发聚合 GPU 利用 口径
1 2卡混合(NUMA) 早期 2 卡 50-66 ~2 并发 0% 纯 decode
2 4卡全显存混合(NUMA) 早期单实例 ~100 ~200 (20并发) 0% 纯 decode
3 4卡纯GPU(无 dspark) 早期单实例 111-128 ~207 (20并发) 61-91% 纯 decode
4 4卡纯GPU+dspark 早期单实例 221-235 829 (20并发) 88-95% 纯 decode
5 4卡 TP+EP + 512K + 30并发 单 4 卡实例 (gpu4-7) ~102* 479 (30并发) / 508 (16并发) ~49% 512-token 含思考
6^^ 8卡双实例(0-3 + 4-7) 整机 --- ~2× 单实例 ~49% ---

关于"测量基准"(过去称"口径"):阶段 1-4 的单流 221-235 是纯 decode 速率 = 只算模型逐 token 生成的速度,不含首 token 等待(prefill)、不含思考 token。阶段 5 的 ~102 是端到端完整生成速率 = 从发请求到整条回复结束的总 token 数 ÷ 总耗时,包含了 prefill、思考 token、首 token 延迟。同一硬件,两种测法数字不同,不是硬件变慢,是测的指标定义不同

  • 阶段 5 单流 ~102 是 conc=1 的 512-token 端到端生成(含思考),与阶段 4 单流 221-235 的纯 decode 基准不同,不可直接比较。
  • 阶段 6 为双实例总吞吐的理论上界(≈2× 单实例);双实例各自服务不同 agent,不做跨实例合流聚合。

十 最终部署与结论

bash 复制代码
bash scripts/deploy_ds4_tp_ep.sh   # gpu4-7: 512K / TP+EP / 30 并发 / 25600 (完整脚本见 §11.1)
bash scripts/start_trio.sh         # 稳定性加固三件套: supervisor + monitor (见 §11.2-11.4)
  • 演进:2 卡混合 → 4 卡纯 GPU + dspark → 8 卡双实例 TP+EP。
  • 双实例分工:GPU0-3(8000)= 1M 专机;GPU4-7(8002)= 512K + 30 并发批量 + TP+EP。
  • TP+EP 结论:24+ 并发 +15~19%,16 以下持平,白拿更大 KV 池,无退步 → 保留 EP。
  • 硬件天花板:NVLink 仅成对 + 跨对 PCIe 下 GPU 均载 ~49%,这是形态决定,不是配置失误。
  • 稳定性 :长上下文需自愈兜底,EXEC_MODEL_TIMEOUT 600 + supervisor + monitor 三件套。
  • 对 64K 批量业务:479 tok/s / 30 并发是这台 8 卡机器的合理水平。

如果这篇对你有帮助,欢迎分享;如果你在自己机器上复现后数字有出入,先检查互联方式(NVLink 是全互联还是仅成对)和测量基准------这两点最影响结论。


十一 附录:完整部署与自愈脚本

为便于直接复现,这里给出与正文一致的脱敏完整脚本 。归档为 deploy_ds4_tp_ep.sh(部署)+ supervisor.sh(自愈)+ start_trio.sh(三件套托管)。monitor.sh 为关联监控脚本,篇幅所限本文给出调用方式。

11.1 部署脚本 deploy_ds4_tp_ep.sh

bash 复制代码
#!/usr/bin/env bash
# =============================================================================
# DeepSeek-V4-Flash-0731 部署脚本 (TP+EP 第二实例, 端口 8002)
# -----------------------------------------------------------------------------
# 用途: 8 卡双实例方案 ------ GPU0-3 (8000, 1M 上下文专机) + GPU4-7 (8002, 本脚本)。
#       本脚本为 GPU 4-7 部署"最佳实践"配置: TP+EP 专家并行 + 512K 上下文 +
#       30 并发 + 25600 预填充, 长上下文与 64K 高并发批量都能较好兼顾。
#
# 关键环境变量 (均可覆盖):
#   GPUS           设备选择, 默认 "device=4,5,6,7"
#   TP_SIZE        张量并行度,  默认 4
#   EP_SIZE        专家并行度,  默认 4 (--enable-expert-parallel 组大小)
#   ENABLE_EP      专家并行开关, 默认 1 (=0 回退纯 TP)
#   MAX_NUM_SEQS   并发序列上限, 默认 30
#   LK_THREADS     lk_moe 线程数, 默认 14 (NUMA node1 56 CPU / TP4)
#   ENABLE_DSPARK  投机解码开关, 默认 1 (dspark=3)
#   GPU_MEM_UTIL   显存利用率, 默认 0.9
#   PATCH_VLLM     思考参数 vLLM 补丁, 默认 1 (5 文件只读挂载)
#   EXEC_MODEL_TIMEOUT  EngineCore 模型执行硬超时秒, 默认 600 (宕机复盘加固)
# =============================================================================

set -e

MODEL_HOST_PATH="${MODEL_HOST_PATH:-/path/to/DeepSeek-V4-Flash-0731}"
MODEL_CTR_PATH="/model"
IMAGE="${IMAGE:-garenleeasa/lvllmds4x:v2.3.9-cuda-12.8.1-ubuntu-22.04}"
CONTAINER="${CONTAINER:-lvllm-ds4-tp4-ep4}"
# 服务端口 (宿主机映射 = 容器内端口)
PORT="${PORT:-8002}"
SERVED_MODEL="DeepSeek-V4-Flash-0731"

# ---- 可覆盖参数 ----
GPUS="${GPUS:-device=4,5,6,7}"
TP_SIZE="${TP_SIZE:-4}"
EP_SIZE="${EP_SIZE:-4}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-30}"
LK_THREADS="${LK_THREADS:-14}"
GPU_RESIDENT_MOE_LAYERS="${GPU_RESIDENT_MOE_LAYERS:-0-42}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.9}"
WATERMARK="${WATERMARK:-0.1}"
KV_OFFLOAD_SIZE="${KV_OFFLOAD_SIZE:-}"
MAX_BATCHED_TOKENS="${MAX_BATCHED_TOKENS:-25600}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-524288}"
ENABLE_DSPARK="${ENABLE_DSPARK:-1}"
DSPARK_SPEC_TOKENS="${DSPARK_SPEC_TOKENS:-3}"
ENABLE_NUMA="${ENABLE_NUMA:-0}"
PATCH_VLLM="${PATCH_VLLM:-1}"
ENABLE_EP="${ENABLE_EP:-1}"
LK_POWER_SAVING="${LK_POWER_SAVING:-1}"
# ---- 稳定性加固 (宕机复盘新增) ----
# EXEC_MODEL_TIMEOUT: EngineCore 等 worker 单步模型执行结果的硬超时 (秒)。
#   ⚠️ 镜像默认 300。512K 超长上下文 decode 单步可超 300s → EngineCore fatal 且
#   vLLM 不自愈 (容器 running 但 API 永久挂死)。调大 (如 600) 可避免"慢但可恢复"被误杀,
#   但真卡死仍需外部 supervisor 兜底。设为 0 表示不设超时。
EXEC_MODEL_TIMEOUT="${EXEC_MODEL_TIMEOUT:-600}"

# ---- 容器内 CUDA_VISIBLE_DEVICES ----
# ⚠️ --gpus "device=4,5,6,7" 将宿主机 GPU 4-7 传入容器后被重新编号为 0..TP-1
# (容器内 nvidia-smi 显示 0-3)。因此容器内 CUDA_VISIBLE_DEVICES 必须是 0,1,..,TP-1,
# 不能直接用宿主机物理索引 (4,5,6,7), 否则 vLLM 的 physical_device_id 映射到不存在的
# NVML 索引 4 → NVMLError_InvalidArgument。
CONTAINER_CUDA_VISIBLE_DEVICES="$(seq -s, 0 $((TP_SIZE-1)))"

if [ "${ENABLE_DSPARK}" = "1" ]; then
  SPEC_ARGS=(--speculative-config "{\"method\":\"dspark\",\"num_speculative_tokens\":${DSPARK_SPEC_TOKENS},\"draft_sample_method\":\"probabilistic\"}")
else
  SPEC_ARGS=()
  echo "[deploy] NOTE: dspark DISABLED (高并发稳定); ENABLE_DSPARK=1 可启用以提升单流速度"
fi

if [ -n "${KV_OFFLOAD_SIZE}" ]; then
  OFFLOAD_ARGS=(--kv-offloading-size "${KV_OFFLOAD_SIZE}")
  echo "[deploy] KV offload ON: ${KV_OFFLOAD_SIZE} GiB 到 CPU 内存 (长上下文解码会变慢)"
else
  OFFLOAD_ARGS=()
  echo "[deploy] KV offload OFF (纯 GPU KV)"
fi

if [ "${ENABLE_NUMA}" = "1" ]; then
  NUMA_ENV=( -e LVLLM_MOE_NUMA_ENABLED=1 -e LVLLM_GPU_RESIDENT_MOE_LAYERS="${GPU_RESIDENT_MOE_LAYERS}" -e LVLLM_GPU_PREFETCH_WINDOW=1 -e LVLLM_GPU_PREFILL_MIN_BATCH_SIZE=128 )
  echo "[deploy] NUMA 混合推理 ON (CPU+GPU); GPU_RESIDENT_MOE_LAYERS=${GPU_RESIDENT_MOE_LAYERS}"
else
  NUMA_ENV=( -e LVLLM_MOE_NUMA_ENABLED=0 )
  echo "[deploy] 纯 GPU 模式 (NUMA OFF): 解码走 GPU, 不浪费 GPU 算力"
fi

# 专家并行 (MoE): --enable-expert-parallel, EP 组大小 = TP 组大小
# 稀疏 MoE (6/256) 下跨卡通信只交换被激活专家输出, 降通信 + 省驻留显存。
if [ "${ENABLE_EP}" = "1" ]; then
  EP_ARGS=(--enable-expert-parallel)
  echo "[deploy] expert-parallel ON (MoE 专家切分到 ${EP_SIZE} 卡; TP=${TP_SIZE} EP=${EP_SIZE})"
else
  EP_ARGS=()
  echo "[deploy] expert-parallel OFF (纯 TP, ENABLE_EP=1 可启用)"
fi

# 捆绑 libnuma 修复: 宿主机 libnuma 覆盖镜像内 RHEL9 libnuma (避免 `NUMA not available! abort.`)
HOST_LIBNUMA="${HOST_LIBNUMA:-/lib/x86_64-linux-gnu/libnuma.so.1.0.0}"
CTR_LIBNUMA="${CTR_LIBNUMA:-/root/miniconda3/lib/python3.12/site-packages/lk_moe.libs/libnuma-14905a75.so.1.0.0}"
if [ ! -f "${HOST_LIBNUMA}" ]; then
  echo "ERROR: ${HOST_LIBNUMA} not found on host" >&2
  exit 1
fi

# 思考参数 vLLM 补丁 (只读挂载覆盖镜像内同名文件); 每个文件必须存在
PATCHES_DIR="$(dirname "$(dirname "$(realpath "$0")")")/patches/vllm"
PATCH_FILES=(
  "entrypoints/openai/chat_completion/protocol.py"
  "entrypoints/anthropic/protocol.py"
  "entrypoints/anthropic/serving.py"
  "tokenizers/deepseek_v4.py"
  "tokenizers/deepseek_v4_encoding.py"
)
PATCH_BINDS=()
if [ "${PATCH_VLLM}" = "1" ]; then
  for pf in "${PATCH_FILES[@]}"; do
    if [ ! -f "${PATCHES_DIR}/${pf}" ]; then
      echo "ERROR: patch file missing: ${PATCHES_DIR}/${pf} (PATCH_VLLM=0 to disable)" >&2
      exit 1
    fi
    PATCH_BINDS+=(-v "${PATCHES_DIR}/${pf}:/root/miniconda3/lib/python3.12/site-packages/vllm/${pf}:ro")
  done
  echo "[deploy] vLLM thinking-patches ON (${#PATCH_FILES[@]} files mounted read-only)"
else
  echo "[deploy] vLLM thinking-patches OFF (PATCH_VLLM=0, 镜像原版行为)"
fi

docker rm -f "${CONTAINER}" 2>/dev/null || true

echo "[deploy] ${CONTAINER} | GPUS=${GPUS} TP=${TP_SIZE} max-num-seqs=${MAX_NUM_SEQS} LK_THREADS=${LK_THREADS} GPU_MEM_UTIL=${GPU_MEM_UTIL}"
echo "[deploy] MAX_MODEL_LEN=${MAX_MODEL_LEN} MAX_BATCHED_TOKENS=${MAX_BATCHED_TOKENS} dspark=${DSPARK_SPEC_TOKENS}"
echo "[deploy] EXEC_MODEL_TIMEOUT=${EXEC_MODEL_TIMEOUT}s (EngineCore 模型执行硬超时)"
echo "[deploy] LVLLM_GPU_RESIDENT_MOE_LAYERS=${GPU_RESIDENT_MOE_LAYERS} (MoE 常驻显存)"

docker run -d --name "${CONTAINER}" \
  --gpus "\"${GPUS}\"" \
  --ipc=host --shm-size=16g \
  --cap-add=SYS_NICE \
  -p ${PORT}:${PORT} \
  -v "${MODEL_HOST_PATH}:${MODEL_CTR_PATH}" \
  -v "${HOST_LIBNUMA}:${CTR_LIBNUMA}:ro" \
  "${PATCH_BINDS[@]}" \
  -v "${CACHE_DIR:-/path/to/cache}:/root/.cache" \
  -e CUDA_DEVICE_ORDER=PCI_BUS_ID \
  -e CUDA_VISIBLE_DEVICES="${CONTAINER_CUDA_VISIBLE_DEVICES}" \
  -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
  "${NUMA_ENV[@]}" \
  -e LK_THREADS="${LK_THREADS}" \
  -e OMP_NUM_THREADS="${LK_THREADS}" \
  -e LK_THREAD_BINDING=CPU_CORE \
  -e LK_POWER_SAVING="${LK_POWER_SAVING}" \
  -e FLASHINFER_DISABLE_VERSION_CHECK=1 \
  -e VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS="${EXEC_MODEL_TIMEOUT}" \
  "${IMAGE}" \
  "${MODEL_CTR_PATH}" \
  --host 0.0.0.0 \
  --port ${PORT} \
  --tensor-parallel-size "${TP_SIZE}" \
  --max-model-len "${MAX_MODEL_LEN}" \
  --gpu-memory-utilization "${GPU_MEM_UTIL}" \
  --trust-remote-code \
  --served-model-name "${SERVED_MODEL}" \
  --compilation_config.cudagraph_mode FULL_DECODE_ONLY \
  --enable-prefix-caching \
  --enable-chunked-prefill \
  --watermark "${WATERMARK}" \
  "${OFFLOAD_ARGS[@]}" \
  --max-num-batched-tokens "${MAX_BATCHED_TOKENS}" \
  --dtype bfloat16 \
  --max-num-seqs "${MAX_NUM_SEQS}" \
  --enable-auto-tool-choice \
  --kv-cache-dtype fp8_ds_mla \
  --tokenizer-mode deepseek_v4 \
  --tool-call-parser deepseek_v4 \
  --reasoning-parser deepseek_v4 \
  --default-chat-template-kwargs '{"enable_thinking": true}' \
  "${EP_ARGS[@]}" \
  "${SPEC_ARGS[@]}" \
  --disable-custom-all-reduce

echo "[deploy] container started. follow logs:  docker logs -f ${CONTAINER}"
  • 正确调用命令:默认即为正文的"最佳实践"配置(512K / TP+EP / 30 并发 / 25600 预填充 / 8002),直接运行即可:
bash 复制代码
# 一步部署 (默认最佳实践: 512K / TP+EP / 30 并发 / 25600 预填充 / 8002)
bash scripts/deploy_ds4_tp_ep.sh

所有关键参数均为环境变量、可显式覆盖(不改脚本):

bash 复制代码
# 显式覆盖参数 (值即默认, 仅为演示; 按需改写)
PORT=8002 MAX_MODEL_LEN=524288 MAX_BATCHED_TOKENS=25600 MAX_NUM_SEQS=30 \
ENABLE_EP=1 bash scripts/deploy_ds4_tp_ep.sh

# 回退纯 TP (关 EP): ENABLE_EP=0
ENABLE_EP=0 bash scripts/deploy_ds4_tp_ep.sh

# 回退旧配置 (256K / 24 并发 / 8192 预填充 / 纯 TP)
MAX_MODEL_LEN=262144 MAX_NUM_SEQS=24 MAX_BATCHED_TOKENS=8192 ENABLE_EP=0 \
bash scripts/deploy_ds4_tp_ep.sh

# KV offload: 长上下文 KV 放 CPU 内存 (decode 变慢换池自由)
KV_OFFLOAD_SIZE=256 bash scripts/deploy_ds4_tp_ep.sh

# dspark 投机解码开关 (默认 1): 高并发稳定可关
ENABLE_DSPARK=0 bash scripts/deploy_ds4_tp_ep.sh

可覆盖环境变量速查(默认值即最佳实践):

变量 默认 说明
GPUS device=4,5,6,7 设备选择
TP_SIZE / EP_SIZE 4 / 4 张量 / 专家并行度
ENABLE_EP 1 1=TP+EP, 0=纯 TP
MAX_MODEL_LEN 524288 (512K) 长上下文上限
MAX_NUM_SEQS 30 并发席位
MAX_BATCHED_TOKENS 25600 预填充 chunk
ENABLE_DSPARK / DSPARK_SPEC_TOKENS 1 / 3 投机解码
KV_OFFLOAD_SIZE (空) 非空则 KV 放 CPU
GPU_MEM_UTIL 0.9 显存利用率
WATERMARK 0.1 KV 空闲保留
EXEC_MODEL_TIMEOUT 600 EngineCore 执行硬超时秒
LK_THREADS 14 lk_moe 线程数

镜像 garenleeasa/lvllmds4x:v2.3.9-cuda-12.8.1-ubuntu-22.04 为公开 Docker 镜像。

11.2 自愈 supervisor supervisor.sh

bash 复制代码
#!/usr/bin/env bash
# =============================================================================
# GPU4-7 (lvllm-ds4-tp4-ep4, 8002) 自愈看门狗
# -----------------------------------------------------------------------------
# 背景 (宕机复盘):
#   vLLM EngineCore 遇 worker 单步执行超时 (execute_model RPC 超
#   VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS 后) 会抛 fatal, 但 vLLM 不会自愈:
#   容器进程仍存活 (docker running=true), API 却永久不可用, 遗留残 worker 占显存。
#
# 作为外部 supervisor 兜底: 周期性探测服务, 一旦发现"僵死"即自动重建
# (docker rm -f + 重新跑 deploy_ds4_tp_ep.sh), 把故障恢复时间从"手工发现"压到分钟级。
#
# 检测逻辑:
#   1) 健康探测: 对 ${BASE_URL}/health (或 /metrics) 做 HTTP 探测, N 次连续失败即判定故障;
#      - 也扫描容器日志中 EngineCore fatal / TimeoutError 关键字 (中毒的强证据)。
#   2) 重建: docker rm -f "${CONTAINER}" → bash deploy_ds4_tp_ep.sh (阻塞等起)。
#
# 用法:
#   ./supervisor.sh            前台循环 (推荐配 systemd / nohup)
#   ./supervisor.sh once       只检查一次 (便于 cron 调用)
#
# 参数 (env 覆盖):
#   BASE_URL     默认 http://127.0.0.1:8002
#   INTERVAL     探测间隔秒, 默认 30
#   FAIL_THRESH  连续失败次数, 默认 3 (约 90s 判定故障)
#   HEALTH_PATH  健康路径, 默认 /health
#   STARTUP_GRACE 容器启动宽限期秒, 默认 600。容器实际启动时间(StartedAt)距今 < 该值
#                 → 跳过健康探测 (避免把正常冷启动误判为宕机)。
#                 ⚠️ 必须 ≥ vLLM 完整启动耗时 (模型加载+JIT 编译+CUDA graph 捕获, 实测 ~6-7 分钟)。
#                 ⚠️⚠️ 若设太小 (如默认早先的 180s), 启动未完成即被连续判死 → 无限重建循环。
#   LOG_KEY      DOCKER 日志中的致命关键字 (正则); 命中即重建, 默认
#                "EngineCore encountered a fatal error|RPC call to .*timed out"
#   ENABLED      =0 关闭 (只输出不动作)
# =============================================================================
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTAINER="${CONTAINER:-lvllm-ds4-tp4-ep4}"
DEPLOY="${SCRIPT_DIR}/deploy_ds4_tp_ep.sh"

BASE_URL="${BASE_URL:-http://127.0.0.1:8002}"
INTERVAL="${INTERVAL:-30}"
FAIL_THRESH="${FAIL_THRESH:-3}"
HEALTH_PATH="${HEALTH_PATH:-/health}"
STARTUP_GRACE="${STARTUP_GRACE:-600}"
ENABLED="${ENABLED:-1}"
LOG_KEY="${LOG_KEY:-EngineCore encountered a fatal error|RPC call to .* timed out}"

TS() { date '+%F %T'; }
fail=0

rebuild() {
  local why="$1"
  echo "[$(TS)] ⚠️ 触发自愈 ($why) --- 重建 ${CONTAINER}..."
  docker rm -f "${CONTAINER}" 2>/dev/null
  if [ "${ENABLED}" = "1" ]; then
    bash "${DEPLOY}" && {
      echo "[$(TS)] ✅ ${CONTAINER} 重建完成"
      fail=0
    } || {
      echo "[$(TS)] ❌ 重建失败, ${INTERVAL} 秒后重试"
      fail=$((FAIL_THRESH + 1))  # 强制下轮继续尝试
    }
  else
    echo "[$(TS)] ENABLED=0, 跳过实际重建 (仅报告)"
  fi
}

once() {
  # 1) 健康探测
  if curl -sf -m 10 "${BASE_URL}${HEALTH_PATH}" >/dev/null 2>&1; then
    fail=0
    return 0
  fi
  # 2) 日志中毒检测 (服务也许还没起来, 但日志已现 fatal → 直接重建)
  if docker logs --tail 200 "${CONTAINER}" 2>/dev/null | grep -qE "${LOG_KEY}"; then
    echo "[$(TS)] 🔴 检测到引擎致命错误日志 (EngineCore fatal), 判定僵死"
    rebuild "日志命中致命关键字"
    return 0
  fi
  # HTTP 失败但不含致命日志 → 累计失败计数
  fail=$((fail + 1))
  echo "[$(TS)] ⚠️ 健康探测失败 ${fail}/${FAIL_THRESH} (${BASE_URL}${HEALTH_PATH})"
  if [ "${fail}" -ge "${FAIL_THRESH}" ]; then
    rebuild "连续 ${fail} 次健康检查失败"
  fi
  return 0
}

# once 模式
if [ "${1:-}" = "once" ]; then
  once
  exit 0
fi

# 前台循环
echo "[$(TS)] supervisor 启动: ${BASE_URL} | interval=${INTERVAL}s | fail_thresh=${FAIL_THRESH} | grace=${STARTUP_GRACE}s | enabled=${ENABLED}"
while true; do
  # 冷启动宽限: 以容器实际启动时间为准 (docker inspect StartedAt)
  # StartedAt 距今 < STARTUP_GRACE → 判定仍在加载/启动期, 跳过健康探测
  # (避免把正常冷启动 (~6-7 分钟) 误判为宕机 → 无限重建循环)
  CONTAINER_STARTED=$(docker inspect -f '{{.State.StartedAt}}' "${CONTAINER}" 2>/dev/null)
  if [ -n "${CONTAINER_STARTED}" ]; then
    STARTED_EPOCH=$(date -d "${CONTAINER_STARTED}" +%s 2>/dev/null || echo 0)
    NOW_EPOCH=$(date +%s)
    if [ "$(( NOW_EPOCH - STARTED_EPOCH ))" -lt "${STARTUP_GRACE}" ]; then
      sleep "${INTERVAL}"
      continue
    fi
  fi
  once
  sleep "${INTERVAL}"
done

⚠️ 教训(2026-08-25) :早期版本用 180s 冷却,而 vLLM 完整启动实测需 ~6-7 分钟 → 每次启动未完成即被判死重建,在无任何真实流量下累计重建 940 次 ,表面上看"GPU4-7 很不稳"。已改为基于容器实际启动时间的 600s 宽限,修复后健康服务零误杀

11.3 三件套一键托管 start_trio.sh

bash 复制代码
#!/usr/bin/env bash
# =============================================================================
# GPU4-7 (lvllm-ds4-tp4-ep4, 8002) 稳定性三件套一键启动
# -----------------------------------------------------------------------------
# 三件套: ① 参数由 deploy 脚本注入 (EXEC_MODEL_TIMEOUT)
#         ② supervisor.sh (自愈兜底)
#         ③ monitor.sh watch (生成吞吐冻结前兆探测 + AUTO_HEAL 联动)
#
# 用法:
#   bash scripts/start_trio.sh         启动三件套 (幂等: 已运行则先清理重启)
#   bash scripts/start_trio.sh status  查看当前状态/守护进程 PID
#   bash scripts/start_trio.sh stop    停止全部守护进程 (不删容器)
#
# 参数 (env 覆盖):
#   BASE_URL    默认 http://127.0.0.1:8002 (GPU4-7)
#   LOG_DIR     守护进程日志目录, 默认 ${SCRIPT_DIR}/logs
# =============================================================================
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_URL="${BASE_URL:-http://127.0.0.1:8002}"
LOG_DIR="${LOG_DIR:-${SCRIPT_DIR}/logs}"
mkdir -p "${LOG_DIR}"

SUP="${SCRIPT_DIR}/supervisor.sh"
MON="${SCRIPT_DIR}/monitor.sh"

stop_all() {
  pkill -f "supervisor.sh" 2>/dev/null && echo "[stop] supervisor 已停止" || echo "[stop] supervisor 未在运行"
  pkill -f "monitor.sh watch" 2>/dev/null && echo "[stop] monitor 已停止" || echo "[stop] monitor 未在运行"
  sleep 1
}

status() {
  echo "=== GPU4-7 稳定性三件套状态 ==="
  echo "BASE_URL=${BASE_URL}"
  local sup mon
  sup="$(pgrep -f "supervisor.sh" | head -1)"
  mon="$(pgrep -f "monitor.sh watch" | head -1)"
  echo "supervisor: ${sup:-未运行}"
  echo "monitor   : ${mon:-未运行}"
  curl -sf -m 5 -o /dev/null "${BASE_URL}/health" && echo "8002 /health: 200 (健康)" || echo "8002 /health: 不可用"
}

start() {
  # 幂等: 先停旧进程再启, 避免重复实例
  stop_all

  # ② supervisor: 自愈兜底 (默认 ENABLED=1)
  ( nohup env BASE_URL="${BASE_URL}" bash "${SUP}" >"${LOG_DIR}/supervisor.log" 2>&1 & )
  sleep 1

  # ③ monitor: watch 模式, 生成吞吐冻结探测 + 自动调 supervisor 重建
  ( nohup env BASE_URL="${BASE_URL}" AUTO_HEAL=1 bash "${MON}" watch >"${LOG_DIR}/monitor.log" 2>&1 & )
  sleep 1

  echo "[start] 三件套已启动:"
  echo "  supervisor → ${LOG_DIR}/supervisor.log  (自愈重建兜底)"
  echo "  monitor    → ${LOG_DIR}/monitor.log     (吞吐冻结前兆 + AUTO_HEAL)"
  echo "  参数层 (EXEC_MODEL_TIMEOUT) 由 deploy_ds4_tp_ep.sh 注入, 本脚本不重复设置"
  status
}

case "${1:-start}" in
  start)  start ;;
  stop)   stop_all ;;
  status) status ;;
  *) echo "用法: $0 [start|stop|status]"; exit 1 ;;
esac

11.4 关联监控 monitor.sh 调用方式

monitor.sh 是本方案的生成吞吐冻结前兆探测,与 supervisor 正交。watch 模式调用:

bash 复制代码
BASE_URL=http://127.0.0.1:8002 GEN_MIN_TPS=1.0 GEN_STALL_SEC=30 \
  AUTO_HEAL=1 bash scripts/monitor.sh watch &
  • Running>0 但生成吞吐 < GEN_MIN_TPS(默认 1.0 tok/s)持续 GEN_STALL_SEC(默认 30s)→ 告警"worker 疑似卡死"(这正是宕机前 16:07 的前兆);
  • AUTO_HEAL=1 时自动回调 supervisor 重建;
  • 与 KV 告警正交:KV 高 + 吞吐冻结 = 卡死高概率信号。

11.5 使用说明

  1. 部署 :先改脚本顶部的 MODEL_HOST_PATHIMAGECACHE_DIRGPUS 为你环境的实际值,再 bash deploy_ds4_tp_ep.sh
  2. 三件套托管 :bash start_trio.sh(status/stop 见脚本用法)。
  3. 脚本间依赖 :supervisor.sh 内部调用 deploy_ds4_tp_ep.sh 重建;start_trio.sh 托管 supervisor.sh + monitor.sh,参数层(EXEC_MODEL_TIMEOUT)由部署脚本注入,不在托管脚本重复设置。
  4. 可覆盖项 :所有关键参数均为环境变量(默认值即正文最佳实践),如 MAX_MODEL_LENMAX_NUM_SEQSMAX_BATCHED_TOKENSENABLE_EPEXEC_MODEL_TIMEOUT,无需改脚本即可调。
相关推荐
YIAN4 小时前
端侧大模型:DeepSeek-R1 WebGPU 推理全流程源码深度解析
前端·typescript·deepseek
张忠琳15 小时前
【deepseek-harness】DeepSeek Harness (dsh) 系统级架构分析之三
ai·agent·deepseek·harness
DS随心转小程序17 小时前
AI pdf 数字化文档落地攻略,靠 AI 导出鸭补齐转换短板,从痛点到实测详解智能化文档导出逻辑
人工智能·豆包·deepseek·ai导出鸭
戒了,最后一次21 小时前
DeepSeek Harness 源码安装教程(Windows 篇)
windows·腾讯云·deepseek·harness
DS随心转APP1 天前
实测 AI 导出鸭实操效果,依托 AI 直接生成 word 功能横向比对五类导出办法,梳理办公文档转换最优路径
人工智能·ai·word·deepseek·ai导出鸭
Zach_菠萝侠1 天前
【deepseek harness研究】进化方向7:分布式与远程执行 思考、设计与实现
分布式·深度学习·deepseek
AC赳赳老秦1 天前
个保法下数据处理:OpenClaw 自动过滤公开数据中的个人信息,保障采集分析合规性
java·python·sqlite·json·php·deepseek·openclaw
kyriewen2 天前
我带着DeepSeek Harness跑了一周真实需求——这份避坑速查表请收好
前端·ai编程·deepseek
大厂码农老A2 天前
177K star,扒开DeepSeek Harness的营销,我看到了什么
人工智能·后端·deepseek