-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
The use of NamedTuple
in Core.kwcall
prevents specialization of keyword arguments
#54661
Comments
It looks like it's a duplicate but let's keep this one open. @vtjnash I think this is a real thing? It's because of the fact that keywords are passed via julia> typeof((; t=Float32))
@NamedTuple{t::DataType} This means that there is no way to force specialization on keywords using the same tricks possible with args. The solution is to apply a Of course you might find that the compiler will inline everything here (like with the example given by @vtjnash — the constant propagation goes through the kwcall), but the inference on the keyword call itself still fails, even if you force it. |
So if I lower julia> f(; t::Type{T}) where {T} = T
f (generic function with 1 method)
julia> @code_lowered f(t=Float32)
CodeInfo(
1 ─ Core.NewvarNode(:(@_4))
│ %2 = Core.isdefined(@_2, :t)
└── goto #3 if not %2
2 ─ @_4 = Core.getfield(@_2, :t)
└── goto #4
3 ─ %6 = Core.UndefKeywordError(:t)
└── @_4 = Core.throw(%6)
4 ┄ %8 = @_4
│ t = %8
│ %10 = (:t,)
│ %11 = Core.apply_type(Core.NamedTuple, %10)
│ %12 = Base.structdiff(@_2, %11)
│ %13 = Base.pairs(%12)
│ %14 = Base.isempty(%13)
└── goto #6 if not %14
5 ─ goto #7
6 ─ Base.kwerr(@_2, @_3)
7 ┄ %18 = Main.:(var"#f#1")(t, @_3)
└── return %18
) So the line Core.apply_type(Core.NamedTuple, %10) Is where the issue stems from. To propagate the user's specialization, I think we might want to have a way to lower to a stable form of this so that specialization of the keywords propagates. I tried to understand the lowering in Julia. My understanding is that this line: Lines 621 to 622 in 13635e1
NamedTuple on the keynames which are the keyword names?
So I guess if instead of wrapping with Core.NamedTuple{keynames,Tuple{map(Core.Typeof,keyvars)...}}(keyvars) then it should fix this, because julia> t = Float64
Float64
julia> keynames = (:t,)
(:t,)
julia> NamedTuple{keynames,Tuple{map(Core.Typeof,(t,))...}}((t,))
@NamedTuple{t::Type{Float64}}((Float64,)) Which keeps the user's specialization into the Thoughts @vtjnash? Sorry if I'm completely misreading things.. |
I think this is maybe mostly an artifact of how
So in context we are able to infer it.
I don't think that's correct --- the problem is that at the keyword arg call site we form a |
I think this is only because the compiler is inlining
The reason for my issue is precisely this — I have a very complex function in SymbolicRegression.jl where So I am wondering if |
Could you make a reduced example? Use If the problem is the NamedTuple formed at the call site, then |
The specific function I first ran into this issue with was this one: https://github.com/MilesCranmer/SymbolicRegression.jl/blob/ea03242d099aa189cad3612291bcaf676d77451c/src/Dataset.jl#L98. If you pass I have since adjusted it so that |
My general workaround for anyone running into this inference bug is to pass types wrapped in a julia> f(; t::Val{T}) where {T} = T
f (generic function with 1 method)
julia> Test.@inferred f(t=Val(Float32))
Float32 This does not have the inference issue because This of course makes sense because If not fixable it would be good to least document it on https://docs.julialang.org/en/v1/manual/performance-tips/#Be-aware-of-when-Julia-avoids-specializing. Basically, Julia will still avoid specializing on types in keyword arguments even if marked with julia> (; f = +)
(f = +,)
julia> (; f = +) |> typeof
@NamedTuple{f::typeof(+)}
julia> (; v = Val(1))
(v = Val{1}(),)
julia> (; v = Val(1)) |> typeof
@NamedTuple{v::Val{1}}
julia> (; t = Float32)
(t = Float32,)
julia> (; t = Float32) |> typeof
@NamedTuple{t::DataType} Which means there is no asymmetry for those other two special cases, it's literally just types. |
This issue is indeed due to the implementations of |
@aviatesk I’m not sure I understand, why do you say it is only an interactive tools problem? Note that even I think this problem be solved without extending the type system. All that needs to happen is for a tiny change in the lowering of a keyword call. Normally it is NamedTuple{keynames,Tuple{map(typeof,keyvars)…}}(keyvars) This should be changed to NamedTuple{keynames,Tuple{map(Core.Typeof,keyvars)...}}(keyvars) Which will cause the NamedTuple to pick up the Again, and this is only for lowering to |
As far as I understand, you check this inference failure with
So to me (just reading through the conversation) it seems that this has been missed? Or are you using some other way to determine the "inference bug"? |
Sorry if I misunderstand; but to clarify, I am using Base.promote_op directly and seeing the same issue with the lack of keyword specialisation. |
Could you show an explicit example of that? |
Ok, see below. First, just some context – I first saw this deep in the call stack of SymbolicRegression.jl. Changing to arguments made the inference issue go away. Hence my interest in fixing this. Also, just to note – obviously if you do: julia> f(; t::Type{T}) where {T} = T
f (generic function with 1 method)
julia> g(::Type{T}) where {T} = f(; t=T)
g (generic function with 1 method)
julia> Core.kwcall((; t=Float32), f)
Float32
julia> Base.promote_op(Core.kwcall, typeof((; t=Float32)), typeof(f))
DataType you will get the failed inference. But I'm assuming you want the Basically it's really hard to prevent the compiler from doing some amount of inlining in a toy example (such as the simple example of just First, here is what happens with keywords. I generate 8 functions (random big number, I'm not sure the exact dividing line) which randomly call each other in a recursion up to a stack depth of 10. Now LLVM has no chance of inlining! gs = [gensym("g") for _ in 1:8]
for g in gs
@eval @noinline function $g(; t::Type{T}, i) where {T}
k = rand(1:8)
if i > 10
return one(T)
elseif k == 1
return $(gs[1])(; t, i=i+1)
elseif k == 2
return $(gs[2])(; t, i=i+1)
elseif k == 3
return $(gs[3])(; t, i=i+1)
elseif k == 4
return $(gs[4])(; t, i=i+1)
elseif k == 5
return $(gs[5])(; t, i=i+1)
elseif k == 6
return $(gs[6])(; t, i=i+1)
elseif k == 7
return $(gs[7])(; t, i=i+1)
else
return $(gs[8])(; t, i=i+1)
end
end
end
@eval @noinline f(t::Type{T}) where {T} = $(gs[1])(; t, i=1) When I run promote_op, I get: julia> Base.promote_op(f, Type{Float64})
Any Now, if I instead try the exact same thing, but with regular arguments: gs = [gensym("g") for _ in 1:8]
for g in gs
@eval @noinline function $g(t::Type{T}, i) where {T}
k = rand(1:8)
if i > 10
return one(T)
elseif k == 1
return $(gs[1])(t, i+1)
elseif k == 2
return $(gs[2])(t, i+1)
elseif k == 3
return $(gs[3])(t, i+1)
elseif k == 4
return $(gs[4])(t, i+1)
elseif k == 5
return $(gs[5])(t, i+1)
elseif k == 6
return $(gs[6])(t, i+1)
elseif k == 7
return $(gs[7])(t, i+1)
else
return $(gs[8])(t, i+1)
end
end
end
@eval @noinline f(t::Type{T}) where {T} = $(gs[1])(t, 1) and we run it, I get a correct inference: julia> Base.promote_op(f, Type{Float64})
Float64 So you can see there is some difference in the behavior of specialisation between keyword arguments and regular arguments. At least, I think this shows it? Obviously this is a contrived example, but there's probably a smaller MWE out there one could pull out. For me I encountered it in that big |
This is indeed an inference issue related to kwfunc, but it is a different problem from the one originally raised in this issue. |
Hm, I wonder if this is the cause of the issue I had meant to report? I have a slight feeling that all of the real-world instances where I have seen this symptom appear were from recursive calls. The stuff I was posting about For the record I really have no idea how |
Hi all, Just following up on this. I am trying to understand this deeply, not only for my own curiosity, but also so that I can improve DispatchDoctor.jl which helps detect type instability. So if you can help me understand the reason behind this, I will go make a patch to DispatchDoctor to improve its instability detection. Basically can someone please confirm to me that julia> f(; t::Type{T}) where {T} = T is type stable, even if not inlined? I think I know how to patch this in DispatchDoctor.jl so it can avoid flagging this as an instability – I will use the example I gave above: julia> t = Float64;
julia> keynames = (:t,);
julia> NamedTuple{keynames,Tuple{map(Core.Typeof,(t,))...}}((t,))
@NamedTuple{t::Type{Float64}}((Float64,)) If I pass this named tuple to |
The Julia docs here say that to force specialization on types, you should use
::Type{T}
. However, this does not work for keyword arguments:The reason is because
Core.kwcall
is passed keyword arguments as aNamedTuple
, and this only encodesDataType
rather thanType{T}
in its parameters. For this reason some of the Julia internals use_stable_typeof
as a workaround version of oftypeof
which mapsFloat32
toType{Float32}
rather thanDataType
.I would like there to be a similar mechanism for
Core.kwcall
so that keyword arguments get the same specialization rules as arguments.For example, one could use
Core.Typeof
in constructing theNamedTuple
passed toCore.kwcall
, so that it preserves the specialization of the calling function. You can actually have theNamedTuple
retain this information:x-ref MilesCranmer/DispatchDoctor.jl#27
The text was updated successfully, but these errors were encountered: