-
-
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
inference: parameterize some of hard-coded inference logic #39439
Conversation
@vtjnash @Keno @JeffBezanson could I hear your ideas on this ? |
bump, @vtjnash @Keno @JeffBezanson |
I think this is fine in general. Of course eventually, we'll probably want to find stable APIs for these sorts for things, but I'm fine with just refactoring in these hooks for now until we figure out what those APIs are. |
bail_out_call(interp::AbstractInterpreter, @nospecialize(t), sv) = t === Any | ||
bail_out_apply(interp::AbstractInterpreter, @nospecialize(t), sv) = t === Any | ||
bail_out_statement(interp::AbstractInterpreter, @nospecialize(t), sv) = t === Bottom | ||
bail_out_local(interp::AbstractInterpreter, @nospecialize(t), sv) = t === Bottom |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between bail_out_statement
and bail_out_local
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bail_out_local
controls the frame-level bail out logic in typeinf_local
while bail_out_statement
does the statement-level bail out logic in abstract_call_statement
.
The reason because I separate them is that it gives us more fine-grained control on the heuristic and we may want to use it.
a34a615
to
999973d
Compare
This commit parameterizes some of hard-coded inference logic: - to bail out from inference when a lattice element can't be refined or a current inference frame is proven to throw or to be dead - to add call backedges when the call return type won't be refined Those `AbstractInterpreter`s used for code optimization (including `NativeInterpreter`) usually just want the methods defined for `AbstractInterpreter`, but some other `AbstractInterpreter` may want other implementations and heuristics to control inference process. For example, [`JETInterpreter`](https://github.com/aviatesk/JET.jl) is used for code analysis and wants to add call backedges even when a call return type is `Any`.
with [the bail out interfaces](JuliaLang/julia#39439)
with [the bail out interfaces](JuliaLang/julia#39439)
@@ -1171,7 +1184,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), | |||
argtypes = Vector{Any}(undef, n) | |||
@inbounds for i = 1:n | |||
ai = abstract_eval_value(interp, ea[i], vtypes, sv) | |||
if ai === Bottom | |||
if bail_out_statement(interp, ai, sv) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upon further reflection, I'm not sure about this one. The return Bottom
here kinda implies that the only thing this could possibly do is to check for a literal Bottom
, no? Maybe it's a similar situation where the bail check needs to be separate from the check for Bottom.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think this could be useful for code analysis (but not in terms of optimization).
Say if we analyze this kind of code below:
function foo(a)
a = a * missing # => NoMethodError
sin_broad(a)
end
sin_broad(a) = sinn.(a) # => UndefVarError
foo([1,2,3]) # entry point
I believe it's better to report two errors in this case (i.e. NoMethodError(*, (Vector{Int}, Missing))
, UndefVarError(:sinn)
)).
With the bail_out_statement
and bail_out_local
interface, we can do something like below:
import Core.Compiler:
bail_out_local,
bail_out_statement,
abstract_call
bail_out_local(interp::JETInterpreter, @nospecialize(t), sv) = false
bail_out_statement(interp::JETInterpreter, @nospecialize(t), sv) = false
function CC.abstract_call(interp::JETInterpreter, ea::Union{Nothing,Vector{Any}}, argtypes::Vector{Any},
sv::InferenceState, max_methods::Int = InferenceParams(interp).MAX_METHODS)
argtypes = Any[a === Bottom ? Any : a for a in argtypes] # assuming the previous error is fixed, keep analysis going
@invoke abstract_call(interp::AbstractInterpreter, ea::Union{Nothing,Vector{Any}}, argtypes::Vector{Any},
sv::InferenceState, max_methods::Int)
end
and keep analysis going and collect as much errors as possible even when we know the actual execution terminates earlier.
I agree with this interface would be weird from optimization perspective, though.
/cc @vtjnash because you added commit to revert these interfaces in #39606. Could I hear your ideas on this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I thought you wanted to use these to early-out in useless analysis rather than to continue into unreachable code-paths. I think doing the latter can be pretty tricky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those aren't the same conditions though. If you did want to bail early, it should be using the Union{}
lattice element (otherwise we may exit before the lattice has finished converging). If you want to bail late, that decision doesn't happen here either (if later tfuncs are correct, they should also return Union{} in this case, but it would be a wild mess add this check everywhere). That decision happens in typeinf_local
, where we decide to stop forwarding information to the next statement rather than, say, attempting to set changes = fill!(similar(changes), VarState(Union{}, true))
so that we reach each subsequent statement (Though most will also error. Since this is dead/unreachable code, that is what we want, to avoid poisoning the actual inference results at the next phi node join point.)
I agree you could replace Union{} with Any (not here, but when they are accessed from the Slot), but that might quite quickly confuse the convergence algorithm and drive many more function towards being uninferrable. To avoid that, I think you'd realistically need to run inference repeatedly, "fixing" one error at a time from the outside driver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, I strongly dislike parametrization just for the sake of parameterization, since it bloats the compiler and makes it harder to follow the logical flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your responses.
Fair. As you pointed out my idea will be really complicated otherwise it will ruin inference results and convergence.
Even when I try that in the future, I think I will end up overloading the entire body of typeinf_local
anyway, so honestly I'm not too stick with bail_out_local/statement
interface and I'm okay with #39606.
In general, I strongly dislike parametrization just for the sake of parameterization, since it bloats the compiler and makes it harder to follow the logical flow.
Yeah, that's what I was worried about. Are you okay with bail_out_call
/bail_out_apply
? I'm fairly sure they won't destroy the convergence nor inference results anyway, but still might look weird in term of the implementation of NativeInterpreter
. Still they are very useful for the implementation of JETInterpreter
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those seem less bad, since it is probably just a performance optimization. There's also possibly not really any other correct behavior there to put in the overload either? We already computed the final call graph (info) and return type (Any), and no further analysis work should alter those. Thus also where we see how the optimizer is designed to run as a separated pass, and consumes both of those bits of information later and avoids being dependent upon the precise way it arrived at the answer.
with [the bail out interfaces](JuliaLang/julia#39439)
Eliminates the hacky overloads with the latest compiler interfaces. Particularly we will use: - [x] the bail out interfaces: <JuliaLang/julia#39439> - [ ] new constant prop' infrastructures: <JuliaLang/julia#39305> The previous code will be kept into `src/legacy` directory to keep the v1.6 compatibility.
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: #39439 (comment)
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: #39439 (comment)
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: #39439 (comment)
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: #39439 (comment)
…#39439) This commit parameterizes some of hard-coded inference logic: - to bail out from inference when a lattice element can't be refined or a current inference frame is proven to throw or to be dead - to add call backedges when the call return type won't be refined Those `AbstractInterpreter`s used for code optimization (including `NativeInterpreter`) usually just want the methods defined for `AbstractInterpreter`, but some other `AbstractInterpreter` may want other implementations and heuristics to control inference process. For example, [`JETInterpreter`](https://github.com/aviatesk/JET.jl) is used for code analysis and wants to add call backedges even when a call return type is `Any`.
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: JuliaLang#39439 (comment)
…#39439) This commit parameterizes some of hard-coded inference logic: - to bail out from inference when a lattice element can't be refined or a current inference frame is proven to throw or to be dead - to add call backedges when the call return type won't be refined Those `AbstractInterpreter`s used for code optimization (including `NativeInterpreter`) usually just want the methods defined for `AbstractInterpreter`, but some other `AbstractInterpreter` may want other implementations and heuristics to control inference process. For example, [`JETInterpreter`](https://github.com/aviatesk/JET.jl) is used for code analysis and wants to add call backedges even when a call return type is `Any`.
This logic is present to avoid asking ill-posed questions after we discover the dataflow is terminated, similar to how we'd remove the code after throw or a constant branch. Refs: JuliaLang#39439 (comment)
This commit parameterizes some of hard-coded inference logic:
a current inference frame is proven to throw or to be dead
Those
AbstractInterpreter
s used for code optimization (includingNativeInterpreter
) usually just want the methods defined forAbstractInterpreter
, but some otherAbstractInterpreter
may wantother implementations and heuristics to control inference process.
For example,
JETInterpreter
isused for code analysis and wants to add call backedges even when a call
return type is
Any
.