|
| 1 | +--- |
| 2 | +name: opus-kernel-best-practice |
| 3 | +description: Compile-time optimization guidance for HIP/C++ kernels using opus.hpp. Use when writing or reviewing OPUS kernels, analyzing compile time, reducing template instantiation overhead, or optimizing hipcc build performance. |
| 4 | +argument-hint: [file or topic] |
| 5 | +--- |
| 6 | + |
| 7 | +# OPUS Kernel Compile-Time Best Practices |
| 8 | + |
| 9 | +Techniques for reducing HIP/C++ kernel compile time when using `opus.hpp`. These patterns were developed while optimizing a GQA flash attention kernel from **4.8s to 1.5s** (70% reduction) in device-only compilation. |
| 10 | + |
| 11 | +## Required headers and include paths |
| 12 | + |
| 13 | +For kernel development with OPUS, use these headers from `csrc/include/`: |
| 14 | + |
| 15 | +- **`opus/opus.hpp`** — the OPUS template library + device intrinsic wrappers. **This is the only include needed for device code.** Provides `opus::thread_id_x()`, `opus::block_id_x()`, `opus::sync_threads()`, `opus::warp_all()`, etc. |
| 16 | +- **`opus/hip_minimal.hpp`** — minimal HIP **host-side only** declarations (`dim3`, `hipMalloc`, `hipLaunchKernelGGL`, etc.). Use on the host pass instead of `<hip/hip_runtime.h>`. |
| 17 | + |
| 18 | +```bash |
| 19 | +hipcc my_kernel.cu -I<aiter_root>/csrc/include -D__HIPCC_RTC__ -std=c++20 -O3 --offload-arch=gfx950 |
| 20 | +``` |
| 21 | + |
| 22 | +| HIP runtime | opus:: wrapper | LLVM builtin | |
| 23 | +|---|---|---| |
| 24 | +| `threadIdx.x` | `opus::thread_id_x()` | `__builtin_amdgcn_workitem_id_x()` | |
| 25 | +| `blockIdx.x` | `opus::block_id_x()` | `__builtin_amdgcn_workgroup_id_x()` | |
| 26 | +| `blockDim.x` | `opus::block_size_x()` | `__builtin_amdgcn_workgroup_size_x()` | |
| 27 | +| `gridDim.x * blockDim.x` | `opus::grid_size_x()` | `__builtin_amdgcn_grid_size_x()` | |
| 28 | +| `__syncthreads()` | `opus::sync_threads()` | `__builtin_amdgcn_s_barrier()` | |
| 29 | +| `__all(pred)` | `opus::warp_all(pred)` | — | |
| 30 | + |
| 31 | +If anything is missing, contact the maintainer (carlus.huang@amd.com) for adding support. |
| 32 | + |
| 33 | +## 0. Always Separate Device and Host Code (Most Important) |
| 34 | + |
| 35 | +**This is the single most impactful technique.** hipcc always performs **two compilation passes** on every `.hip`/`.cu` file — one for the host (x86_64) and one for the device (AMDGPU). The heavy `opus.hpp` template library is only needed on the device side, but without a guard, hipcc parses it on BOTH passes, doubling the frontend cost. |
| 36 | + |
| 37 | +**Always structure your kernel files like this:** |
| 38 | + |
| 39 | +```cpp |
| 40 | +// my_kernel.cu |
| 41 | +#ifdef __HIP_DEVICE_COMPILE__ |
| 42 | +// ── Device pass: include opus.hpp and define kernels ── |
| 43 | +#include "opus/opus.hpp" |
| 44 | + |
| 45 | +__global__ __launch_bounds__(256, 2) |
| 46 | +void my_kernel(const float* src, float* dst, int n) { |
| 47 | + // ... opus layout, load, store, MMA, etc. |
| 48 | +} |
| 49 | + |
| 50 | +#else |
| 51 | +// ── Host pass: minimal declarations + launcher only ── |
| 52 | +#include "opus/hip_minimal.hpp" |
| 53 | + |
| 54 | +__global__ void my_kernel(const float* src, float* dst, int n); // declaration only |
| 55 | + |
| 56 | +extern "C" void run_my_kernel(const void* d_src, void* d_dst, int n) { |
| 57 | + dim3 grid((n + 255) / 256), block(256); |
| 58 | + hipLaunchKernelGGL(my_kernel, grid, block, 0, 0, |
| 59 | + (const float*)d_src, (float*)d_dst, n); |
| 60 | + hipDeviceSynchronize(); |
| 61 | +} |
| 62 | +#endif |
| 63 | +``` |
| 64 | + |
| 65 | +**Why this works:** |
| 66 | +- The device pass sees `opus.hpp` + kernel definitions — full template expansion |
| 67 | +- The host pass sees only `opus/hip_minimal.hpp` (~70 lines) + kernel declaration + launch wrapper |
| 68 | +- **Saves ~50% of total compile time** by eliminating opus.hpp parsing on the host pass |
| 69 | +- The `extern "C"` launcher can be called from Python via `ctypes.CDLL` — no pybind11/torch extension needed |
| 70 | + |
| 71 | +**Compile flags:** |
| 72 | + |
| 73 | +```bash |
| 74 | +hipcc my_kernel.cu \ |
| 75 | + -I<aiter_root>/csrc/include \ |
| 76 | + -D__HIPCC_RTC__ \ |
| 77 | + -std=c++20 -O3 -ffast-math \ |
| 78 | + --offload-arch=gfx950 \ |
| 79 | + -fPIC -shared -o my_kernel.so |
| 80 | +``` |
| 81 | + |
| 82 | +## 1. Minimize Header Overhead |
| 83 | + |
| 84 | +### Replace `<hip/hip_runtime.h>` with `opus/hip_minimal.hpp` |
| 85 | + |
| 86 | +Standard `<hip/hip_runtime.h>` expands to ~190K preprocessed lines. The aiter-provided `opus/hip_minimal.hpp` (~80 lines) declares only what's needed — `dim3`, `hipLaunchKernelGGL`, `hipMalloc`/`hipFree`, `__launch_bounds__`, `__shared__`/`__device__`/`__global__`, and `__all()`. Use AMDGCN compiler builtins for device intrinsics: |
| 87 | + |
| 88 | +```cpp |
| 89 | +int tid = __builtin_amdgcn_workitem_id_x(); // threadIdx.x |
| 90 | +int bid = __builtin_amdgcn_workgroup_id_x(); // blockIdx.x |
| 91 | +int bsz = __builtin_amdgcn_workgroup_size_x(); // blockDim.x |
| 92 | +__builtin_amdgcn_s_barrier(); // __syncthreads() |
| 93 | +``` |
| 94 | + |
| 95 | +### Use `-D__HIPCC_RTC__` to suppress implicit includes |
| 96 | + |
| 97 | +Even with minimal headers, hipcc's implicit `__clang_hip_runtime_wrapper.h` pulls in `<cmath>`, `<cstdlib>`, etc. The `-D__HIPCC_RTC__` flag skips these. Provide `#define INFINITY __builtin_huge_valf()` if needed. |
| 98 | + |
| 99 | +### Use ctypes instead of pybind11/torch extension for Python bindings |
| 100 | + |
| 101 | +The C++ binding layer is often the biggest compile cost. The `extern "C"` + `ctypes.CDLL` pattern from Section 0 eliminates it entirely: |
| 102 | + |
| 103 | +| Binding | Compile time | |
| 104 | +|---------|-------------| |
| 105 | +| torch `CUDAExtension` | ~21s | |
| 106 | +| pybind11 + Ninja | ~4.2s | |
| 107 | +| ctypes (`extern "C"`, see Section 0) | ~0.4s | |
| 108 | + |
| 109 | +## 2. Reduce Template Instantiation Count |
| 110 | + |
| 111 | +### Use runtime loops instead of `static_for` where compile-time indices aren't needed |
| 112 | + |
| 113 | +Each iteration of `static_for<N>([&](auto I){...})` creates a unique lambda instantiation. For large N, this dominates compile time. Replace with plain `for` loops when the loop body doesn't need compile-time `I`: |
| 114 | + |
| 115 | +```cpp |
| 116 | +// SLOW: N unique lambda instantiations |
| 117 | +static_for<N>([&](auto I) { |
| 118 | + r[I.value] = load<vec>(offsets[I.value]); |
| 119 | +}); |
| 120 | + |
| 121 | +// FAST: 1 instantiation, compiler unrolls identically |
| 122 | +for (index_t i = 0; i < N; i++) { |
| 123 | + r[i] = load<vec>(offsets[i]); |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +**When you still need `static_for`**: If the body uses `I` as a template argument (e.g., `number<I.value>{}` for `set_slice`, `slice`, or immediate-offset `_tr_load<vec, off>`), you must keep `static_for`. |
| 128 | + |
| 129 | +### Use runtime `flat_to_coords` instead of compile-time multi-index decomposition |
| 130 | + |
| 131 | +`layout_to_offsets` converts a layout into a precomputed offset array using a runtime loop with `flat_to_coords`, which produces `tuple<index_t, ...>` (one type for all iterations) instead of `tuple<number<a>, number<b>, ...>` (unique type per iteration): |
| 132 | + |
| 133 | +```cpp |
| 134 | +// SLOW: N unique coord_to_linear instantiations (one per multi-index combination) |
| 135 | +static_ford(issue_space_vec, [&](auto... ids) { |
| 136 | + offsets[u_linear(ids...)] = u(ids...); |
| 137 | +}); |
| 138 | + |
| 139 | +// FAST: 1 coord_to_linear instantiation (all iterations use tuple<index_t, ...>) |
| 140 | +for (index_t i = 0; i < num_issues; i++) { |
| 141 | + offsets[i] = u(flat_to_coords(i, make_index_seq<ndim>{}, issue_space_vec)); |
| 142 | +} |
| 143 | +``` |
| 144 | +
|
| 145 | +### Cache constexpr computations in struct members |
| 146 | +
|
| 147 | +Repeated constexpr evaluations in multiple methods trigger re-evaluation in each: |
| 148 | +
|
| 149 | +```cpp |
| 150 | +// SLOW: y_shape_a() + reduce_tuple_mul evaluated in every operator()/step_k() overload |
| 151 | +constexpr auto a_len = get<0>(reduce_tuple_mul(MMA::y_shape_a())); |
| 152 | +
|
| 153 | +// FAST: cached once as class member |
| 154 | +static constexpr index_t mma_a_len = get<0>(reduce_tuple_mul(MMA::y_shape_a())).value; |
| 155 | +``` |
| 156 | + |
| 157 | +## 3. Use LLVM Builtins for Vector Operations |
| 158 | + |
| 159 | +### `__builtin_convertvector` for type conversion |
| 160 | + |
| 161 | +Replaces N-element element-by-element `cast_impl` pack expansion with a single LLVM intrinsic: |
| 162 | + |
| 163 | +```cpp |
| 164 | +// SLOW: 64-element pack expansion |
| 165 | +return vector_return_type<D, decltype(cast<D>(get<Is>(s)))...>{cast<D>(get<Is>(s))...}; |
| 166 | + |
| 167 | +// FAST: single builtin call |
| 168 | +return __builtin_convertvector(s, vector_t<D, size<S>()>); |
| 169 | +``` |
| 170 | +
|
| 171 | +### `__builtin_shufflevector` for vector slice/concat |
| 172 | +
|
| 173 | +Replaces element-by-element `make_vector(get<Is>(c)...)` with a single shuffle: |
| 174 | +
|
| 175 | +```cpp |
| 176 | +// SLOW: N-element braced init |
| 177 | +return make_vector(get<Is>(c)...); |
| 178 | +
|
| 179 | +// FAST: single shuffle (returns GCC-style vector, bit_cast to ext_vector_type) |
| 180 | +using R = vector_t<scalar_type, sizeof...(Is)>; |
| 181 | +return __builtin_bit_cast(R, __builtin_shufflevector(c, c, Is...)); |
| 182 | +``` |
| 183 | + |
| 184 | +## 4. Avoid Intermediate Type Creation |
| 185 | + |
| 186 | +### Bypass `concat_tuple` with direct indexing |
| 187 | + |
| 188 | +`concat_tuple` creates intermediate tuple types when concatenating >4 tuples. Replace with direct per-element computation: |
| 189 | + |
| 190 | +```cpp |
| 191 | +// unfold_x_stride: instead of concat_tuple(per_group_results...) |
| 192 | +// compute each element's stride directly via unfold_x_stride_at<J>() |
| 193 | + |
| 194 | +// pickup_shape: instead of concat_tuple(conditional<match, tuple<T>, tuple<>>{}...) |
| 195 | +// build a filtered index sequence, then make_tuple(get<filtered_indices>(Shape{})...) |
| 196 | + |
| 197 | +// flatten_tuple: instead of concat_tuple(explode_tuple(get<Is>(t))...) |
| 198 | +// directly index as get<local>(get<group>(t)) via flatten_at<T, J, GS>() |
| 199 | +``` |
| 200 | + |
| 201 | +### Specify return type explicitly to avoid `std::common_type` |
| 202 | + |
| 203 | +```cpp |
| 204 | +// SLOW: triggers recursive std::common_type<D, D, D, ..., D> with 64 types |
| 205 | +return vector_return_type<void, decltype(cast<D>(get<Is>(s)))...>{...}; |
| 206 | + |
| 207 | +// FAST: D is already known, skip common_type entirely |
| 208 | +return vector_return_type<D, decltype(cast<D>(get<Is>(s)))...>{...}; |
| 209 | +``` |
| 210 | +
|
| 211 | +### Add fold-expression fast paths for common patterns |
| 212 | +
|
| 213 | +```cpp |
| 214 | +// reduce_tuple_mul for tuple<number<>...>: fold expression instead of recursive reduction |
| 215 | +template<typename... Ns, std::enable_if_t<(is_constant_v<Ns> && ...), bool> = true> |
| 216 | +constexpr auto reduce_tuple_mul(const tuple<Ns...>&) { return tuple<number<(Ns::value * ...)>>{}; } |
| 217 | +``` |
| 218 | + |
| 219 | +## 5. Parallel Compilation |
| 220 | + |
| 221 | +### Split device test files by template-instantiation cost |
| 222 | + |
| 223 | +One file with 14 MFMA template instantiations (~3.9s) bottlenecks parallel builds. Split into per-type files (f16/f32/f8) to balance workload: |
| 224 | + |
| 225 | +``` |
| 226 | +test_mfma.cu (3.9s) -> test_mfma_f16.cu (0.9s) + test_mfma_f32.cu (0.5s) + test_mfma_f8.cu (0.9s) |
| 227 | +``` |
| 228 | + |
| 229 | +### Use `hipcc --genco` for device-only compilation when launching from Python |
| 230 | + |
| 231 | +Eliminates the host pass entirely. Python loads the `.hsaco` via `hipModuleLoad` and launches with `hipModuleLaunchKernel` (HIP driver API). |
| 232 | + |
| 233 | +## Compile-Time Measurement |
| 234 | + |
| 235 | +### Use `-ftime-trace` for profiling |
| 236 | + |
| 237 | +```bash |
| 238 | +hipcc kernel.cc --cuda-device-only -c -o /dev/null \ |
| 239 | + -Xclang -ftime-trace=trace.json |
| 240 | +``` |
| 241 | + |
| 242 | +Analyze with chrome://tracing or a script: |
| 243 | + |
| 244 | +```python |
| 245 | +import json |
| 246 | +with open('trace.json') as f: data = json.load(f) |
| 247 | +events = data.get('traceEvents', data) |
| 248 | +inst = [(e['dur'], e['args']['detail']) for e in events |
| 249 | + if e.get('name') == 'InstantiateFunction' and 'dur' in e] |
| 250 | +inst.sort(key=lambda x: -x[0]) |
| 251 | +for dur, name in inst[:20]: |
| 252 | + print(f"{dur/1000:8.1f}ms {name[:100]}") |
| 253 | +``` |
| 254 | + |
| 255 | +### Key metrics to track |
| 256 | + |
| 257 | +- **Function instantiations**: total count and per-function time |
| 258 | +- **Frontend vs Backend**: frontend = template instantiation, backend = LLVM optimizer + codegen |
| 259 | +- **Critical path**: the single slowest template chain determines wall-clock time |
| 260 | + |
| 261 | +## Summary Table |
| 262 | + |
| 263 | +| Technique | Typical savings | Where applied | |
| 264 | +|-----------|----------------|---------------| |
| 265 | +| **Separate device/host code** (`__HIP_DEVICE_COMPILE__` guard) | **~50% total** | All `.cu`/`.hip` files — always do this first | |
| 266 | +| Runtime `for` loops in load/store/MMA | 30-60% frontend | `buffer_view::load/store`, `tiled_mma_adaptor::operator()` | |
| 267 | +| Runtime `flat_to_coords` | 40-50% frontend | `layout_to_offsets` | |
| 268 | +| `__builtin_convertvector` | 5-10% frontend | `cast` for vectors >16 elements | |
| 269 | +| `__builtin_shufflevector` | 3-5% frontend | `slice_impl` for vectors | |
| 270 | +| Cache constexpr members | 10-15% frontend | `layout_load_traits`, `mma_a/b/c_len` | |
| 271 | +| Direct indexing (bypass concat_tuple) | 5-10% frontend | `unfold_x_stride`, `pickup_shape`, `flatten_tuple` | |
| 272 | +| `-D__HIPCC_RTC__` | ~25% per-file | Compiler flags | |
| 273 | +| `hipcc --genco` | ~15% per-file | Python-launched kernels | |
| 274 | +| Split large TU files | Better parallelism | Test suites, multi-kernel builds | |
0 commit comments