agentleFS
ROCmROCm/aiter/.claude/skills/opus-kernel-best-practice/SKILL.md

opus-kernel-best-practice

Skill605 starsChanged 8 days agoMITView on GitHub

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.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If anything is missing, contact the maintainer (carlus.huang@amd.com) for adding support.

    line 31
  2. Very specific:

    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 …

    line 127

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

OPUS Kernel Compile-Time Best Practices

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.

Required headers and include paths

For kernel development with OPUS, use these headers from csrc/include/:

  • 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.
  • 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>.
hipcc my_kernel.cu -I<aiter_root>/csrc/include -D__HIPCC_RTC__ -std=c++20 -O3 --offload-arch=gfx950
HIP runtimeopus:: wrapperLLVM builtin
threadIdx.xopus::thread_id_x()__builtin_amdgcn_workitem_id_x()
blockIdx.xopus::block_id_x()__builtin_amdgcn_workgroup_id_x()
blockDim.xopus::block_size_x()__builtin_amdgcn_workgroup_size_x()
gridDim.x * blockDim.xopus::grid_size_x()__builtin_amdgcn_grid_size_x()
__syncthreads()opus::sync_threads()__builtin_amdgcn_s_barrier()
__all(pred)opus::warp_all(pred)—

If anything is missing, contact the maintainer (carlus.huang@amd.com) for adding support.

0. Always Separate Device and Host Code (Most Important)

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.

Always structure your kernel files like this:

// my_kernel.cu
#ifdef __HIP_DEVICE_COMPILE__
// ── Device pass: include opus.hpp and define kernels ──
#include "opus/opus.hpp"

__global__ __launch_bounds__(256, 2)
void my_kernel(const float* src, float* dst, int n) {
    // ... opus layout, load, store, MMA, etc.
}

#else
// ── Host pass: minimal declarations + launcher only ──
#include "opus/hip_minimal.hpp"

__global__ void my_kernel(const float* src, float* dst, int n);  // declaration only

extern "C" void run_my_kernel(const void* d_src, void* d_dst, int n) {
    dim3 grid((n + 255) / 256), block(256);
    hipLaunchKernelGGL(my_kernel, grid, block, 0, 0,
                       (const float*)d_src, (float*)d_dst, n);
    hipDeviceSynchronize();
}
#endif

Why this works:

  • The device pass sees opus.hpp + kernel definitions — full template expansion
  • The host pass sees only opus/hip_minimal.hpp (~70 lines) + kernel declaration + launch wrapper
  • Saves ~50% of total compile time by eliminating opus.hpp parsing on the host pass
  • The extern "C" launcher can be called from Python via ctypes.CDLL — no pybind11/torch extension needed

Compile flags:

hipcc my_kernel.cu \
  -I<aiter_root>/csrc/include \
  -D__HIPCC_RTC__ \
  -std=c++20 -O3 -ffast-math \
  --offload-arch=gfx950 \
  -fPIC -shared -o my_kernel.so

1. Minimize Header Overhead

Replace <hip/hip_runtime.h> with opus/hip_minimal.hpp

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:

int tid = __builtin_amdgcn_workitem_id_x();     // threadIdx.x
int bid = __builtin_amdgcn_workgroup_id_x();     // blockIdx.x
int bsz = __builtin_amdgcn_workgroup_size_x();   // blockDim.x
__builtin_amdgcn_s_barrier();                     // __syncthreads()

Use -D__HIPCC_RTC__ to suppress implicit includes

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.

Use ctypes instead of pybind11/torch extension for Python bindings

The C++ binding layer is often the biggest compile cost. The extern "C" + ctypes.CDLL pattern from Section 0 eliminates it entirely:

BindingCompile time
torch CUDAExtension~21s
pybind11 + Ninja~4.2s
ctypes (extern "C", see Section 0)~0.4s

2. Reduce Template Instantiation Count

Use runtime loops instead of static_for where compile-time indices aren't needed

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:

// SLOW: N unique lambda instantiations
static_for<N>([&](auto I) {
    r[I.value] = load<vec>(offsets[I.value]);
});

// FAST: 1 instantiation, compiler unrolls identically
for (index_t i = 0; i < N; i++) {
    r[i] = load<vec>(offsets[i]);
}

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.

Use runtime flat_to_coords instead of compile-time multi-index decomposition

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):

// SLOW: N unique coord_to_linear instantiations (one per multi-index combination)
static_ford(issue_space_vec, [&](auto... ids) {
    offsets[u_linear(ids...)] = u(ids...);
});

// FAST: 1 coord_to_linear instantiation (all iterations use tuple<index_t, ...>)
for (index_t i = 0; i < num_issues; i++) {
    offsets[i] = u(flat_to_coords(i, make_index_seq<ndim>{}, issue_space_vec));
}

Cache constexpr computations in struct members

Repeated constexpr evaluations in multiple methods trigger re-evaluation in each:

// SLOW: y_shape_a() + reduce_tuple_mul evaluated in every operator()/step_k() overload
constexpr auto a_len = get<0>(reduce_tuple_mul(MMA::y_shape_a()));

// FAST: cached once as class member
static constexpr index_t mma_a_len = get<0>(reduce_tuple_mul(MMA::y_shape_a())).value;

3. Use LLVM Builtins for Vector Operations

__builtin_convertvector for type conversion

Replaces N-element element-by-element cast_impl pack expansion with a single LLVM intrinsic:

// SLOW: 64-element pack expansion
return vector_return_type<D, decltype(cast<D>(get<Is>(s)))...>{cast<D>(get<Is>(s))...};

// FAST: single builtin call
return __builtin_convertvector(s, vector_t<D, size<S>()>);

__builtin_shufflevector for vector slice/concat

Replaces element-by-element make_vector(get<Is>(c)...) with a single shuffle:

// SLOW: N-element braced init
return make_vector(get<Is>(c)...);

// FAST: single shuffle (returns GCC-style vector, bit_cast to ext_vector_type)
using R = vector_t<scalar_type, sizeof...(Is)>;
return __builtin_bit_cast(R, __builtin_shufflevector(c, c, Is...));

4. Avoid Intermediate Type Creation

Bypass concat_tuple with direct indexing

concat_tuple creates intermediate tuple types when concatenating >4 tuples. Replace with direct per-element computation:

// unfold_x_stride: instead of concat_tuple(per_group_results...)
// compute each element's stride directly via unfold_x_stride_at<J>()

// pickup_shape: instead of concat_tuple(conditional<match, tuple<T>, tuple<>>{}...)
// build a filtered index sequence, then make_tuple(get<filtered_indices>(Shape{})...)

// flatten_tuple: instead of concat_tuple(explode_tuple(get<Is>(t))...)
// directly index as get<local>(get<group>(t)) via flatten_at<T, J, GS>()

Specify return type explicitly to avoid std::common_type

// SLOW: triggers recursive std::common_type<D, D, D, ..., D> with 64 types
return vector_return_type<void, decltype(cast<D>(get<Is>(s)))...>{...};

// FAST: D is already known, skip common_type entirely
return vector_return_type<D, decltype(cast<D>(get<Is>(s)))...>{...};

Add fold-expression fast paths for common patterns

// reduce_tuple_mul for tuple<number<>...>: fold expression instead of recursive reduction
template<typename... Ns, std::enable_if_t<(is_constant_v<Ns> && ...), bool> = true>
constexpr auto reduce_tuple_mul(const tuple<Ns...>&) { return tuple<number<(Ns::value * ...)>>{}; }

5. Parallel Compilation

Split device test files by template-instantiation cost

One file with 14 MFMA template instantiations (~3.9s) bottlenecks parallel builds. Split into per-type files (f16/f32/f8) to balance workload:

test_mfma.cu (3.9s) -> test_mfma_f16.cu (0.9s) + test_mfma_f32.cu (0.5s) + test_mfma_f8.cu (0.9s)

Use hipcc --genco for device-only compilation when launching from Python

Eliminates the host pass entirely. Python loads the .hsaco via hipModuleLoad and launches with hipModuleLaunchKernel (HIP driver API).

Compile-Time Measurement

Use -ftime-trace for profiling

hipcc kernel.cc --cuda-device-only -c -o /dev/null \
  -Xclang -ftime-trace=trace.json

Analyze with chrome://tracing or a script:

import json
with open('trace.json') as f: data = json.load(f)
events = data.get('traceEvents', data)
inst = [(e['dur'], e['args']['detail']) for e in events
        if e.get('name') == 'InstantiateFunction' and 'dur' in e]
inst.sort(key=lambda x: -x[0])
for dur, name in inst[:20]:
    print(f"{dur/1000:8.1f}ms  {name[:100]}")

Key metrics to track

  • Function instantiations: total count and per-function time
  • Frontend vs Backend: frontend = template instantiation, backend = LLVM optimizer + codegen
  • Critical path: the single slowest template chain determines wall-clock time

Summary Table

TechniqueTypical savingsWhere applied
Separate device/host code (__HIP_DEVICE_COMPILE__ guard)~50% totalAll .cu/.hip files — always do this first
Runtime for loops in load/store/MMA30-60% frontendbuffer_view::load/store, tiled_mma_adaptor::operator()
Runtime flat_to_coords40-50% frontendlayout_to_offsets
__builtin_convertvector5-10% frontendcast for vectors >16 elements
__builtin_shufflevector3-5% frontendslice_impl for vectors
Cache constexpr members10-15% frontendlayout_load_traits, mma_a/b/c_len
Direct indexing (bypass concat_tuple)5-10% frontendunfold_x_stride, pickup_shape, flatten_tuple
-D__HIPCC_RTC__~25% per-fileCompiler flags
hipcc --genco~15% per-filePython-launched kernels
Split large TU filesBetter parallelismTest suites, multi-kernel builds

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.