Repository navigation
Support for large numbers of args/kws with Python 3.10 - #7866
Conversation
| vararg_loc | ||
| ) | ||
| else: | ||
| args = _call_function_ex_replace_args_large( |
There was a problem hiding this comment.
Add comment on the expected IR node for args_def.
There was a problem hiding this comment.
I added a comment explaining my assumptions. Please let me know if it needs further clarification.
|
@ehsantn and @benwilliamgraham thanks for doing an initial review. @stuartarchibald could you label this PR as ready for review? Thanks. |
sklam
left a comment
There was a problem hiding this comment.
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. |
|
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: at the location where |
|
A similar issue exists in the 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: as I think the transform fails due to the branch occurring within the |
|
@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:
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? |
|
@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. |
| 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 |
There was a problem hiding this comment.
(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.
There was a problem hiding this comment.
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)| func_ir._definitions[lhs].clear() | ||
| else: | ||
| raise UnsupportedError( | ||
| "Inconsistency found the in definitions trying to remove varkwargs" |
There was a problem hiding this comment.
Is the error message still accurate? This function seem to be applied generally not just on varkwargs.
There was a problem hiding this comment.
You are correct. I will update this message.
There was a problem hiding this comment.
How is this message? Also thank you for the ping, I missed the original review.
Added sklam's typo fix Co-authored-by: Siu Kwan Lam <1929845+sklam@users.noreply.github.com>
stuartarchibald
left a comment
There was a problem hiding this comment.
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.
| ... | ||
| BUILD_CONST_KEY_MAP # Build a map | ||
|
|
||
| In the IR generated, the varkwarg refer |
There was a problem hiding this comment.
| In the IR generated, the varkwarg refer | |
| In the generated IR, the varkwarg refers |
| # 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: |
There was a problem hiding this comment.
| 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?
There was a problem hiding this comment.
You are correct. I will update the description.
| """ | ||
| Extracts the kws args passed as varkwarg | ||
| for CALL_FUNCTION_EX. This pass is taken when | ||
| n_kws <= 15 and the bytecode looks like: |
There was a problem hiding this comment.
Is this statement correct? Seems like > 15 kws needed to have bytecode that this matches.
There was a problem hiding this comment.
This is an error in the docstring. Thank you for catching this.
| key_val = const_stmt.value.value | ||
| search_start += 1 | ||
| # Now we need to search for a getattr with setitem | ||
| not_found_getattr = True |
There was a problem hiding this comment.
Should this perhaps be called found_getattr and starts as False so as to avoid a double negative?
| 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # varkwarg should be defined second so we start there. | ||
| varkwarg_loc = start_search | ||
| keyword_def = None | ||
| not_found = True |
There was a problem hiding this comment.
| not_found = True | |
| found = False |
(and fix up)?
| # 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) |
There was a problem hiding this comment.
Can probably just remove this, it's easy to add in if/when needed.
| # 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) |
There was a problem hiding this comment.
Remove? Same reasoning as above.
| # self.assertEqual(a, b) | ||
|
|
||
| @skip_unless_py10 | ||
| def test_large_args_uninlined_controlflow(self): |
There was a problem hiding this comment.
| def test_large_args_uninlined_controlflow(self): | |
| def test_large_args_noninlined_controlflow(self): |
(suggest same change throughout).
| # 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) |
|
@stuartarchibald I made your requested changes. There are possibly two things that need to be looked at:
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
left a comment
There was a problem hiding this comment.
Thanks for the fixes @njriasan, there's a typo and a query to resolve else this looks good.
| call = stmt.value | ||
| args = call.args | ||
| kws = call.kws | ||
| # We need to to check the call expression contents if |
There was a problem hiding this comment.
| # We need to to check the call expression contents if | |
| # We need to check the call expression contents if |
| # varkwarg we need to update the actual bytecode. If it | ||
| # just contains vararg we don't need to update the bytecode, |
There was a problem hiding this comment.
Do you mean bytecode or IR?
@njriasan Many thanks.
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.
Couple of minor things I've noted in #7866 (review) else looks good thanks.
No problem, thanks for your efforts on this. |
|
Thank you @stuartarchibald. I made the requested changes. |
stuartarchibald
left a comment
There was a problem hiding this comment.
Thanks for the patch and fixes @njriasan, great to get this merged!
Tests were originally from numba#7866
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.