Repository navigation
perf(transforms): fold BatchNorm into Conv weights - #1334
Draft
ayanshul-amd wants to merge 2 commits into
Draft
ayanshul-amd wants to merge 2 commits into
ayanshul-amd wants to merge 2 commits into
Conversation
Add hip-fold-batchnorm, which folds an inference hip.batch_norm with constant parameters into the filter and bias of the hip.conv feeding it, either directly or through a hip.add of two convs. BN is the per-channel map y = s*x + t with s = scale/sqrt(var+eps) and t = B - mean*s: each filter is scaled by s per output channel, any existing bias by s, and t is added to exactly one conv's bias so it is not counted twice through the add. With the batch norm gone, a following Relu fuses into the conv kernel. The pass runs right after convert-onnx-to-hip in the shared pipeline head, so it applies to both the default and the rocMLIR flows before any conv rewrite or kernel outlining. It reads inline and file-backed constants only; a batch norm over memory-address-backed weights is left in place. The private constant readers in DecomposeConvTranspose and NhwcConvConversion are replaced by a shared readConstantData helper (HipConstantData.h), which the new pass also uses. Their behavior is unchanged. On iri-v1 (f32 800x800, rocMLIR, gfx1151, inline threshold 1 GiB) all 72 BN(Add(Conv, Conv)) sites fold, standalone Relus drop from 76 to 4, and average latency goes from 215.8 ms to 211.3 ms. Output matches the CPU EP (max abs error 1.8e-6). Tests: new LIT test test_fold_batchnorm.mlir (5 folding cases, 3 that must be left alone); new numeric test test_conv_batchnorm.py (f32/f16, bias, grouped, Add(Conv, Conv)+BN+Relu), passing on the default and rocMLIR flows with HIPDNN_EP_STRICT=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks for opening a PR! This project follows LLVM's incremental-development and AI-tool-use Before requesting review, please check that:
Reviewers are assigned through |
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 7922 - Commit: |
Binary sizesBaseline: run 40294400486 -
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
VLM Benchmark Results
Run: 7922 - Commit: |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
hip-fold-batchnorm, which folds an inferencehip.batch_normwith constant parameters into the filter and bias of thehip.convthat feeds it. The conv can feed the batch norm directly or through ahip.addof two convs. With the batch norm gone, a following Relu fuses into the conv kernel.On iri-v1, all 72
Relu(BN(Add(Conv, Conv)))sites fold, and latency improves by about 2% (215.8 ms to 211.3 ms).Related issue or design
None.
Why
Inference BatchNorm is the per-channel affine map
y = s*x + t, withs = scale / sqrt(var + eps)andt = B - mean*s. When it follows a conv, it can be absorbed into the conv weights at compile time.Without the fold, each site runs a standalone
hip_batch_normkernel followed by a standalone Relu. That is two extra full read/write passes over the activation. It also blocks fusion:fuse-rocmlironly grows along pointwise successors, andhip.batch_normis not pointwise.What
sper output channel, and scales any existing bias bys.tto exactly one conv's bias, so it isn't counted twice through the Add.hip.constants and leaves the originals for any other users.hip.constants only. A batch norm over memory-address-backed weights is left in place, the same rulehip-decompose-conv-transposeand the NhwcConv conversion already follow. With the default 127-byte inline threshold, real weights arrive memory-backed, so the fold needsXLNX_ABI_2_0_CLONE_EXTERNAL_DATA_THRESHOLD(or the per-sessionXLNX_model_clone_external_data_thresholdoption) raised.convert-onnx-to-hipinbuildOnnxToHipPipelineHead, before theAfterConvertOnnxToHipplugin slot. The head is shared by the default and rocMLIR flows, so in both the fold runs before any conv rewrite or kernel outlining.DecomposeConvTransposeandNhwcConvConversioneach had a private copy of the same constant reader. Both now use a sharedreadConstantDatahelper (include/hip/Dialect/IR/HipConstantData.h), which the new pass also uses. Their behavior is unchanged.docs/pipeline_pass_menu.md(pass table and pipeline order) anddocs/design/constant-handling-design.md.Compiler IR
Before / after
Performance
hip.batch_normopship.max)Hardware, workload, build configuration, and measurement method:
ENABLE_ROCMLIRTRITON=ON,--hip_arch gfx1151. Before and after are both on base787825af.HIPDNN_EP_ROCMLIR=1,HIPDNN_EP_ROCMLIR_AUTOTUNE=quick,HIPDNN_EP_STRICT=1,XLNX_ABI_2_0_CLONE_EXTERNAL_DATA_THRESHOLD=1073741824.onnxruntime_perf_test -I -t 30 -c 1, runs done serially.The gain is smaller than a DRAM-bandwidth estimate predicted. A proxy model at the same shapes timed the removed kernels at:
That is about 8 ms in total across the 36 + 36 sites. The 128-channel tensors are largely served from the GPU's last-level cache. After the fold, rocMLIR conv/GEMM kernels take about 129 ms of roughly 194 ms of kernel time per inference, and layout copies, transposes and pads take about 50 ms.
Test plan
test/lit/Transforms/test_fold_batchnorm.mlir(new):test/numeric/tests/test_conv_batchnorm.py(new) covers f32/f16 × with or without bias × group 1/4, plusRelu(BN(Add(Conv, Conv)))in f32/f16. Run withHIPDNN_EP_STRICT=1and--ep-option XLNX_model_clone_external_data_threshold=1073741824, together withtest_conv.py:HIPDNN_EP_ROCMLIR=1: 16 passed.pre-commit runon the changed files: all hooks pass.Notes for reviewers
outline-onnx-to-hipdnnclaimsonnx.Convbefore conversion, so those convs never becomehip.convand the fold doesn't see them.DecomposeConvTransposefires. On iri-v1 this leaves the 3 decoder ConvTransposes undecomposed. That is very likely most of the gap between 625 ms at the default threshold and 211 ms at 1 GiB. This predates this PR.HIPDNN_EP_ROCMLIR_AUTOTUNE=quick, a plain 16→64 channel 3x3 conv at 8x8 returns all zeros, even without any BatchNorm. It's correct without autotune and at 32x32. The rocMLIR numeric results above were therefore run without autotune. I'll file this separately.Checklist
AI assistance: implementation, tests, benchmarking and this description were drafted with Cursor (Claude Opus 5.5). They were validated with the LIT, numeric and iri-v1 runs listed above.