Skip to content

Commit 18c4c02

Browse files
authored
Merge pull request #61 from sunway513/feat/dsv4-flydsl-blockscale-moe
feat(flydsl): DSV4 FP4 MoE routing + blockscale kernel port (#37 W4.5)
2 parents a35b45a + e450e4d commit 18c4c02

446 files changed

Lines changed: 39812 additions & 13631 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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 |

.github/runner-config.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
# Used by frameworks-devops-dashboard to display GPU architecture
33
# Update this when runners are added/changed in CI workflows
44
runners:
5-
aiter-1gpu-runner:
6-
gpu_arch: MI325
5+
linux-aiter-mi300x-1:
6+
gpu_arch: MI300X
77
gpu_count: 1
88

9-
aiter-8gpu-runner:
10-
gpu_arch: MI325
9+
linux-aiter-mi300x-8:
10+
gpu_arch: MI300X
1111
gpu_count: 8
1212

1313
linux-aiter-mi355-1:
@@ -17,3 +17,11 @@ runners:
1717
linux-aiter-mi355-8:
1818
gpu_arch: MI355
1919
gpu_count: 8
20+
21+
linux-aiter-mi35x-1:
22+
gpu_arch: MI35X
23+
gpu_count: 1
24+
25+
linux-aiter-mi35x-8:
26+
gpu_arch: MI35X
27+
gpu_count: 8

0 commit comments

Comments
 (0)