Skip to content

perf(wasm): 新增 PackedSimd (WASM SIMD128) 内核,接入 CPU 派发链 - #31

Merged
sdcb merged 3 commits into
mainfrom
feat/wasm-packedsimd
Oct 12, 2026
Merged

sdcb merged 3 commits into
mainfrom
feat/wasm-packedsimd

Conversation

@sdcb

@sdcb sdcb commented Oct 11, 2026

Copy link
Copy Markdown
Owner

概要

CPU 内核派发链补上 WebAssembly SIMD128(System.Runtime.Intrinsics.Wasm.PackedSimd):

AVX-512 → AVX2 → AdvSIMD → PackedSimd → Vector<float> → scalar

此前 browser-wasm 只走 Vector<float> 回退路径。

新增 PackedSimd 内核

内核 做法 单核微基准 (vs Vector<float>)
NHWC pointwise / ConvTranspose 2x2 4 像素 × 8 输出通道寄存器分块 1.4–1.8×
NHWC dense (3x3 等) 同上,加上预计算的 per-k 输入偏移表 1.5–1.9×
NHWC depthwise 2 像素 × 16 通道 strip 1.05–1.15×
MatMul(packed) 4 行 × 8 列 1.5–1.9×
MatMul + ArgMax(CTC 头) 累加器留在寄存器里,不再用 stackalloc 2.3×
Erf / GELU(单独算子和融合 epilogue) SIMD128 多项式 ~1.0×

主要原因:V8 在 x64 上最多给 v128 分配 15 个 XMM。6 行分块(Count == 4 时 12–16 个累加器)会在内层循环溢出,实测 5 行以上都会溢出,所以统一用 4×8。

数值:全部内核与 Vector<float> 逐 lane 保持相同的运算顺序(WASM 没有 FMA,乘和加分开做),所以结果逐位一致。微基准逐元素比对都是 bit-exact;100 张图在 tiny / small / medium 上的文本 Hash 与旧版本完全一致。

netstandard2.0 通过 <Compile Remove="**/*PackedSimd.cs" /> 排除这些文件,派发分支都在 #if !NETSTANDARD2_0 内。

端到端 A/B(Ryzen 7 5800X,Edge 155,4 workers,100 张图)

模型 Vector<float> Mean PackedSimd Mean 提速
tiny 128.1 ms 112.9 ms 1.13×
small 474.6 ms 411.3 ms 1.15×
medium 1,654.6 ms 1,360.4 ms 1.22×

tiny 和 small 是两轮的平均,medium 只跑了一轮。

其他改动

  • docs/wasm.md:换成本机新数据,补上 ISA 派发说明和 A/B 对比。
  • README:同步 WASM 中位数(113 ms,约为同机桌面原生 tiny 4w 中位 45.9 ms 的 2.5 倍)。
  • WasmBench 的 meta 输出 packedSimdSupported,effectiveIsa 改为 packedsimd。

验证

  • dotnet build:net10.0 和 netstandard2.0 都通过。
  • Sdcb.SimdPaddleOCR.UnitTests:119 个测试全部通过(桌面 x64,不会走 PackedSimd 分支)。
  • WASM AOT:tiny / small / medium 各 100 张图,文本 Hash 与改动前一致,准确率不变。

Dispatch is now AVX-512 -> AVX2 -> AdvSIMD -> PackedSimd -> Vector<float> -> scalar.

- NHWC pointwise / dense / transposed conv: 4 px x 8 oc register tile
  (V8 gives v128 at most 15 XMM regs on x64; the 6-row tile spilled).
  Dense conv uses a precomputed per-k input offset table.
- NHWC depthwise: 2 px x 16 ch strip kernels.
- MatMul (packed) and fused MatMul+ArgMax (CTC head) with register
  accumulators instead of the Vector path's 16-acc / stackalloc tiles.
- Erf / GELU (standalone and fused epilogue).

All kernels keep the Vector<float> per-lane operation order (no FMA,
separate mul + add), so results are bit-identical: 100-image text hashes
match the previous build for tiny / small / medium.

Ryzen 7 5800X, Edge 155, 4 workers, mean ms/img (Vector -> PackedSimd):
tiny 128.1 -> 112.9 (1.13x), small 474.6 -> 411.3 (1.15x),
medium 1654.6 -> 1360.4 (1.22x). docs/wasm.md refreshed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sdcb

sdcb commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner Author

这个 PR 可以合并,我没发现会影响正确性的问题。下面列的几项都是测试覆盖或文档上的小问题,不需要因此挡住合并。

在 PR 的 worktree(SimdPaddleOCR-packedsimd)里跑过的结果:

  • 构建:Sdcb.SimdPaddleOCR 的 Release 构建通过(net10.0 和 netstandard2.0)。
  • 单元测试:Sdcb.SimdPaddleOCR.UnitTests 119 个全部通过。不过桌面 x64 不会走 PackedSimd 分支,所以这只能说明没破坏原有路径,新内核本身没有被测到。

逐项核对过、没有问题的地方

  • 派发顺序:MatMul、ArgMax、NHWC pointwise / dense / depthwise / ConvTranspose、Erf、GELU 都按 AdvSIMD → PackedSimd → Vector 插入。所有分支都在 #if !NETSTANDARD2_0 内,Sdcb.SimdPaddleOCR.csproj 也排除了 *PackedSimd.cs。
  • 数值顺序与 Vector 版一致:
    • 累加都从 0 或 bias 开始,按 k 从小到大,乘和加分开做。
    • ConvTranspose 的 bias 放到最后再加,和 StoreEpilogueVec 一样。
    • 单输出 ConvTranspose 按 tap 重排权重后,StoreSelectedScalar 写出的位置正确(lane 0/1 写 out0,lane 2/3 写 out1)。
    • Erf 的区间判断(AndNot、Not(Or))遇到 NaN 时和 Vector 版走到相同结果。
  • Depthwise:strip 的布局 [px][block] 和 InitStrip、FinishStrip 对得上;xLo / xHi / wRow0 的计算和 Vector 版完全一样。
  • Dense:kOffsets 按 k = ic × taps + tap 展开,和权重的 [ic][tap][16] 布局一致。边界 tile 用的 patch 宽度按 4 行 tile 算,也正确。
  • Sigmoid:ActivatePackedSimd 没处理 Sigmoid,但 Vector 版也没有,Sigmoid 在 InferenceSession.Nhwc.cs 里单独执行,行为一致。

建议(按重要性排序)

  1. CI 没有覆盖新内核(最值得补)

    • 新增的约 1100 行内核只能在浏览器里跑;桌面上 PackedSimd.IsSupported 为 false,CI 测不到。
    • 正确性目前只靠这次手动 A/B 时比对的文本 Hash。
    • 建议加一个开关,让 WasmBench 在同一个构建里强制走 Vector 路径,做逐元素对比(例如在内核调度处读一个环境变量)。这样以后每次改动都能在浏览器里 A/B,不用依赖旧构建。
  2. “逐位一致”只对有限值成立(低)

    • ReLU、HardSwish、单输出 ConvTranspose 用的是 PseudoMax / PseudoMin。遇到 NaN 时它们返回 0,而 Vector 版的 Vector.Max 和 MathF.Max 会把 NaN 传下去。
    • Nhwc.Conv.PackedSimd.cs 的注释已经写明“For finite inputs”。PR 描述和 docs/wasm.md 里的“结果逐位一致”可以加上“有限输入下”这个前提。
  3. ArgMax 默认 Vector<float>.Count == 4(低)

    • MatMulArgMaxRows4PackedSimd 用 state[r].AsVector() 把结果交给 ReduceVector,这只在 Count 为 4 时成立。WASM 上 Count 一定是 4,但最好在派发条件里加上 && Vector<float>.Count == 4,或者加一个 Debug.Assert。
  4. 文档有两处不准确(nit)

    • docs/wasm.md 说原分块是“Vector<float>/4 时为 16 个累加器”。实际只有 MatMul 是 16 个;NHWC 的 Vector 实现在 Count 为 4 时是 6 行 × 8 通道分两次,共 12 个累加器。PR 描述写的“12–16”是对的。
    • 同一份文档说 ArgMax“全部驻留寄存器”。实际上乘加累加器在寄存器里,但每行的最大值和下标 state 仍然是 stackalloc 的(每个 8 列半块更新一次,影响很小),说法可以改得更准确。
    • 另外 --- 和 ## 五 之间的空行被删掉了,渲染没问题,只是和其他章节格式不统一。
  5. Pointwise 分组参数没有按新 tile 重新调(nit)

    • PointwiseGroupTiles = 8 是按 6 行 tile(每组 48 像素)调的,PackedSimd 用 4 行 tile 后每组只有 32 像素。可以在浏览器里扫一下 12 左右的取值,也可以保持不变。
  6. effectiveIsa 改了取值(信息)

    • WasmBench 输出的 effectiveIsa 从 wasm-simd128 改成了 packedsimd / vector128。我查了,仓库里只有 TestReport 和 WasmBenchSummary 用它做显示,没有代码按这个字符串判断,不受影响。和历史结果对比时标签会不一样。

附带的 ImageSharp 3.2 升级评估文档不属于这个 PR 的改动,我没有评审它。

sdcb and others added 2 commits October 12, 2026 07:39
- PPOCR_PACKEDSIMD=0 forces the Vector<float> path in the same build;
  WasmBench forwards runtime env vars (--env KEY=VALUE / run.ps1 -Env)
  so both paths can be A/B'd in the browser. Verified: tiny 100 images,
  text hashes identical between the two paths.
- ArgMax PackedSimd dispatch also requires Vector<float>.Count == 4.
- docs/wasm.md: bit-exactness holds for finite inputs; correct the
  original accumulator counts (NHWC 12, MatMul 16); ArgMax max/index
  state still lives on the stack; restore blank line before section 5.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keeps the other review fixes (ArgMax Count == 4 guard, docs wording) and
the generic WasmBench --env / -Env runtime environment passthrough.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sdcb
sdcb merged commit 923267b into main Oct 12, 2026
66 checks passed
@sdcb
sdcb deleted the feat/wasm-packedsimd branch October 12, 2026 01:37
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.

1 participant