perf(rccl): split ce_reduce into per-kernel TUs to fix ~48min build time - #9756
Conversation
✅ All Policy Checks Passed
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🚫 Please fix the failed policies before requesting reviews. The following policy checks failed:
The |
There was a problem hiding this comment.
Pull request overview
This PR addresses the long serialized build-time bottleneck in RCCL’s CE (Copy Engine) AllReduce local-reduce path by splitting the previously monolithic ce_reduce.cc (40 (type, redop) instantiations) into per-instantiation translation units generated under gensrc/ce_reduce/, enabling parallel compilation/device-linking and reducing pathological LLVM codegen overhead.
Changes:
- Add a code generator to emit
ce_reduce_impl.hplus 40ce_reduce_<type>_<redop>.cppinstantiation TUs with per-kernel loop pragma tuning. - Convert
src/device/ce_reduce.ccinto a pure host-side dispatcher that forwards to the generated per-instantiation launchers. - Update the build system to generate and compile these per-instantiation TUs in both normal and device-linker pipelines.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| projects/rccl/src/device/ce_reduce/generate.py | New generator that emits shared kernel/template header and per-(type,redop) launcher TUs with loop pragma controls. |
| projects/rccl/src/device/ce_reduce.cc | Reworked into a host-only dispatcher calling pre-instantiated launcher symbols. |
| projects/rccl/src/CMakeLists.txt | Runs CE-reduce TU generation and updates device-linker filtering expectations. |
| projects/rccl/cmake/DeviceLinker.cmake | Compiles all generated CE-reduce instantiation TUs as separate fat objects in the device-linker pipeline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
43e7a73 to
c6d137f
Compare
c6d137f to
6a71529
Compare
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Can you add the missing files? |
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
fcde209 to
a04c4fd
Compare
|
Yes, sorry — that was actually my mistake: I'd landed the |
mustafabar
left a comment
There was a problem hiding this comment.
Thanks for this PR. I had added a few minor comments.
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
a04c4fd to
fc5a4d1
Compare
RCCL Perf-Regression Gate: ✅ PASSMode: detect (reference vs candidate) Per-collective breakdown
|
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
23b946a to
8ea2e99
Compare
…-loop unroll ncclCeLocalReduceKernelVec's 40 (type, redop) instantiations previously lived in a single ce_reduce.cc, forcing the device-link LTO codegen pass to run serially over all of them. Two of the 40 -- int8_t/uint8_t Min/Max -- balloon to ~56K instructions each because LLVM's runtime loop-unroller was unrolling the `for (r = 1; r < nRanks; r++)` rank-reduction loop by up to 64x (nRanks is a runtime value) on top of the existing manual U/W unroll/vectorization, producing ~900 PHI nodes and ~150 shufflevector ops per kernel. That alone made the file take ~48 minutes to compile. Fix: generate one TU per instantiation (src/device/ce_reduce/generate.py) so ninja parallelizes the 40 kernels like every other device TU, and mark both rank-reduction loops `#pragma clang loop unroll(disable) vectorize(disable)` so LLVM stops auto-unrolling a loop the manual unroll already covers. Build time: ~48.5 min -> ~14.7 min for the ce_reduce kernels. Runtime validated via a standalone microbenchmark (all 40 type/redop combos, correctness-checked against a host-side reference using the kernel's exact fold order, then timed 16B-1GB per-rank chunk sizes): no regression, i8/u8 Min/Max reach ~1.0-1.25 TB/s. Co-authored-by: Cursor <cursoragent@cursor.com>
…ling it Disabling the rank-loop's runtime unroller fixed the ce_reduce compile-time blowup, but the accompanying vectorize(disable) was applied uniformly to all 40 (type, redop) instantiations. Measurement on gfx950 showed that's a loss for int8_t Min/Max/Sum and uint8_t Sum (~10-12% slower than auto-unrolled develop), even though it's a big win elsewhere (int32_t/uint32_t Sum/Prod are ~2.2x faster with it disabled). Key the vectorizer setting per (type, redop) via a CE_REDUCE_VECTORIZE_OK macro defined ahead of the shared kernel header in the affected TUs, using whichever setting measured faster. The runtime unroller stays disabled everywhere (that's what fixes compile time); only the vectorizer choice varies. Verified all 4 previously-pathological kernels (i8/u8 Min/Max) still compile in ~2s. Co-authored-by: Cursor <cursoragent@cursor.com>
- Handle stale subdirectories/symlinks when clearing the output dir, mirroring the symmetric kernel generator's os.path.isfile/isdir check instead of assuming every entry is a plain file (os.remove() would otherwise raise on a leftover subdir from a prior build). - #undef CE_REDUCE_VECTORIZE_OK right after ce_reduce_impl.h consumes it, so the macro doesn't leak into later includes/code in the same TU. Co-authored-by: Cursor <cursoragent@cursor.com>
… files IMPL_HEADER and LAUNCHER_TEMPLATE were multi-hundred-line C++/HIP kernel code buried in Python string literals inside generate.py, making the device code hard to read/edit with normal tooling (per Alex's review on #9756). Move them to ce_reduce_impl.h.in (copied verbatim) and ce_reduce_launcher.cpp.in (expanded via string.Template's $-placeholders instead of doubled {{ }} braces), leaving generate.py as just the TYPES/REDOPS/VECTORIZE_OK tables and a short substitution loop. Verified byte-for-byte identical generator output aside from the intentionally updated doc comments. Also add test_generate_ce_reduce.py (wired into ctest as rccl-generate-ce-reduce, alongside the existing generator test) to cover this pipeline, addressing the missing-unit-test policy check on #9756. Co-authored-by: Cursor <cursoragent@cursor.com>
Mustafa's review on this PR asked whether three loops in ncclCeLocalReduceKernelVec (the elementwise k loop, the drain loop's own outer loop, and the scalar tail's rank loop) were missing an unroll(disable) pragma by oversight, since sibling loops have one. Investigated in #9797: adding the pragma to the elementwise k loop regresses every kernel 2-3x (forces a compile-time-constant register access into a runtime-indexed one, realized via an LDS round trip), and even gating it to only the int8_t/uint8_t kernels that already use the loop vectorizer still regresses those ~1.8x (the "bloat" there is the vectorizer packing the byte-wise reduction into native sub-dword instructions, which needs to see through this loop). The other two loops are confirmed no-ops either way (byte-identical codegen). So all three are left as-is; add short comments explaining why. Co-authored-by: Cursor <cursoragent@cursor.com>
8ea2e99 to
38f648f
Compare
Summary
Fixes #9757.
The CE (Copy Engine) AllReduce local-reduce kernel (
ce_reduce.cc) held all 40(type, redop)instantiations in a single translation unit. LLVM's runtime loop-unroller was auto-unrolling the rank-reduction loop (nRanksis a runtime value) by up to 64x on top of the existing manual unroll/vectorization, producing ~900 PHI nodes and ~150shufflevectorops per kernel forint8_t/uint8_tMin/Max alone. This dominatedllccodegen time and pushed the full RCCL build to ~48 minutes.This PR splits the 40 instantiations into one
.cppfile per(type, redop)(enabling parallel device-link compilation) and disables the runtime unroller on the rank loop. Since disabling the loop vectorizer along with the unroller turned out to help some kernels but hurt others by ~8-12% (real, not noise — confirmed via repeated on-GPU measurement), the vectorizer setting is keyed per-kernel to whichever measured faster, while the unroller stays disabled everywhere.Build time: the single-TU
ce_reduce.ccwas one compile job that couldn't be parallelized and took ~48 minutes by itself, dominating the whole build's critical path regardless of available cores. Splitting it into 40 independent TUs removes that serialized bottleneck: verified with an empty ccache (no caching effects), each of the 40 kernels now compiles in under 2 seconds (the two worst offenders,int8_t/uint8_tMin/Max, previously took the bulk of that 48 minutes on their own), and the full clean build's wall-clock time drops accordingly — from ~48 minutes to well under a minute on a highly parallel (512-core) machine. The exact end-to-end number will vary by machine/parallelism; what's fixed is the specific unparallelizable 48-minute bottleneck, not a general build-cost reduction.Performance (MI350X/gfx950, vs. unmodified develop, measured with 2-3 repeats per build to separate signal from run-to-run noise):
int32_t/uint32_tSum/Prod: +137-139% (this is the dominant win from disabling the pathological auto-unroll)int8_tMin/Max/Sum,uint8_tSum: net neutral (a real ~8-9% regression from disabling the vectorizer was recovered by re-enabling it for just these kernels)uint8_tMin/Max: +8-12%(type, redop)combinations: no measurable change (noise floor)Correctness: all 40
(type, redop)combinations validated bit-exact against a host-side reference reduction (identical fold order, no float-associativity slop) via a standalone microbenchmark driving the launcher symbols directly.Test plan
nRanksparameter set to 8 — this is a local-reduce kernel that foldsnRanksinput buffers together on one GPU, it does not launch across multiple GPUs — compared against pristinedevelopand against the pre-per-kernel-tuning version of this fix, with repeated runs to confirm signal vs. noiseMade with Cursor