[llvm-branch-commits] [RISCV] Attach VLMAX range attribute for vsetvli/vsetvlimax in InstCombine (PR #218652)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Aug 25 03:04:27 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
@llvm/pr-subscribers-backend-risc-v
Author: Pengcheng Wang (wangpc-pp)
<details>
<summary>Changes</summary>
Attach a range return attribute describing the result when InstCombine visits
a `riscv_vsetvli/vsetvlimax` intrinsic. `VLMAX = VLEN * LMUL / SEW`, and the
subtarget's real VLEN bounds give a known range for VLMAX.
* vsetvlimax sets vl = VLMAX, so the result spans the whole VLMAX range.
* vsetvli sets vl = f(AVL, VLMAX) with 0 <= vl <= min(AVL, VLMAX). The
result equals AVL only when AVL cannot exceed the smallest possible VLMAX;
for any larger (or non-constant) AVL, vl may shrink below VLMAX all the way
down to 0, so only the VLMAX-derived upper bound is sound. We must not
claim a VLMAX lower bound in that case.
Materializing the range as an attribute lets the generic value analyses
(computeKnownBits, computeConstantRange, isKnownNonZero) reason about the
result through CallBase::getRange() without target-specific knowledge, and
uses the precise per-subtarget VLEN instead of the architectural maximum.
The IR verifier guarantees these intrinsics have an XLen result and constant
VSEW/VLMUL operands encoding a valid SEW/LMUL pair, so decoding the vtype here
needs no defensive validation and cannot hit the decodeVSEW / decodeVLMUL
asserts.
Assisted-by: TRAE CLI (Opus 4.8)
Co-authored-by: TRAE CLI <traecli@<!-- -->bytedance.com>
---
Full diff: https://github.com/llvm/llvm-project/pull/218652.diff
3 Files Affected:
- (modified) llvm/lib/Target/RISCV/RISCVTargetTransformInfo.cpp (+52)
- (added) llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvli-range.ll (+128)
- (added) llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvlimax-range.ll (+107)
``````````diff
diff --git a/llvm/lib/Target/RISCV/RISCVTargetTransformInfo.cpp b/llvm/lib/Target/RISCV/RISCVTargetTransformInfo.cpp
index ad53632a9cf7d..bf920faa1c066 100644
--- a/llvm/lib/Target/RISCV/RISCVTargetTransformInfo.cpp
+++ b/llvm/lib/Target/RISCV/RISCVTargetTransformInfo.cpp
@@ -3752,6 +3752,58 @@ bool RISCVTTIImpl::shouldCopyAttributeWhenOutliningFrom(
std::optional<Instruction *>
RISCVTTIImpl::instCombineIntrinsic(InstCombiner &IC, IntrinsicInst &II) const {
+ // Attach a range return attribute describing the result of vsetvli/vsetvlimax
+ // so generic value analyses can reason about it. The verifier guarantees an
+ // XLen result and constant VSEW/VLMUL encoding a valid vtype, so no defensive
+ // validation is needed here.
+ if (II.getIntrinsicID() == Intrinsic::riscv_vsetvli ||
+ II.getIntrinsicID() == Intrinsic::riscv_vsetvlimax) {
+ bool HasAVL = II.getIntrinsicID() == Intrinsic::riscv_vsetvli;
+ unsigned Offset = HasAVL ? 1 : 0;
+ unsigned Width = II.getType()->getScalarSizeInBits();
+ ConstantRange VLenRange(APInt(Width, ST->getRealMinVLen()),
+ APInt(Width, ST->getRealMaxVLen()) + 1);
+
+ uint64_t VSEW = cast<ConstantInt>(II.getArgOperand(Offset))->getZExtValue();
+ auto VLMUL = static_cast<RISCVVType::VLMUL>(
+ cast<ConstantInt>(II.getArgOperand(Offset + 1))->getZExtValue());
+ unsigned SEW = RISCVVType::decodeVSEW(VSEW);
+ unsigned Ratio = RISCVVType::getSEWLMULRatio(SEW, VLMUL);
+
+ // VLMAX = VLEN / (SEW / LMUL), clamped to >= 1 for any usable vtype.
+ ConstantRange VLMAXRange =
+ VLenRange.udiv(ConstantRange(APInt(Width, Ratio)))
+ .umax(ConstantRange(APInt(Width, 1)));
+
+ // vsetvlimax returns exactly VLMAX; vsetvli returns vl with
+ // 0 <= vl <= min(AVL, VLMAX). vl == AVL only when AVL <= the smallest
+ // possible VLMAX; otherwise vl can shrink below VLMAX (to 0 at runtime), so
+ // only the VLMAX upper bound is sound.
+ ConstantRange VLRange = VLMAXRange;
+ if (HasAVL) {
+ APInt MaxVL = VLMAXRange.getUnsignedMax();
+ if (auto *AVL = dyn_cast<ConstantInt>(II.getArgOperand(0))) {
+ const APInt &C = AVL->getValue();
+ if (C.ule(VLMAXRange.getUnsignedMin()))
+ VLRange = ConstantRange(C);
+ else
+ VLRange = ConstantRange::getNonEmpty(APInt::getZero(Width),
+ (C.ult(MaxVL) ? C : MaxVL) + 1);
+ } else {
+ VLRange = ConstantRange::getNonEmpty(APInt::getZero(Width), MaxVL + 1);
+ }
+ }
+
+ ConstantRange OldRange =
+ II.getRange().value_or(ConstantRange::getFull(Width));
+ ConstantRange NewRange = VLRange.intersectWith(OldRange);
+ if (NewRange != OldRange) {
+ II.addRangeRetAttr(NewRange);
+ return &II;
+ }
+ return {};
+ }
+
// If all operands of a vmv.v.x are constant, fold a bitcast(vmv.v.x) to scale
// the vmv.v.x, enabling removal of the bitcast. The transform helps avoid
// creating redundant masks.
diff --git a/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvli-range.ll b/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvli-range.ll
new file mode 100644
index 0000000000000..3401959a586d2
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvli-range.ll
@@ -0,0 +1,128 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -p instcombine -mtriple=riscv64 -mattr=+v -S %s | FileCheck %s --check-prefixes=CHECK,VLEN128
+; RUN: opt -p instcombine -mtriple=riscv64 -mattr=+v,+zvl512b -S %s | FileCheck %s --check-prefixes=CHECK,VLEN512
+
+; RISCVTTIImpl::instCombineIntrinsic attaches a range return attribute to
+; vsetvli. The result is vl = f(AVL, VLMAX) with 0 <= vl <= min(AVL, VLMAX).
+; The result equals AVL only when AVL cannot exceed the smallest possible VLMAX;
+; for any larger (or runtime) AVL, vl may shrink below VLMAX all the way to 0,
+; so only the VLMAX-derived upper bound is sound.
+
+;------------------------------------------------------------------------------
+; Constant AVL.
+;------------------------------------------------------------------------------
+
+; AVL below the smallest VLMAX (16 at VLEN128, 64 at VLEN512): vl == AVL exactly.
+define i64 @vsetvli_const_avl_below_min() {
+; CHECK-LABEL: define i64 @vsetvli_const_avl_below_min(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 5, 6) i64 @llvm.riscv.vsetvli.i64(i64 5, i64 0, i64 0)
+; CHECK-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 5, i64 0, i64 0)
+ ret i64 %vl
+}
+
+; AVL == 16: still <= the smallest VLMAX on both subtargets, so vl == AVL.
+define i64 @vsetvli_const_avl_at_min128() {
+; CHECK-LABEL: define i64 @vsetvli_const_avl_at_min128(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 16, 17) i64 @llvm.riscv.vsetvli.i64(i64 16, i64 0, i64 0)
+; CHECK-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 16, i64 0, i64 0)
+ ret i64 %vl
+}
+
+; AVL == 20 sits in (MinVLMAX, ...) at VLEN128, so vl is only known to be
+; [0, 20]; at VLEN512 it is still below VLMAX (64), so vl == 20 exactly.
+define i64 @vsetvli_const_avl_mid() {
+; VLEN128-LABEL: define i64 @vsetvli_const_avl_mid(
+; VLEN128-SAME: ) #[[ATTR0]] {
+; VLEN128-NEXT: [[VL:%.*]] = call range(i64 0, 21) i64 @llvm.riscv.vsetvli.i64(i64 20, i64 0, i64 0)
+; VLEN128-NEXT: ret i64 [[VL]]
+;
+; VLEN512-LABEL: define i64 @vsetvli_const_avl_mid(
+; VLEN512-SAME: ) #[[ATTR0]] {
+; VLEN512-NEXT: [[VL:%.*]] = call range(i64 20, 21) i64 @llvm.riscv.vsetvli.i64(i64 20, i64 0, i64 0)
+; VLEN512-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 20, i64 0, i64 0)
+ ret i64 %vl
+}
+
+; AVL far above the largest VLMAX (8192): vl is capped by VLMAX, giving the
+; VLEN-independent range [0, 8192].
+define i64 @vsetvli_const_avl_above_max() {
+; CHECK-LABEL: define i64 @vsetvli_const_avl_above_max(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 0, 8193) i64 @llvm.riscv.vsetvli.i64(i64 100000, i64 0, i64 0)
+; CHECK-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 100000, i64 0, i64 0)
+ ret i64 %vl
+}
+
+; A small constant AVL (vl == 5) lets a masking AND disappear.
+define i64 @vsetvli_const_avl_and() {
+; CHECK-LABEL: define i64 @vsetvli_const_avl_and(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: ret i64 5
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 5, i64 0, i64 0)
+ %m = and i64 %vl, 7
+ ret i64 %m
+}
+
+; i32 result: small constant AVL is exact regardless of VLEN.
+define i32 @vsetvli_i32_const_avl_below_min() {
+; CHECK-LABEL: define i32 @vsetvli_i32_const_avl_below_min(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i32 5, 6) i32 @llvm.riscv.vsetvli.i32(i32 5, i32 0, i32 0)
+; CHECK-NEXT: ret i32 [[VL]]
+;
+ %vl = call i32 @llvm.riscv.vsetvli.i32(i32 5, i32 0, i32 0)
+ ret i32 %vl
+}
+
+;------------------------------------------------------------------------------
+; Runtime AVL: vl in [0, VLMAX]. These guard against claiming an unsound VLMAX
+; lower bound, which would miscompile the compares below.
+;------------------------------------------------------------------------------
+
+; vl can be as small as 0 (AVL == 0), so this compare must NOT fold.
+define i1 @vsetvli_runtime_avl_eq0_not_folded(i64 %avl) {
+; CHECK-LABEL: define i1 @vsetvli_runtime_avl_eq0_not_folded(
+; CHECK-SAME: i64 [[AVL:%.*]]) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 0, 8193) i64 @llvm.riscv.vsetvli.i64(i64 [[AVL]], i64 0, i64 0)
+; CHECK-NEXT: [[C:%.*]] = icmp eq i64 [[VL]], 0
+; CHECK-NEXT: ret i1 [[C]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 %avl, i64 0, i64 0)
+ %c = icmp eq i64 %vl, 0
+ ret i1 %c
+}
+
+; vl can be below the smallest VLMAX (e.g. AVL == 3), so this must NOT fold.
+define i1 @vsetvli_runtime_avl_lt16_not_folded(i64 %avl) {
+; CHECK-LABEL: define i1 @vsetvli_runtime_avl_lt16_not_folded(
+; CHECK-SAME: i64 [[AVL:%.*]]) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 0, 8193) i64 @llvm.riscv.vsetvli.i64(i64 [[AVL]], i64 0, i64 0)
+; CHECK-NEXT: [[C:%.*]] = icmp samesign ult i64 [[VL]], 16
+; CHECK-NEXT: ret i1 [[C]]
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 %avl, i64 0, i64 0)
+ %c = icmp ult i64 %vl, 16
+ ret i1 %c
+}
+
+; vl <= VLMAX <= 8192 is sound, so an out-of-range compare DOES fold to false.
+define i1 @vsetvli_runtime_avl_gt_max_folds(i64 %avl) {
+; CHECK-LABEL: define i1 @vsetvli_runtime_avl_gt_max_folds(
+; CHECK-SAME: i64 [[AVL:%.*]]) #[[ATTR0]] {
+; CHECK-NEXT: ret i1 false
+;
+ %vl = call i64 @llvm.riscv.vsetvli.i64(i64 %avl, i64 0, i64 0)
+ %c = icmp ugt i64 %vl, 8192
+ ret i1 %c
+}
diff --git a/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvlimax-range.ll b/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvlimax-range.ll
new file mode 100644
index 0000000000000..95430da922f00
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvlimax-range.ll
@@ -0,0 +1,107 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -p instcombine -mtriple=riscv64 -mattr=+v -S %s | FileCheck %s --check-prefixes=CHECK,VLEN128
+; RUN: opt -p instcombine -mtriple=riscv64 -mattr=+v,+zvl512b -S %s | FileCheck %s --check-prefixes=CHECK,VLEN512
+
+; RISCVTTIImpl::instCombineIntrinsic attaches a range return attribute to
+; vsetvlimax. The result is exactly VLMAX = VLEN * LMUL / SEW, so the range uses
+; the subtarget's real VLEN bounds: a wider minimum VLEN (zvl512b) yields a
+; tighter lower bound.
+
+; e8m1: ratio 8, VLMAX = VLEN / 8.
+define i64 @vsetvlimax_e8m1() {
+; VLEN128-LABEL: define i64 @vsetvlimax_e8m1(
+; VLEN128-SAME: ) #[[ATTR0:[0-9]+]] {
+; VLEN128-NEXT: [[VL:%.*]] = call range(i64 16, 8193) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+; VLEN128-NEXT: ret i64 [[VL]]
+;
+; VLEN512-LABEL: define i64 @vsetvlimax_e8m1(
+; VLEN512-SAME: ) #[[ATTR0:[0-9]+]] {
+; VLEN512-NEXT: [[VL:%.*]] = call range(i64 64, 8193) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+; VLEN512-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+ ret i64 %vl
+}
+
+; e8m8: ratio 1, VLMAX = VLEN.
+define i64 @vsetvlimax_e8m8() {
+; VLEN128-LABEL: define i64 @vsetvlimax_e8m8(
+; VLEN128-SAME: ) #[[ATTR0]] {
+; VLEN128-NEXT: [[VL:%.*]] = call range(i64 128, 65537) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 3)
+; VLEN128-NEXT: ret i64 [[VL]]
+;
+; VLEN512-LABEL: define i64 @vsetvlimax_e8m8(
+; VLEN512-SAME: ) #[[ATTR0]] {
+; VLEN512-NEXT: [[VL:%.*]] = call range(i64 512, 65537) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 3)
+; VLEN512-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 3)
+ ret i64 %vl
+}
+
+; e64m1: ratio 64, VLMAX = VLEN / 64.
+define i64 @vsetvlimax_e64m1() {
+; VLEN128-LABEL: define i64 @vsetvlimax_e64m1(
+; VLEN128-SAME: ) #[[ATTR0]] {
+; VLEN128-NEXT: [[VL:%.*]] = call range(i64 2, 1025) i64 @llvm.riscv.vsetvlimax.i64(i64 3, i64 0)
+; VLEN128-NEXT: ret i64 [[VL]]
+;
+; VLEN512-LABEL: define i64 @vsetvlimax_e64m1(
+; VLEN512-SAME: ) #[[ATTR0]] {
+; VLEN512-NEXT: [[VL:%.*]] = call range(i64 8, 1025) i64 @llvm.riscv.vsetvlimax.i64(i64 3, i64 0)
+; VLEN512-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 3, i64 0)
+ ret i64 %vl
+}
+
+; e64mf8: ratio 512. At small VLEN the lower bound underflows to 0, so VLMAX is
+; clamped to be non-zero; the result is VLEN-independent here.
+define i64 @vsetvlimax_e64mf8() {
+; CHECK-LABEL: define i64 @vsetvlimax_e64mf8(
+; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i64 1, 129) i64 @llvm.riscv.vsetvlimax.i64(i64 3, i64 5)
+; CHECK-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 3, i64 5)
+ ret i64 %vl
+}
+
+; The result is always >= the minimum VLMAX (>= 16), so uge 8 folds to true.
+define i1 @vsetvlimax_cmp_folds() {
+; CHECK-LABEL: define i1 @vsetvlimax_cmp_folds(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: ret i1 true
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+ %c = icmp uge i64 %vl, 8
+ ret i1 %c
+}
+
+; The result fits in 14 bits (<= 8192), so masking the high bits is a nop.
+define i64 @vsetvlimax_and_high_bits() {
+; VLEN128-LABEL: define i64 @vsetvlimax_and_high_bits(
+; VLEN128-SAME: ) #[[ATTR0]] {
+; VLEN128-NEXT: [[VL:%.*]] = call range(i64 16, 8193) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+; VLEN128-NEXT: ret i64 [[VL]]
+;
+; VLEN512-LABEL: define i64 @vsetvlimax_and_high_bits(
+; VLEN512-SAME: ) #[[ATTR0]] {
+; VLEN512-NEXT: [[VL:%.*]] = call range(i64 64, 8193) i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+; VLEN512-NEXT: ret i64 [[VL]]
+;
+ %vl = call i64 @llvm.riscv.vsetvlimax.i64(i64 0, i64 0)
+ %m = and i64 %vl, 16383
+ ret i64 %m
+}
+
+; i32 result: e64mf8 clamps to a VLEN-independent [1, 128].
+define i32 @vsetvlimax_i32_e64mf8() {
+; CHECK-LABEL: define i32 @vsetvlimax_i32_e64mf8(
+; CHECK-SAME: ) #[[ATTR0]] {
+; CHECK-NEXT: [[VL:%.*]] = call range(i32 1, 129) i32 @llvm.riscv.vsetvlimax.i32(i32 3, i32 5)
+; CHECK-NEXT: ret i32 [[VL]]
+;
+ %vl = call i32 @llvm.riscv.vsetvlimax.i32(i32 3, i32 5)
+ ret i32 %vl
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/218652
More information about the llvm-branch-commits
mailing list