[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