Skip to content

quant: add SwiGLU forward/backward VMI kernels - #20

Open
kuri780 wants to merge 7 commits into
PTO-ISA:vmi-implfrom
kuri780:swiglu-vmi
Open

kuri780 wants to merge 7 commits into
PTO-ISA:vmi-implfrom
kuri780:swiglu-vmi

Conversation

@kuri780

@kuri780 kuri780 commented Oct 19, 2026 •

Copy link
Copy Markdown

quant: add SwiGLU forward/backward VMI kernels

概述

把 TileKernels 的 SwiGLU 前向/反向 ASC kernel 迁移到 VMI(PTO)后端。外层
T.Kernel / T.Persistent / T.copy / tiling / num_stages / buffer versions /
SF 布局 / token 调度与 ASC 源码保持一致,只把 with T.SimdVF() 内的向量数学改写为
逻辑 T.vmi.*;fast-certificate → exact 重算路径完整保留。

实现要点

  • 不保留 ASC 实现作为 fallback;不引入 host 端预处理,不按 shape/dtype 新增特判。
  • 不使用 ASC 的物理 lane 布局技巧(dist=、PART、PK、INTLV、E2B_B32、ONEPT),
    改用 VMI 的逻辑 vload/vcvt/vstore、vcmax(group=...) 和 compact scale。
  • 可省略的 mask 全部去掉;API 强制要求 mask 的算子
    (vadds/vmuls/vmaxs/vmins/vshrs/vshls、vexpdif、vmula、vcmp/vsel、
    vcadd/vcmax/vcmin、vgather 以及 exact vdiv)保留。
  • 前向 amax 使用宽向量 vcmax:hidden=2048 用 256-lane/group=8,hidden=3584 用
    128-lane/group=4;带 clamp 计数和非量化输出保持 64-lane/group=2。
    vcmax 的 group 是输出个数,因此 256-lane 不能用 group=4(会得到每 64 通道一组,
    与 sf_block=(1,32) 不符)。
  • ptoas 0.66 不支持 256-lane 的 full reduction(vcmin/vcmax/vcadd 的 group=1 报
    VMI-RESIDUAL-OP),certificate 与 clamp 的最终归约使用 grouped reduction + 标量合并。
  • packed SF 的 GM 拷贝通过 uint8 view 完成:当前固定的 tilelang 快照仍要求 MTE 源/目标
    元素宽度一致。上游 tilelang PR 11 Port mhc post kernels to VMI #14(codex/unify-asc-mte-pointer-lowering)合入并升级
    快照后可以删除该 workaround。
  • FP8/bf16 输出 cast 显式 NOSAT:复现 ASC 当前行为并保留 NaN。
  • exact 除法:forward 的 tie-break 用 lt,backward 用 le,与 ASC 源码一致;
    weight_grad 使用 vcadd(reassoc=True),按仓库 1e-4 标准验收。

验证

  • level-0 正确性(forward 24 + backward 24):48/48 通过,输出与 ASC 逐位一致
    (weight_grad 按 1e-4)。
  • 对抗性用例:review_adversarial_ab.py ALL_OK,覆盖 f32/bf16 次正规数、
    amax=inf 的 packed UE8M0、forward/backward 非有限输入。
  • hidden=384(非 256/128 对齐)的 64-lane 回退路径单独通过。

性能

测试条件:num_send_tokens=4001、alignment=256、with_psum_layout=True。
ASC 取 5 次运行 per-file 中位数;VMI 为同一公共 API 的实测值。

方向 配置数 geomean ASC/VMI min max ≥0.90
前向 24 1.061 0.901 1.122 24
反向 24 0.907 0.815 1.030 8
前向 24 配置明细(按 ratio 降序)
方向 tokens h hidden_block fmt experts/topk weights/rsf clamp ASC_us VMI_us ASC/VMI ASC GB/s VMI GB/s 结论
前向 4001 2048 — bf16 32/6 — 0.5 340.2 303.3 1.122 994 1113 ✅ ≥0.90
前向 4001 3584 — bf16 4/8 — 0.5 701.9 629.4 1.115 1105 1232 ✅ ≥0.90
前向 4001 2048 — bf16 4/8 — 0.5 427.6 386.8 1.106 1031 1138 ✅ ≥0.90
前向 4001 2048 — bf16 32/6 yes/2.5 — 260.3 236.8 1.099 1301 1430 ✅ ≥0.90
前向 4001 3584 — bf16 32/6 — 0.5 538.5 490.1 1.099 1104 1213 ✅ ≥0.90
前向 4001 2048 — fp32 4/8 yes/2.5 — 325.6 296.4 1.098 1817 1995 ✅ ≥0.90
前向 4001 3584 — fp32 4/8 yes/2.5 — 541.9 494.3 1.096 1915 2099 ✅ ≥0.90
前向 4001 3584 — bf16 32/6 yes/2.5 — 437.8 399.9 1.095 1362 1492 ✅ ≥0.90
前向 4001 2048 — bf16 4/8 yes/2.5 — 336.4 307.7 1.093 1311 1433 ✅ ≥0.90
前向 4001 3584 — bf16 4/8 yes/2.5 — 563.9 515.9 1.093 1370 1498 ✅ ≥0.90
前向 4001 3584 — e4m3 4/8 yes/2.5 — 539.1 493.8 1.092 1202 1313 ✅ ≥0.90
前向 4001 3584 — fp32 32/6 yes/2.5 — 412.3 378.2 1.090 1901 2072 ✅ ≥0.90
前向 4001 3584 — e4m3 32/6 yes/2.5 — 416.2 385.0 1.081 1192 1289 ✅ ≥0.90
前向 4001 2048 — fp32 32/6 yes/2.5 — 246.7 229.3 1.076 1837 1977 ✅ ≥0.90
前向 4001 3584 — fp32 4/8 — 0.5 664.1 620.9 1.070 1553 1660 ✅ ≥0.90
前向 4001 2048 — fp32 4/8 — 0.5 410.8 384.6 1.068 1446 1544 ✅ ≥0.90
前向 4001 2048 — fp32 32/6 — 0.5 316.1 298.2 1.060 1414 1498 ✅ ≥0.90
前向 4001 2048 — e4m3 4/8 yes/2.5 — 339.7 321.3 1.057 1099 1161 ✅ ≥0.90
前向 4001 3584 — fp32 32/6 — 0.5 511.2 483.9 1.056 1543 1630 ✅ ≥0.90
前向 4001 2048 — e4m3 32/6 yes/2.5 — 259.4 248.8 1.043 1092 1139 ✅ ≥0.90
前向 4001 3584 — e4m3 32/6 — 0.5 467.8 464.0 1.008 1070 1077 ✅ ≥0.90
前向 4001 3584 — e4m3 4/8 — 0.5 587.4 594.2 0.988 1110 1097 ✅ ≥0.90
前向 4001 2048 — e4m3 32/6 — 0.5 309.3 341.3 0.906 927 840 ✅ ≥0.90
前向 4001 2048 — e4m3 4/8 — 0.5 388.2 430.8 0.901 956 853 ✅ ≥0.90
反向 24 配置明细(按 ratio 降序)
方向 tokens h hidden_block fmt experts/topk weights/rsf clamp recompute ASC_us VMI_us ASC/VMI ASC GB/s VMI GB/s 结论
反向 4001 2048 2048 fp32 32/6 — 0.5 no 583.8 566.9 1.030 2299 2371 ✅ ≥0.90
反向 4001 3584 3584 fp32 32/6 — 0.5 no 900.7 891.8 1.010 2608 2637 ✅ ≥0.90
反向 4001 3584 3584 fp32 32/6 yes/2.5 — yes 1044.7 1034.7 1.010 2698 2725 ✅ ≥0.90
反向 4001 2048 2048 fp32 4/8 — 0.5 no 621.4 622.8 0.998 2401 2393 ✅ ≥0.90
反向 4001 2048 2048 fp32 32/6 yes/2.5 — yes 659.7 661.4 0.998 2442 2436 ✅ ≥0.90
反向 4001 2048 2048 fp32 4/8 yes/2.5 — yes 731.3 733.3 0.997 2480 2472 ✅ ≥0.90
反向 4001 3584 3584 fp32 4/8 — 0.5 no 974.2 996.5 0.978 2694 2636 ✅ ≥0.90
反向 4001 3584 3584 fp32 4/8 yes/2.5 — yes 1126.4 1160.3 0.971 2780 2696 ✅ ≥0.90
反向 4001 2048 2048 bf16 32/6 yes/2.5 — yes 571.1 642.2 0.889 1411 1254 ⚠️ <0.90
反向 4001 2048 2048 bf16 4/8 yes/2.5 — yes 632.0 711.7 0.888 1414 1256 ⚠️ <0.90
反向 4001 3584 3584 bf16 32/6 yes/2.5 — yes 973.4 1102.6 0.883 1448 1278 ⚠️ <0.90
反向 4001 3584 3584 bf16 4/8 yes/2.5 — yes 1101.9 1248.1 0.883 1449 1279 ⚠️ <0.90
反向 4001 2048 2048 e4m3 4/8 yes/2.5 — yes 897.9 1022.8 0.878 1028 903 ⚠️ <0.90
反向 4001 2048 2048 e4m3 32/6 yes/2.5 — yes 809.2 922.5 0.877 1013 889 ⚠️ <0.90
反向 4001 2048 2048 e4m3 32/6 — 0.5 no 649.2 742.9 0.874 1066 932 ⚠️ <0.90
反向 4001 2048 2048 e4m3 4/8 — 0.5 no 719.6 825.9 0.871 1074 936 ⚠️ <0.90
反向 4001 3584 3584 e4m3 4/8 yes/2.5 — yes 1523.1 1749.1 0.871 1046 911 ⚠️ <0.90
反向 4001 3584 3584 e4m3 32/6 yes/2.5 — yes 1377.3 1585.3 0.869 1050 912 ⚠️ <0.90
反向 4001 3584 3584 e4m3 4/8 — 0.5 no 1216.0 1409.7 0.863 1113 960 ⚠️ <0.90
反向 4001 3584 3584 e4m3 32/6 — 0.5 no 1096.8 1272.0 0.862 1104 952 ⚠️ <0.90
反向 4001 2048 2048 bf16 32/6 — 0.5 no 415.6 485.5 0.856 1615 1382 ⚠️ <0.90
反向 4001 2048 2048 bf16 4/8 — 0.5 no 450.8 543.8 0.829 1652 1370 ⚠️ <0.90
反向 4001 3584 3584 bf16 4/8 — 0.5 no 751.0 919.6 0.817 1747 1427 ⚠️ <0.90
反向 4001 3584 3584 bf16 32/6 — 0.5 no 672.4 825.3 0.815 1747 1423 ⚠️ <0.90
  • 反向低于 0.90 的配置集中在 bf16/e4m3,试验测得性能差距原因在exact div,去掉exact之后反向性能也全部达标。

kuri780 and others added 5 commits October 19, 2026 11:39
Port the ASC SwiGLU forward and backward kernels to the VMI (PTO)
backend following the PTOAS #1616 migration rules: keep the outer
T.Kernel/T.Persistent/T.copy/tiling/SF layout unchanged and rewrite
only the T.SimdVF bodies with logical T.vmi operations.

Co-Authored-By: Claude Code <noreply@anthropic.com>
- Forward: assert packed UE8M0 requires round_sf, matching the backward
  kernel and the public config builder.
- Make the vdiv precision dispatch explicit; raise on unknown modes
  instead of silently treating every non-ftz_true string as exact.
- Rename the 0-ULP corrections to vdiv_exact_0ulp /
  sigmoid_vdiv_exact_0ulp and document that they start from the hardware
  quotient and intentionally do not use the slower built-in
  V.vdiv(precision="exact") expansion.

Co-Authored-By: Claude Code <noreply@anthropic.com>
- forward exact vdiv uses the 'lt' tie-break of ASC's built-in
  vdiv_0ulp_ftz_true; backward keeps 'le' as in swiglu_backward_asc.py
- spell out NOSAT for packed UE8M0 exponent bytes and FP8 output casts,
  matching ASC's truncation/NaN semantics for amax=inf and non-finite values
- remove dead V.alloc_var locals, unused masks, reverse scratch traversal,
  the redundant packed/round_sf assert, and the no-op fast-math config
- shrink the quant scratch to the compact layout and use scalar vmins/vmaxs
- add vmi_swiglu_plugin.py and ASC-vs-VMI coverage/adversarial scripts

Co-Authored-By: Claude Code <noreply@anthropic.com>
- keep FP8 output casts on saturate='SAT' (faithful ASC translation);
  the minimal repro shows ASC RS_ENABLE currently behaves like NOSAT for
  f32->e4m3/bf16 while VMI SAT really saturates (toolchain bug, issue draft)
- add repro_fp8_saturate.py and extend adversarial coverage with backward
  e4m3 non-finite cases; document that the subnormal sweep does not cover
  the exact-midpoint tie branch
- 5-run median benchmarks: forward geomean 1.023 (22/24 >= 0.90),
  backward 0.904 (8/24 >= 0.90); a concurrent MASBench container on the
  same NPU is the dominant noise source

Co-Authored-By: Claude Code <noreply@anthropic.com>
- FP8 and bf16 output casts explicitly use saturate='NOSAT': ASC's
  RS_ENABLE is a no-op (bug A) and VMI SAT maps NaN to 0x00 (bug B);
  NOSAT matches ASC today and preserves NaN. Add explicit NOSAT to the
  fp->int clamp-count cast and separate the int-narrow comment from the
  fp-narrow decision.
- extend repro_fp8_saturate.py to f32->bf16 and add bf16 non-finite
  adversarial cases (forward output, backward x_grad/y_grad/act_out);
  all adversarial checks are now full bitwise equality, with no
  "assert a difference exists" logic.
- 5-run medians on a quiet device: forward 1.035 (20/24 >= 0.90),
  backward 0.904 (8/24), combined 0.967 (28/48); per-config CV median
  0.2%, max 1.6% forward / 4.4% backward.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Move the reproduction/coverage scripts out of the repository. They remain
available locally under deliverables/repro_scripts/ but are not part of the
mainline change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment on lines +152 to +159
exponent = V.vand(
V.vadd(V.vinterpret_cast(clamped_amax, 'uint32'), exponent_carry, full),
exponent_mask,
full,
)
scale = V.vinterpret_cast(V.vsub(exponent, scale_bias, full), 'float32')
inverse = V.vinterpret_cast(V.vsub(inverse_bias, exponent, full), 'float32')
return scale, inverse

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

能省略的mask尽量省掉

Comment on lines +524 to +527
if out_config.use_tma_aligned_col_major_sf:
output_sf = T.Tensor((num_stored_groups * 2, num_expanded_tokens * 2), T.uint8, x_grad_fp8_sf.data)
else:
output_sf = T.Tensor((num_expanded_tokens, num_stored_groups * 4), T.uint8, x_grad_fp8_sf.data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个PR应该能解决T.copy src/dst类型不一致的问题,可以试试:codex/unify-asc-mte-pointer-lowering

Comment on lines +235 to +238
# 256-element tile amax: two 32-channel groups per 64-lane vector.
for sub in T.unroll(4, explicit=True):
amax = V.vcmax(amax_acc[sub], full, group=2)
V.vstore(amax, amax_ub[tile_id * 8 + sub * 2])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里可以用256长度group=4的vreg表达

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已改。这里 vcmax 的 group 是输出组数,256-lane f32 / group=4 会得到 64 通道一组,与 sf_block=(1,32) 不符,所以 2048 用 256-lane/group=8;3584 用 128-lane/group=4(256-lane 时 num_vecs=14,tile 不满会触发 ptoas 的 layout 冲突)。clamp 计数保持 64-lane,避免宽计数器溢出寄存器。

- Drop redundant full masks on VMI ops that support unmasked forms;
  keep masks required by the API (vadds/vmuls/vmaxs/vmins/vshrs/vshls,
  vexpdif, vmula, vcmp/vsel, vcadd/vcmax, vgather and exact vdiv).
- Forward amax: use the widest vector whose 4-vector tile stays full.
  2048 -> 256-lane vcmax(group=8); 3584 -> 128-lane vcmax(group=4);
  clamp-count and non-quantized paths keep 64-lane vcmax(group=2).
  vcmax group is the output count, so 256-lane/group=4 would produce
  64-channel groups, which is wrong for sf_block=(1,32).
- ptoas 0.66 does not lower 256-lane full reductions (group=1); use
  grouped certificate/clamp reductions plus a scalar combine.
- Document that tilelang PR PTO-ISA#14 (codex/unify-asc-mte-pointer-lowering)
  does not apply to the pinned 5f105e99 snapshot; keep the uint8-view
  workaround for packed SF copies.

Validated: level-0 48/48, review_adversarial_ab.py ALL_OK,
hidden=384 64-lane fallback passes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants