-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Thunderbolt: max — AVX2 vectorized 8x unroll reduction #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,67 @@ inline float max_v2(const float *input, std::size_t n) { | |
| return max_val; | ||
| } | ||
|
|
||
|
|
||
| // ⚡ Thunderbolt: AVX2 Vectorized Max Reduction (8x unroll) | ||
| // Target: AVX2 (Haswell+) | ||
| // Reason: `_mm256_max_ps` has a 4-cycle latency and 0.5-cycle throughput on most modern Intel uarchs. Simple vector reduction loops benefit from aggressive 8x unrolling to fully utilize all 16 YMM registers. A 16x unroll would cause register spilling because the load intrinsic requires temporary registers. An 8-way unroll perfectly covers the 4-cycle latency and shifts bottlenecks directly to L1/L2 cache bandwidth constraints without causing spills. | ||
| // Expected gain: ~1.5x-2.0x throughput over 4x unroll (max_v2) on large arrays. | ||
| inline float max_v4(const float *input, std::size_t n) { | ||
| if (n == 0) return 0.0f; | ||
|
|
||
| std::size_t i = 0; | ||
| __m256 max_v = _mm256_set1_ps(std::numeric_limits<float>::lowest()); | ||
| __m256 m0 = max_v, m1 = max_v, m2 = max_v, m3 = max_v; | ||
| __m256 m4 = max_v, m5 = max_v, m6 = max_v, m7 = max_v; | ||
|
|
||
| // Unroll 8x for 64 elements per iteration | ||
| for (; i + 63 < n; i += 64) { | ||
| m0 = _mm256_max_ps(m0, _mm256_loadu_ps(input + i)); | ||
| m1 = _mm256_max_ps(m1, _mm256_loadu_ps(input + i + 8)); | ||
| m2 = _mm256_max_ps(m2, _mm256_loadu_ps(input + i + 16)); | ||
| m3 = _mm256_max_ps(m3, _mm256_loadu_ps(input + i + 24)); | ||
| m4 = _mm256_max_ps(m4, _mm256_loadu_ps(input + i + 32)); | ||
| m5 = _mm256_max_ps(m5, _mm256_loadu_ps(input + i + 40)); | ||
| m6 = _mm256_max_ps(m6, _mm256_loadu_ps(input + i + 48)); | ||
| m7 = _mm256_max_ps(m7, _mm256_loadu_ps(input + i + 56)); | ||
| } | ||
|
|
||
| // Reduce the 8 vectors into 1 | ||
| m0 = _mm256_max_ps(m0, m4); | ||
| m1 = _mm256_max_ps(m1, m5); | ||
| m2 = _mm256_max_ps(m2, m6); | ||
| m3 = _mm256_max_ps(m3, m7); | ||
|
|
||
| m0 = _mm256_max_ps(m0, m1); | ||
| m2 = _mm256_max_ps(m2, m3); | ||
| m0 = _mm256_max_ps(m0, m2); | ||
|
|
||
| // Remainder loop for multiples of 8 elements | ||
| for (; i + 7 < n; i += 8) { | ||
| m0 = _mm256_max_ps(m0, _mm256_loadu_ps(input + i)); | ||
| } | ||
|
|
||
| // In-register horizontal reduction | ||
| __m128 lo = _mm256_castps256_ps128(m0); | ||
| __m128 hi = _mm256_extractf128_ps(m0, 1); | ||
| lo = _mm_max_ps(lo, hi); | ||
|
|
||
| __m128 shuf = _mm_shuffle_ps(lo, lo, _MM_SHUFFLE(2, 3, 0, 1)); | ||
| lo = _mm_max_ps(lo, shuf); | ||
| shuf = _mm_shuffle_ps(lo, lo, _MM_SHUFFLE(1, 0, 3, 2)); | ||
| lo = _mm_max_ps(lo, shuf); | ||
|
|
||
| float max_val = _mm_cvtss_f32(lo); | ||
|
|
||
| // Scalar epilogue | ||
| for (; i < n; ++i) { | ||
| if (input[i] > max_val) { | ||
| max_val = input[i]; | ||
| } | ||
| } | ||
| return max_val; | ||
| } | ||
|
Comment on lines
+67
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
The full reduction algorithm is duplicated from lines 133-187; only accumulator names differ. This makes the new benchmark comparison measure noise rather than a new implementation. Either implement the intended optimization or remove the duplicate variant and its wiring. 🤖 Prompt for AI Agents |
||
|
|
||
| } // namespace ml_kernels | ||
|
|
||
| // ⚡ Thunderbolt: AVX2 Vectorized Max Reduction (8x unroll) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Place C++ function braces on their own lines.
ml_kernels/include/ml_kernels/max.h#L67-L67: move themax_v4opening brace to the following line.ml_kernels/src/kernel_bench.cpp#L525-L568: move each newMaxV4Benchmarkmethod opening brace to the following line.ml_kernels/src/test_naive_ops.cpp#L185-L220: move thetest_max_v4andmainopening braces to the following line.As per coding guidelines, “Keep braces on their own lines for function bodies.”
📍 Affects 3 files
ml_kernels/include/ml_kernels/max.h#L67-L67(this comment)ml_kernels/src/kernel_bench.cpp#L525-L568ml_kernels/src/test_naive_ops.cpp#L185-L220🤖 Prompt for AI Agents
Source: Coding guidelines