From e661a72849deb5746cbdc5c14b803516faf2e8da Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 19 Jun 2026 05:17:43 -0400 Subject: [PATCH] Fix master CI: expv zero-input NaN, JET-on-1.12 QA, GPU-in-All MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent master-CI failures on the grouped-tests workflow: 1. Core (NaN == 0.0 at basictests.jl:307, flaky across OS/version). The real `expv!(w, t::Real, Ks)` method lacked the `iszero(beta)` guard that the complex method already has. For a zero input vector `firststep!` skips initializing the Krylov basis V (it only fills it when beta != 0), so `lmul!(beta, mul!(w, V, expHe))` computes `0 * `, which is NaN whenever V holds garbage. Add the same early-return guard, making expv of a zero vector exactly zero (matching the complex method). Verified: full Core suite now passes on Julia 1.10 and 1.12 (was reliably NaN on 1.10). 2. QA (6 JET failures on the Julia "1" = 1.12 channel; lts/1.10 was green). On 1.12 JET traces into LinearAlgebra/Base internals (`norm(::Vector)` -> `norm_recursive_check` -> `iterate(::Nothing)`, and the broadcast `unalias`/`copyto_unaliased!` path over `Adjoint{T, Union{}}`) and reports artifacts there that this package does not control. Scope the QA `report_call`s to `target_modules = (ExponentialUtilities,)` — the standard JET-as-QA configuration — which keeps full coverage of this package's own code. That scoping surfaced two genuine `may be undefined` findings, fixed here so the scoped analysis is clean: `si` in `exponential!` and `order`/`kest` in `kiops` are now unconditionally initialized before use. Verified: QA passes 17/17 on Julia 1.10 and 1.12. 3. Core (windows, all versions: "CUDA driver not functional"). On Windows the Core job runs the run_tests "All" aggregate, which pulled in the GPU group and `using CUDA` errored on the non-GPU runner. Mark the GPU group `in_all = false` so it only runs under an explicit GROUP=GPU on the self-hosted CUDA runner. Verified locally: GROUP=All now runs only Core/basictests.jl, never GPU/gputests.jl. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- src/exp_baseexp.jl | 1 + src/kiops.jl | 7 ++++++- src/krylov_phiv.jl | 8 ++++++++ test/qa/qa.jl | 35 ++++++++++++++++++++++++++++------- test/test_groups.toml | 6 +++++- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/exp_baseexp.jl b/src/exp_baseexp.jl index 6ecc9131..6069d93f 100644 --- a/src/exp_baseexp.jl +++ b/src/exp_baseexp.jl @@ -83,6 +83,7 @@ function exponential!( LAPACK.gesv!(temp, X) else s = log2(nA / 5.4) # power of 2 later reversed by squaring + si = 0 # always defined so the s > 0 squaring loop is type-stable if s > 0 si = ceil(Int, s) A ./= convert(T, 2^si) diff --git a/src/kiops.jl b/src/kiops.jl index 9510cfa7..acf3ac98 100644 --- a/src/kiops.jl +++ b/src/kiops.jl @@ -117,10 +117,15 @@ function kiops( omega = NaN orderold = true kestold = true + # `order`/`kest` carry their previous value across iterations (the `orderold`/ + # `kestold` flags select "reuse"); seed them with the first-iteration defaults so + # they are always defined before use rather than only conditionally assigned. + order = 0.0 + kest = 2 l = 1 - local beta, kest + local beta while tau_now < tau_end oldj = Ks.m arnoldi!( diff --git a/src/krylov_phiv.jl b/src/krylov_phiv.jl index a0e521b1..496e330e 100644 --- a/src/krylov_phiv.jl +++ b/src/krylov_phiv.jl @@ -87,6 +87,14 @@ function expv!( ) where {Tw, T, U} m, beta, V, H = Ks.m, Ks.beta, getV(Ks), getH(Ks) @assert length(w) == size(V, 1) "Dimension mismatch" + if iszero(beta) + # Zero input: the Krylov basis V was never initialized (firststep! skips + # the fill when beta == 0), so `beta * V * expHe` would be `0 * garbage`, + # which is NaN whenever V holds uninitialized memory. The result is exactly + # zero, matching the complex `expv!` method's guard below. + w .= false + return w + end if isnothing(cache) cache = Matrix{U}(undef, m, m) elseif isa(cache, ExpvCache) diff --git a/test/qa/qa.jl b/test/qa/qa.jl index dcc58f5c..e096d723 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -14,39 +14,60 @@ using ExponentialUtilities, Aqua, JET, Test Aqua.test_undefined_exports(ExponentialUtilities) end +# Analyze only ExponentialUtilities' own code. Without this, JET on Julia 1.12 traces +# into LinearAlgebra/Base internals (e.g. `norm(::Vector)` -> `norm_recursive_check`, +# and the broadcast `unalias`/`copyto_unaliased!` path over `Adjoint{T, Union{}}`) and +# reports abstract-interpretation artifacts there that are not under this package's +# control. Scoping to `ExponentialUtilities` keeps full coverage of this package's code +# (it still flags real `may be undefined` findings here) without asserting that all of +# the stdlib is JET-clean. +const JET_TARGET = (ExponentialUtilities,) + @testset "JET static analysis" begin @testset "expv" begin - rep = JET.report_call(expv, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + expv, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "arnoldi" begin - rep = JET.report_call(arnoldi, (Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + arnoldi, (Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "phi" begin - rep = JET.report_call(phi, (Matrix{Float64}, Int)) + rep = JET.report_call(phi, (Matrix{Float64}, Int); target_modules = JET_TARGET) @test length(JET.get_reports(rep)) == 0 end @testset "exponential!" begin - rep = JET.report_call(ExponentialUtilities.exponential!, (Matrix{Float64},)) + rep = JET.report_call( + ExponentialUtilities.exponential!, (Matrix{Float64},); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "phiv" begin - rep = JET.report_call(phiv, (Float64, Matrix{Float64}, Vector{Float64}, Int)) + rep = JET.report_call( + phiv, (Float64, Matrix{Float64}, Vector{Float64}, Int); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "kiops" begin - rep = JET.report_call(kiops, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + kiops, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "expv_timestep" begin - rep = JET.report_call(expv_timestep, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + expv_timestep, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end end diff --git a/test/test_groups.toml b/test/test_groups.toml index 9fbb711a..3856fb2a 100644 --- a/test/test_groups.toml +++ b/test/test_groups.toml @@ -6,7 +6,10 @@ # QA runs the metadata/static-analysis checks (Aqua + JET) in the isolated # test/qa environment. # GPU runs the CUDA tests in the isolated test/gpu environment on a self-hosted -# GPU runner (matching the pre-conversion GPU.yml workflow). +# GPU runner (matching the pre-conversion GPU.yml workflow). It is `in_all = false` +# so it only ever runs under an explicit GROUP=GPU on the CUDA-equipped runner and +# is never pulled into the "All" aggregate (which a non-GPU job can fall back to), +# where `using CUDA` errors with "CUDA driver not functional". [Core] versions = ["lts", "1", "pre"] @@ -19,3 +22,4 @@ versions = ["lts", "1"] versions = ["1"] runner = ["self-hosted", "Linux", "X64", "gpu"] timeout = 60 +in_all = false