Lunderberg commented on code in PR #16966:
URL: https://github.com/apache/tvm/pull/16966#discussion_r1594285519


##########
include/tvm/script/ir_builder/tir/ir.h:
##########
@@ -411,8 +411,10 @@ Var EnvThread(String thread_tag, DataType dtype = 
DataType::Int(32));
  * \param buffer The buffer.
  * \param value The value to be stored.
  * \param indices The indices location to be stored.
+ * \param predicate A vector mask of int1 values indicating which lanes of a 
vector are to be

Review Comment:
   Can we specify that the number of lanes for the `predicate` must be equal to 
the number of lanes in the `value`?



##########
tests/python/tir-transform/test_tir_transform_vectorize.py:
##########
@@ -488,5 +529,170 @@ def main(A: T.Buffer((16,), "float32")):
             tvm.tir.transform.VectorizeLoop()(Mod)
 
 
+def test_vectorize_and_predicate_all_buffer_loads_stores():
+    @T.prim_func
+    def before(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0 in T.serial(T.ceildiv(14, 4)):
+            for i_1 in T.vectorized(4):
+                if i_0 * 4 + i_1 < 14:
+                    B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
+
+    @T.prim_func
+    def expected(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": T.bool(True)})
+        for i_0 in range(4):
+            load_a = T.meta_var(
+                A.load(
+                    [T.Ramp(i_0 * 4, 1, 4)], 
predicate=T.get_active_lane_mask("int1x4", i_0 * 4, 14)
+                )
+            )
+            add_1 = T.meta_var(load_a + T.Broadcast(T.float32(1), 4))
+            B.store(
+                add_1,
+                [T.Ramp(i_0 * 4, 1, 4)],
+                predicate=T.get_active_lane_mask("int1x4", i_0 * 4, 14),
+            )
+
+    mod = tvm.IRModule.from_expr(before)
+    with 
tvm.transform.PassContext(config={"tir.enable_buffer_level_predication": True}):
+        after = tvm.tir.transform.VectorizeLoop()(mod)["main"]
+    tvm.ir.assert_structural_equal(after, expected)
+
+
+def test_vectorize_and_predicate_some_buffer_loads_stores():
+    # Currently revert to scalarizing the block if not all accesses
+    # have been predicated, otherwise incorrect code is generated.
+    @T.prim_func
+    def before(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0 in T.serial(T.ceildiv(14, 4)):
+            for i_1 in T.vectorized(4):
+                if i_0 * 4 + i_1 < 14:
+                    B[i_0 * 4 + i_1] = A[i_0] + 1.0
+
+    @T.prim_func
+    def expected(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": T.bool(True)})
+        for i_0, i_1_s in T.grid(4, 4):
+            if i_0 * 4 + i_1_s < 14:
+                B[i_0 * 4 + i_1_s] = A[i_0] + T.float32(1)
+
+    mod = tvm.IRModule.from_expr(before)
+    with 
tvm.transform.PassContext(config={"tir.enable_buffer_level_predication": True}):
+        after = tvm.tir.transform.VectorizeLoop()(mod)["main"]
+    tvm.ir.assert_structural_equal(after, expected)
+
+
+def test_vectorize_and_predicate_multiple_access_statements():
+    @T.prim_func
+    def before(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0 in T.serial(T.ceildiv(14, 4)):
+            for i_1 in T.vectorized(4):
+                if i_0 * 4 + i_1 < 14:
+                    A[i_0 * 4 + i_1] = 2.0
+                    B[i_0 * 4 + i_1] = 1.0
+
+    @T.prim_func
+    def expected(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": T.bool(True)})
+        for i_0 in range(4):
+            A.store(
+                T.Broadcast(T.float32(2), 4),
+                [T.Ramp(i_0 * 4, 1, 4)],
+                predicate=T.get_active_lane_mask("int1x4", i_0 * 4, 14),
+            )
+            B.store(
+                T.Broadcast(T.float32(1), 4),
+                [T.Ramp(i_0 * 4, 1, 4)],
+                predicate=T.get_active_lane_mask("int1x4", i_0 * 4, 14),
+            )
+
+    before_mod = tvm.IRModule.from_expr(before)
+    with 
tvm.transform.PassContext(config={"tir.enable_buffer_level_predication": True}):
+        after = tvm.tir.transform.VectorizeLoop()(before_mod)["main"]
+    tvm.ir.assert_structural_equal(after, expected)
+
+
+def test_vectorize_and_predicate_invalid_conditions():
+    @T.prim_func
+    def before(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0 in T.serial(T.ceildiv(14, 4)):
+            for i_1 in T.vectorized(4):
+                if i_0 * 4 + i_1 > 14:
+                    A[i_0 * 4 + i_1] = 2.0
+                if 14 < i_0 * 4 + i_1:
+                    A[i_0 * 4 + i_1] = 2.0
+                if i_0 * 4 + i_1 < i_0 * 4 + i_1:
+                    A[i_0 * 4 + i_1] = 2.0
+
+    @T.prim_func
+    def expected(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": T.bool(True)})
+        for i_0 in range(4):
+            for i_1_s in range(4):
+                if i_0 * 4 + i_1_s > 14:
+                    A[i_0 * 4 + i_1_s] = T.float32(2)
+            for i_1_s in range(4):
+                if 14 < i_0 * 4 + i_1_s:
+                    A[i_0 * 4 + i_1_s] = T.float32(2)
+            for i_1_s in range(4):
+                if i_0 * 4 + i_1_s < i_0 * 4 + i_1_s:
+                    A[i_0 * 4 + i_1_s] = T.float32(2)
+
+    before_mod = tvm.IRModule.from_expr(before)
+    with 
tvm.transform.PassContext(config={"tir.enable_buffer_level_predication": True}):
+        after = tvm.tir.transform.VectorizeLoop()(before_mod)["main"]
+    tvm.ir.assert_structural_equal(after, expected)
+
+
+def test_vectorize_with_explicitly_disabled_buffer_level_predication():
+    # Since the target is has the SVe feature, buffer level predication is 
enabled
+    # by default. However, it has been explicitely disabled by the pass context
+    # option, so no buffer-level predicates should be added.
+    @T.prim_func
+    def before(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0 in T.serial(T.ceildiv(14, 4)):
+            for i_1 in T.vectorized(4):
+                if i_0 * 4 + i_1 < 14:
+                    B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
+
+    @T.prim_func
+    def expected(a: T.handle, b: T.handle):
+        A = T.match_buffer(a, (16,), "float32")
+        B = T.match_buffer(b, (16,), "float32")
+        T.func_attr({"global_symbol": "main", "tir.noalias": True})
+        for i_0, i_1_s in T.grid(4, 4):
+            if i_0 * 4 + i_1_s < 14:
+                B[i_0 * 4 + i_1_s] = A[i_0 * 4 + i_1_s] + T.float32(1)
+
+    mod = tvm.IRModule.from_expr(before)
+    with 
tvm.transform.PassContext(config={"tir.enable_buffer_level_predication": 
False}):
+        with tvm.target.Target("llvm -mtriple=aarch64-linux-gnu -mattr=+sve"):
+            after = tvm.tir.transform.VectorizeLoop()(mod)["main"]
+    tvm.ir.assert_structural_equal(after, expected)
+

Review Comment:
   Can we also add test cases for the target override both in the function 
attributes (`T.func_attr({'target': T.target(...)})`) and in an attr stmt 
(`T.attr(T.target(...), "target", 0)`)?
   
   
([Link](https://github.com/apache/tvm/blob/main/tests/python/tir-transform/test_tir_transform_annotate_device_regions.py#L27)
 to example for the TVMScript syntax.)



##########
src/tir/transforms/vectorize_loop.cc:
##########
@@ -72,6 +72,126 @@ inline PrimExpr BroadcastTo(PrimExpr e, int lanes, bool 
is_scalable) {
   return Broadcast(e, CreateNewLanes(is_scalable, lanes));
 }
 
+bool EnableBufferLevelPredication() {

Review Comment:
   This will require updating `arith::TargetHasSVE()` as well.  Apologies for 
not catching it earlier in #16893.



##########
include/tvm/tir/expr.h:
##########
@@ -675,7 +679,8 @@ class BufferLoadNode : public PrimExprNode {
  */
 class BufferLoad : public PrimExpr {
  public:
-  TVM_DLL explicit BufferLoad(Buffer buffer, Array<PrimExpr> indices, Span 
span = Span());
+  TVM_DLL explicit BufferLoad(Buffer buffer, Array<PrimExpr> indices,
+                              PrimExpr predicate = PrimExpr(), Span span = 
Span());

Review Comment:
   Using `PrimExpr()` as the default would have an undefined object as the 
predicate.  While this is legal, it is rather unexpected.  While changing 
`PrimExpr` to be non-nullable would involve more breakage than I'd want to 
handle at the moment, we should avoid introducing additional cases where 
`PrimExpr` contains null.
   
   Changing this to `Optional<PrimExpr> predicate = NullOpt` would avoid this 
issue.



##########
include/tvm/tir/stmt.h:
##########
@@ -261,7 +265,7 @@ class BufferStoreNode : public StmtNode {
 class BufferStore : public Stmt {
  public:
   TVM_DLL explicit BufferStore(Buffer buffer, PrimExpr value, Array<PrimExpr> 
indices,
-                               Span span = Span());
+                               PrimExpr predicate = PrimExpr(), Span span = 
Span());

Review Comment:
   Same request here, `Optional<PrimExpr> predicate = NullOpt`.



##########
python/tvm/tir/expr.py:
##########
@@ -1093,20 +1093,27 @@ class BufferLoad(PrimExprWithOp):
         The buffer to be loaded.
 
     indices : List[PrimExpr]
-        The buffer indices.
+        The buffer indices to load values from.
 
     span : Optional[Span]
         The location of this expression in the source code.
+
+    predicate : Optional[PrimExpr]
+        A vector mask of int1 values indicating which lanes of a vector are to 
be loaded.

Review Comment:
   At first glance, the `int1` reads like a typo.  Can we explicitly call it 
`boolean values` instead?



##########
tests/python/codegen/test_target_codegen_aarch64.py:
##########
@@ -700,5 +700,31 @@ def before(a: T.handle):
     assert "get.active.lane.mask" in ll
 
 
[email protected](

Review Comment:
   This validates that we have the correct output for architectures that 
support SVE, but it doesn't test the behavior of other targets that do not 
(yet) support predicated loads/stores.  While the `VectorizeLoop` pass would 
only insert a predicated load/store for targets that support it, the predicated 
load/store could still be generated in hand-written kernels, or through other 
transforms in the future.
   
   Can we add a test, parametrized over each target tested in CI, which 
attempts to compile a PrimFunc containing predicated loads/stores?  For each 
target that supports sve, `tvm.build` should compile without error, and for 
each target that does not, `tvm.build` should raise an exception.
   
   



##########
src/tir/transforms/vectorize_loop.cc:
##########
@@ -72,6 +72,126 @@ inline PrimExpr BroadcastTo(PrimExpr e, int lanes, bool 
is_scalable) {
   return Broadcast(e, CreateNewLanes(is_scalable, lanes));
 }
 
+bool EnableBufferLevelPredication() {

Review Comment:
   I like the fallback, where it checks whether a target supports SVE in order 
to apply the vectorization if not specified in the pass context.  However, this 
function should accept a `Target` argument from the calling scope rather than 
using `Target::Current()`, because the `Target::Current()` may be overridden. 
   
   The override can be either at the function level 
(`prim_func->GetAttr<Target>(tvm::attr::kTarget)`), or for individual blocks of 
TIR (an `AttrStmt` with `attr_stmt->attr_key==tvm::attr::kTarget`).



##########
src/target/llvm/codegen_llvm.cc:
##########
@@ -1768,11 +1774,17 @@ llvm::Value* CodeGenLLVM::VisitExpr_(const 
BufferLoadNode* op) {
 
   std::vector<llvm::Value*> loads;
 
-  auto make_load = [this, &loads](TypedPointer buffer_ptr, int /* subelement_i 
*/, int alignment,
-                                  bool is_volatile) {
+  auto make_load = [this, &loads](TypedPointer buffer_ptr, int /* subelement_i 
*/,
+                                  llvm::Value* predicate, int alignment, bool 
is_volatile) {
 #if TVM_LLVM_VERSION >= 110
-    auto load = builder_->CreateAlignedLoad(buffer_ptr.type, buffer_ptr.addr,
-                                            llvm::Align(alignment), 
is_volatile);
+    llvm::Instruction* load = nullptr;
+    if (predicate != NULL) {
+      load = builder_->CreateMaskedLoad(buffer_ptr.type, buffer_ptr.addr, 
llvm::Align(alignment),
+                                        predicate);
+    } else {
+      load = builder_->CreateAlignedLoad(buffer_ptr.type, buffer_ptr.addr, 
llvm::Align(alignment),
+                                         is_volatile);
+    }
 #elif TVM_LLVM_VERSION >= 80
     auto load =

Review Comment:
   The PR only adds `CreateMaskedLoad` when `TVM_LLVM_VERSION >= 110`.  If 
somebody is using an older version of LLVM, it would silently ignore the 
predicate for the load/store.  We should either support it, or throw an 
exception.
   
   It looks like `CreateMaskedLoad` has been supported in LLVM since [this 
commit](https://github.com/llvm/llvm-project/commit/f1de34b84dea91b5060ee0fafbadaad5deaf199c),
 so I'd lean toward adding it in the other `#elif` branches.



##########
python/tvm/tir/buffer.py:
##########
@@ -141,6 +141,57 @@ def vstore(self, begin, value):
         begin = (begin,) if isinstance(begin, (int, PrimExpr)) else begin
         return _ffi_api.BufferVStore(self, begin, value)  # type: ignore
 
+    def load(self, indices, predicate=None):

Review Comment:
   These look very similar to the existing `vload` and `vstore` methods.  
Should they be extended instead with a `predicate` argument instead, rather 
than introducing new methods?



##########
python/tvm/tir/stmt.py:
##########
@@ -224,24 +224,29 @@ class BufferStore(Stmt):
     indices : List[PrimExpr]
         The indices location to be stored.
 
+    predicate : Optional[PrimExpr]
+        A vector mask of int1 values indicating which lanes of a vector are to 
be stored.

Review Comment:
   Same nitpick here, using `boolean` instead of `int1`.



##########
src/tir/ir/stmt.cc:
##########
@@ -458,7 +458,8 @@ 
TVM_REGISTER_GLOBAL("tir.Evaluate").set_body_typed([](PrimExpr value, Span span)
 TVM_REGISTER_NODE_TYPE(EvaluateNode);
 
 // BufferStore
-BufferStore::BufferStore(Buffer buffer, PrimExpr value, Array<PrimExpr> 
indices, Span span) {
+BufferStore::BufferStore(Buffer buffer, PrimExpr value, Array<PrimExpr> 
indices, PrimExpr predicate,
+                         Span span) {
   ICHECK_EQ(buffer->shape.size(), indices.size())

Review Comment:
   We should add an assert here, that if `predicate` is defined, the number of 
lanes in `value` and in `predicate` are identical.



##########
src/target/llvm/codegen_llvm.cc:
##########
@@ -1768,11 +1774,17 @@ llvm::Value* CodeGenLLVM::VisitExpr_(const 
BufferLoadNode* op) {
 
   std::vector<llvm::Value*> loads;
 
-  auto make_load = [this, &loads](TypedPointer buffer_ptr, int /* subelement_i 
*/, int alignment,
-                                  bool is_volatile) {
+  auto make_load = [this, &loads](TypedPointer buffer_ptr, int /* subelement_i 
*/,
+                                  llvm::Value* predicate, int alignment, bool 
is_volatile) {
 #if TVM_LLVM_VERSION >= 110
-    auto load = builder_->CreateAlignedLoad(buffer_ptr.type, buffer_ptr.addr,
-                                            llvm::Align(alignment), 
is_volatile);
+    llvm::Instruction* load = nullptr;
+    if (predicate != NULL) {
+      load = builder_->CreateMaskedLoad(buffer_ptr.type, buffer_ptr.addr, 
llvm::Align(alignment),
+                                        predicate);

Review Comment:
   It looks like this ignores the `is_volatile` argument.  Can we provide 
`is_volatile` to `CreateMaskedLoad`?  If not, we should specify that these two 
arguments are incompatible and throw an exception if they are both provided.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to