Skip to content

Commit eb25db1

Browse files
authored
[TIRx][RISC-V] Use scalable RVV loops for fixed vectorize (#19776)
This PR improves TIRx vectorization for RISC-V RVV targets. Fixed-width `T.vectorized` loops can be lowered to fixed LLVM vectors such as `<16 x float>`, which LLVM/RVV may scalarize into repeated scalar `flw/fsub.s/fsw` instructions. This PR rewrites fixed-width vectorized loops on RVV targets into scalable `T.vscale() * 4` chunks with lane masks, allowing LLVM to generate RVV load/store instructions instead. The change is limited to RISC-V RVV and does not enable the same automatic rewrite for Arm SVE. Tested on a RISC-V K3 board: Before: flw/fsub.s/fsw = 16/16/16, vle32/vse32 = 0/0 After: flw/fsub.s/fsw = 0/0/0, vle32/vse32 = 1/1 Also added a RISC-V LLVM codegen regression test.
1 parent 9011739 commit eb25db1

2 files changed

Lines changed: 130 additions & 10 deletions

File tree

src/tirx/transform/vectorize_loop.cc

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,27 @@ bool IsVScaleCall(const PrimExpr& expr) {
5454
return false;
5555
}
5656

57+
bool TargetHasRVV(Target target) {
58+
if (!target.defined()) return false;
59+
static auto target_has_feature_fn =
60+
tvm::ffi::Function::GetGlobalRequired("target.target_has_feature");
61+
return target_has_feature_fn("v", target).cast<bool>();
62+
}
63+
5764
// File-local helper: true if the target supports Variable-Length Array extensions
5865
// (AArch64 SVE or RISC-V V).
5966
bool TargetHasVLA(Target target) {
6067
if (!target.defined()) return false;
6168
bool has_vla = target->GetAttr<bool>("feature.has_sve").value_or(false);
62-
static auto target_has_feature_fn =
63-
tvm::ffi::Function::GetGlobalRequired("target.target_has_feature");
64-
has_vla |= target_has_feature_fn("v", target).cast<bool>();
69+
has_vla |= TargetHasRVV(target);
6570
return has_vla;
6671
}
72+
73+
bool ContainsCallNode(const Stmt& stmt) {
74+
return CheckContains::StmtContains(stmt, [](const PrimExpr& expr) {
75+
return expr.as<CallNode>() != nullptr;
76+
});
77+
}
6778
} // namespace
6879

6980
inline PrimExpr CreateNewLanes(bool is_scalable, int lanes_or_vscale_factor) {
@@ -132,7 +143,8 @@ bool EnableBufferLevelPredication(Target target) {
132143
*/
133144
class TryPredicateBufferAccesses : public StmtExprMutator {
134145
public:
135-
TryPredicateBufferAccesses() {}
146+
explicit TryPredicateBufferAccesses(bool allow_offset_predication)
147+
: allow_offset_predication_(allow_offset_predication) {}
136148

137149
/*!
138150
* \brief Run the pass to try to exact predicates.
@@ -157,7 +169,10 @@ class TryPredicateBufferAccesses : public StmtExprMutator {
157169
return {false, stmt};
158170
}
159171

160-
base_ = Downcast<Ramp>(lt->a)->base;
172+
Ramp pred_ramp = Downcast<Ramp>(lt->a);
173+
base_ = pred_ramp->base;
174+
stride_ = pred_ramp->stride;
175+
lanes_ = pred_ramp->lanes;
161176
limit_ = Downcast<Broadcast>(lt->b)->value;
162177

163178
// Now we can try to predicate
@@ -190,27 +205,49 @@ class TryPredicateBufferAccesses : public StmtExprMutator {
190205
}
191206
Ramp ramp = Downcast<Ramp>(node->indices[0]);
192207

193-
// The vectorized access pattern must match the base of the predicate
194-
if (!ffi::StructuralEqual()(ramp->base, base_)) {
208+
if (!ffi::StructuralEqual()(ramp->stride, stride_) ||
209+
!ffi::StructuralEqual()(ramp->lanes, lanes_)) {
195210
return node;
196211
}
197212

213+
bool same_base = ffi::StructuralEqual()(ramp->base, base_);
214+
if (!same_base) {
215+
// The lane mask describes which lanes are active, independent of the
216+
// memory base. This covers accesses such as A[offset + i] guarded by
217+
// a predicate over i.
218+
if (!allow_offset_predication_) {
219+
return node;
220+
}
221+
}
222+
198223
DataType buf_predicate_dtype =
199224
DataType(DataType::kUInt, 1, ramp->dtype.get_lanes_or_vscale_factor(),
200225
ramp->dtype.is_scalable_vector());
201226
Call lane_mask = Call(buf_predicate_dtype, builtin::get_active_lane_mask(), {base_, limit_});
202227

203228
num_accesses_rewritten_ += 1;
204229
auto writer = node.CopyOnWrite();
205-
writer->predicate = lane_mask;
230+
if (node->predicate.defined() && allow_offset_predication_) {
231+
// Buffer predicates are uint1 lane masks, so mask merging uses bitwise
232+
// and rather than logical &&.
233+
writer->predicate = node->predicate.value() & lane_mask;
234+
} else {
235+
writer->predicate = lane_mask;
236+
}
206237
return node;
207238
}
208239

209240
/*! \brief The variable base expr of the predicate. */
210241
PrimExpr base_;
242+
/*! \brief The lane stride of the predicate. */
243+
PrimExpr stride_;
244+
/*! \brief The lane count of the predicate. */
245+
PrimExpr lanes_;
211246
/*! \brief The limit of the predicate. The expr specifies the upper bound of the base's
212247
* evaluated value. */
213248
PrimExpr limit_;
249+
/*! \brief Whether to predicate offset buffer accesses that use the same lane layout. */
250+
bool allow_offset_predication_;
214251
/*! \brief The number of buffer accesses in the stmt we will analyze. */
215252
size_t num_accesses_analyzed_ = 0;
216253
/*! \brief The number of buffer accesses rewritten with predicates. */
@@ -819,7 +856,7 @@ class Vectorizer : public StmtMutator, public ExprFunctor<PrimExpr(const PrimExp
819856
if (EnableBufferLevelPredication(target_) &&
820857
condition.dtype().is_scalable_or_fixed_length_vector() && !else_case.defined()) {
821858
std::pair<bool, Stmt> success_stmt_pair =
822-
TryPredicateBufferAccesses().Run(then_case, condition);
859+
TryPredicateBufferAccesses(TargetHasRVV(target_)).Run(then_case, condition);
823860
bool can_remove_if_then_else = success_stmt_pair.first;
824861
if (can_remove_if_then_else) {
825862
return success_stmt_pair.second;
@@ -975,12 +1012,19 @@ class LoopVectorizer : public StmtMutator {
9751012
if (op->kind == ForKind::kVectorized) {
9761013
auto* extent_as_int = op->extent.as<IntImmNode>();
9771014

1015+
TVM_FFI_ICHECK(is_zero(op->min));
1016+
// General calls still have vectorization paths that query a compile-time
1017+
// lane count, so keep them on the existing fixed-width path for now.
1018+
if (extent_as_int && extent_as_int->value > 1 && TargetHasRVV(target_) &&
1019+
!ContainsCallNode(op->body)) {
1020+
return VectorizeFixedLoopForRVV(op, extent_as_int->value);
1021+
}
1022+
9781023
if (!extent_as_int || extent_as_int->value < 1) {
9791024
bool is_scalable_expr = CheckContains::ExprContains(op->extent, IsVScaleCall);
9801025
TVM_FFI_ICHECK(is_scalable_expr && TargetHasVLA(target_))
9811026
<< "Failed to vectorize loop with extent " << op->extent << " for target " << target_;
9821027
}
983-
TVM_FFI_ICHECK(is_zero(op->min));
9841028
return Vectorizer(op->loop_var, op->extent, target_)(op->body);
9851029
} else {
9861030
return StmtMutator::VisitStmt_(op);
@@ -999,6 +1043,39 @@ class LoopVectorizer : public StmtMutator {
9991043
}
10001044

10011045
private:
1046+
Stmt VectorizeFixedLoopForRVV(const ForNode* op, int64_t extent) {
1047+
// Match the existing TIRx scalable-vector convention. LLVM/RVV still
1048+
// selects the runtime vector length with vsetvli.
1049+
static constexpr int kDefaultVScaleFactor = 4;
1050+
DataType index_dtype = op->loop_var->dtype;
1051+
PrimExpr zero = make_const(index_dtype, 0);
1052+
PrimExpr fixed_extent = make_const(index_dtype, extent);
1053+
PrimExpr scalable_lanes = CreateNewLanes(/*is_scalable=*/true, kDefaultVScaleFactor);
1054+
DataType lane_dtype = scalable_lanes.dtype();
1055+
PrimExpr scalable_lanes_index = scalable_lanes;
1056+
if (scalable_lanes_index.dtype() != index_dtype) {
1057+
scalable_lanes_index = Cast(index_dtype, scalable_lanes_index);
1058+
}
1059+
PrimExpr num_chunks = ceildiv(fixed_extent, scalable_lanes_index);
1060+
1061+
Var outer(op->loop_var->name_hint + ".vla.o", index_dtype);
1062+
Var inner(op->loop_var->name_hint + ".vla.i", lane_dtype);
1063+
PrimExpr inner_index = inner;
1064+
if (inner_index.dtype() != index_dtype) {
1065+
inner_index = Cast(index_dtype, inner_index);
1066+
}
1067+
PrimExpr index = outer * scalable_lanes_index + inner_index;
1068+
Stmt body = Substitute(op->body, {{op->loop_var, index}});
1069+
Stmt guarded_body = IfThenElse(index < fixed_extent, body, std::nullopt, op->span);
1070+
Stmt vector_loop =
1071+
For(inner, make_const(lane_dtype, 0), scalable_lanes, ForKind::kVectorized, guarded_body,
1072+
std::nullopt, op->annotations, std::nullopt, op->span);
1073+
Stmt loop = For(outer, zero, num_chunks, ForKind::kSerial, vector_loop, std::nullopt, {},
1074+
std::nullopt, op->span);
1075+
1076+
return this->VisitStmt(loop);
1077+
}
1078+
10021079
Target target_ = Target::Current();
10031080
};
10041081

tests/python/codegen/test_target_codegen_riscv.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# under the License.
1717
# ruff: noqa: E501, F841
1818

19+
import re
1920
import pytest
2021

2122
import tvm
@@ -113,5 +114,47 @@ def rvv_with_vscale(A_handle: T.handle, B_handle: T.handle, C_handle: T.handle):
113114
f = tvm.tirx.build(rvv_with_vscale, target)
114115

115116

117+
@pytest.mark.skipif(not env.has_llvm_min_version(14), reason="need llvm >= 14")
118+
def test_rvv_fixed_width_vectorized_loop_uses_scalable_chunks():
119+
@T.prim_func(s_tir=True)
120+
def fixed16_negative(
121+
A: T.Buffer((14, 23, 67, 99), "float32"),
122+
B: T.Buffer((14, 23, 67, 99), "float32"),
123+
):
124+
for n, c, h, wo in T.grid(14, 23, 67, 7):
125+
for wi in T.vectorized(0, 16):
126+
if wo * 16 + wi < 99:
127+
B[n, c, h, wo * 16 + wi] = T.float32(0) - A[n, c, h, wo * 16 + wi]
128+
129+
@T.prim_func(s_tir=True)
130+
def fixed16_negative_int64(A: T.Buffer((16,), "float32"), B: T.Buffer((16,), "float32")):
131+
for wi in T.vectorized(T.int64(0), T.int64(16)):
132+
B[wi] = T.float32(0) - A[wi]
133+
134+
target = tvm.target.Target(
135+
{
136+
"kind": "llvm",
137+
"device": "riscv_cpu",
138+
"mtriple": "riscv64-linux-gnu",
139+
"mcpu": "generic-rv64",
140+
"mattr": ["+64bit", "+a", "+c", "+d", "+f", "+m", "+v"],
141+
}
142+
)
143+
144+
def check_codegen(func):
145+
with target:
146+
f = tvm.tirx.build(func, target)
147+
148+
assembly = f.inspect_source("asm")
149+
assert "vle32.v" in assembly
150+
assert "vse32.v" in assembly
151+
assert not re.search(r"\bflw\b", assembly)
152+
assert not re.search(r"\bfsub\.s\b", assembly)
153+
assert not re.search(r"\bfsw\b", assembly)
154+
155+
check_codegen(fixed16_negative)
156+
check_codegen(fixed16_negative_int64)
157+
158+
116159
if __name__ == "__main__":
117160
tvm.testing.main()

0 commit comments

Comments
 (0)