fix(compat): pin SinCosLUT to ~3.2 to dodge Julia 1.10 SIMD codegen crash#196
fix(compat): pin SinCosLUT to ~3.2 to dodge Julia 1.10 SIMD codegen crash#196zsoerenm wants to merge 1 commit into
Conversation
…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>
Verified: pin makes Julia 1.10 reliably greenRan the |
Benchmark Results (minimum time) — macos-14Reporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| 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,
| 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 |
Benchmark Results (minimum time) — ubuntu-latestReporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| 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,
| 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 |
|
Upstream issue for the root cause (SinCosLUT v3.3.0 |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
|
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 |
Problem
The
Julia 1.10 - ubuntu-latestCI job intermittently (~85% of runs) crashes during test codegen: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) callSinCosLUT.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-rewritegenerate_carrier_signs!).Pkg.testre-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