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

inference: parameterize some of hard-coded inference logic #39439

Merged
merged 1 commit into from
Feb 10, 2021

Conversation

aviatesk
Copy link
Member

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 AbstractInterpreters 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 is
used for code analysis and wants to add call backedges even when a call
return type is Any.

@aviatesk
Copy link
Member Author

@vtjnash @Keno @JeffBezanson could I hear your ideas on this ?
This PR won't change any behavior of NativeInterpreter but can help other AbstractIntepreters tweak inference process more easily (otherwise they have to overload the entire body of abstract_call_gf_by_type etc.).
I'd really appreciate if we could merge these changes (unless I'm actually doing something wrong)

@aviatesk
Copy link
Member Author

aviatesk commented Feb 6, 2021

bump, @vtjnash @Keno @JeffBezanson

@aviatesk aviatesk requested review from Keno, vtjnash and JeffBezanson and removed request for vtjnash February 6, 2021 07:11
@Keno
Copy link
Member

Keno commented Feb 6, 2021

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
Copy link
Member

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?

Copy link
Member Author

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.

@aviatesk aviatesk force-pushed the bailout branch 2 times, most recently from a34a615 to 999973d Compare February 7, 2021 04:58
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`.
@Keno Keno merged commit 1ef49c8 into JuliaLang:master Feb 10, 2021
@aviatesk aviatesk deleted the bailout branch February 10, 2021 00:59
aviatesk added a commit to aviatesk/JET.jl that referenced this pull request Feb 10, 2021
aviatesk added a commit to aviatesk/JET.jl that referenced this pull request Feb 10, 2021
@@ -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)
Copy link
Member

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.

Copy link
Member Author

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 ?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

aviatesk added a commit to aviatesk/JET.jl that referenced this pull request Feb 10, 2021
aviatesk added a commit to aviatesk/JET.jl that referenced this pull request Feb 10, 2021
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.
vtjnash added a commit that referenced this pull request Feb 10, 2021
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)
vtjnash added a commit that referenced this pull request Feb 10, 2021
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)
vtjnash added a commit that referenced this pull request Feb 16, 2021
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)
vtjnash added a commit that referenced this pull request Feb 16, 2021
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)
ElOceanografo pushed a commit to ElOceanografo/julia that referenced this pull request May 4, 2021
…#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`.
ElOceanografo pushed a commit to ElOceanografo/julia that referenced this pull request May 4, 2021
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)
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request May 9, 2021
…#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`.
antoine-levitt pushed a commit to antoine-levitt/julia that referenced this pull request May 9, 2021
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants