Skip to content

fix(avx2): narrow flip-loop accumulator to Vec{32} to avoid LLVM-15 register exhaustion#26

Merged
zsoerenm merged 1 commit into
mainfrom
fix-25
Jul 8, 2026
Merged

fix(avx2): narrow flip-loop accumulator to Vec{32} to avoid LLVM-15 register exhaustion#26
zsoerenm merged 1 commit into
mainfrom
fix-25

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #25generate_carrier_signs! (and generate_carrier_signs_mags!) crash LLVM codegen on Julia 1.10 (LLVM 15) when SinCosLUT selects its AVX2 backend:

error: couldn't allocate output register for constraint 'x'

Root cause

The v3.3.0 branch-free sign-pack fill loop (_signs_fill_flips! / _sm_fill_flips!) keeps a Vec{64,UInt32} NCO accumulator live. On AVX2 that single value is 8 of the 16 YMM registers; with the sign-pack temporaries live at the same time, LLVM 15 cannot satisfy the 'x' (SSE/AVX) register constraints and aborts codegen. AVX-512 escapes it (4 ZMM per value, 32 registers), which is why it only surfaces on AVX2-only CI runners and looks flaky across the fleet.

Fix

Give AVX2 a dedicated _signs_fill_flips! / _sm_fill_flips! that keeps a 32-lane (half-word) accumulator resident (4 YMM) plus a broadcast half-word offset, evaluating each word in two 32-lane halves — exactly the width the AVX2 _flip2 / _sm_flip3 already slice down to. This mirrors the existing NEON 16-lane-quarter fill, and keeps the working set well within 16 YMM. The shared _flip2(::AVX2, ::Vec{64}) / _sm_flip3(::AVX2, ::Vec{64}) entry points (still used by the low-frequency run-fill straddle path) now delegate to the new half-width form, so there is a single source of truth.

No behavioural change: output is bit-identical. AVX-512, NEON, and Portable paths are untouched.

Verification

  • Full test suite passes locally (288/288).
  • The exact AVX2 fill paths were exercised directly (forcing AVX2(), which runs on this AVX-512 host since AVX2 ⊂ AVX-512) and confirmed bit-identical to a scalar reference across the issue's frequency = 1e6, sampling_frequency = 5e6 case plus full-word, partial-tail, and fw boundary edge cases.
  • The real reproducer is the Julia 1.10 / ubuntu-latest CI job (AVX2 + LLVM 15); the crash cannot be reproduced on newer LLVM or on AVX-512 hosts (per the issue, LLVM 16+ handles it).

🤖 Generated with Claude Code

…egister exhaustion

The v3.3.0 branch-free sign-pack fill loop keeps a Vec{64,UInt32} NCO
accumulator live. On the AVX2 backend that is 8 of the 16 YMM registers,
and together with the sign-pack temporaries it overflows the register
file — LLVM 15 (Julia 1.10) aborts codegen with "couldn't allocate output
register for constraint 'x'". AVX-512 escapes it (4 ZMM per value, 32
registers), which is why it only surfaces on AVX2-only CI runners.

Give AVX2 a dedicated `_signs_fill_flips!` / `_sm_fill_flips!` that keeps a
32-lane (half-word) accumulator resident (4 YMM) plus a broadcast half-word
offset, evaluating each word in two 32-lane halves — the width the AVX2
`_flip2`/`_sm_flip3` already slice to. Mirrors the existing NEON
16-lane-quarter fill. Bit-identical output, verified against a scalar
reference across the issue's frequency and full/partial-tail edge cases.

Closes #25

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.08%. Comparing base (2ac3059) to head (fad44e0).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #26      +/-   ##
==========================================
- Coverage   81.06%   76.08%   -4.98%     
==========================================
  Files           8        8              
  Lines         565      598      +33     
==========================================
- Hits          458      455       -3     
- Misses        107      143      +36     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median. Ratio = 2ac3059… / fad44e0…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means that benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

Host znver3 · AMD EPYC 7763 64-Core Processor · 4 cores · resolved backends: Int8/64 → AVX2, Int16/64 → portable, Int32/32 → portable

Time benchmarks
2ac3059 fad44e0 2ac3059… / fad44e0
carrier!/Int16/1k 1.17 μs 1.16 μs 1.01
carrier!/Int16/4k 4.6 μs 4.68 μs 0.983
carrier!/Int16/64k 74.9 μs 75.7 μs 0.99
carrier!/Int32/1k 645.0 ns 645.0 ns 1.0
carrier!/Int32/4k 2.55 μs 2.55 μs 0.997
carrier!/Int32/64k 41.6 μs 41.4 μs 1.0
carrier!/Int8/1k 119.0 ns 123.0 ns 0.971
carrier!/Int8/4k 396.0 ns 396.0 ns 1.0
carrier!/Int8/64k 5.96 μs 5.97 μs 0.999
carrier4_fill_Int8/1k 238.0 ns 238.0 ns 0.999
carrier4_fill_Int8/4k 632.0 ns 633.0 ns 0.999
carrier4_fill_Int8/64k 8.84 μs 8.75 μs 1.01
carrier_signs!/1k 72.1 ns 62.5 ns 1.15 ✅
carrier_signs!/4k 225.0 ns 215.0 ns 1.05 ✅
carrier_signs!/64k 3.33 μs 3.26 μs 1.02
carrier_signs_mags!/1k 81.8 ns 73.5 ns 1.11 ✅
carrier_signs_mags!/4k 260.0 ns 253.0 ns 1.03
carrier_signs_mags!/64k 3.83 μs 3.8 μs 1.01
fused_reduce_Int8/1k 278.0 ns 280.0 ns 0.993
fused_reduce_Int8/4k 770.0 ns 769.0 ns 1.0
fused_reduce_Int8/64k 10.6 μs 10.5 μs 1.0
sincos_lut!_Int8/1k 67.7 ns 68.7 ns 0.985
sincos_lut!_Int8/4k 223.0 ns 196.0 ns 1.14 ✅
sincos_lut!_Int8/64k 3.03 μs 3.37 μs 0.897 ⚠️
time_to_load 104.0 μs 104.0 μs 1.0
Memory benchmarks
2ac3059 fad44e0
carrier!/Int16/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int16/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int16/64k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/64k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/64k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/1k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/4k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/64k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/1k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/4k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/64k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median. Ratio = 2ac3059… / fad44e0…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means that benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

Host apple-m1 · Apple M1 (Virtual) · 3 cores · resolved backends: Int8/64 → NEON, Int16/64 → NEON, Int32/32 → portable

Time benchmarks
2ac3059 fad44e0 2ac3059… / fad44e0
carrier!/Int16/1k 281.0 ns 263.0 ns 1.07 ✅
carrier!/Int16/4k 990.0 ns 958.0 ns 1.03
carrier!/Int16/64k 14.6 μs 14.5 μs 1.01
carrier!/Int32/1k 537.0 ns 529.0 ns 1.02
carrier!/Int32/4k 2.17 μs 2.09 μs 1.04
carrier!/Int32/64k 33.2 μs 33.2 μs 1.0
carrier!/Int8/1k 80.1 ns 77.4 ns 1.03
carrier!/Int8/4k 295.0 ns 290.0 ns 1.02
carrier!/Int8/64k 4.58 μs 4.57 μs 1.0
carrier4_fill_Int8/1k 78.9 ns 73.6 ns 1.07 ✅
carrier4_fill_Int8/4k 306.0 ns 286.0 ns 1.07 ✅
carrier4_fill_Int8/64k 4.85 μs 4.68 μs 1.04
carrier_signs!/1k 78.9 ns 76.2 ns 1.04
carrier_signs!/4k 303.0 ns 283.0 ns 1.07 ✅
carrier_signs!/64k 4.58 μs 4.42 μs 1.04
carrier_signs_mags!/1k 100.0 ns 93.5 ns 1.07 ✅
carrier_signs_mags!/4k 375.0 ns 361.0 ns 1.04
carrier_signs_mags!/64k 5.86 μs 5.66 μs 1.04
fused_reduce_Int8/1k 79.8 ns 79.6 ns 1.0
fused_reduce_Int8/4k 311.0 ns 311.0 ns 1.0
fused_reduce_Int8/64k 4.88 μs 5.04 μs 0.967
sincos_lut!_Int8/1k 50.9 ns 51.7 ns 0.985
sincos_lut!_Int8/4k 197.0 ns 190.0 ns 1.04
sincos_lut!_Int8/64k 3.47 μs 3.82 μs 0.91 ⚠️
time_to_load 129.0 μs 136.0 μs 0.95 ⚠️
Memory benchmarks
2ac3059 fad44e0
carrier!/Int16/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int16/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int16/64k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int32/64k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/1k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/4k 0 allocs: 0 B 0 allocs: 0 B
carrier!/Int8/64k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
carrier4_fill_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/1k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/4k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs!/64k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/1k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/4k 0 allocs: 0 B 0 allocs: 0 B
carrier_signs_mags!/64k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
fused_reduce_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/1k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/4k 0 allocs: 0 B 0 allocs: 0 B
sincos_lut!_Int8/64k 0 allocs: 0 B 0 allocs: 0 B
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B

@zsoerenm
zsoerenm merged commit 73b6a8c into main Jul 8, 2026
11 checks passed
@zsoerenm
zsoerenm deleted the fix-25 branch July 8, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v3.3.0 generate_carrier_signs! crashes LLVM codegen on Julia 1.10 / AVX2 (couldn't allocate output register for constraint 'x')

1 participant