Skip to content

iteration fails in tuple comprehension #7394

Closed

Description

@ogauthe

Hello,

As discussed on gitter, I found a bug in list comprehension when passing a tuple as input:

@numba.njit
def f1_fine(array_tuple):
    vals = []
    for a in array_tuple:
        vals.append(a[0])
    return np.array(vals)

@numba.njit
def f2_fine(array_tuple):
    return np.array([array_tuple[i][0] for i in range(len(array_tuple))])


@numba.njit
def f3_crash(array_tuple):
    return np.array([a[0] for a in array_tuple])

at = (np.ones(4), np.ones(3))

f1_fine(at)
f2_fine(at)
f3_crash(at)

While these functions are pretty much the same, f3 does not compile with error

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<intrinsic range_iter_len>) found for signature:
 
 >>> <unknown function>(iter(UniTuple(array(float64, 1d, C) x 2)))
 
There are 2 candidate implementations:
      - Of which 2 did not match due to:
      Intrinsic of function '<unknown function>': File: numba/cpython/rangeobj.py: Line 183.
        With argument(s): '(iter(UniTuple(array(float64, 1d, C) x 2)))':
       No match.

During: resolving callee type: Function(<intrinsic range_iter_len>)
During: typing of call at <ipython-input-20-4047ac44d0b7> (15)


File "<ipython-input-20-4047ac44d0b7>", line 15:
def f3_crash(array_tuple):
    return np.array([a[0] for a in array_tuple])
    ^

numba version 0.53.1

Activity

  1. MegaIng commented on Sep 9, 2021

    @MegaIng
    Contributor

    Seems to be related to #6550

    I also poked around a bit and found what I believe to be the source of the error:

    range_def = guard(_find_iter_range, func_ir, iter_var, swapped)

    The function _find_iter_range raises GuardException when it does not find a range iterator. This exception is turned into None by guard (as expected). However, the following if statements uses that info for different things and essentially assumes that there was a range_iter and only differentiates between range iter with and without step (as far as I can tell). A fix for this specific situation is to insert require(range_def is not None), but I am not sure if that works in general.

  2. changed the title [-]iteration fails in list comprehension[/-] [+]iteration fails in tuple comprehension[/+] on Sep 9, 2021
  3. stuartarchibald commented on Sep 10, 2021

    @stuartarchibald
    Contributor

    Seems to be related to #6550

    I also poked around a bit and found what I believe to be the source of the error:

    range_def = guard(_find_iter_range, func_ir, iter_var, swapped)

    The function _find_iter_range raises GuardException when it does not find a range iterator. This exception is turned into None by guard (as expected). However, the following if statements uses that info for different things and essentially assumes that there was a range_iter and only differentiates between range iter with and without step (as far as I can tell). A fix for this specific situation is to insert require(range_def is not None), but I am not sure if that works in general.

    Thanks for debugging this @MegaIng, the fix you describe will prevent this problem from occurring and I think given the nature of what the optimisation subsequently does it is an appropriate way to fix it. A PR with unit test based on the bug added to:

    class TestArrayComprehension(unittest.TestCase):
    would be welcomed if you have time. Many Thanks!

  4. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    While writing the tests right now, I noticed that the most recent master apparently transforms f1_fine in such a way that it now also breaks.

  5. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    The fix does not work. It breaks a few tests in the same class:

    def test_array_comp_with_iter(self):
    ,
    def test_comp_with_array_2(self):
    and
    def test_comp_with_array_range_and_step(self):
    .

    Apparently the function range_iter_len is just misnamed. It is supposed to handle all possible things one might throw at an array comprehension, not just range iterators. So currently the correct fix is to tell range_iter_len how to deal with tuple iterators (which is what is suggested as solution in #6550). However, that means that for every iterator type there might be the function has to be implemented.

    Alternatively, is there a way to check if a function is implemented for the given parameters? We could check that there, and if no implementation is found reject the optimization.

  6. stuartarchibald commented on Sep 10, 2021

    @stuartarchibald
    Contributor

    While writing the tests right now, I noticed that the most recent master apparently transforms f1_fine in such a way that it now also breaks.

    Thanks. On which version of Numba does it work? I can't reproduce it working on 0.53.1:

    $ cat issue7394.py 
    from numba import njit, version_info
    import numpy as np
    
    print(version_info)
    
    @njit
    def f1_fine(array_tuple):
        vals = []
        for a in array_tuple:
            vals.append(a[0])
        return np.array(vals)
    
    at = (np.ones(4), np.ones(3))
    
    f1_fine(at)
    
    $ python issue7394.py 
    version_info(major=0, minor=53, patch=1, short=(0, 53), full=(0, 53, 1), string='0.53.1', tuple=('0', '53', '1'), git_revision=None)
    Traceback (most recent call last):
      File "issue7394.py", line 15, in <module>
        f1_fine(at)
      File "<path>/numba/core/dispatcher.py", line 420, in _compile_for_args
        error_rewrite(e, 'typing')
      File "<path>/numba/core/dispatcher.py", line 361, in error_rewrite
        raise e.with_traceback(None)
    numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
    No implementation of function Function(<intrinsic range_iter_len>) found for signature:
     
     >>> <unknown function>(iter(UniTuple(array(float64, 1d, C) x 2)))
     
    There are 2 candidate implementations:
       - Of which 2 did not match due to:
       Intrinsic of function '<unknown function>': File: numba/cpython/rangeobj.py: Line 183.
         With argument(s): '(iter(UniTuple(array(float64, 1d, C) x 2)))':
        No match.
    
    During: resolving callee type: Function(<intrinsic range_iter_len>)
    During: typing of call at issue7394.py (8)
    
    
    File "issue7394.py", line 8:
    def f1_fine(array_tuple):
        vals = []
    
  7. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    @stuartarchibald For me it works on 0.54.0 and on 0.53.1

    Python version differences are probably the problem.

  8. stuartarchibald commented on Sep 10, 2021

    @stuartarchibald
    Contributor

    @stuartarchibald For me it works on 0.54.0 and on 0.53.1

    Python version differences are probably the problem.

    Yes, are you on 3.7? I tested this on 3.9.

  9. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    @stuartarchibald I have two different environments: One for numba development, and another where I have numba installed from pip. Both are Python version 3.9.4, but on the one with numba master f1_fine doesn't work, on the other it does.

  10. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    Ok, no worries, I am stupid. My one version of f1_fine did not have a numba.jit decorator. It fails everywhere.

  11. ogauthe commented on Sep 10, 2021

    @ogauthe
    Author

    I run f1_fine with python 3.7.11, numpy 1.20.3 and numba 0.53.1+8.gddf817af0.dirty, it works. So yes probably python version.

  12. stuartarchibald commented on Sep 10, 2021

    @stuartarchibald
    Contributor

    RE failing tests, thanks for trying this out. I've taken a bit more of a look at the code and agree that the original suggestion will not work for the reasons you noted here: #7394 (comment), i.e. if there's no range then range_iter_len is used and it needs to work out the length of the iterator.

    I think the issue here is that as the array comprehension transform is untyped it's not possible to know prior to doing the transform whether range_iter_len actually supports computing a length for the type of the iterator.

    Two things probably need doing:

    1. Write code to handle the tuple case.
    2. Raise a TypingError if none of the types are matched in range_iter_len with a message spelling out the problem.

    Untested patch for the above items:

    diff --git a/numba/cpython/rangeobj.py b/numba/cpython/rangeobj.py
    index 0f61a4f..8a26cbe 100644
    --- a/numba/cpython/rangeobj.py
    +++ b/numba/cpython/rangeobj.py
    @@ -7,7 +7,7 @@ import operator
     import llvmlite.llvmpy.core as lc
     
     from numba import prange
    -from numba.core import types, cgutils
    +from numba.core import types, cgutils, errors
     from numba.cpython.listobj import ListIterInstance
     from numba.np.arrayobj import make_array
     from numba.core.imputils import (lower_builtin, lower_cast,
    @@ -213,7 +213,18 @@ def range_iter_len(typingctx, val):
                 # array iterates along the outer dimension
                 return impl_ret_untracked(context, builder, intp_t, shape[0])
             return signature(types.intp, val), codegen
    -
    +    elif isinstance(val, types.UniTupleIter):
    +        def  codegen(context, builder, sig, args):
    +            (iterty,) = sig.args
    +            tuplety = iterty.container
    +            intp_t = context.get_value_type(types.intp)
    +            count_const = intp_t(tuplety.count)
    +            return impl_ret_untracked(context, builder, intp_t, count_const)
    +        return signature(types.intp, val), codegen
    +    else:
    +        msg = ('Unsupported iterator found in array comprehension, try '
    +               'preallocating the array and filling manually.')
    +        raise errors.TypingError(msg)
  13. MegaIng commented on Sep 10, 2021

    @MegaIng
    Contributor

    I came to the same conclusion. I also think that range_iter_len should be renamed (I choose comprehension_iter_len). It probably should also be moved to another module, including the tests, but for the moment I am just going to leave it there.

  14. added a commit that references this issue on Sep 10, 2021
    aed490b
  15. stuartarchibald commented on Sep 10, 2021

    @stuartarchibald
    Contributor

    I came to the same conclusion. I also think that range_iter_len should be renamed (I choose comprehension_iter_len). It probably should also be moved to another module, including the tests, but for the moment I am just going to leave it there.

    From what I can work out, this function is an intrinsic which when called on an iterable, returns the length of the iterable. I think it's constructing something like:

    x  = <some iterable>
    count = range_iter_len(x)
    ... do something with count
    

    maybe length_of_iterable would be a good fit?

  16. added a commit that references this issue on Nov 1, 2021
    861ce4d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions