You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It seems like the following methods are causing a large fraction of SymbolicRegression.jl's precompiled methods to get invalidated, when loading it inside PySR:
Regarding the other ones that live here, maybe there is a non-breaking way to get rid of the invalidations? Usually it is because an extra method was defined that wasn't needed. And therefore can be excised without any breakage. But Sorry if you are already very familiar with this issue!
I measured this with SnoopCompile on Julia 1.12.7, loading using PythonCall; using SymbolicRegression inside @snoop_invalidations (SymbolicRegression v2.4.2, PythonCall 0.9.36). The metric is the number of unique method instances in invalidation trees whose triggering method is defined in PythonCall, so JSON.jl's share is excluded.
Which methods cause it. The biggest trees in 0.9.36 come from the mixed Py/Number comparisons, with roots like >=(::Any, ::Int64), >(::Int64, ::Any) and !=(::Any, ::Int64):
(These per-method counts overlap, so they add up to more than the unique total.) < and <= do not show up: Base already has many methods for them, so inference never assumed a concrete return type for calls like x < 1 with x::Any. Base defines >, >= and != only through generic fallbacks, so any a > 1 with an abstractly typed a got compiled against that one method, and adding a Py method breaks it.
!= cannot be removed on 0.9. The Base fallback is !=(x, y) = !(x == y), and on 0.9 x == y returns a Py. There is no !(::Py) method, so Py(1) != Py(2) would throw a MethodError. Adding !(::Py) would be worse: ! has far more call sites than !=, and not (a == b) is not the same as a != b in Python (NumPy arrays, for example). On v1, #702 makes != return Bool, and it still invalidates (788), so != remains the main cost there.
> and >= could be removed on 0.9. Base's fallbacks are >(x, y) = y < x and >=(x, y) = y <= x, which route to PythonCall's own < and <= methods. The return type stays Py, and results are identical for ordinary objects; I checked Py(3) > 2, 2 > Py(3), Py(3) >= 3, 4 >= Py(3), Py(3) > Py(1), Py(1) >= Py(3), and Py(3) > 2.5. That removes about 63% of the invalidations (6830 to 2532). One behavior change: x > y would evaluate as Python y < x, so Python tries y.__lt__ before x.__gt__. This matters only for types whose __gt__ and __lt__ disagree, which is arguably a bug in those types, but it is a change, so it is your call whether it fits a 0.9 patch release. The diff:
Prior PR: #698
Which ended up being reverted for correctness reasons. This really needs to be fixed in Base, I made an attempt at it here but it needs more work: JuliaLang/julia#62658
Which ended up being reverted for correctness reasons.
Can you say more? Is it violating some interface on the Julia side or the Python side?
If on the Python side (maybe __gt__ must be checked first or something, I don't know) then could we just handle it internally so that the Python contract is followed?
Activity
cjdoris commentedon Sep 28, 2026
I think #702 and #709 fix this, but they are into the v1 branch because it's a breaking change.
MilesCranmer commentedon Sep 28, 2026
Quick update, it seems like JSON.jl just released a patch for some of these: https://github.com/JuliaIO/JSON.jl/releases/tag/v1.10.0
Regarding the other ones that live here, maybe there is a non-breaking way to get rid of the invalidations? Usually it is because an extra method was defined that wasn't needed. And therefore can be excised without any breakage. But Sorry if you are already very familiar with this issue!
MilesCranmer commentedon Sep 28, 2026
Away from laptop; @MilesCranmerBot can you investigate the above?
MilesCranmerBot commentedon Sep 28, 2026
I measured this with SnoopCompile on Julia 1.12.7, loading
using PythonCall; using SymbolicRegressioninside@snoop_invalidations(SymbolicRegression v2.4.2, PythonCall 0.9.36). The metric is the number of unique method instances in invalidation trees whose triggering method is defined in PythonCall, so JSON.jl's share is excluded.SymbolicRegression.*>and>=methods deletedv1branch (#702 + #709)Which methods cause it. The biggest trees in 0.9.36 come from the mixed
Py/Numbercomparisons, with roots like>=(::Any, ::Int64),>(::Int64, ::Any)and!=(::Any, ::Int64):(These per-method counts overlap, so they add up to more than the unique total.)
<and<=do not show up: Base already has many methods for them, so inference never assumed a concrete return type for calls likex < 1withx::Any. Base defines>,>=and!=only through generic fallbacks, so anya > 1with an abstractly typedagot compiled against that one method, and adding aPymethod breaks it.!=cannot be removed on 0.9. The Base fallback is!=(x, y) = !(x == y), and on 0.9x == yreturns aPy. There is no!(::Py)method, soPy(1) != Py(2)would throw aMethodError. Adding!(::Py)would be worse:!has far more call sites than!=, andnot (a == b)is not the same asa != bin Python (NumPy arrays, for example). Onv1, #702 makes!=returnBool, and it still invalidates (788), so!=remains the main cost there.>and>=could be removed on 0.9. Base's fallbacks are>(x, y) = y < xand>=(x, y) = y <= x, which route to PythonCall's own<and<=methods. The return type staysPy, and results are identical for ordinary objects; I checkedPy(3) > 2,2 > Py(3),Py(3) >= 3,4 >= Py(3),Py(3) > Py(1),Py(1) >= Py(3), andPy(3) > 2.5. That removes about 63% of the invalidations (6830 to 2532). One behavior change:x > ywould evaluate as Pythony < x, so Python triesy.__lt__beforex.__gt__. This matters only for types whose__gt__and__lt__disagree, which is arguably a bug in those types, but it is a change, so it is your call whether it fits a 0.9 patch release. The diff:The remaining mixed-arithmetic methods (
&,>>,<<,xorwithNumber) cannot be removed without breaking changes, and #709 already handles them onv1.Summary.
v1, which cuts these invalidations by about 87%.>and>=methods would cut them by about 63%. The cost is the reflected-operator ordering change described above.!=has to stay on 0.9.MilesCranmer commentedon Sep 28, 2026
@MilesCranmerBot ok sounds reasonable, can you please take that diff and create a PR against master?
JamesWrigley commentedon Sep 29, 2026
Prior PR: #698
Which ended up being reverted for correctness reasons. This really needs to be fixed in Base, I made an attempt at it here but it needs more work: JuliaLang/julia#62658
MilesCranmer commentedon Sep 29, 2026
Thanks, I didn't see.
Can you say more? Is it violating some interface on the Julia side or the Python side?
If on the Python side (maybe
__gt__must be checked first or something, I don't know) then could we just handle it internally so that the Python contract is followed?