[Test] Add Kimi SM120 MXFP4 correctness matrix
This commit is contained in:
parent
dac1bb652d
commit
a1c18d736b
@ -1,6 +1,6 @@
|
|||||||
# sskj — 多平台大模型推理性能基准测试项目
|
# sskj — 多平台大模型推理性能基准测试项目
|
||||||
|
|
||||||
**更新(2026-08-14 17:08:00 CST)**:完成 Kimi-K3 / RTX PRO 6000D / SGLang FlashInfer MXFP4 MoE Phase 1 数据契约审计;已从真实 checkpoint 追到 SM120 CUTLASS 调用,确认 SiTU 激活、gate/up 权重变换和参数透传三处独立缺口,并在 6000D 上复现 C++ binding 的门控 shape 首错,详见 `experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/README.md`。
|
**更新(2026-08-17 11:56:47 CST)**:完成 Kimi-K3 / RTX PRO 6000D / SGLang FlashInfer MXFP4 MoE Phase 2 correctness harness;SwiGLU 控制矩阵 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)到货时,用最短时间完成大模型在该平台上的推理性能评估与部署配置选型。
|
||||||
|
|||||||
@ -231,15 +231,93 @@ FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
|||||||
4. 不把旧 `flashinfer_cutedsl` 实验当作本路径证据。
|
4. 不把旧 `flashinfer_cutedsl` 实验当作本路径证据。
|
||||||
5. 不进入整模型性能测试,直到 Phase 2 的 BF16/PyTorch 或 Marlin 参考正确性通过。
|
5. 不进入整模型性能测试,直到 Phase 2 的 BF16/PyTorch 或 Marlin 参考正确性通过。
|
||||||
|
|
||||||
## 下一阶段硬门槛
|
## Phase 2 correctness harness
|
||||||
|
|
||||||
Phase 2 建立独立、对称的 correctness harness,至少覆盖:
|
状态:**完成**。本阶段建立验证框架并校准 reference,但 SiTU candidate 按预期尚未通过;未产生性能晋级结论。
|
||||||
|
|
||||||
- token 数 1/2/8/32 以及 Kimi 真实 prefill/decode 代表 shape;
|
唯一入口:
|
||||||
- 均匀、倾斜、空 expert 和 masked/global expert id;
|
|
||||||
- SiTU `4/25`、MXFP8 activation、MXFP4 packed weight 与 E8M0 scale;
|
|
||||||
- NaN/Inf、确定性、CUDA Graph capture/replay;
|
|
||||||
- TP1/EP1 micro case 与 TP32/EP32 的 local-expert 映射;
|
|
||||||
- candidate 与 reference 使用同一 routing、同一量化输入和同一 finalize 口径。
|
|
||||||
|
|
||||||
正确性未通过前,不做吞吐结论,不修改 checkpoint,不使用运行时 monkeypatch。
|
```text
|
||||||
|
validate_kimi_sm120_mxfp4.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 对称性约束
|
||||||
|
|
||||||
|
- reference 与 candidate 共用同一 `Case`、随机种子、routing IDs/weights、量化后的 MXFP8 input、packed MXFP4 weights 和 E8M0 scales;reference 不读取量化前的 BF16 权重。
|
||||||
|
- FC1 在生成时显式采用 CUTLASS oracle 布局 `[up; gate]`。这使 harness 同时约束 SGLang 后续必须把 Kimi checkpoint 的 `[gate; up]` 正确交换。
|
||||||
|
- SwiGLU 作为当前 kernel 已支持的控制组;SiTU `(4, 25)` 使用完全相同的数据与调用,只改变 activation contract。
|
||||||
|
- correctness 固定 `use_fused_finalize=False`,避免非结合 atomic reduction 干扰确定性;benchmark 也由同一个 CUDA Event 计时函数驱动,但本阶段设置 iterations=0,不在共享 GPU 负载下生成性能数字。
|
||||||
|
|
||||||
|
覆盖矩阵:
|
||||||
|
|
||||||
|
| 类别 | 覆盖 |
|
||||||
|
|---|---|
|
||||||
|
| token shape | `M=1/2/8/32` |
|
||||||
|
| routing | uniform、skewed、空 expert、`-1` masked slot |
|
||||||
|
| EP | `EP2` global/local ID 过滤;Kimi `EP32/rank7/E_local=28` |
|
||||||
|
| 数值 | finite、NaN/Inf row-level propagation、三次 bitwise determinism |
|
||||||
|
| execution | eager、CUDA Graph capture、两次 replay |
|
||||||
|
| Kimi 真尺寸 | `H=3584, N=3072, E_local=28, topK=16`;decode `M=1`、prefill `M=128` |
|
||||||
|
|
||||||
|
### 执行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4
|
||||||
|
|
||||||
|
# 已支持的 SwiGLU 控制组;本阶段最终证据统一使用 GPU 6
|
||||||
|
CUDA_VISIBLE_DEVICES=6 PYTHONPATH=/tmp/hzy-fi-main-phase1 \
|
||||||
|
FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
||||||
|
/data/hzy/envs/sparse-opd-sm120/bin/python validate_kimi_sm120_mxfp4.py \
|
||||||
|
--suite smoke --activation swiglu \
|
||||||
|
--output results/phase2_control_swiglu_smoke.json
|
||||||
|
|
||||||
|
# Kimi SiTU 目标真尺寸
|
||||||
|
CUDA_VISIBLE_DEVICES=6 PYTHONPATH=/tmp/hzy-fi-main-phase1 \
|
||||||
|
FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
||||||
|
/data/hzy/envs/sparse-opd-sm120/bin/python validate_kimi_sm120_mxfp4.py \
|
||||||
|
--suite real --activation situ \
|
||||||
|
--output results/phase2_target_situ_real_gpu6.json
|
||||||
|
```
|
||||||
|
|
||||||
|
实现完成后的 CI-style gate 必须增加 `--require-situ`;若仍命中已知缺口,入口返回非零。
|
||||||
|
|
||||||
|
### 结果与证据
|
||||||
|
|
||||||
|
| 组别 | 结果 | 关键证据 |
|
||||||
|
|---|---|---|
|
||||||
|
| SwiGLU smoke control | 6/6 PASS | cosine `0.999625–0.999812`,NRMSE `1.50%–2.75%` |
|
||||||
|
| SwiGLU Kimi real shape | 2/2 PASS | decode/prefill cosine `0.999597/0.999621`,NRMSE `2.85%/2.75%` |
|
||||||
|
| SiTU smoke target | 6/6 EXPECTED_MISSING_SITU | 全部命中 binding `256 vs. 128` 首错 |
|
||||||
|
| SiTU Kimi real shape | 2/2 EXPECTED_MISSING_SITU | GPU6 上 decode/prefill 均命中同一首错 |
|
||||||
|
| determinism | PASS | non-fused finalize 三次 bitwise equal |
|
||||||
|
| CUDA Graph | PASS | control replay 与 eager `max_abs=0`,两次 replay bitwise equal |
|
||||||
|
|
||||||
|
原始证据:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/phase2_control_swiglu_smoke_gpu6.log
|
||||||
|
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/phase2_target_situ_smoke_gpu6.log
|
||||||
|
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/phase2_control_swiglu_real_gpu6.log
|
||||||
|
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/phase2_target_situ_real_gpu6.log
|
||||||
|
/data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_sm120_flashinfer_mxfp4/results/phase2_*.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### NaN/Inf 决策记录
|
||||||
|
|
||||||
|
MXFP8 quantize/dequantize 后,注入的 NaN 被量化为有限值,而 Inf 保留。相同 Inf 行在 PyTorch GEMM 与 CUTLASS reduction 中产生不同的逐元素 NaN/Inf 混合,但两者影响同一 token row,未受影响行继续满足普通数值阈值。IEEE reduction 顺序不保证逐元素 NaN/Inf 分类一致,因此 harness 要求:
|
||||||
|
|
||||||
|
1. 相同 token rows 被标记为 non-finite;
|
||||||
|
2. 未受影响 rows 继续满足 `rtol=0.2, atol=0.2`;
|
||||||
|
3. candidate 不崩溃,且三次结果确定。
|
||||||
|
|
||||||
|
这不是放宽有限值正确性,有限 case 仍采用与 FlashInfer 官方 MXFP8×MXFP4 测试一致的阈值。
|
||||||
|
|
||||||
|
## Phase 3 硬门槛
|
||||||
|
|
||||||
|
下一阶段只做最小实现:
|
||||||
|
|
||||||
|
1. FlashInfer C++ 增加与 Python 对齐的 `Situ` enum、gated classification 和 SiTU adaptor,复用已有 per-expert `alpha/beta` 参数与 grouped GEMM。
|
||||||
|
2. SGLang SM120 processor 按 `gate_up_interleaved=False` 处理 Kimi 连续半区,生成 CUTLASS `[up; gate]`,不得复用 GPT-OSS 奇偶行拆分。
|
||||||
|
3. SGLang runner 从 `runner_config` 透传 `ActivationType.Situ` 和 Kimi `4/25` 参数。
|
||||||
|
4. 未满足支持条件时在服务初始化阶段明确选择 Marlin;不得在 kernel 失败后热切换,不修改 checkpoint,不使用 runtime monkeypatch。
|
||||||
|
5. 完成后必须以 `--require-situ --suite all` 通过 Phase 2 全矩阵,才能进入任何吞吐比较。
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
reference: torch.Tensor, candidate: torch.Tensor
|
||||||
|
) -> dict:
|
||||||
|
"""Check stable row-level propagation without over-specifying reduction order.
|
||||||
|
|
||||||
|
An Inf entering a GEMM may produce a different element-wise mix of Inf and
|
||||||
|
NaN when the reduction tree changes. Requiring an identical element mask
|
||||||
|
would therefore be stricter than IEEE arithmetic permits. We require the
|
||||||
|
same affected rows, finite unaffected rows, and ordinary numeric closeness
|
||||||
|
on those unaffected rows.
|
||||||
|
"""
|
||||||
|
reference_rows = (~torch.isfinite(reference)).any(dim=1)
|
||||||
|
candidate_rows = (~torch.isfinite(candidate)).any(dim=1)
|
||||||
|
if not torch.equal(reference_rows, candidate_rows):
|
||||||
|
raise AssertionError(
|
||||||
|
"nonfinite propagation reached different token rows: "
|
||||||
|
f"reference={reference_rows.tolist()} candidate={candidate_rows.tolist()}"
|
||||||
|
)
|
||||||
|
unaffected = ~reference_rows
|
||||||
|
if bool(unaffected.any()):
|
||||||
|
torch.testing.assert_close(
|
||||||
|
candidate[unaffected],
|
||||||
|
reference[unaffected],
|
||||||
|
rtol=0.2,
|
||||||
|
atol=0.2,
|
||||||
|
)
|
||||||
|
metrics = tensor_metrics(reference, candidate)
|
||||||
|
metrics.update(
|
||||||
|
{
|
||||||
|
"contract": "same_affected_rows_and_close_unaffected_rows",
|
||||||
|
"reference_nonfinite_rows": 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()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
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(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()
|
||||||
Loading…
x
Reference in New Issue
Block a user