onnxruntime
245f59b7 - [CUDA] Add HyperConnectionLowRankMix fused low-rank hyper-connection op (#33384)

Commit
17 days ago
[CUDA] Add HyperConnectionLowRankMix fused low-rank hyper-connection op (#33384) ## Description Adds `com.microsoft.HyperConnectionLowRankMix`, a CUDA contrib operator that fuses one hyper-connection block with a **low-rank gate network** (the Qwen3.8-Flash-Next block: 4 residual streams x 2560, rank 320, 96 blocks per model) into a single node: optional post-mix of the previous block, branchwise RMS norm, down projection, `ScaledSiLU`, up projection, sigmoid, pre-mix, and the post-mix inject gate. A chain of blocks needs one node per block; the unfused graph today needs 9-11 nodes (about 10 kernels) per block. ## Motivation and Context On a Qwen3.8-Flash-Next decode graph (batch 1, H200) the hyper-connection chain is ~16% of a decode cycle. In our research stack (a custom-op library, **not** part of this PR) fusing it removed 2.1 ms/token at S=1 (13.4 -> ~11 ms graph replay, bf16). This PR upstreams the part that is generic: standard row-major `[K, N]` weights used in place, no software grid barrier, no thread-block clusters, no process-wide state. The research one-kernel variant (pre-tiled weights + software grid barrier) is intentionally **not** proposed here. `HyperConnectionPreMix` / `HyperConnectionPostMix` (already in `main`) are unchanged and stay the building blocks of the unfused graph. A graph fusion that matches the unfused pattern and emits this op is a follow-up (out of scope here). ### Why one new op (and not extending PreMix/PostMix) The fused block needs the norm weight, two weight matrices with different shapes, four activation constants and two consumers of the normalized tensor. Folding that into `PreMix`/`PostMix` attributes would create a second op under their names. One op with an optional prologue (`prev_block_output`, `prev_post_gate`) gives one node per block in a chain and one place to state the rounding contract. ### Relation to #32123 #32123 (`HyperConnectionMix`, DeepSeek-V4 mHC) is a different op: a single linear gate on the flattened residual with Sinkhorn normalization and scalar-per-stream pre-mix. Qwen's block has a low-rank MLP gate (down -> SiLU -> up -> sigmoid), per-stream-per-feature pre-mix gates, no stream mixing and a per-branch weighted RMS norm, so no attribute setting of #32123 reproduces it. No code is shared; I followed its documentation and testing conventions (rounding points in the schema, torch reference test). ## Schema summary ``` HyperConnectionLowRankMix(streams, norm_scale, down_weight, up_weight, inject_weight [, prev_block_output, prev_post_gate]) -> (pre_output, post_gate [, streams_out]) ``` `T` = float / float16 / bfloat16 for every tensor. `streams` is flattened `(..., C*H)`; `down_weight` `(C*H, R)`, `up_weight` `(R, C*H)`, `inject_weight` `(C*H, C)` row-major `[K, N]`; `prev_block_output` `(..., H)`, `prev_post_gate` `(..., C)`; `pre_output` `(..., H)`, `post_gate` `(..., C)`, `streams_out` = streams after the prologue (a copy of `streams` without prologue). Attributes: `num_branches` (required), `epsilon` (1e-5), `reduction_scale` (1), `silu_alpha` (1), `gate_div` (1), `gate_mul` (1). Qwen3.8 uses `num_branches=4, epsilon=1e-6, reduction_scale=0.25, silu_alpha=0.25, gate_div=4, gate_mul=2`. ``` S = streams + prev_post_gate * prev_block_output (prologue; otherwise S = streams) hn = S / sqrt(mean_h(S^2) + epsilon) * norm_scale per branch d = ScaledSiLU(hn @ down_weight, silu_alpha) (..., R) g = sigmoid(d @ up_weight) per stream and feature pre_output = reduction_scale * sum_c(hn * g) post_gate = sigmoid((hn @ inject_weight) / gate_div) * gate_mul ``` ### Numerics contract Matrix products and reductions accumulate in fp32; the result of every step is rounded once to `T` where the equivalent standard operator would round (listed in the schema doc: `S, hn, down, Z, SG, hidden, up, g, pre_output, inject, post_gate`, as in the `ScaledSiLU` contract). The sigmoid is an accurate fp32 `expf`-based sigmoid rounded once to `T`: it does **not** reproduce the current CUDA `Sigmoid<bf16>` (round after every elementary operation; #33312 is the open fix), so on today's `main` the unfused chain and this op differ at the sigmoid sites, and after #33312 they agree up to accumulation order. Accumulation order is fixed: deterministic, and a row gives the same bits at any batch size. ## Summary of Changes | File | Change | |------|--------| | `onnxruntime/core/graph/contrib_ops/bert_defs.cc`, `ms_opset.h` | Schema + shape/type inference (next to the `HyperConnectionPreMix/PostMix` schemas) | | `onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix.{h,cc}` | Kernel class, input validation, path selection, scratch allocation via the allocator | | `onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix_impl.{h,cu}` | Fused two-kernel path and the general cuBLAS path | | `onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc` | Registration (float, MLFloat16, BFloat16) | | `docs/ContribOperators.md`, `docs/OperatorKernels.md` | New op entries | | `onnxruntime/test/python/transformers/test_hyper_connection_low_rank_mix.py`, `onnxruntime/test/contrib_ops/hyper_connection_low_rank_mix_op_test.cc` | Tests | ### CUDA implementation * **Fused path** (rows <= 8, fp16/bf16, `hidden % 8 == 0 && <= 4096`, `rank % 8 == 0 && <= 512`, `num_branches` in {1,2,4,8}, 16-byte aligned tensors, sm_80+): two kernels. `HcDownKernel`: optional post-mix + RMS norm + down GEMV + inject GEMV; K is split in slices inside one branch and each CTA writes fp32 partial sums. `HcUpPreKernel`: fixed-order partial reduction + ScaledSiLU + up GEMV + sigmoid + pre-mix + inject gate. Weights are streamed through shared memory (`cp.async`) before the kernel waits for its producer. One or two rows share a CTA (and its weight reads), more rows use more CTAs. No atomics, clusters, cooperative launch or spin waits. * **PDL**: on sm_90+ (CUDA >= 12.3) both kernels are launched with programmatic stream serialization (`cudaLaunchKernelEx`); everything that consumes another kernel's output is after `griddepcontrol.wait`, so correctness does not depend on trigger placement. Older GPUs use plain launches (and plain copies instead of `cp.async` below sm_80 are compiled in, though the fused path is only selected on sm_80+). * **General path** (any shape, any row count, fp32): norm kernel + 3 cuBLAS GEMMs (`CUBLAS_COMPUTE_32F`) + elementwise kernels with the same rounding points. * CUDA-graph capturable: no host synchronization, scratch only through the allocator (verified by capture/replay in the tests and the benchmark). ## Testing All run for real on one H200 (`CUDA_VISIBLE_DEVICES=2`, sm_90, CUDA 13.0, Release build of this branch rebased on `main` dc89460336): * `python -m pytest onnxruntime/test/python/transformers/test_hyper_connection_low_rank_mix.py`: **9 tests, 192 subtests passed.** Torch reference built from the ONNX primitives (BranchwiseRMSNorm / MatMul / ScaledSiLU / Sigmoid / PreMix / Div-Sigmoid-Mul / PostMix) in fp32 with the contract's rounding points: real shape (4 x 2560, rank 320) x {fp32, fp16, bf16} x rows {1, 2, 4, 16, 128} x {with, without prologue}; 8 small and irregular shapes (1/2/3/4/8 branches, hidden 32..4096 incl. not a multiple of 8, rank 8..512, multi-slice and multi-chunk cases); default and non-default attributes; two chained blocks; bitwise row independence; `streams_out` passthrough; zero rows; CUDA graph capture and replay (bitwise equal to eager). On the real bf16 shape the fused path is bit-identical to the reference (fp16: 99% of elements, rest 1 ulp; fp32 max abs error 4e-7). * `onnxruntime_provider_test --gtest_filter='HyperConnection*'`: 25/25 pass (10 new `HyperConnectionLowRankMixTest.*` cases: fused/unfused paths, optional output, K slices, weight chunks, fp32, error cases). * `compute-sanitizer` memcheck, racecheck, synccheck on the fused and unfused gtest cases: 0 errors / 0 hazards. * Cross-check against our research kernels (two-block real-shaped random case, bf16): **bit-identical** (all five outputs, rows 1 and 2) to the research two-kernel ops after swapping their bf16-Sigmoid emulation for the accurate sigmoid; with the original research kernels `pre` differs by exactly 1 bf16 ulp at the sigmoid sites (the contract change above). * `lintrunner -a` on the changed files: no issues. ## Benchmark 96-block chain, bf16, CUDA graph replay, 8 distinct weight sets (208 MB, larger than L2); us/block = slope between the 96- and 48-block graphs (min of 15 x 100 replays); H200. The unfused chain is the stock graph per block: PostMix, BranchwiseRMSNorm, MatMul, ScaledSiLU, MatMul, Sigmoid, PreMix, MatMul, Div, Sigmoid, Mul (11 nodes). | rows (S) | fused op | stock unfused chain | research two-kernel ops (clusters), same harness | | ---: | ---: | ---: | ---: | | 1 | **10.2 us** | 33.2 us | 10.5 us | | 2 | **12.9 us** | 36.8 us | 17.5 us | | 4 | 21.1 us | 36.3 us | | | 8 (fused limit) | 33.2 us | 37.6 us | | | 9 / 16 (general path) | 33.9 / 32.8 us | 38.7 / 37.9 us | | fp16: 10.2 / 12.7 us (S=1 / 2) vs 32.2 / 36.0 us unfused. For 96 blocks at S=1 that is 3.2 ms -> 1.0 ms of replay. For reference, our research one-kernel variant (pre-tiled weights, software grid barrier, not in this PR) measured 8.2 us/block (S=1) and 10.1 us (S=2) in the research stack with a different harness, so this two-kernel upstreamable variant is about 2 us (25%) per block slower at S=1 and ~2.8 us at S=2. In a real model the producer kernel does not trigger early, so expect +0.5..1 us per block over the chain benchmark. ## Limitations * CUDA only (no CPU / WebGPU kernel). fp32 uses the general path. Fused kernels need sm_80+; PDL needs sm_90+ (and CUDA >= 12.3 at build time). * Fused path limits: rows <= 8, hidden <= 4096, rank <= 512, branches in {1, 2, 4, 8}, 16-byte aligned tensors; everything else (including prefill) takes the general path, which is correct but about as fast as the unfused chain. * Exact agreement with the stock bf16 `Sigmoid` needs #33312. * No graph fusion transformer yet (follow-up). ## Checklist - [x] Tests added/updated - [x] No breaking changes (new op, no existing behavior touched) - [x] Documentation updated (schema doc, `docs/ContribOperators.md`, `docs/OperatorKernels.md`) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Author
Parents
Loading