Skip to content
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

fix evalpoly type instability and 0-length case #56707

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

stevengj
Copy link
Member

@stevengj stevengj commented Nov 28, 2024

A few fixes/improvements to the evalpoly function:

  1. Fixes evalpoly(x,()) gives unhelpful error. #56699: returns zero for an empty tuple or array of coefficients
  2. Fixes a type-instability in evalpoly(x, ::Vector)
  3. Adds a missing @inbounds annotation
  4. Trivial: Renames a local variable ex that was-copy-pasted from the macro version (where ex referred to an expression) but makes no sense here for the numeric sum.
  5. Removes some extraneous methods (by tightening a method signature, as suggested by @nsajko) — should still dispatch to the same implementations as before.

Probably should be backported.

@stevengj stevengj added maths Mathematical functions bugfix This change fixes an existing bug labels Nov 28, 2024
Copy link
Contributor

@nsajko nsajko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if opening this PR was polite, considering a newcomer submitted #56699 trying to fix the same issue, and the issue is marked as "good first issue". In any case, that PR is now closed, so here are some comments.

@stevengj
Copy link
Member Author

(I hadn't noticed the other PR until after I submitted this one, unfortunately.)

@stevengj
Copy link
Member Author

stevengj commented Dec 2, 2024

CI failures look unrelated.

@stevengj
Copy link
Member Author

Yay, green. Good to merge?

@stevengj stevengj added the triage This should be discussed on a triage call label Dec 31, 2024
@oscardssmith
Copy link
Member

why is triage added here?

@stevengj
Copy link
Member Author

stevengj commented Jan 1, 2025

Because it's been ready to merge for 2 months and needs someone to review it? Is there a better label?

@stevengj stevengj removed the triage This should be discussed on a triage call label Jan 1, 2025
Copy link
Member

@LilithHafner LilithHafner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the existing use of @generated on line 95 invalid because the two branches now return different types:

julia> evalpoly(1.0, (1,))
1

julia> Base.Math._evalpoly(1.0, (1,))
1.0

julia> @less evalpoly(1, (1,))
function evalpoly(x, p::Tuple)
    if @generated
        N = length(p.parameters::Core.SimpleVector)
        ex = :(p[end])
        for i in N-1:-1:1
            ex = :(muladd(x, $ex, p[$i]))
        end
        ex
    else
        _evalpoly(x, p)
    end
end

@stevengj
Copy link
Member Author

stevengj commented Jan 2, 2025

Oh ye gods, børked rebase.

@giordano
Copy link
Contributor

giordano commented Jan 2, 2025

Git history is messed up now.

@stevengj stevengj force-pushed the sgj/evalpoly_fixes branch from c6852d2 to 05cfd26 Compare January 2, 2025 15:13
@stevengj
Copy link
Member Author

stevengj commented Jan 2, 2025

Should be fixed now.

@stevengj
Copy link
Member Author

stevengj commented Jan 2, 2025

@LilithHafner, I fixed the issue with the @generated branches you noted in #56707 (review), and added a test.

@stevengj
Copy link
Member Author

stevengj commented Jan 2, 2025

Currently getting:

 Test threw exception
  Expression: #= /cache/build/tester-amdci4-13/julialang/julia-master/julia-f7b9e88614/share/julia/stdlib/v1.12/LinearAlgebra/test/matmul.jl:647 =# @evalpoly(A33, 1.0I, 1.0I) == I + A33
  MethodError: Cannot `convert` an object of type LinearAlgebra.UniformScaling{Float64} to an object of type Matrix{ComplexF64}
  The function `convert` exists, but no method is defined for this combination of argument types.

This could be fixed by replacing oftype(one(x) * p[end], p[end]) with simply one(x) * p[end]. However, in that case I'm concerned that JuliaLang/LinearAlgebra.jl#1161 will get worse — it will then start silently giving the wrong answer for matrix x and scalar coefficients, because of the weird behavior of muladd (JuliaLang/LinearAlgebra.jl#1162) in this case.

So maybe this PR is blocked by JuliaLang/LinearAlgebra.jl#1161 being fixed within LinearAlgebra.jl?

@LilithHafner
Copy link
Member

This now breaks the identity evalpoly(x, (y,)) === y. e.g:

julia> evalpoly(:sploo, (:bork,))
:bork # now throws

julia> evalpoly(1.0, (true,))::Bool
true # now 1.0

julia> evalpoly(rand(2,2), (1,))
1 # This should possibly be Float64[1 0; 0 1] now throws

According to the docstring, evalpoly(x, (y,)) should return x^0 * y which, after this PR, in the first two cases it does and in the third case it hits JuliaLang/LinearAlgebra.jl#1161.

I think that "breakage" is a bugfix.

@stevengj
Copy link
Member Author

stevengj commented Jan 3, 2025

This now breaks the identity evalpoly(x, (y,)) === y

Not sure why this identity should be expected from the definition?

I think that "breakage" is a bugfix.

I guess you agree with the new behavior?

@LilithHafner
Copy link
Member

Not sure why this identity should be expected from the definition?

If you write out a polynomial in the form ax^2+bx+c, rather than a_2x^2+a_1x^1+a_0x^0, then the degree 0 case is just a which would imply the result would be egal to a.

I guess you agree with the new behavior?

Yes, I do. Regardless of the above.

@stevengj
Copy link
Member Author

stevengj commented Jan 3, 2025

If you write out a polynomial in the form ax^2+bx+c

This is not the definition that is given in the docs for evalpoly. It is defined as $\sum_k x^{k-1} p[k]$.

(Note also that the definition right-multiplies p[k]. I'm not sure if there is someone out there who prefers left-multiplied p[k] for non-commutative algebras, e.g. matrix x and p[k]? They'll have to define their own evalpoly, I guess.)

@fingolfin fingolfin requested a review from nsajko February 14, 2025 21:34
@fingolfin
Copy link
Member

There were CI failures, e.g.

mith-C07ZM05NJYVY-0/julialang/julia-master/julia-f7b9e88614/share/julia/stdlib/v1.12/LinearAlgebra/test/matmul.jl:647
  Test threw exception
  Expression: #= /Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-grannysmith-C07ZM05NJYVY.0/build/default-grannysmith
-C07ZM05NJYVY-0/julialang/julia-master/julia-f7b9e88614/share/julia/stdlib/v1.12/LinearAlgebra/test/matmul.jl:647 =# @evalpoly(A33, 1.0I, 1.0I) == I + A33
  MethodError: Cannot `convert` an object of type LinearAlgebra.UniformScaling{Float64} to an object of type Matrix{ComplexF64}
  The function `convert` exists, but no method is defined for this combination of argument types.
  Closest candidates are:
    Matrix{T}(::LinearAlgebra.UniformScaling, !Matched::Tuple{Int64, Int64}) where T
     @ LinearAlgebra /Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-grannysmith-C07ZM05NJYVY.0/build/default-grannys
mith-C07ZM05NJYVY-0/julialang/julia-master/julia-f7b9e88614/share/julia/stdlib/v1.12/LinearAlgebra/src/uniformscaling.jl:423
    Matrix{T}(::LinearAlgebra.UniformScaling, !Matched::Integer, !Matched::Integer) where T
     @ LinearAlgebra /Users/julia/.julia/scratchspaces/a66863c6-20e8-4ff4-8a62-49f30b1f605e/agent-cache/default-grannysmith-C07ZM05NJYVY.0/build/default-grannys
mith-C07ZM05NJYVY-0/julialang/julia-master/julia-f7b9e88614/share/julia/stdlib/v1.12/LinearAlgebra/src/uniformscaling.jl:431
    convert(::Type{T}, !Matched::AbstractArray) where T<:Array
     @ Base array.jl:614

@stevengj
Copy link
Member Author

Blocked by JuliaLang/LinearAlgebra.jl#1161 as noted above.

@@ -94,7 +94,7 @@ julia> evalpoly(2, (1, 2, 3))
function evalpoly(x, p::Tuple)
if @generated
N = length(p.parameters::Core.SimpleVector)
ex = :(p[end])
ex = :(oftype(one(x) * p[end], p[end]))
Copy link
Contributor

@MasonProtter MasonProtter Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry a bit about this causing performance regressions (especially in cases where multiplication is not cheap), but I also get that it's likely important.

Ideally we'd be able to do something in the type domain here rather than an actual multiplication, but oh well i guess. This is probably less brittle, and won't regress in most cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully the compiler can optimize out one(x) * p[end] in most cases, and just compute the type, since the result is not used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if the multiplication is simple enough and free of side effects like memory allocation? If the scalar is eg a BigInt it may not be able to optimize this away?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix This change fixes an existing bug maths Mathematical functions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

evalpoly(x,()) gives unhelpful error.
7 participants