TIR : Are multiple Let-in with the same var allowed? (SSA)

Hi everyone,

I’m having some quick questions about the Tensor Intermediate Representation (TIR). I’m wondering if the following things are legal in TIR :

    1. Is it valid TIR to have multiple Let-In bindings using the same variable, within different scopes. I’m thinking about something that could for instance look like:

Let a = (let x = 4 in x+1) in Let b = (let x = 5 in x+2) in inst

If that’s not the case, I guess we could say that TIR code has to be in SSA form to be valid.

    1. This one probably makes sense only if the answer to the former was yes. Is it valid TIR to have multiple Let-In bindings using the same variable, this time within the same scope. I’m thinking about something that would look like:

Let x = 1 in Let x = x+1 in inst

    1. If the one just above is valid TIR, is there a way to access the former x from the parent/bigger scope? I’m thinking about something equivalent ::x in C++, apart that in this case we would need something to “exit” an arbitrary amount of scope instead of something for simply switching from the local to the global scope.

Many thanks in advance!

Great question, yes TIR requires a var to be defined in a single place

1 Like

Many thanks for the super fast answer @tqchen . So I guess we could say that TIR is always in SSA form then. Great.

I assume that the TIR analysis pass implemented in tir/analysis/verify_ssa.cc precisely checks that, correct? So it’s not an optional check and any TIR input will be checked with this analysis?

Many thanks again!

all of our passes would require the input/output to be this form. Not sure how does the script parser handle it right now but likely we will need to do the same thing or canonicalize

1 Like

Great. Thanks a lot for your time, I really appreciate it.

I had a quick look at the analysis pass which does this verification. I think I found it in src/tir/analysis/verify_ssa.cc, and there a a couple of things I don’t fully understand about it and would love to have some input if someone knows.

Of course, this visitor explores the AST of the input (both expressions and statements), trying to find variables that are defined multiple times. Actually I realized that variables that are mapped to the same value are allowed. Let’s call this quasi-SSA form.

So in this recursive exploration, we have the important case of a LetNode (the Let-In construction as an expression), and it does what I would have expected : if it finds the current variable being defined by this Let-In in the hash-table of previously defined variables, it will return false if the value associated to the value found in the table (it->second) is not “deeply-equal” to the value to which we are trying to bind this variable now (op->value). Great.

  1. There is just one thing here that annoys me here: I had a look at this deep_equal() function, which is in fact an operator () on the class ExprDeepEqual (in include/tvm/tir/analysis.h), which is being implemented in deep_equal.cc. And I would have expected it to be wary of function calls, that could -despite being deeply_equal on the surface- lead to different value. I’m thinking of something like : Let x = funcWithSideEffect 42 in Let x = funcWithSideEffect 42 in x+x

x can seem to be associated to the same value in both cases (funcWithSideEffect 42) and deep_equal_() will answer yes, but if as its name suggests funcWithSideEffect() does some side effects which makes it such that it does not always return the same value when called with the same argument, then it won’t be in quasi-SSA form.

If later someone uses this quasi-SSA property that for removing redundant computation, the semantics of the program would not be preserved. But perhaps I missed it and this kind of situation is prevented somewhere.

  1. Another thing that I don’t understand is why the same verification is not performed for a LetStmtNode (i.e. the Let-In construction as a statement). Instead, it only marks the variable that it seens in the hash-table. But what if it was already in the hash-table? Shouldn’t we check that and if we find it, and if not associated to the same value, then returning false?

Currently, I have the impression that if the second/third/etc redefinitions are being done in a Let-In Statement, that will pass the bar when it should not be considered in quasi-SSA form. But I’m sure I’ve missed something, as if many passes rely in this, we would probably have seen many problems if this verification was too weak.

I’ll try to define some toy TIR programs to check that in practice, but meanwhile if someone has any explanation I would be very happy to learn more about this.

Kind regards,

Franck

Hi everyone,

I had some time to look deeper at the two points I mentioned above.

  1. There is just one thing here that annoys me here: I had a look at this deep_equal() function, which is in fact an operator () on the class ExprDeepEqual (in include/tvm/tir/analysis.h), which is being implemented in deep_equal.cc. And I would have expected it to be wary of function calls, that could -despite being deeply_equal on the surface- lead to different value. I’m thinking of something like : Let x = funcWithSideEffect 42 in Let x = funcWithSideEffect 42 in x+x

x can seem to be associated to the same value in both cases (funcWithSideEffect 42) and deep_equal_() will answer yes, but if as its name suggests funcWithSideEffect() does some side effects which makes it such that it does not always return the same value when called with the same argument, then it won’t be in quasi-SSA form.

I did some check, and there really is an issue with the way verify_ssa.cc implements this “weak SSA” verification. In this “weak-SSA” form, variables are allowed to be redefined (only with let-in expressions) if their definitions are the same (in order to allow things like (Let x = 1 in x) + (Let x = 1 in x+1)).

But I would expect this notion of being “the same” to mean “will lead to the same result after computing”, not “being syntactically equal”, as this the spirit of the SSA form (and other passes could use this information to avoid some re-computations for instance).

But currently, verify_ssa implements the SSA verification by using the ExprDeepEqual check, which looks for a syntactical equality. So a function call (like f()) will of course be found deeply equal to itself. But that leads the verify SSA pass to think that (Let x = f() in x) + (Let x = f() in x) is in weak SSA-form, when to me it is not ! (unless we know or can prove that the function ‘f’ has no side effects).

You can try this code to easily verify what I’m saying:

    RelayExpr fun = GlobalVar("fun");
    Array<PrimExpr> emptyArray;
    // Function call : f()
    Call call = Call(DataType::Int(32), fun, emptyArray);

    // 1 - What does ExprDeepEqual tells?
    ExprDeepEqual deep_equal_;
    if (deep_equal_(call, call))
    {
        std::cout << "Yes, f() is deeply equal to f()" << std::endl;
    }
    else
    {
        std::cout << "No, f() is NOT deeply equal to f()" << std::endl;
    }

    // 2 - What does VerifySSA tells?
    Var x("x");
    // Let x = f() in x
    Let let1 = Let(x, call, x);
    // Let x = f() in x
    Let let2 = Let(x, call, x);

    // (Let x = f() in x) + (Let x = f() in x)
    PrimExpr sum = Add(let1, let2);

    Var buffer("Mem");
    Var i1("i1");
    // Mem[i1] = (Let x = f() in x) + (Let x = f() in x);
    Stmt progStmt = Store(buffer, sum, i1, IntImm(DataType::Int(32), 1));

    // Just a main function calling progStmt
    Array<tir::Var> emptyVars;
    PrimFunc prog = PrimFunc(emptyVars, progStmt);

    if(VerifySSA(prog))
    {
        std::cout << "Handwritten prog Is SSA!" << std::endl;
    }
    else
    {
        std::cout << "Handwritten prog is NOT SSA" << std::endl;
    }

Which will tell that f() and f() are deeply equal (which is fine), and that the program is in quasi SSA form (which is wrong).

If you tell me that I am correct thinking that VerifySSA() should not return true here, then I will create a proper issue for that and will then try to fix it myself to help. And just to be clear, the problem does not come from ExprDeepEqual, as it is right to tell that f() is deeply equal to f(). It is just that VerifySSA() should be stricter than that.

  1. Another thing that I don’t understand is why the same verification is not performed for a LetStmtNode (i.e. the Let-In construction as a statement). Instead, it only marks the variable that it seens in the hash-table. But what if it was already in the hash-table? Shouldn’t we check that and if we find it, and if not associated to the same value, then returning false?

After looking again at this second point, I was wrong on this one and everything is good regarding this.

It’s just that this “weak-SSA form” allows variables redefinition for Let-In expression (but not for Let-in statements!) as long as the definitions are the same. But for a Let-in statement, it is forbidden to have any kind of redefinition, and this is checked carefully by MarkDef(), which is called systematically. So there is no problem here.

Thanks!