[Torch] handle duplicate symbolic shape bindings (#24875)

torch might emit multiple bindings for a single value, this breaks a
invariant the surrounding code expects which makes this pass error out.

This fixes this by only accepting one binding per value and discarding
the rest.

Assisted by Claude

Signed-off-by: default <ziereis@roofline.ai>
diff --git a/compiler/plugins/input/Torch/InputConversion/BindSymbolicShapes.cpp b/compiler/plugins/input/Torch/InputConversion/BindSymbolicShapes.cpp
index 2291372..692b60c 100644
--- a/compiler/plugins/input/Torch/InputConversion/BindSymbolicShapes.cpp
+++ b/compiler/plugins/input/Torch/InputConversion/BindSymbolicShapes.cpp
@@ -413,6 +413,7 @@
 
     llvm::SmallVector<Operation *> cleanupOpList;
     llvm::SmallVector<TensorBinding> bindings;
+    llvm::DenseMap<Value, Torch::BindSymbolicShapeOp> boundValues;
     // Mapping of SSA value for a torch.symbolic_int (or related op) to its
     // info.
     llvm::DenseMap<Value, SymbolInfo> symbolInfos;
@@ -428,6 +429,22 @@
         if (!isEligibleBinding(bindOp)) {
           return;
         }
+        auto [it, inserted] =
+            boundValues.try_emplace(bindOp.getOperand(), bindOp);
+        if (!inserted) {
+          auto previousBindOp = it->second;
+          if (bindOp.getShapeExpressions() !=
+                  previousBindOp.getShapeExpressions() ||
+              !llvm::equal(bindOp.getShapeSymbols(),
+                           previousBindOp.getShapeSymbols())) {
+            auto diagnostic = bindOp.emitError(
+                "conflicting symbolic shape bindings for the same value");
+            diagnostic.attachNote(previousBindOp.getLoc())
+                << "previous binding is here";
+            return signalPassFailure();
+          }
+          return;
+        }
         auto torchType =
             cast<Torch::ValueTensorType>(bindOp.getOperand().getType());
         auto builtinType = dyn_cast_if_present<RankedTensorType>(
diff --git a/compiler/plugins/input/Torch/InputConversion/test/bind_symbolic_shapes.mlir b/compiler/plugins/input/Torch/InputConversion/test/bind_symbolic_shapes.mlir
index 335ca30..998d34a 100644
--- a/compiler/plugins/input/Torch/InputConversion/test/bind_symbolic_shapes.mlir
+++ b/compiler/plugins/input/Torch/InputConversion/test/bind_symbolic_shapes.mlir
@@ -96,6 +96,36 @@
 }
 
 // -----
+// Verify duplicate bindings are only materialized once.
+// CHECK-LABEL: @duplicate_binding
+module @duplicate_binding {
+  func.func @main(%arg0: !torch.vtensor<[1,?],si64>) -> !torch.vtensor<[1,?],si64> attributes {torch.assume_strict_symbolic_shapes} {
+    // CHECK-COUNT-1: torch_c.to_builtin_tensor %arg0
+    // CHECK-COUNT-1: flow.tensor.tie_shape
+    // CHECK-COUNT-1: torch_c.from_builtin_tensor
+    // CHECK-NOT: torch.bind_symbolic_shape
+    %0 = torch.symbolic_int "s0" {min_val = 2, max_val = 65} : !torch.int
+    torch.bind_symbolic_shape %arg0, [%0], affine_map<()[s0] -> (1, s0 * 32)> : !torch.vtensor<[1,?],si64>
+    %int1 = torch.constant.int 1
+    %1 = torch.aten.add.Scalar %arg0, %int1, %int1 : !torch.vtensor<[1,?],si64>, !torch.int, !torch.int -> !torch.vtensor<[1,?],si64>
+    torch.bind_symbolic_shape %arg0, [%0], affine_map<()[s0] -> (1, s0 * 32)> : !torch.vtensor<[1,?],si64>
+    return %1 : !torch.vtensor<[1,?],si64>
+  }
+}
+
+// -----
+module @conflicting_duplicate_binding {
+  func.func @main(%arg0: !torch.vtensor<[1,?],si64>) {
+    %0 = torch.symbolic_int "s0" {min_val = 2, max_val = 65} : !torch.int
+    // expected-note@+1 {{previous binding is here}}
+    torch.bind_symbolic_shape %arg0, [%0], affine_map<()[s0] -> (1, s0 * 32)> : !torch.vtensor<[1,?],si64>
+    // expected-error@+1 {{conflicting symbolic shape bindings for the same value}}
+    torch.bind_symbolic_shape %arg0, [%0], affine_map<()[s0] -> (1, s0 * 16)> : !torch.vtensor<[1,?],si64>
+    return
+  }
+}
+
+// -----
 // CHECK-LABEL: @add_expr
 module @add_expr {
   func.func @main(%arg0: !torch.vtensor<[?,?],f32>, %arg1: !torch.vtensor<[?,?],f32>) {