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

Segfault on DynamicQuantities range #56610

Open
alhirzel opened this issue Nov 19, 2024 · 22 comments
Open

Segfault on DynamicQuantities range #56610

alhirzel opened this issue Nov 19, 2024 · 22 comments
Labels
bug Indicates an unexpected problem or unintended behavior ranges Everything AbstractRange

Comments

@alhirzel
Copy link
Contributor

Unexpected segfault:

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.11.1 (2024-10-16)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> using DynamicQuantities

julia> collect(1u"inch":0.25u"inch":4u"inch")

[777626] signal 11 (1): Segmentation fault
in expression starting at REPL[2]:1
setindex! at ./array.jl:976 [inlined]
Array at ./range.jl:1375
Array at ./boot.jl:605 [inlined]
collect at ./range.jl:1380
unknown function (ip: 0x72f99c120dc2)
jl_apply at /cache/build/builder-demeter6-6/julialang/julia-master/src/julia.h:2157 [inlined]
do_call at /cache/build/builder-demeter6-6/julialang/julia-master/src/interpreter.c:126
eval_value at /cache/build/builder-demeter6-6/julialang/julia-master/src/interpreter.c:223
eval_stmt_value at /cache/build/builder-demeter6-6/julialang/julia-master/src/interpreter.c:174 [inlined]
eval_body at /cache/build/builder-demeter6-6/julialang/julia-master/src/interpreter.c:663
jl_interpret_toplevel_thunk at /cache/build/builder-demeter6-6/julialang/julia-master/src/interpreter.c:821
jl_toplevel_eval_flex at /cache/build/builder-demeter6-6/julialang/julia-master/src/toplevel.c:943
jl_toplevel_eval_flex at /cache/build/builder-demeter6-6/julialang/julia-master/src/toplevel.c:886
ijl_toplevel_eval_in at /cache/build/builder-demeter6-6/julialang/julia-master/src/toplevel.c:994
eval at ./boot.jl:430 [inlined]
eval_user_input at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:245
repl_backend_loop at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:342
#start_repl_backend#59 at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:327
start_repl_backend at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:324
#run_repl#72 at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:483
run_repl at /cache/build/builder-demeter6-6/julialang/julia-master/usr/share/julia/stdlib/v1.11/REPL/src/REPL.jl:469
jfptr_run_repl_10088 at /usr/share/julia/compiled/v1.11/REPL/u0gqU_GYsA8.so (unknown line)
#1139 at ./client.jl:446
jfptr_YY.1139_14649 at /usr/share/julia/compiled/v1.11/REPL/u0gqU_GYsA8.so (unknown line)
jl_apply at /cache/build/builder-demeter6-6/julialang/julia-master/src/julia.h:2157 [inlined]
jl_f__call_latest at /cache/build/builder-demeter6-6/julialang/julia-master/src/builtins.c:875
#invokelatest#2 at ./essentials.jl:1055 [inlined]
invokelatest at ./essentials.jl:1052 [inlined]
run_main_repl at ./client.jl:430
repl_main at ./client.jl:567 [inlined]
_start at ./client.jl:541
jfptr__start_72144.1 at /usr/lib/julia/sys.so (unknown line)
jl_apply at /cache/build/builder-demeter6-6/julialang/julia-master/src/julia.h:2157 [inlined]
true_main at /cache/build/builder-demeter6-6/julialang/julia-master/src/jlapi.c:900
jl_repl_entrypoint at /cache/build/builder-demeter6-6/julialang/julia-master/src/jlapi.c:1059
main at julia (unknown line)
unknown function (ip: 0x72f9bfb25e07)
__libc_start_main at /usr/bin/../lib/libc.so.6 (unknown line)
unknown function (ip: 0x4010b8)
Allocations: 3644119 (Pool: 3643983; Big: 136); GC: 5
@giordano
Copy link
Contributor

% julia --project=/tmp --check-bounds=yes -q
julia> using DynamicQuantities

julia> collect(1u"inch":0.25u"inch":4u"inch")
ERROR: BoundsError: attempt to access 12-element Vector{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}} at index [13]
Stacktrace:
 [1] throw_boundserror(A::Vector{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}}, I::Tuple{Int64})
   @ Base ./essentials.jl:14
 [2] setindex!
   @ ./array.jl:975 [inlined]
 [3] Vector{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}}(r::StepRange{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}, Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}})
   @ Base ./range.jl:1375
 [4] Array
   @ ./boot.jl:605 [inlined]
 [5] collect(r::StepRange{Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}, Quantity{Float64, Dimensions{FixedRational{Int32, 25200}}}})
   @ Base ./range.jl:1380
 [6] top-level scope
   @ REPL[2]:1

It's an out-of-bound issue, there's probably an unsafe @inbounds somewhere.

@oscardssmith
Copy link
Member

I'm guessing that this is a Floating point range issue (i.e. we are collecting the range and it is shorter than we thought it would be do to weird rounding).

@christiangnrd
Copy link
Contributor

DynamicQuantities is somehow creating a StepRange instead of StepRangeLen and StepRange is not supposed to use floats.

@oscardssmith
Copy link
Member

looks like that is the default behavior. In range.jl there's

(:)(start::T, step, stop::T) where {T} = _colon(start, step, stop)
(:)(start::T, step, stop::T) where {T<:Real} = _colon(start, step, stop)
# without the second method above, the first method above is ambiguous with
# (:)(start::A, step, stop::C) where {A<:Real,C<:Real}
function _colon(start::T, step, stop::T) where T
    T′ = typeof(start+zero(step))
    StepRange(convert(T′,start), step, convert(T′,stop))
end

while the code that creates StepRangeLen is specifically for function (:)(start::T, step::T, stop::T) where T<:IEEEFloat. @StefanKarpinski what should we do to make sure we generate the appropriate range type?

@christiangnrd
Copy link
Contributor

christiangnrd commented Nov 19, 2024

Should DynamicQuantities maybe borrow some code from Unitful.jl?

Edit: Probably not. Seems like it's all pirated #56610 (comment)

@mcabbott
Copy link
Contributor

mcabbott commented Nov 19, 2024

julia> x = 1:0.25:4;
julia> xu = 1u"inch":0.25u"inch":4u"inch";
julia> xval = (1u"inch".value):(0.25u"inch".value):(4u"inch".value)
0.025400000000000002:0.006350000000000001:0.10160000000000001

julia> length(x), length(xu), length(xval)
(13, 12, 13)

julia> @less Vector{eltype(xu)}(xu)

This iterates the range and indexes @inbounds:

function Array{T,1}(r::AbstractRange{T}) where {T}
    a = Vector{T}(undef, length(r))
    i = 1
    for x in r
        @inbounds a[i] = x
        i += 1
    end
    return a
end

And the iteration for StepRange assumes that prev + step(r) will exactly hit last(r):

iterate(r::OrdinalRange) = isempty(r) ? nothing : (first(r), first(r))

function iterate(r::OrdinalRange{T}, i) where {T}
    @inline
    i == last(r) && return nothing
    next = convert(T, i + step(r))
    (next, next)
end

@oscardssmith
Copy link
Member

It looks like we can solve this by defining

import Base: :
function (:)(start::T, step::T, stop::T) where {T<:Quantity}
    range(start, stop, length=length(start.value:step.value:stop.value))
end

but it is definitely a bug in Base that this could happen in the first place without DynamicQuantities doing anything wrong.

@mcabbott
Copy link
Contributor

mcabbott commented Nov 19, 2024

Yes, seems like Base should not create a StepRange unless it's certain that the types obey its rules.

Nicest path for DynamicQuantities to encourage is probably something like this, so that floating point magic happens on the numbers as written, before conversion, not after:

julia> (1:0.25:4)u"inch"
ERROR: Cannot create an additive identity from `Type{<:Quantity}`, as the dimensions are unknown. Please use `zero(::Quantity)` instead.

@alhirzel
Copy link
Contributor Author

alhirzel commented Nov 19, 2024

Seriously impressed at everyone's diligence as applied to this imperfectly-reported bug. Bravo!

I was suspicious that this error should concern Julia core/base because of the accessibility of the segfault. I appreciate everyone's validation on this point. Separately, should I open a report over at DynamicQuantities.jl with any suggestion(s)? What I have seen suggested so far seems like documentation-type fixes. If instead the goal is to have the above code work as-written, I wonder what can be done? One solution would be to override the colon and double-colon operators to "lower" coloning to floating point types underlying a Quantity (as partially suggested by @oscardssmith), then to have a slim wrapper type of some kind until the sequence must be realized. Do I understand? I have not looked at how Unitful.jl does it (as suggested by @christiangnrd) but can look and propose to DynamicQuantities.jl if this was another intended solution to make the original code work:

julia> collect(1u"inch":0.25u"inch":4u"inch")

Note that this originally arose in a situation where these values were variables with different units, so I felt motivation to report this specific case and feel motivated to help it work.

@oscardssmith
Copy link
Member

oscardssmith commented Nov 19, 2024

looks like Unitful solves this by just pirating all of the Base range code https://github.com/PainterQubits/Unitful.jl/blob/b53bc81daea02c55e625c337962087e0f3af63d9/src/range.jl.. That's definitely not a great solution.

@MilesCranmer
Copy link
Member

MilesCranmer commented Nov 19, 2024

I think one fix to this issue would be if the stdlib had:

- TwicePrecision{T}(x::T) where {T} = TwicePrecision{T}(x, zero(T))
+ TwicePrecision{T}(x::T) where {T} = TwicePrecision{T}(x, zero(x))

Going to put this in DQ for AbstractQuantity at least

@MilesCranmer
Copy link
Member

MilesCranmer commented Nov 19, 2024

If I define:

function Base.:(:)(start::T, step::T, stop::T) where {T<:$type}
    range(start, stop, length=length(start.value:step.value:stop.value))
end

then it will still break for mixed units/integers (which are valid – u"1" is a dimensionless Quantity object) –

julia> collect(1:0.25u"1":4u"1")
ERROR: MethodError: no method matching (::Colon)(::Int64, ::Quantity{Float64, Dimensions{FixedRational{…}}}, ::Quantity{Float64, Dimensions{FixedRational{…}}})
The function `Colon()` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  (::Colon)(::T, ::T, ::T) where T<:AbstractQuantity
   @ DynamicQuantities ~/PermaDocuments/SymbolicRegressionMonorepo/DynamicQuantities.jl/src/math.jl:63
  (::Colon)(::T, ::Any, ::T) where T<:Real
   @ Base range.jl:50
  (::Colon)(::A, ::Any, ::C) where {A<:Real, C<:Real}
   @ Base range.jl:10
  ...

I guess I would need to loop over types for all start, step, and stop, to complete it?

@oscardssmith
Copy link
Member

ah the dispatch hiercarchy here is unfortunate. ideally, there would be a fallback method here that would promote...

@giordano
Copy link
Contributor

looks like Unitful solves this by just pirating all of the Base range code https://github.com/PainterQubits/Unitful.jl/blob/b53bc81daea02c55e625c337962087e0f3af63d9/src/range.jl.. That's definitely not a great solution.

I may have missed it, but which method is exactly type piracy? I see a bunch of non-public functions being extended, and that's not ideal (probably an indication it couldn't be done in a better way), but as far as I can see at a quick glance all signatures include types defined by the package, didn't looks to me full blown type piracy.

@bvdmitri
Copy link
Contributor

From @mcabbott message I understand that this is a problematic function.

function Array{T,1}(r::AbstractRange{T}) where {T}
    a = Vector{T}(undef, length(r))
    i = 1
    for x in r
        @inbounds a[i] = x
        i += 1
    end
    return a
end

This may sound naive, but why no one considers simply removing the problematic @inbounds?
There is no reason to assume that an arbitrary AbstractRange implements length correctly so this generic code shouldn't really use @inbounds in the first place. Specialised versions of specific AbstractRanges e.g. UnitRange may still use the @inbounds

@KristofferC
Copy link
Member

Julia also typically claims that there is nothing really special about the Base types but having it only use "fast" versions of its algorithms on Base types goes a but against that.

@mcabbott
Copy link
Contributor

There is no reason to assume that an arbitrary AbstractRange implements length correctly

I do think an object lying about its length is illegal. But that's not the problem here, it's that iteration of a StepRange <: OrdinalRange relies on the (documented) assumption that adding step repeatedly will exactly hit the endpoint. Which is violated if you ever make a StepRange of things with floating point error.

Certainly the fallback path of a:b:c should not produce a StepRange when it's not sure whether the resulting step is kosher. It should probably fall back to StepRangeLen.

Possibly the iteration should be made more robust by checking >= instead of ==, but I don't think that will eliminate all such mistakes.

@alhirzel
Copy link
Contributor Author

alhirzel commented Nov 20, 2024

Is one of the underlying questions here how to exactly predict the length of a sequence a:b:c? If so, are there any guidelines provided to type authors that include best practices to satisfy this? At the very core of this issue seems to be the bad behavior of floating point numbers. Perhaps couching some general language-level guidance on the intent for floating-point ranges could help with this. For instance, in the case of a "near miss" due to floating point error, is it semantically expected that the "predictable" length of the list takes precedence or the stopping value takes precedence.

Heaven forbid there ever be a geometric series, or a sufficiently small step that someone tries to repeat history, or an additive type that accumulates undue floating point error under the hood; unfortunately, heaven will not save us from these eventualities, only documentation might.

@mcabbott
Copy link
Contributor

Things like 0:0.1:10 perform some black magic to almost always do what you mean. But the general rule is, if in doubt, to stop short, like 2:3:10 == [2,5,8]. That's why the range with units here has length 12 not 13, which seems OK. The important thing is that, after you've constructed this range (keeping the given step and adjusting the given stop), there should be no further surprises.

@oscardssmith oscardssmith added bug Indicates an unexpected problem or unintended behavior ranges Everything AbstractRange labels Nov 20, 2024
@MilesCranmer
Copy link
Member

MilesCranmer commented Nov 20, 2024

Am implementing @oscardssmith's suggested workaround in DQ with the following code:

# Needed for multiplying range with quantities:
Base.TwicePrecision{T}(x::T) where {T<:AbstractQuantity} = Base.TwicePrecision{typeof(x)}(x, zero(x))

# Needed for robust ranges from quantities:
for T1 in (AbstractQuantity{<:Real}, Real),
    T2 in (AbstractQuantity{<:Real}, Real),
    T3 in (AbstractQuantity{<:Real}, Real)

    T1 === T2 === T3 === Real && continue

    @eval function Base.:(:)(start::$T1, step::$T2, stop::$T3)
        return range(start, stop, length=length(ustrip(start):ustrip(step):ustrip(stop)))
    end
end

which will force a StepRangeLen.

One quirk is that cases like range(1, 5, length=1) (i.e., start+step > stop) will return an error (see below) so I will need to catch length==1 separately.

julia> range(1, 5, length=1)
ERROR: ArgumentError: range(1.0, stop=5.0, length=1): endpoints differ
Stacktrace:
 [1] _linspace1(::Type{Float64}, start::Float64, stop::Float64, len::Int64)
   @ Base ./twiceprecision.jl:737
 [2] _linspace(::Type{Float64}, start_n::Int64, stop_n::Int64, len::Int64, den::Int64)
   @ Base ./twiceprecision.jl:717
 [3] _linspace
   @ ./twiceprecision.jl:714 [inlined]
 [4] range_start_stop_length
   @ ./range.jl:597 [inlined]
 [5] _range
   @ ./range.jl:167 [inlined]
 [6] #range#84
   @ ./range.jl:150 [inlined]
 [7] top-level scope
   @ REPL[25]:1

@alhirzel
Copy link
Contributor Author

alhirzel commented Nov 20, 2024

@eval function Base.:(:)(start::$T1, step::$T2, stop::$T3)
    return range(start, stop, length=length(ustrip(start):ustrip(step):ustrip(stop)))
end

@MilesCranmer, quick sanity check for symbolic units:

using DynamicQuantities

(a, b, c) = 1u"ft", 1u"inch", 1u"m"   # (0.3048 m, 0.025400000000000002 m, 1.0 m)
ustrip(a):ustrip(b):ustrip(c)         # 0.3048:0.025400000000000002:0.9906000000000001

(as, bs, cs) = 1us"ft", 1us"inch", 1us"m"  # (1.0 ft, 1.0 inch, 1.0 m)
ustrip(as):ustrip(bs):ustrip(cs)           # 1.0:1.0:1.0

@MilesCranmer
Copy link
Member

MilesCranmer commented Nov 20, 2024

Nice catch!

Edit: seems like I am just missing a check for compatible units.

    @eval function Base.:(:)(start::$T1, step::$T2, stop::$T3)
        dimension(start) == dimension(step) || throw(DimensionError(start, step))
        dimension(start) == dimension(stop) || throw(DimensionError(start, stop))
        return range(start, stop, length=length(ustrip(start):ustrip(step):ustrip(stop)))
    end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Indicates an unexpected problem or unintended behavior ranges Everything AbstractRange
Projects
None yet
Development

No branches or pull requests

8 participants