Tullio.jl icon indicating copy to clipboard operation
Tullio.jl copied to clipboard

Bounds checking too conservative on sub-arrays

Open hessammehr opened this issue 3 years ago • 7 comments

The following minimal example complains about the indices of a being out of range even though 1 <= a[1, j] <= 3. I wonder if bounds checking could be modified to handle scenarios like this?

using Tullio

a = [1 2 3
     4 5 6]
b = [1, 2, 3]
@tullio x := b[a[1, j]]

# "extrema of index a[1, j] must fit within b"

hessammehr avatar Sep 13 '20 13:09 hessammehr

Yes it's a bit conservative, I hadn't thought of this particular case but I think it's easy to address.

This one is a bug I meant to figure out:

@tullio x := b[a[1, j] + 1]  # junk

Cases where the index doesn't span the whole array are tricky, and still too conservative:

@tullio x := b[vec(a)[i+0]]  i in 1:3  # error

mcabbott avatar Sep 13 '20 14:09 mcabbott

Another case: scatter operations have their own code for bounds checks, so this complains about a column of inds which it won't read:

inds = rand(1:10, 333, 100); inds[:,50] .= 99;
vals = randn(333);
out = zeros(10,10)
@tullio out[inds[r,1], inds[r,100]] += vals[r]

mcabbott avatar Sep 25 '20 05:09 mcabbott

I get the impression Tullio eventually needs to implement/duplicate a lot of Julia, not just indexing logic but also figuring out dataflow. I wonder if operating on IR/SSA rather than AST input would lead to fewer edge cases.

hessammehr avatar Sep 25 '20 09:09 hessammehr

That's an interesting thought. But the naiive execution has a bounds check on every getindex, whereas Tullio wants to check extrema(inds) once, up front. Is there a way to go from one to the other?

mcabbott avatar Sep 26 '20 16:09 mcabbott

I must admit I'm probably out of my depth here but as a rough sketch perhaps something like this:

julia> struct Index
       end

julia> f(a,b,c) = begin
           i = Index()
           j = Index()
           @inbounds a[j] = b[i, j+1] + c[i, j-1]
       end
f (generic function with 2 methods)

julia> @code_lowered f(a,b,c)
CodeInfo(
1 ─       i = Main.Index()
│         j = Main.Index()
│         $(Expr(:inbounds, true))
│   %4  = i
│   %5  = j + 1
│   %6  = Base.getindex(b, %4, %5)
│   %7  = i
│   %8  = j - 1
│   %9  = Base.getindex(c, %7, %8)
│   %10 = %6 + %9
│         Base.setindex!(a, %10, j)
│         val = %10
│         $(Expr(:inbounds, :pop))
└──       return val
)

I haven't looked at packages like IRTools or Mjolnir too closely but wonder if inferring loop bounds from the getindex and setindex ops here would be more robust.

hessammehr avatar Sep 26 '20 17:09 hessammehr

Now I had another look at https://github.com/shashi/ArrayMeta.jl, which is perhaps a more Julian approach than mine... although it's still parsing the expression itself, it just builds up another tree structure & passes this to a generated function to make the loops at compile-time, instead of making them directly in the macro during parsing.

mcabbott avatar Sep 27 '20 09:09 mcabbott

Interesting, I wonder to what extent Meta.@lower overlaps with the functionality implemented in ArrayMeta.

julia> Meta.@lower a[j] = b[i, j+1] + c[i, j-1]
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─ %1 = j + 1
│   %2 = Base.getindex(b, i, %1)
│   %3 = j - 1
│   %4 = Base.getindex(c, i, %3)
│   %5 = %2 + %4
│        Base.setindex!(a, %5, j)
└──      return %5
))))

hessammehr avatar Sep 28 '20 09:09 hessammehr