[Torch] Support explicit scale values from `tm_tensor.attention` (#24566)

With dynamically sized GQA, explicit scale settings show up in
ONNX/Torch models such as Qwen3. Consume scale values from TMTensor op
when provided.

Depends on llvm/torch-mlir#4593.

Signed-off-by: Artem Gindinson <gindinson@roofline.ai>
diff --git a/compiler/plugins/input/Torch/InputConversion/ConvertTMTensorToLinalgExt.cpp b/compiler/plugins/input/Torch/InputConversion/ConvertTMTensorToLinalgExt.cpp
index 85121b5..6dcffbc 100644
--- a/compiler/plugins/input/Torch/InputConversion/ConvertTMTensorToLinalgExt.cpp
+++ b/compiler/plugins/input/Torch/InputConversion/ConvertTMTensorToLinalgExt.cpp
@@ -4,6 +4,7 @@
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+#include <mlir/IR/BuiltinAttributes.h>
 #include <cstdint>
 #include <numeric>
 
@@ -162,15 +163,24 @@
     // have support for batch dims using more general indexing maps, we should
     // change this and rely on more general mechanisms.
 
-    // Compute scale = rsqrt(head_dim) in f32.
-    int64_t queryRank = op.getQueryType().getRank();
-    Value dimIdx =
-        rewriter.createOrFold<tensor::DimOp>(loc, query, queryRank - 1);
-    Value dimInt = rewriter.createOrFold<arith::IndexCastOp>(
-        loc, rewriter.getI64Type(), dimIdx);
-    Value dimFloat = rewriter.createOrFold<arith::SIToFPOp>(
-        loc, rewriter.getF32Type(), dimInt);
-    Value scale = rewriter.createOrFold<math::RsqrtOp>(loc, dimFloat);
+    Value scale;
+    auto scaleAttr = op.getScaleAttr();
+    if (scaleAttr) {
+      auto targetType = cast<FloatType>(op.getQueryType().getElementType());
+      scale = arith::ConstantOp::create(
+          rewriter, loc, targetType,
+          rewriter.getFloatAttr(targetType, scaleAttr.getValueAsDouble()));
+    } else {
+      // Compute scale = rsqrt(head_dim) in f32.
+      int64_t queryRank = op.getQueryType().getRank();
+      Value dimIdx =
+          rewriter.createOrFold<tensor::DimOp>(loc, query, queryRank - 1);
+      Value dimInt = rewriter.createOrFold<arith::IndexCastOp>(
+          loc, rewriter.getI64Type(), dimIdx);
+      Value dimFloat = rewriter.createOrFold<arith::SIToFPOp>(
+          loc, rewriter.getF32Type(), dimInt);
+      scale = rewriter.createOrFold<math::RsqrtOp>(loc, dimFloat);
+    }
 
     // Add batches to standard attention indexing maps.
     SmallVector<AffineMap> indexingMaps =
diff --git a/compiler/plugins/input/Torch/InputConversion/test/attention.mlir b/compiler/plugins/input/Torch/InputConversion/test/attention.mlir
index fd58364..fcb6f64 100644
--- a/compiler/plugins/input/Torch/InputConversion/test/attention.mlir
+++ b/compiler/plugins/input/Torch/InputConversion/test/attention.mlir
@@ -59,7 +59,7 @@
 
 // CHECK-LABEL:         func.func @attention(
 // CHECK-SAME:         %[[ARG0:.*]]: tensor<1x3x4xf32>, %[[ARG1:.*]]: tensor<1x3x4xf32>, %[[ARG2:.*]]: tensor<1x3x4xf32>,
-// CHECK:         %[[ARG3:.*]]: tensor<1x3x4xf32>) -> tensor<1x3x4xf32> {
+// CHECK-SAME:         %[[ARG3:.*]]: tensor<1x3x4xf32>) -> tensor<1x3x4xf32> {
 // CHECK:         %[[SCALE:.*]] = arith.constant 5.000000e-01 : f32
 // CHECK:         %[[EMPTY:.*]] = tensor.empty() : tensor<1x3x4xf32>
 // CHECK:         %[[ATTN:.*]] = iree_linalg_ext.attention {indexing_maps = [#[[$MAP_Q]], #[[$MAP_K]], #[[$MAP_V]], #[[$MAP_S]], #[[$MAP_O]]]} ins(%[[ARG0]], %[[ARG1]], %[[ARG2]], %[[SCALE]] : tensor<1x3x4xf32>, tensor<1x3x4xf32>, tensor<1x3x4xf32>, f32) outs(%[[EMPTY]] : tensor<1x3x4xf32>) {
@@ -69,6 +69,29 @@
 // CHECK:         return %[[ATTN]] : tensor<1x3x4xf32>
 
 // -----
+func.func @attention_scaled(%arg0: tensor<1x3x4xf32>, %arg1: tensor<1x3x4xf32>, %arg2: tensor<1x3x4xf32>, %arg3: tensor<1x3x4xf32>) -> (tensor<1x3x4xf32>) {
+  %0 = tm_tensor.attention {scale = 0.125 : f64} ins(%arg0, %arg1, %arg2 : tensor<1x3x4xf32>, tensor<1x3x4xf32>, tensor<1x3x4xf32>) outs(%arg3: tensor<1x3x4xf32>) -> tensor<1x3x4xf32>
+  return %0 : tensor<1x3x4xf32>
+}
+
+// CHECK-DAG: #[[$MAP_Q:.+]] = affine_map<(d0, d1, d2, d3, d4) -> (d0, d1, d3)>
+// CHECK-DAG: #[[$MAP_K:.+]] = affine_map<(d0, d1, d2, d3, d4) -> (d0, d4, d3)>
+// CHECK-DAG: #[[$MAP_V:.+]] = affine_map<(d0, d1, d2, d3, d4) -> (d0, d4, d2)>
+// CHECK-DAG: #[[$MAP_S:.+]] = affine_map<(d0, d1, d2, d3, d4) -> ()>
+// CHECK-DAG: #[[$MAP_O:.+]] = affine_map<(d0, d1, d2, d3, d4) -> (d0, d1, d2)>
+
+// CHECK-LABEL:         func.func @attention_scaled(
+// CHECK-SAME:         %[[ARG0:.*]]: tensor<1x3x4xf32>, %[[ARG1:.*]]: tensor<1x3x4xf32>, %[[ARG2:.*]]: tensor<1x3x4xf32>,
+// CHECK-SAME:         %[[ARG3:.*]]: tensor<1x3x4xf32>) -> tensor<1x3x4xf32> {
+// CHECK:         %[[SCALE:.*]] = arith.constant 1.250000e-01 : f32
+// CHECK:         %[[EMPTY:.*]] = tensor.empty() : tensor<1x3x4xf32>
+// CHECK:         %[[ATTN:.*]] = iree_linalg_ext.attention {indexing_maps = [#[[$MAP_Q]], #[[$MAP_K]], #[[$MAP_V]], #[[$MAP_S]], #[[$MAP_O]]]} ins(%[[ARG0]], %[[ARG1]], %[[ARG2]], %[[SCALE]] : tensor<1x3x4xf32>, tensor<1x3x4xf32>, tensor<1x3x4xf32>, f32) outs(%[[EMPTY]] : tensor<1x3x4xf32>) {
+// CHECK:    ^[[BLOCK:.+]](%[[SCORE:.+]]: f32):
+// CHECK:         linalg_ext.yield %[[SCORE]]
+// CHECK: } -> tensor<1x3x4xf32>
+// CHECK:         return %[[ATTN]] : tensor<1x3x4xf32>
+
+// -----
 func.func @attention_dyn(%arg0: tensor<?x?x4xf32>, %arg1: tensor<?x?x4xf32>, %arg2: tensor<?x?x4xf32>, %arg3: tensor<?x?x4xf32>) -> (tensor<?x?x4xf32>) {
   %0 = tm_tensor.attention ins(%arg0, %arg1, %arg2 : tensor<?x?x4xf32>, tensor<?x?x4xf32>, tensor<?x?x4xf32>) outs(%arg3: tensor<?x?x4xf32>) -> tensor<?x?x4xf32>
   return %0 : tensor<?x?x4xf32>