Sitelet https://github.com/openvpi/DiffSinger/pull/341
Skip to content

perf: channels-last Conv2d dwconv for LynxNet2/ConvNeXt, AdamW routing for dwconv - #341

Open
KakaruHayate wants to merge 8 commits into
openvpi:mainfrom
KakaruHayate:perf/nhwc-dwconv-conv2d
Open

KakaruHayate wants to merge 8 commits into
openvpi:mainfrom
KakaruHayate:perf/nhwc-dwconv-conv2d

Conversation

@KakaruHayate

@KakaruHayate KakaruHayate commented Oct 3, 2026 •

Copy link
Copy Markdown

动机

LynxNet2 / ConvNeXt 的 depthwise conv(dwconv)在训练时存在实测开销(RTX 2070 / torch 2.12.1+cu130 / cuDNN 9.20):dwconv 的输入常是 transpose 视图(非连续),Conv1d 的 cuDNN 路径会做整张量隐式 contiguous 拷贝;profiler 显示单个 block forward 仅 aten::copy_ 即约 676µs/iter,与 dwconv 本身的计算量相当。

改动(仅训练加速,导出图不变)

  • 新增 NHWCConv1d(AdamWConv1d):与普通 Conv1d 完全同接口([B, C, T] 进 / [B, C, T] 出),eval、推理与图导出恒走原 Conv1d 计算路径——导出图与改动前逐位一致(已用官方导出管线验证:算子序列与全部 initializer 完全相同)。
  • 仅在训练且 T <= 4096 帧时启用 channels-last 快路径:输入是 [B, T, C] 序数据的 transpose 视图时(LayerNorm/Linear 天然产出该布局),以零拷贝视图上的 channels-last Conv2d 计算,避免 Conv1d 对非连续输入的隐式拷贝以及后续 Linear 的拷贝;连续输入内部重排一次。T > 4096 回退原路径(cuDNN depthwise backward 在长序列上病态退化)。
  • 集成 = 在 net 里替换一个模块:LynxNet2Block 的 net 与 ConvNeXtBlock 的 dwconv 仅换类,两个 forward 与 main 逐字相同。
  • dwconv 路由到 AdamW:NHWCConv1d 继承 AdamWConv1d,自动被 Muon 排除(muon.py 零改动),覆盖 LynxNet / LynxNet2 / ConvNeXt 三处 dwconv。AdamWConv1d 增加 init_method 参数(默认仍 kaiming_normal_),dwconv 传入原 Conv1d 默认初始化,新训练的初始化行为不变。
  • LynxNet v1 的 dwconv 输入本身连续,仅做 AdamW 路由(AdamWConv1d),不改前向。

实测(fp16 16-mixed, fwd+bwd, 同会话新旧路径 A/B, 完整模型)

配置 形状 提速
acoustic LYNXNet2 1024×6 k31 B=16 T=781 1.07×
acoustic LYNXNet2 1024×6 k31 B=8 T=1563 1.15×
acoustic LYNXNet2 1024×6 k31 B=4 T=3125 1.13×
variance-pitch LYNXNet2 512×6 k31 B=16 T=781 1.11×
variance-pitch LYNXNet2 512×6 k31 B=8 T=1563 1.20×
variance-multi LYNXNet2 384×6 k31 B=8 T=1563 1.04×
convnext aux 512×6 k7 B=4 T=12500 1.10×
训练回退区(T > 4096) — 1.00×(数值不变)

兼容性

  • 旧 checkpoint:state_dict 键与形状零变化,双向完全兼容(权重仍为 3D (C, 1, K),无需任何加载/保存钩子)
  • Triton fused kernel(softsign GLU):wrap_lynxnet2_block 的 Sequential 结构校验全部通过,fused 训练路径与 eval 回落路径数值一致
  • ONNX 导出图:与改动前完全一致(acoustic 默认配置 + variance 全参数,官方 scripts/export.py 管线导出并 ORT CPU/DML 端到端验证,输出逐位一致)

…g for dwconv

- LynxNet2 blocks and the ConvNeXt decoder keep the [B, T, C] layout end to
  end: the depthwise conv runs as channels-last Conv2d over zero-copy views
  of the Conv1d weights, avoiding the transposes plus the hidden contiguity
  copies Conv1d makes on transposed input (full-model fp16 fwd+bwd measured
  10-23% faster for T <= 4096 frames)
- eager falls back to the original Conv1d path above 4096 frames (cuDNN
  depthwise backward regresses on long sequences); graph export always emits
  the Conv2d form (ORT CPU EP runs 1D depthwise conv 2-17x slower than the
  2D form, DirectML is indifferent)
- new AdamWDWConv1d marker subclass routes depthwise conv weights to AdamW
  instead of Muon (same mechanism as AdamWLinear/AdamWConv1d), applied to
  LynxNet, LynxNet2 and ConvNeXt
- state_dict compatible with existing checkpoints; Triton fused-kernel patch
  for LynxNet2 (softsign GLU) stays compatible; exported graphs verified
  equivalent to the previous Conv1d graphs on ORT CPU/DML
KakaruHayate and others added 3 commits October 3, 2026 15:28
Per maintainer feedback: swapping a single module inside net is the whole
integration, forwards stay verbatim.

- NHWCConv1d(AdamWConv1d) takes [B, C, T] like a regular Conv1d: when the
  input is a transpose view of [B, T, C]-ordered data it computes channels-
  last Conv2d over zero-copy views and returns a [B, C, T] transpose view
  (contiguous input is reordered once); long training sequences fall back
  to the plain Conv1d computation; graph export always uses channels-last
- LynxNet2 and ConvNeXt forwards are verbatim main; only the dwconv module
  class is swapped, and state_dict is index-based so old ckpts load as-is
- AdamWConv1d gains an init_method override (default stays kaiming_normal_);
  dwconv passes the plain-Conv1d default so fresh-run init is unchanged
- muon.py untouched: NHWCConv1d inherits the AdamW routing from AdamWConv1d
- convnext inconv/outconv keep their classes: ORT CPU runs depthwise conv1d
  2-17x slower than conv2d, but plain groups=1 conv1d is fine (measured
  faster than the 2D form), so only dwconv needs the swap
@KakaruHayate

Copy link
Copy Markdown
Author

Verification summary (on commit b177ca7)

All checks pass; scripts in the PR author's local bench dir (not part of this PR).

Compat / correctness

  • Old checkpoints: strict=True state_dict load passes (keys unchanged — module swap only, Sequential indices untouched)
  • Triton fused kernel (softsign GLU): wrap_lynxnet2_block layout validation passes; fused train/eval forwards match unpatched
  • Eager outputs vs pre-change main: identical within fp32 kernel noise (≤ 1.2e-06 on randomized output weights, incl. dual-timestep mask path)

Performance (RTX 2070, fp16, fwd+bwd, same-session A/B)

  • LYNXNet2 ×6: B=16/T=781 1.19×, B=8/T=1563 1.22×
  • ConvNeXt ×6: B=16/T=3125 1.09×, B=4/T=12500 (training fallback) 1.00× — no regression

ONNX (legacy exporter dynamo=False + onnxsim, same as the deployment pipeline)

  • Exported graphs: dwconv → 2D Conv (4D weights); conditioner/inconv/outconv keep their original 1D form (groups=1 conv1d is fine on ORT CPU — measured faster than the 2D form)
  • New graphs vs old Conv1d graphs: outputs identical (ORT CPU & DML, T=781/3125/10000)
  • Full LYNXNet2 export, ORT CPU EP, B=1/T=781: 259.4 ms → 184.5 ms (1.4×)

The trace -> jit.script -> ONNX export flow degraded reshape(int-list) and
squeeze into dynamic Shape/Gather/Concat chains and guarded onnx::If nodes
that onnxsim cannot fold inside the sampling Loop subgraph. Replace them
with single static ops: unsqueeze for the 4D input/weight views and
select(2, 0) to drop the always-1 height dim. A dwconv segment is now just
Unsqueeze/Unsqueeze/Conv/Gather in the exported graph.

Verified with the full official acoustic export (conditioner extraction,
onnxsim x3, graph merge): ORT CPU loads and runs the merged model, outputs
identical to the pre-change Conv1d graphs (CPU & DML, T=781/3125/10000).
The exported graphs carried an extra Unsqueeze per dwconv because the
parameter was stored 3D and viewed at runtime. Hold the weight as
(C, 1, 1, K) directly (Conv2d consumes the initializer without any shape
op) and reshape legacy (C, 1, K) weights in _load_from_state_dict, so old
checkpoints keep loading; checkpoints saved by this class store the 4D
shape.

Note: an earlier attempt also overrode _save_to_state_dict to keep saving
the 3D shape, but any _save_to_state_dict override on the module makes the
legacy ONNX exporter fail with 'Cannot insert a Tensor that requires grad
as a constant', so the override was dropped.
@KakaruHayate

Copy link
Copy Markdown
Author

Note on ONNX CPU EP performance (correction)

Earlier benchmark quoted 1.4× on ORT CPU EP — that was a backbone-only export (main-graph Conv). Profiling the full official acoustic export shows that inside the sampling Loop subgraph, ORT executes 1D and 2D depthwise Conv identically (~8 ms/layer over 20 steps), so the full model is at parity on CPU EP (0.99–1.01×, outputs bit-identical). DML is also at parity. The PR description has been updated accordingly. Training speedups and the cleaner exported graphs (4D weight initializers, no shape nodes) remain the motivation.

Per review feedback the ONNX export must stay on the original 1D Conv
graphs, so the channels-last path now runs only while training with
T <= 4096 frames; evaluation, inference and graph export always use the
plain Conv1d computation. Verified the exported graphs are identical to
the pre-change graphs (op sequence and all initializers) with the official
acoustic and variance export pipelines, and that ORT CPU/DML outputs are
bit-identical.

The weight is back to the plain 3D (C, 1, K) shape: state_dict keys and
shapes are unchanged, checkpoints stay compatible in both directions, and
no state_dict hooks are involved.

Training speedups are unchanged (1.07-1.20x on the production configs).
@KakaruHayate

Copy link
Copy Markdown
Author

Update: scoped to training acceleration only

Per review feedback, the export path is reverted to the original 1D Conv graphs — this PR now only accelerates training:

  • NHWCConv1d takes the channels-last fast path only while training with T <= 4096; eval, inference and graph export always run the plain Conv1d computation
  • Verified with the official export pipeline (acoustic default config + variance with all parameters): exported graphs are identical to the pre-change graphs (same op sequence, same initializers), ORT CPU/DML outputs bit-identical, end-to-end inference OK
  • The weight stays 3D (C, 1, K): state_dict keys and shapes unchanged, checkpoints are compatible in both directions, no hooks involved
  • Training speedups unchanged (1.07–1.20× on the real configs, see the table above)

The fast path expanded the weight with unsqueeze(1), which only produces
the required (C_out, C_in / groups, 1, K) shape when C_in / groups == 1
(depthwise). For grouped or dense convs it put the input-channel dim into
the height position and failed with a shape error. Use unsqueeze(2) so the
kernel always spans the width (T) dim, and cover the grouped/dense cases in
the tests.

Also reject string padding and padding_mode != 'zeros' in the constructor:
the fast path maps the Conv1d kernel onto the Conv2d width dim and cannot
honour either of them.
@autumn-2-net

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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