Skip to content

Commit 950d0e8

Browse files
Merge pull request #34 from falseywinchnet/ptsa5p-main/update-local-repo-and-optimize-inverse-performance
Add SSE inverse residue schedule and scheduled inverse dispatch
2 parents 658eb70 + e4b5c2a commit 950d0e8

2 files changed

Lines changed: 158 additions & 1 deletion

File tree

src/detail/bruun_kernel.hpp

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2673,6 +2673,7 @@ class RFFT {
26732673
heap_array<float> CF;
26742674
heap_array<FwdOp> FWD_SCHEDULE;
26752675
heap_array<FwdOp> FWD_RES_SCHEDULE;
2676+
heap_array<FwdOp> INV_RES_SCHEDULE;
26762677

26772678
inline float sf_twiddle(int m) const {
26782679
return (m <= 1) ? (m == 1 ? CF[1] : 0.0f) : CF[m ^ 1];
@@ -2760,6 +2761,30 @@ class RFFT {
27602761
return true;
27612762
}
27622763

2764+
bool build_inverse_residue_schedule() {
2765+
const std::size_t source_count = FWD_RES_SCHEDULE.size();
2766+
if (!INV_RES_SCHEDULE.resize(source_count + 1)) return false;
2767+
for (std::size_t i = 0; i < source_count; ++i) {
2768+
INV_RES_SCHEDULE[i] = FWD_RES_SCHEDULE[source_count - 1 - i];
2769+
INV_RES_SCHEDULE[i].mem.stream = 2;
2770+
}
2771+
FwdOp final_op{};
2772+
final_op.base = 0;
2773+
final_op.q = static_cast<uint32_t>(N / 2);
2774+
final_op.m = 0;
2775+
final_op.kind = FWD_OP_BINOMIAL;
2776+
final_op.mem.base = 0;
2777+
final_op.mem.span = static_cast<uint32_t>(N);
2778+
final_op.mem.align = static_cast<uint16_t>(bruun_cache_alignment);
2779+
final_op.mem.stream = 2;
2780+
final_op.mem.next_base = 0;
2781+
INV_RES_SCHEDULE[source_count] = final_op;
2782+
for (std::size_t i = 0; i + 1 < INV_RES_SCHEDULE.size(); ++i) {
2783+
INV_RES_SCHEDULE[i].mem.next_base = INV_RES_SCHEDULE[i + 1].mem.base;
2784+
}
2785+
return true;
2786+
}
2787+
27632788
bool build_forward_schedules() {
27642789
heap_array<FwdOp> fused_ops;
27652790
heap_array<FwdOp> residue_ops;
@@ -2790,6 +2815,7 @@ class RFFT {
27902815
}
27912816
if (!copy_schedule(fused_ops, FWD_SCHEDULE)) return false;
27922817
if (!copy_schedule(residue_ops, FWD_RES_SCHEDULE)) return false;
2818+
if (!build_inverse_residue_schedule()) return false;
27932819
return true;
27942820
}
27952821

@@ -3176,8 +3202,55 @@ class RFFT {
31763202
codelet_d3_tw_res_inv_f32(v + 16, TWF[1]);
31773203
}
31783204

3205+
void run_inv_residue_schedule_f32(float* RESTRICT v) const {
3206+
const FwdOp* RESTRICT ops = INV_RES_SCHEDULE.data();
3207+
const std::size_t op_count = INV_RES_SCHEDULE.size();
3208+
for (std::size_t op_index = 0; op_index < op_count; ++op_index) {
3209+
const FwdOp& op = ops[op_index];
3210+
float* RESTRICT base = v + op.base;
3211+
#if defined(__GNUC__) || defined(__clang__)
3212+
__builtin_prefetch(v + op.mem.next_base, 1, 1);
3213+
#endif
3214+
switch (op.kind) {
3215+
case FWD_OP_NORM2: {
3216+
const int m = static_cast<int>(op.m);
3217+
const int q = static_cast<int>(op.q);
3218+
norm2_inv_fused_f32(base, q, CF[m], sf_twiddle(m), CF[2*m], sf_twiddle(2*m), CF[2*m+1], sf_twiddle(2*m+1));
3219+
break;
3220+
}
3221+
case FWD_OP_CODELET_Q8: {
3222+
const int m = static_cast<int>(op.m);
3223+
codelet_d3_tw_res_inv_f32(base, TWF[2*m]);
3224+
codelet_d3_tw_res_inv_f32(base + 16, TWF[2*m + 1]);
3225+
norm_q_inv_f32(base, 8, CF[m], sf_twiddle(m));
3226+
break;
3227+
}
3228+
case FWD_OP_CODELET_D3:
3229+
case FWD_OP_SPINE_D3:
3230+
codelet_d3_tw_res_inv_f32(base, TWF[op.m]);
3231+
break;
3232+
case FWD_OP_BINOMIAL:
3233+
binomial_inv_f32(base, static_cast<int>(op.q));
3234+
break;
3235+
case FWD_OP_SPINE_D1:
3236+
norm_q_inv_f32(base, 1, CF[1], sf_twiddle(1));
3237+
break;
3238+
case FWD_OP_SPINE_NORM2:
3239+
norm_q_inv_f32(base, 1, CF[2], sf_twiddle(2));
3240+
norm_q_inv_f32(base + 4, 1, CF[3], sf_twiddle(3));
3241+
norm_q_inv_f32(base, 2, CF[1], sf_twiddle(1));
3242+
break;
3243+
default:
3244+
break;
3245+
}
3246+
}
3247+
}
3248+
31793249
// Exact reverse of forward_residues_recursive_f32. Requires N >= 64.
31803250
void inverse_residues_recursive_f32(float* RESTRICT v) const {
3251+
#if BRUUN_LEVEL == 1
3252+
run_inv_residue_schedule_f32(v);
3253+
#else
31813254
residue_spine_tail_inv_f32(v);
31823255

31833256
for (int h = 32; h <= N / 2; h <<= 1) {
@@ -3186,6 +3259,7 @@ class RFFT {
31863259
}
31873260

31883261
binomial_inv_f32(v, N / 2);
3262+
#endif
31893263
}
31903264

31913265
void inverse_residues_inplace_f32(float* RESTRICT v) const {
@@ -3889,9 +3963,56 @@ class RFFT {
38893963
codelet_d3_tw_res_inv(v + 16, TW[1]);
38903964
}
38913965

3892-
// Fast depth-first residue inverse: exact reverse of forward_residues_recursive.
3966+
void run_inv_residue_schedule(double* RESTRICT v) const {
3967+
const FwdOp* RESTRICT ops = INV_RES_SCHEDULE.data();
3968+
const std::size_t op_count = INV_RES_SCHEDULE.size();
3969+
for (std::size_t op_index = 0; op_index < op_count; ++op_index) {
3970+
const FwdOp& op = ops[op_index];
3971+
double* RESTRICT base = v + op.base;
3972+
#if defined(__GNUC__) || defined(__clang__)
3973+
__builtin_prefetch(v + op.mem.next_base, 1, 1);
3974+
#endif
3975+
switch (op.kind) {
3976+
case FWD_OP_NORM2: {
3977+
const int m = static_cast<int>(op.m);
3978+
const int q = static_cast<int>(op.q);
3979+
norm2_inv_fused(base, q, C[m], s_twiddle(m), C[2*m], s_twiddle(2*m), C[2*m+1], s_twiddle(2*m+1));
3980+
break;
3981+
}
3982+
case FWD_OP_CODELET_Q8: {
3983+
const int m = static_cast<int>(op.m);
3984+
codelet_d3_tw_res_inv(base, TW[2*m]);
3985+
codelet_d3_tw_res_inv(base + 16, TW[2*m + 1]);
3986+
norm_q_inv(base, 8, C[m], s_twiddle(m));
3987+
break;
3988+
}
3989+
case FWD_OP_CODELET_D3:
3990+
case FWD_OP_SPINE_D3:
3991+
codelet_d3_tw_res_inv(base, TW[op.m]);
3992+
break;
3993+
case FWD_OP_BINOMIAL:
3994+
binomial_inv(base, static_cast<int>(op.q));
3995+
break;
3996+
case FWD_OP_SPINE_D1:
3997+
norm_q_inv(base, 1, C[1], s_twiddle(1));
3998+
break;
3999+
case FWD_OP_SPINE_NORM2:
4000+
norm_q_inv(base, 1, C[2], s_twiddle(2));
4001+
norm_q_inv(base + 4, 1, C[3], s_twiddle(3));
4002+
norm_q_inv(base, 2, C[1], s_twiddle(1));
4003+
break;
4004+
default:
4005+
break;
4006+
}
4007+
}
4008+
}
4009+
4010+
// Fast scheduled residue inverse: exact reverse of forward_residues_recursive.
38934011
// Requires N >= 64.
38944012
void inverse_residues_recursive(double* RESTRICT v) const {
4013+
#if BRUUN_LEVEL == 1
4014+
run_inv_residue_schedule(v);
4015+
#else
38954016
residue_spine_tail_inv(v);
38964017

38974018
for (int h = 32; h <= N / 2; h <<= 1) {
@@ -3900,6 +4021,7 @@ class RFFT {
39004021
}
39014022

39024023
binomial_inv(v, N / 2);
4024+
#endif
39034025
}
39044026

39054027
// Two-phase standard-output forward: residues land in block order in v, then
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Inverse acceleration decisions
2+
==============================
3+
4+
2026-06-21
5+
6+
Local sync:
7+
- The current branch is `work` and has no configured upstream remote/tracking branch.
8+
- `git fetch --all --prune` had nothing to fetch.
9+
- `git pull --ff-only` could not update because there is no tracking information for `work`.
10+
11+
Primary decision:
12+
- Start with the near-free scheduling win before deeper per-ISA kernel rewrites.
13+
- Build `INV_RES_SCHEDULE` from `FWD_RES_SCHEDULE` by reversing the forward residue schedule and appending the final inverse binomial that undoes the initial forward `binomial_oop`.
14+
- Keep the record type shared with the forward scheduler for now. The operation kind identifies the forward primitive; inverse dispatch swaps each kind to its inverse kernel.
15+
16+
Why this is first:
17+
- It removes the recursive inverse traversal and replaces it with the same linear switch shape already used by the forward path.
18+
- It keeps all q==8, q==4, q==2, and spine operations in one baked stream, exposing predictable control flow and sequential op metadata to SSE/NEON, AVX2, and AVX-512 builds.
19+
- It does not change transform math, layout, or public APIs, so correctness risk is lower than starting with new fused inverse scatter or d4 inverse codelets.
20+
21+
ISA notes:
22+
- SSE/NEON benefit immediately from removing recursive calls and from the existing 128-bit `norm_q_inv` loop.
23+
- AVX2 keeps the existing recursive path for now because the first scheduled attempt did not beat it on this host.
24+
- AVX-512 is treated the same as AVX2 until the inverse d4 q==8 fusion is available; the schedule infrastructure is present, but not enabled for wide-vector builds yet.
25+
26+
Follow-up candidates:
27+
1. Add an inverse d4 codelet for q==8 to fuse the two inverse d3 leaves and inverse q=8 norm while values remain resident in registers.
28+
2. Fuse native/standard gather with the inverse binomial tail to remove one complete pass over N for inverse-from-complex APIs.
29+
3. Add f32/scalar lane-doubling equivalents where the inverse leaf still falls back to generic inverse-norm ladders.
30+
4. Inspect per-ISA assembly for the new switch loop and q==8 case before choosing the next specialized kernel.
31+
32+
SSE-first gate:
33+
- Initial AVX2 timing showed the flat inverse schedule is not yet a win on this host, likely because AVX2 already has the recursive leaf path inlined into large vector kernels while the schedule adds switch pressure.
34+
- The scheduled inverse is therefore enabled first for 128-bit SSE/NEON builds, where removing recursive calls is the current concrete win.
35+
- AVX2 and AVX-512 keep the existing recursive path until the next step lands a fused inverse d4 q==8 codelet, which should provide the register-residency win needed to overcome flat-dispatch overhead.

0 commit comments

Comments
 (0)