Hypot is not consistent?

Just saw this:

julia> Base.infer_effects(hypot, NTuple{2, Float64})
(!c,+e,!n,+t,+s,+m,+u,+o,+r)

How come hypot() is not consistent? Is this a problem of inference not being precise, or is there actually something that makes hypot() inconsistent?

hypot() not being consistent doesn’t really matter to me, just curious…

We produce different results depending on whether your CPU has FMA.

Shouldn’t these effects results be for a given CPU target though?

You can actually track this down yourself using Cthulhu.jl. Specifically, if you do descend_code_typed(Base.Math._hypot, NTuple{2, Float64}; effects = true), you can see that on line 769 there is a call to Intrinsics.have_fma, which taints the consistent effect bit:

│    @ math.jl:769 within `unknown scope`
│    %134 = Base.Math.Core::Core.Const(Core) (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %135 =   dynamic Base.getproperty(%134, :Intrinsics)::Core.Const(Core.Intrinsics) (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %136 =   dynamic Base.getproperty(%135, :have_fma)::Core.Const(Core.Intrinsics.have_fma) (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %137 = Base.Math.typeof::Core.Const(typeof) (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %138 = h::Float64 (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %139 =   builtin (%137)(%138)::Core.Const(Float64) (+c,+e,+n,+t,+s,+m,+u,+o,+r)
│    %140 = intrinsic (%136)(%139)::Bool (!c,+e,+n,+t,+s,+m,+u,+o,+r)
└───        goto #16 if not %140 (+c,+e,+n,+t,+s,+m,+u,+o,+r)

To expand on Oscar’s answer, that have_fma call taints consistency due to the presence of multiversioning, where LLVM might compile this function for an architecture version that doesn’t have fma, so Julia’s compiler can’t assume have_fma always returns the same answer.

I was investigating and I thought I had ruled out fma, or related muladd used for !c, but not for !n (nor for sqrt)…:

julia> Base.infer_effects(muladd, NTuple{3, Float64})
(+c,+e,!n,+t,+s,+m,+u,+o,+r)

julia> Base.infer_effects(fma, NTuple{3, Float64})
(+c,+e,+n,+t,+s,+m,+u,+o,+r)

julia> Base.infer_effects(sqrt, NTuple{1, Float64})
(+c,+e,!n,+t,+s,+m,+u,+o,+r)

Where do you read that from, is that the line to look at?! What does +c mean there as opposed to !c?

Base.infer_effects(Core.Intrinsics.have_fma, NTuple{1, Float64})  # Core.Intrinsics.have_fma(typeof(h))
(!c,+e,!n,+t,+s,+m,+u,+o,+r)

How bad of an idea would it be for us to use @assume_effects :consistent on functions like hypot?

Would this cause actual miscompilation problems?

That depends on what :consistent means, I think it means you get same output out for any input, and that happens if you have fma hardware, i.e. for most users. For the others they also get same answers for any input and i.e. same for all other non-fma hardware users, the results for the muladd used in the else branch, just not bit-identical to the others on fma hardware because:

    # This branch is correctly rounded but requires a native hardware fma.
    if Core.Intrinsics.have_fma(typeof(h))
        hsquared = h*h
        axsquared = ax*ax
        h -= (fma(-ay, ay, hsquared-axsquared) + fma(h, h,-hsquared) - fma(ax, ax, -axsquared))/(2*h)
    # This branch is within one ulp of correctly rounded.
    else

I’ve never seen code like this, I though muladd generally used, and it meant fma on such hardware, but the else branch is a bit different to compensate, but not fully. I’ve been investigating where we get correctly rounded, and if Julia should promise, this is an interesting case. Should Julia at least promise at least “within one ulp of correctly rounded”? And should docs of hypot mention this?

What is the practical effect of !c, then considered a non-pure function, and results not cached and reused? And if you lie with @assume_effects :consistent possibly sightly better code generation? But would some people or tools infer correctly rounded, or identical rounding to old platforms?