Skip to main content

opus-module-build-optimization

Module-level JIT build-wall optimization for opus-based aiter modules. Use when an aiter JIT module's first-call build wall is a user-visible bottleneck or when adding a new module.

Informations de source

Dépôt
ROCm/aiter
Dernière activité de la source
27 mai 2026 à 13:02
Langue détectée de SKILL.md
anglais
Étoiles
582
Forks
619

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
opus-module-build-optimization
description
Module-level JIT build-wall optimization for opus-based aiter modules. Use when an aiter JIT module's first-call build wall is a user-visible bottleneck or when adding a new module.
argument-hint
["module name or topic"]
# Opus Module Build-Time Optimization Techniques for reducing the JIT build wall of an aiter module bundling N opus kernel TUs + a dispatcher + a pybind TU. Patterns developed while taking `module_deepgemm_opus` from **48.4s to 11.1s** (-77%) end-to-end rebuild. This skill is the **module-level** companion to `opus-kernel-best-practice` (which covers single-kernel codegen). Use this one when the bottleneck is host-pass header weight, TU count + parallelism, or dispatcher / pybind TU cost. Use the kernel-author skill instead when a single TU's device-pass MCG dominates. ## 0. Diagnose Before Optimizing (Most Important) **Always run these probes first.** Speculation on what saves how much is unreliable -- module bottlenecks are non-obvious and often counter-intuitive (we expected `-mllvm` flags to matter, they didn't; we expected splitk to be slower, it was actually a hipgraph trap). Set these once before running any snippet below: ```bash export AITER_ROOT=$(pwd) # your aiter checkout export MODULE=module_<name> # e.g. module_deepgemm_opus export BUILD_DIR=$AITER_ROOT/aiter/jit/build/$MODULE/build ``` ### End-to-end build wall (3-trial) ```bash for i in 1 2 3; do rm -rf $AITER_ROOT/aiter/jit/build/$MODULE \ $AITER_ROOT/aiter/jit/$MODULE.so AITER_REBUILD=1 AITER_LOG_LEVEL=info python3 -c \ "import aiter.ops.<module_python_path>" 2>&1 | grep "finish build" done ``` ### Per-TU wall ranking (find the critical path) ```bash cd $BUILD_DIR for tu in *.cuda.o; do ninja -t commands $tu > /tmp/cmd.sh; chmod +x /tmp/cmd.sh rm -f $tu t0=$(date +%s.%N); /tmp/cmd.sh > /dev/null 2>&1; t1=$(date +%s.%N) printf "%5ss %s\n" "$(awk -v t0=$t0 -v t1=$t1 'BEGIN{printf "%.2f", t1-t0}')" "$tu" done | sort -rn | head -10 ``` Top 1-2 TUs ARE the build wall under `MAX_JOBS >= N`. Optimize those; don't touch the long tail. ### Per-pass timing (host vs device) ```bash export TU=<one_slow_tu>.cuda.o cd $BUILD_DIR ninja -t commands $TU > /tmp/cmd.sh sed -i 's| -c | -ftime-report -c |' /tmp/cmd.sh; chmod +x /tmp/cmd.sh rm -f $TU; /tmp/cmd.sh 2>&1 | tee /tmp/tr.log ``` The output has one block per pass. Read the `Total` row of each: - **Host-pass dominant** (Front end > 5s) → header weight or template explosion → this skill (rounds 1-6) - **Device-pass dominant** (Machine code generation > 70% of total) → kernel-internal codegen → `opus-kernel-best-practice` ### Preprocessed input size ```bash cd $BUILD_DIR ninja -t commands $TU > /tmp/cmd.sh sed -E 's| -c | --cuda-host-only -E |; s| -o [^ ]+\.cuda\.o||' \ /tmp/cmd.sh > /tmp/cmd_pp.sh; chmod +x /tmp/cmd_pp.sh echo "host-pass lines: $(/tmp/cmd_pp.sh 2>/dev/null | wc -l)" /tmp/cmd_pp.sh 2>/dev/null | \ awk '/^# [0-9]+ "[^"]+"/{f=$3; gsub(/"/,"",f); next} {n[f]++} END {for (k in n) printf "%8d %s\n", n[k], k}' \ | sort -rn | head -20 ``` Calibration: torch-free dispatcher TU should be <150K lines. >400K means torch leaked in via some include chain. ## 1. Host/Device Pass Split in Launcher Headers hipcc compiles every `.cu` twice (host + device). Without a guard, every per-instance launcher TU's host pass parses heavy headers AND its device pass codegens the kernel, serially in one `hipcc` invocation. **Fix**: guard the launcher body and heavy host includes so the device pass takes the lean path. Kernel template forward declaration MUST stay visible on both passes. ```cpp // SLOW: device pass parses torch + ATen for every TU #include "aiter_tensor.h" template<typename T> __global__ void my_kernel(my_kargs_t k); template<typename T> void my_launcher(aiter_tensor_t& A, ...) { ... } // FAST: device pass parses ~10K lines instead of ~70K #if !defined(__HIP_DEVICE_COMPILE__) && !defined(__HIPCC_RTC__) #include "aiter_tensor.h" #endif template<typename T> __global__ void my_kernel(my_kargs_t k); // both passes #if !defined(__HIP_DEVICE_COMPILE__) && !defined(__HIPCC_RTC__) template<typename T> void my_launcher(aiter_tensor_t& A, ...) { ... } #endif ``` **When NOT to apply**: single-TU kernels with no per-instance expansion -- guarding the launcher body alone is enough. ## 2. Fused Host TU + Per-File `-D__HIPCC_RTC__` If your module has N>=5 codegen-emitted per-instance TUs, every one of them re-parses the host headers. N × parse cost adds up. **Fix**: collapse all launchers into one `all_instances_host.cu`; emit per-instance `*.device.cu` files that carry only the `template __global__` instantiation. ```cpp // instances/all_instances_host.cu (codegen-emitted) #define OPUS_FUSED_HOST_TU #include "aiter_tensor.h" #include "aiter_stream.h" #include <optional> #include "impl/kid_a.cuh" // each .cuh detects OPUS_FUSED_HOST_TU #include "impl/kid_b.cuh" // and pulls only the traits header #ifndef __HIP_DEVICE_COMPILE__ template void launcher_a<bf16_t>(...); // explicit instantiations template void launcher_b<fp32_t>(...); #endif ``` Per-kid `.cuh` switches header inclusion based on the macro: ```cpp #ifdef OPUS_FUSED_HOST_TU #include "<traits>.cuh" // type defs only -- no functions template<typename T> __global__ void kernel(my_kargs_t k); #else #include "<full_pipeline>.cuh" // pipeline body for device codegen #endif ``` Add per-source `-D__HIPCC_RTC__` for `*.device.cu` so their host pass takes the lean RTC branch: ```json "module_<name>": { "flags_extra_hip_per_source": { "*.device.cu": ["-D__HIPCC_RTC__"] } } ``` **Trap**: the fused TU MUST NOT pull in two pipeline headers that both define same-named `inline __device__` helpers -- ODR violation. Use the traits header only. **When NOT to apply**: fewer than ~5 per-instance TUs; the codegen complexity isn't worth it. ## 3. Torch Removal in Launchers Even after sections 1-2, fused host TU may still parse `<torch/all.h>` + ATen because the launcher takes `torch::Tensor`. **Fix**: switch to a torch-free POD view. The canonical type in aiter is `aiter_tensor_t` (defined in `csrc/include/aiter_tensor.h`). | Before | After | |---|---| | `torch::Tensor& X` | `aiter_tensor_t& X` | | `TORCH_CHECK(...)` | `AITER_CHECK(...)` | | `at::ScalarType::BFloat16` | `AITER_DTYPE_bf16` | | `at::cuda::getCurrentCUDAStream()` | `aiter::getCurrentHIPStream()` | | `return Y;` | `void` (in-place on Y) | Python wrapper takes `@compile_ops(..., develop=True)` to auto-convert `torch.Tensor` args to `aiter_tensor_t` and inject the current stream. Keep the `return Y` contract by writing `raw(...); return Y` since C++ is `void` now. Saves ~75% of the fused host TU's preprocessed input (e.g. 440K → 110K lines). Pattern mirrors PR #2932. ## 4. Dispatcher / Pybind TU Torch Removal After section 3, the dispatcher or pybind TU may still be heavy because some intermediate header still pulls torch. **Fix**: trace the include chain that opens `<torch/...>` first: ```bash grep -n '^# 1 "[^"]*\(torch\|ATen\)' /tmp/dispatcher_pp.txt | head # Walk back from the first torch open to find the nearest of YOUR # headers above it -- that's the include leaking torch. ``` Replace with the lightest header that gives the same primitive types (`bf16_t`, `fp32_t`). Single `#include` swap can save several seconds. ## 5. Flat-Array Lookup Tables If the dispatcher uses `std::unordered_map<key, std::function, ...>` for runtime kernel lookup, the heavyweight templates alone add ~1s of frontend / template instantiation per dispatcher TU. ```cpp // SLOW: heap-allocating std::function values + hashtable templates using Map = std::unordered_map<Tuple, std::function<...>, TupleHash>; static const auto lookup = [] { return Map{GENERATE_LOOKUP_MACRO(...)}; }(); // FAST: zero template overhead, no first-call heap alloc using KernelPtr = void (*)(aiter_tensor_t&, aiter_tensor_t&, ...); struct Entry { Shape key; KernelPtr func; }; static constexpr Entry kLookup[] = { GENERATE_LOOKUP_MACRO(CTYPE) // codegen emits sorted by key }; auto it = std::lower_bound(kLookup, kLookup + N, needle, entry_less); if (it != end && entry_eq(*it, needle)) return it->func; ``` The codegen emits entries pre-sorted by key. Function-pointer typedef replaces `std::function`. **When NOT to apply**: dispatcher TU already <2s; the gain is bounded by `unordered_map` + `std::function` template work. ## 6. Dedicated TU for Shared Sub-Kernels If every per-instance device.cu redundantly emits instantiations of a shared sub-kernel (e.g. a reduce kernel with N output-dtype specialisations), each TU pays the full RA + ISA emit cost; the linker dedupes the resulting weak symbols later. **Fix**: emit one `instances/shared_kernel.device.cu` carrying all instantiations of the shared sub-kernel; remove them from each per-instance device.cu. Forward declarations in the fused host TU still resolve at link time. Per-instance TU walls drop; CPU footprint shrinks proportionally to N. End-to-end wall doesn't always move (the slowest TU may not be the shared one), but helps under constrained `MAX_JOBS`. **When NOT to apply**: fewer than ~5 instances of the shared sub-kernel. ## 7. Workspace Cache (Optional) **Applies only if** section 3 left the launcher allocating a per-call workspace via `hipMallocAsync(stream)` / `hipFreeAsync(stream)` to stay torch-free. Caller-provided workspace, `torch::empty` (graph-aware caching allocator), or no workspace -- skip this section. Not strictly a build-time optimization; it's a runtime correctness fix that the torch-removal in section 3 makes necessary. Bundle it with the build refactor or your tuner will misinterpret graph-mode allocator overhead as kernel-perf differences. **Why**: hipgraph capture on current ROCm releases does NOT record the stream-ordered allocator as a no-op. Every replay re-runs alloc + free host-side; an aiter-style tuner running in graph mode then sees the allocator overhead instead of real kernel work. **Suggested pattern**: replace per-call alloc with a `thread_local` cache that grows on demand. Inside capture the grow path is forbidden; `hipStreamIsCapturing` gates it. ```cpp // SLOW: every replay re-runs alloc + free HIP_CALL(hipMallocAsync(&ws, ws_bytes, stream)); launch_main_kernel<<<..., stream>>>(ws); HIP_CALL(hipFreeAsync(ws, stream)); // FAST: one-time alloc, reused across replays static thread_local void* ws_cached_ptr = nullptr; static thread_local size_t ws_cached_bytes = 0; if (ws_cached_ptr == nullptr || ws_bytes > ws_cached_bytes) { hipStreamCaptureStatus s = hipStreamCaptureStatusNone; HIP_CALL(hipStreamIsCapturing(stream, &s)); AITER_CHECK(s == hipStreamCaptureStatusNone, "workspace cache miss inside HIP graph capture. " "Warm the cache once eagerly first."); if (ws_cached_ptr) { HIP_CALL(hipDeviceSynchronize()); HIP_CALL(hipFree(ws_cached_ptr)); } constexpr size_t kAlign = 4ull * 1024 * 1024; size_t grow = ((ws_bytes + kAlign - 1) / kAlign) * kAlign; HIP_CALL(hipMalloc(&ws_cached_ptr, grow)); ws_cached_bytes = grow; } launch_main_kernel<<<..., stream>>>(ws_cached_ptr); // no free -- cache held until thread exit ``` Callers that capture must warm the cache once eagerly (with the largest workspace they will use) BEFORE entering capture. **Alternative**: revert to `torch::empty(...)` if the module can re-accept the torch parse cost from section 3. **Verifying the fix**: time the launcher in eager and graph modes (`torch.cuda.Event` + `torch.cuda.CUDAGraph`); if `graph >> eager` after applying section 3, this section's cache is needed. ## Anti-Patterns - **Don't optimize without measurement.** Always run section 0 first. - **Don't add `-mllvm` flags blindly.** Module-private overrides rarely help and can regress kernel perf. Override the aiter-global flags only after measuring both build wall AND kernel perf vs the default. - **Don't put module-specific findings in shared headers.** Comments in `csrc/include/opus/*.hpp` (used by every opus kernel) should explain WHY a fix is there but NOT the specific kernel name or perf number that motivated it. Module-specific findings belong in the module's README. ## Summary Table | Technique | Typical savings | Where applied | |---|---|---| | **Host/device pass split** (`__HIP_DEVICE_COMPILE__` + `__HIPCC_RTC__` guard) | ~50% per-TU | Every codegen-emitted launcher .cuh |
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub