Compare commits

...

38 Commits

Author SHA1 Message Date
Zhiyi Hong
a58b931cc8 [Profiling] Explain Kimi-K3 Deep PP scaling 2026-08-21 18:05:42 +08:00
Zhiyi Hong
0433fcc3ee [Benchmark] Add Kimi-K3 PP16 prefill results 2026-08-21 17:08:26 +08:00
Zhiyi Hong
d790df39b2 [Docs] Explain Kimi-K3 EP32 and EP4 execution 2026-08-21 15:19:12 +08:00
Zhiyi Hong
96ffea6d37 [Docs] Explain Kimi-K3 Deep PP Prefill optimization 2026-08-21 14:53:44 +08:00
Zhiyi Hong
d7381abe84 [Test] Add Kimi-K3 Prefill PP baseline search 2026-08-21 14:20:59 +08:00
Zhiyi Hong
60f77cd4ef [Profile] Attribute Kimi-K3 Prefill communication 2026-08-20 16:03:58 +08:00
Zhiyi Hong
6fac5ad567 [Docs] Attribute Kimi-K3 Prefill collectives 2026-08-20 13:54:18 +08:00
Zhiyi Hong
2a3b12fa78 [Docs] Reject Kimi-K3 Prefill TP Reduce Scatter path 2026-08-20 10:25:37 +08:00
Zhiyi Hong
08a35066d7 [Test] Complete Kimi-K3 Prefill MoE backend report 2026-08-19 16:54:45 +08:00
Zhiyi Hong
74ec19dd48 [Docs] Close Kimi SM120 delivery audit 2026-08-19 14:25:03 +08:00
Zhiyi Hong
0fdcab9927 [Docs] Rebase Kimi SM120 Draft onto synced main 2026-08-19 14:02:35 +08:00
Zhiyi Hong
a5248ed80e [Docs] Record exact Draft patch verification 2026-08-19 13:31:56 +08:00
Zhiyi Hong
fc336a3c7b [Test] Finalize Kimi SM120 PR representative benchmark 2026-08-19 13:11:21 +08:00
Zhiyi Hong
63f2327a90 [Fix] Validate per-request benchmark errors correctly 2026-08-19 11:54:34 +08:00
Zhiyi Hong
a90c898683 [Fix] Use official kernel version-check override for validation 2026-08-19 11:29:55 +08:00
Zhiyi Hong
ab9a5422f6 [Fix] Persist FlashInfer JIT cache across services 2026-08-19 11:16:07 +08:00
Zhiyi Hong
7f67dfe6b3 [Fix] Keep ABI-matched SGLang kernel in PR image 2026-08-19 11:00:50 +08:00
Zhiyi Hong
e8ff3ce1e8 [Test] Add exact Kimi SM120 PR validation point 2026-08-19 10:47:26 +08:00
Zhiyi Hong
d28db48e4b [Docs] Scope SGLang draft to compatibility 2026-08-19 00:12:38 +08:00
Zhiyi Hong
39f692caae [Docs] Keep draft checklist evidence-based 2026-08-19 00:10:20 +08:00
Zhiyi Hong
a9206ff105 [Docs] Add Kimi SM120 completion audit 2026-08-18 23:58:37 +08:00
Zhiyi Hong
a11c80b703 [Docs] Finalize Kimi SM120 SGLang draft PR 2026-08-18 23:47:34 +08:00
Zhiyi Hong
e01df16667 [Docs] Prepare Kimi SM120 SGLang draft PR 2026-08-18 23:16:53 +08:00
Zhiyi Hong
ec7b604a50 [Docs] Record Kimi EP4 MoE backend acceptance 2026-08-18 18:38:59 +08:00
Zhiyi Hong
27b8be09cb [Fix] Keep Kimi benchmark tokenizer offline 2026-08-18 15:12:24 +08:00
Zhiyi Hong
be9d6bfe3a [Fix] Materialize FlashInfer MXFP8 input layout 2026-08-18 14:50:13 +08:00
Zhiyi Hong
d0863501ca [Fix] Support legacy Kimi MoE runner config 2026-08-18 14:33:42 +08:00
Zhiyi Hong
13944079fa [Test] Add EP4 maximum-pressure capacity probe 2026-08-18 13:57:31 +08:00
Zhiyi Hong
e3974e2352 [Fix] Patch Kimi image for SM120 FlashInfer MXFP4 2026-08-18 13:06:32 +08:00
Zhiyi Hong
b50de8fe99 [Fix] Keep Kimi image dependency baseline for Phase 5 2026-08-18 12:46:08 +08:00
Zhiyi Hong
c14f8aa43a [Fix] Preserve FlashInfer wheel filename in image build 2026-08-18 12:40:02 +08:00
Zhiyi Hong
daeffd147b [Fix] Use built-in random IDs for Kimi Prefill matrix 2026-08-18 12:37:18 +08:00
Zhiyi Hong
5454fb984e [Test] Add Kimi SM120 real-serving MoE backend matrix 2026-08-18 12:32:06 +08:00
Zhiyi Hong
c8f30ab7dc [Perf] Profile Kimi SM120 FlashInfer MXFP4 MoE 2026-08-18 11:04:06 +08:00
Zhiyi Hong
6493798ad5 [Feature] Complete Kimi SM120 FlashInfer MXFP4 integration 2026-08-17 14:58:52 +08:00
Zhiyi Hong
a1c18d736b [Test] Add Kimi SM120 MXFP4 correctness matrix 2026-08-17 12:08:06 +08:00
Zhiyi Hong
dac1bb652d [Test] Reproduce Kimi SM120 SiTU contract gap 2026-08-14 17:09:13 +08:00
Zhiyi Hong
0684d269df [Docs] Audit Kimi-K3 SM120 FlashInfer MXFP4 gap 2026-08-14 15:52:14 +08:00
53 changed files with 8786 additions and 0 deletions

View File

@ -1,5 +1,31 @@
# sskj — 多平台大模型推理性能基准测试项目 # sskj — 多平台大模型推理性能基准测试项目
**更新2026-08-20 10:13:01 CST**:完成 Kimi-K3 Prefill TP Reduce Scatter 可行性审计并停止该方向。K3 的 MLA 输出门控仍依赖完整 7168 维 hidden且 69/93 层为 KDA保留或恢复 gate hidden 后,原 hidden All-Reduce 无法消除并新增 2112 维 latent All-Gather估算通信量反增约 14.7%。研究原型仅保留为否决证据,不进入四节点实验或上游 PR后续转向 MoE A2A 与 Pipeline Parallelism。详见 `experiments/pro6000/kimi3_pro6000_sglang_tp_reduce_scatter_prefill/README.md`
**更新2026-08-19 16:51:43 CST**:完成 Kimi-K3 / RTX 6000D / SGLang Prefill MoE backend 交付文档收敛与 Chunk=4K 补测。四节点 TP32×EP4、16K→1、C=8/16 下 12/12 重复、480/480 请求成功FlashInfer 相对 Marlin 提升约 9.8% Input TPS、降低约 9.0% TTFT。结合 4K/8K/16K 全部结果,最终推荐保持 `flashinfer_mxfp4 + chunked_prefill_size=8192`
**更新2026-08-19 14:23:52 CST**601 的 GitLab SSH key 已生效,内部 Kimi-K3 SM120 审计分支已推送至 `qqtang/sskj:hzy-kimi-k3-sm120-flashinfer-mxfp4`。远端已有普通分支 `hzy`,因此不能创建 `hzy/...` ref最终采用无层级冲突的分支名GitHub SGLang feature 分支保持不变。
**更新2026-08-19 13:58:50 CST**:用户同步 GitHub fork 后,将 Kimi-K3 SM120 SGLang 的 7 个提交无冲突重放到最新 `main` `593b1a9``range-diff` 逐提交确认补丁等价;最终 Draft HEAD `b95b534` 已推送至 `ACEEE-1222/sglang:hzy/kimi-k3-sm120-flashinfer-mxfp4`。在 601 统一验证镜像中重跑最终 HEAD 的语法与定向回归,结果为 `10 passed, 3 deselected`;代表性四机性能数据保留,不重复消耗 GPU。
**更新2026-08-19 13:07:48 CST**:完成 Kimi-K3 SM120 SGLang Draft PR 的最终代表点验收。基于 SGLang `300c87a` 与 FlashInfer #4460 `b525c51`,在 601-604 四节点 TP32×EP4 上完成 16K→1、C=8、Chunk=8K 的 Marlin/FlashInfer 各 3 次 A/B240/240 请求成功。FlashInfer 的 Input TPS 中位数提升 10.96%TTFT P50/P95 分别下降 9.91%/9.92%。公开 PR Checklist 已全部勾选,轻量汇总和原始证据索引已归档。
**更新2026-08-19 10:45:18 CST**:新增 Kimi-K3 SM120 SGLang Draft PR 的单点代表性验收入口。使用精确 SGLang `300c87a`、FlashInfer #4460 实现 `b525c51` 和统一镜像,在 601-604 四节点 TP32×EP4 上仅复测 16K→1、C=8、Chunk=8K 的 Marlin/FlashInfer A/B各 3 次重复;完整命令、镜像身份、原始日志和汇总统一落入单个 Run 目录。
**更新2026-08-18 23:00:42 CST**:完成依赖 FlashInfer #4460 的 SGLang Draft PR 收敛。确认不提交任何 FlashInfer PR也不 vendor 或 pin 未合并 kernel在 601 GPU6 上从 #4460 源码构建 FlashInfer 0.6.18 wheelSGLang 定向测试 8/8 通过。Draft 标题、依赖、兼容策略、测试和四机 EP4 数据见 `experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/SGLANG_DRAFT_PR.md`
**更新2026-08-18 22:21:40 CST**:完成 Kimi-K3 SM120 FlashInfer MXFP4 去重审计。确认 CUTLASS SiTU kernel 已由未合并的 FlashInfer PR #4460 实现,不再提交平行 kernelSGLang 贡献收敛为 Kimi gate/up 与 scale 布局、SiTU 4.0/25.0 参数映射、非连续输入和 SM120 attention-residual guard并保留 601-604 EP4 的全部服务级结果。详见 `experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/UPSTREAM_DUPLICATION_AUDIT.md`
**更新2026-08-18 18:38:17 CST**:完成 Kimi-K3 四节点 TP32×EP4 的真实 Prefill MoE backend 验收24/24 重复均 40/40 成功FlashInfer MXFP4 在 8K Chunk、C=8/16 下相对 Marlin 提升约 11% Input TPS、降低约 10% TTFT并确认 8K 优于 16K。实验默认 EP 改为 4补齐非连续 Prefill 输入与 FlashInfer 官方 SiTU 回归,详见 `experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/README.md`
**更新2026-08-18 13:55:30 CST**Kimi-K3 四节点 MoE backend 验收入口支持通过环境变量覆盖 EP size 及 backend/chunk/concurrency 子矩阵,用于先执行 TP32×EP4、16K Chunk、16K→1、C=16 的最大压力容量验证,同时保留默认 TP32×EP32 全矩阵。
**更新2026-08-18 13:03:33 CST**Phase 5 候选镜像改为针对 Kimi 基础镜像原生 SGLang 源码应用最小兼容补丁,仅补齐 SM120 FlashInfer MXFP4 backend 分发、权重布局与 SiTU 激活映射,避免整体替换新版 Python 包造成 `sglang-kernel` 版本错配。
**更新2026-08-18 12:24:19 CST**:新增 Kimi-K3 / 601-604 / SGLang TP32×EP32 的真实 Prefill MoE backend 验收实验;固定比较 Marlin 与 FlashInfer MXFP4 在 16K→1、C=8/16、Chunk=8K/16K 下的 TTFT、E2E 与 Input TPS并提供四节点一致候选镜像构建、原始证据和自动汇总详见 `experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/README.md`
**更新2026-08-17 11:56:47 CST**:完成 Kimi-K3 / RTX PRO 6000D / SGLang FlashInfer MXFP4 MoE Phase 2 correctness harnessSwiGLU 控制矩阵 8/8 通过,覆盖 edge routing、EP global ID、NaN/Inf、确定性、CUDA Graph 和 Kimi 真尺寸SiTU 目标矩阵 8/8 稳定复现同一 C++ 缺口,详见 `experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/README.md`
> 历史更新见 `git log`。项目目的与工作流见下方。 > 历史更新见 `git log`。项目目的与工作流见下方。
> **项目目的**当新显卡GPU/NPU到货时用最短时间完成大模型在该平台上的推理性能评估与部署配置选型。 > **项目目的**当新显卡GPU/NPU到货时用最短时间完成大模型在该平台上的推理性能评估与部署配置选型。
> 当前模型DeepSeek-V4-FlashFP8 / INT8后续接入 GLM5.2**完全复用**本项目的实验与报告流程。 > 当前模型DeepSeek-V4-FlashFP8 / INT8后续接入 GLM5.2**完全复用**本项目的实验与报告流程。
@ -45,6 +71,8 @@
| `experiments/pro6000/dsv4_pro6000_vllm_tp_dp_matrix/` | RTX 6000D + vLLM | | `experiments/pro6000/dsv4_pro6000_vllm_tp_dp_matrix/` | RTX 6000D + vLLM |
| `experiments/pro6000/dsv4_pro6000_sglang_tp_dp_matrix/` | RTX 6000D + SGLang | | `experiments/pro6000/dsv4_pro6000_sglang_tp_dp_matrix/` | RTX 6000D + SGLang |
| `experiments/pro6000/kimi3_pro6000_sglang_tp32ep32/` | RTX 6000D + SGLangKimi-K3TP32×EP32部署手册见 docs/KIMI_K3_DEPLOY.md | | `experiments/pro6000/kimi3_pro6000_sglang_tp32ep32/` | RTX 6000D + SGLangKimi-K3TP32×EP32部署手册见 docs/KIMI_K3_DEPLOY.md |
| `experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/` | RTX 6000D + SGLangKimi-K3 真实 Prefill 的 Marlin/FlashInfer MXFP4 与 Chunk 8K/16K 对照 |
| `experiments/pro6000/kimi3_pro6000_sglang_tp_reduce_scatter_prefill/` | Kimi-K3 Prefill TP Reduce Scatter 可行性审计;因 MLA gate 与 KDA 依赖完整 hidden结论为不推荐继续 |
| `experiments/pro6000/kimi3_pro6000_pd_rdma/` | RTX 6000D + SGLangKimi-K3 PD 分离MoonCake RDMA8 节点,见 deploy_pd.sh + docs/KIMI_K3_DEPLOY.md 附录 B | | `experiments/pro6000/kimi3_pro6000_pd_rdma/` | RTX 6000D + SGLangKimi-K3 PD 分离MoonCake RDMA8 节点,见 deploy_pd.sh + docs/KIMI_K3_DEPLOY.md 附录 B |
每个目录内:`run_bench.sh` 跑固定并发矩阵;`run_adaptive_concurrency.sh` 从 C=1 指数倍增搜饱和点;`run_adaptive_concurrency_add16.sh` 从 C=16 线性 +16 步进、带 TTFT SLO 停止与回退(当前主力用法,见 `experiments/ADAPTIVE_CONCURRENCY_USAGE.md`)。 每个目录内:`run_bench.sh` 跑固定并发矩阵;`run_adaptive_concurrency.sh` 从 C=1 指数倍增搜饱和点;`run_adaptive_concurrency_add16.sh` 从 C=16 线性 +16 步进、带 TTFT SLO 停止与回退(当前主力用法,见 `experiments/ADAPTIVE_CONCURRENCY_USAGE.md`)。

View File

@ -0,0 +1,454 @@
# Kimi K3 推理优化 - 优化 Prefill - Deep PP
## 1. 工作背景
Kimi-K3 在 4 台 RTX 6000D、共 32 张 GPU 上部署时,最直接的方式是
`PP1 / TP32 / EP4`:完整模型只有一个 Pipeline StageTensor Parallel
覆盖全部 32 张卡MoE Expert Parallel 使用 4 张卡。
这套配置可以正确运行,但 16K 长输入的 Prefill 延迟很高。既有 Nsight
Systems Profile 显示,在 `16K -> 1、C=8、chunk=8K` 场景中:
- 稳定 GPU 的 Prefill 窗口中NCCL 暴露时间占比中位数为 **57.27%**
- 单个 Profile 窗口约有 **557 次 AllReduce**
- AllReduce p50 约为 **7.10 ms**
- 通信与计算重叠率接近 **0%**
- 其中 MoE 相关 AllReduce 约占已分类 AllReduce 时间的 **58.73%**
这说明 PP1/TP32 并不只是“32 张卡一起算得更快”。Kimi-K3 每层中频繁出现
的 TP Collective 会跨越 4 台服务器,在没有 NVLink 的 RTX 6000D 上通过
RoCE 执行;等待通信的时间已经超过实际计算时间。
## 2. 为什么尝试 Deep PP
本实验所说的 Deep PP是把一个模型划分为更多 Pipeline Stage同时相应
缩小每个 Stage 内的 TP Group
```text
总 GPU 数 = PP × TP = 32
```
在固定 32 张 GPU 的前提下,本实验考察:
| 配置 | 每个 Stage 使用的 GPU | Stage 与节点的关系 |
|---|---:|---|
| PP1 / TP32 | 32 | 一个 Stage 跨 4 个节点 |
| PP2 / TP16 | 16 | 一个 Stage 跨 2 个节点 |
| PP4 / TP8 | 8 | 一个 Stage 完整落在 1 个节点 |
| PP8 / TP4 | 4 | 每个节点放置 2 个 Stage |
| PP16 / TP2 | 2 | 每个节点放置 4 个 Stage |
PP 加深后会新增 Pipeline Activation 的 Stage-to-Stage 传输,也会产生流水线
填充和排空造成的 bubble因此 PP 并不是越大越快。它可能带来收益的原因是:
1. 每层高频 TP AllReduce 的通信域从 32 卡逐步缩小到 16、8、4 卡;
2. PP4 开始TP Group 完整落在单机内,不再让每次 TP AllReduce 都跨节点;
3. PP8 中 TP4/EP4 都位于单机,跨节点通信主要集中在少数 Pipeline 边界;
4. 只要 C=8/16 提供了足够多的请求Scheduler 就可能用不同请求交错填充
Pipeline抵消一部分 bubble。
因此这一阶段要回答的不是“PP 能不能启动”,而是:在 Kimi-K3、6000D
和固定 Prefill 压力下,缩小 TP 通信域的收益能否超过 Pipeline 开销,以及
拐点出现在 PP2、PP4、PP8 还是 PP16。
## 3. 实现方式
### 3.1 唯一实验入口
实验只保留一个入口脚本:
```text
run_pp_baseline_search.sh
```
脚本负责:
1. 检查 601-604 的模型、镜像与 `/dev/infiniband`
2. 依次启动 PP2/TP16、PP4/TP8、PP8/TP4、PP16/TP2 服务;
3. 通过 `/get_server_info` 校验实际 PP、TP、EP 和 MoE backend
4. 每个配置执行 C=8/16各重复 3 次;
5. 校验每轮是否完成 40/40 请求以及是否存在请求错误;
6. 保存完整 Docker 命令、服务日志、benchmark 日志、原始 JSONL 和 GPU 快照;
7. 生成逐轮 `results.csv` 和中位数 `summary.csv`
8. 无论成功或失败,最终清理 4 台服务器上的实验容器。
### 3.2 SGLang PP 路径
本轮使用镜像:
```text
local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5
```
服务直接通过 SGLang 官方参数启动:
```bash
python3 -m sglang.launch_server \
--model-path /data/hf_models/Kimi-K3 \
--tp-size "${TP_SIZE}" \
--pp-size "${PP_SIZE}" \
--ep-size "${EP_SIZE}" \
--nnodes 4 \
--node-rank "${NODE_RANK}" \
--dist-init-addr 174.1.60.1:20000 \
--moe-runner-backend flashinfer_mxfp4 \
--chunked-prefill-size 8192
```
脚本默认取 `EP_SIZE=min(4, TP_SIZE)`。因此 PP2/4/8 使用 EP4PP16 因 TP2
必须使用 EP2满足 SGLang 的 `TP % EP == 0` 约束。
实验没有复制同事旧代码,也没有注入 `patch_pp_group.py`。准备阶段曾验证过
一版运行时 PP Group patch但它会让 Python Object 通信和 Tensor/P2P 通信
复用错误的 Gloo Group出现消息大小不匹配。回到当前 SGLang 上游实现后,
PP2、PP4 和 PP8 均能正常启动,因此该 patch 被完全移除,没有进入正式数据。
### 3.3 网络配置
4 个节点都向容器映射 `/dev/infiniband`,并使用:
```bash
NCCL_SOCKET_IFNAME=bond0
GLOO_SOCKET_IFNAME=bond0
NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
NCCL_IB_GID_INDEX=3
```
`bond0` 承担 Bootstrap/控制连接,`mlx5_0~3` 是 NCCL 的 RoCE HCA。正式运行
中没有出现 NCCL、Gloo、RDMA、connection refused 或 communicator failure。
## 4. 实验设计
为了让结果只反映 PP/TP 变化,其他主要变量保持固定:
| 项目 | 固定值 |
|---|---|
| 模型 | Kimi-K3 |
| GPU | 601-60432 × RTX 6000D |
| MoE runner | FlashInfer MXFP4 |
| A2A backend | none |
| EP | PP1/2/4/8 为 4PP16 为 2 |
| 输入/输出 | 16,384 / 1 token |
| Chunked Prefill | 8,192 token |
| 并发 | C=8、C=16 |
| 每轮请求数 | 40 |
| 重复次数 | 3 |
| 数据集 | random-ids输入长度比例固定为 1.0 |
| Prefix Cache | `--disable-radix-cache` |
| 显存比例 | 0.88 |
输出长度固定为 1是为了让 TTFT 和 Input TPS 主要反映 Prefill而不是被长
Decode 混入。每个点重复 3 次,最终使用中位数,避免首轮 JIT、缓存状态或系统
瞬时抖动主导结果。
PP1 没有重复跑。它来自前一阶段已经完成的 FlashInfer MXFP4、EP4、chunk8K
实验。经逐项核对PP1 与本轮使用相同镜像、模型、MoE backend、EP、输入输出、
并发、请求数、显存比例和网络参数,区别只有 `PP1/TP32`,因此可以直接并入。
## 5. 一个关键实现差异PP Micro Batch
同事旧 PP4 配置显式设置过:
```bash
--pp-max-micro-batch-size 1
```
当前 SGLang 在该参数未设置时,会在 Scheduler 初始化后自动计算:
```python
pp_max_micro_batch_size = max(max_running_requests // pp_size, 1)
```
接纳新请求时又会执行:
```python
allocatable_requests = pp_max_micro_batch_size - running_batch_size
```
因此这里的 `1` 不只是一个普通的 Kernel batch 参数,它会把 PP Scheduler
允许同时运行的请求数压到 1。对于 C=8/16这会让请求大量排队Pipeline
难以通过请求交错保持各 Stage 忙碌bubble、P2P 和调度固定成本都会被放大。
本实验不传这个参数,使用 SGLang 当前的自动值。旧实验还混合了 Marlin、
chunk16K、EP1/EP8、PP16、旧镜像和运行时 PP patch因此旧数据不能作为
“PP 天生更慢”的单变量证据。
## 6. 实验过程与正确性
正式 Run ID
```text
kimi3-pp-upstream-20260821-120208
kimi3-pp16-20260821-161721
```
服务启动时间分别为:
| 配置 | 健康检查通过时间 |
|---|---:|
| PP2 / TP16 / EP4 | 417 s |
| PP4 / TP8 / EP4 | 405 s |
| PP8 / TP4 / EP4 | 338 s |
| PP16 / TP2 / EP2 | 538 s |
PP2、PP4、PP8 共形成 18 个正式样本;补充的 PP16 形成 6 个正式样本。
每个样本均完成 40/40 请求,原始结果中的 errors 为空。服务和 benchmark
日志没有发现:
- CUDA OOM / OutOfMemory
- Traceback / Exception
- NCCL / Gloo / RDMA failure
- Engine failure
- 请求连接失败或提前终止。
PP8 在 C=8 和 C=16 下三次 Input TPS 的极差都小于 0.7%,说明结果不是某
一次运行的偶然峰值。
PP16 首次启动在 decode CUDA Graph 捕获时触发 custom all-reduce IPC buffer
注册错误;关闭 PP16 的 decode graph 后16-stage warmup 又超过默认 300 秒
watchdog。正式 PP16 使用 `--disable-decode-cuda-graph --watchdog-timeout
3000000`538 秒后健康。该差异不改变本轮 Prefill 路径:输出仅 1 token
没有迭代 Decode但 PP16 的 Decode 性能不能由本实验推断。PP16 三次 Input
TPS 极差不超过 0.7%,结果同样稳定。
## 7. 性能结果
下表均为 3 次正式运行的中位数:
| PP / TP / EP | C | Input TPS | TTFT p50 | TTFT p95 | Input TPS 相对 PP1 | TTFT p50 相对 PP1 |
|---|---:|---:|---:|---:|---:|---:|
| 1 / 32 / 4 | 8 | 3,257.96 | 39.19 s | 41.62 s | 基线 | 基线 |
| 2 / 16 / 4 | 8 | 5,015.95 | 25.22 s | 26.69 s | +53.96% | -35.65% |
| 4 / 8 / 4 | 8 | 7,155.20 | 17.28 s | 18.62 s | +119.62% | -55.90% |
| 8 / 4 / 4 | 8 | **8,809.50** | **13.47 s** | **16.45 s** | **+170.40%** | **-65.64%** |
| 16 / 2 / 2 | 8 | 6,701.04 | 17.94 s | 21.39 s | +105.68% | -54.22% |
| 1 / 32 / 4 | 16 | 3,260.14 | 78.39 s | 80.88 s | 基线 | 基线 |
| 2 / 16 / 4 | 16 | 5,018.38 | 50.70 s | 52.14 s | +53.93% | -35.32% |
| 4 / 8 / 4 | 16 | 7,152.58 | 34.84 s | 35.95 s | +119.39% | -55.56% |
| 8 / 4 / 4 | 16 | **8,824.12** | **27.31 s** | **29.77 s** | **+170.67%** | **-65.16%** |
| 16 / 2 / 2 | 16 | 6,700.96 | 35.88 s | 39.49 s | +105.54% | -54.23% |
从 PP1 到 PP8Input TPS 沿着 PP2、PP4、PP8 单调上升TTFT 也同步下降;
PP16 则出现反转。相对 PP8PP16 在 C=8/16 的 Input TPS 分别下降 23.93%
和 24.06%TTFT p50 分别增加 33.18% 和 31.37%。这说明继续把 TP4 缩到
TP2 的收益已无法抵消 16-stage bubble、更多 P2P 边界和调度开销。
PP16 同时把 EP4 改为 EP2因此它不是严格的纯 PP 单变量对照。但它准确代表
32 卡上可实际部署的 `PP16/TP2` 组合,足以判定该组合不应替代 PP8 基线。
C=16 的 TTFT 大致是 C=8 的两倍,而同一 PP 下 Input TPS 基本不变。这表明
这批 Prefill 工作已接近该配置的稳定吞吐区:增加并发主要增加排队等待,而不是
继续提高输入吞吐。PP 改善的是服务处理这批 token 的速率,因此两个并发点都
出现了接近一致的 TPS 增益。
## 8. 显存结果
下表取每个配置 benchmark 完成后4 个节点 32 张卡中的最高显存:
| PP / TP / EP | 峰值显存 | 相对 85,651 MiB 的剩余空间 |
|---|---:|---:|
| 1 / 32 / 4 | 81,355 MiB | 4,296 MiB |
| 2 / 16 / 4 | 80,335 MiB | 5,316 MiB |
| 4 / 8 / 4 | 82,417 MiB | 3,234 MiB |
| 8 / 4 / 4 | 79,407 MiB | 6,244 MiB |
| 16 / 2 / 2 | 79,989 MiB | 5,662 MiB |
PP 的 Stage 不会获得完全相同的层、输出头和状态,因此各 PP 配置的显存不再
像 PP1 那样均匀。PP4 的最重 Stage 已使用约 82.4 GiBPP8 的最重 Stage
约 79.4 GiB在本轮反而保留了最多的最坏 Rank 余量。
这一点对下一阶段 A2A 很重要,因为 DeepEP 或 FlashInfer A2A 都可能申请额外
dispatch/combine buffer。只看 TPS 而忽略最重 Rank可能出现基线可跑、打开
A2A 后启动或 Prefill OOM 的情况。
## 9. 通信结果如何解释
### 9.1 累计收益变大,但每次增加 PP 的边际收益在下降
第 7 节相对 PP1 的累计数字会让人感觉“PP 越大,收益增长越快”。逐级比较后,
实际规律相反。以 C=8 为例:
| 变化 | Input TPS 边际变化 | TTFT p50 边际变化 |
|---|---:|---:|
| PP1 -> PP2 | +53.96% | -35.65% |
| PP2 -> PP4 | +42.65% | -31.48% |
| PP4 -> PP8 | +23.12% | -22.05% |
| PP8 -> PP16 | -23.93% | +33.18% |
所以 PP1 到 PP8 的累计收益持续扩大,但每次把 PP 翻倍带来的新增收益已经逐步
减小PP16 则越过最优点。C=16 也呈现相同趋势。
### 9.2 节点内 PCIe 与节点间 RoCE 的实测
在 601--604 上使用 NCCL 2.28.9 + CUDA 13 的 `all_reduce_perf`,分别测量与
PP Stage 对应的 TP4、TP8、TP16 和 TP32 通信域。所有测试均为 0 个错误值:
| 通信域 | 实际拓扑 | 128 MiB 延迟 | 128 MiB busbw | 128/256 MiB 平均 busbw |
|---|---|---:|---:|---:|
| TP4 | 单节点 GPU0--3同一 PCIe/NUMA 岛 | 4.93 ms | 40.85 GB/s | 41.00 GB/s |
| TP8 | 单节点 8 卡,跨两个 NUMA 岛 | 6.00 ms | 39.13 GB/s | 38.89 GB/s |
| TP16 | 两节点 | 6.24 ms | 40.31 GB/s | 40.60 GB/s |
| TP32 | 四节点 | 6.59 ms | 39.47 GB/s | 40.08 GB/s |
大消息带宽都在约 39--41 GB/s说明 PP 收益不是因为节点间 400G RoCE 的
持续带宽远低于节点内 PCIe。真正变化更明显的是 collective 的参与 Rank 数、
固定延迟和同步范围128 MiB AllReduce 从 TP32 的 6.59 ms 降到 TP4 的
4.93 ms。
两个拓扑边界尤其重要:
- PP2 -> PP4 把每个 Stage 的 TP 域从两节点 TP16 缩到单节点 TP8逐层
AllReduce 不再跨节点;
- PP4 -> PP8 把 TP8 缩到 TP4每个 Stage 可落在同一个 PCIe/NUMA 岛,避免
单节点内跨 CPU Root Complex同时每卡得到更大的 GEMM 分片,减轻 TP32
过度切分造成的小矩阵低效率。
### 9.3 PP1 与 PP8 的 Nsight 对照
为避免把 Profiler 开销混入第 7 节性能结果,另起同场景短窗口 Nsight
`16K -> 1, C=8, chunk=8K`。以下是稳定 Rank 的中位数:
| 每个活跃 Rank/Stage 的指标 | PP1 / TP32 | PP8 / TP4 | 变化 |
|---|---:|---:|---:|
| 普通 AllReduce 调用数 | 557 | 66 | -88.2% |
| 普通 AllReduce 累计时间 | 4,160.6 ms | 392.3 ms | -90.6% |
| 单次 AllReduce p50 | 7.10 ms | 5.83 ms | -17.9% |
| 计算与 NCCL 时间线重叠 | 约 0 | 约 0 | 未形成有效重叠 |
这里的 557 -> 66 是**单个 Rank/Stage**的 Profile 窗口,不是一个请求在整个
模型上的 collective 总数。PP8 把模型层分给 8 个 Stage一个请求仍要依次经过
全部 Stage它减少的是每个活跃 Rank 在一个 Stage 内负责的层数和同步工作,
并让不同请求的 Stage 可以流水并行,而不是凭空删除模型全部通信。
PP8 Trace 还出现约 3.43 s 的非 AllReduce NCCL 区间,主要对应 Pipeline
Send/Recv 及 Stage 等待。该值包含异步 Stage 等待,不能直接当作 3.43 s 的
网络传输时间,也不能把 PP8 的 NCCL wall 百分比与 PP1 做等口径比较。可靠的
对照是上表中的调用数、普通 AllReduce 累计时间和单次延迟。
### 9.4 为什么 PP8 最优PP16 反而退化
PP1 -> PP8 的收益由四项共同产生:
1. TP 通信域由 32 Rank 缩到 4 Rank单次 collective 延迟降低;
2. PP4/PP8 的 TP collective 保持在节点内PP8 进一步保持在同一 PCIe/NUMA
岛;
3. 每个 Rank 只执行所在 Stage 的层C=8/16 的不同请求能在多个 Stage 并行,
将通信和计算流水化;
4. TP32 过度切分的 GEMM 在 TP4 下变成更大的本地矩阵Kernel 效率更高。
收益没有继续延伸到 PP16C=8 时请求数甚至少于 16 个 Stage流水线无法填满
C=16 也只有一波请求。更多 Stage 边界、P2P Send/Recv、调度和 bubble 超过了
TP4 -> TP2 的剩余收益,而且 PP16 还因整除约束从 EP4 改成 EP2。因此当前证据
支持的是“PP8 在这个 Prefill 压力点达到通信、计算粒度与流水线开销的平衡”,
而不是“PP 越大越快”。
## 10. 最终判断与后续使用
在本次固定场景中,`PP8 / TP4 / EP4` 同时得到:
- 最高的 Input TPS
- 最低的 TTFT p50/p95
- 三次重复中稳定的结果;
- 比 PP1 和 PP4 更充足的最坏 Rank 显存余量;
- 单节点内的 TP4/EP4 通信域。
因此,下一阶段 MoE A2A 的首个兼容性和显存 Smoke 应固定使用:
```text
PP8 / TP4 / EP4
16K -> 1
C=8/16
chunked_prefill_size=8192
FlashInfer MXFP4
```
`PP4 / TP8 / EP4` 保留为回退对照:如果某个 A2A backend 对 PP8/TP4 有当前
代码未覆盖的约束,可以快速判断问题来自 backend 兼容性,还是 A2A 本身。
`PP16 / TP2 / EP2` 不进入下一阶段默认矩阵。它相对 PP1 仍有明显收益,但比
PP8 慢约 24%,并引入 decode graph 兼容性、超长启动 warmup 和 EP2 变量。
这项结果不能外推到低并发 C=1、长 Decode、在线混合流量或更多节点。当前
TTFT 即使在 PP8 下仍为 13.47 sC8和 27.31 sC16距离 5 s 目标仍有
明显差距。Deep PP 解决了一个主要通信瓶颈,但不是 Prefill 优化的终点。
## 11. 复现命令与证据
在 601 执行:
```bash
cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search
RUN_ID=kimi3-pp-$(date +%Y%m%d-%H%M%S) \
SUDO_PASSWORD_FILE=/path/to/password-file \
bash run_pp_baseline_search.sh run
```
PP2/4/8 正式结果:
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_pp_baseline_search/
results/kimi3-pp-upstream-20260821-120208/
```
其中:
```text
summary.csv 6 个 PP×C 中位数
results.csv 18 个逐轮结果
metadata/ 实际服务配置与 Run manifest
service/ 四节点 Docker 命令和完整服务日志
bench/ benchmark 文本输出
raw/ benchmark JSONL
gpu/ 四节点启动前、健康、完成和清理后的显存快照
```
PP1 原始结果:
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/
results/kimi3-ep4-moe-full-20260818-151349/
```
PP16 正式结果:
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_pp_baseline_search/
results/kimi3-pp16-20260821-161721/
```
PP16 启动兼容性证据(未产生 benchmark 数据):
```text
results/kimi3-pp16-20260821-155734/ # custom AR decode graph IPC 错误
results/kimi3-pp16-20260821-160555/ # 默认 300s watchdog 超时
```
PP1 Nsight 通信归因:
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_prefill_communication_profile/
results/kimi3-prefill-comm-20260820-143749/nsys_analysis.json
```
PP8 Nsight 通信归因与四组 NCCL 微基准:
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_prefill_communication_profile/
results/kimi3-prefill-comm-pp8-20260821-172717/
nsys_analysis.json # PP8 各 Rank CUDA/NCCL 汇总
nsys_device_metrics.csv # PP8 各设备时间线指标
nsys/node0/prefill_node0.nsys-rep # 601 原始 Nsight Trace
nsys/node1/prefill_node1.nsys-rep # 602 原始 Nsight Trace
nsys/node2/prefill_node2.nsys-rep # 603 原始 Nsight Trace
nsys/node3/prefill_node3.nsys-rep # 604 原始 Nsight Trace
nccl_tp4_intra_node.log # 单节点同 PCIe/NUMA 岛
nccl_tp8_intra_node.log # 单节点跨 NUMA
nccl_tp16_2node.log # 两节点
nccl_tp32_4node.log # 四节点
```

View File

@ -0,0 +1,379 @@
#!/usr/bin/env bash
# Search Kimi-K3 prefill pipeline-parallel baselines on 601-604.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/common/lib.sh"
ACTION="${1:-run}"
EXPERIMENT="kimi3_pro6000_sglang_pp_baseline_search"
RUN_ID="${RUN_ID:-kimi3-pp-baseline-$(date '+%Y%m%d-%H%M%S')}"
RESULT_ROOT="${RESULT_ROOT:-${SCRIPT_DIR}/results/${RUN_ID}}"
MODEL_PATH="${MODEL_PATH:-/data/hf_models/Kimi-K3}"
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-kimi-k3}"
DOCKER_IMAGE="${DOCKER_IMAGE:-local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5}"
PORT="${PORT:-30000}"
HEAD_HOST="${HEAD_HOST:-174.1.60.1}"
NODE_SSH_USER="${NODE_SSH_USER:-user}"
NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4)
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
DIST_PORT="${DIST_PORT:-20000}"
MAX_EP_SIZE=4
read -r -a PP_SIZES <<< "${PP_SIZES_OVERRIDE:-2 4 8 16}"
CONCURRENCIES=(8 16)
INPUT_LEN=16384
OUTPUT_LEN=1
CHUNKED_PREFILL_SIZE=8192
NUM_PROMPTS=40
REPEATS="${REPEATS:-3}"
WARMUP_REQUESTS=2
HEALTH_WAIT_S="${HEALTH_WAIT_S:-2400}"
CONTAINER_PREFIX="${EXPERIMENT}"
mkdir -p "${RESULT_ROOT}"/{service,raw,bench,gpu,metadata}
log_init "${RESULT_ROOT}/orchestrator.log"
usage() {
printf 'Usage: SUDO_PASSWORD=... %s {dry-run|run|summarize|stop}\n' "$0"
}
require_password() {
if [[ -z "${SUDO_PASSWORD:-}" && -n "${SUDO_PASSWORD_FILE:-}" ]]; then
[[ -r "$SUDO_PASSWORD_FILE" ]] || {
echo "ERROR: cannot read SUDO_PASSWORD_FILE=$SUDO_PASSWORD_FILE" >&2
exit 2
}
IFS= read -r SUDO_PASSWORD <"$SUDO_PASSWORD_FILE"
fi
[[ -n "${SUDO_PASSWORD:-}" ]] || {
echo "ERROR: set SUDO_PASSWORD or SUDO_PASSWORD_FILE" >&2
exit 2
}
}
is_head() {
[[ "$1" == "$HEAD_HOST" ]]
}
sudo_host() {
local host="$1"
shift
require_password
if is_head "$host"; then
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' -- "$@"
return
fi
local remote_cmd
printf -v remote_cmd '%q ' "$@"
printf '%s\n' "$SUDO_PASSWORD" | ssh "${SSH_OPTS[@]}" \
"${NODE_SSH_USER}@${host}" "sudo -S -p '' -- ${remote_cmd}"
}
container_name() {
printf '%s_node%s' "$CONTAINER_PREFIX" "$1"
}
tp_for_pp() {
local pp="$1"
(( 32 % pp == 0 )) || { echo "ERROR: PP=$pp does not divide 32" >&2; return 1; }
echo $((32 / pp))
}
ep_for_tp() {
local tp="$1"
local ep="$MAX_EP_SIZE"
if (( tp < ep )); then
ep="$tp"
fi
(( tp % ep == 0 )) || {
echo "ERROR: TP=$tp must be divisible by EP=$ep" >&2
return 1
}
echo "$ep"
}
preflight() {
require_password
[[ -d "$MODEL_PATH" ]] || { echo "ERROR: missing model $MODEL_PATH" >&2; exit 2; }
local host
for host in "${NODE_HOSTS[@]}"; do
sudo_host "$host" docker image inspect "$DOCKER_IMAGE" >/dev/null
sudo_host "$host" test -e /dev/infiniband/uverbs0
done
{
echo "run_id=$RUN_ID"
echo "image=$DOCKER_IMAGE"
echo "model=$MODEL_PATH"
echo "pp_sizes=${PP_SIZES[*]}"
local pp tp ep
for pp in "${PP_SIZES[@]}"; do
tp="$(tp_for_pp "$pp")"
ep="$(ep_for_tp "$tp")"
echo "layout=pp${pp}_tp${tp}_ep${ep}"
done
echo "shape=${INPUT_LEN}->${OUTPUT_LEN}"
echo "chunked_prefill_size=$CHUNKED_PREFILL_SIZE"
echo "concurrencies=${CONCURRENCIES[*]}"
echo "num_prompts=$NUM_PROMPTS"
echo "repeats=$REPEATS"
} >"${RESULT_ROOT}/metadata/manifest.env"
}
stop_service() {
local rank host name
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
name="$(container_name "$rank")"
sudo_host "$host" docker rm -f "$name" >/dev/null 2>&1 || true
done
}
collect_service_logs() {
local label="$1" rank host name
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
name="$(container_name "$rank")"
sudo_host "$host" docker logs "$name" \
>"${RESULT_ROOT}/service/${label}_node${rank}.log" 2>&1 || true
done
}
collect_gpu_snapshot() {
local label="$1" rank host
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
sudo_host "$host" nvidia-smi \
--query-gpu=timestamp,index,memory.used,memory.total,utilization.gpu,power.draw \
--format=csv,noheader,nounits \
>"${RESULT_ROOT}/gpu/${label}_node${rank}.csv" 2>&1 || true
done
}
start_node() {
local rank="$1" pp="$2" tp="$3" ep="$4"
local host="${NODE_HOSTS[$rank]}" name bootstrap pp16_args=""
name="$(container_name "$rank")"
# PP16/TP2 currently fails while registering custom-all-reduce IPC buffers
# for decode graph capture. This prefill-only OSL=1 experiment does not use
# iterative decode. Its 16-stage startup also exceeds the default 300-second
# watchdog while lazily creating adjacent-stage P2P communicators.
if (( pp == 16 )); then
pp16_args="--disable-decode-cuda-graph --watchdog-timeout 3000000"
fi
bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)) && exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size ${tp} --pp-size ${pp} --ep-size ${ep} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size ${CHUNKED_PREFILL_SIZE} --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 ${pp16_args} --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}"
local -a cmd=(
docker run -d --name "$name"
--gpus all --network host --ipc=host --ulimit memlock=-1
--device /dev/infiniband --shm-size 32g --entrypoint bash
-v "${MODEL_PATH}:${MODEL_PATH}:ro"
-e CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
-e NCCL_SOCKET_IFNAME=bond0 -e GLOO_SOCKET_IFNAME=bond0
-e NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
-e NCCL_IB_GID_INDEX=3 -e NCCL_IB_TIMEOUT=22 -e NCCL_IB_RETRY_CNT=7
-e NCCL_CUMEM_ENABLE=1 -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
-e SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 -e SGLANG_MOE_FUSED_GATE_RADIX=1
-e FLASHINFER_DISABLE_JIT=1 -e FLASHINFER_DISABLE_VERSION_CHECK=1
"$DOCKER_IMAGE" -lc "$bootstrap"
)
printf '%q ' "${cmd[@]}" >"${RESULT_ROOT}/service/pp${pp}_node${rank}.cmd.txt"
printf '\n' >>"${RESULT_ROOT}/service/pp${pp}_node${rank}.cmd.txt"
sudo_host "$host" "${cmd[@]}" >/dev/null
}
wait_health() {
local pp="$1" i
for ((i = 1; i <= HEALTH_WAIT_S; i++)); do
if curl --fail --silent --max-time 5 "http://${HEAD_HOST}:${PORT}/health" >/dev/null 2>&1; then
log "service healthy PP=${pp} wait_s=${i}"
return 0
fi
if (( i % 30 == 0 )); then
log "waiting for PP=${pp} service elapsed_s=${i}"
collect_service_logs "pp${pp}_starting"
if grep -Eiq 'Traceback|CUDA out of memory|NCCL.*(error|failed)|RuntimeError|AssertionError' \
"${RESULT_ROOT}/service/pp${pp}_starting_node"*.log; then
log "ERROR: PP=${pp} startup log contains a fatal error"
return 1
fi
fi
sleep 1
done
return 1
}
verify_server() {
local pp="$1" tp="$2" ep="$3" info="${RESULT_ROOT}/metadata/pp${pp}_server_info.json"
curl --fail --silent "http://${HEAD_HOST}:${PORT}/get_server_info" >"$info"
python3 - "$info" "$pp" "$tp" "$ep" <<'PY'
import json, sys
path, pp, tp, ep = sys.argv[1], *map(int, sys.argv[2:])
info = json.load(open(path, encoding="utf-8"))
expected = {"pp_size": pp, "tp_size": tp, "ep_size": ep,
"moe_runner_backend": "flashinfer_mxfp4"}
bad = [f"{k}={info.get(k)!r}, expected={v!r}" for k, v in expected.items()
if info.get(k) != v]
if bad:
raise SystemExit("server configuration mismatch: " + "; ".join(bad))
print(expected)
PY
}
start_service() {
local pp="$1" tp ep
tp="$(tp_for_pp "$pp")"
ep="$(ep_for_tp "$tp")"
stop_service
log "starting PP=${pp} TP=${tp} EP=${ep} on 601-604"
start_node 1 "$pp" "$tp" "$ep"
start_node 2 "$pp" "$tp" "$ep"
start_node 3 "$pp" "$tp" "$ep"
sleep 5
start_node 0 "$pp" "$tp" "$ep"
if ! wait_health "$pp"; then
collect_service_logs "pp${pp}_startup_failed"
stop_service
return 1
fi
verify_server "$pp" "$tp" "$ep"
collect_service_logs "pp${pp}_healthy"
collect_gpu_snapshot "pp${pp}_healthy"
}
run_one_bench() {
local pp="$1" concurrency="$2" repeat="$3"
local stem="pp${pp}_c${concurrency}_r${repeat}"
local output_file="${RESULT_ROOT}/raw/${stem}.jsonl"
local bench_log="${RESULT_ROOT}/bench/${stem}.log"
rm -f "$output_file"
log "bench PP=${pp} C=${concurrency} repeat=${repeat}/${REPEATS}"
sudo_host "$HEAD_HOST" docker run --rm --network host \
-v "${MODEL_PATH}:${MODEL_PATH}:ro" \
-v "${RESULT_ROOT}:${RESULT_ROOT}" \
-e PYTHONUNBUFFERED=1 --entrypoint python3 "$DOCKER_IMAGE" \
-m sglang.benchmark.serving \
--backend sglang --host "$HEAD_HOST" --port "$PORT" \
--tokenizer "$MODEL_PATH" --dataset-name random-ids \
--random-input-len "$INPUT_LEN" --random-output-len "$OUTPUT_LEN" \
--random-range-ratio 1.0 --num-prompts "$NUM_PROMPTS" \
--max-concurrency "$concurrency" --request-rate 10000 \
--warmup-requests "$WARMUP_REQUESTS" --output-file "$output_file" \
--output-details --disable-tqdm >"$bench_log" 2>&1
python3 - "$output_file" "$NUM_PROMPTS" <<'PY'
import json, sys
path, expected = sys.argv[1], int(sys.argv[2])
rows = [json.loads(line) for line in open(path, encoding="utf-8") if line.strip()]
assert len(rows) == 1, (path, len(rows))
assert rows[0].get("completed") == expected, rows[0].get("completed")
assert not any(rows[0].get("errors", [])), "benchmark contains request errors"
PY
}
summarize() {
python3 - "$RESULT_ROOT" <<'PY'
import csv, json, re, statistics, sys
from pathlib import Path
root = Path(sys.argv[1])
pattern = re.compile(r"pp(\d+)_c(\d+)_r(\d+)\.jsonl$")
metrics = [
"request_throughput", "input_throughput", "output_throughput",
"total_throughput", "mean_ttft_ms", "median_ttft_ms", "p95_ttft_ms",
"p99_ttft_ms", "mean_e2e_latency_ms", "median_e2e_latency_ms",
"p95_e2e_latency_ms", "p99_e2e_latency_ms",
]
rows = []
for path in sorted((root / "raw").glob("*.jsonl")):
match = pattern.match(path.name)
if not match:
continue
data = next(json.loads(line) for line in path.open(encoding="utf-8") if line.strip())
pp, concurrency, repeat = map(int, match.groups())
tp = 32 // pp
row = {"pp_size": pp, "tp_size": tp, "ep_size": min(4, tp),
"concurrency": concurrency, "repeat": repeat,
"completed": data.get("completed")}
row.update({name: data.get(name) for name in metrics})
rows.append(row)
fields = list(rows[0]) if rows else []
with (root / "results.csv").open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
if fields:
writer.writeheader()
writer.writerows(rows)
groups = {}
for row in rows:
groups.setdefault((row["pp_size"], row["concurrency"]), []).append(row)
summary = []
for (pp, concurrency), group in sorted(groups.items()):
tp = 32 // pp
item = {"pp_size": pp, "tp_size": tp, "ep_size": min(4, tp),
"concurrency": concurrency, "repeats": len(group)}
for name in metrics:
values = [float(row[name]) for row in group if row.get(name) is not None]
item[f"median_{name}"] = statistics.median(values) if values else None
summary.append(item)
summary_fields = list(summary[0]) if summary else []
with (root / "summary.csv").open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=summary_fields)
if summary_fields:
writer.writeheader()
writer.writerows(summary)
print(f"wrote {len(rows)} result rows and {len(summary)} summary rows")
PY
}
run_matrix() {
preflight
collect_gpu_snapshot before_all
local pp concurrency repeat
for pp in "${PP_SIZES[@]}"; do
if ! start_service "$pp"; then
log "ERROR: PP=${pp} failed to start; preserving evidence and continuing"
continue
fi
for concurrency in "${CONCURRENCIES[@]}"; do
for ((repeat = 1; repeat <= REPEATS; repeat++)); do
run_one_bench "$pp" "$concurrency" "$repeat"
done
done
collect_service_logs "pp${pp}_completed"
collect_gpu_snapshot "pp${pp}_completed"
stop_service
sleep 5
done
collect_gpu_snapshot after_all
summarize
}
dry_run() {
local pp tp ep
for pp in "${PP_SIZES[@]}"; do
tp="$(tp_for_pp "$pp")"
ep="$(ep_for_tp "$tp")"
echo "PP=${pp} TP=${tp} EP=${ep} ${INPUT_LEN}->${OUTPUT_LEN} C=${CONCURRENCIES[*]} chunk=${CHUNKED_PREFILL_SIZE}"
done
bash -n "$0"
}
case "$ACTION" in
dry-run) dry_run ;;
run)
trap stop_service EXIT INT TERM
run_matrix
;;
summarize) summarize ;;
stop)
require_password
stop_service
;;
*) usage; exit 2 ;;
esac

View File

@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""Summarize CUDA/NCCL time and overlap from stage-scoped Nsight SQLite files."""
from __future__ import annotations
import argparse
import csv
import json
import math
import sqlite3
from collections import defaultdict
from pathlib import Path
def merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
merged: list[list[int]] = []
for start, end in sorted(intervals):
if end <= start:
continue
if not merged or start > merged[-1][1]:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)
return [(start, end) for start, end in merged]
def interval_length(intervals: list[tuple[int, int]]) -> int:
return sum(end - start for start, end in intervals)
def intersection_length(
left: list[tuple[int, int]], right: list[tuple[int, int]]
) -> int:
i = j = total = 0
while i < len(left) and j < len(right):
start = max(left[i][0], right[j][0])
end = min(left[i][1], right[j][1])
if end > start:
total += end - start
if left[i][1] <= right[j][1]:
i += 1
else:
j += 1
return total
def percentile(values: list[float], pct: float) -> float:
if not values:
return math.nan
ordered = sorted(values)
pos = (len(ordered) - 1) * pct
low = math.floor(pos)
high = math.ceil(pos)
if low == high:
return ordered[low]
return ordered[low] * (high - pos) + ordered[high] * (pos - low)
def analyze_sqlite(path: Path) -> tuple[list[dict], dict[str, dict]]:
connection = sqlite3.connect(path)
rows = connection.execute(
"""
SELECT k.deviceId, k.start, k.end, s.value
FROM CUPTI_ACTIVITY_KIND_KERNEL AS k
JOIN StringIds AS s ON k.shortName = s.id
ORDER BY k.deviceId, k.start
"""
).fetchall()
connection.close()
by_device: dict[int, list[tuple[int, int, str]]] = defaultdict(list)
top_names: dict[str, dict[str, float]] = defaultdict(
lambda: {"count": 0, "duration_ns": 0}
)
for device, start, end, name in rows:
by_device[device].append((start, end, name))
top_names[name]["count"] += 1
top_names[name]["duration_ns"] += end - start
output = []
node = path.parent.name
for device, kernels in sorted(by_device.items()):
all_intervals = merge_intervals([(start, end) for start, end, _ in kernels])
nccl_rows = [row for row in kernels if row[2].lower().startswith("nccl")]
compute_rows = [row for row in kernels if not row[2].lower().startswith("nccl")]
nccl_intervals = merge_intervals([(start, end) for start, end, _ in nccl_rows])
compute_intervals = merge_intervals(
[(start, end) for start, end, _ in compute_rows]
)
wall_ns = max(end for _, end, _ in kernels) - min(start for start, _, _ in kernels)
kernel_union_ns = interval_length(all_intervals)
nccl_union_ns = interval_length(nccl_intervals)
compute_union_ns = interval_length(compute_intervals)
overlap_ns = intersection_length(nccl_intervals, compute_intervals)
allreduce_ms = [
(end - start) / 1e6
for start, end, name in nccl_rows
if "allreduce" in name.lower()
]
normal_allreduce_ms = [value for value in allreduce_ms if value < 50.0]
# The 8K Prefill trace has a clean gap between the two known payloads:
# [M, 7168] ~= 112 MiB and M * (3584 + 7168) ~= 168 MiB.
hidden_allreduce_ms = [value for value in normal_allreduce_ms if value < 7.5]
moe_allreduce_ms = [value for value in normal_allreduce_ms if value >= 7.5]
normal_allreduce_total_ms = sum(normal_allreduce_ms)
output.append(
{
"node": node,
"local_gpu": device,
"wall_ms": wall_ns / 1e6,
"gpu_busy_union_ms": kernel_union_ns / 1e6,
"gpu_busy_pct": 100.0 * kernel_union_ns / wall_ns,
"nccl_union_ms": nccl_union_ns / 1e6,
"nccl_wall_pct": 100.0 * nccl_union_ns / wall_ns,
"compute_union_ms": compute_union_ns / 1e6,
"compute_wall_pct": 100.0 * compute_union_ns / wall_ns,
"nccl_compute_overlap_ms": overlap_ns / 1e6,
"nccl_overlap_pct": (
100.0 * overlap_ns / nccl_union_ns if nccl_union_ns else 0.0
),
"nccl_exposed_ms": (nccl_union_ns - overlap_ns) / 1e6,
"nccl_exposed_wall_pct": 100.0
* (nccl_union_ns - overlap_ns)
/ wall_ns,
"allreduce_count": len(allreduce_ms),
"allreduce_normal_count": len(normal_allreduce_ms),
"allreduce_mean_ms": (
sum(normal_allreduce_ms) / len(normal_allreduce_ms)
if normal_allreduce_ms
else math.nan
),
"allreduce_p50_ms": percentile(normal_allreduce_ms, 0.50),
"allreduce_p95_ms": percentile(normal_allreduce_ms, 0.95),
"allreduce_max_ms": max(allreduce_ms, default=math.nan),
"hidden_allreduce_count": len(hidden_allreduce_ms),
"hidden_allreduce_sum_ms": sum(hidden_allreduce_ms),
"hidden_allreduce_p50_ms": percentile(hidden_allreduce_ms, 0.50),
"moe_allreduce_count": len(moe_allreduce_ms),
"moe_allreduce_sum_ms": sum(moe_allreduce_ms),
"moe_allreduce_p50_ms": percentile(moe_allreduce_ms, 0.50),
"moe_allreduce_time_share_pct": (
100.0 * sum(moe_allreduce_ms) / normal_allreduce_total_ms
if normal_allreduce_total_ms
else 0.0
),
}
)
return output, top_names
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("result_root", type=Path)
args = parser.parse_args()
sqlite_paths = sorted((args.result_root / "nsys").glob("node*/*.sqlite"))
if len(sqlite_paths) != 4:
raise SystemExit(f"expected four SQLite files, found {len(sqlite_paths)}")
devices: list[dict] = []
global_names: dict[str, dict[str, float]] = defaultdict(
lambda: {"count": 0, "duration_ns": 0}
)
for path in sqlite_paths:
device_rows, names = analyze_sqlite(path)
devices.extend(device_rows)
for name, values in names.items():
global_names[name]["count"] += values["count"]
global_names[name]["duration_ns"] += values["duration_ns"]
stable_devices = [
row for row in devices if row["allreduce_max_ms"] < 50.0
]
metric_names = [
"wall_ms",
"gpu_busy_pct",
"nccl_union_ms",
"nccl_wall_pct",
"compute_union_ms",
"compute_wall_pct",
"nccl_compute_overlap_ms",
"nccl_overlap_pct",
"nccl_exposed_ms",
"nccl_exposed_wall_pct",
"allreduce_count",
"allreduce_mean_ms",
"allreduce_p50_ms",
"allreduce_p95_ms",
"hidden_allreduce_count",
"hidden_allreduce_sum_ms",
"hidden_allreduce_p50_ms",
"moe_allreduce_count",
"moe_allreduce_sum_ms",
"moe_allreduce_p50_ms",
"moe_allreduce_time_share_pct",
]
stable_medians = {
name: percentile([float(row[name]) for row in stable_devices], 0.50)
for name in metric_names
}
top_kernels = [
{
"name": name,
"count": int(values["count"]),
"duration_ms": values["duration_ns"] / 1e6,
}
for name, values in sorted(
global_names.items(),
key=lambda item: item[1]["duration_ns"],
reverse=True,
)[:40]
]
payload = {
"result_root": str(args.result_root),
"sqlite_files": [str(path) for path in sqlite_paths],
"devices": devices,
"stable_device_rule": "allreduce_max_ms < 50; excludes profiler trigger lanes",
"stable_device_count": len(stable_devices),
"stable_medians": stable_medians,
"top_kernels": top_kernels,
}
output_json = args.result_root / "nsys_analysis.json"
output_csv = args.result_root / "nsys_device_metrics.csv"
output_json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
with output_csv.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(devices[0]))
writer.writeheader()
writer.writerows(devices)
print(json.dumps(payload["stable_medians"], indent=2))
print(f"stable devices: {len(stable_devices)}/{len(devices)}")
print(f"wrote {output_json}")
print(f"wrote {output_csv}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,265 @@
#!/usr/bin/env bash
# Capture a short, stage-scoped Nsight Systems trace for Kimi-K3 prefill.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUN_ID="${RUN_ID:-kimi3-prefill-comm-$(date '+%Y%m%d-%H%M%S')}"
RESULT_BASE="${RESULT_BASE:-${SCRIPT_DIR}/results}"
RESULT_ROOT="${RESULT_BASE}/${RUN_ID}"
MODEL_PATH="${MODEL_PATH:-/data/hf_models/Kimi-K3}"
IMAGE="${IMAGE:-local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5}"
HEAD_HOST="${HEAD_HOST:-174.1.60.1}"
NODE_USER="${NODE_USER:-user}"
NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4)
PORT="${PORT:-30000}"
DIST_PORT="${DIST_PORT:-20000}"
PROFILE_STEPS="${PROFILE_STEPS:-3}"
CONCURRENCY="${CONCURRENCY:-8}"
NUM_PROMPTS="${NUM_PROMPTS:-40}"
PP_SIZE="${PP_SIZE:-1}"
TP_SIZE="${TP_SIZE:-32}"
EP_SIZE="${EP_SIZE:-4}"
CONTAINER_PREFIX="kimi3_prefill_comm_profile"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
mkdir -p "${RESULT_ROOT}"/{bench,nsys,service,gpu}
log() {
printf '[%(%F %T)T] %s\n' -1 "$*" | tee -a "${RESULT_ROOT}/orchestrator.log"
}
require_password() {
if [[ -z "${SUDO_PASSWORD:-}" && -n "${SUDO_PASSWORD_FILE:-}" ]]; then
IFS= read -r SUDO_PASSWORD <"${SUDO_PASSWORD_FILE}"
fi
[[ -n "${SUDO_PASSWORD:-}" ]] || {
echo "ERROR: set SUDO_PASSWORD or SUDO_PASSWORD_FILE" >&2
exit 2
}
}
is_head() {
[[ "$1" == "$HEAD_HOST" ]]
}
sudo_host() {
local host="$1"
shift
require_password
if is_head "$host"; then
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' -- "$@"
else
local command
printf -v command '%q ' "$@"
printf '%s\n' "$SUDO_PASSWORD" | ssh "${SSH_OPTS[@]}" "${NODE_USER}@${host}" \
"sudo -S -p '' -- ${command}"
fi
}
run_host() {
local host="$1"
shift
if is_head "$host"; then
"$@"
else
ssh "${SSH_OPTS[@]}" "${NODE_USER}@${host}" "$@"
fi
}
container_name() {
printf '%s_node%s' "$CONTAINER_PREFIX" "$1"
}
prepare_nodes() {
local host rank=0
for host in "${NODE_HOSTS[@]}"; do
run_host "$host" mkdir -p "${RESULT_ROOT}/nsys/node${rank}"
rank=$((rank + 1))
done
}
stop_service() {
local host rank=0
for host in "${NODE_HOSTS[@]}"; do
sudo_host "$host" docker rm -f "$(container_name "$rank")" >/dev/null 2>&1 || true
rank=$((rank + 1))
done
}
start_node() {
local rank="$1" host name output launch
host="${NODE_HOSTS[$rank]}"
name="$(container_name "$rank")"
output="${RESULT_ROOT}/nsys/node${rank}/prefill_node${rank}"
launch="export SGLANG_HOST_IP=174.1.60.$((rank + 1)); exec nsys profile --trace=cuda,nvtx,nccl --nccl-trace=api,group,gpu,coll,kernel-launch --sample=none --cpuctxsw=none --capture-range=cudaProfilerApi --capture-range-end=stop --cuda-graph-trace=node --force-overwrite=true --stats=true --output ${output} python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name kimi-k3 --tp-size ${TP_SIZE} --pp-size ${PP_SIZE} --ep-size ${EP_SIZE} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size 8192 --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}"
local -a command=(
docker run -d --name "$name"
--gpus all --network host --ipc=host --ulimit memlock=-1
--device /dev/infiniband --shm-size 32g --entrypoint bash
-v "${MODEL_PATH}:${MODEL_PATH}:ro"
-v "${RESULT_ROOT}:${RESULT_ROOT}"
-e CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
-e NCCL_SOCKET_IFNAME=bond0 -e GLOO_SOCKET_IFNAME=bond0
-e NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
-e NCCL_IB_GID_INDEX=3 -e NCCL_IB_TIMEOUT=22 -e NCCL_IB_RETRY_CNT=7
-e NCCL_CUMEM_ENABLE=1 -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
-e SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 -e SGLANG_MOE_FUSED_GATE_RADIX=1
-e FLASHINFER_DISABLE_JIT=1 -e FLASHINFER_DISABLE_VERSION_CHECK=1
"$IMAGE" -lc "$launch"
)
printf '%q ' "${command[@]}" >"${RESULT_ROOT}/service/node${rank}.cmd.txt"
printf '\n' >>"${RESULT_ROOT}/service/node${rank}.cmd.txt"
sudo_host "$host" "${command[@]}" >/dev/null
}
wait_health() {
local elapsed
for ((elapsed = 1; elapsed <= 2400; elapsed++)); do
if curl --fail --silent --max-time 5 "http://${HEAD_HOST}:${PORT}/health" >/dev/null 2>&1; then
log "service healthy after ${elapsed}s"
return 0
fi
if (( elapsed % 30 == 0 )); then
log "waiting for service: ${elapsed}s"
collect_logs starting
if grep -Eiq 'Traceback|CUDA out of memory|NCCL.*(error|failed)|RuntimeError' \
"${RESULT_ROOT}/service/starting_node"*.log; then
log "ERROR: startup log contains a fatal error"
return 1
fi
fi
sleep 1
done
return 1
}
collect_logs() {
local label="$1" host rank=0
for host in "${NODE_HOSTS[@]}"; do
sudo_host "$host" docker logs "$(container_name "$rank")" \
>"${RESULT_ROOT}/service/${label}_node${rank}.log" 2>&1 || true
rank=$((rank + 1))
done
}
collect_gpu() {
local label="$1" host rank=0
for host in "${NODE_HOSTS[@]}"; do
sudo_host "$host" nvidia-smi \
--query-gpu=timestamp,index,memory.used,memory.total,utilization.gpu,power.draw \
--format=csv,noheader,nounits \
>"${RESULT_ROOT}/gpu/${label}_node${rank}.csv" 2>&1 || true
rank=$((rank + 1))
done
}
run_bench() {
local label="$1" prompts="$2" concurrency="$3"
local output="${RESULT_ROOT}/bench/${label}.jsonl"
sudo_host "$HEAD_HOST" docker run --rm --network host \
-v "${MODEL_PATH}:${MODEL_PATH}:ro" -v "${RESULT_ROOT}:${RESULT_ROOT}" \
-e PYTHONUNBUFFERED=1 --entrypoint python3 "$IMAGE" \
-m sglang.benchmark.serving --backend sglang --host "$HEAD_HOST" --port "$PORT" \
--tokenizer "$MODEL_PATH" --dataset-name random-ids \
--random-input-len 16384 --random-output-len 1 --random-range-ratio 1.0 \
--num-prompts "$prompts" --max-concurrency "$concurrency" --request-rate 10000 \
--warmup-requests 0 --output-file "$output" --output-details --disable-tqdm \
>"${RESULT_ROOT}/bench/${label}.log" 2>&1
}
trigger_profile() {
local payload response
payload="{\"activities\":[\"CUDA_PROFILER\"],\"num_steps\":${PROFILE_STEPS},\"profile_by_stage\":true,\"profile_id\":\"${RUN_ID}\",\"profile_prefix\":\"prefill\"}"
printf '%s\n' "$payload" >"${RESULT_ROOT}/service/start_profile_request.json"
response="$(curl --fail --silent --show-error -X POST \
"http://${HEAD_HOST}:${PORT}/start_profile" \
-H 'Content-Type: application/json' -d "$payload")"
printf '%s\n' "$response" | tee "${RESULT_ROOT}/service/start_profile_response.json"
}
copy_worker_artifacts() {
local host rank
for rank in 1 2 3; do
host="${NODE_HOSTS[$rank]}"
printf '%s\n' "$SUDO_PASSWORD" | ssh "${SSH_OPTS[@]}" "${NODE_USER}@${host}" \
"sudo -S -p '' -- tar -C '${RESULT_ROOT}' -cf - 'nsys/node${rank}'" | \
tar -C "${RESULT_ROOT}" -xf -
done
}
write_manifest() {
{
printf 'run_id=%s\n' "$RUN_ID"
printf 'image=%s\n' "$IMAGE"
printf 'model=%s\n' "$MODEL_PATH"
printf 'pp=%s\ntp=%s\nep=%s\ndp=1\n' "$PP_SIZE" "$TP_SIZE" "$EP_SIZE"
printf 'moe_runner_backend=flashinfer_mxfp4\nmoe_a2a_backend=none\n'
printf 'input_len=16384\noutput_len=1\nconcurrency=%s\n' "$CONCURRENCY"
printf 'chunked_prefill_size=8192\nprofile_steps=%s\n' "$PROFILE_STEPS"
} >"${RESULT_ROOT}/manifest.env"
}
run_all() {
require_password
(( PP_SIZE * TP_SIZE == 32 )) || {
echo "ERROR: PP_SIZE * TP_SIZE must equal 32" >&2
return 2
}
(( TP_SIZE % EP_SIZE == 0 )) || {
echo "ERROR: TP_SIZE must be divisible by EP_SIZE" >&2
return 2
}
prepare_nodes
write_manifest
stop_service
collect_gpu before
log "starting PP${PP_SIZE}/TP${TP_SIZE}/EP${EP_SIZE}/FlashInfer service under Nsight"
start_node 1
start_node 2
start_node 3
sleep 5
start_node 0
wait_health
collect_logs healthy
collect_gpu healthy
log "warming kernels without profiling"
run_bench warmup 2 2
log "arming ${PROFILE_STEPS} Prefill steps"
trigger_profile
run_bench profile_c8 "$NUM_PROMPTS" "$CONCURRENCY"
collect_gpu profiled
collect_logs profiled
log "stopping services to finalize reports"
stop_service
sleep 10
copy_worker_artifacts
collect_gpu after
find "${RESULT_ROOT}/nsys" -type f -printf '%p %s bytes\n' | sort \
>"${RESULT_ROOT}/nsys/files.txt"
log "capture complete: ${RESULT_ROOT}"
}
cleanup() {
collect_logs cleanup 2>/dev/null || true
stop_service 2>/dev/null || true
}
case "${1:-run}" in
run)
trap cleanup EXIT INT TERM
run_all
trap - EXIT INT TERM
;;
stop)
require_password
stop_service
;;
*)
echo "Usage: SUDO_PASSWORD=... bash $0 [run|stop]" >&2
exit 2
;;
esac

View File

@ -0,0 +1,63 @@
# Kimi-K3 SM120 FlashInfer MXFP4 完成审计
审计时间2026-08-19。本文只记录 goal 的逐项验收状态;技术结论见
`UPSTREAM_DUPLICATION_AUDIT.md`,公开 PR 正文见 `SGLANG_DRAFT_PR.md`
## 验收矩阵
| 要求 | 状态 | 权威证据 |
|---|---|---|
| 固定 605 历史环境身份 | 已证明 | profile 为 `/data/yy/sskj/deploy/profiles/pro6000/kimi3_pro6000_sglang_tp32ep32_flashinfer.env`;镜像 `lmsysorg/sglang:kimi-k3-fiv617situ-warm`;两个 runtime patch SHA256 见去重审计 |
| 区分官方 0.6.17 与实际能力 | 已证明 | 605 profile 设置 `FLASHINFER_DISABLE_VERSION_CHECK=1` 并启动前执行两个补丁;报告未再声称 0.6.17 原版直接支持完整组合 |
| 逐文件比较 605 补丁、FlashInfer #4460、SGLang 上游 | 已证明 | `UPSTREAM_DUPLICATION_AUDIT.md` 的“上游已有/605 已有/我们新增/明确排除”四部分605 补丁目标已从原文件重新核验 |
| 不提交重复 FlashInfer kernel | 已满足 | FlashInfer 历史实现与测试仅作为本地研究记录SGLang Draft 不含 FlashInfer C++、依赖 pin 或 private capability marker |
| SGLang 仅保留真实增量 | 已满足 | SGLang base `593b1a9b8aee` 到 head `b95b5341ab95` 仅修改 5 个文件;补丁位于 `patches/sglang-kimi-k3-sm120-flashinfer-mxfp4-draft.patch` |
| 可应用 patch 与 Draft HEAD 一致 | 已证明 | 在全新 detached base `593b1a9b8aee` 应用仓库 patch 并 `git add -A` 后,暂存树与 `b95b5341ab95` 的 diff 为空 |
| Kimi gate/up 权重和 scale 布局 | 已证明 | 注册测试覆盖连续 `[gate; up]` 到 CUTLASS `[up; gate]`,并保留 GPT-OSS pair-interleaved 分支 |
| SiTU 4.0/25.0 参数透传 | 已证明 | #4460 ABI 的 SM120 GPU correctness 测试覆盖独立 `situ_beta``situ_linear_beta` |
| 非连续 hidden states | 已证明 | GPU correctness 测试输入为非连续 viewrunner 在 MXFP8 quantize 边界 materialize contiguous tensor |
| attn_residual SM120 guard | 已证明 | 注册架构测试覆盖 SM100/103/110 为 true、SM120/SM90 为 false |
| autotune 稳定性边界 | 已证明且不纳入 PR | 605 的 skip workaround 未进入 Draft正式 24 点候选矩阵未复现 autotune 崩溃,因此没有用历史 workaround 扩大 PR |
| 最终定向 correctness | 已通过 | RTX 6000D GPU6、#4460 snapshot`8 passed, 17 warnings in 559.87s`wheel SHA256 `deafdac2dc44b331441daba64e5b5dd28f591fe44f087af0aac3521c6ddbfa1f` |
| fork 同步与补丁等价性 | 已通过 | 旧 base/head `63d783b..300c87a` 重放到 `593b1a9..b95b534`7/7 commits 在 `git range-diff` 中均为 `=` |
| 最终兼容回归 | 已通过 | Draft HEAD `b95b534`compileall 通过,`10 passed, 3 deselected, 17 warnings in 33.30s` |
| 格式和 CI 注册 | 已通过 | 五个变更文件完整 pre-commit 全部通过;两个测试文件注册到 CUDA CI `base-b/1-gpu-small` |
| 保留 EP4 服务级结果 | 已证明 | 601 原始目录有 24 个正式 JSONL 点;每组 3 repeats、每点 40/40 成功;错误扫描为空;未重跑 EP32 |
| 当前 Draft 代表点 A/B | 已通过 | Run `kimi3-sm120-pr-representative-20260819-115530`Marlin/FlashInfer 各 3 次240/240 请求成功FlashInfer Input TPS +10.96%TTFT P50/P95 -9.91%/-9.92% |
| 公开 PR 标题、正文、依赖和已知限制 | Draft 正文已完成 | `SGLANG_DRAFT_PR.md` 遵循官方模板Checklist 全部勾选;标题为 `[Feature] Support Kimi-K3 FlashInfer MXFP4 MoE on SM120`Speed Tests 只报告一个目标硬件代表点,不作普遍加速声明 |
| GitHub fork 交付 | 已完成 | `ACEEE-1222/sglang:hzy/kimi-k3-sm120-flashinfer-mxfp4`Draft HEAD `b95b534`,远端 ref 已复核 |
| 内部仓库独立分支 | 已完成 | 601 本地分支 `hzy/feature-kimi-k3-sm120-flashinfer-mxfp4` 已推送到 `qqtang/sskj:hzy-kimi-k3-sm120-flashinfer-mxfp4`;远端已有普通 `hzy` ref因此不能使用 `hzy/...` 子分支名 |
## 原始证据位置
```text
# EP4 服务级原始结果601
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-full-20260818-151349/
/data/hzy/kimi3-ep4-moe-full-20260818-151349.log
# #4460 ABI correctness601
/data/hzy/phase5_sglang_pr4460_draft_gpu6_v4.log
/data/hzy/phase5_sglang_pr4460_draft_cpu_final.log
# 当前 SGLang Draft worktree601
/data/hzy/src/sglang-kimi-sm120-draft-wt
# 当前 Draft 代表点601
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/results/kimi3-sm120-pr-representative-20260819-115530/
/data/hzy/kimi3-sm120-pr-representative-20260819-115530.log
```
## 交付确认
```text
GitHub code branch:
ACEEE-1222/sglang:hzy/kimi-k3-sm120-flashinfer-mxfp4
Internal audit branch:
qqtang/sskj:hzy-kimi-k3-sm120-flashinfer-mxfp4
Internal compare:
https://git.meta-stone.net/qqtang/sskj/compare/main...hzy-kimi-k3-sm120-flashinfer-mxfp4
```
代码、代表点、原始证据索引、公开 PR 正文、可应用 patch 和两处远端分支均已完成。

View File

@ -0,0 +1,29 @@
ARG BASE_IMAGE=lmsysorg/sglang:kimi-k3-fiv617situ-warm
FROM ${BASE_IMAGE}
ARG SGLANG_COMMIT
ARG FLASHINFER_COMMIT
ENV FLASHINFER_DISABLE_VERSION_CHECK=1 \
PYTHONPATH=/sgl-workspace/sglang/python \
PYTHONUNBUFFERED=1
COPY flashinfer_python-0.6.18-py3-none-any.whl /tmp/
RUN python3 -m pip install --no-deps --force-reinstall \
/tmp/flashinfer_python-0.6.18-py3-none-any.whl && \
rm -f /tmp/*.whl
# Replace the image's older Python package with the exact Draft tree. Keep the
# base image's ABI-matched torch and sglang-kernel binaries: the Draft changes
# only the Python Kimi/FlashInfer integration and does not require a new kernel.
RUN rm -rf /sgl-workspace/sglang/python/sglang
COPY sglang/ /sgl-workspace/sglang/python/sglang/
COPY test_mxfp4_sm120_cutlass.py /opt/pr-tests/test_mxfp4_sm120_cutlass.py
COPY source_identity.txt /opt/pr-build/source_identity.txt
RUN python3 -m compileall -q /sgl-workspace/sglang/python/sglang && \
python3 -c "import inspect; from flashinfer.fused_moe import cutlass_fused_moe; from flashinfer.fused_moe.core import ActivationType; assert hasattr(ActivationType, 'Situ'); assert 'situ_beta' in inspect.signature(cutlass_fused_moe).parameters; import sglang.srt.layers.quantization.mxfp4"
LABEL ai.meta-stone.purpose="Kimi-K3 SM120 SGLang Draft representative validation" \
ai.meta-stone.sglang.commit="${SGLANG_COMMIT}" \
ai.meta-stone.flashinfer.commit="${FLASHINFER_COMMIT}"

View File

@ -0,0 +1,281 @@
# Kimi-K3 SM120 FlashInfer MXFP4 MoE
## 目标
为 Kimi-K3 在 NVIDIA SM120RTX 6000D上的 SGLang 部署补齐
FlashInfer MXFP8 activation × MXFP4 weight fused MoE 支持,并形成:
- 可上游审查的最小 SGLang 改动;
- 精确覆盖 Kimi-K3 的正确性测试;
- RTX 6000D 单卡与四节点真实服务证据;
- Marlin 与 FlashInfer 的同口径性能结论。
本目录不修改已有 Kimi-K3 基线实验,也不提交与 FlashInfer 上游重复的 kernel。
## 交付状态
状态:**完成SGLang Draft 代码与审计分支已推送。**
| 项目 | 结果 |
|---|---|
| GPU | NVIDIA RTX 6000DSM120 |
| 模型 | Kimi-K3MXFP4SiTU `(4.0, 25.0)` |
| SGLang base | `593b1a9b8aeedb908ee21870c02a0fd1a3dc30a5` |
| SGLang Draft HEAD | `b95b5341ab9555ba5ea53f5828f6de203d749cce` |
| GitHub 分支 | `ACEEE-1222/sglang:hzy/kimi-k3-sm120-flashinfer-mxfp4` |
| FlashInfer 依赖 | PR #4460implementation `b525c513fef7` |
| 默认行为 | 不改变默认 MoE backendMarlin 仍可用 |
公开 PR 正文:[`SGLANG_DRAFT_PR.md`](SGLANG_DRAFT_PR.md)
可应用补丁:
[`patches/sglang-kimi-k3-sm120-flashinfer-mxfp4-draft.patch`](patches/sglang-kimi-k3-sm120-flashinfer-mxfp4-draft.patch)
逐项验收:[`COMPLETION_AUDIT.md`](COMPLETION_AUDIT.md)
## 交付内容
### SGLang 实现
1. 为 Kimi-K3 在 SM120 上接入 FlashInfer CUTLASS MXFP4 MoE
2. 将 Kimi 连续 `[gate; up]` 权重与 scale 转换为 CUTLASS 所需布局;
3. 独立透传 `situ_beta=4.0``situ_linear_beta=25.0`
4. 在 MXFP8 量化前处理非连续 hidden states
5. 排除 SM120 误入 tcgen05 attention-residual 路径;
6. 保持 GPT-OSS 路径、旧 FlashInfer import 和默认 Marlin 行为不变。
变更文件:
```text
python/sglang/srt/layers/attn_residual.py
python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
python/sglang/srt/layers/quantization/mxfp4.py
test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py
test/registered/unit/layers/test_attn_residual_arch_gate.py
```
### 测试覆盖
- Kimi gate/up 权重和 scale 布局;
- SiTU `(4.0, 25.0)` 参数;
- MXFP8 activation × MXFP4 weight
- 非连续输入;
- standard routing 与 EP expert IDs
- eager、CUDA Graph capture/replay
- SM120 attention-residual architecture gate
- 缺少新 API 时的兼容性与明确报错。
## 上游边界
FlashInfer PR #4460 负责公共 CUTLASS SiTU kernel 和参数 API本工作不提交
平行的 FlashInfer kernel PR。
SGLang Draft 只保留 Kimi-K3 的真实框架增量:权重/scale 布局、SiTU 参数、
非连续输入、SM120 guard 和测试。它不 vendor kernel、不 pin 未合并私人 commit、
不改变默认 backend。
完整去重审计见:
[`UPSTREAM_DUPLICATION_AUDIT.md`](UPSTREAM_DUPLICATION_AUDIT.md)
## 正确性结果
使用 FlashInfer #4460 构建的 wheel在 RTX 6000D GPU6 上执行精确 Kimi 组合:
```text
8 passed, 17 warnings in 559.87s
```
覆盖 `SM120 + MXFP8 + MXFP4 + SiTU(4,25)`、Kimi 布局、非连续输入和
CUDA Graph。Kimi 真尺寸结果:
| Case | cosine | NRMSE |
|---|---:|---:|
| Decode M=1 | 0.9996223 | 2.748% |
| Prefill M=128 | 0.9996152 | 2.774% |
最终 Draft HEAD 的 compileall 和非 JIT 回归:
```text
10 passed, 3 deselected, 17 warnings in 33.30s
```
五个变更文件已通过完整 pre-commit。同步 fork 后的 7 个提交与同步前版本在
`git range-diff` 中全部 patch-equivalent。
原始日志:
```text
/data/hzy/phase5_sglang_pr4460_draft_gpu6_v4.log
/data/hzy/phase5_sglang_pr4460_draft_cpu_final.log
```
## 四节点真实 Serving
### 实验口径
| 维度 | 取值 |
|---|---|
| 节点 | 601-604共 32 张 RTX 6000D |
| 并行 | TP32 / EP4 / DP1 |
| 输入/输出 | 16K / 1 token |
| 并发 | C=8、C=16 |
| Backend | Marlin、FlashInfer MXFP4 |
| Chunk | 4K、8K、16K |
| 请求与重复 | 每点 40 请求3 次重复 |
| Cache | 禁用 Radix Cache |
正式 Run
```text
8K/16K: kimi3-ep4-moe-full-20260818-151349
4K: kimi3-ep4-moe-chunk4k-20260819-150632
```
36/36 repeat 均完成 40/40 请求,输出长度均为 1`errors` 为空;日志未发现
OOM、Traceback、NCCL error 或 EngineDeadError。下表为三次重复的中位数。
### 实验结果
| Chunk | C | Backend | Input TPS | TTFT P50 | TTFT P95 | FlashInfer vs Marlin |
|---:|---:|---|---:|---:|---:|---:|
| 4K | 8 | Marlin | 2694.53 | 48.86 s | 49.20 s | baseline |
| 4K | 8 | FlashInfer | **2957.23** | **44.48 s** | **44.86 s** | +9.75% TPS / -8.96% TTFT |
| 4K | 16 | Marlin | 2692.60 | 96.25 s | 97.96 s | baseline |
| 4K | 16 | FlashInfer | **2960.41** | **87.61 s** | **89.04 s** | +9.95% TPS / -8.98% TTFT |
| 8K | 8 | Marlin | 2935.02 | 43.51 s | 46.22 s | baseline |
| 8K | 8 | FlashInfer | **3257.96** | **39.19 s** | **41.62 s** | +11.00% TPS / -9.92% TTFT |
| 8K | 16 | Marlin | 2934.90 | 87.09 s | 89.86 s | baseline |
| 8K | 16 | FlashInfer | **3260.14** | **78.39 s** | **80.88 s** | +11.08% TPS / -9.99% TTFT |
| 16K | 8 | Marlin | 2812.91 | 45.47 s | 45.86 s | baseline |
| 16K | 8 | FlashInfer | **3027.50** | **42.27 s** | **42.63 s** | +7.63% TPS / -7.04% TTFT |
| 16K | 16 | Marlin | 2812.33 | 90.97 s | 91.36 s | baseline |
| 16K | 16 | FlashInfer | **3027.34** | **84.52 s** | **84.85 s** | +7.65% TPS / -7.09% TTFT |
### 结果结论
1. FlashInfer 在 6 个正式 shape 上均优于 Marlin
2. 4K Chunk 的收益约为 `+9.8% Input TPS / -9.0% TTFT`
3. 8K Chunk 的收益约为 `+11% Input TPS / -10% TTFT`
4. 16K Chunk 的收益约为 `+7.6% Input TPS / -7.1% TTFT`
5. 同一 backend 下8K Chunk 同时优于 4K 和 16K4K 切分轮次过多16K 单步过重;
6. C 从 8 增至 16 后吞吐基本不变、TTFT 约翻倍C=8 已接近 Prefill 吞吐饱和;
7. 当前推荐为 `EP4 + flashinfer_mxfp4 + chunked_prefill_size=8192`
该优化有效但不是数量级变化。最佳 TTFT P50 仍约 39 秒,后续需要继续优化
TP/EP 通信与 Prefill 并行结构。
### Chunk=4K 补测结论
4K 补测复用了 8K/16K 矩阵的镜像、模型、服务参数和入口。12/12 repeat
全部有效,共 480/480 请求成功。
FlashInfer 在 C=8/16 下仍分别提升 `9.75%/9.95%` Input TPS但 4K 的绝对
Input TPS 比 8K 低约 9%TTFT 也更高。该结果补齐了 Chunk 曲线并确认 8K
不是网格边界造成的偶然最优点。
## 最终 PR 代表点
最终又用等价 SGLang patch `300c87a` 和 FlashInfer #4460 implementation
`b525c513` 重跑 8K/C8 代表点:
```text
Run ID: kimi3-sm120-pr-representative-20260819-115530
TP32 / EP4 / DP1
ISL=16384 / OSL=1 / C=8 / requests=40 / chunk=8192
Marlin 3 repeats + FlashInfer MXFP4 3 repeats
```
| Backend | Input TPS | TTFT P50 | TTFT P95 |
|---|---:|---:|---:|
| Marlin | 2930.03 | 43.587 s | 46.287 s |
| FlashInfer MXFP4 | 3251.25 | 39.267 s | 41.694 s |
| 相对变化 | +10.96% | -9.91% | -9.92% |
六轮均为 40/40 成功,所有 `output_lens=1`,无 OOM、Traceback、NCCL error
或 EngineDeadError。
## 复现命令
四节点矩阵只在 601 执行:
```bash
cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill
read -rsp 'sudo password: ' SUDO_PASSWORD; echo
export SUDO_PASSWORD
export RUN_ID=kimi3-moe-prefill-$(date +%Y%m%d-%H%M%S)
bash run_moe_backend_prefill.sh run
```
可通过以下环境变量缩小矩阵:
```text
EP_SIZE
BACKENDS_CSV
CHUNKED_PREFILL_SIZES_CSV
CONCURRENCIES_CSV
REPEATS
```
4K 补测使用:
```bash
EP_SIZE=4 \
BACKENDS_CSV=marlin,flashinfer_mxfp4 \
CHUNKED_PREFILL_SIZES_CSV=4096 \
CONCURRENCIES_CSV=8,16 \
REPEATS=3 \
bash run_moe_backend_prefill.sh run
```
## 证据路径
### 8K/16K 正式矩阵
```text
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-full-20260818-151349/
/data/hzy/kimi3-ep4-moe-full-20260818-151349.log
results/phase5_ep4_moe_backend_prefill_summary.json
results/phase5_ep4_moe_backend_prefill_results.csv
```
### 4K 补测
```text
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-chunk4k-20260819-150632/
/data/hzy/kimi3-ep4-moe-chunk4k-20260819-150632.log
results/phase5_ep4_moe_backend_chunk4k_summary.json
results/phase5_ep4_moe_backend_chunk4k_results.csv
```
### 最终 PR 代表点
```text
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/results/kimi3-sm120-pr-representative-20260819-115530/
/data/hzy/kimi3-sm120-pr-representative-20260819-115530.log
results/pr_representative_16k_c8_chunk8k_summary.json
results/pr_representative_16k_c8_chunk8k_results.csv
```
### 单卡测试与 Profiler
```text
results/phase3_target_situ_all_gpu6_v3.json
results/phase4_mscan_ep32_5rounds_gpu6.json
results/phase4_nsys_ab_gpu6.nsys-rep
results/phase4_ncu_2026_2_1_ab_gpu6.ncu-rep
```
## 后续方向
MoE backend 选型完成后,不再扩大同类 backend 网格。后续 Prefill 优化按独立
任务推进:
1. TP Reduce Scatter / Attention TP input scattered
2. MoE A2A
3. Prefill Pipeline Parallelism
4. 对最优新配置重新做 Nsight 时间线验证。

View File

@ -0,0 +1,145 @@
# [Feature] Support Kimi-K3 FlashInfer MXFP4 MoE on SM120
> Draft: depends on flashinfer-ai/flashinfer#4460. The dependency is not
> vendored or pinned by this PR, and the default MoE backend is unchanged.
## Motivation
SGLang already supports the FlashInfer CUTLASS MXFP4 MoE path for GPT-OSS on
SM120, but Kimi-K3 needs additional model-specific integration:
- Kimi stores gate and up projections as contiguous halves, while the CUTLASS
kernel consumes the opposite half order. This differs from GPT-OSS's
pair-interleaved layout.
- Kimi-K3 uses SiTU with independent parameters
`situ_beta=4.0` and `situ_linear_beta=25.0`.
- Chunked Prefill can pass non-contiguous hidden states to the MXFP8
quantizer.
- RTX 6000D (SM120) must not enter the tcgen05 fused attention-residual path
intended for GB100/GB200/GB300-class architectures.
FlashInfer PR #4460 owns the public CUTLASS SiTU kernel and API. This PR only
adds the SGLang-side Kimi-K3 integration and should remain Draft until #4460
is available in an official RC or release.
## Modifications
- Detect the public FlashInfer CUTLASS SiTU capability without making the new
enum or function parameters mandatory at SGLang import time.
- Dispatch Kimi-K3 MXFP8-activation-by-MXFP4-weight MoE to FlashInfer CUTLASS
on SM120 only when `--moe-runner-backend flashinfer_mxfp4` is selected.
- Convert Kimi's contiguous `[gate; up]` weight and scale halves to CUTLASS's
`[up; gate]` order while preserving the existing GPT-OSS interleaved path.
- Forward `situ_beta` and `situ_linear_beta` independently through the
FlashInfer #4460 API.
- Materialize non-contiguous hidden states immediately before MXFP8
quantization.
- Exclude SM120 from the tcgen05 attention-residual architecture gate.
- Add registered unit tests for capability fallback, legacy FlashInfer
compatibility, Kimi/GPT-OSS layouts, SiTU parameters, non-contiguous input,
numerical parity, and the architecture gate.
The PR does not copy FlashInfer C++ code, pin an unmerged commit, alter the
default MoE backend, or remove the Marlin fallback.
## Accuracy Tests
Target hardware: NVIDIA RTX 6000D, compute capability 12.0. FlashInfer was
built from a snapshot of PR #4460 fetched on 2026-08-18.
```bash
pytest -q -s \
test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py \
test/registered/unit/layers/test_attn_residual_arch_gate.py
```
Result with the FlashInfer kernel enabled:
```text
8 passed, 17 warnings in 559.87s
```
The GPU test covers the exact Kimi combination of SM120, MXFP8 activations,
MXFP4 weights, SiTU `(4.0, 25.0)`, Kimi gate/up layout, and non-contiguous
input. It compares the SGLang adapter output directly with the public
FlashInfer CUTLASS API.
After syncing the fork and rebasing the seven-patch series onto SGLang main
`593b1a9b8aeedb908ee21870c02a0fd1a3dc30a5`, the non-JIT subset was rerun
from the final Draft head `b95b5341ab9555ba5ea53f5828f6de203d749cce`:
```bash
pytest -q -s \
test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py \
test/registered/unit/layers/test_attn_residual_arch_gate.py \
-k "not matches_direct_flashinfer and not padding_layout_and_kernel and not situ_layout_and_noncontiguous_input"
```
```text
10 passed, 3 deselected, 17 warnings in 33.30s
```
## Speed Tests and Profiling
This is a compatibility PR, so the following result is a target-hardware
validation point rather than a general speed claim. Marlin and
`flashinfer_mxfp4` used the same SGLang commit, image, model, service flags,
requests, and three-repeat protocol.
```text
Hardware: 4 nodes / 32 NVIDIA RTX 6000D GPUs (SM120)
Model: Kimi-K3
Parallelism: TP32 / EP4 / DP1
Workload: ISL=16384, OSL=1, concurrency=8, 40 requests
Chunked Prefill: 8192 tokens
SGLang patch series used for the run: 300c87a431ac40d3e7817246376b7fe20932db09
Final rebased Draft head: b95b5341ab9555ba5ea53f5828f6de203d749cce
Final SGLang main base: 593b1a9b8aeedb908ee21870c02a0fd1a3dc30a5
FlashInfer: PR #4460 implementation b525c513fef7c1d162efd3ce8636fcc8760de66a
Repeats: 3 per backend
```
The service benchmark was collected before the fork sync. `git range-diff`
reports all seven commits as patch-equivalent after the rebase, and the
directed tests above were rerun from the final Draft head. The performance
result is therefore retained without claiming that the benchmark container
contained the new commit object.
| Backend | Median Input TPS | Median TTFT P50 | Median TTFT P95 |
|---|---:|---:|---:|
| Marlin | 2930.03 | 43.587 s | 46.287 s |
| FlashInfer MXFP4 | 3251.25 | 39.267 s | 41.694 s |
| Change | +10.96% | -9.91% | -9.92% |
All six repeats completed 40/40 requests, all outputs contained exactly one
token, and no request errors, OOM, traceback, NCCL error, or engine-death
event was observed. The result only establishes this exact configuration;
the default backend remains unchanged.
## Checklist
- [x] Format your code according to the [Format code with pre-commit](https://docs.sglang.io/developer_guide/contribution_guide.html#format-code-with-pre-commit).
- [x] Add unit tests according to the [Run and add unit tests](https://docs.sglang.io/developer_guide/contribution_guide.html#run-and-add-unit-tests).
- [x] Update documentation according to [Write documentations](https://docs.sglang.io/developer_guide/contribution_guide.html#write-documentations). (N/A for this Draft: it does not expose a stable released capability or change defaults; user-facing documentation should be added after #4460 is released.)
- [x] Provide accuracy and speed benchmark results according to [Test the accuracy](https://docs.sglang.io/developer_guide/contribution_guide.html#test-the-accuracy) and [Benchmark the speed](https://docs.sglang.io/developer_guide/contribution_guide.html#benchmark-the-speed).
- [x] Follow the SGLang code style [guidance](https://docs.sglang.io/developer_guide/contribution_guide.html#code-style-guidance).
Full pre-commit validation was run on all five changed files and every hook
passed. The two test files are registered with CUDA CI in `base-b` using the
`1-gpu-small` runner.
## Dependency and Merge Order
1. FlashInfer #4460 merges and publishes an RC or release with a stable API.
2. This Draft updates its version gate to that released package if required.
3. SGLang CI runs with the released FlashInfer dependency.
4. The Draft becomes Ready for Review.
The SM120 attention-residual guard is kept as a separate commit so reviewers
can split it into a `[BugFix]` PR if preferred.
References:
- https://github.com/flashinfer-ai/flashinfer/pull/4460
- https://github.com/sgl-project/sglang/pull/24816
- https://github.com/sgl-project/sglang/pull/33997

View File

@ -0,0 +1,178 @@
# Kimi-K3 SM120 FlashInfer MXFP4 去重审计
审计时间2026-08-19 13:07:48 CST。
## 结论
FlashInfer CUTLASS SiTU kernel 不再作为我们的独立实现提交。真正的上游
kernel 工作是尚未合并的 FlashInfer PR #4460。我们不提交 FlashInfer PR
只在本地将精确 Kimi 测试作为依赖验证SM120、MXFP8 activation、MXFP4
weight、SiTU `4.0/25.0` 和 Kimi shape。
SGLang 仍有独立增量Kimi-K3 的 gate/up 权重与 scale 布局、SiTU 参数映射、
非连续 hidden states、SM120 backend 分发,以及阻止 SM120 误入 tcgen05
attention-residual 路径。SGLang 改动必须显式依赖 FlashInfer #4460,不能通过
runtime monkey patch 或私有 capability marker 假装 0.6.17 已完整支持。
## 上游已有内容
| 项目 | 状态 | 已覆盖 | 未覆盖或边界 |
|---|---|---|---|
| FlashInfer #4180 | 已合并 | TRTLLM-gen MoE 的 SiTU | 不是 CUTLASS SM120 MXFP8×MXFP4 路径 |
| FlashInfer #4209 | 开放、未合并 | 早期参数化 SiTU 方案 | 与 #4460 竞争;未作为当前依赖方向 |
| FlashInfer #4460 | 开放、未合并;维护者已 approveCI 22/22 | CUTLASS `ActivationType.Situ``situ_beta``situ_linear_beta`,覆盖 SM89/90/100/103/120 | 现有 `test_moe_mxfp8_mxfp4` 未明确覆盖 Kimi SiTU 4/25 |
| SGLang #32668 | 已合并 | GPT-OSS SM120 FlashInfer MXFP4 | GPT-OSS 是 pair-interleaved gate/up不是 Kimi 连续半区布局,也没有 Kimi SiTU 接入 |
| SGLang #32650 | 已关闭、未合并 | Kimi SM90 参数化 SiTU 草案 | 采用 #4209 风格接口,不是当前 #4460 ABI没有 SM120 服务级验收 |
关键链接:
- <https://github.com/flashinfer-ai/flashinfer/pull/4460>
- <https://github.com/flashinfer-ai/flashinfer/pull/4209>
- <https://github.com/flashinfer-ai/flashinfer/pull/4180>
- <https://github.com/sgl-project/sglang/pull/32668>
- <https://github.com/sgl-project/sglang/pull/32650>
## 605 已有方案
605 使用的不是“官方 FlashInfer 0.6.17 开箱即用”,而是旧 SGLang 镜像加
运行时移植:
```text
profile: /data/yy/sskj/deploy/profiles/pro6000/kimi3_pro6000_sglang_tp32ep32_flashinfer.env
image: lmsysorg/sglang:kimi-k3-fiv617situ-warm
image: sha256:66f5e4ba1da183621c3ca01ebb2b699e04373a2680085291f5840b01bdfe0e2c
SGLang: 0.5.16
flashinfer-python: 0.6.17
flashinfer-cubin: 0.6.15.post1
flashinfer-jit-cache: 0.6.15.post1+cu130
```
它设置 `FLASHINFER_DISABLE_VERSION_CHECK=1`,并在启动前执行:
```text
/tmp/patch_k3_sm120.py
/tmp/patch_k3_flashinfer_sm120.py
```
补丁 SHA256
```text
patch_k3_sm120.py:
30975b43bd9abda98eefb5d9f5a6a39c319a8d1cce9c3b298a73a42ce3db5166
patch_k3_flashinfer_sm120.py:
202fe03ff800dab06b45fde24ef8b870d5e1021fe53ea145e5ce8df1eee50ecb
```
`patch_k3_flashinfer_sm120.py` 明确写明移植 #4460,并额外处理 Kimi 布局、
非连续输入和 SM120 autotune workaround。`patch_k3_sm120.py` 修复
attention residual 架构 gate。它们证明功能方向可行但 runtime 文本替换、
混合版本包和跳过 autotune 不适合作为上游最终实现。
605 模型配置实测为:
```text
activation_situ_beta = 4.0
activation_situ_linear_beta = 25.0
```
## 我们保留的新增内容
| 层次 | 新增内容 | 证据或提交 |
|---|---|---|
| SGLang | 按 `gate_up_interleaved` 区分 GPT-OSS pair-interleaved 与 Kimi `[gate; up]` 连续半区,并转换为 CUTLASS `[up; gate]` | `cab839c` |
| SGLang | Kimi SiTU 映射到 #4460`ActivationType.Situ``situ_beta=4.0``situ_linear_beta=25.0` | `2adc099` |
| SGLang | `mxfp8_quantize` 前 materialize 非连续 Prefill hidden states | `35a9612` |
| SGLang | 禁止 SM120 进入 GB100/GB200/GB300 tcgen05 attention-residual 路径 | `5e61458` |
| 本地依赖验证 | 在 #4460 之上运行精确 MXFP8×MXFP4 + Kimi SiTU 4/25 + SM120 case | 本地提交 `542310c`;不提交 FlashInfer PR |
| 6000D 验收 | 601-604、TP32/EP4、16K→1、C8/C16、Chunk 8K/16K、三次重复 | `kimi3-ep4-moe-full-20260818-151349` |
| 当前 Draft 代表点 | 精确 SGLang `300c87a` + #4460 `b525c513`TP32/EP4、16K→1、C8、Chunk 8K各三次 | `kimi3-sm120-pr-representative-20260819-115530` |
## 明确排除的重复内容
- FlashInfer 历史提交 `7d1a4ee` 是平行 SiTU kernel实现与 #4460 高度
重合,不进入 PR。
- 历史测试提交 `e0bc653` 使用旧 SwiGLU 字段承载 4/25已由基于 #4460
独立字段的 `542310c` 取代。
- 605 的 `skip_ops` autotune workaround 不默认进入 SGLang PR。我们的候选
镜像完成 autotune 和 24/24 服务矩阵;只有在可复现崩溃时才另报问题。
- 不把 FlashInfer 0.6.17 的版本号当成功能证明。完整组合依赖未合并的
#4460 或等价后续合并版本。
## 固定实验身份与原始证据
SGLang Draft 的完整可审查 diff
```text
base: 593b1a9b8aeedb908ee21870c02a0fd1a3dc30a5
head: b95b5341ab9555ba5ea53f5828f6de203d749cce
patch: patches/sglang-kimi-k3-sm120-flashinfer-mxfp4-draft.patch
```
该 7 提交系列由旧范围 `63d783b..300c87a` 重放到用户新同步的 main
`git range-diff` 逐项均为 `=`。在新 base 的独立 worktree 应用上述 patch、
暂存全部文件后,与 `b95b534` 比较的 diff 为空。
GitHub 交付分支:
```text
fork: ACEEE-1222/sglang
branch: hzy/kimi-k3-sm120-flashinfer-mxfp4
compare: https://github.com/sgl-project/sglang/compare/main...ACEEE-1222:sglang:hzy/kimi-k3-sm120-flashinfer-mxfp4?expand=1
```
候选镜像:
```text
local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5
sha256:0b8e8ddceccecc0daf10b59ddd4177036affc81facc8bf87ea9e2456ab1e96b9
SGLang 0.5.16
flashinfer-python 0.6.18(历史候选源码 wheel
flashinfer-cubin 0.6.15.post1
flashinfer-jit-cache 0.6.15.post1+cu130
```
原始服务级结果:
```text
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-full-20260818-151349/
/data/hzy/kimi3-ep4-moe-full-20260818-151349.log
```
原始 GPU6 correctness 日志:
```text
/data/hzy/phase5_sglang_noncontiguous_gpu6_v2.log
/data/hzy/phase5_flashinfer_official_situ_gpu6.log
/data/hzy/phase5_sglang_pr4460_draft_gpu6_v4.log
/data/hzy/phase5_sglang_pr4460_draft_cpu_final.log
```
最终组合验证已在 601 的 GPU6 完成。测试从本地 #4460 工作树构建
`flashinfer-python 0.6.18` wheel再加载当前 SGLang Draft 源码:
```text
wheel sha256: deafdac2dc44b331441daba64e5b5dd28f591fe44f087af0aac3521c6ddbfa1f
result: 8 passed, 17 warnings in 559.87s
GPU: NVIDIA RTX 6000D, SM120
```
最终审查发现普通 SwiGLU 路径也曾无条件访问 `ActivationType.Situ`,可能误伤
旧 FlashInfer。提交 `775aa076` 改为动态加入 SiTU enum并新增旧版兼容回归。
同步前提交 `300c87a` 为两个测试文件增加正式 CUDA CI 注册,并通过完整
pre-commit。同步后的最终 HEAD `b95b534` 在 601 通过 compileall能力探测、
旧版兼容和架构 gate 为 `10 passed, 3 deselected, 17 warnings in 33.30s`
GPU kernel 路径继续由前述 8/8 测试覆盖。
完整 pre-commit 命令覆盖全部五个变更文件isort、ruff、black、codespell、
registered-tests 检查及其他 hook 全部通过。下载 GitHub hook 时只对该命令使用
HTTPS-to-SSH URL rewrite没有修改全局 Git 配置。
最终代表点使用同步前等价 patch `300c87a`#4460 implementation commit 的统一组合,
六轮 240/240 请求成功。FlashInfer 在所有 32 rank 完成 autotune因此 605 的
skip-autotune workaround 仍不属于 SGLang PR 增量。该固定点的 FlashInfer
Input TPS 为 Marlin 的 `+10.96%`TTFT P50/P95 为 `-9.91%/-9.92%`;这只作为
SM120 集成的服务级证据,不扩展为所有 shape 的性能结论。
前两次失败日志分别暴露 `FLASHINFER_DISABLE_JIT=0` 仍被视为启用、源码包缺少
生成的 `data/csrc`;第三次暴露只读源码不能生成 `_build_meta.py`。这些都是
测试装配问题,最终 v4 采用可写临时源码树构建 wheel 后通过,并非功能回归。

View File

@ -0,0 +1,197 @@
# Kimi-K3 SM120 FlashInfer MXFP4 Decision Log
本文件记录可恢复的关键决策、失败尝试和证据路径。成功结论见 `README.md`
## 2026-08-19 fork 同步与最终交付
- 用户重新同步 `ACEEE-1222/sglang:main` 后,最终 base 为 `593b1a9b8aee`
- 原 7 提交系列从 `63d783b..300c87a` 无冲突重放为
`593b1a9..b95b534``git range-diff` 的 7 项全部为 `=`,没有语义改动。
- 最终 HEAD 在 601 统一验证镜像中通过 compileall 和定向非 JIT 回归:
`10 passed, 3 deselected, 17 warnings in 33.30s`
- GitHub feature 分支通过精确 `force-with-lease``300c87a` 更新到
`b95b534`,随后用 `ls-remote` 复核远端 ref。
- 四机代表点不重跑。其容器记录的是同步前 `300c87a`,但对应 patch 与最终
HEAD 逐提交等价PR 正文同时列出实验 patch SHA 和最终 HEAD不混淆对象。
## 2026-08-19 最终 PR 代表点
- 只补跑一个代表点,不重跑 EP32TP32/EP4、16K→1、C=8、Chunk=8K
Marlin/FlashInfer 各三次Run ID
`kimi3-sm120-pr-representative-20260819-115530`
- 当前组合固定为 SGLang `300c87a431ac`、FlashInfer #4460 实现
`b525c513fef7`;精确 FlashInfer wheel SHA256 为
`5c67841b0bc5aa72d04910ee1cb727ad5fc9c13b5b80c14e26ac40de1583ce88`
- 六轮 240/240 请求成功,输出长度全部为 1错误扫描为空。
- 三次中位数Marlin Input TPS `2930.03`、TTFT P50/P95
`43.587/46.287 s`FlashInfer 为 `3251.25``39.267/41.694 s`
- 该点证明当前 Draft 组合在目标硬件可用,并在这一固定 shape 上相对 Marlin
为 Input TPS `+10.96%`、TTFT P50/P95 `-9.91%/-9.92%`;不外推到其他
shape也不改变默认 backend。
- 第一次离线汇总误报 `expected 3 repeats`,根因是 benchmark JSON 的
`backend=sglang` 覆盖了文件名解析出的 MoE backend修复字段合并顺序后
原始六轮数据直接生成最终汇总,没有重跑或删除结果。
- 公开 PR 的 Checklist 全部勾选Speed Tests 仅报告该代表点,仍把 PR 定位为
compatibility support。
## 2026-08-18 上游去重审计
- FlashInfer #4460 已覆盖 CUTLASS SiTU kernel、`ActivationType.Situ` 和独立
`situ_beta/situ_linear_beta`;历史独立 kernel 提交 `7d1a4ee` 不提交。
- FlashInfer 侧只在本地保留基于 #4460 的精确 Kimi
MXFP8×MXFP4+SiTU 4/25 依赖验证;
旧测试 `e0bc653` 由新测试提交 `542310c` 取代。
- SGLang 接入改为公开 API 能力检查,不再依赖自造
`CUTLASS_FUSED_MOE_SUPPORTS_SITU` marker也不再借用 SwiGLU 字段传 4/25。
- 605 的 runtime patches 作为历史可行性证据,不直接移植;其中 autotune
skip 只有在重新复现崩溃后才考虑单独修复。
- EP4 24/24 服务实验不丢弃、不重跑 EP32。它证明候选组合可用且相对
Marlin 有收益,但最终上游 correctness 必须在 #4460 ABI 上重跑。
- 详细矩阵、版本、SHA256 和原始路径见 `UPSTREAM_DUPLICATION_AUDIT.md`
### 最终 PR 边界与 #4460 验证
- 不提交 FlashInfer PR本地 `542310c` 仅用于验证 #4460 对 Kimi 精确组合的
correctness不作为上游贡献。
- SGLang 只开 Draft PR明确依赖 FlashInfer #4460;不 vendor kernel、不在依赖
文件 pin 未合并 commit也不改变默认 MoE backend。
- 缺少 CUTLASS SiTU API 时,普通 SGLang import 和默认 Marlin 路径不受影响;
只有显式选择 Kimi SM120 `flashinfer_mxfp4` 时给出升级或回退错误。
- 601 GPU6 使用 #4460 构建的 FlashInfer 0.6.18 wheel 跑当前 SGLang Draft
`8 passed, 17 warnings in 559.87s`。正式日志:
`/data/hzy/phase5_sglang_pr4460_draft_gpu6_v4.log`
- 最终审查发现普通 SwiGLU 路径也曾无条件访问 `ActivationType.Situ`
`775aa076` 改为动态 enum 检测并补旧 FlashInfer 兼容回归。最终提交
`300c87a` 增加 CUDA CI 注册并通过全部 pre-commit hook。能力探测、旧版兼容
和 SM120 架构 gate 为 `8 passed, 5 deselected, 17 warnings in 22.23s`;日志:
`/data/hzy/phase5_sglang_pr4460_draft_cpu_final.log`
- 公开 Draft PR 正文遵循 SGLang 官方模板,不暴露内网原始路径;原始日志路径、
镜像 SHA、wheel SHA 和去重证据继续保留在本仓库审计文件中。
- Checklist 在最终代表点和内部档案完成后全部勾选。公开 PR 仅报告一个固定
shape 的目标硬件结果,不做普遍提速声明;用户文档仍等待 #4460 发布稳定 API。
## 2026-08-18 Phase 5
### 真实 serving 暴露的兼容问题
- Kimi 基线镜像早于通用 `MoeRunnerConfig.gemm1_beta` 字段,首次
FlashInfer EP4 启动在权重处理阶段抛出 `AttributeError`。镜像兼容补丁
改为 `getattr(..., "gemm1_beta", None)`SiTU 的 beta 仍从 Kimi 已有的
`gemm1_clamp_limit=25` 读取;没有改变新版本源码的通用字段语义。
- 第二次启动完成权重和 KV cache 初始化后autotune 收到非连续 Prefill
hidden states`mxfp8_quantize` 抛出 `RuntimeError: input must be contiguous`
修复位于 FlashInfer runner 边界,在量化前执行 `x.contiguous()`;正式
24 点矩阵验证了该路径。
- benchmark 容器最初尝试访问 Hugging Face。入口增加
`--tokenizer /data/hf_models/Kimi-K3`,正式矩阵完全使用本地 tokenizer。
### EP4 正式矩阵
- Run ID`kimi3-ep4-moe-full-20260818-151349`
- 口径TP32/EP4/DP116K 输入、1 token 输出、40 requests
Marlin/FlashInfer MXFP4chunk 8K/16KC8/C16每点三次。
- 结果24/24 raw JSONL 均为 40/40 成功且 errors 为空;服务与 bench 日志
未发现 OOM、Traceback、NCCL error 或 EngineDeadError。
- 8K chunkFlashInfer Input TPS 提升 11.00%-11.08%TTFT P50 降低
9.92%-9.99%。
- 16K chunkFlashInfer Input TPS 提升 7.63%-7.65%TTFT P50 降低
7.04%-7.09%。
- 决策:该 16K/1 Prefill 工作负载采用 EP4 + FlashInfer MXFP4 + 8K chunk。
### EP32 证据处理
- 不补跑 EP32。仅复用已有 `kimi3-moe-prefill-20260818-130900`
Marlin/8K/C8 两次结果。
- EP32 中位数为 Input TPS 2531.43、TTFT P50 50.52 sEP4 三次中位数为
2935.02、43.51 s即 EP4 吞吐 +15.94%、TTFT P50 -13.88%。
- EP32 只有两次重复,因此只作为方向性同口径对照,不替代 EP4 正式矩阵。
### 原始证据与清理
```text
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-full-20260818-151349/
/data/hzy/kimi3-ep4-moe-full-20260818-151349.log
```
仓库内保留小型汇总:
```text
results/phase5_ep4_moe_backend_prefill_summary.json
results/phase5_ep4_moe_backend_prefill_results.csv
```
运行结束后 601-604 无实验容器32 张 GPU 均为 0 MiB / 0% utilization。
### PR 回归补充
- SGLang runner 增加非连续输入回归。第一次测试把
`expected=torch.empty_like(noncontiguous_x)` 也创建成非连续 stride底层
kernel 按连续输出 ABI 写入后被测试错误解读导致数值错位。reference
输出改为 `memory_format=torch.contiguous_format` 后通过。
- SGLang 最终结果:`1 passed, 5 deselected`,提交 `8a2d501`
- FlashInfer 官方 `test_trtllm_cutlass_fused_moe.py` 增加 SiTU
`(alpha=4, beta=25)` 的 MXFP8×MXFP4 case覆盖 FP16/BF16 和 autotune
on/off最终 `4 passed, 153 deselected`,提交 `e0bc653`
- 证据日志:
```text
/data/hzy/phase5_sglang_noncontiguous_gpu6.log
/data/hzy/phase5_sglang_noncontiguous_gpu6_v2.log
/data/hzy/phase5_flashinfer_official_situ_gpu6.log
```
## 2026-08-18 Phase 4
### 对称 A/B
- 决策baseline/candidate 都走 SGLang production runner共享输入、权重、
scale、routing、SiTU 参数和计时方式。
- 结果M1/M128 FlashInfer 略慢;扩展五轮扫描中 M512/2048/8192 分别比
Marlin 快约 1.46x/2.08x/2.13x。
- 限制M 是单卡本地 runner token 数,不是 HTTP ISL 或全局 batch。
- 证据:`results/phase4_ab_5rounds_gpu6.json`
`results/phase4_mscan_ep32_5rounds_gpu6.json`
### GPU 绑定事故
- 现象:最初 Nsight 命令同时使用 `--gpus device=6``--privileged`UUID
guard 检测到实际设备为物理 GPU0 并终止。
- 根因privileged 重新暴露全部 NVIDIA device node破坏容器 GPU 隔离。
- 处理:移除 privileged固定 `--gpus device=6`
`NVIDIA_VISIBLE_DEVICES=6` 和预期 UUID。未继续占用 GPU0。
### JIT 与 AOT
- 现象:纯 Python FlashInfer wheel 在新进程重新 JIT。NCU profiler 注入
JIT 子进程后产生大量挂起 `nvcc`
- 处理:将已验证的 `fused_moe_120``mxfp8_quantization_sm100`
`fp4_quantization_120f` `.so` 写入 wheel 的 AOT 目录;设置
`FLASHINFER_DISABLE_JIT=1`;持久化 SGLang JIT cache。
- 验证:`results/phase4_aot_nojit_smoke_gpu6.json`
### Profiler 工具
- NCU 2025.3.1RTX 6000D/SM120 不受支持,报
`Profiling is not supported on device 0`
- NCU 2026.2.1:从 NVIDIA 官方 CUDA repository 下载并校验 SHA256解压到
`/data/hzy/tools/nsight-compute-2026.2.1/`,正式捕获成功。
- Nsight Systems正式捕获成功原始文件为
`results/phase4_nsys_ab_gpu6.nsys-rep`
### 大输出统计
- 现象M8192 输出约 2936 万元素,`torch.quantile` 报 input too large。
- 处理P95/P99 改用精确 `torch.kthvalue`。未抽样,未改正确性阈值。
### Warmup 敏感性
- M512 pilot 使用 warmup=1得到与正式结果相反的排序。
- 正式五轮使用 warmup=5、每轮 30 次,五轮速度比为
`1.428x-1.474x`,采用正式结果。
- pilot 日志保留,不作为性能结论。
### 阶段出口
- 不继续 synthetic EP4 扫描。
- 下一步需要 601-604 共 32 卡运行真实 TP32/EP32 Kimi-K3 serving。
- 未经资源授权不清理现有作业、不启动 Phase 5。

View File

@ -0,0 +1,610 @@
diff --git a/python/sglang/srt/layers/attn_residual.py b/python/sglang/srt/layers/attn_residual.py
index 2e3cc94e4206208264fa49331c4201482f9c8ee6..72f0f7b3c6f97a1aefc8453ebd7871403972f0a4 100644
--- a/python/sglang/srt/layers/attn_residual.py
+++ b/python/sglang/srt/layers/attn_residual.py
@@ -8,7 +8,7 @@
# fast — warp-specialized TMA kernel: cp.async.bulk producer +
# online-softmax consumers over a double-buffered chunk ring, out
# norm fused, per-nvb tuned launch config, one persistent CTA per
-# SM. Taken on SM100+ with H=7168.
+# SM. Taken on GB100/GB200/GB300 (SM100/SM103/SM110) with H=7168.
# hip — single Triton kernel, everything in one launch; taken on ROCm
# within its register budget.
# fused — Triton 2-kernel pipeline with full H-parallelism; the fallback
@@ -34,14 +34,17 @@ _HIP_SHAPE_GATE = None
def _use_fast(hidden_size: int) -> bool:
- """The TMA kernel needs SM100+ (tcgen05, cp.async.bulk) and its H=7168
- template instantiation; everything else takes the triton pipeline."""
+ """Select the tcgen05 TMA kernel only on supported data-center Blackwell.
+
+ RTX 6000D is SM120 but does not support this GB100/GB200/GB300-specific
+ kernel. It must use the Triton pipeline.
+ """
global _FAST_SUPPORTED
if is_npu():
return False
if _FAST_SUPPORTED is None:
major, _ = torch.cuda.get_device_capability()
- _FAST_SUPPORTED = major >= 10
+ _FAST_SUPPORTED = major in (10, 11)
return _FAST_SUPPORTED and hidden_size == 7168
diff --git a/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py b/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
index 6a592fb5ebbda136fa13ac423f6bd36455af1a87..d824a8a3dd4f3938682f7d14f1ac5ff04f48d669 100644
--- a/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
+++ b/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
@@ -8,6 +8,7 @@ Quantization methods prepare a small quant_info payload and route through
from __future__ import annotations
+import inspect
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
@@ -89,6 +90,10 @@ class FlashInferCutlassMxfp4MoeQuantInfo(MoeQuantInfo):
swiglu_beta: Optional[torch.Tensor] = None
swiglu_limit: Optional[torch.Tensor] = None
+ # Optional per-expert SiTU scales, fp32 [E].
+ situ_beta: Optional[torch.Tensor] = None
+ situ_linear_beta: Optional[torch.Tensor] = None
+
# TP/EP topology (forwarded to the FlashInfer kernel)
moe_tp_size: int = 1
moe_tp_rank: int = 0
@@ -111,22 +116,44 @@ def _flashinfer_cutlass_fused_moe():
return cutlass_fused_moe, ActivationType
+def flashinfer_cutlass_supports_situ() -> bool:
+ """Return whether the installed FlashInfer exposes the CUTLASS SiTU API."""
+ try:
+ cutlass_fused_moe, activation_type = _flashinfer_cutlass_fused_moe()
+ parameters = inspect.signature(cutlass_fused_moe).parameters
+ except (ImportError, RuntimeError, TypeError, ValueError):
+ return False
+ return (
+ hasattr(activation_type, "Situ")
+ and "situ_beta" in parameters
+ and "situ_linear_beta" in parameters
+ )
+
+
def _activation_type(runner_config: MoeRunnerConfig):
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import get_activation_type
_, ActivationType = _flashinfer_cutlass_fused_moe()
- activation = ActivationType(
- get_activation_type(
- runner_config.activation,
- is_gated=runner_config.is_gated,
+ situ_type = getattr(ActivationType, "Situ", None)
+ if runner_config.activation == "situ" and runner_config.is_gated:
+ if situ_type is None:
+ raise RuntimeError("FlashInfer CUTLASS SiTU support is not available.")
+ activation = situ_type
+ else:
+ activation = ActivationType(
+ get_activation_type(
+ runner_config.activation,
+ is_gated=runner_config.is_gated,
+ )
)
- )
supported = {
ActivationType.Swiglu,
ActivationType.Geglu,
ActivationType.Relu2,
ActivationType.Identity,
}
+ if situ_type is not None:
+ supported.add(situ_type)
assert activation in supported, (
f"Activation {runner_config.activation!r} "
f"(is_gated={runner_config.is_gated}) maps to {activation.name}, "
@@ -308,7 +335,7 @@ def fused_experts_none_to_flashinfer_mxfp4(
quant_info, FlashInferCutlassMxfp4MoeQuantInfo
), f"Unexpected quant_info type for flashinfer_mxfp4: {type(quant_info)}"
- flashinfer_cutlass_fused_moe, ActivationType = _flashinfer_cutlass_fused_moe()
+ flashinfer_cutlass_fused_moe, _ = _flashinfer_cutlass_fused_moe()
x = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
@@ -342,6 +369,7 @@ def fused_experts_none_to_flashinfer_mxfp4(
if weight_global_scale is not None:
from flashinfer import mxfp8_quantize
+ x = x.contiguous()
x, input_sf = mxfp8_quantize(
x,
is_sf_swizzled_layout=True,
@@ -380,13 +408,15 @@ def fused_experts_none_to_flashinfer_mxfp4(
swiglu_alpha=quant_info.swiglu_alpha,
swiglu_beta=quant_info.swiglu_beta,
swiglu_limit=quant_info.swiglu_limit,
+ situ_beta=quant_info.situ_beta,
+ situ_linear_beta=quant_info.situ_linear_beta,
tp_size=quant_info.moe_tp_size,
tp_rank=quant_info.moe_tp_rank,
ep_size=quant_info.moe_ep_size,
ep_rank=quant_info.moe_ep_rank,
use_w4_group_scaling=not use_mxfp8_act_scaling,
use_mxfp8_act_scaling=use_mxfp8_act_scaling,
- activation_type=ActivationType.Swiglu,
+ activation_type=_activation_type(runner_config),
tune_max_num_tokens=next_power_of_2(x.shape[0]),
output=out,
use_fused_finalize=envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.get(),
diff --git a/python/sglang/srt/layers/quantization/mxfp4.py b/python/sglang/srt/layers/quantization/mxfp4.py
index 12ed0cf2728d23d9ea6554d7ae713fd6814898e8..7946f685957b50914168b6813b3e1cb85b0cfd09 100644
--- a/python/sglang/srt/layers/quantization/mxfp4.py
+++ b/python/sglang/srt/layers/quantization/mxfp4.py
@@ -1153,15 +1153,14 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
torch.cuda.empty_cache()
def _process_weights_for_sm120_cutlass(self, layer):
- """Prepare GPT-OSS MXFP4 experts for FlashInfer CUTLASS on SM120.
-
- GPT-OSS stores gate/up rows pair-wise as
- ``[gate_0, up_0, gate_1, up_1, ...]``. FlashInfer's fused MoE consumes
- two contiguous halves in ``[up; gate]`` order. Build that layout after
- checkpoint loading so padding cannot move the split, pad both GEMMs to
- CUTLASS's 128-element alignment, and swizzle the native E8M0 scales for
- the SM120 MXFP8-by-MXFP4 kernels. Packed FP4 weight bytes themselves do
- not need an SM120 permutation.
+ """Prepare MXFP4 experts for FlashInfer CUTLASS on SM120.
+
+ FlashInfer consumes two contiguous halves in ``[up; gate]`` order.
+ GPT-OSS checkpoints store pair-interleaved ``[gate_i, up_i]`` rows,
+ while Kimi-K3 stores contiguous ``[gate; up]`` halves. Normalize either
+ layout after loading, pad both GEMMs to CUTLASS's 128-element alignment,
+ and swizzle the native E8M0 scales. Packed FP4 bytes need no additional
+ SM120 permutation.
"""
from flashinfer import block_scale_interleave
@@ -1173,9 +1172,15 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
E = layer.num_local_experts
device = layer.w13_weight.device
+ gate_up_interleaved = self.moe_runner_config.gate_up_interleaved
+
+ def _split_gate_up(unpadded):
+ if gate_up_interleaved:
+ return unpadded[:, 0::2, :], unpadded[:, 1::2, :]
+ return unpadded[:, :N_un, :], unpadded[:, N_un : 2 * N_un, :]
+
def _stack_up_gate_w13(unpadded, last_pad, last_un):
- gate_rows = unpadded[:, 0::2, :]
- up_rows = unpadded[:, 1::2, :]
+ gate_rows, up_rows = _split_gate_up(unpadded)
out = torch.zeros(
E, 2 * N_pad, last_pad, dtype=unpadded.dtype, device=device
)
@@ -1192,8 +1197,14 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
bias_dtype = layer.w13_weight_bias.dtype
w13_bias_padded = torch.zeros(E, 2 * N_pad, dtype=bias_dtype, device=device)
- w13_bias_padded[:, :N_un] = layer.w13_weight_bias.data[:, 1::2]
- w13_bias_padded[:, N_pad : N_pad + N_un] = layer.w13_weight_bias.data[:, 0::2]
+ if gate_up_interleaved:
+ gate_bias = layer.w13_weight_bias.data[:, 0::2]
+ up_bias = layer.w13_weight_bias.data[:, 1::2]
+ else:
+ gate_bias = layer.w13_weight_bias.data[:, :N_un]
+ up_bias = layer.w13_weight_bias.data[:, N_un : 2 * N_un]
+ w13_bias_padded[:, :N_un] = up_bias
+ w13_bias_padded[:, N_pad : N_pad + N_un] = gate_bias
def _pad_w2_3d(unpadded, last_pad, last_un):
out = torch.zeros(E, K_pad, last_pad, dtype=unpadded.dtype, device=device)
@@ -1225,17 +1236,59 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
layer.w13_weight_bias = Parameter(w13_bias_padded, requires_grad=False)
layer.w2_weight_bias = Parameter(w2_bias_padded, requires_grad=False)
- layer.swiglu_alpha = Parameter(
- torch.full((E,), 1.702, dtype=torch.float32, device=device),
- requires_grad=False,
+ activation = self.moe_runner_config.activation
+ alpha = self.moe_runner_config.gemm1_alpha
+ beta = self.moe_runner_config.gemm1_beta
+ limit = self.moe_runner_config.gemm1_clamp_limit
+ if activation == "situ":
+ situ_beta = 4.0 if alpha is None else alpha
+ situ_linear_beta = 25.0 if limit is None else limit
+ alpha = beta = limit = None
+ else:
+ situ_beta = situ_linear_beta = None
+ alpha = 1.702 if alpha is None else alpha
+ beta = 1.0 if beta is None else beta
+ limit = 7.0 if limit is None else limit
+
+ layer.swiglu_alpha = (
+ None
+ if alpha is None
+ else Parameter(
+ torch.full((E,), alpha, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
)
- layer.swiglu_beta = Parameter(
- torch.ones(E, dtype=torch.float32, device=device),
- requires_grad=False,
+ layer.swiglu_beta = (
+ None
+ if beta is None
+ else Parameter(
+ torch.full((E,), beta, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
)
- layer.swiglu_limit = Parameter(
- torch.full((E,), 7.0, dtype=torch.float32, device=device),
- requires_grad=False,
+ layer.swiglu_limit = (
+ None
+ if limit is None
+ else Parameter(
+ torch.full((E,), limit, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
+ )
+ layer.situ_beta = (
+ None
+ if situ_beta is None
+ else Parameter(
+ torch.full((E,), situ_beta, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
+ )
+ layer.situ_linear_beta = (
+ None
+ if situ_linear_beta is None
+ else Parameter(
+ torch.full((E,), situ_linear_beta, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
)
# The MXFP4 ABI uses a neutral global weight scale for each GEMM.
layer.mxfp4_weight_global_scale = Parameter(
@@ -1278,6 +1331,22 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
"cutlass_sm90",
"cutlass_sm120",
):
+ if (
+ self._fi_kernel == "cutlass_sm120"
+ and moe_runner_config.activation == "situ"
+ ):
+ from sglang.srt.layers.moe.moe_runner.flashinfer_cutlass import (
+ flashinfer_cutlass_supports_situ,
+ )
+
+ if not flashinfer_cutlass_supports_situ():
+ raise RuntimeError(
+ "Kimi-K3 FlashInfer MXFP4 MoE on SM120 requires a "
+ "FlashInfer build with CUTLASS SiTU support "
+ "(ActivationType.Situ plus situ_beta/situ_linear_beta). "
+ "Upgrade FlashInfer or restart with "
+ "--moe-runner-backend marlin."
+ )
# Register the fused func at runner construction so the FusedOpPool
# lookup at `MoeRunner.__init__` finds it.
import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass # noqa: F401
@@ -1338,7 +1407,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
)
def _apply_sm120_cutlass(self, layer, dispatch_output):
- """SM120 GPT-OSS MXFP8 x MXFP4 MoE via FlashInfer CUTLASS."""
+ """SM120 MXFP8 x MXFP4 MoE via FlashInfer CUTLASS."""
from sglang.srt.layers.moe.moe_runner.flashinfer_cutlass import (
FlashInferCutlassMxfp4MoeQuantInfo,
)
@@ -1349,11 +1418,13 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
w13_weight_scale=layer.w13_weight_scale,
w2_weight_scale=layer.w2_weight_scale,
mxfp4_weight_global_scale=layer.mxfp4_weight_global_scale,
- w13_bias=layer.w13_weight_bias,
- w2_bias=layer.w2_weight_bias,
+ w13_bias=layer.w13_weight_bias if self.with_bias else None,
+ w2_bias=layer.w2_weight_bias if self.with_bias else None,
swiglu_alpha=layer.swiglu_alpha,
swiglu_beta=layer.swiglu_beta,
swiglu_limit=layer.swiglu_limit,
+ situ_beta=layer.situ_beta,
+ situ_linear_beta=layer.situ_linear_beta,
moe_tp_size=layer.moe_tp_size,
moe_tp_rank=layer.moe_tp_rank,
moe_ep_size=layer.moe_ep_size,
diff --git a/test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py b/test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py
index 435c6d7fe682197282ed7f7cb8f336d7a70f4b0e..0eed38e03f980fc9919f84a95265e0b6b8e55df9 100644
--- a/test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py
+++ b/test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py
@@ -6,6 +6,7 @@ import builtins
import importlib
import sys
from contextlib import nullcontext
+from enum import IntEnum
from types import SimpleNamespace
import pytest
@@ -105,6 +106,54 @@ def test_dsv4_sm120_load_contract(monkeypatch):
assert captured["fp4_scale_dtype"] == torch.float8_e8m0fnu
+def test_kimi_k3_sm120_situ_requires_flashinfer_cutlass_situ_api(monkeypatch):
+ import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass as runner_module
+ import sglang.srt.layers.quantization.mxfp4 as mxfp4_module
+ from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
+ from sglang.srt.layers.moe.utils import MoeRunnerBackend
+ from sglang.srt.layers.quantization.mxfp4 import Mxfp4MoEMethod
+
+ monkeypatch.setattr(
+ mxfp4_module,
+ "get_moe_runner_backend",
+ lambda: MoeRunnerBackend.FLASHINFER_MXFP4,
+ )
+ monkeypatch.setattr(
+ runner_module, "flashinfer_cutlass_supports_situ", lambda: False
+ )
+
+ method = Mxfp4MoEMethod.__new__(Mxfp4MoEMethod)
+ method._fi_kernel = "cutlass_sm120"
+ config = MoeRunnerConfig(activation="situ", is_gated=True)
+ with pytest.raises(RuntimeError, match="--moe-runner-backend marlin"):
+ method.create_moe_runner(SimpleNamespace(), config)
+
+
+def test_non_situ_activation_supports_legacy_flashinfer(monkeypatch):
+ import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass as runner_module
+ import sglang.srt.layers.moe.moe_runner.flashinfer_trtllm as trtllm_module
+
+ class LegacyActivationType(IntEnum):
+ Swiglu = 0
+ Geglu = 1
+ Relu2 = 2
+ Identity = 3
+
+ monkeypatch.setattr(
+ runner_module,
+ "_flashinfer_cutlass_fused_moe",
+ lambda: (object(), LegacyActivationType),
+ )
+ monkeypatch.setattr(
+ trtllm_module,
+ "get_activation_type",
+ lambda *args, **kwargs: LegacyActivationType.Swiglu.value,
+ )
+
+ config = SimpleNamespace(activation="silu", is_gated=True)
+ assert runner_module._activation_type(config) == LegacyActivationType.Swiglu
+
+
def test_dsv4_sm120_matches_direct_flashinfer(monkeypatch):
if not torch.cuda.is_available():
pytest.skip("CUDA required")
@@ -317,6 +366,7 @@ def test_gpt_oss_sm120_padding_layout_and_kernel(monkeypatch):
method.intermediate_size_per_partition = intermediate
method._padded_hidden = padded_hidden
method._padded_intermediate = padded_intermediate
+ method.with_bias = True
config = MoeRunnerConfig(
num_experts=num_experts,
num_local_experts=num_experts,
@@ -433,5 +483,161 @@ def test_gpt_oss_sm120_padding_layout_and_kernel(monkeypatch):
assert torch.equal(actual, expected[:, :hidden].contiguous())
+def test_kimi_k3_sm120_situ_layout_and_noncontiguous_input(monkeypatch):
+ if not torch.cuda.is_available():
+ pytest.skip("CUDA required")
+ if torch.cuda.get_device_capability() != (12, 0):
+ pytest.skip("SM120 required")
+ pytest.importorskip("flashinfer.fused_moe")
+
+ from flashinfer import block_scale_interleave, mxfp8_quantize
+ from flashinfer.fused_moe import cutlass_fused_moe
+ from flashinfer.fused_moe.core import ActivationType
+
+ import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass as runner_module
+ from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
+ from sglang.srt.layers.moe.moe_runner.runner import MoeRunner
+ from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatchOutput
+ from sglang.srt.layers.moe.topk import StandardTopKOutput
+ from sglang.srt.layers.moe.utils import MoeRunnerBackend
+ from sglang.srt.layers.quantization.mxfp4 import Mxfp4MoEMethod
+
+ if not runner_module.flashinfer_cutlass_supports_situ():
+ pytest.skip("FlashInfer CUTLASS SiTU support required")
+
+ monkeypatch.setattr(
+ runner_module, "use_symmetric_memory", lambda *args, **kwargs: nullcontext()
+ )
+ monkeypatch.setattr(runner_module, "is_allocation_symmetric", lambda: False)
+ monkeypatch.setattr(runner_module, "get_tp_group", lambda: None)
+
+ num_experts, hidden, intermediate = 4, 256, 256
+ w13, w2, w13_scale, w2_scale = _random_weights(num_experts, hidden, intermediate)
+ gate, up = w13.chunk(2, dim=1)
+ gate_scale, up_scale = w13_scale.chunk(2, dim=1)
+ generator = torch.Generator(device="cuda").manual_seed(3)
+ layer = SimpleNamespace(
+ w13_weight=torch.nn.Parameter(w13.view(torch.uint8), requires_grad=False),
+ w2_weight=torch.nn.Parameter(w2.view(torch.uint8), requires_grad=False),
+ w13_weight_scale=torch.nn.Parameter(
+ w13_scale.view(torch.uint8), requires_grad=False
+ ),
+ w2_weight_scale=torch.nn.Parameter(
+ w2_scale.view(torch.uint8), requires_grad=False
+ ),
+ # CUDA weight creation keeps zero placeholders even for bias-free models.
+ w13_weight_bias=torch.nn.Parameter(
+ torch.zeros(
+ num_experts,
+ 2 * intermediate,
+ dtype=torch.bfloat16,
+ device="cuda",
+ ),
+ requires_grad=False,
+ ),
+ w2_weight_bias=torch.nn.Parameter(
+ torch.zeros(num_experts, hidden, dtype=torch.bfloat16, device="cuda"),
+ requires_grad=False,
+ ),
+ num_local_experts=num_experts,
+ moe_tp_size=1,
+ moe_tp_rank=0,
+ moe_ep_size=1,
+ moe_ep_rank=0,
+ )
+
+ method = Mxfp4MoEMethod.__new__(Mxfp4MoEMethod)
+ method._fi_kernel = "cutlass_sm120"
+ method.num_experts = num_experts
+ method.hidden_size = hidden
+ method.intermediate_size_per_partition = intermediate
+ method._padded_hidden = hidden
+ method._padded_intermediate = intermediate
+ method.with_bias = False
+ config = MoeRunnerConfig(
+ num_experts=num_experts,
+ num_local_experts=num_experts,
+ hidden_size=hidden,
+ intermediate_size_per_partition=intermediate,
+ top_k=2,
+ activation="situ",
+ is_gated=True,
+ gemm1_alpha=4.0,
+ gemm1_clamp_limit=25.0,
+ gate_up_interleaved=False,
+ )
+ method.moe_runner_config = config
+ method.runner = MoeRunner(MoeRunnerBackend.FLASHINFER_MXFP4, config)
+ method._process_weights_for_sm120_cutlass(layer)
+
+ expected_w13 = torch.cat((up, gate), dim=1).view(torch.uint8)
+ expected_w13_scale = block_scale_interleave(
+ torch.cat((up_scale.view(torch.uint8), gate_scale.view(torch.uint8)), dim=1)
+ ).reshape_as(layer.w13_weight_scale)
+ assert torch.equal(layer.w13_weight, expected_w13)
+ assert torch.equal(layer.w13_weight_scale, expected_w13_scale)
+ assert layer.swiglu_alpha is None
+ assert layer.swiglu_beta is None
+ assert layer.swiglu_limit is None
+ assert torch.all(layer.situ_beta == 4.0)
+ assert torch.all(layer.situ_linear_beta == 25.0)
+ assert runner_module._activation_type(config) == ActivationType.Situ
+
+ x = (
+ torch.randn(
+ hidden,
+ 8,
+ dtype=torch.bfloat16,
+ device="cuda",
+ generator=generator,
+ ).t()
+ * 0.1
+ )
+ assert not x.is_contiguous()
+ logits = torch.randn(
+ 8,
+ num_experts,
+ dtype=torch.float32,
+ device="cuda",
+ generator=generator,
+ )
+ topk_weights, topk_ids = torch.topk(torch.softmax(logits, dim=-1), 2, dim=-1)
+ topk_weights /= topk_weights.sum(dim=-1, keepdim=True)
+ dispatch_output = StandardDispatchOutput(
+ x,
+ None,
+ StandardTopKOutput(topk_weights, topk_ids.to(torch.int32), logits),
+ )
+ actual = method._apply_sm120_cutlass(layer, dispatch_output).hidden_states
+
+ x_quant, x_scale = mxfp8_quantize(
+ x.contiguous(), is_sf_swizzled_layout=True, alignment=32
+ )
+ expected = torch.empty_like(x, memory_format=torch.contiguous_format)
+ cutlass_fused_moe(
+ input=x_quant,
+ token_selected_experts=topk_ids.to(torch.int32),
+ token_final_scales=topk_weights,
+ fc1_expert_weights=layer.w13_weight.view(torch.int64),
+ fc2_expert_weights=layer.w2_weight.view(torch.int64),
+ output_dtype=torch.bfloat16,
+ quant_scales=[
+ layer.w13_weight_scale.view(torch.int32),
+ layer.mxfp4_weight_global_scale,
+ layer.w2_weight_scale.view(torch.int32),
+ layer.mxfp4_weight_global_scale,
+ ],
+ input_sf=x_scale,
+ situ_beta=layer.situ_beta,
+ situ_linear_beta=layer.situ_linear_beta,
+ use_w4_group_scaling=False,
+ use_mxfp8_act_scaling=True,
+ activation_type=ActivationType.Situ,
+ tune_max_num_tokens=8,
+ output=expected,
+ )
+ assert torch.equal(actual, expected)
+
+
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
diff --git a/test/registered/unit/layers/test_attn_residual_arch_gate.py b/test/registered/unit/layers/test_attn_residual_arch_gate.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c55b924312a7d2537f8221b5e985e7927e527d4
--- /dev/null
+++ b/test/registered/unit/layers/test_attn_residual_arch_gate.py
@@ -0,0 +1,46 @@
+"""Architecture gate tests for Kimi-K3's fused attention residual."""
+
+from unittest.mock import patch
+
+import pytest
+
+import sglang.srt.layers.attn_residual as attn_residual
+from sglang.test.ci.ci_register import register_cuda_ci
+
+register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-small")
+
+
+@pytest.mark.parametrize(
+ ("capability", "expected"),
+ [
+ ((10, 0), True),
+ ((10, 3), True),
+ ((11, 0), True),
+ ((12, 0), False),
+ ((9, 0), False),
+ ],
+)
+def test_fast_attn_residual_arch_gate(capability, expected):
+ with (
+ patch.object(attn_residual, "_FAST_SUPPORTED", None),
+ patch.object(attn_residual, "is_npu", return_value=False),
+ patch.object(
+ attn_residual.torch.cuda,
+ "get_device_capability",
+ return_value=capability,
+ ),
+ ):
+ assert attn_residual._use_fast(7168) is expected
+
+
+def test_fast_attn_residual_requires_kimi_hidden_size():
+ with (
+ patch.object(attn_residual, "_FAST_SUPPORTED", None),
+ patch.object(attn_residual, "is_npu", return_value=False),
+ patch.object(
+ attn_residual.torch.cuda,
+ "get_device_capability",
+ return_value=(10, 0),
+ ),
+ ):
+ assert not attn_residual._use_fast(4096)

View File

@ -0,0 +1,583 @@
#!/usr/bin/env python3
"""Profile Kimi-K3 MXFP4 MoE on SM120: SGLang Marlin vs FlashInfer.
Both backends consume the same BF16 input, packed MXFP4 checkpoint values,
E8M0 scales, global top-k ids/weights, SiTU parameters, and EP32/rank7 mapping.
Backend-specific post-load layouts are derived once before correctness checks,
warmup, timing, and profiling.
"""
from __future__ import annotations
import argparse
from contextlib import nullcontext
from dataclasses import asdict
from datetime import datetime
import json
import math
import os
from pathlib import Path
import statistics
import time
from types import SimpleNamespace
from typing import Callable
import torch
import torch.nn.functional as F
from flashinfer import (
SfLayout,
block_scale_interleave,
mxfp4_dequantize,
mxfp4_quantize,
mxfp8_quantize,
)
from sglang.srt.distributed import init_distributed_environment
from sglang.srt.distributed.parallel_state import (
destroy_model_parallel,
initialize_model_parallel,
)
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
from sglang.srt.layers.moe.moe_runner.flashinfer_cutlass import (
FlashInferCutlassMxfp4MoeQuantInfo,
fused_experts_none_to_flashinfer_mxfp4,
)
from sglang.srt.layers.moe.moe_runner.marlin import (
MarlinMoeQuantInfo,
fused_experts_none_to_marlin,
)
from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatchOutput
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.layers.quantization.marlin_utils_fp4 import (
prepare_moe_mxfp4_layer_for_marlin,
)
from validate_kimi_sm120_mxfp4 import (
Case,
QuantizedCase,
dequantize_mxfp8_input,
make_routing,
run_reference,
)
SHAPES = {
"decode": Case(
"kimi_decode_m1_ep32",
1,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
"prefill": Case(
"kimi_prefill_m128_ep32",
128,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
"prefill_m512": Case(
"kimi_prefill_m512_ep32",
512,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
"prefill_m2048": Case(
"kimi_prefill_m2048_ep32",
2048,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
"prefill_m8192": Case(
"kimi_prefill_m8192_ep32",
8192,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
}
def _quantize_experts_linear(
weight: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
packed = []
scales = []
dequantized = []
for expert_weight in weight:
expert_packed, expert_scale = mxfp4_quantize(
expert_weight,
sfLayout=SfLayout.layout_linear,
)
packed.append(expert_packed)
scales.append(expert_scale)
dequantized.append(
mxfp4_dequantize(
expert_packed.cpu(),
expert_scale.cpu(),
sfLayout=SfLayout.layout_linear,
)
)
return (
torch.stack(packed),
torch.stack(scales),
torch.stack(dequantized).to(device=weight.device, dtype=weight.dtype),
)
def _prepare_linear_checkpoint_case(
case: Case,
seed: int,
device: torch.device,
) -> QuantizedCase:
generator = torch.Generator(device=device).manual_seed(seed)
dtype = torch.bfloat16
x = torch.randn(
case.tokens,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
)
gate = torch.randn(
case.local_experts,
case.intermediate,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
) * (1.0 / math.sqrt(case.hidden))
up = torch.randn(
case.local_experts,
case.intermediate,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
) * (1.0 / math.sqrt(case.hidden))
down = torch.randn(
case.local_experts,
case.hidden,
case.intermediate,
generator=generator,
dtype=dtype,
device=device,
) * (1.0 / math.sqrt(case.intermediate))
input_q, input_sf = mxfp8_quantize(
x,
is_sf_swizzled_layout=True,
alignment=32,
)
input_dq = dequantize_mxfp8_input(input_q, input_sf, dtype)
fc1_q, fc1_sf, fc1_dq = _quantize_experts_linear(
torch.cat((up, gate), dim=1).contiguous()
)
fc2_q, fc2_sf, fc2_dq = _quantize_experts_linear(down)
topk_ids, topk_weights = make_routing(case, device)
return QuantizedCase(
case=case,
input_q=input_q,
input_sf=input_sf,
input_dq=input_dq,
fc1_q=fc1_q,
fc1_sf=fc1_sf,
fc1_dq=fc1_dq,
fc2_q=fc2_q,
fc2_sf=fc2_sf,
fc2_dq=fc2_dq,
topk_ids=topk_ids,
topk_weights=topk_weights,
expert_scale=torch.ones(
case.local_experts, dtype=torch.float32, device=device
),
situ_gate_beta=torch.full(
(case.local_experts,), 4.0, dtype=torch.float32, device=device
),
situ_linear_beta=torch.full(
(case.local_experts,), 25.0, dtype=torch.float32, device=device
),
)
def _init_sglang_distributed() -> None:
"""Create the rank-one groups that production MoE wrappers expect."""
torch.cuda.set_device(0)
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="tcp://127.0.0.1:29541",
backend="nccl",
)
initialize_model_parallel(
tensor_model_parallel_size=1,
expert_model_parallel_size=1,
)
def _destroy_sglang_distributed() -> None:
destroy_model_parallel()
if torch.distributed.is_initialized():
torch.distributed.destroy_process_group()
def _quantile_exact(values: torch.Tensor, quantile: float) -> float:
"""Return an exact quantile without torch.quantile's 2^24 element limit."""
rank = max(1, math.ceil(quantile * values.numel()))
return float(torch.kthvalue(values, rank).values.item())
def _metrics(reference: torch.Tensor, candidate: torch.Tensor) -> dict:
ref = reference.float().reshape(-1)
got = candidate.float().reshape(-1)
diff = got - ref
abs_diff = diff.abs()
denominator = torch.sqrt(torch.mean(ref.square())).clamp_min(1e-12)
return {
"mean_abs": float(abs_diff.mean().item()),
"p95_abs": _quantile_exact(abs_diff, 0.95),
"p99_abs": _quantile_exact(abs_diff, 0.99),
"max_abs": float(abs_diff.max().item()),
"nrmse": float((torch.sqrt(torch.mean(diff.square())) / denominator).item()),
"cosine": float(F.cosine_similarity(ref, got, dim=0).item()),
"reference_rms": float(denominator.item()),
"reference_max_abs": float(ref.abs().max().item()),
"finite": bool(torch.isfinite(candidate).all()),
}
def _assert_close(name: str, reference: torch.Tensor, candidate: torch.Tensor) -> dict:
metrics = _metrics(reference, candidate)
if not metrics["finite"]:
raise AssertionError(f"{name} produced non-finite output: {metrics}")
torch.testing.assert_close(candidate, reference, rtol=0.2, atol=0.2)
return metrics
def _checkpoint_gate_up_from_flashinfer_up_gate(tensor: torch.Tensor) -> torch.Tensor:
up, gate = tensor.chunk(2, dim=1)
return torch.cat((gate, up), dim=1).contiguous()
def _build_expert_map(case: Case, device: torch.device) -> torch.Tensor:
expert_map = torch.full(
(case.total_experts,), -1, dtype=torch.int32, device=device
)
start = case.local_expert_start
expert_map[start : start + case.local_experts] = torch.arange(
case.local_experts, dtype=torch.int32, device=device
)
return expert_map
class BackendPair:
def __init__(self, case: Case, seed: int):
self.case = case
self.data = _prepare_linear_checkpoint_case(
case,
seed,
torch.device("cuda"),
)
self.config = MoeRunnerConfig(
num_experts=case.total_experts,
num_local_experts=case.local_experts,
hidden_size=case.hidden,
intermediate_size_per_partition=case.intermediate,
top_k=case.top_k,
activation="situ",
is_gated=True,
gemm1_alpha=4.0,
gemm1_clamp_limit=25.0,
gate_up_interleaved=False,
)
logits = torch.zeros(
case.tokens,
case.total_experts,
dtype=torch.float32,
device="cuda",
)
self.expert_map = _build_expert_map(case, torch.device("cuda"))
self.flashinfer_dispatch = StandardDispatchOutput(
self.data.input_dq.contiguous(),
None,
StandardTopKOutput(
self.data.topk_weights,
self.data.topk_ids,
logits,
),
)
marlin_topk_ids = self.expert_map[self.data.topk_ids]
self.marlin_dispatch = StandardDispatchOutput(
self.data.input_dq.contiguous(),
None,
StandardTopKOutput(
self.data.topk_weights,
marlin_topk_ids,
logits,
),
)
self.reference = run_reference(self.data, "situ")
self.fi_quant = self._build_flashinfer_quant_info()
self.marlin_quant = self._build_marlin_quant_info()
def _build_flashinfer_quant_info(self) -> FlashInferCutlassMxfp4MoeQuantInfo:
case = self.case
w13_scale = block_scale_interleave(self.data.fc1_sf).reshape_as(
self.data.fc1_sf
)
w2_scale = block_scale_interleave(self.data.fc2_sf).reshape_as(
self.data.fc2_sf
)
return FlashInferCutlassMxfp4MoeQuantInfo(
w13_weight=self.data.fc1_q,
w2_weight=self.data.fc2_q,
w13_weight_scale=w13_scale.contiguous(),
w2_weight_scale=w2_scale.contiguous(),
mxfp4_weight_global_scale=self.data.expert_scale,
w13_bias=None,
w2_bias=None,
swiglu_alpha=self.data.situ_gate_beta,
swiglu_beta=self.data.situ_linear_beta,
swiglu_limit=None,
moe_tp_size=1,
moe_tp_rank=0,
moe_ep_size=case.ep_size,
moe_ep_rank=case.ep_rank,
padded_hidden=case.hidden,
)
def _build_marlin_quant_info(self) -> MarlinMoeQuantInfo:
case = self.case
layer = SimpleNamespace(
w13_weight=torch.nn.Parameter(
_checkpoint_gate_up_from_flashinfer_up_gate(self.data.fc1_q.clone()),
requires_grad=False,
),
w2_weight=torch.nn.Parameter(
self.data.fc2_q.clone(), requires_grad=False
),
w13_weight_scale=torch.nn.Parameter(
_checkpoint_gate_up_from_flashinfer_up_gate(
self.data.fc1_sf.clone()
),
requires_grad=False,
),
w2_weight_scale=torch.nn.Parameter(
self.data.fc2_sf.clone(), requires_grad=False
),
orig_dtype=torch.bfloat16,
)
prepare_moe_mxfp4_layer_for_marlin(layer)
return MarlinMoeQuantInfo(
w13_qweight=layer.w13_weight,
w2_qweight=layer.w2_weight,
w13_scales=layer.w13_weight_scale,
w2_scales=layer.w2_weight_scale,
w13_g_idx_sort_indices=None,
w2_g_idx_sort_indices=None,
weight_bits=4,
is_k_full=True,
expert_map=self.expert_map,
global_num_experts=case.total_experts,
)
def flashinfer(self) -> torch.Tensor:
return fused_experts_none_to_flashinfer_mxfp4(
self.flashinfer_dispatch, self.fi_quant, self.config
).hidden_states
def marlin(self) -> torch.Tensor:
return fused_experts_none_to_marlin(
self.marlin_dispatch, self.marlin_quant, self.config
).hidden_states[:, : self.case.hidden]
def validate(self) -> dict:
fi = self.flashinfer()
marlin = self.marlin()
torch.cuda.synchronize()
metrics = {
"flashinfer_vs_reference": _metrics(self.reference, fi),
"marlin_vs_reference": _metrics(self.reference, marlin),
"flashinfer_vs_marlin": _metrics(marlin, fi),
}
print("CORRECTNESS " + json.dumps(metrics, sort_keys=True), flush=True)
_assert_close("flashinfer", self.reference, fi)
_assert_close("marlin", self.reference, marlin)
_assert_close("cross_backend", marlin, fi)
return metrics
def _measure(fn: Callable[[], torch.Tensor], iterations: int) -> dict:
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
torch.cuda.synchronize()
wall_start = time.perf_counter()
start.record()
for _ in range(iterations):
fn()
end.record()
torch.cuda.synchronize()
wall_ms = (time.perf_counter() - wall_start) * 1000.0 / iterations
return {
"gpu_ms": start.elapsed_time(end) / iterations,
"wall_ms": wall_ms,
}
def _warmup(pair: BackendPair, iterations: int) -> None:
for _ in range(iterations):
pair.marlin()
pair.flashinfer()
torch.cuda.synchronize()
def run_benchmark(args: argparse.Namespace) -> dict:
report = {
"created_at": datetime.now().astimezone().isoformat(),
"device": torch.cuda.get_device_name(),
"compute_capability": list(torch.cuda.get_device_capability()),
"cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"),
"rounds": args.rounds,
"iterations": args.iterations,
"shapes": {},
}
for shape_name in args.shapes:
pair = BackendPair(SHAPES[shape_name], args.seed)
correctness = pair.validate()
_warmup(pair, args.warmup)
rounds = []
for round_index in range(args.rounds):
order = (
("marlin", "flashinfer")
if round_index % 2 == 0
else ("flashinfer", "marlin")
)
result = {"round": round_index + 1, "order": list(order)}
for backend in order:
result[backend] = _measure(getattr(pair, backend), args.iterations)
rounds.append(result)
summary = {}
for backend in ("marlin", "flashinfer"):
summary[backend] = {
metric: statistics.median(
result[backend][metric] for result in rounds
)
for metric in ("gpu_ms", "wall_ms")
}
summary["speedup_marlin_over_flashinfer"] = {
metric: summary["marlin"][metric] / summary["flashinfer"][metric]
for metric in ("gpu_ms", "wall_ms")
}
report["shapes"][shape_name] = {
"case": asdict(pair.case),
"correctness": correctness,
"rounds": rounds,
"summary": summary,
}
del pair
torch.cuda.empty_cache()
return report
def run_profile(args: argparse.Namespace) -> None:
shape_names = tuple(SHAPES) if args.shape == "all" else (args.shape,)
pairs = [
(shape_name, BackendPair(SHAPES[shape_name], args.seed))
for shape_name in shape_names
]
for shape_name, pair in pairs:
print(
"PROFILE_CORRECTNESS "
+ json.dumps({shape_name: pair.validate()}, sort_keys=True),
flush=True,
)
_warmup(pair, args.warmup)
torch.cuda.synchronize()
torch.cuda.cudart().cudaProfilerStart()
backends = (
(args.backend,)
if args.backend != "both"
else ("marlin", "flashinfer")
)
for shape_name, pair in pairs:
for backend in backends:
for index in range(args.profile_iterations):
label = f"{backend}_{shape_name}_iteration_{index + 1}"
context = (
torch.cuda.nvtx.range(label)
if torch.cuda.is_available()
else nullcontext()
)
with context:
getattr(pair, backend)()
torch.cuda.synchronize()
torch.cuda.cudart().cudaProfilerStop()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=("benchmark", "profile"), default="benchmark")
parser.add_argument("--shapes", nargs="+", choices=tuple(SHAPES), default=list(SHAPES))
parser.add_argument("--shape", choices=(*SHAPES, "all"), default="decode")
parser.add_argument("--backend", choices=("marlin", "flashinfer", "both"), default="both")
parser.add_argument("--rounds", type=int, default=3)
parser.add_argument("--iterations", type=int, default=30)
parser.add_argument("--warmup", type=int, default=5)
parser.add_argument("--profile-iterations", type=int, default=3)
parser.add_argument("--seed", type=int, default=20260817)
parser.add_argument("--output", type=Path)
return parser.parse_args()
def main() -> None:
args = parse_args()
if not torch.cuda.is_available():
raise SystemExit("CUDA is required")
expected_uuid = os.environ.get("EXPECTED_GPU_UUID")
actual_uuid = str(torch.cuda.get_device_properties(0).uuid)
if expected_uuid and actual_uuid.lower() != expected_uuid.lower():
raise SystemExit(
f"GPU binding mismatch: expected {expected_uuid}, got {actual_uuid}"
)
if torch.cuda.get_device_capability() != (12, 0):
raise SystemExit(f"SM120 is required, got {torch.cuda.get_device_capability()}")
_init_sglang_distributed()
try:
with torch.inference_mode():
if args.mode == "profile":
run_profile(args)
return
report = run_benchmark(args)
finally:
_destroy_sglang_distributed()
rendered = json.dumps(report, indent=2, sort_keys=True)
print(rendered)
if args.output is not None:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered + "\n", encoding="utf-8")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Reproduce the missing SM120 CUTLASS SiTU contract with MXFP8 x MXFP4."""
from __future__ import annotations
import argparse
import importlib
from pathlib import Path
import sys
import torch
from flashinfer import mxfp8_quantize
from flashinfer.fused_moe import cutlass_fused_moe
from flashinfer.tllm_enums import ActivationType
def load_official_helpers(flashinfer_source: Path):
sys.path.insert(0, str(flashinfer_source))
return importlib.import_module(
"tests.moe.test_trtllm_cutlass_fused_moe"
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--flashinfer-source",
type=Path,
default=Path("/data/hzy/src/flashinfer-kimi-sm120"),
)
args = parser.parse_args()
helpers = load_official_helpers(args.flashinfer_source)
torch.manual_seed(42)
tokens, hidden, experts, top_k, intermediate = 1, 128, 2, 2, 128
dtype = torch.bfloat16
x = torch.randn(tokens, hidden, dtype=dtype, device="cuda")
w31 = torch.randn(
experts, 2 * intermediate, hidden, dtype=dtype, device="cuda"
) / 10
w2 = torch.randn(
experts, hidden, intermediate, dtype=dtype, device="cuda"
) / 10
x_q, x_sf = mxfp8_quantize(x, True, 32)
w31_q, w31_sf = helpers.quant_mxfp4_batches(w31, experts)
w2_q, w2_sf = helpers.quant_mxfp4_batches(w2, experts)
logits = torch.randn(tokens, experts, dtype=dtype, device="cuda")
weights, ids = helpers.compute_routing(logits, top_k)
neutral_global_scale = torch.ones(experts, dtype=torch.float32, device="cuda")
situ_gate_beta = torch.full(
(experts,), 4.0, dtype=torch.float32, device="cuda"
)
situ_linear_beta = torch.full(
(experts,), 25.0, dtype=torch.float32, device="cuda"
)
output = torch.empty_like(x)
cutlass_fused_moe(
input=x_q,
token_selected_experts=ids.to(torch.int32),
token_final_scales=weights,
fc1_expert_weights=w31_q.contiguous().view(torch.int64),
fc2_expert_weights=w2_q.contiguous().view(torch.int64),
output_dtype=dtype,
quant_scales=[
w31_sf.view(torch.int32),
neutral_global_scale,
w2_sf.view(torch.int32),
neutral_global_scale,
],
input_sf=x_sf,
swiglu_alpha=situ_gate_beta,
swiglu_beta=situ_linear_beta,
swiglu_limit=None,
activation_type=ActivationType.Situ,
use_mxfp8_act_scaling=True,
output=output,
)
raise SystemExit("unexpected success: SM120 CUTLASS SiTU gap is no longer present")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,81 @@
{
"arguments": {
"activation": "swiglu",
"benchmark_iterations": 0,
"output": "results/phase2_control_swiglu_real_gpu6.json",
"require_situ": false,
"seed": 20260817,
"suite": "real",
"warmup": 3
},
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T12:02:07.099079+08:00",
"cuda_version": "13.0",
"cuda_visible_devices": "6",
"device": "NVIDIA RTX 6000D",
"flashinfer_version": "0.6.18",
"logical_cuda_device": 0,
"results": [
{
"activation": "swiglu",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_decode_m1_ep32",
"routing": "ep_mixed",
"tokens": 1,
"top_k": 16
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 13.75742506980896,
"metrics": {
"cosine": 0.9995965957641602,
"finite_elements": 3584,
"max_abs": 0.01025390625,
"nrmse": 0.028520602732896805,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 12.48392629623413,
"metrics": {
"cosine": 0.9996211528778076,
"finite_elements": 458752,
"max_abs": 0.013671875,
"nrmse": 0.027537915855646133,
"same_nonfinite_mask": true
},
"status": "PASS"
}
],
"schema_version": 1,
"torch_version": "2.11.0+cu130"
}

View File

@ -0,0 +1,214 @@
{
"arguments": {
"activation": "swiglu",
"benchmark_iterations": 0,
"output": "results/phase2_control_swiglu_smoke_gpu6.json",
"require_situ": false,
"seed": 20260817,
"suite": "smoke",
"warmup": 3
},
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T12:01:32.407549+08:00",
"cuda_version": "13.0",
"cuda_visible_devices": "6",
"device": "NVIDIA RTX 6000D",
"flashinfer_version": "0.6.18",
"logical_cuda_device": 0,
"results": [
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m1_uniform",
"routing": "uniform",
"tokens": 1,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 1.2227470874786377,
"metrics": {
"cosine": 0.9998119473457336,
"finite_elements": 128,
"max_abs": 0.03125,
"nrmse": 0.019757770001888275,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m2_skewed",
"routing": "skewed",
"tokens": 2,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.03631448745727539,
"metrics": {
"cosine": 0.9997045397758484,
"finite_elements": 256,
"max_abs": 0.03125,
"nrmse": 0.024355093017220497,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m8_empty_experts",
"routing": "skewed",
"tokens": 8,
"top_k": 2
},
"cuda_graph": {
"replay_matches_eager": {
"cosine": 0.9999999403953552,
"finite_elements": 1024,
"max_abs": 0.0,
"nrmse": 0.0,
"same_nonfinite_mask": true
},
"replays_bitwise_equal": true
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.08502435684204102,
"metrics": {
"cosine": 0.9996250867843628,
"finite_elements": 1024,
"max_abs": 0.0546875,
"nrmse": 0.02746359072625637,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m32_masked_slot",
"routing": "masked",
"tokens": 32,
"top_k": 4
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.013071775436401367,
"metrics": {
"cosine": 0.9996730089187622,
"finite_elements": 4096,
"max_abs": 0.0390625,
"nrmse": 0.02586047165095806,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 2,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "ep2_global_ids",
"routing": "ep_mixed",
"tokens": 8,
"top_k": 4
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.0670325756072998,
"metrics": {
"cosine": 0.9996550679206848,
"finite_elements": 1024,
"max_abs": 0.01953125,
"nrmse": 0.02657792530953884,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "swiglu",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": true,
"intermediate": 128,
"local_experts": 4,
"name": "nonfinite_activation",
"routing": "uniform",
"tokens": 2,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.009414196014404297,
"metrics": {
"candidate_inf_elements": 68,
"candidate_nan_elements": 60,
"candidate_nonfinite_rows": [
false,
true
],
"contract": "same_affected_rows_and_close_unaffected_rows",
"cosine": 0.9998903870582581,
"finite_elements": 128,
"max_abs": 0.025146484375,
"nrmse": 0.015044113621115685,
"reference_nonfinite_rows": [
false,
true
],
"same_nonfinite_mask": false
},
"status": "PASS"
}
],
"schema_version": 1,
"torch_version": "2.11.0+cu130"
}

View File

@ -0,0 +1,61 @@
{
"arguments": {
"activation": "situ",
"benchmark_iterations": 0,
"output": "results/phase2_target_situ_real_gpu6.json",
"require_situ": false,
"seed": 20260817,
"suite": "real",
"warmup": 3
},
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T12:04:05.231369+08:00",
"cuda_version": "13.0",
"cuda_visible_devices": "6",
"device": "NVIDIA RTX 6000D",
"flashinfer_version": "0.6.18",
"logical_cuda_device": 0,
"results": [
{
"activation": "situ",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_decode_m1_ep32",
"routing": "ep_mixed",
"tokens": 1,
"top_k": 16
},
"elapsed_s": 15.5287344455719,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (6144 vs. 3072) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"elapsed_s": 11.09748888015747,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (6144 vs. 3072) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
}
],
"schema_version": 1,
"torch_version": "2.11.0+cu130"
}

View File

@ -0,0 +1,133 @@
{
"arguments": {
"activation": "situ",
"benchmark_iterations": 0,
"output": "results/phase2_target_situ_smoke_gpu6.json",
"require_situ": false,
"seed": 20260817,
"suite": "smoke",
"warmup": 3
},
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T12:00:47.947426+08:00",
"cuda_version": "13.0",
"cuda_visible_devices": "6",
"device": "NVIDIA RTX 6000D",
"flashinfer_version": "0.6.18",
"logical_cuda_device": 0,
"results": [
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m1_uniform",
"routing": "uniform",
"tokens": 1,
"top_k": 2
},
"elapsed_s": 2.840806484222412,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m2_skewed",
"routing": "skewed",
"tokens": 2,
"top_k": 2
},
"elapsed_s": 0.03530144691467285,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m8_empty_experts",
"routing": "skewed",
"tokens": 8,
"top_k": 2
},
"elapsed_s": 0.016995668411254883,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m32_masked_slot",
"routing": "masked",
"tokens": 32,
"top_k": 4
},
"elapsed_s": 0.01120138168334961,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 2,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "ep2_global_ids",
"routing": "ep_mixed",
"tokens": 8,
"top_k": 4
},
"elapsed_s": 0.007741689682006836,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": true,
"intermediate": 128,
"local_experts": 4,
"name": "nonfinite_activation",
"routing": "uniform",
"tokens": 2,
"top_k": 2
},
"elapsed_s": 0.007404804229736328,
"error": "Check failed: fc1_expert_weights.size(1) == fc2_expert_weights.size(2) * mInnerDimMultiplier (256 vs. 128) : fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.",
"status": "EXPECTED_MISSING_SITU"
}
],
"schema_version": 1,
"torch_version": "2.11.0+cu130"
}

View File

@ -0,0 +1,296 @@
{
"arguments": {
"activation": "situ",
"benchmark_iterations": 3,
"output": "results/phase3_target_situ_all_gpu6_v3.json",
"require_situ": true,
"seed": 20260817,
"suite": "all",
"warmup": 2
},
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T14:54:33.526558+08:00",
"cuda_version": "13.0",
"cuda_visible_devices": "6",
"device": "NVIDIA RTX 6000D",
"flashinfer_version": "0.6.18",
"logical_cuda_device": 0,
"results": [
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m1_uniform",
"routing": "uniform",
"tokens": 1,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 1.2600979804992676,
"latency_ms": {
"candidate": 0.24848000208536783,
"reference": 0.7109653155008951
},
"metrics": {
"cosine": 0.9996331930160522,
"finite_elements": 128,
"max_abs": 0.046875,
"nrmse": 0.038191329687833786,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "decode_m2_skewed",
"routing": "skewed",
"tokens": 2,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.041979312896728516,
"latency_ms": {
"candidate": 0.24086399873097739,
"reference": 0.6774506568908691
},
"metrics": {
"cosine": 0.9996317625045776,
"finite_elements": 256,
"max_abs": 0.046875,
"nrmse": 0.03233937919139862,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m8_empty_experts",
"routing": "skewed",
"tokens": 8,
"top_k": 2
},
"cuda_graph": {
"replay_matches_eager": {
"cosine": 0.9999998807907104,
"finite_elements": 1024,
"max_abs": 0.0,
"nrmse": 0.0,
"same_nonfinite_mask": true
},
"replays_bitwise_equal": true
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.0908958911895752,
"latency_ms": {
"candidate": 0.23886932929356894,
"reference": 0.7901653448740641
},
"metrics": {
"cosine": 0.9996166229248047,
"finite_elements": 1024,
"max_abs": 0.046875,
"nrmse": 0.02830333448946476,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 8,
"name": "batch_m32_masked_slot",
"routing": "masked",
"tokens": 32,
"top_k": 4
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.02089524269104004,
"latency_ms": {
"candidate": 0.22908800840377808,
"reference": 1.0701546669006348
},
"metrics": {
"cosine": 0.9996225237846375,
"finite_elements": 4096,
"max_abs": 0.03125,
"nrmse": 0.027592986822128296,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 2,
"hidden": 128,
"inject_nonfinite": false,
"intermediate": 128,
"local_experts": 4,
"name": "ep2_global_ids",
"routing": "ep_mixed",
"tokens": 8,
"top_k": 4
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.07790017127990723,
"latency_ms": {
"candidate": 0.23056000471115112,
"reference": 1.2077759901682537
},
"metrics": {
"cosine": 0.9995924830436707,
"finite_elements": 1024,
"max_abs": 0.0166015625,
"nrmse": 0.02855238877236843,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 0,
"ep_size": 1,
"hidden": 128,
"inject_nonfinite": true,
"intermediate": 128,
"local_experts": 4,
"name": "nonfinite_activation",
"routing": "uniform",
"tokens": 2,
"top_k": 2
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 0.008931636810302734,
"metrics": {
"candidate_inf_elements": 0,
"candidate_nan_elements": 0,
"candidate_nonfinite_rows": [
false,
false
],
"contract": "mxfp8_satfinite_input_and_finite_candidate",
"input_payload_finite": true,
"max_scale_code": 254,
"reference_nonfinite_rows_diagnostic_only": [
false,
true
],
"scale_has_nan_code": false
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_decode_m1_ep32",
"routing": "ep_mixed",
"tokens": 1,
"top_k": 16
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 12.66703748703003,
"latency_ms": {
"candidate": 0.295904000600179,
"reference": 2.942485491434733
},
"metrics": {
"cosine": 0.9996223449707031,
"finite_elements": 3584,
"max_abs": 0.009765625,
"nrmse": 0.027483593672513962,
"same_nonfinite_mask": true
},
"status": "PASS"
},
{
"activation": "situ",
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"determinism": {
"bitwise_equal": true,
"runs": 3
},
"elapsed_s": 12.947205066680908,
"latency_ms": {
"candidate": 0.6200213432312012,
"reference": 8.530954360961914
},
"metrics": {
"cosine": 0.9996151924133301,
"finite_elements": 458752,
"max_abs": 0.013671875,
"nrmse": 0.02774309180676937,
"same_nonfinite_mask": true
},
"status": "PASS"
}
],
"schema_version": 1,
"torch_version": "2.11.0+cu130"
}

View File

@ -0,0 +1,293 @@
{
"compute_capability": [
12,
0
],
"created_at": "2026-08-17T09:47:32.426483+00:00",
"cuda_visible_devices": null,
"device": "NVIDIA RTX 6000D",
"iterations": 100,
"rounds": 5,
"shapes": {
"decode": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_decode_m1_ep32",
"routing": "ep_mixed",
"tokens": 1,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996320605278015,
"finite": true,
"max_abs": 0.00978851318359375,
"mean_abs": 0.002149919280782342,
"nrmse": 0.02712450921535492,
"p95_abs": 0.00537109375,
"p99_abs": 0.007080078125,
"reference_max_abs": 0.37890625,
"reference_rms": 0.09998540580272675
},
"flashinfer_vs_reference": {
"cosine": 0.9996223449707031,
"finite": true,
"max_abs": 0.009765625,
"mean_abs": 0.00218405993655324,
"nrmse": 0.027483593672513962,
"p95_abs": 0.00537109375,
"p99_abs": 0.0072021484375,
"reference_max_abs": 0.37890625,
"reference_rms": 0.10006484389305115
},
"marlin_vs_reference": {
"cosine": 0.9999805688858032,
"finite": true,
"max_abs": 0.00390625,
"mean_abs": 0.00045798509381711483,
"nrmse": 0.0062673031352460384,
"p95_abs": 0.0009765625,
"p99_abs": 0.001953125,
"reference_max_abs": 0.37890625,
"reference_rms": 0.10006484389305115
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 0.3265369415283203,
"wall_ms": 0.32803289126604795
},
"marlin": {
"gpu_ms": 0.30863103866577146,
"wall_ms": 0.3124843700788915
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 0.3199043273925781,
"wall_ms": 0.32471023965626955
},
"marlin": {
"gpu_ms": 0.3098236846923828,
"wall_ms": 0.3145282808691263
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 0.32463966369628905,
"wall_ms": 0.3285454004071653
},
"marlin": {
"gpu_ms": 0.30631423950195313,
"wall_ms": 0.3102407115511596
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 0.31916448593139646,
"wall_ms": 0.32354930881410837
},
"marlin": {
"gpu_ms": 0.3083350372314453,
"wall_ms": 0.3130973596125841
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 0.3248342514038086,
"wall_ms": 0.326665451284498
},
"marlin": {
"gpu_ms": 0.31008224487304686,
"wall_ms": 0.31198404962196946
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 0.32463966369628905,
"wall_ms": 0.326665451284498
},
"marlin": {
"gpu_ms": 0.30863103866577146,
"wall_ms": 0.3124843700788915
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 0.9506880186843276,
"wall_ms": 0.9565883654061232
}
}
},
"prefill": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996392726898193,
"finite": true,
"max_abs": 0.0126953125,
"mean_abs": 0.0021022639703005552,
"nrmse": 0.02686966024339199,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.462890625,
"reference_rms": 0.09850681573152542
},
"flashinfer_vs_reference": {
"cosine": 0.9996151924133301,
"finite": true,
"max_abs": 0.013671875,
"mean_abs": 0.00217337254434824,
"nrmse": 0.02774309180676937,
"p95_abs": 0.00537109375,
"p99_abs": 0.00689697265625,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
},
"marlin_vs_reference": {
"cosine": 0.9999709129333496,
"finite": true,
"max_abs": 0.00390625,
"mean_abs": 0.000566127069760114,
"nrmse": 0.007645560894161463,
"p95_abs": 0.00146484375,
"p99_abs": 0.001953125,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 0.9432563018798829,
"wall_ms": 0.9479056694544852
},
"marlin": {
"gpu_ms": 0.8844432067871094,
"wall_ms": 0.8977009798400104
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 0.9416944122314453,
"wall_ms": 0.9463393897749484
},
"marlin": {
"gpu_ms": 0.88412353515625,
"wall_ms": 0.8888133312575519
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 0.9426467132568359,
"wall_ms": 0.9474158892408013
},
"marlin": {
"gpu_ms": 0.8948384094238281,
"wall_ms": 0.8993784594349563
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 0.9381990051269531,
"wall_ms": 0.9428730909712613
},
"marlin": {
"gpu_ms": 0.89110595703125,
"wall_ms": 0.8956197183579206
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 0.9436812591552735,
"wall_ms": 0.9483181592077017
},
"marlin": {
"gpu_ms": 0.8909331512451172,
"wall_ms": 0.895569019485265
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 0.9426467132568359,
"wall_ms": 0.9474158892408013
},
"marlin": {
"gpu_ms": 0.8909331512451172,
"wall_ms": 0.8956197183579206
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 0.945140038909117,
"wall_ms": 0.9453290033752898
}
}
}
}
}

View File

@ -0,0 +1,173 @@
{
"compute_capability": [
12,
0
],
"created_at": "2026-08-18T02:17:06.360422+00:00",
"cuda_visible_devices": null,
"device": "NVIDIA RTX 6000D",
"iterations": 1,
"rounds": 1,
"shapes": {
"decode": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_decode_m1_ep32",
"routing": "ep_mixed",
"tokens": 1,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996320605278015,
"finite": true,
"max_abs": 0.00978851318359375,
"mean_abs": 0.002149919280782342,
"nrmse": 0.02712450921535492,
"p95_abs": 0.00537109375,
"p99_abs": 0.007080078125,
"reference_max_abs": 0.37890625,
"reference_rms": 0.09998540580272675
},
"flashinfer_vs_reference": {
"cosine": 0.9996223449707031,
"finite": true,
"max_abs": 0.009765625,
"mean_abs": 0.00218405993655324,
"nrmse": 0.027483593672513962,
"p95_abs": 0.00537109375,
"p99_abs": 0.0072021484375,
"reference_max_abs": 0.37890625,
"reference_rms": 0.10006484389305115
},
"marlin_vs_reference": {
"cosine": 0.9999805688858032,
"finite": true,
"max_abs": 0.00390625,
"mean_abs": 0.00045798509381711483,
"nrmse": 0.0062673031352460384,
"p95_abs": 0.0009765625,
"p99_abs": 0.001953125,
"reference_max_abs": 0.37890625,
"reference_rms": 0.10006484389305115
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 0.5889919996261597,
"wall_ms": 0.6109171081334352
},
"marlin": {
"gpu_ms": 0.5362880229949951,
"wall_ms": 0.6202820222824812
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
}
],
"summary": {
"flashinfer": {
"gpu_ms": 0.5889919996261597,
"wall_ms": 0.6109171081334352
},
"marlin": {
"gpu_ms": 0.5362880229949951,
"wall_ms": 0.6202820222824812
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 0.9105183488661707,
"wall_ms": 1.015329271392741
}
}
},
"prefill": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996392726898193,
"finite": true,
"max_abs": 0.0126953125,
"mean_abs": 0.0021022639703005552,
"nrmse": 0.02686966024339199,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.462890625,
"reference_rms": 0.09850681573152542
},
"flashinfer_vs_reference": {
"cosine": 0.9996151924133301,
"finite": true,
"max_abs": 0.013671875,
"mean_abs": 0.00217337254434824,
"nrmse": 0.02774309180676937,
"p95_abs": 0.00537109375,
"p99_abs": 0.00689697265625,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
},
"marlin_vs_reference": {
"cosine": 0.9999709129333496,
"finite": true,
"max_abs": 0.00390625,
"mean_abs": 0.000566127069760114,
"nrmse": 0.007645560894161463,
"p95_abs": 0.00146484375,
"p99_abs": 0.001953125,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 0.9108160138130188,
"wall_ms": 0.9314471390098333
},
"marlin": {
"gpu_ms": 0.813152015209198,
"wall_ms": 0.8787710685282946
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
}
],
"summary": {
"flashinfer": {
"gpu_ms": 0.9108160138130188,
"wall_ms": 0.9314471390098333
},
"marlin": {
"gpu_ms": 0.813152015209198,
"wall_ms": 0.8787710685282946
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 0.8927730769741712,
"wall_ms": 0.943447063955196
}
}
}
}
}

View File

@ -0,0 +1,573 @@
{
"compute_capability": [
12,
0
],
"created_at": "2026-08-18T02:53:56.080340+00:00",
"cuda_visible_devices": "0",
"device": "NVIDIA RTX 6000D",
"iterations": 30,
"rounds": 5,
"shapes": {
"prefill": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m128_ep32",
"routing": "ep_mixed",
"tokens": 128,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996392726898193,
"finite": true,
"max_abs": 0.0126953125,
"mean_abs": 0.0021022639703005552,
"nrmse": 0.02686966024339199,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.462890625,
"reference_rms": 0.09850681573152542
},
"flashinfer_vs_reference": {
"cosine": 0.9996151924133301,
"finite": true,
"max_abs": 0.013671875,
"mean_abs": 0.00217337254434824,
"nrmse": 0.02774309180676937,
"p95_abs": 0.00537109375,
"p99_abs": 0.00689697265625,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
},
"marlin_vs_reference": {
"cosine": 0.9999709129333496,
"finite": true,
"max_abs": 0.00390625,
"mean_abs": 0.000566127069760114,
"nrmse": 0.007645560894161463,
"p95_abs": 0.00146484375,
"p99_abs": 0.001953125,
"reference_max_abs": 0.46484375,
"reference_rms": 0.09856347739696503
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 0.9099744160970052,
"wall_ms": 0.9106991346925497
},
"marlin": {
"gpu_ms": 0.8695210774739583,
"wall_ms": 0.871785699079434
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 0.910638936360677,
"wall_ms": 0.91134087027361
},
"marlin": {
"gpu_ms": 0.8706239700317383,
"wall_ms": 0.8713232663770517
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 0.928394635518392,
"wall_ms": 0.9290497982874513
},
"marlin": {
"gpu_ms": 0.8668832143147787,
"wall_ms": 0.8675735288610061
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 0.9538229624430339,
"wall_ms": 0.9544791265701255
},
"marlin": {
"gpu_ms": 0.8697290420532227,
"wall_ms": 0.8704239269718528
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 0.9259029388427734,
"wall_ms": 0.9265734348446131
},
"marlin": {
"gpu_ms": 0.8654357274373372,
"wall_ms": 0.8660859350735942
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 0.9259029388427734,
"wall_ms": 0.9265734348446131
},
"marlin": {
"gpu_ms": 0.8695210774739583,
"wall_ms": 0.8704239269718528
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 0.9391060779661385,
"wall_ms": 0.9394009090254388
}
}
},
"prefill_m2048": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m2048_ep32",
"routing": "ep_mixed",
"tokens": 2048,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996377229690552,
"finite": true,
"max_abs": 0.013671875,
"mean_abs": 0.0021010376513004303,
"nrmse": 0.026925193145871162,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.5546875,
"reference_rms": 0.09821589291095734
},
"flashinfer_vs_reference": {
"cosine": 0.9996229410171509,
"finite": true,
"max_abs": 0.013671875,
"mean_abs": 0.002144214231520891,
"nrmse": 0.027460230514407158,
"p95_abs": 0.00537109375,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.55859375,
"reference_rms": 0.09827093780040741
},
"marlin_vs_reference": {
"cosine": 0.9999799132347107,
"finite": true,
"max_abs": 0.005859375,
"mean_abs": 0.00045405104174278677,
"nrmse": 0.006360496394336224,
"p95_abs": 0.0009765625,
"p99_abs": 0.001953125,
"reference_max_abs": 0.55859375,
"reference_rms": 0.09827093780040741
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 4.343468729654948,
"wall_ms": 4.344448462749521
},
"marlin": {
"gpu_ms": 9.073473103841145,
"wall_ms": 9.075486830746135
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 4.388905843098958,
"wall_ms": 4.389951967944701
},
"marlin": {
"gpu_ms": 9.065022786458334,
"wall_ms": 9.065993401842812
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 4.355183919270833,
"wall_ms": 4.35632496761779
},
"marlin": {
"gpu_ms": 9.069696044921875,
"wall_ms": 9.070580204327902
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 4.35906016031901,
"wall_ms": 4.360084631480277
},
"marlin": {
"gpu_ms": 9.070074462890625,
"wall_ms": 9.071069831649462
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 4.34208984375,
"wall_ms": 4.343056639966865
},
"marlin": {
"gpu_ms": 9.072443644205729,
"wall_ms": 9.073465705538789
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 4.355183919270833,
"wall_ms": 4.35632496761779
},
"marlin": {
"gpu_ms": 9.070074462890625,
"wall_ms": 9.071069831649462
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 2.0825927517681464,
"wall_ms": 2.082275748269046
}
}
},
"prefill_m512": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m512_ep32",
"routing": "ep_mixed",
"tokens": 512,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996380805969238,
"finite": true,
"max_abs": 0.01318359375,
"mean_abs": 0.0021038446575403214,
"nrmse": 0.026911206543445587,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.5,
"reference_rms": 0.0984053835272789
},
"flashinfer_vs_reference": {
"cosine": 0.9996137619018555,
"finite": true,
"max_abs": 0.012939453125,
"mean_abs": 0.002174302702769637,
"nrmse": 0.027790850028395653,
"p95_abs": 0.00537109375,
"p99_abs": 0.0069580078125,
"reference_max_abs": 0.498046875,
"reference_rms": 0.09845462441444397
},
"marlin_vs_reference": {
"cosine": 0.9999691247940063,
"finite": true,
"max_abs": 0.005859375,
"mean_abs": 0.0005840405938215554,
"nrmse": 0.007868881337344646,
"p95_abs": 0.00146484375,
"p99_abs": 0.001953125,
"reference_max_abs": 0.498046875,
"reference_rms": 0.09845462441444397
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 1.81865603129069,
"wall_ms": 1.8193970046316583
},
"marlin": {
"gpu_ms": 2.5967445373535156,
"wall_ms": 2.5989100647469363
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 1.7714890797932943,
"wall_ms": 1.7721137730404735
},
"marlin": {
"gpu_ms": 2.600511932373047,
"wall_ms": 2.6012318286423883
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 1.7895263671875,
"wall_ms": 1.7902158355961244
},
"marlin": {
"gpu_ms": 2.6133761088053387,
"wall_ms": 2.6140614645555615
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 1.7714154561360678,
"wall_ms": 1.7720839319129784
},
"marlin": {
"gpu_ms": 2.6104395548502604,
"wall_ms": 2.611126432505747
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 1.7820064544677734,
"wall_ms": 1.7826298717409372
},
"marlin": {
"gpu_ms": 2.6109214782714845,
"wall_ms": 2.6116128312423825
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 1.7820064544677734,
"wall_ms": 1.7826298717409372
},
"marlin": {
"gpu_ms": 2.6104395548502604,
"wall_ms": 2.611126432505747
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 1.4648878225471482,
"wall_ms": 1.4647608423366596
}
}
},
"prefill_m8192": {
"case": {
"ep_rank": 7,
"ep_size": 32,
"hidden": 3584,
"inject_nonfinite": false,
"intermediate": 3072,
"local_experts": 28,
"name": "kimi_prefill_m8192_ep32",
"routing": "ep_mixed",
"tokens": 8192,
"top_k": 16
},
"correctness": {
"flashinfer_vs_marlin": {
"cosine": 0.9996379613876343,
"finite": true,
"max_abs": 0.0146484375,
"mean_abs": 0.0021007475443184376,
"nrmse": 0.0269180778414011,
"p95_abs": 0.005126953125,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.5703125,
"reference_rms": 0.09823954105377197
},
"flashinfer_vs_reference": {
"cosine": 0.9996234178543091,
"finite": true,
"max_abs": 0.0146484375,
"mean_abs": 0.0021431385539472103,
"nrmse": 0.027442343533039093,
"p95_abs": 0.00537109375,
"p99_abs": 0.0068359375,
"reference_max_abs": 0.5703125,
"reference_rms": 0.09829506278038025
},
"marlin_vs_reference": {
"cosine": 0.9999799132347107,
"finite": true,
"max_abs": 0.005859375,
"mean_abs": 0.0004543001123238355,
"nrmse": 0.006363501772284508,
"p95_abs": 0.0009765625,
"p99_abs": 0.001953125,
"reference_max_abs": 0.5703125,
"reference_rms": 0.09829506278038025
}
},
"rounds": [
{
"flashinfer": {
"gpu_ms": 16.167036946614584,
"wall_ms": 16.168367435845237
},
"marlin": {
"gpu_ms": 34.38465983072917,
"wall_ms": 34.38796966802329
},
"order": [
"marlin",
"flashinfer"
],
"round": 1
},
{
"flashinfer": {
"gpu_ms": 16.178935750325522,
"wall_ms": 16.179970768280327
},
"marlin": {
"gpu_ms": 34.393994140625,
"wall_ms": 34.39523999889692
},
"order": [
"flashinfer",
"marlin"
],
"round": 2
},
{
"flashinfer": {
"gpu_ms": 16.147456868489584,
"wall_ms": 16.148571266482275
},
"marlin": {
"gpu_ms": 34.362894694010414,
"wall_ms": 34.36435852975895
},
"order": [
"marlin",
"flashinfer"
],
"round": 3
},
{
"flashinfer": {
"gpu_ms": 16.169014485677085,
"wall_ms": 16.169893369078636
},
"marlin": {
"gpu_ms": 34.378727213541666,
"wall_ms": 34.37981543441614
},
"order": [
"flashinfer",
"marlin"
],
"round": 4
},
{
"flashinfer": {
"gpu_ms": 16.138995361328124,
"wall_ms": 16.140034631825984
},
"marlin": {
"gpu_ms": 34.34004313151042,
"wall_ms": 34.341279161162674
},
"order": [
"marlin",
"flashinfer"
],
"round": 5
}
],
"summary": {
"flashinfer": {
"gpu_ms": 16.167036946614584,
"wall_ms": 16.168367435845237
},
"marlin": {
"gpu_ms": 34.378727213541666,
"wall_ms": 34.37981543441614
},
"speedup_marlin_over_flashinfer": {
"gpu_ms": 2.126470504586844,
"wall_ms": 2.126362823632778
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
Time (%),Total Time (ns),Num Calls,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
87.0,139383,10,13938.3,11355.5,9370,21685,4468.6,cudaLaunchKernelExC
7.2,11500,12,958.3,916.0,880,1324,122.6,cudaGetDriverEntryPointByVersion
5.8,9349,12,779.1,705.5,565,1516,256.7,cuTensorMapEncodeTiled
1 Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 87.0 139383 10 13938.3 11355.5 9370 21685 4468.6 cudaLaunchKernelExC
3 7.2 11500 12 958.3 916.0 880 1324 122.6 cudaGetDriverEntryPointByVersion
4 5.8 9349 12 779.1 705.5 565 1516 256.7 cuTensorMapEncodeTiled

View File

@ -0,0 +1,4 @@
Time (%),Total Time (ns),Num Calls,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
85.2,127306,10,12730.6,10292.0,8707,19421,4222.6,cudaLaunchKernelExC
7.8,11727,12,977.3,641.0,518,3906,959.1,cuTensorMapEncodeTiled
7.0,10386,12,865.5,818.5,790,1158,114.1,cudaGetDriverEntryPointByVersion
1 Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 85.2 127306 10 12730.6 10292.0 8707 19421 4222.6 cudaLaunchKernelExC
3 7.8 11727 12 977.3 641.0 518 3906 959.1 cuTensorMapEncodeTiled
4 7.0 10386 12 865.5 818.5 790 1158 114.1 cudaGetDriverEntryPointByVersion

View File

@ -0,0 +1,5 @@
Time (%),Total Time (ns),Num Calls,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
61.2,71109,5,14221.8,14981.0,12385,15556,1523.4,cudaLaunchKernel
24.7,28632,2,14316.0,14316.0,13516,15116,1131.4,cuLaunchKernelEx
11.3,13082,1,13082.0,13082.0,13082,13082,0.0,cudaLaunchKernelExC
2.8,3307,5,661.4,627.0,593,747,67.7,cuKernelGetName
1 Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 61.2 71109 5 14221.8 14981.0 12385 15556 1523.4 cudaLaunchKernel
3 24.7 28632 2 14316.0 14316.0 13516 15116 1131.4 cuLaunchKernelEx
4 11.3 13082 1 13082.0 13082.0 13082 13082 0.0 cudaLaunchKernelExC
5 2.8 3307 5 661.4 627.0 593 747 67.7 cuKernelGetName

View File

@ -0,0 +1,7 @@
Time (%),Total Time (ns),Instances,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
94.4,109374,2,54687.0,54687.0,38783,70591,22491.7,"void sglang::device::marlin_moe::Marlin<__nv_bfloat16, (long)562949953487106, (long)2814749767106568, (int)128, (int)1, (int)8, (int)4, (bool)1, (int)4, (int)2, (bool)0, (bool)1, (bool)0>(const int4 *, const int4 *, int4 *, int4 *, const int4 *, const int4 *, const unsigned short *, const int4 *, const int *, const int *, const int *, const int *, const float *, int, bool, bool, int, int, int, int, int *, bool, bool, bool, int)"
1.6,1888,1,1888.0,1888.0,1888,1888,0.0,"void sglang::topk_sum_kernel<(int)256, (bool)1>(sglang::TopkSumParams)"
1.2,1344,1,1344.0,1344.0,1344,1344,0.0,_moe_align_small_numel_kernel
1.1,1312,1,1312.0,1312.0,1312,1312,0.0,_situ_and_mul_kernel
1.1,1248,2,624.0,624.0,608,640,22.6,"void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<c10::BFloat16>, std::array<char *, (unsigned long)1>>(int, T2, T3)"
0.6,704,1,704.0,704.0,704,704,0.0,"void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<int>, std::array<char *, (unsigned long)1>>(int, T2, T3)"
1 Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 94.4 109374 2 54687.0 54687.0 38783 70591 22491.7 void sglang::device::marlin_moe::Marlin<__nv_bfloat16, (long)562949953487106, (long)2814749767106568, (int)128, (int)1, (int)8, (int)4, (bool)1, (int)4, (int)2, (bool)0, (bool)1, (bool)0>(const int4 *, const int4 *, int4 *, int4 *, const int4 *, const int4 *, const unsigned short *, const int4 *, const int *, const int *, const int *, const int *, const float *, int, bool, bool, int, int, int, int, int *, bool, bool, bool, int)
3 1.6 1888 1 1888.0 1888.0 1888 1888 0.0 void sglang::topk_sum_kernel<(int)256, (bool)1>(sglang::TopkSumParams)
4 1.2 1344 1 1344.0 1344.0 1344 1344 0.0 _moe_align_small_numel_kernel
5 1.1 1312 1 1312.0 1312.0 1312 1312 0.0 _situ_and_mul_kernel
6 1.1 1248 2 624.0 624.0 608 640 22.6 void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<c10::BFloat16>, std::array<char *, (unsigned long)1>>(int, T2, T3)
7 0.6 704 1 704.0 704.0 704 704 0.0 void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<int>, std::array<char *, (unsigned long)1>>(int, T2, T3)

View File

@ -0,0 +1,5 @@
Time (%),Total Time (ns),Num Calls,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
75.4,96744,7,13820.6,14257.0,10984,15791,1900.7,cudaLaunchKernel
11.6,14830,1,14830.0,14830.0,14830,14830,0.0,cuLaunchKernelEx
9.7,12416,1,12416.0,12416.0,12416,12416,0.0,cudaLaunchKernelExC
3.3,4295,7,613.6,582.0,475,798,114.7,cuKernelGetName
1 Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 75.4 96744 7 13820.6 14257.0 10984 15791 1900.7 cudaLaunchKernel
3 11.6 14830 1 14830.0 14830.0 14830 14830 0.0 cuLaunchKernelEx
4 9.7 12416 1 12416.0 12416.0 12416 12416 0.0 cudaLaunchKernelExC
5 3.3 4295 7 613.6 582.0 475 798 114.7 cuKernelGetName

View File

@ -0,0 +1,8 @@
Time (%),Total Time (ns),Instances,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
94.1,709879,3,236626.3,181534.0,180702,347643,96144.2,"void sglang::device::marlin_moe::Marlin<__nv_bfloat16, (long)562949953487106, (long)2814749767106568, (int)256, (int)4, (int)16, (int)4, (bool)0, (int)4, (int)2, (bool)0, (bool)1, (bool)0>(const int4 *, const int4 *, int4 *, int4 *, const int4 *, const int4 *, const unsigned short *, const int4 *, const int *, const int *, const int *, const int *, const float *, int, bool, bool, int, int, int, int, int *, bool, bool, bool, int)"
2.2,16671,1,16671.0,16671.0,16671,16671,0.0,_situ_and_mul_kernel
1.7,13120,2,6560.0,6560.0,4896,8224,2353.3,"void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<c10::BFloat16>, std::array<char *, (unsigned long)1>>(int, T2, T3)"
1.1,8448,1,8448.0,8448.0,8448,8448,0.0,"void sglang::topk_sum_kernel<(int)256, (bool)1>(sglang::TopkSumParams)"
0.5,4064,1,4064.0,4064.0,4064,4064,0.0,"void moe_align_block_size_kernel<int>(const T1 *, int *, int *, int *, int, int, unsigned long, int *, bool, bool, int, int)"
0.2,1696,1,1696.0,1696.0,1696,1696,0.0,"void count_and_sort_expert_tokens_kernel<int>(const T1 *, int *, int *, unsigned long, bool)"
0.1,768,1,768.0,768.0,768,768,0.0,"void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<int>, std::array<char *, (unsigned long)1>>(int, T2, T3)"
1 Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 94.1 709879 3 236626.3 181534.0 180702 347643 96144.2 void sglang::device::marlin_moe::Marlin<__nv_bfloat16, (long)562949953487106, (long)2814749767106568, (int)256, (int)4, (int)16, (int)4, (bool)0, (int)4, (int)2, (bool)0, (bool)1, (bool)0>(const int4 *, const int4 *, int4 *, int4 *, const int4 *, const int4 *, const unsigned short *, const int4 *, const int *, const int *, const int *, const int *, const float *, int, bool, bool, int, int, int, int, int *, bool, bool, bool, int)
3 2.2 16671 1 16671.0 16671.0 16671 16671 0.0 _situ_and_mul_kernel
4 1.7 13120 2 6560.0 6560.0 4896 8224 2353.3 void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<c10::BFloat16>, std::array<char *, (unsigned long)1>>(int, T2, T3)
5 1.1 8448 1 8448.0 8448.0 8448 8448 0.0 void sglang::topk_sum_kernel<(int)256, (bool)1>(sglang::TopkSumParams)
6 0.5 4064 1 4064.0 4064.0 4064 4064 0.0 void moe_align_block_size_kernel<int>(const T1 *, int *, int *, int *, int, int, unsigned long, int *, bool, bool, int, int)
7 0.2 1696 1 1696.0 1696.0 1696 1696 0.0 void count_and_sort_expert_tokens_kernel<int>(const T1 *, int *, int *, unsigned long, bool)
8 0.1 768 1 768.0 768.0 768 768 0.0 void at::native::vectorized_elementwise_kernel<(int)4, at::native::FillFunctor<int>, std::array<char *, (unsigned long)1>>(int, T2, T3)

View File

@ -0,0 +1,9 @@
Time (%),Total Time (ns),Num Calls,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name
45.0,1657471,110,15067.9,13500.5,8707,37211,5252.2,cudaLaunchKernelExC
27.8,1024642,60,17077.4,15240.0,10648,99929,11361.2,cudaLaunchKernel
11.0,404869,1,404869.0,404869.0,404869,404869,0.0,cudaDeviceSynchronize
7.7,283828,15,18921.9,16005.0,12865,41329,8082.9,cuLaunchKernelEx
3.3,121690,120,1014.1,915.0,790,4516,414.6,cudaGetDriverEntryPointByVersion
2.9,105105,120,875.9,712.0,518,3906,507.8,cuTensorMapEncodeTiled
1.2,43805,60,730.1,650.0,468,2774,328.4,cuKernelGetName
1.1,40880,1,40880.0,40880.0,40880,40880,0.0,cuProfilerStart
1 Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
2 45.0 1657471 110 15067.9 13500.5 8707 37211 5252.2 cudaLaunchKernelExC
3 27.8 1024642 60 17077.4 15240.0 10648 99929 11361.2 cudaLaunchKernel
4 11.0 404869 1 404869.0 404869.0 404869 404869 0.0 cudaDeviceSynchronize
5 7.7 283828 15 18921.9 16005.0 12865 41329 8082.9 cuLaunchKernelEx
6 3.3 121690 120 1014.1 915.0 790 4516 414.6 cudaGetDriverEntryPointByVersion
7 2.9 105105 120 875.9 712.0 518 3906 507.8 cuTensorMapEncodeTiled
8 1.2 43805 60 730.1 650.0 468 2774 328.4 cuKernelGetName
9 1.1 40880 1 40880.0 40880.0 40880 40880 0.0 cuProfilerStart

View File

@ -0,0 +1,21 @@
Range,Style,Total Proj Time (ns),Total Range Time (ns),Range Instances,Proj Avg (ns),Proj Med (ns),Proj Min (ns),Proj Max (ns),Proj StdDev (ns),Total GPU Ops,Avg GPU Ops,Avg Range Lvl,Avg Num Child
:marlin_decode_iteration_1,PushPop,1646632,2030238,1,1646632.0,1646632.0,1646632,1646632,0.0,8,8.0,0.0,0.0
:flashinfer_prefill_iteration_1,PushPop,1499497,1236905,1,1499497.0,1499497.0,1499497,1499497,0.0,10,10.0,0.0,0.0
:marlin_prefill_iteration_1,PushPop,1400491,1514107,1,1400491.0,1400491.0,1400491,1400491,0.0,9,9.0,0.0,0.0
:marlin_decode_iteration_2,PushPop,1317581,1499161,1,1317581.0,1317581.0,1317581,1317581,0.0,8,8.0,0.0,0.0
:flashinfer_decode_iteration_1,PushPop,1304108,1433573,1,1304108.0,1304108.0,1304108,1304108,0.0,10,10.0,0.0,0.0
:marlin_prefill_iteration_2,PushPop,1252429,1302983,1,1252429.0,1252429.0,1252429,1252429,0.0,9,9.0,0.0,0.0
:marlin_prefill_iteration_3,PushPop,1181582,1220545,1,1181582.0,1181582.0,1181582,1181582,0.0,9,9.0,0.0,0.0
:marlin_decode_iteration_3,PushPop,1171887,1334668,1,1171887.0,1171887.0,1171887,1171887,0.0,8,8.0,0.0,0.0
:marlin_prefill_iteration_4,PushPop,1161231,1184045,1,1161231.0,1161231.0,1161231,1161231,0.0,9,9.0,0.0,0.0
:marlin_prefill_iteration_5,PushPop,1156238,1170223,1,1156238.0,1156238.0,1156238,1156238,0.0,9,9.0,0.0,0.0
:marlin_decode_iteration_5,PushPop,1147087,1295194,1,1147087.0,1147087.0,1147087,1147087,0.0,8,8.0,0.0,0.0
:flashinfer_prefill_iteration_2,PushPop,1132207,1082480,1,1132207.0,1132207.0,1132207,1132207,0.0,10,10.0,0.0,0.0
:marlin_decode_iteration_4,PushPop,1126735,1274663,1,1126735.0,1126735.0,1126735,1126735,0.0,8,8.0,0.0,0.0
:flashinfer_decode_iteration_2,PushPop,1081231,1167266,1,1081231.0,1081231.0,1081231,1081231,0.0,10,10.0,0.0,0.0
:flashinfer_prefill_iteration_3,PushPop,1067216,1023953,1,1067216.0,1067216.0,1067216,1067216,0.0,10,10.0,0.0,0.0
:flashinfer_prefill_iteration_4,PushPop,1003985,967242,1,1003985.0,1003985.0,1003985,1003985,0.0,10,10.0,0.0,0.0
:flashinfer_decode_iteration_3,PushPop,991185,1054475,1,991185.0,991185.0,991185,991185,0.0,10,10.0,0.0,0.0
:flashinfer_prefill_iteration_5,PushPop,955122,904533,1,955122.0,955122.0,955122,955122,0.0,10,10.0,0.0,0.0
:flashinfer_decode_iteration_4,PushPop,924402,977971,1,924402.0,924402.0,924402,924402,0.0,10,10.0,0.0,0.0
:flashinfer_decode_iteration_5,PushPop,900691,947759,1,900691.0,900691.0,900691,900691,0.0,10,10.0,0.0,0.0
1 Range Style Total Proj Time (ns) Total Range Time (ns) Range Instances Proj Avg (ns) Proj Med (ns) Proj Min (ns) Proj Max (ns) Proj StdDev (ns) Total GPU Ops Avg GPU Ops Avg Range Lvl Avg Num Child
2 :marlin_decode_iteration_1 PushPop 1646632 2030238 1 1646632.0 1646632.0 1646632 1646632 0.0 8 8.0 0.0 0.0
3 :flashinfer_prefill_iteration_1 PushPop 1499497 1236905 1 1499497.0 1499497.0 1499497 1499497 0.0 10 10.0 0.0 0.0
4 :marlin_prefill_iteration_1 PushPop 1400491 1514107 1 1400491.0 1400491.0 1400491 1400491 0.0 9 9.0 0.0 0.0
5 :marlin_decode_iteration_2 PushPop 1317581 1499161 1 1317581.0 1317581.0 1317581 1317581 0.0 8 8.0 0.0 0.0
6 :flashinfer_decode_iteration_1 PushPop 1304108 1433573 1 1304108.0 1304108.0 1304108 1304108 0.0 10 10.0 0.0 0.0
7 :marlin_prefill_iteration_2 PushPop 1252429 1302983 1 1252429.0 1252429.0 1252429 1252429 0.0 9 9.0 0.0 0.0
8 :marlin_prefill_iteration_3 PushPop 1181582 1220545 1 1181582.0 1181582.0 1181582 1181582 0.0 9 9.0 0.0 0.0
9 :marlin_decode_iteration_3 PushPop 1171887 1334668 1 1171887.0 1171887.0 1171887 1171887 0.0 8 8.0 0.0 0.0
10 :marlin_prefill_iteration_4 PushPop 1161231 1184045 1 1161231.0 1161231.0 1161231 1161231 0.0 9 9.0 0.0 0.0
11 :marlin_prefill_iteration_5 PushPop 1156238 1170223 1 1156238.0 1156238.0 1156238 1156238 0.0 9 9.0 0.0 0.0
12 :marlin_decode_iteration_5 PushPop 1147087 1295194 1 1147087.0 1147087.0 1147087 1147087 0.0 8 8.0 0.0 0.0
13 :flashinfer_prefill_iteration_2 PushPop 1132207 1082480 1 1132207.0 1132207.0 1132207 1132207 0.0 10 10.0 0.0 0.0
14 :marlin_decode_iteration_4 PushPop 1126735 1274663 1 1126735.0 1126735.0 1126735 1126735 0.0 8 8.0 0.0 0.0
15 :flashinfer_decode_iteration_2 PushPop 1081231 1167266 1 1081231.0 1081231.0 1081231 1081231 0.0 10 10.0 0.0 0.0
16 :flashinfer_prefill_iteration_3 PushPop 1067216 1023953 1 1067216.0 1067216.0 1067216 1067216 0.0 10 10.0 0.0 0.0
17 :flashinfer_prefill_iteration_4 PushPop 1003985 967242 1 1003985.0 1003985.0 1003985 1003985 0.0 10 10.0 0.0 0.0
18 :flashinfer_decode_iteration_3 PushPop 991185 1054475 1 991185.0 991185.0 991185 991185 0.0 10 10.0 0.0 0.0
19 :flashinfer_prefill_iteration_5 PushPop 955122 904533 1 955122.0 955122.0 955122 955122 0.0 10 10.0 0.0 0.0
20 :flashinfer_decode_iteration_4 PushPop 924402 977971 1 924402.0 924402.0 924402 924402 0.0 10 10.0 0.0 0.0
21 :flashinfer_decode_iteration_5 PushPop 900691 947759 1 900691.0 900691.0 900691 900691 0.0 10 10.0 0.0 0.0

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
Time (%),Total Time (ns),Instances,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Style,Range
8.2,2030238,1,2030238.0,2030238.0,2030238,2030238,0.0,PushPop,:marlin_decode_iteration_1
6.1,1514107,1,1514107.0,1514107.0,1514107,1514107,0.0,PushPop,:marlin_prefill_iteration_1
6.1,1499161,1,1499161.0,1499161.0,1499161,1499161,0.0,PushPop,:marlin_decode_iteration_2
5.8,1433573,1,1433573.0,1433573.0,1433573,1433573,0.0,PushPop,:flashinfer_decode_iteration_1
5.4,1334668,1,1334668.0,1334668.0,1334668,1334668,0.0,PushPop,:marlin_decode_iteration_3
5.3,1302983,1,1302983.0,1302983.0,1302983,1302983,0.0,PushPop,:marlin_prefill_iteration_2
5.3,1295194,1,1295194.0,1295194.0,1295194,1295194,0.0,PushPop,:marlin_decode_iteration_5
5.2,1274663,1,1274663.0,1274663.0,1274663,1274663,0.0,PushPop,:marlin_decode_iteration_4
5.0,1236905,1,1236905.0,1236905.0,1236905,1236905,0.0,PushPop,:flashinfer_prefill_iteration_1
5.0,1220545,1,1220545.0,1220545.0,1220545,1220545,0.0,PushPop,:marlin_prefill_iteration_3
4.8,1184045,1,1184045.0,1184045.0,1184045,1184045,0.0,PushPop,:marlin_prefill_iteration_4
4.8,1170223,1,1170223.0,1170223.0,1170223,1170223,0.0,PushPop,:marlin_prefill_iteration_5
4.7,1167266,1,1167266.0,1167266.0,1167266,1167266,0.0,PushPop,:flashinfer_decode_iteration_2
4.4,1082480,1,1082480.0,1082480.0,1082480,1082480,0.0,PushPop,:flashinfer_prefill_iteration_2
4.3,1054475,1,1054475.0,1054475.0,1054475,1054475,0.0,PushPop,:flashinfer_decode_iteration_3
4.2,1023953,1,1023953.0,1023953.0,1023953,1023953,0.0,PushPop,:flashinfer_prefill_iteration_3
4.0,977971,1,977971.0,977971.0,977971,977971,0.0,PushPop,:flashinfer_decode_iteration_4
3.9,967242,1,967242.0,967242.0,967242,967242,0.0,PushPop,:flashinfer_prefill_iteration_4
3.8,947759,1,947759.0,947759.0,947759,947759,0.0,PushPop,:flashinfer_decode_iteration_5
3.7,904533,1,904533.0,904533.0,904533,904533,0.0,PushPop,:flashinfer_prefill_iteration_5
1 Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Style Range
2 8.2 2030238 1 2030238.0 2030238.0 2030238 2030238 0.0 PushPop :marlin_decode_iteration_1
3 6.1 1514107 1 1514107.0 1514107.0 1514107 1514107 0.0 PushPop :marlin_prefill_iteration_1
4 6.1 1499161 1 1499161.0 1499161.0 1499161 1499161 0.0 PushPop :marlin_decode_iteration_2
5 5.8 1433573 1 1433573.0 1433573.0 1433573 1433573 0.0 PushPop :flashinfer_decode_iteration_1
6 5.4 1334668 1 1334668.0 1334668.0 1334668 1334668 0.0 PushPop :marlin_decode_iteration_3
7 5.3 1302983 1 1302983.0 1302983.0 1302983 1302983 0.0 PushPop :marlin_prefill_iteration_2
8 5.3 1295194 1 1295194.0 1295194.0 1295194 1295194 0.0 PushPop :marlin_decode_iteration_5
9 5.2 1274663 1 1274663.0 1274663.0 1274663 1274663 0.0 PushPop :marlin_decode_iteration_4
10 5.0 1236905 1 1236905.0 1236905.0 1236905 1236905 0.0 PushPop :flashinfer_prefill_iteration_1
11 5.0 1220545 1 1220545.0 1220545.0 1220545 1220545 0.0 PushPop :marlin_prefill_iteration_3
12 4.8 1184045 1 1184045.0 1184045.0 1184045 1184045 0.0 PushPop :marlin_prefill_iteration_4
13 4.8 1170223 1 1170223.0 1170223.0 1170223 1170223 0.0 PushPop :marlin_prefill_iteration_5
14 4.7 1167266 1 1167266.0 1167266.0 1167266 1167266 0.0 PushPop :flashinfer_decode_iteration_2
15 4.4 1082480 1 1082480.0 1082480.0 1082480 1082480 0.0 PushPop :flashinfer_prefill_iteration_2
16 4.3 1054475 1 1054475.0 1054475.0 1054475 1054475 0.0 PushPop :flashinfer_decode_iteration_3
17 4.2 1023953 1 1023953.0 1023953.0 1023953 1023953 0.0 PushPop :flashinfer_prefill_iteration_3
18 4.0 977971 1 977971.0 977971.0 977971 977971 0.0 PushPop :flashinfer_decode_iteration_4
19 3.9 967242 1 967242.0 967242.0 967242 967242 0.0 PushPop :flashinfer_prefill_iteration_4
20 3.8 947759 1 947759.0 947759.0 947759 947759 0.0 PushPop :flashinfer_decode_iteration_5
21 3.7 904533 1 904533.0 904533.0 904533 904533 0.0 PushPop :flashinfer_prefill_iteration_5

View File

@ -0,0 +1,13 @@
backend,chunked_prefill_size,concurrency,repeat,completed,duration,request_throughput,input_throughput,output_throughput,total_throughput,mean_ttft_ms,median_ttft_ms,p95_ttft_ms,p99_ttft_ms,mean_e2e_latency_ms,median_e2e_latency_ms,p95_e2e_latency_ms,p99_e2e_latency_ms
flashinfer_mxfp4,4096,16,1,40,221.47698600799777,0.18060567249436724,2959.043338147713,0.18060567249436724,2959.2239438202073,72603.08281157631,87635.37671801168,89080.70434568217,89951.34060245706,72603.12579314923,87635.42024500202,89080.75052604545,89951.384754295
flashinfer_mxfp4,4096,16,2,40,221.06144716404378,0.1809451648541732,2964.6055809707736,0.1809451648541732,2964.786526135628,72440.78481566976,87606.67828551959,88998.53118621977,89664.52184239868,72440.8288814302,87606.71954799909,88998.57463886729,89664.56696634414
flashinfer_mxfp4,4096,16,3,40,221.3745526280254,0.18068924149205084,2960.412532605761,0.18068924149205084,2960.593221847253,72566.08219580376,87594.9859394459,89038.73863747576,89889.50585184386,72566.12714612857,87595.03182361368,89038.78330618609,89889.55271399347
flashinfer_mxfp4,4096,8,1,40,221.64001683914103,0.1804728251262978,2956.8667668692633,0.1804728251262978,2957.0472396943896,40754.60876591387,44480.76946451329,44945.41641417891,45654.79073660448,40754.653389629675,44480.81373295281,44945.46504281461,45654.83455775771
flashinfer_mxfp4,4096,8,2,40,221.6125555918552,0.18049518852022162,2957.233168715311,0.18049518852022162,2957.4136639038315,40748.24627056951,44506.1850650236,44862.61514539365,45574.393972705584,40748.29224284622,44506.22835196555,44862.659902649466,45574.439538051374
flashinfer_mxfp4,4096,8,3,40,221.44526491500437,0.1806315434893263,2959.467208529122,0.1806315434893263,2959.647840072612,40712.37499640556,44453.15464294981,44811.94024076685,45526.09015815426,40712.42369815009,44453.20301700849,44811.989083641674,45526.13830527756
marlin,4096,16,1,40,243.22425961703993,0.16445727931490292,2694.4680642953695,0.16445727931490292,2694.6325215746842,79749.63234093157,96212.83750247676,97888.6785025592,98836.45602966193,79749.6743179392,96212.87480182946,97888.72231170535,98836.50125178043
marlin,4096,16,2,40,243.39303705701604,0.1643432387534973,2692.5996237372997,0.1643432387534973,2692.763966976053,79811.2635479949,96251.52806309052,97955.36642154912,98938.37275844067,79811.30552649265,96251.56629201956,97955.41314255679,98938.41525124619
marlin,4096,16,3,40,243.62333092605695,0.16418788729286585,2690.054345406314,0.16418788729286585,2690.218533293607,79882.47500148718,96372.09082697518,98021.89738572342,99003.82948873332,79882.51752386568,96372.13580147363,98021.94035288412,99003.87401685584
marlin,4096,8,1,40,243.21893303701654,0.16446088098705797,2694.5270740919577,0.16446088098705797,2694.6915349729447,44725.21210900741,48850.54129792843,49209.063365869224,50020.41739182314,44725.258720549755,48850.58849258348,49209.11144018173,50020.46818676172
marlin,4096,8,2,40,243.2142291299533,0.16446406175778208,2694.5791878395016,0.16446406175778208,2694.743651901259,44724.67264174484,48868.09575161897,49182.01875761151,50006.30366436206,44724.714723881334,48868.13376657665,49182.06452928716,50006.34664747398
marlin,4096,8,3,40,243.25217532995157,0.1644384061344705,2694.1588461071647,0.1644384061344705,2694.323284513299,44730.88195841992,48857.85997705534,49201.6794851399,50016.33869816782,44730.92947611003,48857.90269903373,49201.72802127199,50016.38562648789
1 backend chunked_prefill_size concurrency repeat completed duration request_throughput input_throughput output_throughput total_throughput mean_ttft_ms median_ttft_ms p95_ttft_ms p99_ttft_ms mean_e2e_latency_ms median_e2e_latency_ms p95_e2e_latency_ms p99_e2e_latency_ms
2 flashinfer_mxfp4 4096 16 1 40 221.47698600799777 0.18060567249436724 2959.043338147713 0.18060567249436724 2959.2239438202073 72603.08281157631 87635.37671801168 89080.70434568217 89951.34060245706 72603.12579314923 87635.42024500202 89080.75052604545 89951.384754295
3 flashinfer_mxfp4 4096 16 2 40 221.06144716404378 0.1809451648541732 2964.6055809707736 0.1809451648541732 2964.786526135628 72440.78481566976 87606.67828551959 88998.53118621977 89664.52184239868 72440.8288814302 87606.71954799909 88998.57463886729 89664.56696634414
4 flashinfer_mxfp4 4096 16 3 40 221.3745526280254 0.18068924149205084 2960.412532605761 0.18068924149205084 2960.593221847253 72566.08219580376 87594.9859394459 89038.73863747576 89889.50585184386 72566.12714612857 87595.03182361368 89038.78330618609 89889.55271399347
5 flashinfer_mxfp4 4096 8 1 40 221.64001683914103 0.1804728251262978 2956.8667668692633 0.1804728251262978 2957.0472396943896 40754.60876591387 44480.76946451329 44945.41641417891 45654.79073660448 40754.653389629675 44480.81373295281 44945.46504281461 45654.83455775771
6 flashinfer_mxfp4 4096 8 2 40 221.6125555918552 0.18049518852022162 2957.233168715311 0.18049518852022162 2957.4136639038315 40748.24627056951 44506.1850650236 44862.61514539365 45574.393972705584 40748.29224284622 44506.22835196555 44862.659902649466 45574.439538051374
7 flashinfer_mxfp4 4096 8 3 40 221.44526491500437 0.1806315434893263 2959.467208529122 0.1806315434893263 2959.647840072612 40712.37499640556 44453.15464294981 44811.94024076685 45526.09015815426 40712.42369815009 44453.20301700849 44811.989083641674 45526.13830527756
8 marlin 4096 16 1 40 243.22425961703993 0.16445727931490292 2694.4680642953695 0.16445727931490292 2694.6325215746842 79749.63234093157 96212.83750247676 97888.6785025592 98836.45602966193 79749.6743179392 96212.87480182946 97888.72231170535 98836.50125178043
9 marlin 4096 16 2 40 243.39303705701604 0.1643432387534973 2692.5996237372997 0.1643432387534973 2692.763966976053 79811.2635479949 96251.52806309052 97955.36642154912 98938.37275844067 79811.30552649265 96251.56629201956 97955.41314255679 98938.41525124619
10 marlin 4096 16 3 40 243.62333092605695 0.16418788729286585 2690.054345406314 0.16418788729286585 2690.218533293607 79882.47500148718 96372.09082697518 98021.89738572342 99003.82948873332 79882.51752386568 96372.13580147363 98021.94035288412 99003.87401685584
11 marlin 4096 8 1 40 243.21893303701654 0.16446088098705797 2694.5270740919577 0.16446088098705797 2694.6915349729447 44725.21210900741 48850.54129792843 49209.063365869224 50020.41739182314 44725.258720549755 48850.58849258348 49209.11144018173 50020.46818676172
12 marlin 4096 8 2 40 243.2142291299533 0.16446406175778208 2694.5791878395016 0.16446406175778208 2694.743651901259 44724.67264174484 48868.09575161897 49182.01875761151 50006.30366436206 44724.714723881334 48868.13376657665 49182.06452928716 50006.34664747398
13 marlin 4096 8 3 40 243.25217532995157 0.1644384061344705 2694.1588461071647 0.1644384061344705 2694.323284513299 44730.88195841992 48857.85997705534 49201.6794851399 50016.33869816782 44730.92947611003 48857.90269903373 49201.72802127199 50016.38562648789

View File

@ -0,0 +1,109 @@
{
"run_id": "kimi3-ep4-moe-chunk4k-20260819-150632",
"summary": [
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 4096,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.18049518852022162,
"median_input_throughput": 2957.233168715311,
"median_total_throughput": 2957.4136639038315,
"median_mean_ttft_ms": 40748.24627056951,
"median_median_ttft_ms": 44480.76946451329,
"median_p95_ttft_ms": 44862.61514539365,
"median_p99_ttft_ms": 45574.393972705584,
"median_mean_e2e_latency_ms": 40748.29224284622,
"median_median_e2e_latency_ms": 44480.81373295281,
"median_p95_e2e_latency_ms": 44862.659902649466
},
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 4096,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.18068924149205084,
"median_input_throughput": 2960.412532605761,
"median_total_throughput": 2960.593221847253,
"median_mean_ttft_ms": 72566.08219580376,
"median_median_ttft_ms": 87606.67828551959,
"median_p95_ttft_ms": 89038.73863747576,
"median_p99_ttft_ms": 89889.50585184386,
"median_mean_e2e_latency_ms": 72566.12714612857,
"median_median_e2e_latency_ms": 87606.71954799909,
"median_p95_e2e_latency_ms": 89038.78330618609
},
{
"backend": "marlin",
"chunked_prefill_size": 4096,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.16446088098705797,
"median_input_throughput": 2694.5270740919577,
"median_total_throughput": 2694.6915349729447,
"median_mean_ttft_ms": 44725.21210900741,
"median_median_ttft_ms": 48857.85997705534,
"median_p95_ttft_ms": 49201.6794851399,
"median_p99_ttft_ms": 50016.33869816782,
"median_mean_e2e_latency_ms": 44725.258720549755,
"median_median_e2e_latency_ms": 48857.90269903373,
"median_p95_e2e_latency_ms": 49201.72802127199
},
{
"backend": "marlin",
"chunked_prefill_size": 4096,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.1643432387534973,
"median_input_throughput": 2692.5996237372997,
"median_total_throughput": 2692.763966976053,
"median_mean_ttft_ms": 79811.2635479949,
"median_median_ttft_ms": 96251.52806309052,
"median_p95_ttft_ms": 97955.36642154912,
"median_p99_ttft_ms": 98938.37275844067,
"median_mean_e2e_latency_ms": 79811.30552649265,
"median_median_e2e_latency_ms": 96251.56629201956,
"median_p95_e2e_latency_ms": 97955.41314255679
}
],
"comparisons": [
{
"chunked_prefill_size": 4096,
"concurrency": 8,
"flashinfer_input_throughput_change_pct": 9.749617925508659,
"flashinfer_total_throughput_change_pct": 9.749617925508659,
"flashinfer_median_ttft_ms_change_pct": -8.958825692729121,
"flashinfer_p95_ttft_ms_change_pct": -8.818935420805618,
"flashinfer_median_e2e_latency_ms_change_pct": -8.958814693794636
},
{
"chunked_prefill_size": 4096,
"concurrency": 16,
"flashinfer_input_throughput_change_pct": 9.946258125697117,
"flashinfer_total_throughput_change_pct": 9.946258125697138,
"flashinfer_median_ttft_ms_change_pct": -8.981519516141546,
"flashinfer_p95_ttft_ms_change_pct": -9.102745576695416,
"flashinfer_median_e2e_latency_ms_change_pct": -8.981512797197189
}
]
}

View File

@ -0,0 +1,25 @@
backend,chunked_prefill_size,concurrency,repeat,completed,duration,request_throughput,input_throughput,output_throughput,total_throughput,mean_ttft_ms,median_ttft_ms,p95_ttft_ms,p99_ttft_ms,mean_e2e_latency_ms,median_e2e_latency_ms,p95_e2e_latency_ms,p99_e2e_latency_ms
flashinfer_mxfp4,16384,16,1,40,216.50833019288257,0.1847503971988739,3026.95050770635,0.1847503971988739,3027.135258103549,72316.50786341052,84522.44199160486,84848.25946678175,91778.32209909335,72316.55178854708,84522.48549158685,84848.3054406708,91778.3653298742
flashinfer_mxfp4,16384,16,2,40,216.24680145387538,0.1849738342073552,3030.6112996533075,0.1849738342073552,3030.796273487515,72215.42132570758,84481.09910136554,84765.86677560116,91561.01098880405,72215.46733585419,84481.14997893572,84765.91166217111,91561.05765777174
flashinfer_mxfp4,16384,16,3,40,216.48059168993495,0.18477406998818618,3027.3383626864425,0.18477406998818618,3027.5231367564306,72312.7285073162,84540.76406499371,84853.01167063879,91778.99107307894,72312.77419594117,84540.81119748298,84853.05667960782,91779.0367784421
flashinfer_mxfp4,16384,8,1,40,216.53949848609045,0.18472380456985968,3026.514814072581,0.18472380456985968,3026.6995378771508,40339.51893080957,42272.18415902462,42653.667874005616,49590.42487496044,40339.55953667755,42272.22537947819,42653.713031823274,49590.473274949465
flashinfer_mxfp4,16384,8,2,40,216.4690438960679,0.18478392697666732,3027.4998595857173,0.18478392697666732,3027.684643512694,40325.29723601765,42272.719990462065,42599.55736350965,49530.63742398982,40325.33475726959,42272.74997299537,42599.60809347685,49530.707661700435
flashinfer_mxfp4,16384,8,3,40,216.46678952896036,0.18478585138644807,3027.531389115565,0.18478585138644807,3027.7161749669513,40328.56577691855,42263.43939546496,42628.12673794104,49566.40897058882,40328.609929577215,42263.489623437636,42628.16974768647,49566.45701683592
flashinfer_mxfp4,8192,16,1,40,201.0955079239793,0.1989104600741321,3258.9489778545803,0.1989104600741321,3259.147888314654,66464.03402339201,78428.61395492218,80900.29283260228,82248.80441807676,66464.07695321832,78428.6568548996,80900.33666997915,82248.85000464972
flashinfer_mxfp4,8192,16,2,40,201.02179174800403,0.1989834020091862,3260.144058518507,0.1989834020091862,3260.343041920516,66441.70982562937,78389.55687300768,80875.61333409976,82222.40279711317,66441.75368165597,78389.59934201557,80875.65720523708,82222.44497414678
flashinfer_mxfp4,8192,16,3,40,200.99849134706892,0.19900646881439046,3260.5219850549734,0.19900646881439046,3260.7209915237877,66426.99547338416,78381.78810151294,80856.50636442006,82182.5564461993,66427.0412968297,78381.83325249702,80856.55405281577,82182.60281610303
flashinfer_mxfp4,8192,8,1,40,201.29355681990273,0.19871475586169896,3255.7425600380757,0.19871475586169896,3255.9412747939377,37166.8226861686,39200.42008545715,41681.43365625292,43097.31771401828,37166.86569513404,39200.45927807223,41681.47955433233,43097.35930965515
flashinfer_mxfp4,8192,8,2,40,201.1566553460434,0.1988499954485188,3257.958325428532,0.1988499954485188,3258.157175423981,37137.24017466302,39192.07067997195,41617.94732188573,43050.63308592187,37137.28631501435,39192.11111799814,41617.99226633739,43050.676568434574
flashinfer_mxfp4,8192,8,3,40,201.02496831398457,0.198980257703726,3260.0925422178466,0.198980257703726,3260.29152247555,37112.54617974046,39172.589769004844,41604.221663821954,43020.16220120946,37112.590136541985,39172.621404984966,41604.265929351095,43020.20668042591
marlin,16384,16,1,40,232.999846650986,0.17167393272973525,2812.7057138439823,0.17167393272973525,2812.877387776712,77838.70012289844,90956.05235511903,91350.91751561266,98844.17919690021,77838.74413453159,90956.10008307267,91350.963561167,98844.22676438
marlin,16384,16,2,40,233.0310456538573,0.17165094842948833,2812.329139068737,0.17165094842948833,2812.5007900171663,77849.1269534803,90990.29910599347,91359.9561718176,98847.1738615795,77849.17135268915,90990.34174648114,91360.00232317018,98847.21923549892
marlin,16384,16,3,40,233.0339416109491,0.1716488152905216,2812.2941897199057,0.1716488152905216,2812.4658385351963,77849.66164607904,90972.34335646499,91361.77453174023,98851.06839299435,77849.70525740064,90972.38598857075,91361.8199098506,98851.1173977633
marlin,16384,8,1,40,233.02748973597772,0.17165356776284363,2812.37205422643,0.17165356776284363,2812.543707794193,43413.6843872373,45488.352137384936,45877.185943827484,53396.961687700816,43413.72910180944,45488.39849594515,45877.235028997515,53397.00680534588
marlin,16384,8,2,40,232.74746951600537,0.17186008545304213,2815.755640062642,0.17186008545304213,2815.927500148095,43359.47861029417,45474.66833144426,45731.592441652894,53163.52174516069,43359.524680202594,45474.7117330553,45731.640505709205,53163.57911798171
marlin,16384,8,3,40,232.98255807999521,0.1716866718677966,2812.9144318819795,0.1716866718677966,2813.086118553847,43401.67168469634,45470.05465638358,45861.754696990814,53373.858650114846,43401.71520576696,45470.09659849573,45861.829353275236,53373.90170200961
marlin,8192,16,1,40,223.29927809908986,0.17913179272460458,2934.8952919999215,0.17913179272460458,2935.074423792646,73815.6138905033,87082.98856834881,89829.93190474808,91355.88917817222,73815.65661169589,87083.03174446337,89829.9770831829,91355.93388687819
marlin,8192,16,2,40,223.2892873159144,0.17913980773921837,2935.0266099993537,0.17913980773921837,2935.205749807093,73812.15487574227,87086.07016585302,89856.62238476798,91390.91669618152,73812.19822114217,87086.11152344383,89856.66677479167,91390.96168792807
marlin,8192,16,3,40,223.33232814306393,0.1791052837383063,2934.4609687684106,0.1791052837383063,2934.640074052149,73832.87409407203,87089.25215096679,89883.80955031607,91406.37799005723,73832.91684102733,87089.2927980749,89883.8516859687,91406.42066749977
marlin,8192,8,1,40,223.2894644900225,0.17913966559666036,2935.0242811356834,0.17913966559666036,2935.20342080128,41227.18363396125,43507.58656300604,46237.23470707191,47776.81458188454,41227.227372379275,43507.62654503342,46237.28018605616,47776.86155497795
marlin,8192,8,2,40,223.2271411139518,0.17918968007380884,2935.843718329284,0.17918968007380884,2936.0229080093577,41215.50619005575,43508.31371441018,46201.26801881706,47751.084343302064,41215.55230935919,43508.363836910576,46201.314437552355,47751.12928038696
marlin,8192,8,3,40,223.2904916331172,0.17913884154871654,2935.0107799341718,0.17913884154871654,2935.189918775721,41230.31144623528,43496.72723049298,46221.18599918904,47788.806532013696,41230.35565857426,43496.76744209137,46221.24284039019,47788.8507199334
1 backend chunked_prefill_size concurrency repeat completed duration request_throughput input_throughput output_throughput total_throughput mean_ttft_ms median_ttft_ms p95_ttft_ms p99_ttft_ms mean_e2e_latency_ms median_e2e_latency_ms p95_e2e_latency_ms p99_e2e_latency_ms
2 flashinfer_mxfp4 16384 16 1 40 216.50833019288257 0.1847503971988739 3026.95050770635 0.1847503971988739 3027.135258103549 72316.50786341052 84522.44199160486 84848.25946678175 91778.32209909335 72316.55178854708 84522.48549158685 84848.3054406708 91778.3653298742
3 flashinfer_mxfp4 16384 16 2 40 216.24680145387538 0.1849738342073552 3030.6112996533075 0.1849738342073552 3030.796273487515 72215.42132570758 84481.09910136554 84765.86677560116 91561.01098880405 72215.46733585419 84481.14997893572 84765.91166217111 91561.05765777174
4 flashinfer_mxfp4 16384 16 3 40 216.48059168993495 0.18477406998818618 3027.3383626864425 0.18477406998818618 3027.5231367564306 72312.7285073162 84540.76406499371 84853.01167063879 91778.99107307894 72312.77419594117 84540.81119748298 84853.05667960782 91779.0367784421
5 flashinfer_mxfp4 16384 8 1 40 216.53949848609045 0.18472380456985968 3026.514814072581 0.18472380456985968 3026.6995378771508 40339.51893080957 42272.18415902462 42653.667874005616 49590.42487496044 40339.55953667755 42272.22537947819 42653.713031823274 49590.473274949465
6 flashinfer_mxfp4 16384 8 2 40 216.4690438960679 0.18478392697666732 3027.4998595857173 0.18478392697666732 3027.684643512694 40325.29723601765 42272.719990462065 42599.55736350965 49530.63742398982 40325.33475726959 42272.74997299537 42599.60809347685 49530.707661700435
7 flashinfer_mxfp4 16384 8 3 40 216.46678952896036 0.18478585138644807 3027.531389115565 0.18478585138644807 3027.7161749669513 40328.56577691855 42263.43939546496 42628.12673794104 49566.40897058882 40328.609929577215 42263.489623437636 42628.16974768647 49566.45701683592
8 flashinfer_mxfp4 8192 16 1 40 201.0955079239793 0.1989104600741321 3258.9489778545803 0.1989104600741321 3259.147888314654 66464.03402339201 78428.61395492218 80900.29283260228 82248.80441807676 66464.07695321832 78428.6568548996 80900.33666997915 82248.85000464972
9 flashinfer_mxfp4 8192 16 2 40 201.02179174800403 0.1989834020091862 3260.144058518507 0.1989834020091862 3260.343041920516 66441.70982562937 78389.55687300768 80875.61333409976 82222.40279711317 66441.75368165597 78389.59934201557 80875.65720523708 82222.44497414678
10 flashinfer_mxfp4 8192 16 3 40 200.99849134706892 0.19900646881439046 3260.5219850549734 0.19900646881439046 3260.7209915237877 66426.99547338416 78381.78810151294 80856.50636442006 82182.5564461993 66427.0412968297 78381.83325249702 80856.55405281577 82182.60281610303
11 flashinfer_mxfp4 8192 8 1 40 201.29355681990273 0.19871475586169896 3255.7425600380757 0.19871475586169896 3255.9412747939377 37166.8226861686 39200.42008545715 41681.43365625292 43097.31771401828 37166.86569513404 39200.45927807223 41681.47955433233 43097.35930965515
12 flashinfer_mxfp4 8192 8 2 40 201.1566553460434 0.1988499954485188 3257.958325428532 0.1988499954485188 3258.157175423981 37137.24017466302 39192.07067997195 41617.94732188573 43050.63308592187 37137.28631501435 39192.11111799814 41617.99226633739 43050.676568434574
13 flashinfer_mxfp4 8192 8 3 40 201.02496831398457 0.198980257703726 3260.0925422178466 0.198980257703726 3260.29152247555 37112.54617974046 39172.589769004844 41604.221663821954 43020.16220120946 37112.590136541985 39172.621404984966 41604.265929351095 43020.20668042591
14 marlin 16384 16 1 40 232.999846650986 0.17167393272973525 2812.7057138439823 0.17167393272973525 2812.877387776712 77838.70012289844 90956.05235511903 91350.91751561266 98844.17919690021 77838.74413453159 90956.10008307267 91350.963561167 98844.22676438
15 marlin 16384 16 2 40 233.0310456538573 0.17165094842948833 2812.329139068737 0.17165094842948833 2812.5007900171663 77849.1269534803 90990.29910599347 91359.9561718176 98847.1738615795 77849.17135268915 90990.34174648114 91360.00232317018 98847.21923549892
16 marlin 16384 16 3 40 233.0339416109491 0.1716488152905216 2812.2941897199057 0.1716488152905216 2812.4658385351963 77849.66164607904 90972.34335646499 91361.77453174023 98851.06839299435 77849.70525740064 90972.38598857075 91361.8199098506 98851.1173977633
17 marlin 16384 8 1 40 233.02748973597772 0.17165356776284363 2812.37205422643 0.17165356776284363 2812.543707794193 43413.6843872373 45488.352137384936 45877.185943827484 53396.961687700816 43413.72910180944 45488.39849594515 45877.235028997515 53397.00680534588
18 marlin 16384 8 2 40 232.74746951600537 0.17186008545304213 2815.755640062642 0.17186008545304213 2815.927500148095 43359.47861029417 45474.66833144426 45731.592441652894 53163.52174516069 43359.524680202594 45474.7117330553 45731.640505709205 53163.57911798171
19 marlin 16384 8 3 40 232.98255807999521 0.1716866718677966 2812.9144318819795 0.1716866718677966 2813.086118553847 43401.67168469634 45470.05465638358 45861.754696990814 53373.858650114846 43401.71520576696 45470.09659849573 45861.829353275236 53373.90170200961
20 marlin 8192 16 1 40 223.29927809908986 0.17913179272460458 2934.8952919999215 0.17913179272460458 2935.074423792646 73815.6138905033 87082.98856834881 89829.93190474808 91355.88917817222 73815.65661169589 87083.03174446337 89829.9770831829 91355.93388687819
21 marlin 8192 16 2 40 223.2892873159144 0.17913980773921837 2935.0266099993537 0.17913980773921837 2935.205749807093 73812.15487574227 87086.07016585302 89856.62238476798 91390.91669618152 73812.19822114217 87086.11152344383 89856.66677479167 91390.96168792807
22 marlin 8192 16 3 40 223.33232814306393 0.1791052837383063 2934.4609687684106 0.1791052837383063 2934.640074052149 73832.87409407203 87089.25215096679 89883.80955031607 91406.37799005723 73832.91684102733 87089.2927980749 89883.8516859687 91406.42066749977
23 marlin 8192 8 1 40 223.2894644900225 0.17913966559666036 2935.0242811356834 0.17913966559666036 2935.20342080128 41227.18363396125 43507.58656300604 46237.23470707191 47776.81458188454 41227.227372379275 43507.62654503342 46237.28018605616 47776.86155497795
24 marlin 8192 8 2 40 223.2271411139518 0.17918968007380884 2935.843718329284 0.17918968007380884 2936.0229080093577 41215.50619005575 43508.31371441018 46201.26801881706 47751.084343302064 41215.55230935919 43508.363836910576 46201.314437552355 47751.12928038696
25 marlin 8192 8 3 40 223.2904916331172 0.17913884154871654 2935.0107799341718 0.17913884154871654 2935.189918775721 41230.31144623528 43496.72723049298 46221.18599918904 47788.806532013696 41230.35565857426 43496.76744209137 46221.24284039019 47788.8507199334

View File

@ -0,0 +1,211 @@
{
"run_id": "kimi3-ep4-moe-full-20260818-151349",
"summary": [
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 8192,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.1988499954485188,
"median_input_throughput": 3257.958325428532,
"median_total_throughput": 3258.157175423981,
"median_mean_ttft_ms": 37137.24017466302,
"median_median_ttft_ms": 39192.07067997195,
"median_p95_ttft_ms": 41617.94732188573,
"median_p99_ttft_ms": 43050.63308592187,
"median_mean_e2e_latency_ms": 37137.28631501435,
"median_median_e2e_latency_ms": 39192.11111799814,
"median_p95_e2e_latency_ms": 41617.99226633739
},
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 8192,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.1989834020091862,
"median_input_throughput": 3260.144058518507,
"median_total_throughput": 3260.343041920516,
"median_mean_ttft_ms": 66441.70982562937,
"median_median_ttft_ms": 78389.55687300768,
"median_p95_ttft_ms": 80875.61333409976,
"median_p99_ttft_ms": 82222.40279711317,
"median_mean_e2e_latency_ms": 66441.75368165597,
"median_median_e2e_latency_ms": 78389.59934201557,
"median_p95_e2e_latency_ms": 80875.65720523708
},
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 16384,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.18478392697666732,
"median_input_throughput": 3027.4998595857173,
"median_total_throughput": 3027.684643512694,
"median_mean_ttft_ms": 40328.56577691855,
"median_median_ttft_ms": 42272.18415902462,
"median_p95_ttft_ms": 42628.12673794104,
"median_p99_ttft_ms": 49566.40897058882,
"median_mean_e2e_latency_ms": 40328.609929577215,
"median_median_e2e_latency_ms": 42272.22537947819,
"median_p95_e2e_latency_ms": 42628.16974768647
},
{
"backend": "flashinfer_mxfp4",
"chunked_prefill_size": 16384,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.18477406998818618,
"median_input_throughput": 3027.3383626864425,
"median_total_throughput": 3027.5231367564306,
"median_mean_ttft_ms": 72312.7285073162,
"median_median_ttft_ms": 84522.44199160486,
"median_p95_ttft_ms": 84848.25946678175,
"median_p99_ttft_ms": 91778.32209909335,
"median_mean_e2e_latency_ms": 72312.77419594117,
"median_median_e2e_latency_ms": 84522.48549158685,
"median_p95_e2e_latency_ms": 84848.3054406708
},
{
"backend": "marlin",
"chunked_prefill_size": 8192,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.17913966559666036,
"median_input_throughput": 2935.0242811356834,
"median_total_throughput": 2935.20342080128,
"median_mean_ttft_ms": 41227.18363396125,
"median_median_ttft_ms": 43507.58656300604,
"median_p95_ttft_ms": 46221.18599918904,
"median_p99_ttft_ms": 47776.81458188454,
"median_mean_e2e_latency_ms": 41227.227372379275,
"median_median_e2e_latency_ms": 43507.62654503342,
"median_p95_e2e_latency_ms": 46221.24284039019
},
{
"backend": "marlin",
"chunked_prefill_size": 8192,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.17913179272460458,
"median_input_throughput": 2934.8952919999215,
"median_total_throughput": 2935.074423792646,
"median_mean_ttft_ms": 73815.6138905033,
"median_median_ttft_ms": 87086.07016585302,
"median_p95_ttft_ms": 89856.62238476798,
"median_p99_ttft_ms": 91390.91669618152,
"median_mean_e2e_latency_ms": 73815.65661169589,
"median_median_e2e_latency_ms": 87086.11152344383,
"median_p95_e2e_latency_ms": 89856.66677479167
},
{
"backend": "marlin",
"chunked_prefill_size": 16384,
"concurrency": 8,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.1716866718677966,
"median_input_throughput": 2812.9144318819795,
"median_total_throughput": 2813.086118553847,
"median_mean_ttft_ms": 43401.67168469634,
"median_median_ttft_ms": 45474.66833144426,
"median_p95_ttft_ms": 45861.754696990814,
"median_p99_ttft_ms": 53373.858650114846,
"median_mean_e2e_latency_ms": 43401.71520576696,
"median_median_e2e_latency_ms": 45474.7117330553,
"median_p95_e2e_latency_ms": 45861.829353275236
},
{
"backend": "marlin",
"chunked_prefill_size": 16384,
"concurrency": 16,
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.17165094842948833,
"median_input_throughput": 2812.329139068737,
"median_total_throughput": 2812.5007900171663,
"median_mean_ttft_ms": 77849.1269534803,
"median_median_ttft_ms": 90972.34335646499,
"median_p95_ttft_ms": 91359.9561718176,
"median_p99_ttft_ms": 98847.1738615795,
"median_mean_e2e_latency_ms": 77849.17135268915,
"median_median_e2e_latency_ms": 90972.38598857075,
"median_p95_e2e_latency_ms": 91360.00232317018
}
],
"comparisons": [
{
"chunked_prefill_size": 8192,
"concurrency": 8,
"flashinfer_input_throughput_change_pct": 11.002772493857949,
"flashinfer_total_throughput_change_pct": 11.002772493857949,
"flashinfer_median_ttft_ms_change_pct": -9.918996257778002,
"flashinfer_p95_ttft_ms_change_pct": -9.959153097854456,
"flashinfer_median_e2e_latency_ms_change_pct": -9.918986094468796
},
{
"chunked_prefill_size": 8192,
"concurrency": 16,
"flashinfer_input_throughput_change_pct": 11.082125055880665,
"flashinfer_total_throughput_change_pct": 11.082125055880665,
"flashinfer_median_ttft_ms_change_pct": -9.986112906786438,
"flashinfer_p95_ttft_ms_change_pct": -9.994821541601407,
"flashinfer_median_e2e_latency_ms_change_pct": -9.98610688810826
},
{
"chunked_prefill_size": 16384,
"concurrency": 8,
"flashinfer_input_throughput_change_pct": 7.628579997727458,
"flashinfer_total_throughput_change_pct": 7.62857999772748,
"flashinfer_median_ttft_ms_change_pct": -7.042347508899216,
"flashinfer_p95_ttft_ms_change_pct": -7.050816045775821,
"flashinfer_median_e2e_latency_ms_change_pct": -7.0423455840166405
},
{
"chunked_prefill_size": 16384,
"concurrency": 16,
"flashinfer_input_throughput_change_pct": 7.645236847665093,
"flashinfer_total_throughput_change_pct": 7.645236847665093,
"flashinfer_median_ttft_ms_change_pct": -7.089958471869751,
"flashinfer_p95_ttft_ms_change_pct": -7.127517325850641,
"flashinfer_median_e2e_latency_ms_change_pct": -7.089954195324976
}
]
}

View File

@ -0,0 +1,7 @@
backend,repeat,completed,request_throughput,input_throughput,total_throughput,median_ttft_ms,p95_ttft_ms,median_e2e_latency_ms
flashinfer_mxfp4,1,40,0.19844076757687357,3251.2535359794965,3251.451976747074,39266.71834103763,41694.44455524208,39266.759283607826
flashinfer_mxfp4,2,40,0.19842530057784252,3251.000124667372,3251.19854996795,39268.08554097079,41711.827712808736,39268.12894491013
flashinfer_mxfp4,3,40,0.198540460122689,3252.8868986501366,3253.0854391102594,39249.804597580805,41681.47474176949,39249.838569085114
marlin,1,40,0.17882371078176418,2929.8476774484243,2930.026501159206,43587.397116585635,46338.065504981205,43587.43864367716
marlin,2,40,0.17884277000539248,2930.1599437683503,2930.3387865383556,43590.378262568265,46287.321725080255,43590.42251249775
marlin,3,40,0.178834593712505,2930.025983385682,2930.2048179793946,43578.72363855131,46280.03058874747,43578.76205956563
1 backend repeat completed request_throughput input_throughput total_throughput median_ttft_ms p95_ttft_ms median_e2e_latency_ms
2 flashinfer_mxfp4 1 40 0.19844076757687357 3251.2535359794965 3251.451976747074 39266.71834103763 41694.44455524208 39266.759283607826
3 flashinfer_mxfp4 2 40 0.19842530057784252 3251.000124667372 3251.19854996795 39268.08554097079 41711.827712808736 39268.12894491013
4 flashinfer_mxfp4 3 40 0.198540460122689 3252.8868986501366 3253.0854391102594 39249.804597580805 41681.47474176949 39249.838569085114
5 marlin 1 40 0.17882371078176418 2929.8476774484243 2930.026501159206 43587.397116585635 46338.065504981205 43587.43864367716
6 marlin 2 40 0.17884277000539248 2930.1599437683503 2930.3387865383556 43590.378262568265 46287.321725080255 43590.42251249775
7 marlin 3 40 0.178834593712505 2930.025983385682 2930.2048179793946 43578.72363855131 46280.03058874747 43578.76205956563

View File

@ -0,0 +1,48 @@
{
"run_id": "kimi3-sm120-pr-representative-20260819-115530",
"shape": {
"input_len": 16384,
"output_len": 1,
"concurrency": 8,
"chunked_prefill_size": 8192,
"tp": 32,
"ep": 4
},
"summary": [
{
"backend": "marlin",
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.178834593712505,
"median_input_throughput": 2930.025983385682,
"median_total_throughput": 2930.2048179793946,
"median_median_ttft_ms": 43587.397116585635,
"median_p95_ttft_ms": 46287.321725080255,
"median_median_e2e_latency_ms": 43587.43864367716
},
{
"backend": "flashinfer_mxfp4",
"repeats": 3,
"completed_each": [
40,
40,
40
],
"median_request_throughput": 0.19844076757687357,
"median_input_throughput": 3251.2535359794965,
"median_total_throughput": 3251.451976747074,
"median_median_ttft_ms": 39266.71834103763,
"median_p95_ttft_ms": 41694.44455524208,
"median_median_e2e_latency_ms": 39266.759283607826
}
],
"comparison": {
"input_throughput_change_pct": 10.963300476353854,
"median_ttft_change_pct": -9.912679034242956,
"p95_ttft_change_pct": -9.922538178201778
}
}

View File

@ -0,0 +1,424 @@
#!/usr/bin/env bash
# Validate the exact SGLang Draft with one four-node Kimi-K3 serving point.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# shellcheck source=/dev/null
source "${REPO_ROOT}/scripts/common/lib.sh"
ACTION="${1:-all}"
RUN_ID="${RUN_ID:-kimi3-sm120-pr-representative-$(date '+%Y%m%d-%H%M%S')}"
RESULT_ROOT="${RESULT_ROOT:-${SCRIPT_DIR}/results/${RUN_ID}}"
MODEL_PATH="${MODEL_PATH:-/data/hf_models/Kimi-K3}"
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-kimi-k3}"
BASE_IMAGE="${BASE_IMAGE:-lmsysorg/sglang:kimi-k3-fiv617situ-warm}"
PR_IMAGE="${PR_IMAGE:-local/sglang:kimi-k3-sm120-pr-300c87a-fi-b525c51}"
SGLANG_SOURCE="${SGLANG_SOURCE:-/data/hzy/src/sglang-kimi-sm120-draft-wt}"
FLASHINFER_SOURCE="${FLASHINFER_SOURCE:-/data/hzy/src/flashinfer-pr4460-kimi-test-wt}"
ARTIFACT_DIR="${ARTIFACT_DIR:-/data/hzy/artifacts/sglang-pr-kimi-sm120-300c87a}"
FLASHINFER_WHEEL="${FLASHINFER_WHEEL:-${ARTIFACT_DIR}/flashinfer_python-0.6.18-py3-none-any.whl}"
BUILD_CONTEXT="${BUILD_CONTEXT:-/tmp/kimi3_sm120_pr_validation_context}"
FLASHINFER_CACHE="${FLASHINFER_CACHE:-/data/hzy/cache/flashinfer-pr-b525c51}"
HEAD_HOST="${HEAD_HOST:-174.1.60.1}"
NODE_SSH_USER="${NODE_SSH_USER:-user}"
NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4)
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
DIST_PORT="${DIST_PORT:-20000}"
PORT="${PORT:-30000}"
CONTAINER_PREFIX="kimi3_sm120_pr_validation"
INPUT_LEN=16384
OUTPUT_LEN=1
CONCURRENCY=8
CHUNKED_PREFILL_SIZE=8192
NUM_PROMPTS="${NUM_PROMPTS:-40}"
REPEATS="${REPEATS:-3}"
WARMUP_REQUESTS="${WARMUP_REQUESTS:-2}"
HEALTH_WAIT_S="${HEALTH_WAIT_S:-2400}"
BACKENDS=(marlin flashinfer_mxfp4)
mkdir -p "${RESULT_ROOT}"/{build,service,raw,bench,gpu}
log_init "${RESULT_ROOT}/orchestrator.log"
require_password() {
if [[ -z "${SUDO_PASSWORD:-}" && -n "${SUDO_PASSWORD_FILE:-}" ]]; then
[[ -r "${SUDO_PASSWORD_FILE}" ]] || {
echo "ERROR: cannot read SUDO_PASSWORD_FILE=${SUDO_PASSWORD_FILE}" >&2
exit 2
}
IFS= read -r SUDO_PASSWORD <"${SUDO_PASSWORD_FILE}"
fi
[[ -n "${SUDO_PASSWORD:-}" ]] || {
echo "ERROR: set SUDO_PASSWORD or SUDO_PASSWORD_FILE" >&2
exit 2
}
}
is_head() { [[ "$1" == "${HEAD_HOST}" ]]; }
sudo_host() {
local host="$1"
shift
require_password
if is_head "$host"; then
printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p '' -- "$@"
else
local remote_cmd
printf -v remote_cmd '%q ' "$@"
printf '%s\n' "${SUDO_PASSWORD}" | \
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"sudo -S -p '' -- ${remote_cmd}"
fi
}
check_inputs() {
local path
for path in "${MODEL_PATH}" "${SGLANG_SOURCE}/python/sglang" \
"${FLASHINFER_SOURCE}" "${FLASHINFER_WHEEL}" \
"${SCRIPT_DIR}/Dockerfile.pr_validation"; do
[[ -e "$path" ]] || { echo "ERROR: missing ${path}" >&2; exit 2; }
done
SGLANG_COMMIT="$(git -C "${SGLANG_SOURCE}" rev-parse HEAD)"
FLASHINFER_COMMIT="$(git -C "${FLASHINFER_SOURCE}" rev-parse HEAD~1)"
[[ "${SGLANG_COMMIT}" == 300c87a431ac40d3e7817246376b7fe20932db09 ]] || {
echo "ERROR: unexpected SGLang commit ${SGLANG_COMMIT}" >&2
exit 2
}
[[ "${FLASHINFER_COMMIT}" == b525c51* ]] || {
echo "ERROR: unexpected FlashInfer implementation commit ${FLASHINFER_COMMIT}" >&2
exit 2
}
export SGLANG_COMMIT FLASHINFER_COMMIT
}
prepare_build_context() {
log "preparing exact Draft build context"
rm -rf "${BUILD_CONTEXT}"
mkdir -p "${BUILD_CONTEXT}/sglang"
cp "${SCRIPT_DIR}/Dockerfile.pr_validation" "${BUILD_CONTEXT}/Dockerfile"
cp "${FLASHINFER_WHEEL}" "${BUILD_CONTEXT}/"
cp "${SGLANG_SOURCE}/test/registered/unit/layers/quantization/test_mxfp4_sm120_cutlass.py" \
"${BUILD_CONTEXT}/test_mxfp4_sm120_cutlass.py"
tar -C "${SGLANG_SOURCE}/python/sglang" -cf - . | \
tar -C "${BUILD_CONTEXT}/sglang" -xf -
{
printf 'sglang=%s\n' "${SGLANG_COMMIT}"
printf 'flashinfer_implementation=%s\n' "${FLASHINFER_COMMIT}"
sha256sum "${FLASHINFER_WHEEL}"
} >"${BUILD_CONTEXT}/source_identity.txt"
cp "${BUILD_CONTEXT}/source_identity.txt" "${RESULT_ROOT}/build/"
du -sh "${BUILD_CONTEXT}" | tee "${RESULT_ROOT}/build/context_size.txt"
}
stage_context() {
local host="$1"
is_head "$host" && return
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"rm -rf '${BUILD_CONTEXT}' && mkdir -p '${BUILD_CONTEXT}'"
tar -C "${BUILD_CONTEXT}" -cf - . | \
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"tar -C '${BUILD_CONTEXT}' -xf -"
}
build_one_node() {
local host="$1"
sudo_host "$host" docker build \
--build-arg "BASE_IMAGE=${BASE_IMAGE}" \
--build-arg "SGLANG_COMMIT=${SGLANG_COMMIT}" \
--build-arg "FLASHINFER_COMMIT=${FLASHINFER_COMMIT}" \
--tag "${PR_IMAGE}" "${BUILD_CONTEXT}" \
>"${RESULT_ROOT}/build/${host}.log" 2>&1
sudo_host "$host" docker image inspect "${PR_IMAGE}" \
--format '{{.Id}} {{.Size}} {{json .Config.Labels}}' \
>"${RESULT_ROOT}/build/${host}.image.txt"
sudo_host "$host" docker run --rm --entrypoint python3 "${PR_IMAGE}" -c \
"import importlib.metadata as m; print('sglang=' + m.version('sglang')); print('sglang-kernel=' + m.version('sglang-kernel')); print('flashinfer-python=' + m.version('flashinfer-python')); print(open('/opt/pr-build/source_identity.txt').read(), end='')" \
>"${RESULT_ROOT}/build/${host}.packages.txt"
}
build_all_nodes() {
check_inputs
prepare_build_context
local host pid rc=0
for host in "${NODE_HOSTS[@]}"; do stage_context "$host"; done
local -a pids=()
for host in "${NODE_HOSTS[@]}"; do
build_one_node "$host" &
pids+=("$!")
done
for pid in "${pids[@]}"; do wait "$pid" || rc=1; done
(( rc == 0 )) || { log "ERROR: image build failed"; return 1; }
log "exact Draft image built on all nodes"
}
prewarm_one_node() {
local host="$1"
sudo_host "$host" mkdir -p "${FLASHINFER_CACHE}"
sudo_host "$host" docker run --rm --gpus device=0 \
-v "${FLASHINFER_CACHE}:/root/.cache/flashinfer" \
-e FLASHINFER_WORKSPACE_BASE=/root \
-e FLASHINFER_DISABLE_VERSION_CHECK=1 \
--entrypoint python3 "${PR_IMAGE}" -m pytest -q -s \
/opt/pr-tests/test_mxfp4_sm120_cutlass.py \
-k kimi_k3_sm120_situ_layout_and_noncontiguous_input \
>"${RESULT_ROOT}/build/${host}.prewarm.log" 2>&1
}
prewarm_all_nodes() {
local host pid rc=0
local -a pids=()
for host in "${NODE_HOSTS[@]}"; do
prewarm_one_node "$host" &
pids+=("$!")
done
for pid in "${pids[@]}"; do wait "$pid" || rc=1; done
(( rc == 0 )) || { log "ERROR: FlashInfer prewarm failed"; return 1; }
log "exact #4460 kernel prewarmed on all nodes"
}
container_name() { printf '%s_node%s' "${CONTAINER_PREFIX}" "$1"; }
stop_service() {
local rank host
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
sudo_host "$host" docker rm -f "$(container_name "$rank")" \
>/dev/null 2>&1 || true
done
}
collect_service_logs() {
local label="$1" rank host
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
sudo_host "$host" docker logs "$(container_name "$rank")" \
>"${RESULT_ROOT}/service/${label}_node${rank}.log" 2>&1 || true
done
}
collect_gpu() {
local label="$1" rank host
for rank in 0 1 2 3; do
host="${NODE_HOSTS[$rank]}"
sudo_host "$host" nvidia-smi \
--query-gpu=timestamp,index,memory.used,memory.total,utilization.gpu,power.draw \
--format=csv,noheader,nounits \
>"${RESULT_ROOT}/gpu/${label}_node${rank}.csv" 2>&1 || true
done
}
verify_service_logs() {
local label="$1"
local pattern='CUDA out of memory|torch\.OutOfMemoryError|Traceback|EngineDeadError|NCCL[^[:cntrl:]]*(error|failed)|connection refused|Terminated'
if grep -Ein "${pattern}" "${RESULT_ROOT}/service/${label}_node"*.log \
>"${RESULT_ROOT}/service/${label}_fatal_scan.txt"; then
log "ERROR: fatal pattern found in service logs label=${label}"
return 1
fi
: >"${RESULT_ROOT}/service/${label}_fatal_scan.txt"
}
start_node() {
local rank="$1" backend="$2"
local host="${NODE_HOSTS[$rank]}" name bootstrap
name="$(container_name "$rank")"
bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)); exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size 32 --ep-size 4 --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend ${backend} --chunked-prefill-size ${CHUNKED_PREFILL_SIZE} --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}"
local -a cmd=(
docker run -d --name "$name"
--gpus all --network host --ipc=host --ulimit memlock=-1
--device /dev/infiniband --shm-size 32g --entrypoint bash
-v "${MODEL_PATH}:${MODEL_PATH}:ro"
-v "${FLASHINFER_CACHE}:/root/.cache/flashinfer"
-e CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
-e NCCL_SOCKET_IFNAME=bond0 -e GLOO_SOCKET_IFNAME=bond0
-e NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
-e NCCL_IB_GID_INDEX=3 -e NCCL_IB_TIMEOUT=22 -e NCCL_IB_RETRY_CNT=7
-e NCCL_CUMEM_ENABLE=1 -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
-e SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 -e SGLANG_MOE_FUSED_GATE_RADIX=1
-e SGLANG_SKIP_SGL_KERNEL_VERSION_CHECK=1
-e FLASHINFER_WORKSPACE_BASE=/root
-e FLASHINFER_DISABLE_VERSION_CHECK=1
"${PR_IMAGE}" -lc "$bootstrap"
)
printf '%q ' "${cmd[@]}" \
>"${RESULT_ROOT}/service/${backend}_node${rank}.cmd.txt"
printf '\n' >>"${RESULT_ROOT}/service/${backend}_node${rank}.cmd.txt"
sudo_host "$host" "${cmd[@]}" >/dev/null
}
wait_health() {
local backend="$1" i
for ((i = 1; i <= HEALTH_WAIT_S; i++)); do
if curl --fail --silent --max-time 5 \
"http://${HEAD_HOST}:${PORT}/health" >/dev/null 2>&1; then
log "service healthy backend=${backend} wait_s=${i}"
return 0
fi
if (( i % 30 == 0 )); then
log "waiting backend=${backend} elapsed_s=${i}"
collect_service_logs "${backend}_starting"
if grep -Eiq 'Traceback|CUDA out of memory|NCCL.*(error|failed)|EngineDeadError' \
"${RESULT_ROOT}/service/${backend}_starting_node"*.log; then
return 1
fi
fi
sleep 1
done
return 1
}
start_service() {
local backend="$1"
stop_service
log "starting TP32/EP4 backend=${backend} chunk=8192"
start_node 1 "$backend"
start_node 2 "$backend"
start_node 3 "$backend"
sleep 5
start_node 0 "$backend"
wait_health "$backend" || {
collect_service_logs "${backend}_startup_failed"
return 1
}
collect_service_logs "${backend}_healthy"
collect_gpu "${backend}_healthy"
}
run_bench() {
local backend="$1" repeat="$2"
local stem="${backend}_chunk8192_c8_r${repeat}"
local output="${RESULT_ROOT}/raw/${stem}.jsonl"
rm -f "$output"
log "bench backend=${backend} repeat=${repeat}/${REPEATS}"
sudo_host "${HEAD_HOST}" docker run --rm --network host \
-v "${MODEL_PATH}:${MODEL_PATH}:ro" \
-v "${RESULT_ROOT}:${RESULT_ROOT}" \
-e PYTHONUNBUFFERED=1 --entrypoint python3 "${PR_IMAGE}" \
-m sglang.benchmark.serving \
--backend sglang --host "${HEAD_HOST}" --port "${PORT}" \
--tokenizer "${MODEL_PATH}" --dataset-name random-ids \
--random-input-len "${INPUT_LEN}" --random-output-len "${OUTPUT_LEN}" \
--random-range-ratio 1.0 --num-prompts "${NUM_PROMPTS}" \
--max-concurrency "${CONCURRENCY}" --request-rate 10000 \
--warmup-requests "${WARMUP_REQUESTS}" --output-file "$output" \
--output-details --disable-tqdm \
>"${RESULT_ROOT}/bench/${stem}.log" 2>&1
python3 - "$output" "${NUM_PROMPTS}" "${OUTPUT_LEN}" <<'PY'
import json, sys
path, expected, expected_output_len = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
rows = [json.loads(x) for x in open(path, encoding="utf-8") if x.strip()]
assert len(rows) == 1, (path, len(rows))
assert rows[0].get("completed") == expected, rows[0].get("completed")
errors = rows[0].get("errors") or []
assert all(not error for error in errors), errors
output_lens = rows[0].get("output_lens") or []
assert len(output_lens) == expected, (len(output_lens), expected)
assert all(length == expected_output_len for length in output_lens), output_lens
PY
}
run_ab() {
local backend repeat
collect_gpu before
for backend in "${BACKENDS[@]}"; do
start_service "$backend"
for ((repeat = 1; repeat <= REPEATS; repeat++)); do
run_bench "$backend" "$repeat"
done
collect_service_logs "${backend}_completed"
verify_service_logs "${backend}_completed"
collect_gpu "${backend}_completed"
stop_service
sleep 5
done
collect_gpu after
}
summarize() {
python3 - "${RESULT_ROOT}" <<'PY'
import csv, json, re, statistics, sys
from pathlib import Path
root = Path(sys.argv[1])
pat = re.compile(r"(.+)_chunk8192_c8_r(\d+)\.jsonl$")
rows = []
for path in sorted((root / "raw").glob("*.jsonl")):
m = pat.match(path.name)
if not m:
continue
data = next(json.loads(x) for x in path.read_text().splitlines() if x.strip())
# The benchmark JSON reports the framework as backend="sglang". Keep the
# MoE implementation parsed from the filename as the comparison backend.
rows.append({**data, "backend": m.group(1), "repeat": int(m.group(2))})
metrics = ["request_throughput", "input_throughput", "total_throughput",
"median_ttft_ms", "p95_ttft_ms", "median_e2e_latency_ms"]
summary = []
for backend in ("marlin", "flashinfer_mxfp4"):
group = [x for x in rows if x["backend"] == backend]
if len(group) != 3:
raise SystemExit(f"expected 3 repeats for {backend}, got {len(group)}")
item = {"backend": backend, "repeats": len(group),
"completed_each": [x.get("completed") for x in group]}
for metric in metrics:
item[f"median_{metric}"] = statistics.median(float(x[metric]) for x in group)
summary.append(item)
idx = {x["backend"]: x for x in summary}
base, cand = idx["marlin"], idx["flashinfer_mxfp4"]
comparison = {
"input_throughput_change_pct":
(cand["median_input_throughput"] / base["median_input_throughput"] - 1) * 100,
"median_ttft_change_pct":
(cand["median_median_ttft_ms"] / base["median_median_ttft_ms"] - 1) * 100,
"p95_ttft_change_pct":
(cand["median_p95_ttft_ms"] / base["median_p95_ttft_ms"] - 1) * 100,
}
payload = {
"run_id": root.name,
"shape": {"input_len": 16384, "output_len": 1,
"concurrency": 8, "chunked_prefill_size": 8192,
"tp": 32, "ep": 4},
"summary": summary,
"comparison": comparison,
}
(root / "summary.json").write_text(json.dumps(payload, indent=2))
with (root / "results.csv").open("w", newline="") as f:
fields = ["backend", "repeat", "completed", *metrics]
w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
w.writeheader(); w.writerows(rows)
print(json.dumps(payload, indent=2))
PY
}
cleanup() {
collect_service_logs cleanup 2>/dev/null || true
stop_service 2>/dev/null || true
}
trap cleanup EXIT INT TERM
case "${ACTION}" in
build)
require_password; check_inputs; build_all_nodes; prewarm_all_nodes
;;
run)
require_password; check_inputs; run_ab; summarize
;;
all)
require_password; check_inputs; build_all_nodes; prewarm_all_nodes
run_ab; summarize
;;
summarize)
summarize
;;
stop)
require_password; stop_service
;;
*)
echo "Usage: $0 {all|build|run|summarize|stop}" >&2
exit 2
;;
esac

View File

@ -0,0 +1,682 @@
#!/usr/bin/env python3
"""Validate Kimi-K3 SiTU semantics for FlashInfer SM120 MXFP8 x MXFP4 MoE.
The control and target paths share the same generated case, quantized tensors,
routing metadata, CUTLASS ABI wrapper, synchronization, and timing method. The
only intentional difference is the activation contract: supported SwiGLU is a
harness control, while SiTU is the Kimi-K3 target.
"""
from __future__ import annotations
import argparse
from dataclasses import asdict, dataclass
from datetime import datetime
import importlib.metadata
import json
import math
import os
from pathlib import Path
import sys
import time
from typing import Callable, Literal
import torch
import torch.nn.functional as F
from flashinfer import (
mxfp4_dequantize,
mxfp4_quantize,
mxfp8_dequantize_host,
mxfp8_quantize,
)
from flashinfer.fused_moe import cutlass_fused_moe
from flashinfer.tllm_enums import ActivationType
ActivationName = Literal["swiglu", "situ"]
RoutingName = Literal["uniform", "skewed", "masked", "ep_mixed"]
KNOWN_MISSING_SITU_ERROR = (
"fc1_expert_weights inter size must be equal to "
"fc2_expert_weights inter size"
)
@dataclass(frozen=True)
class Case:
name: str
tokens: int
hidden: int
intermediate: int
local_experts: int
top_k: int
routing: RoutingName
ep_size: int = 1
ep_rank: int = 0
inject_nonfinite: bool = False
@property
def total_experts(self) -> int:
return self.local_experts * self.ep_size
@property
def local_expert_start(self) -> int:
return self.local_experts * self.ep_rank
@dataclass
class QuantizedCase:
case: Case
input_q: torch.Tensor
input_sf: torch.Tensor
input_dq: torch.Tensor
fc1_q: torch.Tensor
fc1_sf: torch.Tensor
fc1_dq: torch.Tensor
fc2_q: torch.Tensor
fc2_sf: torch.Tensor
fc2_dq: torch.Tensor
topk_ids: torch.Tensor
topk_weights: torch.Tensor
expert_scale: torch.Tensor
situ_gate_beta: torch.Tensor
situ_linear_beta: torch.Tensor
def quantize_mxfp4_experts(weight: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
packed = []
scales = []
for expert_weight in weight:
expert_packed, expert_scale = mxfp4_quantize(expert_weight)
packed.append(expert_packed)
scales.append(expert_scale)
return torch.stack(packed), torch.stack(scales)
def dequantize_mxfp4_experts(
packed: torch.Tensor, scales: torch.Tensor, dtype: torch.dtype
) -> torch.Tensor:
outputs = []
flat_scales = scales.reshape(scales.shape[0], -1)
for expert in range(packed.shape[0]):
outputs.append(
mxfp4_dequantize(
packed[expert].cpu().view(torch.uint8),
flat_scales[expert].cpu().view(torch.uint8),
)
)
return torch.stack(outputs).to(device=packed.device, dtype=dtype)
def dequantize_mxfp8_input(
packed: torch.Tensor, scales: torch.Tensor, dtype: torch.dtype
) -> torch.Tensor:
return (
mxfp8_dequantize_host(
packed.cpu().view(torch.uint8),
scales.cpu().view(torch.uint8).reshape(-1),
True,
)
.to(device=packed.device, dtype=dtype)
.reshape(packed.shape)
)
def make_routing(case: Case, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
ids = torch.empty(case.tokens, case.top_k, dtype=torch.int32, device=device)
local_start = case.local_expert_start
local_end = local_start + case.local_experts
if case.routing == "uniform":
for token in range(case.tokens):
for slot in range(case.top_k):
ids[token, slot] = (token * case.top_k + slot) % case.total_experts
elif case.routing == "skewed":
chosen = torch.arange(case.top_k, device=device) % case.total_experts
ids.copy_(chosen.expand(case.tokens, -1))
elif case.routing == "masked":
chosen = torch.arange(case.top_k, device=device) % case.total_experts
ids.copy_(chosen.expand(case.tokens, -1))
ids[:, -1] = -1
elif case.routing == "ep_mixed":
remote_start = local_end % case.total_experts
for token in range(case.tokens):
for slot in range(case.top_k):
if slot % 2 == 0:
ids[token, slot] = local_start + (token + slot) % case.local_experts
else:
ids[token, slot] = remote_start + (token + slot) % case.local_experts
else:
raise ValueError(f"unsupported routing mode: {case.routing}")
logits = torch.linspace(
0.25,
1.25,
case.top_k,
dtype=torch.float32,
device=device,
).expand(case.tokens, -1)
weights = torch.softmax(logits, dim=-1)
weights = torch.where(ids >= 0, weights, torch.zeros_like(weights))
weights /= weights.sum(dim=-1, keepdim=True)
return ids, weights.contiguous()
def prepare_case(case: Case, seed: int, device: torch.device) -> QuantizedCase:
generator = torch.Generator(device=device).manual_seed(seed)
dtype = torch.bfloat16
x = torch.randn(
case.tokens,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
)
if case.inject_nonfinite:
x[0, 0] = float("nan")
if case.hidden > 1:
x[-1, 1] = float("inf")
std_fc1 = 1.0 / math.sqrt(case.hidden)
std_fc2 = 1.0 / math.sqrt(case.intermediate)
gate = torch.randn(
case.local_experts,
case.intermediate,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
) * std_fc1
up = torch.randn(
case.local_experts,
case.intermediate,
case.hidden,
generator=generator,
dtype=dtype,
device=device,
) * std_fc1
down = torch.randn(
case.local_experts,
case.hidden,
case.intermediate,
generator=generator,
dtype=dtype,
device=device,
) * std_fc2
# CUTLASS consumes [up; gate]. Kimi checkpoints load as [gate; up], so
# this is also the oracle layout required from the SGLang weight processor.
fc1 = torch.cat([up, gate], dim=1).contiguous()
input_q, input_sf = mxfp8_quantize(x, True, 32)
fc1_q, fc1_sf = quantize_mxfp4_experts(fc1)
fc2_q, fc2_sf = quantize_mxfp4_experts(down)
input_dq = dequantize_mxfp8_input(input_q, input_sf, dtype)
fc1_dq = dequantize_mxfp4_experts(fc1_q, fc1_sf, dtype)
fc2_dq = dequantize_mxfp4_experts(fc2_q, fc2_sf, dtype)
topk_ids, topk_weights = make_routing(case, device)
return QuantizedCase(
case=case,
input_q=input_q,
input_sf=input_sf,
input_dq=input_dq,
fc1_q=fc1_q,
fc1_sf=fc1_sf,
fc1_dq=fc1_dq,
fc2_q=fc2_q,
fc2_sf=fc2_sf,
fc2_dq=fc2_dq,
topk_ids=topk_ids,
topk_weights=topk_weights,
expert_scale=torch.ones(
case.local_experts, dtype=torch.float32, device=device
),
situ_gate_beta=torch.full(
(case.local_experts,), 4.0, dtype=torch.float32, device=device
),
situ_linear_beta=torch.full(
(case.local_experts,), 25.0, dtype=torch.float32, device=device
),
)
def apply_activation(
gate: torch.Tensor, up: torch.Tensor, activation: ActivationName
) -> torch.Tensor:
if activation == "swiglu":
return F.silu(gate) * up
if activation == "situ":
gate = 4.0 * torch.tanh(gate / 4.0) * torch.sigmoid(gate)
up = 25.0 * torch.tanh(up / 25.0)
return gate * up
raise ValueError(f"unsupported activation: {activation}")
def run_reference(data: QuantizedCase, activation: ActivationName) -> torch.Tensor:
case = data.case
output = torch.zeros(
case.tokens,
case.hidden,
dtype=torch.bfloat16,
device=data.input_q.device,
)
local_start = case.local_expert_start
for local_expert in range(case.local_experts):
global_expert = local_start + local_expert
selected = data.topk_ids == global_expert
if not bool(selected.any()):
continue
token_ids, slots = torch.where(selected)
up_weight, gate_weight = torch.chunk(data.fc1_dq[local_expert], 2, dim=0)
expert_input = data.input_dq[token_ids]
gate = expert_input @ gate_weight.transpose(0, 1)
up = expert_input @ up_weight.transpose(0, 1)
hidden = apply_activation(gate, up, activation)
expert_output = hidden @ data.fc2_dq[local_expert].transpose(0, 1)
output[token_ids] += (
data.topk_weights[token_ids, slots, None].to(expert_output.dtype)
* expert_output
)
return output
def run_candidate(
data: QuantizedCase,
activation: ActivationName,
*,
use_fused_finalize: bool,
output: torch.Tensor | None = None,
) -> torch.Tensor:
case = data.case
if output is None:
output = torch.empty(
case.tokens,
case.hidden,
dtype=torch.bfloat16,
device=data.input_q.device,
)
activation_type = (
ActivationType.Swiglu if activation == "swiglu" else ActivationType.Situ
)
alpha = data.situ_gate_beta if activation == "situ" else None
beta = data.situ_linear_beta if activation == "situ" else None
cutlass_fused_moe(
input=data.input_q,
token_selected_experts=data.topk_ids,
token_final_scales=data.topk_weights,
fc1_expert_weights=data.fc1_q.contiguous().view(torch.int64),
fc2_expert_weights=data.fc2_q.contiguous().view(torch.int64),
output_dtype=torch.bfloat16,
quant_scales=[
data.fc1_sf.view(torch.int32),
data.expert_scale,
data.fc2_sf.view(torch.int32),
data.expert_scale,
],
input_sf=data.input_sf,
swiglu_alpha=alpha,
swiglu_beta=beta,
swiglu_limit=None,
tp_size=1,
tp_rank=0,
ep_size=case.ep_size,
ep_rank=case.ep_rank,
use_mxfp8_act_scaling=True,
activation_type=activation_type,
tune_max_num_tokens=1 << (case.tokens - 1).bit_length(),
output=output,
use_fused_finalize=use_fused_finalize,
)
return output
def tensor_metrics(reference: torch.Tensor, candidate: torch.Tensor) -> dict:
ref_nan = torch.isnan(reference)
got_nan = torch.isnan(candidate)
ref_inf = torch.isinf(reference)
got_inf = torch.isinf(candidate)
same_nonfinite = bool(torch.equal(ref_nan, got_nan) and torch.equal(ref_inf, got_inf))
finite = torch.isfinite(reference) & torch.isfinite(candidate)
if not bool(finite.any()):
return {
"same_nonfinite_mask": same_nonfinite,
"finite_elements": 0,
"max_abs": None,
"nrmse": None,
"cosine": None,
}
ref = reference[finite].float()
got = candidate[finite].float()
diff = got - ref
denominator = torch.sqrt(torch.mean(ref.square())).clamp_min(1e-12)
if bool(torch.count_nonzero(ref)) and bool(torch.count_nonzero(got)):
cosine = float(F.cosine_similarity(ref, got, dim=0).item())
else:
cosine = 1.0 if torch.equal(ref, got) else 0.0
return {
"same_nonfinite_mask": same_nonfinite,
"finite_elements": int(finite.sum().item()),
"max_abs": float(diff.abs().max().item()),
"nrmse": float((torch.sqrt(torch.mean(diff.square())) / denominator).item()),
"cosine": cosine,
}
def assert_matches(reference: torch.Tensor, candidate: torch.Tensor) -> dict:
metrics = tensor_metrics(reference, candidate)
if not metrics["same_nonfinite_mask"]:
raise AssertionError(f"NaN/Inf masks differ: {metrics}")
torch.testing.assert_close(
candidate,
reference,
rtol=0.2,
atol=0.2,
equal_nan=True,
)
return metrics
def assert_nonfinite_contract(
data: QuantizedCase,
reference: torch.Tensor,
candidate: torch.Tensor,
) -> dict:
"""Validate the defined MXFP8 boundary for non-finite BF16 inputs.
``mxfp8_quantize`` uses saturating conversion: the generated E4M3 payload
is finite and E8M0 codes 0x00..0xFE denote finite powers of two; only 0xFF
denotes NaN. A BF16 dequantized reference can overflow for the 0xFE scale,
so its NaN/Inf rows are diagnostic only and are not a valid Tensor Core
oracle. The kernel contract here is finite, deterministic output from the
finite quantized representation.
"""
input_payload_finite = bool(torch.isfinite(data.input_q.float()).all())
scale_has_nan_code = bool((data.input_sf == 0xFF).any())
candidate_finite = bool(torch.isfinite(candidate).all())
if not input_payload_finite or scale_has_nan_code or not candidate_finite:
raise AssertionError(
"MXFP8 saturation contract failed: "
f"input_payload_finite={input_payload_finite} "
f"scale_has_nan_code={scale_has_nan_code} "
f"candidate_finite={candidate_finite}"
)
reference_rows = (~torch.isfinite(reference)).any(dim=1)
candidate_rows = (~torch.isfinite(candidate)).any(dim=1)
return {
"contract": "mxfp8_satfinite_input_and_finite_candidate",
"input_payload_finite": input_payload_finite,
"scale_has_nan_code": scale_has_nan_code,
"max_scale_code": int(data.input_sf.max().item()),
"reference_nonfinite_rows_diagnostic_only": reference_rows.tolist(),
"candidate_nonfinite_rows": candidate_rows.tolist(),
"candidate_nan_elements": int(torch.isnan(candidate).sum().item()),
"candidate_inf_elements": int(torch.isinf(candidate).sum().item()),
}
def timed_ms(fn: Callable[[], torch.Tensor], warmup: int, iterations: int) -> float:
for _ in range(warmup):
fn()
torch.cuda.synchronize()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(iterations):
fn()
end.record()
end.synchronize()
return float(start.elapsed_time(end) / iterations)
def validate_determinism(data: QuantizedCase, activation: ActivationName) -> dict:
outputs = [
run_candidate(data, activation, use_fused_finalize=False).clone()
for _ in range(3)
]
exact = True
for item in outputs[1:]:
try:
torch.testing.assert_close(
outputs[0], item, rtol=0.0, atol=0.0, equal_nan=True
)
except AssertionError:
exact = False
break
if not exact:
raise AssertionError("non-fused finalize is not bitwise deterministic")
return {"runs": len(outputs), "bitwise_equal": exact}
def validate_cuda_graph(data: QuantizedCase, activation: ActivationName) -> dict:
eager = run_candidate(data, activation, use_fused_finalize=False).clone()
static_output = torch.empty_like(eager)
# Warm module loading and tactic selection before capture.
run_candidate(
data,
activation,
use_fused_finalize=False,
output=static_output,
)
torch.cuda.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
run_candidate(
data,
activation,
use_fused_finalize=False,
output=static_output,
)
graph.replay()
torch.cuda.synchronize()
first = static_output.clone()
graph.replay()
torch.cuda.synchronize()
second = static_output.clone()
metrics = assert_matches(eager, first)
if not torch.equal(first, second):
raise AssertionError("CUDA Graph replay is not bitwise deterministic")
return {"replay_matches_eager": metrics, "replays_bitwise_equal": True}
def smoke_cases() -> list[Case]:
return [
Case("decode_m1_uniform", 1, 128, 128, 4, 2, "uniform"),
Case("decode_m2_skewed", 2, 128, 128, 4, 2, "skewed"),
Case("batch_m8_empty_experts", 8, 128, 128, 8, 2, "skewed"),
Case("batch_m32_masked_slot", 32, 128, 128, 8, 4, "masked"),
Case("ep2_global_ids", 8, 128, 128, 4, 4, "ep_mixed", ep_size=2),
Case(
"nonfinite_activation",
2,
128,
128,
4,
2,
"uniform",
inject_nonfinite=True,
),
]
def real_cases() -> list[Case]:
# One TP32/EP32 rank owns 28 of Kimi-K3's 896 experts. Global expert IDs
# intentionally mix local and remote routes, matching the per-rank kernel ABI.
return [
Case(
"kimi_decode_m1_ep32",
1,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
Case(
"kimi_prefill_m128_ep32",
128,
3584,
3072,
28,
16,
"ep_mixed",
ep_size=32,
ep_rank=7,
),
]
def run_one(
case: Case,
activation: ActivationName,
args: argparse.Namespace,
) -> dict:
started = time.time()
data = prepare_case(case, args.seed, torch.device("cuda"))
reference = run_reference(data, activation)
result = {
"case": asdict(case),
"activation": activation,
"status": "UNKNOWN",
}
try:
candidate = run_candidate(
data,
activation,
use_fused_finalize=False,
)
result["metrics"] = (
assert_nonfinite_contract(data, reference, candidate)
if case.inject_nonfinite
else assert_matches(reference, candidate)
)
result["determinism"] = validate_determinism(data, activation)
if case.name == "batch_m8_empty_experts":
result["cuda_graph"] = validate_cuda_graph(data, activation)
if args.benchmark_iterations > 0 and not case.inject_nonfinite:
result["latency_ms"] = {
"reference": timed_ms(
lambda: run_reference(data, activation),
args.warmup,
args.benchmark_iterations,
),
"candidate": timed_ms(
lambda: run_candidate(
data,
activation,
use_fused_finalize=False,
),
args.warmup,
args.benchmark_iterations,
),
}
result["status"] = "PASS"
except RuntimeError as error:
if activation == "situ" and KNOWN_MISSING_SITU_ERROR in str(error):
result["status"] = "EXPECTED_MISSING_SITU"
result["error"] = str(error)
if args.require_situ:
raise
else:
result["status"] = "ERROR"
result["error"] = repr(error)
raise
finally:
result["elapsed_s"] = time.time() - started
return result
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--suite",
choices=("smoke", "real", "all"),
default="smoke",
)
parser.add_argument(
"--activation",
choices=("swiglu", "situ", "both"),
default="both",
)
parser.add_argument(
"--require-situ",
action="store_true",
help="Fail instead of recording the known missing-SiTU boundary.",
)
parser.add_argument("--seed", type=int, default=20260817)
parser.add_argument("--warmup", type=int, default=3)
parser.add_argument("--benchmark-iterations", type=int, default=0)
parser.add_argument("--output", type=Path)
return parser.parse_args()
def main() -> None:
args = parse_args()
if not torch.cuda.is_available():
raise SystemExit("CUDA is required")
major, minor = torch.cuda.get_device_capability()
if major != 12:
raise SystemExit(f"SM120/SM121 is required, got SM{major}{minor}")
cases = []
if args.suite in ("smoke", "all"):
cases.extend(smoke_cases())
if args.suite in ("real", "all"):
cases.extend(real_cases())
activations: list[ActivationName] = (
["swiglu", "situ"] if args.activation == "both" else [args.activation]
)
report = {
"schema_version": 1,
"created_at": datetime.now().astimezone().isoformat(),
"torch_version": torch.__version__,
"cuda_version": torch.version.cuda,
"flashinfer_version": importlib.metadata.version("flashinfer-python"),
"device": torch.cuda.get_device_name(),
"cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"),
"logical_cuda_device": torch.cuda.current_device(),
"compute_capability": [major, minor],
"arguments": vars(args) | {"output": str(args.output) if args.output else None},
"results": [],
}
failed = False
for activation in activations:
for case in cases:
print(f"[RUN] activation={activation} case={case.name}", flush=True)
try:
result = run_one(case, activation, args)
except Exception as error:
failed = True
result = {
"case": asdict(case),
"activation": activation,
"status": "ERROR",
"error": repr(error),
}
report["results"].append(result)
print(
f"[{result['status']}] activation={activation} case={case.name}",
flush=True,
)
output = args.output
if output is None:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
output = Path("results") / f"correctness_{args.suite}_{stamp}.json"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
print(f"report={output.resolve()}")
statuses = [item["status"] for item in report["results"]]
counts = {status: statuses.count(status) for status in sorted(set(statuses))}
print("summary=" + json.dumps(counts))
if failed:
raise SystemExit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,39 @@
ARG BASE_IMAGE=lmsysorg/sglang:kimi-k3-fiv617situ-warm
FROM ${BASE_IMAGE}
ENV FLASHINFER_DISABLE_JIT=1 \
FLASHINFER_DISABLE_VERSION_CHECK=1 \
PYTHONUNBUFFERED=1
RUN rm -rf \
/usr/local/lib/python3.12/dist-packages/flashinfer \
/usr/local/lib/python3.12/dist-packages/flashinfer_python-*.dist-info
COPY flashinfer_python-0.6.18-py3-none-any.whl /tmp/flashinfer_python-0.6.18-py3-none-any.whl
RUN python3 -m pip install --no-deps --force-reinstall \
/tmp/flashinfer_python-0.6.18-py3-none-any.whl && \
rm -f /tmp/flashinfer_python-0.6.18-py3-none-any.whl
# The wheel contains Python/C++ sources. These three libraries are the exact
# SM120 AOT artifacts that passed the Phase 3/4 correctness gate.
COPY aot/ /usr/local/lib/python3.12/dist-packages/flashinfer/data/aot/
# Apply only the SM120 backend-selection, weight-layout, and SiTU activation
# changes against this Kimi image's exact SGLang source. Replacing the newer
# Python tree would break the image's intentional sglang-kernel 0.4.5 pin.
COPY sglang.patch /tmp/sglang.patch
RUN patch --dry-run --batch --forward -p1 -d /sgl-workspace/sglang \
< /tmp/sglang.patch && \
patch --batch --forward -p1 -d /sgl-workspace/sglang \
< /tmp/sglang.patch && \
rm -f /tmp/sglang.patch
# Existing Kimi image compatibility fix: tcgen05 attention-residual is valid on
# SM100/SM103, not consumer Blackwell SM120. Bake the fallback into the image.
RUN python3 -c "from pathlib import Path; p=Path('/sgl-workspace/sglang/python/sglang/srt/layers/attn_residual.py'); s=p.read_text(); old=' _FAST_SUPPORTED = major >= 10'; new=' _FAST_SUPPORTED = major in (10, 11)'; assert old in s; p.write_text(s.replace(old,new))" && \
python3 -m compileall -q /sgl-workspace/sglang/python/sglang/srt/layers && \
python3 -c "from flashinfer.fused_moe import core; assert core.CUTLASS_FUSED_MOE_SUPPORTS_SITU is True"
LABEL ai.meta-stone.purpose="Kimi-K3 SM120 FlashInfer MXFP4 Phase 5 acceptance" \
ai.meta-stone.sglang.patch="kimi-image-sm120-flashinfer-mxfp4" \
ai.meta-stone.flashinfer.patch="7d1a4ee"

View File

@ -0,0 +1,390 @@
# Kimi-K3 EP32 与 EP4实现方式及 TTFT 差异分析
## 1. 先给结论
在当前四机 32 卡、`TP=32``DP=1``moe_a2a_backend=none` 的 Kimi-K3 部署里:
- `EP32` 不是 32 份模型副本,而是 896 个专家分散到 32 张卡;每张卡持有 28 个完整专家。
- `EP4` 也不是只用 4 张卡跑 MoE而是把 32 张卡分解成 `EP4 × MoE-TP8`。每个节点持有 224 个专家,每个专家的中间维再由节点内 8 张卡切分。
- 两种布局每卡承载的专家参数量和理论 MoE FLOPs 接近,但计算颗粒度不同。
- 当前没有启用 A2A。每张卡仍看到同一批 token只计算自己负责的专家或专家分片随后在 TP32 上汇总结果。
- 同后端 Marlin、同请求口径下EP4 相比 EP32 的 Input TPS 提高约 **15.9%**TTFT P50 降低约 **13.9%**
- 这次收益不是“把 AllReduce 从 32 卡缩成 4 卡”。Kimi-K3 当前 `A2A=none` 路径仍在完整 TP32 组上归约。更合理的解释是EP4 把每个专家的计算切到 8 卡,改善了路由偏斜、热点专家关键路径和 grouped MoE kernel 的并行形状。
## 2. 审计对象
### 2.1 模型参数
来源:`/data/hf_models/Kimi-K3/config.json``text_config`
| 参数 | 数值 |
|---|---:|
| hidden size | 7168 |
| Latent MoE hidden size | 3584 |
| 单专家 intermediate size | 3072 |
| routed experts | 896 |
| 每 token 激活专家数 | 16 |
| Transformer 层数 | 93 |
### 2.2 源码版本
审计仓库:
```text
/data/hzy/src/sglang-kimi-sm120
commit fb929bbccb7a34640f3d4904174767eb7ae95509
```
实验镜像:
```text
local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5
```
EP32 和 EP4 的 Marlin 实验使用同一镜像、同一模型、同一请求、同一 chunk 和同一服务参数。启动命令中唯一关键差异是:
```text
EP32: --tp-size 32 --ep-size 32
EP4 : --tp-size 32 --ep-size 4
```
## 3. EP、MoE-TP 和外层 TP 的关系
SGLang 不是把 `TP``EP` 相乘申请 GPU而是在外层 TP 组内部进一步分解 MoE
```python
moe_tp_size = tensor_model_parallel_size // moe_ep_size // moe_dp_size
```
源码:
```text
python/sglang/srt/distributed/parallel_state.py:2526-2528
python/sglang/srt/model_executor/model_runner_components/moe_ep_setup.py:123-137
```
本实验 `TP=32``MoE-DP=1`,因此:
```text
TP32 = EP32 × MoE-TP1
TP32 = EP4 × MoE-TP8
```
| 配置 | EP size | 派生 MoE-TP size | 含义 |
|---|---:|---:|---|
| EP32 | 32 | 1 | 每个专家只落在 1 张 GPU 上 |
| EP4 | 4 | 8 | 每个专家落在一个 8 GPU MoE-TP 组上 |
所以 `EP4` 的准确叫法是“较小 EP + 较大 MoE-TP”的混合专家布局。
## 4. 32 张卡具体怎么分组
假设全局 rank `0-7 / 8-15 / 16-23 / 24-31` 分别属于四个节点。
### 4.1 EP32
```text
MoE-EP group: [0, 1, 2, ..., 31]
MoE-TP group: 每个 rank 自己size=1
```
896 个专家平均切到 32 个 EP rank
```text
每 rank 专家数 = 896 / 32 = 28
```
例如:
```text
rank 0 负责专家 0-27
rank 1 负责专家 28-55
...
rank 31 负责专家 868-895
```
每个专家在其所属 GPU 上保留完整的 intermediate `3072`
### 4.2 EP4
SGLang 构造的 MoE-TP 组是连续 8 rank
```text
[0-7], [8-15], [16-23], [24-31]
```
每组正好对应一个 8 卡节点。MoE-EP 组则按 MoE-TP rank 跨节点取 rank
```text
[0,8,16,24], [1,9,17,25], ..., [7,15,23,31]
```
源码:
```text
python/sglang/srt/distributed/parallel_state.py:2559-2615
```
896 个专家先切为 4 份:
```text
每个 EP rank 专家数 = 896 / 4 = 224
```
每个专家的 intermediate 再切给 MoE-TP8
```text
每卡专家分片宽度 = 3072 / 8 = 384
```
因此:
```text
节点 0 的 8 张卡共同持有专家 0-223
节点 1 的 8 张卡共同持有专家 224-447
节点 2 的 8 张卡共同持有专家 448-671
节点 3 的 8 张卡共同持有专家 672-895
```
节点内 8 张卡不是各存一份 224 个完整专家,而是各存这些专家的 `1/8` intermediate 分片。
## 5. 为什么 EP4 没有让每卡专家权重暴涨 8 倍
SGLang 先按 EP 切专家数量,再按 MoE-TP 切专家中间维:
```python
self._num_local_routed = self._num_global_routed // storage_ep_size
self.intermediate_size_per_partition = intermediate_size // self.moe_tp_size
```
源码:
```text
python/sglang/srt/layers/moe/fused_moe_triton/layer.py:291-309
```
权重加载时,`w1/w3` 沿输出维切分,`w2` 沿输入维切分:
```text
python/sglang/srt/layers/moe/fused_moe_triton/layer.py:618-682
python/sglang/srt/layers/moe/fused_moe_triton/layer.py:702-770
```
只看与 intermediate 成正比的部分,每卡工作量满足:
```text
EP32: 28 × 3072 = 86016
EP4 : 224 × 384 = 86016
```
真实服务也印证了每卡权重显存接近:
| 配置 | 后端 | 每卡 weight memory |
|---|---|---:|
| EP32 | Marlin | 59.63 GiB |
| EP4 | Marlin | 59.28 GiB |
| EP4 | FlashInfer MXFP4 | 59.65 GiB |
小幅差异来自权重布局、padding、workspace 和后端转换,不是模型参数量发生了数量级变化。
## 6. 当前 `A2A=none` 时,一个 token 怎么执行
### 6.1 路由
Kimi-K3 的 gate 为每个 token 计算 896 个专家分数,再选 top-16
```text
hidden states
-> gate / grouped top-k
-> 16 个全局 expert id + routing weight
```
模型入口:
```text
python/sglang/srt/models/kimi_k3.py:386-469
python/sglang/srt/models/kimi_k3.py:1020-1059
```
### 6.2 没有 token A2A 搬运
`moe_a2a_backend=none` 会创建 `StandardDispatcher`
```text
python/sglang/srt/layers/moe/fused_moe_triton/layer.py:137-178
```
StandardDispatcher 不会把 token 发到专家所在 GPU。它保留 hidden states只处理专家 ID
```python
hidden_states = hidden_states
```
Marlin 路径把不属于本 EP rank 的全局专家映射为 `-1`,本地 kernel 只计算自己负责的专家:
```text
python/sglang/srt/layers/moe/token_dispatcher/standard.py:182-246
python/sglang/srt/layers/moe/fused_moe_triton/layer.py:903-911
```
FlashInfer 路径不在 dispatcher 中改 ID而是把 `moe_ep_size/rank``moe_tp_size/rank` 传给 CUTLASS runner由 runner 解释全局专家 ID
```text
python/sglang/srt/layers/quantization/mxfp4.py:1378-1401
```
两者实现位置不同,但语义一致:没有 A2A token dispatch每个 rank 从同一批 token 中计算自己的专家贡献。
### 6.3 汇总部分结果
Kimi-K3 的 routed expert 输出位于 3584 维 latent 空间。A2A 未启用时,源码要求在 RMSNorm 前完成归约:
```python
def _routed_needs_reduce(self):
return self.tp_size > 1 and get_moe_a2a_backend().is_none()
return self._latent_norm(tensor_model_parallel_all_reduce(latent))
```
源码:
```text
python/sglang/srt/models/kimi_k3.py:699-702
python/sglang/srt/models/kimi_k3.py:955-960
```
这里调用的是完整的 `get_tp_group()`,即 TP32。某些优化路径会把这次归约与相邻算子融合但数学上的通信域仍是 TP32。
因此,本次 EP4 收益不能解释成“MoE AllReduce 从 32 rank 缩成 4 rank”。
## 7. EP4 为什么更快
### 7.1 同口径实验结果
唯一能隔离 EP 变量的比较是 Marlin 对 Marlin
```text
模型 Kimi-K3
4 节点 / 32 GPU
TP32 / PP1 / DP1
ISL=16K / OSL=1 / C=8
chunked_prefill_size=8K
moe_a2a_backend=none
moe_runner_backend=marlin
```
| 配置 | repeats | Input TPS | TTFT P50 | TTFT P95 |
|---|---:|---:|---:|---:|
| EP32 | 2 | 2531.43 | 50.52 s | 53.64 s |
| EP4 | 3 | 2935.02 | 43.51 s | 46.22 s |
| EP4 vs EP32 | | **+15.9%** | **-13.9%** | **-13.8%** |
结果路径:
```text
EP32:
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-moe-prefill-20260818-130900/raw/
EP4:
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/results/kimi3-ep4-moe-full-20260818-151349/raw/
```
EP4 + FlashInfer 的同场景结果为 3257.96 Input TPS、39.19 s TTFT P50。它相对 EP4 + Marlin 还包含约 11% 的 MoE backend 收益,不能全部归因于 EP。
### 7.2 主要机制:把单专家关键路径从 1 卡摊到 8 卡
Kimi-K3 每个 token 激活 16 个专家。路由并不保证所有专家收到完全一样数量的 token总会出现热点专家和冷门专家。
EP32 中:
```text
一个专家只属于一张 GPU
热点专家的完整 3584 -> 3072 -> 3584 计算由这张 GPU 独自完成
其他 GPU 即使更空,也不能帮助它完成该专家
```
EP4 中:
```text
一个专家属于一个节点的 8 卡 MoE-TP 组
同一专家的 intermediate 3072 被切成 8 份,每卡计算 384
热点专家的矩阵计算由 8 张 GPU 共同承担
```
这不会减少总 FLOPs但会降低单个热门专家压在单卡上的关键路径并让节点内 8 卡对同一专家协同计算。
### 7.3 grouped MoE kernel 的执行形状改变
每卡的理论 FLOPs 接近,但 kernel 看到的形状不同:
| 配置 | 本地专家槽位 | 单专家 intermediate 分片 | 平均每 token 本 rank 参与的专家分片 |
|---|---:|---:|---:|
| EP32 | 28 | 3072 | 16/32 = 0.5 |
| EP4 | 224 | 384 | 16/4 = 4 |
EP4 让更多专家分片同时出现在一个 rank 的 grouped MoE 调度中,并把单个专家的大矩阵切成更多可并行的小分片。在本次 SM120 + Marlin 实测中,这个形状明显更合适。
这属于“同样总计算,不同并行颗粒度”的收益,不是模型数学变化。
### 7.4 通信没有消失,但也没有因 MoE-TP8 再增加一次完整归约
当前 Kimi `A2A=none` 路径最终直接在 TP32 汇总 latent 输出。这一次 TP32 AllReduce 同时合并了:
- 不同 EP rank 持有的不同专家贡献;
- EP4 中同一专家的 8 个 MoE-TP 分片贡献。
所以 EP4 没有额外增加一轮独立的“EP4 AllReduce + TP8 AllReduce”。这使它可以获得更好的计算分摊而不在当前实现中付出第二次完整 collective 的代价。
## 8. EP4 与真正 A2A EP 的区别
当前实验:
```text
--ep-size 4
--moe-a2a-backend none
```
它只是改变专家权重的 EP/MoE-TP 布局,不会按路由结果搬 token。
真正的 DeepEP 或 FlashInfer A2A 会执行:
```text
dispatch: token 按 top-k 发往专家所在 rank
compute : rank 只计算收到的 token
combine : 专家结果返回 token 原所属 rank 并加权合并
```
SGLang 的 dispatcher 选择源码:
```text
Standard/A2A 分支python/sglang/srt/layers/moe/fused_moe_triton/layer.py:137-178
DeepEP dispatcherpython/sglang/srt/layers/moe/token_dispatcher/
```
Kimi-K3 源码也明确说明A2A 后 combine 已返回完整 routed sum因此不能再做 TP AllReduce
```text
python/sglang/srt/models/kimi_k3.py:506-516
python/sglang/srt/models/kimi_k3.py:699-709
```
所以“EP4 比 EP32 快”不能直接推出“DeepEP EP4 一定更快”。A2A 会引入完全不同的 token dispatch/combine、buffer 和共享专家约束,需要单独实验。
## 9. 证据边界
已经由源码和实验确认:
1. `TP32/EP32` 派生 `MoE-TP1``TP32/EP4` 派生 `MoE-TP8`
2. EP32 每卡 28 个完整专家EP4 每个节点 224 个专家、每卡持有其 1/8 intermediate 分片。
3. 两种 Marlin 实验只改变 `--ep-size`EP4 的 TTFT P50 低 13.9%。
4. 当前 `A2A=none` 不搬 tokenKimi routed latent 仍在完整 TP32 上归约。
5. 每卡权重显存基本相同,收益不是因为 EP4 少加载了大量权重。
目前还没有由成对 Nsight Trace 直接确认:
1. 13.9% TTFT 收益中Marlin grouped MoE kernel 本身占多少;
2. 路由负载偏斜改善占多少;
3. padding、排序和 expert mapping 开销分别变化多少。
若需要把原因从“源码约束下的机制解释”升级为“算子级定量归因”,下一步只需补一组相同 16K、C8、chunk8K 的 EP32/EP4 Nsight 对照,比较每层 Marlin MoE kernel、top-k/sort 与 TP32 AllReduce 时间。现有结果已经足够说明 EP4 是当前部署更好的基线,但不应虚构尚未采集的算子时间占比。

View File

@ -0,0 +1,135 @@
# Kimi-K3 / Pro6000D / SGLang TP32 EP4 MoE Backend Prefill
This experiment is the real-serving acceptance gate for Kimi-K3 FlashInfer
MXFP4 MoE on NVIDIA SM120. It does not modify the existing baseline experiment.
## Fixed matrix
| Dimension | Values |
|---|---|
| Nodes | 601-604, 8 GPUs per node |
| Parallelism | TP32, EP4, DP1 |
| MoE runner | `marlin`, `flashinfer_mxfp4` |
| Input / output | 16K / 1 token |
| Concurrency | 8, 16 |
| Chunked prefill | 8K, 16K |
| Requests | 40 per repeat |
| Repeats | 3 |
The eight configurations are identical except for MoE runner, chunk size, and
benchmark concurrency. The directory retains its historical `tp32ep32` name so
existing evidence paths remain stable; the production comparison and the
default `EP_SIZE` are EP4.
The client uses SGLang `random-ids`, which produces the exact synthetic token
length without depending on a ShareGPT file or text truncation. Radix cache is
disabled. `OSL=1` makes TTFT, E2E latency and input TPS the primary metrics;
TPOT is not meaningful for this matrix.
The defaults above can be narrowed for a capacity-first run with `EP_SIZE`,
`BACKENDS_CSV`, `CHUNKED_PREFILL_SIZES_CSV`, and `CONCURRENCIES_CSV`. For
example, the TP32/EP4 maximum-pressure probe uses Marlin, 16K chunk, and C=16
before spending time on the complete backend comparison.
## Accepted EP4 result
Run ID: `kimi3-ep4-moe-full-20260818-151349`.
All 24 repeat files completed 40/40 requests with empty error arrays. No OOM,
Traceback, NCCL error, or EngineDeadError was found. Values below are medians
across three repeats.
| Chunk | C | Backend | Input TPS | TTFT P50 | TTFT P95 |
|---:|---:|---|---:|---:|---:|
| 8K | 8 | Marlin | 2935.02 | 43.51 s | 46.22 s |
| 8K | 8 | FlashInfer MXFP4 | 3257.96 | 39.19 s | 41.62 s |
| 8K | 16 | Marlin | 2934.90 | 87.09 s | 89.86 s |
| 8K | 16 | FlashInfer MXFP4 | 3260.14 | 78.39 s | 80.88 s |
| 16K | 8 | Marlin | 2812.91 | 45.47 s | 45.86 s |
| 16K | 8 | FlashInfer MXFP4 | 3027.50 | 42.27 s | 42.63 s |
| 16K | 16 | Marlin | 2812.33 | 90.97 s | 91.36 s |
| 16K | 16 | FlashInfer MXFP4 | 3027.34 | 84.52 s | 84.85 s |
FlashInfer improves Input TPS by 11.00%-11.08% with an 8K chunk and by
7.63%-7.65% with a 16K chunk. It reduces median TTFT by 9.92%-9.99% and
7.04%-7.09%, respectively. The accepted Prefill setting is therefore
`flashinfer_mxfp4`, EP4, and an 8K chunk for this 16K/1 workload.
The only matching existing EP32 evidence is Marlin/8K/C8 with two repeats.
Its median Input TPS is 2531.43 and TTFT P50/P95 are 50.52/53.64 seconds.
Against that evidence, EP4 raises Input TPS by 15.94% and lowers TTFT P50 by
13.88%. EP32 was not rerun.
## Unique entrypoint
Run on 601 only. The sudo password is supplied at runtime and is never written
to Git or result files.
```bash
cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill
read -rsp 'sudo password: ' SUDO_PASSWORD; echo
export SUDO_PASSWORD
export RUN_ID=kimi3-moe-prefill-$(date +%Y%m%d-%H%M%S)
bash run_moe_backend_prefill.sh build-image
bash run_moe_backend_prefill.sh run
```
For an unattended run, use the same entrypoint under tmux:
```bash
umask 077
printf '%s\n' "$SUDO_PASSWORD" > /tmp/kimi3-phase5-sudo
unset SUDO_PASSWORD
tmux new-session -d -s kimi3-moe-prefill \
"cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill && \
SUDO_PASSWORD_FILE=/tmp/kimi3-phase5-sudo RUN_ID=\"${RUN_ID}\" \
bash run_moe_backend_prefill.sh all 2>&1 | tee /data/hzy/${RUN_ID}.log"
```
Delete `/tmp/kimi3-phase5-sudo` after the run. The file is mode `0600` and is
never copied to result directories.
## Outputs
```text
results/<RUN_ID>/
build/ per-node image build and smoke evidence
service/ exact docker commands and all four server logs
raw/ one benchmark JSONL per repeat
bench/ human-readable benchmark logs
gpu/ per-node GPU snapshots
results.csv flat repeat-level metrics
summary.json median metrics and FlashInfer-vs-Marlin deltas
orchestrator.log lifecycle and progress
```
The run aborts on a failed service start, any failed request, a missing result,
or an image capability smoke failure. Containers are removed on exit.
The candidate image preserves the Kimi base image's dependency set. It installs
the patched FlashInfer wheel/AOT libraries and applies a minimal diff against
the image's exact two SGLang source files; it does not replace the complete
SGLang Python tree or require a newer `sglang-kernel` package.
## Audited software identity and upstream boundary
The accepted run used the following immutable candidate identity:
```text
image: local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5
image ID: sha256:0b8e8ddceccecc0daf10b59ddd4177036affc81facc8bf87ea9e2456ab1e96b9
SGLang: 0.5.16
flashinfer-python: 0.6.18 (candidate source wheel)
flashinfer-cubin: 0.6.15.post1
flashinfer-jit-cache: 0.6.15.post1+cu130
Kimi SiTU: beta=4.0, linear_beta=25.0
```
These results remain valid for that candidate. They do not mean stock
FlashInfer 0.6.17 supports the complete Kimi SM120 combination. The CUTLASS
SiTU kernel is being upstreamed in FlashInfer PR #4460. The final SGLang change
depends on that API and retains only Kimi layout/parameter integration,
noncontiguous input handling, the SM120 attention-residual guard, and tests.
See `../kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/UPSTREAM_DUPLICATION_AUDIT.md`.

View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Kimi-K3 real-serving MoE backend acceptance on 601-604.
EXPERIMENT="kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill"
MODEL_PATH="${MODEL_PATH:-/data/hf_models/Kimi-K3}"
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-kimi-k3}"
PORT="${PORT:-30000}"
HEAD_HOST="${HEAD_HOST:-174.1.60.1}"
NODE_SSH_USER="${NODE_SSH_USER:-user}"
NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4)
DIST_PORT="${DIST_PORT:-20000}"
EP_SIZE="${EP_SIZE:-4}"
BASE_IMAGE="${BASE_IMAGE:-lmsysorg/sglang:kimi-k3-fiv617situ-warm}"
CANDIDATE_IMAGE="${CANDIDATE_IMAGE:-local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5}"
FLASHINFER_TARGET="${FLASHINFER_TARGET:-/tmp/hzy-fi-situ-phase3-v3}"
FLASHINFER_WHEEL="${FLASHINFER_WHEEL:-/data/hzy/artifacts/flashinfer-kimi-sm120-situ-phase3-v3/flashinfer_python-0.6.18-py3-none-any.whl}"
BENCH_DATASET_NAME="${BENCH_DATASET_NAME:-random-ids}"
BENCH_MODULE="${BENCH_MODULE:-sglang.benchmark.serving}"
RESULT_BASE="${RESULT_BASE:-${SCRIPT_DIR}/results}"
INPUT_LEN="${INPUT_LEN:-16384}"
OUTPUT_LEN="${OUTPUT_LEN:-1}"
NUM_PROMPTS="${NUM_PROMPTS:-40}"
REPEATS="${REPEATS:-3}"
WARMUP_REQUESTS="${WARMUP_REQUESTS:-2}"
HEALTH_WAIT_S="${HEALTH_WAIT_S:-2400}"
IFS=',' read -r -a BACKENDS <<<"${BACKENDS_CSV:-marlin,flashinfer_mxfp4}"
IFS=',' read -r -a CHUNKED_PREFILL_SIZES \
<<<"${CHUNKED_PREFILL_SIZES_CSV:-8192,16384}"
IFS=',' read -r -a CONCURRENCIES <<<"${CONCURRENCIES_CSV:-8,16}"

View File

@ -0,0 +1,309 @@
--- a/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
+++ b/python/sglang/srt/layers/moe/moe_runner/flashinfer_cutlass.py
@@ -114,17 +114,21 @@
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import get_activation_type
_, ActivationType = _flashinfer_cutlass_fused_moe()
- activation = ActivationType(
- get_activation_type(
- runner_config.activation,
- is_gated=runner_config.is_gated,
+ if runner_config.activation == "situ" and runner_config.is_gated:
+ activation = ActivationType.Situ
+ else:
+ activation = ActivationType(
+ get_activation_type(
+ runner_config.activation,
+ is_gated=runner_config.is_gated,
+ )
)
- )
supported = {
ActivationType.Swiglu,
ActivationType.Geglu,
ActivationType.Relu2,
ActivationType.Identity,
+ ActivationType.Situ,
}
assert activation in supported, (
f"Activation {runner_config.activation!r} "
@@ -306,7 +310,7 @@
quant_info, FlashInferCutlassMxfp4MoeQuantInfo
), f"Unexpected quant_info type for flashinfer_mxfp4: {type(quant_info)}"
- flashinfer_cutlass_fused_moe, ActivationType = _flashinfer_cutlass_fused_moe()
+ flashinfer_cutlass_fused_moe, _ = _flashinfer_cutlass_fused_moe()
x = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
@@ -343,3 +347,4 @@
+ x = x.contiguous()
x, input_sf = mxfp8_quantize(
x,
is_sf_swizzled_layout=True,
@@ -384,7 +389,7 @@
ep_rank=quant_info.moe_ep_rank,
use_w4_group_scaling=not use_mxfp8_act_scaling,
use_mxfp8_act_scaling=use_mxfp8_act_scaling,
- activation_type=ActivationType.Swiglu,
+ activation_type=_activation_type(runner_config),
tune_max_num_tokens=next_power_of_2(x.shape[0]),
output=out,
)
--- a/python/sglang/srt/layers/quantization/mxfp4.py
+++ b/python/sglang/srt/layers/quantization/mxfp4.py
@@ -340,8 +340,7 @@
self.flashinfer_mxfp4_moe_precision = (
get_server_args().flashinfer_mxfp4_moe_precision
)
- # When `flashinfer_mxfp4` is enabled, dispatch to one of two FlashInfer
- # entry points depending on the GPU:
+ # When `flashinfer_mxfp4` is enabled, dispatch by GPU architecture:
# - SM100 (Blackwell) -> trtllm_fp4_block_scale_moe (existing)
# - SM90 (Hopper) -> cutlass_fused_moe(use_w4_group_scaling=True)
# (FlashInfer PR #3084, post-0.6.10)
@@ -349,6 +348,8 @@
if self.use_flashinfer:
if is_sm100_supported():
self._fi_kernel = "trtllm_sm100"
+ elif is_sm120_supported():
+ self._fi_kernel = "cutlass_sm120"
elif is_sm90_supported():
if not _FI_HAS_SM90_CUTLASS_MXFP4:
raise RuntimeError(
@@ -360,7 +361,8 @@
self._fi_kernel = "cutlass_sm90"
else:
raise NotImplementedError(
- "moe_runner_backend=flashinfer_mxfp4 requires SM90 or SM100."
+ "moe_runner_backend=flashinfer_mxfp4 requires SM90, SM100, "
+ "or SM120."
)
def create_weights(
@@ -412,7 +414,7 @@
intermediate_size_per_partition_after_pad = round_up(
intermediate_size_per_partition, triton_kernels_padding_alignment
)
- elif self._fi_kernel == "cutlass_sm90":
+ elif self._fi_kernel in ("cutlass_sm90", "cutlass_sm120"):
# cutlass mixed-input GEMM contraction dim K must be % 128 == 0
# (interleave factor for MXFP4 group_size=32 is 4). The kernel
# also expects ``fc1_expert_weights`` in halved ``[up; gate]``
@@ -609,6 +611,9 @@
if self._fi_kernel == "cutlass_sm90":
self._process_weights_for_sm90_cutlass(layer)
return
+ if self._fi_kernel == "cutlass_sm120":
+ self._process_weights_for_sm120_cutlass(layer)
+ return
if self.use_flashinfer:
# Per-expert buffers are local (create_weights uses num_local_experts);
# the global self.num_experts here breaks EP>1. Mirrors the SM90 path.
@@ -1130,7 +1135,136 @@
layer.w2_weight_bias = Parameter(w2_bias_padded, requires_grad=False)
torch.cuda.empty_cache()
+
+ def _process_weights_for_sm120_cutlass(self, layer):
+ """Prepare Kimi/GPT-OSS MXFP4 experts for FlashInfer CUTLASS SM120."""
+ from flashinfer import block_scale_interleave
+
+ sf_block_size = 32
+ n_un = layer.w13_weight.shape[1] // 2
+ k_un = layer.w13_weight.shape[2] * 2
+ n_pad = self._padded_intermediate
+ k_pad = self._padded_hidden
+ num_experts = layer.num_local_experts
+ device = layer.w13_weight.device
+ gate_up_interleaved = getattr(
+ layer.moe_runner_config, "gate_up_interleaved", True
+ )
+
+ def split_gate_up(unpadded):
+ if gate_up_interleaved:
+ return unpadded[:, 0::2, :], unpadded[:, 1::2, :]
+ return unpadded[:, :n_un, :], unpadded[:, n_un : 2 * n_un, :]
+
+ def stack_up_gate(unpadded, last_pad, last_un):
+ gate_rows, up_rows = split_gate_up(unpadded)
+ out = torch.zeros(
+ num_experts,
+ 2 * n_pad,
+ last_pad,
+ dtype=unpadded.dtype,
+ device=device,
+ )
+ out[:, :n_un, :last_un] = up_rows
+ out[:, n_pad : n_pad + n_un, :last_un] = gate_rows
+ return out
+
+ w13_padded = stack_up_gate(
+ layer.w13_weight.data, k_pad // 2, k_un // 2
+ )
+ w13_scale_padded = stack_up_gate(
+ layer.w13_weight_scale.data,
+ k_pad // sf_block_size,
+ k_un // sf_block_size,
+ )
+ bias_dtype = layer.w13_weight_bias.dtype
+ w13_bias_padded = torch.zeros(
+ num_experts, 2 * n_pad, dtype=bias_dtype, device=device
+ )
+ if gate_up_interleaved:
+ gate_bias = layer.w13_weight_bias.data[:, 0::2]
+ up_bias = layer.w13_weight_bias.data[:, 1::2]
+ else:
+ gate_bias = layer.w13_weight_bias.data[:, :n_un]
+ up_bias = layer.w13_weight_bias.data[:, n_un : 2 * n_un]
+ w13_bias_padded[:, :n_un] = up_bias
+ w13_bias_padded[:, n_pad : n_pad + n_un] = gate_bias
+
+ def pad_w2(unpadded, last_pad, last_un):
+ out = torch.zeros(
+ num_experts,
+ k_pad,
+ last_pad,
+ dtype=unpadded.dtype,
+ device=device,
+ )
+ out[:, :k_un, :last_un] = unpadded[:, :k_un, :]
+ return out
+
+ w2_padded = pad_w2(layer.w2_weight.data, n_pad // 2, n_un // 2)
+ w2_scale_padded = pad_w2(
+ layer.w2_weight_scale.data,
+ n_pad // sf_block_size,
+ n_un // sf_block_size,
+ )
+ w2_bias_padded = torch.zeros(
+ num_experts, k_pad, dtype=bias_dtype, device=device
+ )
+ w2_bias_padded[:, :k_un] = layer.w2_weight_bias.data
+
+ layer.w13_weight = Parameter(w13_padded, requires_grad=False)
+ layer.w2_weight = Parameter(w2_padded, requires_grad=False)
+ layer.w13_weight_scale = Parameter(
+ block_scale_interleave(w13_scale_padded).reshape_as(w13_scale_padded),
+ requires_grad=False,
+ )
+ layer.w2_weight_scale = Parameter(
+ block_scale_interleave(w2_scale_padded).reshape_as(w2_scale_padded),
+ requires_grad=False,
+ )
+ layer.w13_weight_bias = Parameter(w13_bias_padded, requires_grad=False)
+ layer.w2_weight_bias = Parameter(w2_bias_padded, requires_grad=False)
+
+ activation = layer.moe_runner_config.activation
+ alpha = layer.moe_runner_config.gemm1_alpha
+ # Older Kimi image baselines predate the generic gemm1_beta field.
+ beta = getattr(layer.moe_runner_config, "gemm1_beta", None)
+ limit = layer.moe_runner_config.gemm1_clamp_limit
+ if activation == "situ":
+ alpha = 4.0 if alpha is None else alpha
+ beta = 25.0 if limit is None else limit
+ limit = None
+ else:
+ alpha = 1.702 if alpha is None else alpha
+ beta = 1.0 if beta is None else beta
+ limit = 7.0 if limit is None else limit
+
+ layer.swiglu_alpha = Parameter(
+ torch.full((num_experts,), alpha, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
+ layer.swiglu_beta = Parameter(
+ torch.full((num_experts,), beta, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
+ layer.swiglu_limit = (
+ None
+ if limit is None
+ else Parameter(
+ torch.full(
+ (num_experts,), limit, dtype=torch.float32, device=device
+ ),
+ requires_grad=False,
+ )
+ )
+ layer.mxfp4_weight_global_scale = Parameter(
+ torch.ones(num_experts, dtype=torch.float32, device=device),
+ requires_grad=False,
+ )
+ layer._mxfp4_backend = "flashinfer_cutlass_sm120"
+ torch.cuda.empty_cache()
+
def create_moe_runner(
self, layer: torch.nn.Module, moe_runner_config: MoeRunnerConfig
):
@@ -1154,10 +1288,23 @@
or moe_runner_backend.is_deep_gemm()
):
self.runner = MoeRunner(moe_runner_backend, moe_runner_config)
- elif (
- moe_runner_backend.is_flashinfer_mxfp4()
- and self._fi_kernel == "cutlass_sm90"
+ elif moe_runner_backend.is_flashinfer_mxfp4() and self._fi_kernel in (
+ "cutlass_sm90",
+ "cutlass_sm120",
):
+ if (
+ self._fi_kernel == "cutlass_sm120"
+ and moe_runner_config.activation == "situ"
+ ):
+ from flashinfer.fused_moe import core as flashinfer_moe_core
+
+ if not getattr(
+ flashinfer_moe_core, "CUTLASS_FUSED_MOE_SUPPORTS_SITU", False
+ ):
+ raise RuntimeError(
+ "Kimi-K3 FlashInfer MXFP4 MoE on SM120 requires a "
+ "FlashInfer build with CUTLASS SiTU support."
+ )
# Register the fused func at runner construction so the FusedOpPool
# lookup at `MoeRunner.__init__` finds it.
import sglang.srt.layers.moe.moe_runner.flashinfer_cutlass # noqa: F401
@@ -1215,8 +1362,33 @@
quant_info = build_marlin_moe_quant_info(layer)
return self.runner.run(
dispatch_output._replace(hidden_states=x_padded), quant_info
+ )
+
+ def _apply_sm120_cutlass(self, layer, dispatch_output):
+ """SM120 MXFP8 x MXFP4 MoE via FlashInfer CUTLASS."""
+ from sglang.srt.layers.moe.moe_runner.flashinfer_cutlass import (
+ FlashInferCutlassMxfp4MoeQuantInfo,
)
+ quant_info = FlashInferCutlassMxfp4MoeQuantInfo(
+ w13_weight=layer.w13_weight,
+ w2_weight=layer.w2_weight,
+ w13_weight_scale=layer.w13_weight_scale,
+ w2_weight_scale=layer.w2_weight_scale,
+ mxfp4_weight_global_scale=layer.mxfp4_weight_global_scale,
+ w13_bias=layer.w13_weight_bias if self.with_bias else None,
+ w2_bias=layer.w2_weight_bias if self.with_bias else None,
+ swiglu_alpha=layer.swiglu_alpha,
+ swiglu_beta=layer.swiglu_beta,
+ swiglu_limit=layer.swiglu_limit,
+ moe_tp_size=layer.moe_tp_size,
+ moe_tp_rank=layer.moe_tp_rank,
+ moe_ep_size=layer.moe_ep_size,
+ moe_ep_rank=layer.moe_ep_rank,
+ padded_hidden=self._padded_hidden,
+ )
+ return self.runner.run(dispatch_output, quant_info)
+
def apply(
self,
layer: torch.nn.Module,
@@ -1290,6 +1462,8 @@
if self._fi_kernel == "cutlass_sm90":
return self._apply_sm90_cutlass(layer, dispatch_output)
+ if self._fi_kernel == "cutlass_sm120":
+ return self._apply_sm120_cutlass(layer, dispatch_output)
if self.use_flashinfer:
# When bf16 mode is enabled, we don't need to quantize the input,
# TRT-LLM automatically handles quantization in the kernel implementation and pipelines it with GEMM operations,

View File

@ -0,0 +1,432 @@
#!/usr/bin/env bash
# Compare Marlin and FlashInfer MXFP4 on real four-node Kimi-K3 prefill.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/../../../scripts/common/lib.sh"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/../../../scripts/common/platform.sh"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/config.env"
ACTION="${1:-all}"
RUN_ID="${RUN_ID:-kimi3-moe-prefill-$(date '+%Y%m%d-%H%M%S')}"
RESULT_ROOT="${RESULT_BASE}/${RUN_ID}"
BUILD_CONTEXT="/tmp/${EXPERIMENT}_image_context"
CONTAINER_PREFIX="${EXPERIMENT}"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
mkdir -p "${RESULT_ROOT}"/{build,service,raw,bench,gpu}
log_init "${RESULT_ROOT}/orchestrator.log"
usage() {
cat <<'EOF'
Usage: SUDO_PASSWORD=... bash run_moe_backend_prefill.sh ACTION
Actions:
build-image Build the same candidate image on 601-604.
run Run the fixed 8-case matrix with an existing image.
all Build the image, run the matrix, and summarize it (default).
summarize Rebuild CSV summaries from an existing RUN_ID.
stop Remove this experiment's containers from all four nodes.
EOF
}
require_password() {
if [[ -z "${SUDO_PASSWORD:-}" && -n "${SUDO_PASSWORD_FILE:-}" ]]; then
[[ -r "$SUDO_PASSWORD_FILE" ]] || {
echo "ERROR: cannot read SUDO_PASSWORD_FILE=$SUDO_PASSWORD_FILE" >&2
exit 2
}
IFS= read -r SUDO_PASSWORD <"$SUDO_PASSWORD_FILE"
fi
if [[ -z "${SUDO_PASSWORD:-}" ]]; then
echo "ERROR: set SUDO_PASSWORD or SUDO_PASSWORD_FILE at runtime" >&2
exit 2
fi
}
is_head() {
[[ "$1" == "$HEAD_HOST" ]]
}
run_host() {
local host="$1"
shift
if is_head "$host"; then
"$@"
else
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" "$@"
fi
}
sudo_host() {
local host="$1"
shift
require_password
if is_head "$host"; then
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' -- "$@"
return
fi
local remote_cmd
printf -v remote_cmd '%q ' "$@"
printf '%s\n' "$SUDO_PASSWORD" | \
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"sudo -S -p '' -- ${remote_cmd}"
}
check_inputs() {
local path
for path in "$MODEL_PATH" "$FLASHINFER_TARGET/flashinfer" \
"$FLASHINFER_WHEEL" \
"$SCRIPT_DIR/patches/sglang-kimi-image-sm120-flashinfer-mxfp4.patch"; do
[[ -e "$path" ]] || { echo "ERROR: required path missing: $path" >&2; exit 2; }
done
[[ -d "$FLASHINFER_TARGET/flashinfer/data/aot/fused_moe_120" ]] || {
echo "ERROR: fused_moe_120 AOT artifact is missing" >&2
exit 2
}
}
prepare_build_context() {
log "preparing candidate image context at ${BUILD_CONTEXT}"
rm -rf "$BUILD_CONTEXT"
mkdir -p "$BUILD_CONTEXT/aot"
cp "$SCRIPT_DIR/Dockerfile" "$BUILD_CONTEXT/Dockerfile"
cp "$SCRIPT_DIR/patches/sglang-kimi-image-sm120-flashinfer-mxfp4.patch" \
"$BUILD_CONTEXT/sglang.patch"
cp "$FLASHINFER_WHEEL" "$BUILD_CONTEXT/flashinfer_python-0.6.18-py3-none-any.whl"
cp -a "$FLASHINFER_TARGET/flashinfer/data/aot/." "$BUILD_CONTEXT/aot/"
find "$BUILD_CONTEXT" -type d -name __pycache__ -prune -exec rm -rf {} +
find "$BUILD_CONTEXT" -type f -name '*.pyc' -delete
du -sh "$BUILD_CONTEXT" | tee "$RESULT_ROOT/build/context_size.txt"
}
stage_context() {
local host="$1"
if is_head "$host"; then
return
fi
log "staging image context on ${host}"
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"rm -rf '$BUILD_CONTEXT' && mkdir -p '$BUILD_CONTEXT'"
tar -C "$BUILD_CONTEXT" -cf - . | \
ssh "${SSH_OPTS[@]}" "${NODE_SSH_USER}@${host}" \
"tar -C '$BUILD_CONTEXT' -xf -"
}
build_one_node() {
local host="$1"
local log_path="$RESULT_ROOT/build/${host}.log"
log "building ${CANDIDATE_IMAGE} on ${host}"
sudo_host "$host" docker build \
--build-arg "BASE_IMAGE=${BASE_IMAGE}" \
--tag "$CANDIDATE_IMAGE" "$BUILD_CONTEXT" >"$log_path" 2>&1
sudo_host "$host" docker image inspect "$CANDIDATE_IMAGE" \
--format '{{.Id}} {{.Size}}' >"$RESULT_ROOT/build/${host}.image.txt"
}
smoke_image() {
local host="$1"
sudo_host "$host" docker run --rm --gpus device=0 \
--entrypoint python3 "$CANDIDATE_IMAGE" -c \
"import torch; from flashinfer.fused_moe import core; import sglang; assert torch.cuda.get_device_capability()[0] == 12; assert core.CUTLASS_FUSED_MOE_SUPPORTS_SITU is True; print(sglang.__file__, core.ActivationType.Situ)" \
>"$RESULT_ROOT/build/${host}.smoke.txt" 2>&1
}
build_image_all_nodes() {
require_password
check_inputs
prepare_build_context
local host
for host in "${NODE_HOSTS[@]}"; do stage_context "$host"; done
local -a pids=()
for host in "${NODE_HOSTS[@]}"; do
build_one_node "$host" &
pids+=("$!")
done
local rc=0 pid
for pid in "${pids[@]}"; do wait "$pid" || rc=1; done
(( rc == 0 )) || { log "ERROR: candidate image build failed"; return 1; }
for host in "${NODE_HOSTS[@]}"; do smoke_image "$host"; done
log "candidate image built and smoke-tested on all four nodes"
}
container_name() {
local rank="$1"
printf '%s_node%s' "$CONTAINER_PREFIX" "$rank"
}
collect_gpu_snapshot() {
local label="$1" host rank=0
for host in "${NODE_HOSTS[@]}"; do
sudo_host "$host" nvidia-smi \
--query-gpu=timestamp,index,memory.used,memory.total,utilization.gpu,power.draw \
--format=csv,noheader,nounits \
>"$RESULT_ROOT/gpu/${label}_node${rank}.csv" 2>&1 || true
rank=$((rank + 1))
done
}
collect_service_logs() {
local label="$1" host rank=0 name
for host in "${NODE_HOSTS[@]}"; do
name="$(container_name "$rank")"
sudo_host "$host" docker logs "$name" \
>"$RESULT_ROOT/service/${label}_node${rank}.log" 2>&1 || true
rank=$((rank + 1))
done
}
stop_service() {
local host rank=0 name
for host in "${NODE_HOSTS[@]}"; do
name="$(container_name "$rank")"
sudo_host "$host" docker rm -f "$name" >/dev/null 2>&1 || true
rank=$((rank + 1))
done
}
start_node() {
local rank="$1" backend="$2" chunk="$3"
local host="${NODE_HOSTS[$rank]}" name bootstrap
name="$(container_name "$rank")"
bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)); exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size 32 --ep-size ${EP_SIZE} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend ${backend} --chunked-prefill-size ${chunk} --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}"
local -a cmd=(
docker run -d --name "$name"
--gpus all --network host --ipc=host --ulimit memlock=-1
--device /dev/infiniband --shm-size 32g --entrypoint bash
-v "${MODEL_PATH}:${MODEL_PATH}:ro"
-e CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
-e NCCL_SOCKET_IFNAME=bond0 -e GLOO_SOCKET_IFNAME=bond0
-e NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
-e NCCL_IB_GID_INDEX=3 -e NCCL_IB_TIMEOUT=22 -e NCCL_IB_RETRY_CNT=7
-e NCCL_CUMEM_ENABLE=1 -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
-e SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 -e SGLANG_MOE_FUSED_GATE_RADIX=1
-e FLASHINFER_DISABLE_JIT=1 -e FLASHINFER_DISABLE_VERSION_CHECK=1
"$CANDIDATE_IMAGE" -lc "$bootstrap"
)
printf '%q ' "${cmd[@]}" >"$RESULT_ROOT/service/${backend}_chunk${chunk}_node${rank}.cmd.txt"
printf '\n' >>"$RESULT_ROOT/service/${backend}_chunk${chunk}_node${rank}.cmd.txt"
sudo_host "$host" "${cmd[@]}" >/dev/null
}
wait_health() {
local backend="$1" chunk="$2" i
for ((i = 1; i <= HEALTH_WAIT_S; i++)); do
if curl --fail --silent --max-time 5 "http://${HEAD_HOST}:${PORT}/health" >/dev/null 2>&1; then
log "service healthy backend=${backend} chunk=${chunk} wait_s=${i}"
return 0
fi
if (( i % 30 == 0 )); then
log "waiting for service backend=${backend} chunk=${chunk} elapsed_s=${i}"
collect_service_logs "${backend}_chunk${chunk}_starting"
if grep -Eiq 'Traceback|CUDA out of memory|NCCL.*(error|failed)|RuntimeError' \
"$RESULT_ROOT/service/${backend}_chunk${chunk}_starting_node"*.log; then
log "startup log contains an error; stopping early"
return 1
fi
fi
sleep 1
done
return 1
}
start_service() {
local backend="$1" chunk="$2"
stop_service
log "starting TP32/EP${EP_SIZE} backend=${backend} chunk=${chunk} on 601-604"
start_node 1 "$backend" "$chunk"
start_node 2 "$backend" "$chunk"
start_node 3 "$backend" "$chunk"
sleep 5
start_node 0 "$backend" "$chunk"
if ! wait_health "$backend" "$chunk"; then
collect_service_logs "${backend}_chunk${chunk}_startup_failed"
stop_service
return 1
fi
collect_gpu_snapshot "${backend}_chunk${chunk}_healthy"
collect_service_logs "${backend}_chunk${chunk}_healthy"
}
run_one_bench() {
local backend="$1" chunk="$2" concurrency="$3" repeat="$4"
local stem="${backend}_chunk${chunk}_c${concurrency}_r${repeat}"
local output_file="${RESULT_ROOT}/raw/${stem}.jsonl"
local bench_log="${RESULT_ROOT}/bench/${stem}.log"
local -a dataset_args=(--dataset-name "$BENCH_DATASET_NAME")
rm -f "$output_file"
log "bench backend=${backend} chunk=${chunk} C=${concurrency} repeat=${repeat}/${REPEATS}"
sudo_host "$HEAD_HOST" docker run --rm --network host \
-v "${MODEL_PATH}:${MODEL_PATH}:ro" \
-v "${RESULT_ROOT}:${RESULT_ROOT}" \
-e PYTHONUNBUFFERED=1 --entrypoint python3 "$CANDIDATE_IMAGE" \
-m "$BENCH_MODULE" \
--backend sglang --host "$HEAD_HOST" --port "$PORT" \
--tokenizer "$MODEL_PATH" \
"${dataset_args[@]}" \
--random-input-len "$INPUT_LEN" --random-output-len "$OUTPUT_LEN" \
--random-range-ratio 1.0 --num-prompts "$NUM_PROMPTS" \
--max-concurrency "$concurrency" --request-rate 10000 \
--warmup-requests "$WARMUP_REQUESTS" --output-file "$output_file" \
--output-details --disable-tqdm >"$bench_log" 2>&1
python3 - "$output_file" "$NUM_PROMPTS" <<'PY'
import json, sys
path, expected = sys.argv[1], int(sys.argv[2])
with open(path, encoding="utf-8") as f:
rows = [json.loads(line) for line in f if line.strip()]
assert len(rows) == 1, (path, len(rows))
assert rows[0].get("completed") == expected, rows[0].get("completed")
assert not any(rows[0].get("errors", [])), "benchmark contains request errors"
PY
}
run_matrix() {
require_password
check_inputs
local backend chunk concurrency repeat label
collect_gpu_snapshot before_all
for chunk in "${CHUNKED_PREFILL_SIZES[@]}"; do
for backend in "${BACKENDS[@]}"; do
label="${backend}_chunk${chunk}"
if ! start_service "$backend" "$chunk"; then
log "ERROR: service failed for ${label}; preserving evidence and aborting"
return 1
fi
for concurrency in "${CONCURRENCIES[@]}"; do
for ((repeat = 1; repeat <= REPEATS; repeat++)); do
run_one_bench "$backend" "$chunk" "$concurrency" "$repeat"
done
done
collect_service_logs "${label}_completed"
collect_gpu_snapshot "${label}_completed"
stop_service
sleep 5
done
done
collect_gpu_snapshot after_all
}
summarize() {
python3 - "$RESULT_ROOT" <<'PY'
import csv, json, re, statistics, sys
from pathlib import Path
root = Path(sys.argv[1])
pattern = re.compile(r"(.+)_chunk(\d+)_c(\d+)_r(\d+)\.jsonl$")
fields = [
"backend", "chunked_prefill_size", "concurrency", "repeat", "completed",
"duration", "request_throughput", "input_throughput", "output_throughput",
"total_throughput", "mean_ttft_ms", "median_ttft_ms", "p95_ttft_ms",
"p99_ttft_ms", "mean_e2e_latency_ms", "median_e2e_latency_ms",
"p95_e2e_latency_ms", "p99_e2e_latency_ms",
]
rows = []
for path in sorted((root / "raw").glob("*.jsonl")):
match = pattern.match(path.name)
if not match:
continue
with path.open(encoding="utf-8") as f:
data = next(json.loads(line) for line in f if line.strip())
backend, chunk, concurrency, repeat = match.groups()
row = {key: data.get(key) for key in fields}
row.update(
backend=backend,
chunked_prefill_size=int(chunk),
concurrency=int(concurrency),
repeat=int(repeat),
)
rows.append(row)
with (root / "results.csv").open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
metric_names = [
"request_throughput", "input_throughput", "total_throughput",
"mean_ttft_ms", "median_ttft_ms", "p95_ttft_ms", "p99_ttft_ms",
"mean_e2e_latency_ms", "median_e2e_latency_ms", "p95_e2e_latency_ms",
]
groups = {}
for row in rows:
key = (row["backend"], row["chunked_prefill_size"], row["concurrency"])
groups.setdefault(key, []).append(row)
summary = []
for (backend, chunk, concurrency), group in sorted(groups.items()):
item = {
"backend": backend,
"chunked_prefill_size": chunk,
"concurrency": concurrency,
"repeats": len(group),
"completed_each": [row["completed"] for row in group],
}
for metric in metric_names:
values = [float(row[metric]) for row in group if row.get(metric) is not None]
item[f"median_{metric}"] = statistics.median(values) if values else None
summary.append(item)
comparisons = []
index = {(x["backend"], x["chunked_prefill_size"], x["concurrency"]): x for x in summary}
for chunk in sorted({x["chunked_prefill_size"] for x in summary}):
for concurrency in sorted({x["concurrency"] for x in summary}):
base = index.get(("marlin", chunk, concurrency))
cand = index.get(("flashinfer_mxfp4", chunk, concurrency))
if not base or not cand:
continue
comp = {"chunked_prefill_size": chunk, "concurrency": concurrency}
for metric in ("input_throughput", "total_throughput"):
b = base[f"median_{metric}"]
c = cand[f"median_{metric}"]
comp[f"flashinfer_{metric}_change_pct"] = (c / b - 1.0) * 100.0
for metric in ("median_ttft_ms", "p95_ttft_ms", "median_e2e_latency_ms"):
b = base[f"median_{metric}"]
c = cand[f"median_{metric}"]
comp[f"flashinfer_{metric}_change_pct"] = (c / b - 1.0) * 100.0
comparisons.append(comp)
payload = {"run_id": root.name, "summary": summary, "comparisons": comparisons}
(root / "summary.json").write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(json.dumps(payload, indent=2))
PY
}
cleanup() {
collect_service_logs cleanup 2>/dev/null || true
stop_service 2>/dev/null || true
}
trap cleanup EXIT INT TERM
case "$ACTION" in
build-image)
build_image_all_nodes
;;
run)
run_matrix
summarize
;;
all)
build_image_all_nodes
run_matrix
summarize
;;
summarize)
summarize
;;
stop)
stop_service
;;
-h|--help|help)
usage
;;
*)
usage >&2
exit 2
;;
esac
trap - EXIT INT TERM
log "action=${ACTION} complete run_id=${RUN_ID} result_root=${RESULT_ROOT}"

View File

@ -0,0 +1,273 @@
# Kimi K3 推理优化 - 优化 Prefill - TP Reduce Scatter
## 结论
**不推荐在当前 Kimi-K3 TP32 部署上继续这条优化路线。**
SGLang 的通用 `--enable-attn-tp-input-scattered` 优化适用于后续 Attention
只依赖 QKV latent 的 MLA 模型。Kimi-K3 的 MLA 额外包含
`g_proj(hidden_states)` 输出门控,而且 93 层中只有 24 层为 MLA、其余 69
层为 KDA。为了保证门控和 KDA 正确,仍需恢复完整的 7168 维 hidden。
这样不仅无法消除原有 hidden All-Reduce还会增加一次 2112 维 latent
All-Gather与“降低跨节点通信”的目标相反。
因此本任务在源码审计和最小原型验证后停止,不进入四节点性能测试,不形成
SGLang PR。后续 Prefill 通信优化应优先转向 MoE A2A 和 Pipeline
Parallelism。
## 目标与固定测试口径
原计划比较以下两组:
```text
模型Kimi-K3
节点601-60432 GPU
并行TP32 / EP4 / DP1
MoE runnerflashinfer_mxfp4
请求16K -> 1
并发C=8、C=16
Chunked Prefill8K
变量:--enable-attn-tp-input-scattered 关闭 / 开启
```
源码审计已证明当前开关在 Kimi-K3 中没有完整接线,而且直接移植不能降低
通信,因此没有消耗四节点资源运行该矩阵。
## DeepSeek 路径为什么成立
通用优化把原路径:
```text
embed / MLP partial output
-> All-Reduce hidden
-> RMSNorm(full hidden)
-> qkv_a(full hidden)
```
改为:
```text
embed / MLP partial output
-> Reduce-Scatter hidden by token
-> RMSNorm(local tokens)
-> qkv_a(local tokens)
-> All-Gather QKV latent
```
Kimi-K3 的张量宽度为:
```text
hidden = 7168
QKV latent = q_lora_rank + kv_lora_rank + qk_rope_head_dim
= 1536 + 512 + 64
= 2112
```
如果后续只需要 latent将 7168 维 hidden 的 All-Reduce 改成 7168 维
Reduce-Scatter再 All-Gather 2112 维 latent可以同时减少 RMSNorm/QKV
重复计算和通信量。
## Kimi-K3 为什么不能直接复用
### MLA 输出门控仍依赖完整 hidden
Kimi-K3 MLA 有一条独立门控支路:
```text
full hidden
|-- qkv_a -> QKV latent -> Attention --|
`-- g_proj -> sigmoid(gate) ------------|-> elementwise gate
```
相关实现位于:
```text
python/sglang/srt/models/kimi_k3.py
KimiK3MLAAttention.__init__
KimiK3MLAAttention.forward
_gated_o_proj_forward
```
`g_proj` 是按 Attention Head 切分的 `ColumnParallelLinear`。TP32 时,每个
rank 只持有自己的 3 个 head 的权重,但必须为全部 token 计算门控:
```text
hidden_all: [T, 7168]
W_gate_rank: [384, 7168]
gate_rank: [T, 384]
```
Reduce-Scatter 后每个 rank 只有 `[T/32, 7168]`,只能得到
`[T/32, 384]`。把各 rank 的 gate 直接 All-Gather 也不正确,因为每段
token 使用了不同 rank、不同 head 的权重;本 rank 需要的是自己的
`W_gate_rank` 对全部 token 的结果。
QKV latent 的 All-Gather 不能解决这个问题,因为 `g_proj` 的输入是原始
hidden不是 QKV latent。
### KDA 层要求完整 hidden
Kimi-K3 共 93 层,其中 24 层为 MLA、69 层为 KDA。KDA 的宽 Q/K/V/G
投影直接消费完整 hidden不能复用 MLA 的 2112 维 latent。即使只在 MLA
层建立 scattered window也需要在频繁的 MLA/KDA 边界恢复完整 hidden
进一步削弱收益并增加实现复杂度。
## 通信量判断
忽略常数、数据类型和不同 collective 算法,令:
```text
H = 7168 # hidden width
L = 2112 # QKV latent width
```
Ring collective 的每 rank 主量级可写为:
| 路径 | 相对通信项 |
|---|---:|
| 原始 hidden All-Reduce | `2H = 14336` |
| 理想 DeepSeek 路径hidden RS + latent AG | `H + L = 9280` |
| K3 若恢复 gate hiddenhidden RS + hidden AG + latent AG | `2H + L = 16448` |
| K3 最小原型:保留 hidden AR + latent AG | `2H + L = 16448` |
理想路径比原始路径少约 35.3% 通信K3 为门控保留或恢复完整 hidden 后,
反而比原始路径多约 14.7%。实际延迟还会叠加跨四节点 collective 的固定
开销,因此没有理由期待它改善当前以通信为目标的 Prefill。
## Prefill 通信归因
复用既有 `16K -> 256、C=8、Chunk=8K、TP32、无 MoE A2A`
PyTorch Trace并用 `record_shapes + with_stack` 原始事件重新统计。虽然该
Trace 使用 EP32/Marlin而当前验收配置为 EP4/FlashInfer但两者均为
`TP32 + moe_a2a_backend=none`MoE runner 和 EP 值会改变专家计算,不改变
下列 TP32 collective 的调用位置与张量形状。
原始 Trace
```text
/data/yy/sskj/experiments/pro6000/kimi3_pro6000_decode_profile/
trace/1786591959.655009/
decode_c8_i16384_o256-1786591959.6615255-TP-0-EP-0-EXTEND.trace.json.gz
```
Trace 捕获了 9 个 Prefill engine step。每个 step 的主 AllReduce 构成为:
| 通信来源 | 每 step 次数 | `M=8192` 单次输入 | 每 step 输入量 | 占比 |
|---|---:|---:|---:|---:|
| Embedding + 93 层 Attention `o_proj` + 首层 dense MLP | 95 | `[8192,7168]`112 MiB | 10.39 GiB | 40.8% |
| 92 个 MoE 层的 routed latent + shared expert | 92 | `8192x(3584+7168)`168 MiB | 15.09 GiB | 59.2% |
| 合计 | 187 | - | 25.48 GiB | 100% |
这也解释了旧资料里的约 187 次 collective它不是“全部来自 MoE”而是
95 次完整 hidden 归约和 92 次 MoE 尾部归约之和。Kimi-K3 配置有 93 层,
`first_k_dense_replace=1`,因此只有后 92 层走 MoE。
当前 SM120 四机部署不会启用 `k3_ar_fusion`:该路径只在 SM100/SM103 且
CustomAllReduceV2 multicast 可用时自动开启;跨节点日志也明确显示
`CustomAllreduce is disabled because this process group spans across nodes`
所以以上标准 NCCL 调用仍是当前 EP4/FlashInfer 配置的真实结构。
### 当前 EP4 + FlashInfer 真机时间线
为消除旧 Trace 使用 EP32/Marlin 的不确定性,又对当前正式配置做了三步
Prefill Nsight 捕获:
```text
TP32 / EP4 / DP1
MoE runner: flashinfer_mxfp4
MoE A2A: none
16K -> 1, C=8, Chunk=8K
```
原始结果位于 601
```text
/data/hzy/sskj/experiments/pro6000/
kimi3_pro6000_sglang_prefill_communication_profile/results/
kimi3-prefill-comm-20260820-143749/
```
四个节点均生成 `.nsys-rep``.sqlite`,每份报告覆盖本节点 8 张 GPU。
`nsys_analysis.json``nsys_device_metrics.csv` 是离线分析结果。为避免
`cudaProfilerStart` 边界开销污染结论,统计排除了出现单次 AllReduce
大于 50 ms 的 5 条触发 lane保留 27/32 条稳定 lane 取中位数。
| 指标 | 稳定 rank 中位数 | 解释 |
|---|---:|---|
| 捕获窗口 | 7274.23 ms | 三个 8K Prefill step |
| GPU busy | 99.37% | 几乎没有 GPU 空洞 |
| NCCL 时间 | 4161.36 ms57.27% | 当前 Prefill 的第一瓶颈 |
| 非 NCCL 计算 | 3062.48 ms42.10% | Attention、MoE GEMM、KDA 等 |
| NCCL 与计算重叠 | 0 ms0% | Collective 完整暴露在关键路径上 |
| AllReduce 次数 | 557 | 捕获边界略少于理论 561 次 |
| AllReduce P50 / P95 | 7.10 / 9.14 ms | 全部选择 `RING_LL` |
AllReduce 时长呈现无重叠的双峰:约 `6.1 ms``8.7 ms`,与源码 Trace
中的 112 MiB hidden 消息和 168 MiB MoE 消息相符。按两个峰的累计时间:
| 通信类别 | 三步累计时间 | 占 AllReduce 时间 | 占完整窗口 |
|---|---:|---:|---:|
| hidden / Attention 路径 | 1719.78 ms | 41.27% | 23.64% |
| MoE 尾部归约 | 2440.52 ms | 58.73% | 33.55% |
稳定 rank 的 AllReduce 平均值约为 `7.347.56 ms`,未发现固定慢节点。
这说明问题不是某张卡掉速,而是 TP32 每层同步通信本身占据关键路径。
带 Nsight 的完整 benchmark 为 40/40 请求成功,但 Input TPS 为 2573低于
无 Profiler 基线 3257.96;该值只用于证明 workload 完整成功,不能作为性能
回归结果。性能比较仍使用无 Profiler 的正式基线。
### 优化优先级
1. **MoE A2A / SP-MoE 优先验证。** 它瞄准占输入字节 59.2% 的 MoE 尾部
collective并可避免每个 TP rank 对同一批 token 的重复路由与专家计算。
但 A2A 会引入 dispatch/combine必须以四机实测判断净收益。
2. **Pipeline Parallelism 第二。** 用 TP16/PP2 或 TP8/PP4 缩小 TP
collective 通信域,同时只增加少数 stage-boundary P2P需验证流水线空泡
对 TTFT 的影响。
3. **通信量化作为独立高风险项。** 它能同时压缩两类大消息,但需要准确性与
backend 支持验证。
4. **不优先做 NCCL 算法或 launch fusion。** Prefill 单消息为 112/168 MiB
首要矛盾是字节量和 32-rank 跨节点通信域,不是小消息启动延迟。
因此,通用 TP Reduce Scatter 即使成功,也只触及字节口径约 40.8%、时间
口径约 41.3% 的 hidden 通信;它无法处理占完整 Prefill 时间约 33.6% 的
MoE 尾部归约,而且 K3 gate/KDA 又迫使完整 hidden 存在。它不如先处理
MoE 通信,再用 PP 缩小剩余 TP collective 的通信域。
## FlashInfer MoE 的关系
当前 `flashinfer_mxfp4` 是 MoE runner且实验配置保持
`moe_a2a_backend=none`,因此它不违反通用 input-scattered 的静态约束。
但它也无法解决 MLA gate 或 KDA 对完整 hidden 的依赖。
换言之:**FlashInfer MoE 与该优化不冲突,但不会使该优化在 Kimi-K3 上
成立。**
## 原型与验证记录
为验证最小可行边界,建立过仅分摊 MLA qkv latent GEMM 的研究分支:
```text
601 worktree: /data/hzy/src/sglang-kimi-tp-rs-wt
branch: hzy/kimi-k3-attn-tp-latent-scattered
commit: f5577d09
```
原型保留完整 hidden 给 K3 gate只把 replicated 的
`7168 -> 2112` qkv latent GEMM 按 token 分给 TP ranks再 All-Gather
latent。CPU 边界单测为 `3 passed`,证明 token 分片与 fallback 逻辑正确。
该原型没有进入 GPU/四节点服务测试,因为通信公式已经表明它保留原
All-Reduce 并新增 latent All-Gather。分支仅作为否决证据保留不应合并、
发布或提交上游。
## 后续建议
1. 保留已验收的 `flashinfer_mxfp4 + EP4 + chunked_prefill_size=8192`
2. 下一步测试 MoE A2A目标是减少或替换 MoE 的跨 rank collective而非
在 MLA gate 前后增加 collective。
3. 再测试 Prefill Pipeline Parallelism用更小 TP 通信域换取流水线开销,
继续使用固定口径 `16K -> 1、C=8/16、Chunk=8K`
4. 只有出现 gate-aware 的实现(例如低成本复制/重排 gate 权重,且通信模型
明确优于原始 All-Reduce才重新开启 TP Reduce Scatter 方向。