Skip to content

fix(compat): pin SinCosLUT to ~3.2 to dodge Julia 1.10 SIMD codegen crash#196

Closed
zsoerenm wants to merge 1 commit into
ss/two-bit-downconvert-and-correlatefrom
fix/pin-sincoslut-julia110
Closed

fix(compat): pin SinCosLUT to ~3.2 to dodge Julia 1.10 SIMD codegen crash#196
zsoerenm wants to merge 1 commit into
ss/two-bit-downconvert-and-correlatefrom
fix/pin-sincoslut-julia110

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member

Problem

The Julia 1.10 - ubuntu-latest CI job intermittently (~85% of runs) crashes during test codegen:

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

This is an LLVM 15 register-allocation failure on 16-register AVX2 targets while compiling the Int16 / bit-wise downconvert kernels — a compiler crash, not a test failure. It affects the whole branch (reproduced on unmodified base) and blocks #194.

Root cause

downconvert_and_correlate_int16.jl (and the one-/two-bit kernels) call SinCosLUT.generate_carrier_signs!. SinCosLUT v3.3.0 (released 2026-07-08) rewrote that function with new AVX2/AVX-512 sign-pack SIMD (vpmovmskb/vpmovb2m, per its changelog: "branch-free flip loop and table-free sign packs"). That inline-asm codegen trips the LLVM 15 register allocator on Julia 1.10.

CI last passed on 2026-07-04, when the SinCosLUT = "3" range resolved to 3.2.0 (the pre-rewrite generate_carrier_signs!). Pkg.test re-resolves dependencies per run, so runs after 2026-07-08 pull 3.3.0 and crash.

Fix

Pin SinCosLUT = "~3.2" ([3.2.0, 3.3.0)) to restore the last-known-good SIMD path on Julia 1.10. Every SinCosLUT symbol this package uses exists in 3.2.x (base built and passed with 3.2.0), so nothing else changes.

This is a workaround. The proper fix is in SinCosLUT's 3.3.0 codegen (make the new sign-pack SIMD survive LLVM 15 / Julia 1.10 register allocation), or dropping Julia 1.10 from support. Lift the pin once SinCosLUT ships a 1.10-safe release.

Verification

Since the crash is AVX2-specific and this box only offers AVX-512 (32 registers, which hides it) or a non-native target (which trips an unrelated SinCosLUT fallback bug), it can't be reproduced locally — verified via CI: this PR's Julia 1.10 job should pass reliably across re-runs, vs ~85% failure on the unpinned base.

🤖 Generated with Claude Code

…rash

The Julia 1.10 CI job began crashing during test codegen with

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

while compiling the Int16 / bit-wise downconvert kernels. It is an LLVM 15
register-allocation failure on 16-register AVX2 targets, not a test failure, and
it is intermittent (~85% of runs), so it also hits the base branch and #194.

Root cause: `downconvert_and_correlate_int16.jl` (and the one-/two-bit kernels)
call `SinCosLUT.generate_carrier_signs!`, which SinCosLUT v3.3.0 (released
2026-07-08) rewrote with new AVX2/AVX-512 sign-pack SIMD (`vpmovmskb` /
`vpmovb2m`, "branch-free flip loop and table-free sign packs"). That inline-asm
codegen trips the LLVM 15 register allocator on Julia 1.10. CI last passed on
2026-07-04, when the compat range resolved SinCosLUT 3.2.0 (the pre-rewrite
`generate_carrier_signs!`); `Pkg.test` re-resolves per run, so newer runs pull
3.3.0 and crash.

Pin `SinCosLUT = "~3.2"` ([3.2.0, 3.3.0)) to restore the last-known-good SIMD
path on Julia 1.10. All symbols this package uses exist in 3.2.x, so the build
is unaffected. This is a workaround; the proper fix is in SinCosLUT's 3.3.0
codegen (or dropping Julia 1.10). Lift the pin once SinCosLUT ships a 1.10-safe
release.

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

zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Verified: pin makes Julia 1.10 reliably green

Ran the Julia 1.10 - ubuntu-latest job 3 times in a row on this pinned build — all passed. Against the unpinned base's ~85% failure rate, three consecutive passes is ~0.3% by luck, so the SinCosLUT = "~3.2" pin reliably clears the LLVM 15 register-allocation crash.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 11.9 μs 7.76 μs 4.3 μs 8.22 μs 1.53 ✅ 2.76 ✅ 1.44 ✅
GPS L1CA, 8 sats @ 40 MHz 371.0 μs 144.0 μs 63.6 μs 97.5 μs 2.58 ✅ 5.83 ✅ 3.8 ✅
GPS L1CA, 8 sats @ 5 MHz 53.1 μs 24.1 μs 15.7 μs 20.8 μs 2.2 ✅ 3.39 ✅ 2.56 ✅
Galileo E1B, 4 sats @ 25 MHz 141.0 μs 69.3 μs 2.04 ✅
dynamic taps @ 5 MHz (kernel) 6.68 μs 2.56 μs 1.53 μs 2.62 ✅ 4.36 ✅
multi-signal N=3 @ 5 MHz 10.7 μs 6.14 μs 3.93 μs 8.08 μs 1.74 ✅ 2.71 ✅ 1.32 ✅
Time benchmarks (base vs PR head)

Ratio = 81e5299… / e661bc1…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

81e5299 e661bc1 81e5299… / e661bc1
downconvert and correlate/CPU/Float32 2.5 μs 2.51 μs 0.996
downconvert and correlate/CPU/Float32 4ant 4.94 μs 5.07 μs 0.974
downconvert and correlate/CPU/Float64 2.71 μs 2.69 μs 1.01
downconvert and correlate/CPU/Int16 2.72 μs 2.62 μs 1.04
downconvert and correlate/CPU/Int16 4ant 4.88 μs 5.04 μs 0.969
downconvert and correlate/CPU/Int32 2.64 μs 2.62 μs 1.01
fused kernel/1-ant dynamic taps 2.56 μs 2.57 μs 0.996
fused kernel/1-ant static taps 2.13 μs 2.13 μs 1.0
fused kernel/4-ant dynamic taps 7.78 μs 7.81 μs 0.996
fused kernel/4-ant static taps 4.43 μs 4.44 μs 0.999
fused tuple kernel/1-ant N=2 3.28 μs 3.27 μs 1.0
fused tuple kernel/1-ant N=3 3.41 μs 3.41 μs 1.0
fused tuple kernel/2-ant N=2 6.32 μs 6.33 μs 0.999
fused tuple kernel/2-ant N=3 5.62 μs 5.79 μs 0.97
fused tuple kernel/4-ant N=2 11.6 μs 12.0 μs 0.969
fused tuple kernel/4-ant N=3 10.8 μs 10.5 μs 1.03
track/1. Float32/2K – track 2.75 μs 2.66 μs 1.03
track/1. Float32/2K – track! 2.92 μs 2.78 μs 1.05
track/2. L1 8sat/5K – track 50.5 μs 49.9 μs 1.01
track/2. L1 8sat/5K – track! 49.7 μs 49.4 μs 1.01
track/2. L1 8sat/5K – track!-threaded 51.9 μs 50.0 μs 1.04
track/2. L1 8sat/5K – track-threaded 50.3 μs 51.8 μs 0.97
track/3. E1B 4sat/25K – track 138.0 μs 136.0 μs 1.02
track/3. E1B 4sat/25K – track! 135.0 μs 136.0 μs 0.998
track/3. E1B 4sat/25K – track!-threaded 136.0 μs 136.0 μs 0.999
track/3. E1B 4sat/25K – track-threaded 136.0 μs 136.0 μs 1.0
track/4. 8L1+8E1B/25K – track 491.0 μs 490.0 μs 1.0
track/4. 8L1+8E1B/25K – track! 491.0 μs 490.0 μs 1.0
track/4. 8L1+8E1B/25K – track!-threaded 493.0 μs 491.0 μs 1.01
track/4. 8L1+8E1B/25K – track-threaded 492.0 μs 493.0 μs 0.996
track/5. multi-signal N=1/5K – track 6.56 μs 6.58 μs 0.997
track/5. multi-signal N=1/5K – track! 6.56 μs 6.72 μs 0.975
track/6. multi-signal N=2/5K – track 11.0 μs 10.6 μs 1.04
track/6. multi-signal N=2/5K – track! 10.9 μs 11.0 μs 0.996
track/7. multi-signal N=3/5K – track 11.9 μs 11.8 μs 1.01
track/7. multi-signal N=3/5K – track! 12.2 μs 12.3 μs 0.997
time_to_load 175.0 μs 304.0 μs 0.576 ⚠️
Memory benchmarks (base vs PR head)
81e5299 e661bc1
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 1 allocs: 26 B 1 allocs: 26 B
track/2. L1 8sat/5K – track 10 allocs: 4656 B 10 allocs: 4656 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4656 B 10 allocs: 4656 B
track/3. E1B 4sat/25K – track 10 allocs: 3056 B 10 allocs: 3056 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 3056 B 10 allocs: 3056 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10464 B 26 allocs: 10464 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10464 B 26 allocs: 10464 B
track/5. multi-signal N=1/5K – track 12 allocs: 1024 B 12 allocs: 1024 B
track/5. multi-signal N=1/5K – track! 3 allocs: 80 B 3 allocs: 80 B
track/6. multi-signal N=2/5K – track 15 allocs: 1568 B 15 allocs: 1568 B
track/6. multi-signal N=2/5K – track! 6 allocs: 160 B 6 allocs: 160 B
track/7. multi-signal N=3/5K – track 18 allocs: 2000 B 18 allocs: 2000 B
track/7. multi-signal N=3/5K – track! 9 allocs: 240 B 9 allocs: 240 B
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 12.2 μs 6.3 μs 5.26 μs 9.72 μs 1.94 ✅ 2.32 ✅ 1.26 ✅
GPS L1CA, 8 sats @ 40 MHz 331.0 μs 129.0 μs 76.0 μs 111.0 μs 2.56 ✅ 4.36 ✅ 2.97 ✅
GPS L1CA, 8 sats @ 5 MHz 49.1 μs 27.6 μs 20.6 μs 27.8 μs 1.78 ✅ 2.39 ✅ 1.77 ✅
Galileo E1B, 4 sats @ 25 MHz 123.0 μs 55.0 μs 2.23 ✅
dynamic taps @ 5 MHz (kernel) 5.77 μs 3.34 μs 2.04 μs 1.73 ✅ 2.83 ✅
multi-signal N=3 @ 5 MHz 9.55 μs 5.78 μs 5.21 μs 9.3 μs 1.65 ✅ 1.83 ✅ 1.03
Time benchmarks (base vs PR head)

Ratio = 81e5299… / e661bc1…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

81e5299 e661bc1 81e5299… / e661bc1
downconvert and correlate/CPU/Float32 2.34 μs 2.34 μs 1.0
downconvert and correlate/CPU/Float32 4ant 4.77 μs 4.73 μs 1.01
downconvert and correlate/CPU/Float64 2.76 μs 2.76 μs 1.0
downconvert and correlate/CPU/Int16 2.43 μs 2.41 μs 1.01
downconvert and correlate/CPU/Int16 4ant 5.19 μs 5.2 μs 0.998
downconvert and correlate/CPU/Int32 2.4 μs 2.41 μs 0.997
fused kernel/1-ant dynamic taps 2.15 μs 2.16 μs 0.999
fused kernel/1-ant static taps 1.88 μs 1.88 μs 1.0
fused kernel/4-ant dynamic taps 5.27 μs 5.28 μs 0.998
fused kernel/4-ant static taps 4.15 μs 4.14 μs 1.0
fused tuple kernel/1-ant N=2 2.35 μs 2.35 μs 1.0
fused tuple kernel/1-ant N=3 2.82 μs 2.85 μs 0.989
fused tuple kernel/2-ant N=2 3.62 μs 3.61 μs 1.0
fused tuple kernel/2-ant N=3 4.67 μs 4.54 μs 1.03
fused tuple kernel/4-ant N=2 6.01 μs 6.07 μs 0.99
fused tuple kernel/4-ant N=3 9.72 μs 9.74 μs 0.998
track/1. Float32/2K – track 2.57 μs 2.55 μs 1.01
track/1. Float32/2K – track! 2.65 μs 2.65 μs 0.998
track/2. L1 8sat/5K – track 48.5 μs 48.0 μs 1.01
track/2. L1 8sat/5K – track! 47.0 μs 47.2 μs 0.996
track/2. L1 8sat/5K – track!-threaded 47.2 μs 47.3 μs 0.999
track/2. L1 8sat/5K – track-threaded 47.8 μs 48.1 μs 0.994
track/3. E1B 4sat/25K – track 117.0 μs 120.0 μs 0.982
track/3. E1B 4sat/25K – track! 117.0 μs 119.0 μs 0.982
track/3. E1B 4sat/25K – track!-threaded 117.0 μs 119.0 μs 0.985
track/3. E1B 4sat/25K – track-threaded 117.0 μs 120.0 μs 0.983
track/4. 8L1+8E1B/25K – track 433.0 μs 438.0 μs 0.99
track/4. 8L1+8E1B/25K – track! 432.0 μs 436.0 μs 0.992
track/4. 8L1+8E1B/25K – track!-threaded 432.0 μs 436.0 μs 0.99
track/4. 8L1+8E1B/25K – track-threaded 434.0 μs 438.0 μs 0.992
track/5. multi-signal N=1/5K – track 6.23 μs 6.22 μs 1.0
track/5. multi-signal N=1/5K – track! 6.2 μs 6.23 μs 0.996
track/6. multi-signal N=2/5K – track 8.58 μs 8.56 μs 1.0
track/6. multi-signal N=2/5K – track! 8.64 μs 8.76 μs 0.986
track/7. multi-signal N=3/5K – track 10.9 μs 11.0 μs 0.993
track/7. multi-signal N=3/5K – track! 11.1 μs 11.3 μs 0.988
time_to_load 144.0 μs 101.0 μs 1.42 ✅
Memory benchmarks (base vs PR head)
81e5299 e661bc1
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 1 allocs: 26 B 1 allocs: 26 B
track/2. L1 8sat/5K – track 10 allocs: 4536 B 10 allocs: 4536 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4536 B 10 allocs: 4536 B
track/3. E1B 4sat/25K – track 10 allocs: 2808 B 10 allocs: 2808 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 2808 B 10 allocs: 2808 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10352 B 26 allocs: 10352 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10352 B 26 allocs: 10352 B
track/5. multi-signal N=1/5K – track 12 allocs: 1024 B 12 allocs: 1024 B
track/5. multi-signal N=1/5K – track! 3 allocs: 80 B 3 allocs: 80 B
track/6. multi-signal N=2/5K – track 15 allocs: 1568 B 15 allocs: 1568 B
track/6. multi-signal N=2/5K – track! 6 allocs: 160 B 6 allocs: 160 B
track/7. multi-signal N=3/5K – track 18 allocs: 2000 B 18 allocs: 2000 B
track/7. multi-signal N=3/5K – track! 9 allocs: 240 B 9 allocs: 240 B
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B

@zsoerenm

zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Upstream issue for the root cause (SinCosLUT v3.3.0 generate_carrier_signs! AVX2 codegen crash on Julia 1.10), with a minimal reproducer: JuliaGNSS/SinCosLUT.jl#25. Lift this pin once a 1.10-safe SinCosLUT release ships.

@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 97.78%. Comparing base (81e5299) to head (e661bc1).
⚠️ Report is 2 commits behind head on ss/two-bit-downconvert-and-correlate.

Additional details and impacted files
@@                           Coverage Diff                            @@
##           ss/two-bit-downconvert-and-correlate     #196      +/-   ##
========================================================================
- Coverage                                 98.05%   97.78%   -0.28%     
========================================================================
  Files                                        29       29              
  Lines                                      2933     2933              
========================================================================
- Hits                                       2876     2868       -8     
- Misses                                       57       65       +8     

☔ 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.

@zsoerenm

zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

No longer needed: SinCosLUT v3.3.1 fixes the root cause upstream ("avx2: narrow flip-loop accumulator to Vec{32} to avoid LLVM-15 register exhaustion", closes #25). Tracking's existing SinCosLUT = "3" now resolves the fixed 3.3.1, so no pin is required. Closing; verifying #194 goes green with 3.3.1.

@zsoerenm zsoerenm closed this Jul 8, 2026
@zsoerenm
zsoerenm deleted the fix/pin-sincoslut-julia110 branch July 8, 2026 19:11
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.

1 participant