Skip to content

Support for large numbers of args/kws with Python 3.10 - #7866

Merged
sklam merged 24 commits into
numba:mainfrom
njriasan:nick/16_kwargs_py310_peephole
May 6, 2022
Merged

sklam merged 24 commits into
numba:mainfrom
njriasan:nick/16_kwargs_py310_peephole

Conversation

@njriasan

Copy link
Copy Markdown
Contributor

Closes #7812. This seems to be working for the set of tests I have covered. I think the code quality probably needs to improve considerably. In addition any code to test for unintended side effects is greatly appreciated.

@benwilliamgraham benwilliamgraham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@stuartarchibald stuartarchibald added the Effort - long Long size effort needed label Feb 25, 2022

@ehsantn ehsantn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the effort @njriasan ! I took a quick look and the code makes sense to me.

Comment thread numba/core/interpreter.py
Comment thread numba/core/interpreter.py
vararg_loc
)
else:
args = _call_function_ex_replace_args_large(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment on the expected IR node for args_def.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment explaining my assumptions. Please let me know if it needs further clarification.

Comment thread numba/core/interpreter.py Outdated
@njriasan
njriasan marked this pull request as ready for review February 28, 2022 02:39
@njriasan njriasan changed the title [WIP] Support for large numbers of args/kws with Python 3.10 Support for large numbers of args/kws with Python 3.10 Feb 28, 2022
@njriasan

njriasan commented Feb 28, 2022 •

Copy link
Copy Markdown
Contributor Author

@ehsantn and @benwilliamgraham thanks for doing an initial review.

@stuartarchibald could you label this PR as ready for review? Thanks.

@sklam sklam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick check on coverage and found the following lines to be never covered. I only checked a random 20% of the test suite and all the new tests thus the coverage data may not be 100% accurate. If these lines are not really dead, they should have more comments to explain them.

Comment thread numba/core/interpreter.py
Comment thread numba/core/interpreter.py Outdated
Comment thread numba/core/interpreter.py Outdated
Comment thread numba/core/interpreter.py
@njriasan

njriasan commented Mar 1, 2022

Copy link
Copy Markdown
Contributor Author

I did a quick check on coverage and found the following lines to be never covered. I only checked a random 20% of the test suite and all the new tests thus the coverage data may not be 100% accurate. If these lines are not really dead, they should have more comments to explain them.

Thanks @sklam. I'll update my tests to add checks to two of the regions and I'll expand the comments on those regions.

Two of the areas that are uncovered were my attempts to be conservative against minor changes in CPython or the IR generated because I wasn't sure if my tests are exhaustive. If you feel like they are unnecessary I'm happy to remove them.

@njriasan

njriasan commented Mar 2, 2022

Copy link
Copy Markdown
Contributor Author

I did a quick check on coverage and found the following lines to be never covered. I only checked a random 20% of the test suite and all the new tests thus the coverage data may not be 100% accurate. If these lines are not really dead, they should have more comments to explain them.

Thanks @sklam. I'll update my tests to add checks to two of the regions and I'll expand the comments on those regions.

Two of the areas that are uncovered were my attempts to be conservative against minor changes in CPython or the IR generated because I wasn't sure if my tests are exhaustive. If you feel like they are unnecessary I'm happy to remove them.

@sklam I pushed an update to the tests and updated my comments.

@stuartarchibald

Copy link
Copy Markdown
Contributor

I don't think at present the transform will be able to handle inline control flow at the call site. For example, the rather contrived:

import numba
import dis

@numba.njit
def my_func1(a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None, i=None, j=None, k=None, l=None, m=None, n=None, o=None, p=None):
    return my_func2(
        a=a,
        b=b,
        c=c,
        d=d,
        e=e,
        f=f,
        g=g,
        h=h,
        i=i,
        j=j,
        k=k,
        l=l,
        m=m,
        n=n,
        o=o,
        p = False if a else True
    )


@numba.njit
def my_func2(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p):
    return

print(dis.dis(my_func1.py_func))
print(my_func1())

which gives a bytecode that has:

  6          96 LOAD_CONST              16 ('p')

 22          98 LOAD_FAST                0 (a)
            100 POP_JUMP_IF_FALSE       55 (to 110)
            102 LOAD_CONST              17 (False)
            104 MAP_ADD                  1

  6         106 CALL_FUNCTION_EX         1
            108 RETURN_VALUE

 22     >>  110 LOAD_CONST              18 (True)
            112 MAP_ADD                  1

  6         114 CALL_FUNCTION_EX         1

at the location where p is resolved and added to the map prior to the call.

@stuartarchibald

Copy link
Copy Markdown
Contributor

A similar issue exists in the nargs>30 use case:

import numba
import dis

@numba.njit
def my_func1(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10,
             a11, a12, a13, a14, a15, a16, a17, a18, a19, a20,
             a21, a22, a23, a24, a25, a26, a27, a28, a29, a30,
             a31,
             ):
    return my_func2(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10,
                    a11, a12, a13, a14, a15, a16, a17, a18, a19, True if a01 else False,
                    a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31)


@numba.njit
def my_func2(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10,
             a11, a12, a13, a14, a15, a16, a17, a18, a19, a20,
             a21, a22, a23, a24, a25, a26, a27, a28, a29, a30,
             a31):
    return

print(dis.dis(my_func1.py_func))
print(my_func1(*range(31)))

which gives:

Unsupported op-code encountered: list_to_tuple(info=('$phi92.1',))

as I think the transform fails due to the branch occurring within the LOAD_FAST+LIST_APPEND sequence that occurs ahead of the LIST_TO_TUPLE.

@njriasan

Copy link
Copy Markdown
Contributor Author

@stuartarchibald Thank you for finding these cases! You are definitely correct and I need to work on a more complete solution.

@stuartarchibald stuartarchibald added 4 - Waiting on author Waiting for author to respond to review and removed 3 - Ready for Review labels Mar 17, 2022
@stuartarchibald

stuartarchibald commented Mar 22, 2022 •

Copy link
Copy Markdown
Contributor

@stuartarchibald Thank you for finding these cases! You are definitely correct and I need to work on a more complete solution.

No problem.

I think given the rarity of the occurrence of these patterns, it would acceptable to do the following:

  • Update this patch to catch the cases where control flow occurs. Inform users that control flow on arguments within call sites is not supported and refer them to docs on how to adjust their code.
  • Add tests for the above.

The most common use cases of this somewhat rare pattern would then be covered and workarounds for the uncommon use cases of this rare pattern would be shown to users. Essentially, this patch makes the rare patterns work much more often, so I think it's better that it is reviewed and merged with acknowledgement of the imperfection instead of trying to cover everything. This of course also doesn't preclude further fixes later. What do you think?

@njriasan

Copy link
Copy Markdown
Contributor Author

@sklam I made your requested changes.

@stuartarchibald I made your changes as well and added tests. Proper error handling requires some extra code, so there may need to be to be a more detailed review. I also added a test for objmode which fails when the definitions are not properly updated.

@stuartarchibald stuartarchibald added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 4 - Waiting on author Waiting for author to respond to review labels Apr 20, 2022
Comment thread numba/core/interpreter.py Outdated
Comment on lines +696 to +704
while vararg_loc >= 0 and not_found:
args_def = blk.body[vararg_loc]
if (
isinstance(args_def, ir.Assign)
and args_def.target.name == vararg.name
):
not_found = False
else:
vararg_loc -= 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Leaving thought trail in case we need it in the future. This probably doesn't present to be a problem)

With bytecode from:

args = (*(0, 1, 2),)   # the starunpack triggers LIST_TO_TUPLE
y = my_func2(*args)
 16           0 BUILD_LIST               0
              2 LOAD_CONST               1 ((0, 1, 2))
              4 LIST_EXTEND              1
              6 LIST_TO_TUPLE
              8 STORE_FAST              31 (args)

 17          10 LOAD_GLOBAL              0 (my_func2)
             12 LOAD_FAST               31 (args)
             14 CALL_FUNCTION_EX         0
             16 STORE_FAST              32 (y)

the code here scan back from the call to my_func2 to find the definition of args, which is suppose to be a LIST_TO_TUPLE opcode, but is replaced by peep_hole_list_to_tuple; otherwise, the later check will reject this call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have also tried to trick this part of the code with the walrus operator but Numba is already miscompiling this on mainline:

my_func2(args:=(*(0, 1, 2),), *args)

Comment thread numba/core/interpreter.py Outdated
func_ir._definitions[lhs].clear()
else:
raise UnsupportedError(
"Inconsistency found the in definitions trying to remove varkwargs"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the error message still accurate? This function seem to be applied generally not just on varkwargs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@njriasan, gently ping ^

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. I will update this message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this message? Also thank you for the ping, I missed the original review.

@sklam sklam added 4 - Waiting on author Waiting for author to respond to review and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author labels Apr 21, 2022
Comment thread numba/core/interpreter.py Outdated
Added sklam's typo fix

Co-authored-by: Siu Kwan Lam <1929845+sklam@users.noreply.github.com>

@stuartarchibald stuartarchibald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-reviewed this patch following the changes to catch problems associated with control flow at call sites. It now seems to be handling the cases outlined in the original ticket as expected and failing in a controlled manner else where. Given @sklam has run coverage on this patch I've not checked this further. There's a few minor things to look at in line and once resolved I think this should be ready to merge. Thanks again for your efforts on this @njriasan.

Comment thread numba/core/interpreter.py Outdated
...
BUILD_CONST_KEY_MAP # Build a map

In the IR generated, the varkwarg refer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the IR generated, the varkwarg refer
In the generated IR, the varkwarg refers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/core/interpreter.py
# End for each argument

In the IR generated, the initial build map is empty and a series
of setitems are applied afterwards. THE IR looks like:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
of setitems are applied afterwards. THE IR looks like:
of setitems are applied afterwards. THE IR looks like:

Is what it looks like missing from this description/docstring, or is it included in the description a few lines down?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. I will update the description.

Comment thread numba/core/interpreter.py Outdated
"""
Extracts the kws args passed as varkwarg
for CALL_FUNCTION_EX. This pass is taken when
n_kws <= 15 and the bytecode looks like:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this statement correct? Seems like > 15 kws needed to have bytecode that this matches.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error in the docstring. Thank you for catching this.

Comment thread numba/core/interpreter.py Outdated
key_val = const_stmt.value.value
search_start += 1
# Now we need to search for a getattr with setitem
not_found_getattr = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this perhaps be called found_getattr and starts as False so as to avoid a double negative?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Comment thread numba/core/interpreter.py
Comment on lines +241 to +257
if not (
isinstance(setitem_stmt, ir.Assign)
and isinstance(setitem_stmt.value, ir.Expr)
and setitem_stmt.value.op == "call"
and (
setitem_stmt.value.func.name
== getattr_stmt.target.name
)
and len(setitem_stmt.value.args) == 2
and (
setitem_stmt.value.args[0].name
== key_var_name
)
):
# We cannot handle this format so raise the
# original error message.
raise UnsupportedError(errmsg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What triggers this branch? It doesn't seem like the tests hit it? Is it an "abundance of caution" branch to ensure that the getattr is followed by an appropriate call?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is meant as a precaution. I will add a comment indicating this and I will replace the error with a raise AssertionError("unreachable") to make this more clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually for now I will just add the comment. If you would like me to change to an AssertionError I would be happy to, but I'm not certain this is actually unreachable.

Comment thread numba/core/interpreter.py Outdated
# varkwarg should be defined second so we start there.
varkwarg_loc = start_search
keyword_def = None
not_found = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
not_found = True
found = False

(and fix up)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/tests/test_interpreter.py Outdated
Comment on lines +390 to +396
# Uncomment sections when inlined control flow is
# actually supported.
# py_func = inline_func
# cfunc = njit()(inline_func)
# a = py_func(False)
# b = cfunc(False)
# self.assertEqual(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably just remove this, it's easy to add in if/when needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/tests/test_interpreter.py Outdated
Comment on lines +507 to +513
# Uncomment sections when inlined control flow is
# actually supported.
# py_func = inline_func
# cfunc = njit()(inline_func)
# a = py_func(False)
# b = cfunc(False)
# self.assertEqual(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove? Same reasoning as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/tests/test_interpreter.py Outdated
# self.assertEqual(a, b)

@skip_unless_py10
def test_large_args_uninlined_controlflow(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_large_args_uninlined_controlflow(self):
def test_large_args_noninlined_controlflow(self):

(suggest same change throughout).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/tests/test_interpreter.py Outdated
Comment on lines +601 to +607
# Uncomment sections when inlined control flow is
# actually supported.
# py_func = inline_func
# cfunc = njit()(inline_func)
# a = py_func(False)
# b = cfunc(False)
# self.assertEqual(a, b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@njriasan

njriasan commented May 4, 2022

Copy link
Copy Markdown
Contributor Author

@stuartarchibald I made your requested changes. There are possibly two things that need to be looked at:

  1. Should the "abundance of caution" check contain a different error like an unreachable assertion. I don't have enough confidence with the code structure to say its truly unreachable so I just added a comment.
  2. Is the comment explaining when to check the call expression clear enough?

If both these changes seem fine, I think everything else should be a direct application of your feedback and we should be ready to merge. Thank you so much for the detailed review!

@stuartarchibald stuartarchibald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes @njriasan, there's a typo and a query to resolve else this looks good.

Comment thread numba/core/interpreter.py Outdated
call = stmt.value
args = call.args
kws = call.kws
# We need to to check the call expression contents if

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# We need to to check the call expression contents if
# We need to check the call expression contents if

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread numba/core/interpreter.py Outdated
Comment on lines +509 to +510
# varkwarg we need to update the actual bytecode. If it
# just contains vararg we don't need to update the bytecode,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean bytecode or IR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IR thank you.

@stuartarchibald

Copy link
Copy Markdown
Contributor

@stuartarchibald I made your requested changes.

@njriasan Many thanks.

There are possibly two things that need to be looked at:

1. Should the "abundance of caution" check contain a different error like an unreachable assertion. I don't have enough confidence with the code structure to say its truly unreachable so I just added a comment.

I think it can be left as-is for now. Given it cannot be handled at present, if it turns out that there's use cases in practice that will trigger it, I would guess someone will make a request for it/report it. I don't think it warrants an exception containing the reportable condition text because it's already known that it might happen and it can't be handled.

2. Is the comment explaining when to check the call expression clear enough?

Couple of minor things I've noted in #7866 (review) else looks good thanks.

If both these changes seem fine, I think everything else should be a direct application of your feedback and we should be ready to merge. Thank you so much for the detailed review!

No problem, thanks for your efforts on this.

@njriasan

njriasan commented May 6, 2022

Copy link
Copy Markdown
Contributor Author

Thank you @stuartarchibald. I made the requested changes.

@njriasan
njriasan requested a review from stuartarchibald May 6, 2022 13:59

@stuartarchibald stuartarchibald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patch and fixes @njriasan, great to get this merged!

@stuartarchibald stuartarchibald added 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on author Waiting for author to respond to review labels May 6, 2022
@sklam
sklam merged commit 0f5953d into numba:main May 6, 2022
sklam added a commit to sklam/numba that referenced this pull request Apr 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge Effort - long Long size effort needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Providing many keyword arguments triggers assertion error with Python 3.10 due to unsupported bytecode

6 participants