Skip to content

[mlir][tensor][affine] tensor.pad and affine.delinearize_index / semi-affine affine.apply are still Pure -> LICM introduces SIGFPE (follow-up to #233213) #241711

Open

Description

@wildoranges

This is a follow-up to #233213. That issue reported four groups of ops that are Pure (and therefore AlwaysSpeculatable) although they can trap, so -loop-invariant-code-motion hoists them out of loops that may run zero times. #235770 fixed arith.floordivsi and llvm.urem/llvm.srem, and the issue was closed. The other two groups are still Pure:

  1. tensor.pad. It is Pure no matter what its padding region computes. tensor.generate, which has the same kind of body, is only RecursiveMemoryEffects.
    def Tensor_PadOp : Tensor_Op<"pad", [
    DeclareOpInterfaceMethods<OpAsmOpInterface, ["getAsmResultNames"]>,
    DeclareOpInterfaceMethods<ReifyRankedShapedTypeOpInterface, [
    "reifyResultShapes"]>,
    AttrSizedOperandSegments,
    Pure,
  2. affine.delinearize_index. Its doc says "A dynamic basis element being 0 or negative causes undefined behavior". The same holds for affine.apply with a semi-affine map (floordiv/mod by a symbol). Both are Pure.
    def AffineDelinearizeIndexOp : Affine_Op<"delinearize_index",
    [Pure, Elementwise,

    Due to the constraints of affine maps, all the basis elements must
    be strictly positive. A dynamic basis element being 0 or negative causes
    undefined behavior.

In every reproducer below the loop runs zero times exactly when the divisor is 0, so the original program is UB-free. Without LICM, each one prints its untouched initial value. That value is -1; the index versions print it as 18446744073709551615. With --loop-invariant-code-motion, each one dies with SIGFPE.

($LLVM_BUILD is the LLVM/MLIR build directory.)

$ RUN="mlir-runner -e main -entry-point-result=void -shared-libs=$LLVM_BUILD/lib/libmlir_c_runner_utils.so,$LLVM_BUILD/lib/libmlir_runner_utils.so"

1. tensor.pad whose padding region divides

pad.mlir:

// tensor.pad is declared Pure (AlwaysSpeculatable + NoMemoryEffect) regardless of
// what its padding region computes (tensor.generate, with the same kind of body,
// is only RecursiveMemoryEffects and not speculatable).  Here the padding value
// is %x / %d, which is UB when %d == 0; the loop guarding the pad runs zero
// times in exactly that case, so the original program never divides by zero.
func.func @f(%t: tensor<2xi32>, %x: i32, %d: i32, %n: index, %out: memref<1xi32>) {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  scf.for %i = %c0 to %n step %c1 {
    %p = tensor.pad %t low[1] high[1] {
    ^bb0(%j: index):
      %v = arith.divsi %x, %d : i32
      tensor.yield %v : i32
    } : tensor<2xi32> to tensor<4xi32>
    %e = tensor.extract %p[%c0] : tensor<4xi32>
    memref.store %e, %out[%c0] : memref<1xi32>
  }
  return
}

func.func @main() {
  %c0 = arith.constant 0 : index
  %t = arith.constant dense<[5, 6]> : tensor<2xi32>
  %out = memref.alloca() : memref<1xi32>
  %m1 = arith.constant -1 : i32
  memref.store %m1, %out[%c0] : memref<1xi32>
  %x = arith.constant 7 : i32
  %d = arith.constant 0 : i32
  %n = arith.constant 0 : index
  call @f(%t, %x, %d, %n, %out) : (tensor<2xi32>, i32, i32, index, memref<1xi32>) -> ()
  %v = memref.load %out[%c0] : memref<1xi32>
  vector.print %v : i32
  return
}
$ LOWER_T="--one-shot-bufferize=bufferize-function-boundaries --buffer-deallocation-pipeline --convert-linalg-to-loops --expand-strided-metadata --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
$ mlir-opt pad.mlir $LOWER_T | $RUN
-1
$ mlir-opt pad.mlir --loop-invariant-code-motion | mlir-opt $LOWER_T | $RUN
Floating point exception (core dumped)      # exit code 136

2. affine.delinearize_index and semi-affine affine.apply

delinearize.mlir:

// affine.delinearize_index is Pure although its documentation says "A dynamic
// basis element being 0 or negative causes undefined behavior".  The loop below
// iterates %j over [0, %c): when %c == 0 it runs zero times, so the original
// program never evaluates the delinearization with a zero basis.
func.func @f(%x: index, %c: index, %out: memref<2xindex>) {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  scf.for %j = %c0 to %c step %c1 {
    %q:2 = affine.delinearize_index %x into (%c) : index, index
    memref.store %q#0, %out[%c0] : memref<2xindex>
    memref.store %q#1, %out[%c1] : memref<2xindex>
  }
  return
}

func.func @main() {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  %m1 = arith.constant -1 : index
  %out = memref.alloca() : memref<2xindex>
  memref.store %m1, %out[%c0] : memref<2xindex>
  memref.store %m1, %out[%c1] : memref<2xindex>
  %x = arith.constant 7 : index
  %c = arith.constant 0 : index
  call @f(%x, %c, %out) : (index, index, memref<2xindex>) -> ()
  %a = memref.load %out[%c0] : memref<2xindex>
  %b = memref.load %out[%c1] : memref<2xindex>
  vector.print %a : index
  vector.print %b : index
  return
}

apply.mlir:

// Variant: affine.apply with a semi-affine map (floordiv/mod by a symbol) is Pure.
// The loop runs zero times exactly when the divisor symbol %c is 0.
func.func @f(%x: index, %c: index, %out: memref<2xindex>) {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  scf.for %j = %c0 to %c step %c1 {
    %q0 = affine.apply affine_map<()[s0, s1] -> (s0 floordiv s1)>()[%x, %c]
    %q1 = affine.apply affine_map<()[s0, s1] -> (s0 mod s1)>()[%x, %c]
    memref.store %q0, %out[%c0] : memref<2xindex>
    memref.store %q1, %out[%c1] : memref<2xindex>
  }
  return
}

func.func @main() {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  %m1 = arith.constant -1 : index
  %out = memref.alloca() : memref<2xindex>
  memref.store %m1, %out[%c0] : memref<2xindex>
  memref.store %m1, %out[%c1] : memref<2xindex>
  %x = arith.constant 7 : index
  %c = arith.constant 0 : index
  call @f(%x, %c, %out) : (index, index, memref<2xindex>) -> ()
  %a = memref.load %out[%c0] : memref<2xindex>
  %b = memref.load %out[%c1] : memref<2xindex>
  vector.print %a : index
  vector.print %b : index
  return
}
$ LOWER_A="--affine-expand-index-ops --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
$ mlir-opt delinearize.mlir $LOWER_A | $RUN
18446744073709551615
18446744073709551615
$ mlir-opt delinearize.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
Floating point exception (core dumped)      # exit code 136
$ mlir-opt apply.mlir $LOWER_A | $RUN
18446744073709551615
18446744073709551615
$ mlir-opt apply.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
Floating point exception (core dumped)      # exit code 136

Suggested fixes:

Version: llvm-project main @ 4c407e8 (2026-10-20), Release + assertions, x86_64 Linux. Also reproduced on main @ a60677d (2026-11-12).

Disclosure: I wrote this report with the help of Claude Code. I reproduced and reviewed everything in this report myself.

Activity

  1. llvmorg-github-actions commented on Nov 12, 2026

    @llvmorg-github-actions

    @llvm/issue-subscribers-mlir

    Author: Jinbao Chen (wildoranges)

    Details This is a follow-up to #233213. That issue reported four groups of ops that are `Pure` (and therefore `AlwaysSpeculatable`) although they can trap, so `-loop-invariant-code-motion` hoists them out of loops that may run zero times. #235770 fixed `arith.floordivsi` and `llvm.urem`/`llvm.srem`, and the issue was closed. The other two groups are still `Pure`:
    1. tensor.pad. It is Pure no matter what its padding region computes. tensor.generate, which has the same kind of body, is only RecursiveMemoryEffects.
      def Tensor_PadOp : Tensor_Op<"pad", [
      DeclareOpInterfaceMethods<OpAsmOpInterface, ["getAsmResultNames"]>,
      DeclareOpInterfaceMethods<ReifyRankedShapedTypeOpInterface, [
      "reifyResultShapes"]>,
      AttrSizedOperandSegments,
      Pure,
    2. affine.delinearize_index. Its doc says "A dynamic basis element being 0 or negative causes undefined behavior". The same holds for affine.apply with a semi-affine map (floordiv/mod by a symbol). Both are Pure.
      def AffineDelinearizeIndexOp : Affine_Op<"delinearize_index",
      [Pure, Elementwise,

      Due to the constraints of affine maps, all the basis elements must
      be strictly positive. A dynamic basis element being 0 or negative causes
      undefined behavior.

    In every reproducer below the loop runs zero times exactly when the divisor is 0, so the original program is UB-free. Without LICM, each one prints its untouched initial value. That value is -1; the index versions print it as 18446744073709551615. With --loop-invariant-code-motion, each one dies with SIGFPE.

    ($LLVM_BUILD is the LLVM/MLIR build directory.)

    $ RUN="mlir-runner -e main -entry-point-result=void -shared-libs=$LLVM_BUILD/lib/libmlir_c_runner_utils.so,$LLVM_BUILD/lib/libmlir_runner_utils.so"

    1. tensor.pad whose padding region divides

    pad.mlir:

    // tensor.pad is declared Pure (AlwaysSpeculatable + NoMemoryEffect) regardless of
    // what its padding region computes (tensor.generate, with the same kind of body,
    // is only RecursiveMemoryEffects and not speculatable).  Here the padding value
    // is %x / %d, which is UB when %d == 0; the loop guarding the pad runs zero
    // times in exactly that case, so the original program never divides by zero.
    func.func @<!-- -->f(%t: tensor&lt;2xi32&gt;, %x: i32, %d: i32, %n: index, %out: memref&lt;1xi32&gt;) {
      %c0 = arith.constant 0 : index
      %c1 = arith.constant 1 : index
      scf.for %i = %c0 to %n step %c1 {
        %p = tensor.pad %t low[1] high[1] {
        ^bb0(%j: index):
          %v = arith.divsi %x, %d : i32
          tensor.yield %v : i32
        } : tensor&lt;2xi32&gt; to tensor&lt;4xi32&gt;
        %e = tensor.extract %p[%c0] : tensor&lt;4xi32&gt;
        memref.store %e, %out[%c0] : memref&lt;1xi32&gt;
      }
      return
    }
    
    func.func @<!-- -->main() {
      %c0 = arith.constant 0 : index
      %t = arith.constant dense&lt;[5, 6]&gt; : tensor&lt;2xi32&gt;
      %out = memref.alloca() : memref&lt;1xi32&gt;
      %m1 = arith.constant -1 : i32
      memref.store %m1, %out[%c0] : memref&lt;1xi32&gt;
      %x = arith.constant 7 : i32
      %d = arith.constant 0 : i32
      %n = arith.constant 0 : index
      call @<!-- -->f(%t, %x, %d, %n, %out) : (tensor&lt;2xi32&gt;, i32, i32, index, memref&lt;1xi32&gt;) -&gt; ()
      %v = memref.load %out[%c0] : memref&lt;1xi32&gt;
      vector.print %v : i32
      return
    }
    $ LOWER_T="--one-shot-bufferize=bufferize-function-boundaries --buffer-deallocation-pipeline --convert-linalg-to-loops --expand-strided-metadata --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
    $ mlir-opt pad.mlir $LOWER_T | $RUN
    -1
    $ mlir-opt pad.mlir --loop-invariant-code-motion | mlir-opt $LOWER_T | $RUN
    Floating point exception (core dumped)      # exit code 136

    2. affine.delinearize_index and semi-affine affine.apply

    delinearize.mlir:

    // affine.delinearize_index is Pure although its documentation says "A dynamic
    // basis element being 0 or negative causes undefined behavior".  The loop below
    // iterates %j over [0, %c): when %c == 0 it runs zero times, so the original
    // program never evaluates the delinearization with a zero basis.
    func.func @<!-- -->f(%x: index, %c: index, %out: memref&lt;2xindex&gt;) {
      %c0 = arith.constant 0 : index
      %c1 = arith.constant 1 : index
      scf.for %j = %c0 to %c step %c1 {
        %q:2 = affine.delinearize_index %x into (%c) : index, index
        memref.store %q#<!-- -->0, %out[%c0] : memref&lt;2xindex&gt;
        memref.store %q#<!-- -->1, %out[%c1] : memref&lt;2xindex&gt;
      }
      return
    }
    
    func.func @<!-- -->main() {
      %c0 = arith.constant 0 : index
      %c1 = arith.constant 1 : index
      %m1 = arith.constant -1 : index
      %out = memref.alloca() : memref&lt;2xindex&gt;
      memref.store %m1, %out[%c0] : memref&lt;2xindex&gt;
      memref.store %m1, %out[%c1] : memref&lt;2xindex&gt;
      %x = arith.constant 7 : index
      %c = arith.constant 0 : index
      call @<!-- -->f(%x, %c, %out) : (index, index, memref&lt;2xindex&gt;) -&gt; ()
      %a = memref.load %out[%c0] : memref&lt;2xindex&gt;
      %b = memref.load %out[%c1] : memref&lt;2xindex&gt;
      vector.print %a : index
      vector.print %b : index
      return
    }

    apply.mlir:

    // Variant: affine.apply with a semi-affine map (floordiv/mod by a symbol) is Pure.
    // The loop runs zero times exactly when the divisor symbol %c is 0.
    func.func @<!-- -->f(%x: index, %c: index, %out: memref&lt;2xindex&gt;) {
      %c0 = arith.constant 0 : index
      %c1 = arith.constant 1 : index
      scf.for %j = %c0 to %c step %c1 {
        %q0 = affine.apply affine_map&lt;()[s0, s1] -&gt; (s0 floordiv s1)&gt;()[%x, %c]
        %q1 = affine.apply affine_map&lt;()[s0, s1] -&gt; (s0 mod s1)&gt;()[%x, %c]
        memref.store %q0, %out[%c0] : memref&lt;2xindex&gt;
        memref.store %q1, %out[%c1] : memref&lt;2xindex&gt;
      }
      return
    }
    
    func.func @<!-- -->main() {
      %c0 = arith.constant 0 : index
      %c1 = arith.constant 1 : index
      %m1 = arith.constant -1 : index
      %out = memref.alloca() : memref&lt;2xindex&gt;
      memref.store %m1, %out[%c0] : memref&lt;2xindex&gt;
      memref.store %m1, %out[%c1] : memref&lt;2xindex&gt;
      %x = arith.constant 7 : index
      %c = arith.constant 0 : index
      call @<!-- -->f(%x, %c, %out) : (index, index, memref&lt;2xindex&gt;) -&gt; ()
      %a = memref.load %out[%c0] : memref&lt;2xindex&gt;
      %b = memref.load %out[%c1] : memref&lt;2xindex&gt;
      vector.print %a : index
      vector.print %b : index
      return
    }
    $ LOWER_A="--affine-expand-index-ops --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
    $ mlir-opt delinearize.mlir $LOWER_A | $RUN
    18446744073709551615
    18446744073709551615
    $ mlir-opt delinearize.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
    Floating point exception (core dumped)      # exit code 136
    $ mlir-opt apply.mlir $LOWER_A | $RUN
    18446744073709551615
    18446744073709551615
    $ mlir-opt apply.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
    Floating point exception (core dumped)      # exit code 136

    Suggested fixes:

    Version: llvm-project main @ 4c407e8 (2026-10-20), Release + assertions, x86_64 Linux. Also reproduced on main @ a60677d (2026-11-12).

    Disclosure: I wrote this report with the help of Claude Code. I reproduced and reviewed everything in this report myself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions