Repository navigation
[CUDA] Add HyperConnectionLowRankMix fused low-rank hyper-connection op - #33384
Tianlei Wu (tianleiwu) wants to merge 6 commits into
Conversation
Add com.microsoft.HyperConnectionLowRankMix, one operator per hyper-connection block with a low-rank gate network: optional post-mix of the previous block, branchwise RMS norm, down projection, ScaledSiLU, up projection, sigmoid, pre-mix and the post-mix inject gate. CUDA implementation: a two-kernel fused path for 1..8 rows (fp16/bf16, weights used in place as [K, N], fp32 accumulation in a fixed order, programmatic dependent launch on sm_90+, no atomics/clusters/grid barriers) and a general cuBLAS based path for other shapes, row counts and fp32. The schema documents the rounding points. Tests: python (torch reference, real and small shapes, all dtypes, CUDA graph replay) and C++ op tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Early weight reads under programmatic dependent launch can race with upstream producers and produce incorrect results.
1 open finding
What changed in this PR
Adds a CUDA-only HyperConnectionLowRankMix contrib operator to fuse low-rank hyper-connection blocks. Existing PreMix/PostMix operators remain unchanged; graph fusion is deferred.
Changes:
- Adds the schema, type/shape inference, and CUDA registration.
- Implements a two-kernel fused path and a general cuBLAS fallback.
- Adds numerical, validation, and CUDA graph tests plus operator documentation.
| File | Description |
|---|---|
onnxruntime/test/python/transformers/test_hyper_connection_low_rank_mix.py |
Reference comparisons and CUDA graph tests. |
onnxruntime/test/contrib_ops/hyper_connection_low_rank_mix_op_test.cc |
Kernel correctness and validation tests. |
onnxruntime/core/graph/contrib_ops/ms_opset.h |
Registers the schema. |
onnxruntime/core/graph/contrib_ops/bert_defs.cc |
Defines schema, numerics, and shape inference. |
onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc |
Registers supported CUDA types. |
onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix.h |
Declares the kernel class. |
onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix.cc |
Validates inputs and selects execution paths. |
onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix_impl.h |
Defines launch interfaces and configuration. |
onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix_impl.cu |
Implements fused kernels and fallback. |
docs/OperatorKernels.md |
Lists CUDA support. |
docs/ContribOperators.md |
Documents the operator contract. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…perConnectionLowRankMix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…h wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p unused streams scratch in HyperConnectionLowRankMix Allocate streams_scratch only on the fused path when streams_out is omitted with a prologue, and guard streams_out writes in the unfused NormKernel so the general path avoids unnecessary scratch allocations and full-buffer global writes. Set CUBLAS_MATH_DISALLOW_REDUCED_PRECISION_REDUCTION via an RAII scope guard around the unfused cuBLAS GEMMs (restoring the previous handle math mode on exit/failure) to uphold the FP32-accumulation single-rounding contract when split-K kernels are selected, and add regression tests for both paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onnection-lowrank-mix
| .TypeConstraint("T", | ||
| {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, | ||
| "Constrain all floating-point inputs and outputs to the same type.") | ||
| .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { |
There was a problem hiding this comment.
Please also register and implement HyperConnectionLowRankMix in the Python symbolic-shape inference tool, alongside the existing hyper-connection handlers. This C++ schema callback does not provide inference support to that tool.
I reproduced a failure using the PR-head implementation with a valid node: streams=["batch",16], num_branches=2, norm_scale=[16], down_weight=[16,8], up_weight=[8,16], and inject_weight=[16,2]. When the output shapes are unspecified, SymbolicShapeInference.infer_shapes(model, auto_merge=True) raises Incomplete symbolic shape inference. An existing HyperConnectionPreMix control successfully infers ["batch",8].
Please add the dispatcher entry and handler to infer pre_output=[...,H], post_gate=[...,C], and optional streams_out=[...,C*H], preserving the input element type. Add regression coverage with symbolic leading dimensions and both two-output and three-output forms. Otherwise valid graphs containing this new operator fail the repository’s symbolic shape-inference preprocessing.
- Request programmatic dependent launch only when the driver is >= 12.3 and the kernel image was built for sm_90+ (griddepcontrol is compiled out otherwise), and fall back to a plain launch if the driver rejects the launch attribute. - Return a clear error for BFloat16 on devices below sm_80 instead of reaching cuBLAS; skip BFloat16 C++ and Python tests on such devices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| auto cuda = DefaultCudaExecutionProvider(); | ||
| if (!cuda) { | ||
| GTEST_SKIP() << "HyperConnectionLowRankMix is only implemented for CUDA"; | ||
| } |


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 inmain) 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/PostMixattributes 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
T= float / float16 / bfloat16 for every tensor.streamsis 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 ofstreamswithout prologue).Attributes:
num_branches(required),epsilon(1e-5),reduction_scale(1),silu_alpha(1),gate_div(1),gate_mul(1). Qwen3.8 usesnum_branches=4, epsilon=1e-6, reduction_scale=0.25, silu_alpha=0.25, gate_div=4, gate_mul=2.Numerics contract
Matrix products and reductions accumulate in fp32; the result of every step is rounded once to
Twhere 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 theScaledSiLUcontract). The sigmoid is an accurate fp32expf-based sigmoid rounded once toT: it does not reproduce the current CUDASigmoid<bf16>(round after every elementary operation; #33312 is the open fix), so on today'smainthe 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
onnxruntime/core/graph/contrib_ops/bert_defs.cc,ms_opset.hHyperConnectionPreMix/PostMixschemas)onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix.{h,cc}onnxruntime/contrib_ops/cuda/bert/hyper_connection_low_rank_mix_impl.{h,cu}onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.ccdocs/ContribOperators.md,docs/OperatorKernels.mdonnxruntime/test/python/transformers/test_hyper_connection_low_rank_mix.py,onnxruntime/test/contrib_ops/hyper_connection_low_rank_mix_op_test.ccCUDA implementation
hidden % 8 == 0 && <= 4096,rank % 8 == 0 && <= 512,num_branchesin {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.cudaLaunchKernelEx); everything that consumes another kernel's output is aftergriddepcontrol.wait, so correctness does not depend on trigger placement. Older GPUs use plain launches (and plain copies instead ofcp.asyncbelow sm_80 are compiled in, though the fused path is only selected on sm_80+).CUBLAS_COMPUTE_32F) + elementwise kernels with the same rounding points.Testing
All run for real on one H200 (
CUDA_VISIBLE_DEVICES=2, sm_90, CUDA 13.0, Release build of this branch rebased onmaindc89460):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_outpassthrough; 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 newHyperConnectionLowRankMixTest.*cases: fused/unfused paths, optional output, K slices, weight chunks, fp32, error cases).compute-sanitizermemcheck, racecheck, synccheck on the fused and unfused gtest cases: 0 errors / 0 hazards.prediffers by exactly 1 bf16 ulp at the sigmoid sites (the contract change above).lintrunner -aon 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).
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
Sigmoidneeds [CUDA] fix Nemotron-H relu2, RMSNorm, and activation precision #33312.Checklist
docs/ContribOperators.md,docs/OperatorKernels.md)