This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 14dcefa2b4 [REFACTOR][TIRx] Store tile operands and configuration as 
expressions (#20576)
14dcefa2b4 is described below

commit 14dcefa2b446d1a9c6038846c0f379ca7c7a2477
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Oct 7 07:20:12 2026 -0400

    [REFACTOR][TIRx] Store tile operands and configuration as expressions 
(#20576)
---
 include/tvm/ir/base_expr.h                         |   1 +
 include/tvm/tirx/tile_primitive.h                  |  21 +-
 .../backend/cuda/tile_primitive/copy/vec_forced.py |   6 +-
 .../cuda/tile_primitive/copy_async/tcgen05_cp.py   |  11 +-
 .../backend/cuda/tile_primitive/copy_async/tma.py  |  18 +-
 .../cuda/tile_primitive/elementwise/ops/binary.py  |   2 +-
 .../cuda/tile_primitive/elementwise/ops/unary.py   |  30 +-
 .../cuda/tile_primitive/gemm_async/tcgen05.py      |  21 +-
 .../trn/tile_primitive/compose_op/binary_reduce.py |   4 +-
 .../trn/tile_primitive/compose_op/unary_reduce.py  |  28 +-
 .../trn/tile_primitive/instruction_generator.py    |   6 +-
 .../backend/trn/tile_primitive/private_alloc.py    |  25 +-
 .../backend/trn/tile_primitive/reduction/utils.py  |   2 +-
 .../trn/tile_primitive/unary/with_bias_scale.py    |  12 +-
 python/tvm/tirx/operator/tile_primitive/ops.py     |  83 +++--
 python/tvm/tirx/script/ir_builder/tirx.py          | 381 +++++++++++----------
 python/tvm/tirx/script/tile.py                     |  15 +-
 python/tvm/tirx/tile_primitive.py                  |  17 +-
 src/backend/trn/transform/lower_trainium_layout.cc |   3 +-
 src/ir/expr.cc                                     |  13 +
 src/relax/op/tensor/create.cc                      |   3 +-
 src/relax/op/tensor/create.h                       |   2 +-
 src/relax/op/tensor/manipulate.cc                  |   5 +-
 src/relax/op/tensor/manipulate.h                   |   2 +-
 src/tirx/ir/stmt_functor.cc                        |  85 +----
 src/tirx/ir/tir_visitor_with_path.cc               |  29 +-
 src/tirx/ir/tirx_stmt.cc                           |  28 +-
 src/tirx/op/tirx.cc                                |  60 +++-
 src/tirx/script/printer/stmt.cc                    |  44 ++-
 src/tirx/transform/lower_tirx_cleanup.cc           |   4 +-
 .../tile_primitive/cuda/elementwise/test_unary.py  |  56 +--
 .../tile_primitive/trn/test_compose_op_trn.py      |   4 +-
 .../tile_primitive/trn/test_private_alloc_trn.py   |  14 +-
 .../operator/tile_primitive/trn/test_unary_trn.py  |   6 +-
 tests/python/tirx/script/test_tirx_script_gpu.py   |   2 +-
 tests/python/tirx/test_hint.py                     |   2 +-
 36 files changed, 606 insertions(+), 439 deletions(-)

diff --git a/include/tvm/ir/base_expr.h b/include/tvm/ir/base_expr.h
index 74e9c7a65f..b8760f673b 100644
--- a/include/tvm/ir/base_expr.h
+++ b/include/tvm/ir/base_expr.h
@@ -621,6 +621,7 @@ template <>
 struct TypeTraits<Expr> : public ObjectRefWithFallbackTraitsBase<Expr, 
PrimExpr, ffi::String> {
   TVM_FFI_INLINE static Expr ConvertFallbackValue(PrimExpr value) { return 
value; }
   TVM_DLL static Expr ConvertFallbackValue(ffi::String value);
+  TVM_DLL static std::optional<Expr> TryCastFromAnyView(const TVMFFIAny* src);
 };
 }  // namespace ffi
 
diff --git a/include/tvm/tirx/tile_primitive.h 
b/include/tvm/tirx/tile_primitive.h
index 1716e29a03..7af00985c1 100644
--- a/include/tvm/tirx/tile_primitive.h
+++ b/include/tvm/tirx/tile_primitive.h
@@ -148,9 +148,9 @@ class TilePrimitiveCallNode : public StmtNode {
  public:
   explicit TilePrimitiveCallNode(ffi::UnsafeInit tag) : op(tag) {}
 
-  TilePrimitiveCallNode(tvm::Op op, ffi::Array<ffi::Any> args,
+  TilePrimitiveCallNode(tvm::Op op, ffi::Array<Expr> args,
                         ffi::Map<ffi::String, TensorVar> workspace,
-                        ffi::Map<ffi::String, ffi::Any> config, 
ffi::Optional<ffi::String> dispatch,
+                        ffi::Map<ffi::String, Expr> config, 
ffi::Optional<ffi::String> dispatch,
                         ExecScope scope)
       : op(std::move(op)),
         args(std::move(args)),
@@ -163,13 +163,13 @@ class TilePrimitiveCallNode : public StmtNode {
   tvm::Op op;
 
   // Arguments to the operator.
-  ffi::Array<ffi::Any> args;
+  ffi::Array<Expr> args;
 
   // Workspace (pre-allocated buffers) for the operator.
   ffi::Map<ffi::String, TensorVar> workspace;
 
   // Config for the operator/scheduler.
-  ffi::Map<ffi::String, ffi::Any> config;
+  ffi::Map<ffi::String, Expr> config;
 
   // Optional dispatch variant name registered via @register_dispatch.
   ffi::Optional<ffi::String> dispatch{std::nullopt};
@@ -197,14 +197,12 @@ class TilePrimitiveCallNode : public StmtNode {
  */
 class TilePrimitiveCall : public Stmt {
  public:
-  TVM_DLL TilePrimitiveCall(tvm::Op op, ffi::Array<ffi::Any> args,
+  TVM_DLL TilePrimitiveCall(tvm::Op op, ffi::Array<Expr> args,
                             ffi::Map<ffi::String, TensorVar> workspace = {},
-                            ffi::Map<ffi::String, ffi::Any> config = {},
+                            ffi::Map<ffi::String, Expr> config = {},
                             ffi::Optional<ffi::String> dispatch = std::nullopt,
                             ExecScope scope = ExecScope(ScopeKind::kThread));
 
-  static bool IsValidOpCallArgType(const ffi::Any& arg);
-
   explicit TilePrimitiveCall(ffi::ObjectPtr<TilePrimitiveCallNode> node) : 
Stmt(std::move(node)) {}
 
   TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(TilePrimitiveCall, Stmt, 
TilePrimitiveCallNode);
@@ -257,10 +255,16 @@ TVM_DLL const Op& gemm_async();
 TVM_DLL const Op& zero();
 
 TVM_DLL const Op& sqrt();
+TVM_DLL const Op& sqrt_with_scale_bias();
 
 TVM_DLL const Op& exp();
+TVM_DLL const Op& exp_with_scale_bias();
 
 TVM_DLL const Op& exp2();
+TVM_DLL const Op& exp2_with_scale_bias();
+
+TVM_DLL const Op& log2();
+TVM_DLL const Op& log2_with_scale_bias();
 
 TVM_DLL const Op& add();
 
@@ -289,6 +293,7 @@ TVM_DLL const Op& reduce_negate();
 TVM_DLL const Op& binary_reduce();
 
 TVM_DLL const Op& unary_reduce();
+TVM_DLL const Op& unary_reduce_with_scale_bias();
 
 TVM_DLL const Op& binary_chain();
 
diff --git a/python/tvm/backend/cuda/tile_primitive/copy/vec_forced.py 
b/python/tvm/backend/cuda/tile_primitive/copy/vec_forced.py
index b9e392bae5..a5cc788b91 100644
--- a/python/tvm/backend/cuda/tile_primitive/copy/vec_forced.py
+++ b/python/tvm/backend/cuda/tile_primitive/copy/vec_forced.py
@@ -80,12 +80,12 @@ def _ld_cache_config(op_call: TilePrimitiveCall) -> 
tuple[str | None, dict[str,
     """
     cache = op_call.config.get("cache", None)
     if cache is not None:
-        cache = str(cache)
+        cache = cache.value
     hints: dict[str, str] = {}
     for key in _LD_CACHE_HINT_KEYS:
         value = op_call.config.get(key, None)
-        if value is not None and str(value):
-            hints[key] = str(value)
+        if value is not None and value.value:
+            hints[key] = value.value
     return cache, hints
 
 
diff --git a/python/tvm/backend/cuda/tile_primitive/copy_async/tcgen05_cp.py 
b/python/tvm/backend/cuda/tile_primitive/copy_async/tcgen05_cp.py
index 0a62e645ea..aed613780e 100644
--- a/python/tvm/backend/cuda/tile_primitive/copy_async/tcgen05_cp.py
+++ b/python/tvm/backend/cuda/tile_primitive/copy_async/tcgen05_cp.py
@@ -196,8 +196,10 @@ def _cp_lane_replica_pattern(shape: str, multicast: str):
 
 def _resolve_cp_shape(op_call: TilePrimitiveCall):
     """Resolve (shape, multicast) from an explicit ``shape=`` config."""
-    shape = str(op_call.config["shape"])
+    shape = op_call.config["shape"].value
     multicast = op_call.config.get("multicast")
+    if isinstance(multicast, tvm.ir.StringImm):
+        multicast = multicast.value
     allowed = _CP_SHAPE_MULTICASTS.get(shape)
     if allowed is None:
         raise ValueError(
@@ -334,6 +336,8 @@ def _build_plan(op_call: TilePrimitiveCall):
         return _plan_for_shape(op_call, shape, multicast)
     # No shape config: infer from the buffer layouts.
     multicast_cfg = op_call.config.get("multicast")
+    if isinstance(multicast_cfg, tvm.ir.StringImm):
+        multicast_cfg = multicast_cfg.value
     errors = []
     for shape, multicast in _CP_SHAPE_CANDIDATES:
         if multicast_cfg is not None and str(multicast_cfg) != multicast:
@@ -740,7 +744,10 @@ def _validate_smem_tmem_copy(op_call: TilePrimitiveCall, 
sctx: DispatchContext):
 # need synchronization.
 # -----------------------------------------------------------------------------
 def copy_smem_tmem_impl(op_call: TilePrimitiveCall, sctx: DispatchContext) -> 
PrimFunc | None:
-    if op_call.config.get("decompress"):
+    decompress = op_call.config.get("decompress")
+    if isinstance(decompress, StringImm):
+        decompress = decompress.value
+    if decompress:
         # fp4/fp6->fp8 in-flight decompression needs dtype-pair plan derivation
         # the planner can't lower; reject loudly rather than copy 
undecompressed.
         raise ValueError("tcgen05.cp planner does not support decompress")
diff --git a/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py 
b/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
index 1913a423b7..dab89ef5d7 100644
--- a/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
+++ b/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
@@ -339,6 +339,8 @@ def _target_sm(arch: str) -> int:
 
 
 def _normalize_l2_promotion(value) -> int:
+    if isinstance(value, tvm.ir.StringImm):
+        value = value.value
     if value is None:
         return 2
     if isinstance(value, IntImm):
@@ -360,6 +362,8 @@ def _normalize_l2_promotion(value) -> int:
 
 
 def _normalize_oob(value) -> int:
+    if isinstance(value, tvm.ir.StringImm):
+        value = value.value
     if value is None or value == "zero":
         return 0
     if value == "nan":
@@ -368,6 +372,8 @@ def _normalize_oob(value) -> int:
 
 
 def _normalize_cache_hint(cache_hint):
+    if isinstance(cache_hint, tvm.ir.StringImm):
+        cache_hint = cache_hint.value
     if cache_hint is None:
         return "", None
     if isinstance(cache_hint, str):
@@ -1585,6 +1591,8 @@ def _runtime_config(op_call, sctx, direction: str, *, 
explicit: bool):
             fail("cta_mask is only valid for global-to-shared TMA")
 
     use_tma_reduce = op_call.config.get("use_tma_reduce")
+    if isinstance(use_tma_reduce, tvm.ir.StringImm):
+        use_tma_reduce = use_tma_reduce.value
     if use_tma_reduce is not None:
         if direction != "s2g":
             fail("use_tma_reduce is only valid for shared-to-global TMA")
@@ -1612,7 +1620,7 @@ def _runtime_config(op_call, sctx, direction: str, *, 
explicit: bool):
         "l2_promotion": 
_normalize_l2_promotion(op_call.config.get("tensormap_l2_promotion")),
         "oob_fill": _normalize_oob(op_call.config.get("oob")),
         "prefetch": bool(op_call.config.get("prefetch_tensormap", False)),
-        "tma_dtype": op_call.config.get("tma_dtype"),
+        "tma_dtype": (op_call.config["tma_dtype"].value if "tma_dtype" in 
op_call.config else None),
         "target_arch": sctx.target.arch,
     }
 
@@ -1860,7 +1868,7 @@ def _explicit_spec_for_gmem(
 def _normalize_gather4(value):
     if value is None:
         return ()
-    if not isinstance(value, list | tuple | tvm.ir.Array) or len(value) != 4:
+    if not isinstance(value, list | tuple | tvm.ir.Array | tvm.ir.Tuple) or 
len(value) != 4:
         fail("tma_explicit gather4 must contain exactly four row coordinates")
     return tuple(value)
 
@@ -1899,11 +1907,11 @@ def _validate_gather4_dst(s_buf: Var, s_starts, 
s_extents, spec: TensorMapSpec)
 def _normalize_src_selector(value):
     if value is None:
         return ()
-    if not isinstance(value, list | tuple | tvm.ir.Array):
-        fail("tma_explicit src_selector must be a list of (condition, global 
Var/view)")
+    if not isinstance(value, list | tuple | tvm.ir.Array | tvm.ir.Tuple) or 
len(value) == 0:
+        fail("tma_explicit src_selector must be a nonempty list of (condition, 
global Var/view)")
     result = []
     for idx, item in enumerate(value):
-        if not isinstance(item, list | tuple | tvm.ir.Array) or len(item) != 2:
+        if not isinstance(item, list | tuple | tvm.ir.Array | tvm.ir.Tuple) or 
len(item) != 2:
             fail(f"tma_explicit src_selector[{idx}] must be a (condition, 
Var/view) pair")
         condition, buffer = item
         if not isinstance(condition, tvm.tirx.Expr):
diff --git a/python/tvm/backend/cuda/tile_primitive/elementwise/ops/binary.py 
b/python/tvm/backend/cuda/tile_primitive/elementwise/ops/binary.py
index 3b943440c5..1d3a214842 100644
--- a/python/tvm/backend/cuda/tile_primitive/elementwise/ops/binary.py
+++ b/python/tvm/backend/cuda/tile_primitive/elementwise/ops/binary.py
@@ -82,7 +82,7 @@ def _parse_binary_for(op_name: str):
         extras: dict[str, Any] = {}
         rm = op.config.get("rounding_mode", None)
         if rm is not None:
-            extras["rounding_mode"] = rm
+            extras["rounding_mode"] = rm.value
         return Plan(dst=_dst, srcs=srcs, extras=extras), None
 
     return parse
diff --git a/python/tvm/backend/cuda/tile_primitive/elementwise/ops/unary.py 
b/python/tvm/backend/cuda/tile_primitive/elementwise/ops/unary.py
index ed6d921414..ee1458b5e8 100644
--- a/python/tvm/backend/cuda/tile_primitive/elementwise/ops/unary.py
+++ b/python/tvm/backend/cuda/tile_primitive/elementwise/ops/unary.py
@@ -17,8 +17,8 @@
 
 """Unary elementwise ops: zero / fill / reciprocal / sqrt / exp / exp2 / log2 
/ silu.
 
-All carry the same ``T.<unary>(dst, src[, bias, scale])`` shape (bias / scale
-optional; ``silu`` ignores bias/scale to preserve legacy behavior).
+Plain unary operations take dst and src. Explicit scale/bias variants 
additionally
+take scale and bias, applying both before the unary operation.
 """
 
 from __future__ import annotations
@@ -28,18 +28,22 @@ from typing import Any
 from tvm.ir import TensorRegion, is_prim_expr
 from tvm.script import tirx as T
 from tvm.tirx import TilePrimitiveCall
-from tvm.tirx.expr import FloatImm
 
 from .._common import scalar_dtype
 from . import OpSpec, Plan, SrcSpec
 
 
 def _parse_unary(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
-    """T.<unary>(dst, src[, bias, scale]) → Plan."""
+    """T.<unary>(dst, src[, scale, bias]) → Plan."""
     _dst: TensorRegion = op.args[0]
     _src = op.args[1]
-    _bias = op.args[2] if len(op.args) > 2 else None
-    _scale = op.args[3] if len(op.args) > 2 else None
+    _scale = op.args[2] if len(op.args) == 4 else None
+    _bias = op.args[3] if len(op.args) == 4 else None
+
+    if _bias is not None and not isinstance(_bias, TensorRegion) and not 
is_prim_expr(_bias):
+        return None, f"unsupported bias type {type(_bias).__name__}"
+    if _scale is not None and not is_prim_expr(_scale):
+        return None, f"unsupported scale type {type(_scale).__name__}"
 
     srcs: list[SrcSpec] = []
     if isinstance(_src, TensorRegion):
@@ -51,7 +55,7 @@ def _parse_unary(op: TilePrimitiveCall) -> tuple[Plan | None, 
str | None]:
 
     extras: dict[str, Any] = {
         "scale": _scale,
-        "bias_const": _bias if isinstance(_bias, FloatImm) else None,
+        "bias_const": _bias if is_prim_expr(_bias) else None,
     }
     if isinstance(_bias, TensorRegion):
         srcs.append(SrcSpec(buf_region=_bias))
@@ -115,8 +119,20 @@ UNARY_OPS: dict[str, OpSpec] = {
     "fill": OpSpec("fill", _parse_unary, _compute_fill, _check_unary_extras),
     "reciprocal": OpSpec("reciprocal", _parse_unary, _compute_reciprocal, 
_check_unary_extras),
     "sqrt": OpSpec("sqrt", _parse_unary, _with_bias_scale(T.sqrt), 
_check_unary_extras),
+    "sqrt_with_scale_bias": OpSpec(
+        "sqrt_with_scale_bias", _parse_unary, _with_bias_scale(T.sqrt), 
_check_unary_extras
+    ),
     "exp": OpSpec("exp", _parse_unary, _with_bias_scale(T.exp), 
_check_unary_extras),
+    "exp_with_scale_bias": OpSpec(
+        "exp_with_scale_bias", _parse_unary, _with_bias_scale(T.exp), 
_check_unary_extras
+    ),
     "exp2": OpSpec("exp2", _parse_unary, _with_bias_scale(T.exp2), 
_check_unary_extras),
+    "exp2_with_scale_bias": OpSpec(
+        "exp2_with_scale_bias", _parse_unary, _with_bias_scale(T.exp2), 
_check_unary_extras
+    ),
     "log2": OpSpec("log2", _parse_unary, _with_bias_scale(T.log2), 
_check_unary_extras),
+    "log2_with_scale_bias": OpSpec(
+        "log2_with_scale_bias", _parse_unary, _with_bias_scale(T.log2), 
_check_unary_extras
+    ),
     "silu": OpSpec("silu", _parse_unary, _compute_silu, _check_unary_extras),
 }
diff --git a/python/tvm/backend/cuda/tile_primitive/gemm_async/tcgen05.py 
b/python/tvm/backend/cuda/tile_primitive/gemm_async/tcgen05.py
index 0c9822f94a..0e7e07669f 100644
--- a/python/tvm/backend/cuda/tile_primitive/gemm_async/tcgen05.py
+++ b/python/tvm/backend/cuda/tile_primitive/gemm_async/tcgen05.py
@@ -30,7 +30,7 @@ from tvm.ir import Call, DataTypeImm, DictAttrs, StringImm, 
TensorRegion, Tuple
 from tvm.runtime import DataType
 from tvm.script import tirx as T
 from tvm.sym.analyzer import Analyzer
-from tvm.tirx import PrimFunc
+from tvm.tirx import IntImm, PrimFunc
 from tvm.tirx import op as tirx_op
 from tvm.tirx.layout import (
     ComposeLayout,
@@ -279,12 +279,12 @@ def _get_explicit_mma_tile(config):
     values = []
     for name in ("mma_m", "mma_n"):
         value = config[name]
-        if isinstance(value, bool):
+        if isinstance(value, bool) or (isinstance(value, IntImm) and 
str(value.ty.dtype) == "bool"):
             raise ValueError(
                 f"gemm_async[tcgen05]: {name} must be a positive integer, got 
{value!r}"
             )
         try:
-            value = operator.index(value)
+            value = int(value) if isinstance(value, IntImm) else 
operator.index(value)
         except TypeError as err:
             raise ValueError(
                 f"gemm_async[tcgen05]: {name} must be a positive integer, got 
{value!r}"
@@ -466,6 +466,8 @@ def gemm_async_tcgen05_impl(op_call: TilePrimitiveCall, 
sctx: DispatchContext) -
     if is_block_scaled:
         SFA_buffer_region, SFB_buffer_region = op_call.sfa, op_call.sfb
         transA, transB, accum = op_call.transA, op_call.transB, op_call.accum
+        transA = bool(transA) if isinstance(transA, IntImm) else transA
+        transB = bool(transB) if isinstance(transB, IntImm) else transB
         SFA_buffer: tvm.ir.Var = SFA_buffer_region.source
         SFB_buffer: tvm.ir.Var = SFB_buffer_region.source
         SFA_scope, SFB_scope = SFA_buffer.scope(), SFB_buffer.scope()
@@ -499,8 +501,12 @@ def gemm_async_tcgen05_impl(op_call: TilePrimitiveCall, 
sctx: DispatchContext) -
         _validate_sf_tmem_layout(SFB_slice_layout, SFB_rows, SFB_K_total, 
sfb_sf_mma_k, "SFB")
     else:
         transA, transB, accum = op_call.transA, op_call.transB, op_call.accum
+        transA = bool(transA) if isinstance(transA, IntImm) else transA
+        transB = bool(transB) if isinstance(transB, IntImm) else transB
 
     cta_group = op_call.config.get("cta_group", 1)
+    if isinstance(cta_group, IntImm):
+        cta_group = cta_group.value
     assert cta_group in [1, 2], f"tcgen05 schedule expected cta_group=1 or 2, 
got {cta_group}"
     # descI (pre-encoded uint32 instruction descriptor): rejected on the dense
     # path (dispatcher encodes it); block-scaled callers may still pass it in.
@@ -950,7 +956,12 @@ def gemm_async_tcgen05_impl(op_call: TilePrimitiveCall, 
sctx: DispatchContext) -
     # Packed Layout-E C (M, 2, N//2) is uniquely the cta_group::1 M=64 .ws 
datapath
     # (PTX §9.7.16.10.5), so .ws is inferred; weight_stationary=False on it is 
rejected.
     if packed_n2 and not is_2x2:
-        if op_call.config.get("weight_stationary") is False:
+        explicit_ws = op_call.config.get("weight_stationary")
+        if (
+            isinstance(explicit_ws, IntImm)
+            and str(explicit_ws.ty.dtype) == "bool"
+            and not int(explicit_ws)
+        ):
             raise ValueError(
                 "gemm_async[tcgen05]: C uses the packed (M, 2, N//2):(1@TLane, 
"
                 "64@TLane, 1@TCol) Layout-E TMEM layout, which is the M=64 "
@@ -1208,6 +1219,8 @@ def gemm_async_tcgen05_impl(op_call: TilePrimitiveCall, 
sctx: DispatchContext) -
     # smem_desc modes: "hoist" (default, encode once after alloc), 
"local_hoist"
     # (encode at call site, reuse via add_16B_offset), "encode"/"recompute" 
(per MMA).
     smem_desc_mode = op_call.config.get("smem_desc", "hoist")
+    if isinstance(smem_desc_mode, tvm.ir.StringImm):
+        smem_desc_mode = smem_desc_mode.value
     local_hoist = smem_desc_mode == "local_hoist"
     encode_per_mma = smem_desc_mode == "encode"
     use_add = smem_desc_mode not in ("recompute", "encode")
diff --git a/python/tvm/backend/trn/tile_primitive/compose_op/binary_reduce.py 
b/python/tvm/backend/trn/tile_primitive/compose_op/binary_reduce.py
index 39fb66e417..c89ce1e626 100644
--- a/python/tvm/backend/trn/tile_primitive/compose_op/binary_reduce.py
+++ b/python/tvm/backend/trn/tile_primitive/compose_op/binary_reduce.py
@@ -43,7 +43,9 @@ def binary_reduce_trn(op: TilePrimitiveCall, sctx: 
DispatchContext) -> PrimFunc
     analyzer = init_analyzer(sctx)
 
     # Normalize negative axes
-    reduce_axes = [i if i >= 0 else len(binary_output.source.ty.shape) + i for 
i in reduce_axes]
+    reduce_axes = [
+        int(i) if int(i) >= 0 else len(binary_output.source.ty.shape) + int(i) 
for i in reduce_axes
+    ]
 
     # Find instruction patterns
     inst_gen = InstructionGenerator(
diff --git a/python/tvm/backend/trn/tile_primitive/compose_op/unary_reduce.py 
b/python/tvm/backend/trn/tile_primitive/compose_op/unary_reduce.py
index ae8c720a3d..98a31442cc 100644
--- a/python/tvm/backend/trn/tile_primitive/compose_op/unary_reduce.py
+++ b/python/tvm/backend/trn/tile_primitive/compose_op/unary_reduce.py
@@ -39,13 +39,18 @@ def unary_reduce_trn(op: TilePrimitiveCall, sctx: 
DispatchContext) -> PrimFunc |
 
     # Extract operation components
     unary_output, reduce_output = op.dsts
-    unary_input, bias, scale = op.srcs
+    unary_input = op.unary_input
+    if op.op.name == "tirx.tile.unary_reduce_with_scale_bias":
+        scale, bias = op.scale, op.bias
+    else:
+        scale, bias = 1.0, 0.0
     analyzer = init_analyzer(sctx)
 
     # Normalize axes and default values
-    reduce_axes = [i if i >= 0 else len(unary_output.source.ty.shape) + i for 
i in op.reduce_axes]
-    scale = 1.0 if scale is None else scale
-    bias = 0.0 if bias is None else bias
+    reduce_axes = [
+        int(i) if int(i) >= 0 else len(unary_output.source.ty.shape) + int(i)
+        for i in op.reduce_axes
+    ]
 
     inst_gen = InstructionGenerator([unary_output, unary_input, bias, 
reduce_output], analyzer)
     reduce_dim_map = get_reduction_dim_map(unary_output, reduce_output, 
reduce_axes, analyzer)
@@ -156,6 +161,21 @@ def unary_reduce_trn(op: TilePrimitiveCall, sctx: 
DispatchContext) -> PrimFunc |
         return impl
 
 
+@register_dispatch(
+    "unary_reduce_with_scale_bias",
+    "trn",
+    variant="default",
+    priority=10,
+    when=[
+        predicate(
+            "exec_scope",
+            lambda op, sctx: (
+                sctx.scope_kind == "thread",
+                f"unsupported exec_scope {sctx.scope_kind}",
+            ),
+        )
+    ],
+)
 @register_dispatch(
     "unary_reduce",
     "trn",
diff --git a/python/tvm/backend/trn/tile_primitive/instruction_generator.py 
b/python/tvm/backend/trn/tile_primitive/instruction_generator.py
index 6693596dae..d7a079c097 100644
--- a/python/tvm/backend/trn/tile_primitive/instruction_generator.py
+++ b/python/tvm/backend/trn/tile_primitive/instruction_generator.py
@@ -30,7 +30,7 @@ from tvm.backend.trn.layout import is_trainium_layout
 from tvm.ir import Range, TensorRegion
 from tvm.script import tirx as T
 from tvm.sym.analyzer import Analyzer
-from tvm.tirx import BufferRegion, Expr, Var, is_tensor_var
+from tvm.tirx import BufferRegion, Expr, IntImm, Var, is_tensor_var
 from tvm.tirx.layout import Iter
 
 from .dim_utils import DimensionMapper, RangeInfo, normalize_and_group
@@ -82,7 +82,9 @@ class InstructionRepr:
         self.selected_data_iter_ids = selected_data_iter_ids
 
     def bound_inst_size(self, max_inst_size: int | None, analyzer: Analyzer):
-        if max_inst_size is None:
+        if max_inst_size is None or (
+            isinstance(max_inst_size, int | IntImm) and int(max_inst_size) == 
-1
+        ):
             return
         if analyzer.can_prove(self.size <= max_inst_size):
             return
diff --git a/python/tvm/backend/trn/tile_primitive/private_alloc.py 
b/python/tvm/backend/trn/tile_primitive/private_alloc.py
index ceafb52120..9a7bbd260d 100644
--- a/python/tvm/backend/trn/tile_primitive/private_alloc.py
+++ b/python/tvm/backend/trn/tile_primitive/private_alloc.py
@@ -21,13 +21,15 @@ from tvm.backend.trn.tile_primitive.common import 
init_analyzer, nki_dim
 from tvm.backend.trn.tile_primitive.dim_utils import get_ewise_dim_map
 from tvm.backend.trn.tile_primitive.instruction_generator import 
InstructionGenerator
 from tvm.script import tirx as T
-from tvm.tirx import FloatImm, Stmt, Var
+from tvm.tirx import FloatImm, IntImm, Stmt, Var
 from tvm.tirx.operator.tile_primitive.ops import (
     BinaryReduce,
     Copy,
+    Exp,
     Gemm,
     ReduceOp,
-    UnaryOpWithBiasScale,
+    Sqrt,
+    UnaryOpWithScaleBias,
     UnaryReduce,
 )
 from tvm.tirx.operator.tile_primitive.registry import f_op_dispatcher
@@ -48,19 +50,22 @@ def _scalar_dtype(scalar) -> str:
 def alloc_const_bias_trn(
     op: TilePrimitiveCall, buffer_dict: dict[Any, tuple[Var, Stmt | None]], 
sctx: DispatchContext
 ) -> dict[str, Any]:
-    bias = op.bias if op.bias is not None else 
FloatImm(op.dsts[0].source.ty.dtype, 0.0)
+    bias = getattr(op, "bias", FloatImm(op.dsts[0].source.ty.dtype, 0.0))
     if "const_bias" in op.workspace:
         return {}
     if not isinstance(bias, (FloatImm)):
         return {}
+    bias_key = ("const_bias", _scalar_dtype(bias), float(bias.value).hex())
     par_size = op.dsts[0].source.ty.layout.size("P")
     max_inst_size = op.config.get("max_inst_size", 512)
-    if ("const_bias", bias.value) in buffer_dict:
-        bias_buffer, bias_init_stmt = buffer_dict[("const_bias", bias.value)]
+    if isinstance(max_inst_size, int | IntImm) and int(max_inst_size) == -1:
+        raise ValueError("Constant bias workspace allocation requires a finite 
max_inst_size")
+    if bias_key in buffer_dict:
+        bias_buffer, bias_init_stmt = buffer_dict[bias_key]
         old_shape = bias_buffer.ty.shape
         new_shape = [max(par_size, old_shape[0]), max(max_inst_size, 
old_shape[1])]
         if new_shape[0] == old_shape[0] and new_shape[1] == old_shape[1]:
-            return {"const_bias": ("const_bias", bias.value)}
+            return {"const_bias": bias_key}
     else:
         new_shape = (par_size, max_inst_size)
     new_buffer = T.Var(
@@ -76,8 +81,8 @@ def alloc_const_bias_trn(
                     T.evaluate(T.nki.memset(new_buffer[p_loop, f_loop], bias))
         T.tvm_kernel_replace_point()
 
-    buffer_dict[("const_bias", bias.value)] = (new_buffer, 
const_bias_init.body)
-    return {"const_bias": ("const_bias", bias.value)}
+    buffer_dict[bias_key] = (new_buffer, const_bias_init.body)
+    return {"const_bias": bias_key}
 
 
 def alloc_partial_reduce_trn(
@@ -197,7 +202,9 @@ def alloc_unary_reduce_trn(
         return workspace_dict
 
 
-UnaryOpWithBiasScale.get_private_buffers_trn = alloc_const_bias_trn
+UnaryOpWithScaleBias.get_private_buffers_trn = alloc_const_bias_trn
+Sqrt.get_private_buffers_trn = alloc_const_bias_trn
+Exp.get_private_buffers_trn = alloc_const_bias_trn
 ReduceOp.get_private_buffers_trn = alloc_partial_reduce_trn
 Copy.get_private_buffers_trn = alloc_copy_trn
 Gemm.get_private_buffers_trn = alloc_acc_psum_trn
diff --git a/python/tvm/backend/trn/tile_primitive/reduction/utils.py 
b/python/tvm/backend/trn/tile_primitive/reduction/utils.py
index 3ca7277238..860d1b8124 100644
--- a/python/tvm/backend/trn/tile_primitive/reduction/utils.py
+++ b/python/tvm/backend/trn/tile_primitive/reduction/utils.py
@@ -83,7 +83,7 @@ def reduction_trn(
     # Extract buffers
     dst = dst_buffer_region.source
     src = src_buffer_region.source
-    axes = [i if i >= 0 else len(src.ty.shape) + i for i in axes]
+    axes = [int(i) if int(i) >= 0 else len(src.ty.shape) + int(i) for i in 
axes]
     dim_map = get_reduction_dim_map(src_buffer_region, dst_buffer_region, 
axes, analyzer)
 
     # Layout validation
diff --git a/python/tvm/backend/trn/tile_primitive/unary/with_bias_scale.py 
b/python/tvm/backend/trn/tile_primitive/unary/with_bias_scale.py
index 6c728c35ad..d65b794bcd 100644
--- a/python/tvm/backend/trn/tile_primitive/unary/with_bias_scale.py
+++ b/python/tvm/backend/trn/tile_primitive/unary/with_bias_scale.py
@@ -37,10 +37,11 @@ def unary_with_bias_scale_trn(
     if not (sctx.is_target("trn") and sctx.scope_kind == "thread"):
         fail("requires Trainium target and thread exec_scope")
 
-    # Extract operation arguments with defaults
-    dst_buffer_region, src_buffer_region, _bias, scale = op.args
-    scale = 1.0 if scale is None else scale
-    _bias = 0.0 if _bias is None else _bias
+    if len(op.args) == 2:
+        dst_buffer_region, src_buffer_region = op.args
+        scale, _bias = 1.0, 0.0
+    else:
+        dst_buffer_region, src_buffer_region, scale, _bias = op.args
 
     # Initialize analyzer and validate operation type
     analyzer = init_analyzer(sctx)
@@ -84,5 +85,8 @@ from tvm.tirx.operator.tile_primitive import 
register_dispatch  # noqa: E402
 for _op_name, _op_type in {"sqrt": MapOpType.SQRT, "exp": 
MapOpType.EXP}.items():
 
     @register_dispatch(_op_name, "trn", variant="unary_with_bias_scale", 
priority=0)
+    @register_dispatch(
+        _op_name + "_with_scale_bias", "trn", variant="unary_with_bias_scale", 
priority=0
+    )
     def _unary_bs_dispatch(op, sctx, _ty=_op_type):
         return unary_with_bias_scale_trn(op, _ty, sctx)
diff --git a/python/tvm/tirx/operator/tile_primitive/ops.py 
b/python/tvm/tirx/operator/tile_primitive/ops.py
index 9b10f03374..69d0dc2760 100644
--- a/python/tvm/tirx/operator/tile_primitive/ops.py
+++ b/python/tvm/tirx/operator/tile_primitive/ops.py
@@ -58,20 +58,20 @@ class UnaryOp(TilePrimitiveCall):
         return [self.output]
 
 
-class UnaryOpWithBiasScale(UnaryOp):
-    """Extended unary operator with bias and scale parameters: 
unary_with_bias_scale(output, input, bias, scale).
+class UnaryOpWithScaleBias(UnaryOp):
+    """Extended unary operator with bias and scale parameters: 
unary_with_scale_bias(output, input, scale, bias).
 
-    These operators support additional bias and scale parameters for more 
complex operations (only on trn).
+    Both scale and bias are required operands, applied before the unary 
operation.
     output = unary(input * scale + bias)
     """  # noqa: E501
 
-    bias = ArgProperty(2)
-    scale = ArgProperty(3)
+    scale = ArgProperty(2)
+    bias = ArgProperty(3)
 
     @property
     def srcs(self) -> list[Expr]:
         """Get the source expressions (inputs) of the operator."""
-        return [self.input, self.bias, self.scale]
+        return [self.input, self.scale, self.bias]
 
 
 class BinaryOp(TilePrimitiveCall):
@@ -124,15 +124,18 @@ class Zero(UnaryOp):
     op = get_tirx_op("zero")
 
 
-class Sqrt(UnaryOpWithBiasScale):
-    """Compute square root of all elements in src and store to dst.
-
-    If bias and scale are provided: dst = sqrt(src * scale + bias)
-    """
+class Sqrt(UnaryOp):
+    """Compute sqrt(src) and store to dst."""
 
     op = get_tirx_op("sqrt")
 
 
+class SqrtWithScaleBias(UnaryOpWithScaleBias):
+    """Compute sqrt(src * scale + bias) and store to dst."""
+
+    op = get_tirx_op("sqrt_with_scale_bias")
+
+
 class Fill(UnaryOp):
     """Fill dst with a scalar value."""
 
@@ -382,33 +385,42 @@ class Minimum(BinaryOp):
     op = get_tirx_op("minimum")
 
 
-class Exp(UnaryOpWithBiasScale):
-    """Compute exponential (e^x) of all elements in src and store to dst.
-
-    If bias and scale are provided: dst = exp(src * scale + bias)
-    """
+class Exp(UnaryOp):
+    """Compute exp(src) and store to dst."""
 
     op = get_tirx_op("exp")
 
 
-class Exp2(UnaryOpWithBiasScale):
-    """Compute base-2 exponential (2^x) of all elements in src and store to 
dst.
+class ExpWithScaleBias(UnaryOpWithScaleBias):
+    """Compute exp(src * scale + bias) and store to dst."""
+
+    op = get_tirx_op("exp_with_scale_bias")
 
-    If bias and scale are provided: dst = exp2(src * scale + bias)
-    """
+
+class Exp2(UnaryOp):
+    """Compute exp2(src) and store to dst."""
 
     op = get_tirx_op("exp2")
 
 
-class Log2(UnaryOpWithBiasScale):
-    """Compute base-2 logarithm of all elements in src and store to dst.
+class Exp2WithScaleBias(UnaryOpWithScaleBias):
+    """Compute exp2(src * scale + bias) and store to dst."""
+
+    op = get_tirx_op("exp2_with_scale_bias")
 
-    If bias and scale are provided: dst = log2(src * scale + bias)
-    """
+
+class Log2(UnaryOp):
+    """Compute log2(src) and store to dst."""
 
     op = get_tirx_op("log2")
 
 
+class Log2WithScaleBias(UnaryOpWithScaleBias):
+    """Compute log2(src * scale + bias) and store to dst."""
+
+    op = get_tirx_op("log2_with_scale_bias")
+
+
 class Select(BinaryOp):
     """Select elements from src1 or src2 based on the predicate.
 
@@ -450,8 +462,8 @@ class BinaryReduce(TilePrimitiveCall):
 class UnaryReduce(TilePrimitiveCall):
     """Combine a unary operation with a reduction operation.
 
-    unary_reduce(unary_output, reduce_output, unary_input, unary_op, 
reduce_op, bias, scale, reduce_axes)
-    """  # noqa: E501
+    unary_reduce(unary_output, reduce_output, unary_input, unary_op, 
reduce_op, reduce_axes)
+    """
 
     op = get_tirx_op("unary_reduce")
 
@@ -460,14 +472,12 @@ class UnaryReduce(TilePrimitiveCall):
     unary_input = ArgProperty(2)
     unary_op = ArgProperty(3)
     reduce_op = ArgProperty(4)
-    bias = ArgProperty(5)
-    scale = ArgProperty(6)
-    reduce_axes = ArgProperty(7)
+    reduce_axes = ArgProperty(5)
 
     @property
     def srcs(self) -> list[Expr]:
         """Get the source expressions (inputs) of the operator."""
-        return [self.unary_input, self.bias, self.scale]
+        return [self.unary_input]
 
     @property
     def dsts(self) -> list[Expr]:
@@ -475,6 +485,19 @@ class UnaryReduce(TilePrimitiveCall):
         return [self.unary_output, self.reduce_output]
 
 
+class UnaryReduceWithScaleBias(UnaryReduce):
+    """Write unary(input * scale + bias), then reduce the unary output."""
+
+    op = get_tirx_op("unary_reduce_with_scale_bias")
+    scale = ArgProperty(5)
+    bias = ArgProperty(6)
+    reduce_axes = ArgProperty(7)
+
+    @property
+    def srcs(self) -> list[Expr]:
+        return [self.unary_input, self.scale, self.bias]
+
+
 class BinaryChain(TilePrimitiveCall):
     """Chain multiple binary operations together.
 
diff --git a/python/tvm/tirx/script/ir_builder/tirx.py 
b/python/tvm/tirx/script/ir_builder/tirx.py
index e9cec36ff6..d6051b442f 100644
--- a/python/tvm/tirx/script/ir_builder/tirx.py
+++ b/python/tvm/tirx/script/ir_builder/tirx.py
@@ -137,7 +137,7 @@ def _to_region(buffer: TensorRegion | Var):
 
 
 def _wrap_elem_in_tuple(e):
-    if isinstance(e, tuple | list):
+    if isinstance(e, tuple | list | tvm.ir.Array | tvm.ir.Tuple):
         return e
     return (e,)
 
@@ -184,57 +184,57 @@ def zero(
 def sqrt(
     dst: TensorRegion | Var,
     src: TensorRegion | Var | None = None,
-    bias: TensorRegion | Var | FloatImm | None = None,
-    scale: FloatImm | None = None,
     workspace: dict[str, Var] | None = None,
     dispatch: str | None = None,
     scope: ExecScope | None = None,
     **kwargs,
 ):
-    """Sqrt all elements in src and store to dst.
-
-    dst = sqrt(src * scale + bias)  (if scale or bias are provided)
-
-    Parameters
-    ----------
-    dst : Union[TensorRegion, Var]
-        The destination buffer region for sqrt result.
-        When src is omitted, also used as the source (in-place).
-
-    src : Union[TensorRegion, Var], optional
-        The source buffer region. If omitted, dst is used (in-place).
-
-    bias : Optional[Union[TensorRegion, Var, FloatImm]]
-        The bias of the sqrt src. Only supported on Trn.
-
-    scale : Optional[FloatImm]
-        The scale of the sqrt src. Only supported on Trn.
-
-    workspace : Optional[Dict[str, Var]]
-        The workspace of the operator.
-    """
-    # Expression-form overload: ``sqrt(value)`` returns the underlying 
expression.
+    """Compute sqrt(src), using dst as the source when omitted."""
     from tvm import tirx as _tirx
 
+    if "scale" in kwargs or "bias" in kwargs:
+        raise TypeError("Use sqrt_with_scale_bias for scale and bias operands")
     if not _is_buffer_or_region(dst):
         return _tirx.sqrt(dst)
     if src is None:
         src = dst
-    if workspace is None:
-        workspace = {}
-    config = kwargs or {}
-    dst = _to_region(dst)
-    src = _to_region(src)
-    if bias is not None and is_tensor_var(bias):
-        bias = _to_region(bias)
     return f_insert(
         tirx_op.Sqrt(
-            dst,
-            src,
-            bias,
+            _to_region(dst),
+            _to_region(src),
+            workspace=workspace,
+            config=kwargs,
+            dispatch=dispatch,
+            scope=scope,
+        )
+    )
+
+
+@ScopedOp
+def sqrt_with_scale_bias(
+    dst: TensorRegion | Var,
+    src: TensorRegion | Var | None = None,
+    *,
+    scale: FloatImm,
+    bias: TensorRegion | Var | FloatImm,
+    workspace: dict[str, Var] | None = None,
+    dispatch: str | None = None,
+    scope: ExecScope | None = None,
+    **kwargs,
+):
+    """Compute sqrt(src * scale + bias), with both operands required."""
+    if src is None:
+        src = dst
+    if is_tensor_var(bias):
+        bias = _to_region(bias)
+    return f_insert(
+        tirx_op.SqrtWithScaleBias(
+            _to_region(dst),
+            _to_region(src),
             scale,
+            bias,
             workspace=workspace,
-            config=config,
+            config=kwargs,
             dispatch=dispatch,
             scope=scope,
         )
@@ -531,10 +531,13 @@ def _check_copy_regions_match(dst, src, config, name, 
dispatch=None):
         dst_bits = _payload_bits(dst)
         src_bits = _payload_bits(src)
         gather4 = config.get("gather4")
-        if gather4 is not None:
+        if "gather4" in config:
             if dispatch != "tma_explicit":
                 raise ValueError("copy_async: gather4 is only supported by 
dispatch='tma_explicit'")
-            if not isinstance(gather4, list | tuple | tvm.ir.Array) or 
len(gather4) != 4:
+            if (
+                not isinstance(gather4, list | tuple | tvm.ir.Array | 
tvm.ir.Tuple)
+                or len(gather4) != 4
+            ):
                 raise ValueError("copy_async: gather4 must contain exactly 
four row coordinates")
             if len(src.region) != 2:
                 raise ValueError("copy_async: gather4 requires a rank-2 global 
source")
@@ -1163,55 +1166,26 @@ def minimum(
 def exp(
     dst: TensorRegion | Var,
     src: TensorRegion | Var | None = None,
-    bias: TensorRegion | Var | FloatImm | None = None,
-    scale: FloatImm | None = None,
     workspace: dict[str, Var] | None = None,
     dispatch: str | None = None,
     scope: ExecScope | None = None,
     **kwargs,
 ):
-    """Exponentiate all elements in src and store to dst.
-
-    Parameters
-    ----------
-    dst : Union[TensorRegion, Var]
-        The destination buffer region for exp result.
-        When src is omitted, also used as the source (in-place).
-
-    src : Union[TensorRegion, Var], optional
-        The source buffer region. If omitted, dst is used (in-place).
-
-    bias : Optional[Union[TensorRegion, Var, FloatImm]]
-        The bias of the exp src. Only supported on Trn.
-
-    scale : Optional[FloatImm]
-        The scale of the exp src. Only supported on Trn.
-
-    workspace : Dict[str, Var]
-        The workspace of the operator.
-    """
-    # Expression-form overload: ``exp(value)`` returns the underlying 
expression.
+    """Compute exp(src), using dst as the source when omitted."""
     from tvm import tirx as _tirx
 
+    if "scale" in kwargs or "bias" in kwargs:
+        raise TypeError("Use exp_with_scale_bias for scale and bias operands")
     if not _is_buffer_or_region(dst):
         return _tirx.exp(dst)
     if src is None:
         src = dst
-    if workspace is None:
-        workspace = {}
-    config = kwargs or {}
-    dst = _to_region(dst)
-    src = _to_region(src)
-    if bias is not None and is_tensor_var(bias):
-        bias = _to_region(bias)
     return f_insert(
         tirx_op.Exp(
-            dst,
-            src,
-            bias,
-            scale,
+            _to_region(dst),
+            _to_region(src),
             workspace=workspace,
-            config=config,
+            config=kwargs,
             dispatch=dispatch,
             scope=scope,
         )
@@ -1219,58 +1193,91 @@ def exp(
 
 
 @ScopedOp
-def exp2(
+def exp_with_scale_bias(
     dst: TensorRegion | Var,
     src: TensorRegion | Var | None = None,
-    bias: TensorRegion | Var | FloatImm | None = None,
-    scale: FloatImm | None = None,
+    *,
+    scale: FloatImm,
+    bias: TensorRegion | Var | FloatImm,
     workspace: dict[str, Var] | None = None,
     dispatch: str | None = None,
     scope: ExecScope | None = None,
     **kwargs,
 ):
-    """Compute base-2 exponential (2^x) of all elements in src and store to 
dst.
-
-    Parameters
-    ----------
-    dst : Union[TensorRegion, Var]
-        The destination buffer region for exp2 result.
-        When src is omitted, also used as the source (in-place).
-
-    src : Union[TensorRegion, Var], optional
-        The source buffer region. If omitted, dst is used (in-place).
-
-    bias : Optional[Union[TensorRegion, Var, FloatImm]]
-        The bias of the exp2 src.
+    """Compute exp(src * scale + bias), with both operands required."""
+    if src is None:
+        src = dst
+    if is_tensor_var(bias):
+        bias = _to_region(bias)
+    return f_insert(
+        tirx_op.ExpWithScaleBias(
+            _to_region(dst),
+            _to_region(src),
+            scale,
+            bias,
+            workspace=workspace,
+            config=kwargs,
+            dispatch=dispatch,
+            scope=scope,
+        )
+    )
 
-    scale : Optional[FloatImm]
-        The scale of the exp2 src.
 
-    workspace : Dict[str, Var]
-        The workspace of the operator.
-    """
-    # Expression-form overload: ``exp2(value)`` returns the underlying 
expression.
+@ScopedOp
+def exp2(
+    dst: TensorRegion | Var,
+    src: TensorRegion | Var | None = None,
+    workspace: dict[str, Var] | None = None,
+    dispatch: str | None = None,
+    scope: ExecScope | None = None,
+    **kwargs,
+):
+    """Compute exp2(src), using dst as the source when omitted."""
     from tvm import tirx as _tirx
 
+    if "scale" in kwargs or "bias" in kwargs:
+        raise TypeError("Use exp2_with_scale_bias for scale and bias operands")
     if not _is_buffer_or_region(dst):
         return _tirx.exp2(dst)
     if src is None:
         src = dst
-    if workspace is None:
-        workspace = {}
-    config = kwargs or {}
-    dst = _to_region(dst)
-    src = _to_region(src)
-    if bias is not None and is_tensor_var(bias):
-        bias = _to_region(bias)
     return f_insert(
         tirx_op.Exp2(
-            dst,
-            src,
-            bias,
+            _to_region(dst),
+            _to_region(src),
+            workspace=workspace,
+            config=kwargs,
+            dispatch=dispatch,
+            scope=scope,
+        )
+    )
+
+
+@ScopedOp
+def exp2_with_scale_bias(
+    dst: TensorRegion | Var,
+    src: TensorRegion | Var | None = None,
+    *,
+    scale: FloatImm,
+    bias: TensorRegion | Var | FloatImm,
+    workspace: dict[str, Var] | None = None,
+    dispatch: str | None = None,
+    scope: ExecScope | None = None,
+    **kwargs,
+):
+    """Compute exp2(src * scale + bias), with both operands required."""
+    if src is None:
+        src = dst
+    if is_tensor_var(bias):
+        bias = _to_region(bias)
+    return f_insert(
+        tirx_op.Exp2WithScaleBias(
+            _to_region(dst),
+            _to_region(src),
             scale,
+            bias,
             workspace=workspace,
-            config=config,
+            config=kwargs,
             dispatch=dispatch,
             scope=scope,
         )
@@ -1281,36 +1288,57 @@ def exp2(
 def log2(
     dst: TensorRegion | Var,
     src: TensorRegion | Var | None = None,
-    bias: TensorRegion | Var | FloatImm | None = None,
-    scale: FloatImm | None = None,
     workspace: dict[str, Var] | None = None,
     dispatch: str | None = None,
     scope: ExecScope | None = None,
     **kwargs,
 ):
-    """Compute base-2 logarithm of all elements in src and store to dst."""
-    # Expression-form overload: ``log2(value)`` returns the underlying 
expression.
+    """Compute log2(src), using dst as the source when omitted."""
     from tvm import tirx as _tirx
 
+    if "scale" in kwargs or "bias" in kwargs:
+        raise TypeError("Use log2_with_scale_bias for scale and bias operands")
     if not _is_buffer_or_region(dst):
         return _tirx.log2(dst)
     if src is None:
         src = dst
-    if workspace is None:
-        workspace = {}
-    config = kwargs or {}
-    dst = _to_region(dst)
-    src = _to_region(src)
-    if bias is not None and is_tensor_var(bias):
-        bias = _to_region(bias)
     return f_insert(
         tirx_op.Log2(
-            dst,
-            src,
-            bias,
+            _to_region(dst),
+            _to_region(src),
+            workspace=workspace,
+            config=kwargs,
+            dispatch=dispatch,
+            scope=scope,
+        )
+    )
+
+
+@ScopedOp
+def log2_with_scale_bias(
+    dst: TensorRegion | Var,
+    src: TensorRegion | Var | None = None,
+    *,
+    scale: FloatImm,
+    bias: TensorRegion | Var | FloatImm,
+    workspace: dict[str, Var] | None = None,
+    dispatch: str | None = None,
+    scope: ExecScope | None = None,
+    **kwargs,
+):
+    """Compute log2(src * scale + bias), with both operands required."""
+    if src is None:
+        src = dst
+    if is_tensor_var(bias):
+        bias = _to_region(bias)
+    return f_insert(
+        tirx_op.Log2WithScaleBias(
+            _to_region(dst),
+            _to_region(src),
             scale,
+            bias,
             workspace=workspace,
-            config=config,
+            config=kwargs,
             dispatch=dispatch,
             scope=scope,
         )
@@ -1408,83 +1436,69 @@ def unary_reduce(
     unary_input: TensorRegion | Var,
     unary_op: str | Op,
     reduce_op: str | Op,
-    bias: TensorRegion | Var | FloatImm | None = None,
-    scale: FloatImm | None = None,
     reduce_axes: int | tuple[int] = -1,
     workspace: dict[str, Var] | None = None,
     dispatch: str | None = None,
     scope: ExecScope | None = None,
     **kwargs,
 ):
-    """Combine a unary operation with a reduction operation.
-
-    Parameters
-    ----------
-    unary_output : Union[TensorRegion, Var]
-        The destination buffer region for unary operation result.
-
-    reduce_output : Union[TensorRegion, Var]
-        The destination buffer region for reduction result.
-
-    unary_input : Union[TensorRegion, Var]
-        The source input for unary operation.
-
-    unary_op : Union[str, Op]
-        The unary operation to perform.
-
-    reduce_op : Union[str, Op]
-        The reduction operation to perform.
-
-    bias : Optional[Union[TensorRegion, Var, FloatImm]]
-        The bias to apply before unary operation.
-
-    scale : Optional[FloatImm]
-        The scale to apply before unary operation.
-
-    reduce_axes : Union[int, Tuple[int]]
-        The axes to reduce over.
-
-    workspace : Dict[str, Var]
-        The workspace of the operator.
-
-    dispatch : str, optional
-        The dispatch implementation requested for this operator.
-
-    scope : ExecScope, optional
-        The execution scope for this operator.
+    """Write the unary output, then its reduction."""
+    if "scale" in kwargs or "bias" in kwargs:
+        raise TypeError("Use unary_reduce_with_scale_bias for scale and bias 
operands")
+    if isinstance(unary_op, str):
+        unary_op = tirx_op.get_tirx_op(unary_op)
+    if isinstance(reduce_op, str):
+        reduce_op = tirx_op.get_tirx_op(reduce_op)
+    return f_insert(
+        tirx_op.UnaryReduce(
+            _to_region(unary_output),
+            _to_region(reduce_output),
+            _to_region(unary_input),
+            unary_op,
+            reduce_op,
+            _wrap_elem_in_tuple(reduce_axes),
+            workspace=workspace,
+            config=kwargs,
+            dispatch=dispatch,
+            scope=scope,
+        )
+    )
 
-    **kwargs
-        Scheduler configuration passed as keyword arguments.
-    """
-    if workspace is None:
-        workspace = {}
-    unary_output = _to_region(unary_output)
-    reduce_output = _to_region(reduce_output)
-    unary_input = _to_region(unary_input)
 
-    if bias is not None and is_tensor_var(bias):
+@ScopedOp
+def unary_reduce_with_scale_bias(
+    unary_output: TensorRegion | Var,
+    reduce_output: TensorRegion | Var,
+    unary_input: TensorRegion | Var,
+    unary_op: str | Op,
+    reduce_op: str | Op,
+    scale: FloatImm,
+    bias: TensorRegion | Var | FloatImm,
+    reduce_axes: int | tuple[int] = -1,
+    workspace: dict[str, Var] | None = None,
+    dispatch: str | None = None,
+    scope: ExecScope | None = None,
+    **kwargs,
+):
+    """Write the unary output, then its reduction. Scale and bias apply before 
unary."""
+    if is_tensor_var(bias):
         bias = _to_region(bias)
-
-    reduce_axes = _wrap_elem_in_tuple(reduce_axes)
-
     if isinstance(unary_op, str):
         unary_op = tirx_op.get_tirx_op(unary_op)
     if isinstance(reduce_op, str):
         reduce_op = tirx_op.get_tirx_op(reduce_op)
-
-    config = kwargs or {}
     return f_insert(
-        tirx_op.UnaryReduce(
-            unary_output,
-            reduce_output,
-            unary_input,
+        tirx_op.UnaryReduceWithScaleBias(
+            _to_region(unary_output),
+            _to_region(reduce_output),
+            _to_region(unary_input),
             unary_op,
             reduce_op,
-            bias,
             scale,
-            reduce_axes,
+            bias,
+            _wrap_elem_in_tuple(reduce_axes),
             workspace=workspace,
-            config=config,
+            config=kwargs,
             dispatch=dispatch,
             scope=scope,
         )
@@ -1783,12 +1797,15 @@ __all__ = [
     "cta",
     "exp",
     "exp2",
+    "exp2_with_scale_bias",
+    "exp_with_scale_bias",
     "fdiv",
     "fill",
     "fma",
     "gemm",
     "gemm_async",
     "log2",
+    "log2_with_scale_bias",
     "max",
     "maximum",
     "memset",
@@ -1802,10 +1819,12 @@ __all__ = [
     "select",
     "silu",
     "sqrt",
+    "sqrt_with_scale_bias",
     "sub",
     "sum",
     "thread",
     "unary_reduce",
+    "unary_reduce_with_scale_bias",
     "warp",
     "warpgroup",
     "wg",
diff --git a/python/tvm/tirx/script/tile.py b/python/tvm/tirx/script/tile.py
index a7b08d6741..208420a0a7 100644
--- a/python/tvm/tirx/script/tile.py
+++ b/python/tvm/tirx/script/tile.py
@@ -40,10 +40,18 @@ def _require_buffer_arg(op_name, arg_name, value):
 
 
 def _validate_tile_call(op_name, args, kwargs):
+    if op_name in {"unary_reduce", "unary_reduce_with_scale_bias"}:
+        for index, name in enumerate(("unary_output", "reduce_output", 
"unary_input")):
+            _require_buffer_arg(op_name, name, _get_arg(args, kwargs, index, 
name))
+        return
     dst = _get_arg(args, kwargs, 0, "dst")
     _require_buffer_arg(op_name, "dst", dst)
 
-    if op_name in {"cast", "max", "min", "permute_layout", "silu"}:
+    if op_name.endswith("_with_scale_bias"):
+        src = _get_arg(args, kwargs, 1, "src")
+        if src is not None:
+            _require_buffer_arg(op_name, "src", src)
+    elif op_name in {"cast", "max", "min", "permute_layout", "silu"}:
         src = _get_arg(args, kwargs, 1, "src")
         _require_buffer_arg(op_name, "src", src)
     elif op_name in {"sqrt", "exp", "exp2", "log2", "reciprocal"}:
@@ -71,8 +79,11 @@ _SCOPED_TILE_OP_NAMES = [
     "copy",
     "copy_async",
     "exp",
+    "exp_with_scale_bias",
     "exp2",
+    "exp2_with_scale_bias",
     "log2",
+    "log2_with_scale_bias",
     "fdiv",
     "fill",
     "fma",
@@ -90,9 +101,11 @@ _SCOPED_TILE_OP_NAMES = [
     "select",
     "silu",
     "sqrt",
+    "sqrt_with_scale_bias",
     "sub",
     "sum",
     "unary_reduce",
+    "unary_reduce_with_scale_bias",
     "zero",
 ]
 
diff --git a/python/tvm/tirx/tile_primitive.py 
b/python/tvm/tirx/tile_primitive.py
index 48049f4a0f..a1a7edfc0a 100644
--- a/python/tvm/tirx/tile_primitive.py
+++ b/python/tvm/tirx/tile_primitive.py
@@ -31,7 +31,7 @@ from tvm.target import Target
 
 from . import _ffi_api
 from .exec_scope import ExecScope
-from .expr import FloatImm, IterVar
+from .expr import IterVar
 from .stmt import Stmt
 
 
@@ -219,12 +219,6 @@ class DispatchContext(Object, Scriptable):
         return self.scope_kind == "cluster"
 
 
-def normalize_const_arg(arg) -> Expr:
-    if isinstance(arg, float):
-        return FloatImm("float32", arg)
-    return arg
-
-
 @tvm_ffi.register_object("tirx.TilePrimitiveCall")
 class TilePrimitiveCall(Stmt):
     """TilePrimitiveCall node.
@@ -240,8 +234,10 @@ class TilePrimitiveCall(Stmt):
     workspace : Map[str, Var]
         The workspace.
 
-    config : Map[str, ObjectRef]
-        The scheduler/config dictionary.
+    config : Map[str, Expr]
+        The scheduler/config dictionary. Omit unused keys; explicit None values
+        are invalid. max_inst_size=-1 means unbounded, while omission retains
+        the backend default. A present gather4 contains exactly four 
coordinates.
 
     dispatch : Optional[str]
         The explicit variant name to dispatch to.
@@ -252,7 +248,7 @@ class TilePrimitiveCall(Stmt):
 
     args: list[Expr]
     workspace: dict[str, Var]
-    config: dict[str, Any]
+    config: dict[str, Expr]
     dispatch: str | None
     scope: ExecScope
     _registry: ClassVar[dict[Op, type["TilePrimitiveCall"]]] = {}
@@ -277,7 +273,6 @@ class TilePrimitiveCall(Stmt):
                 "Directly instantiating TilePrimitiveCall needs to specify the 
op"
             )
             op = self.__class__.op
-        args = list(map(normalize_const_arg, args))
         self.__init_handle_by_constructor__(
             _ffi_api.TilePrimitiveCall,
             op,
diff --git a/src/backend/trn/transform/lower_trainium_layout.cc 
b/src/backend/trn/transform/lower_trainium_layout.cc
index fa1fe1c371..822943f94f 100644
--- a/src/backend/trn/transform/lower_trainium_layout.cc
+++ b/src/backend/trn/transform/lower_trainium_layout.cc
@@ -254,7 +254,8 @@ class TrainiumLayoutApplier : public 
tirx::IRMutatorWithAnalyzer {
   }
 
   UnchangedOr<Stmt> Mutate_(const tirx::TilePrimitiveCallNode* op, InplaceMode 
inplace_mode) final {
-    auto args = op->args.Map([this](const ffi::Any& arg) { return 
MutateTileArgument(arg); });
+    auto args = op->args.Map(
+        [this](const Expr& arg) { return 
MutateTileArgument(arg).as_or_throw<Expr>(); });
     if (args.same_as(op->args)) {
       return ffi::Unchanged();
     } else {
diff --git a/src/ir/expr.cc b/src/ir/expr.cc
index 6ab088995e..b1b6b7e4f9 100644
--- a/src/ir/expr.cc
+++ b/src/ir/expr.cc
@@ -833,6 +833,19 @@ PrimExpr::PrimExpr(float value) : 
PrimExpr(FloatImm(PrimType::Float(32), value))
 
 Expr ffi::TypeTraits<Expr>::ConvertFallbackValue(ffi::String value) { return 
StringImm(value); }
 
+std::optional<Expr> ffi::TypeTraits<Expr>::TryCastFromAnyView(const TVMFFIAny* 
src) {
+  if (auto value =
+          ObjectRefWithFallbackTraitsBase<Expr, PrimExpr, 
ffi::String>::TryCastFromAnyView(src)) {
+    return value;
+  }
+  // Keep the recursive array conversion out of line: each element uses this
+  // trait again, while an outer Array<Expr> remains an array.
+  if (auto fields = 
ffi::TypeTraits<ffi::Array<Expr>>::TryCastFromAnyView(src)) {
+    return tvm::Tuple(std::move(*fields));
+  }
+  return std::nullopt;
+}
+
 namespace ffi {
 
 PrimExpr TypeTraits<PrimExpr>::ConvertFallbackValue(StrictBool value) {
diff --git a/src/relax/op/tensor/create.cc b/src/relax/op/tensor/create.cc
index d768d862e9..604b60237b 100644
--- a/src/relax/op/tensor/create.cc
+++ b/src/relax/op/tensor/create.cc
@@ -45,7 +45,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 /* Initialization operators */
 
 /* relax.full */
-Expr full(ffi::Variant<Expr, ffi::Array<PrimExpr>> shape, Expr fill_value,
+// Prefer shape arrays to the generic Expr conversion of sequences to tuples.
+Expr full(ffi::Variant<ffi::Array<PrimExpr>, Expr> shape, Expr fill_value,
           ffi::Optional<DLDataType> dtype) {
   Expr shape_in_expr = [&]() -> Expr {
     if (const auto* expr = shape.as<ExprNode>()) {
diff --git a/src/relax/op/tensor/create.h b/src/relax/op/tensor/create.h
index fea1741a54..4552878990 100644
--- a/src/relax/op/tensor/create.h
+++ b/src/relax/op/tensor/create.h
@@ -41,7 +41,7 @@ namespace relax {
  * If dtype is not given, it will by default use the dtype of fill_value.
  * \return The result tensor.
  */
-Expr full(ffi::Variant<Expr, ffi::Array<PrimExpr>> shape, Expr fill_value,
+Expr full(ffi::Variant<ffi::Array<PrimExpr>, Expr> shape, Expr fill_value,
           ffi::Optional<DLDataType> dtype);
 
 /*!
diff --git a/src/relax/op/tensor/manipulate.cc 
b/src/relax/op/tensor/manipulate.cc
index 62b4182d4b..c9f9b7682f 100644
--- a/src/relax/op/tensor/manipulate.cc
+++ b/src/relax/op/tensor/manipulate.cc
@@ -890,7 +890,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
 /* relax.reshape */
 Expr ConvertNewShapeToExpr(const Expr& data,
-                           const ffi::Variant<Expr, ffi::Array<PrimExpr>>& 
shape) {
+                           const ffi::Variant<ffi::Array<PrimExpr>, Expr>& 
shape) {
   const ffi::ArrayObj* array;
   // Treat shape expressions as constant arrays to handle special values.
   if (const auto* e = shape.as<ShapeExprNode>()) {
@@ -990,7 +990,8 @@ Expr ConvertNewShapeToExpr(const Expr& data,
   return ShapeExpr(array_ref);
 }
 
-Expr reshape(Expr x, ffi::Variant<Expr, ffi::Array<PrimExpr>> shape) {
+// Prefer shape arrays to the generic Expr conversion of sequences to tuples.
+Expr reshape(Expr x, ffi::Variant<ffi::Array<PrimExpr>, Expr> shape) {
   Expr shape_in_expr = ConvertNewShapeToExpr(x, shape);
   static const Op op = Op::Get("relax.reshape");
   return Call::Unchecked(Type::Missing(), op, {std::move(x), 
std::move(shape_in_expr)}, Attrs(),
diff --git a/src/relax/op/tensor/manipulate.h b/src/relax/op/tensor/manipulate.h
index 2066c7b3b4..7525a33dfe 100644
--- a/src/relax/op/tensor/manipulate.h
+++ b/src/relax/op/tensor/manipulate.h
@@ -87,7 +87,7 @@ Expr permute_dims(Expr x, ffi::Optional<ffi::Array<int64_t>> 
axes);
  * It is required to be either an Array of PrimExpr, or a Shape in Relax
  * \return The reshaped result.
  */
-Expr reshape(Expr x, ffi::Variant<Expr, ffi::Array<PrimExpr>> shape);
+Expr reshape(Expr x, ffi::Variant<ffi::Array<PrimExpr>, Expr> shape);
 
 /*!
  * \brief Split input tensor along axis by sections or indices.
diff --git a/src/tirx/ir/stmt_functor.cc b/src/tirx/ir/stmt_functor.cc
index ae1b53fac8..530dde0e2e 100644
--- a/src/tirx/ir/stmt_functor.cc
+++ b/src/tirx/ir/stmt_functor.cc
@@ -213,27 +213,11 @@ ffi::Optional<VisitInterrupt> 
StmtExprVisitor::Visit_(const ScopeIdDefStmtNode*
 }
 
 ffi::Optional<VisitInterrupt> StmtExprVisitor::Visit_(const 
TilePrimitiveCallNode* op) {
-  std::function<ffi::Optional<VisitInterrupt>(const ffi::Any&)> fvisit;
-  fvisit = [this, &fvisit](const ffi::Any& e) -> ffi::Optional<VisitInterrupt> 
{
-    if (e == nullptr) return std::nullopt;
-    if (auto buffer_region = e.as<TensorRegion>()) {
-      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->Visit(buffer_region.value()));
-    } else if (auto expr = e.as<Expr>()) {
-      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->Visit(expr.value()));
-    } else if (auto stmt = e.as<Stmt>()) {
-      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->Visit(stmt.value()));
-    } else if (auto array = e.as<ffi::Array<ffi::Any>>()) {
-      for (const ffi::Any& item : array.value()) {
-        TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(fvisit(item));
-      }
-    }
-    return std::nullopt;
-  };
-  for (const ffi::Any& arg : op->args) {
-    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(fvisit(arg));
+  for (const Expr& arg : op->args) {
+    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->Visit(arg));
   }
   for (const auto& [key, value] : op->config) {
-    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(fvisit(value));
+    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->Visit(value));
   }
   return std::nullopt;
 }
@@ -460,33 +444,6 @@ UnchangedOr<Stmt> StmtExprMutator::Mutate_(const 
SeqStmtNode* op, InplaceMode in
   });
 }
 
-namespace {
-
-template <typename T, typename F>
-UnchangedOr<ffi::Array<T>> MutateTileArray(const ffi::ArrayObj* values, 
InplaceMode mode,
-                                           F fmutate) {
-  // Borrow both the owning container and its elements throughout recursion.
-  if (!values->unique()) mode = InplaceMode::kDisallow;
-  std::vector<std::pair<size_t, T>> replacements;
-  for (size_t i = 0; i < values->size(); ++i) {
-    UnchangedOr<ffi::Any> result = fmutate(ffi::AnyView((*values)[i]), mode);
-    if (!result.UnchangedOrSameAs((*values)[i])) {
-      replacements.emplace_back(i, std::move(result).ValueUnchecked().template 
as_or_throw<T>());
-    }
-  }
-  if (replacements.empty()) return ffi::Unchanged();
-  if (mode == InplaceMode::kAllow) {
-    auto* writable = const_cast<ffi::ArrayObj*>(values);
-    for (auto& [i, value] : replacements) writable->SetItem(i, 
std::move(value));
-    return ffi::Unchanged();
-  }
-  ffi::Array<T> 
result(ffi::GetObjectPtr<ffi::ArrayObj>(const_cast<ffi::ArrayObj*>(values)));
-  for (auto& [i, value] : replacements) result.Set(i, std::move(value));
-  return result;
-}
-
-}  // namespace
-
 UnchangedOr<Stmt> StmtExprMutator::Mutate_(const ScopeIdDefStmtNode* op, 
InplaceMode inplace_mode) {
   // The definition owns both optional arrays; it is skipped by this semantic 
hook.
   InplaceMode def_mode = op->def.unique() ? inplace_mode : 
InplaceMode::kDisallow;
@@ -510,39 +467,9 @@ UnchangedOr<Stmt> StmtExprMutator::Mutate_(const 
ScopeIdDefStmtNode* op, Inplace
 
 UnchangedOr<Stmt> StmtExprMutator::Mutate_(const TilePrimitiveCallNode* op,
                                            InplaceMode inplace_mode) {
-  std::function<UnchangedOr<ffi::Any>(ffi::AnyView, InplaceMode)> mutate_arg;
-  mutate_arg = [&](ffi::AnyView value, InplaceMode mode) -> 
UnchangedOr<ffi::Any> {
-    if (value.as<TensorRegionNode>()) {
-      return Mutate(value, mode);
-    }
-    if (value.as<Expr>() || value.as<StmtNode>()) return Mutate(value, mode);
-    if (const auto* array = value.as<ffi::ArrayObj>()) {
-      return MutateTileArray<ffi::Any>(array, mode, mutate_arg);
-    }
-    return ffi::Unchanged();
-  };
-  auto args = MutateTileArray<ffi::Any>(op->args.GetArrayObj(), inplace_mode, 
mutate_arg);
-  // A config map is another owning container on the path to its values.
-  auto config_mode = op->config.unique() ? inplace_mode : 
InplaceMode::kDisallow;
-  UnchangedOr<ffi::Map<ffi::String, ffi::Any>> config = ffi::Unchanged();
-  std::vector<std::pair<ffi::String, ffi::Any>> replacements;
-  for (const auto& [key, value] : *static_cast<const 
ffi::MapObj*>(op->config.get())) {
-    auto result = mutate_arg(value, config_mode);
-    if (!result.UnchangedOrSameAs(value)) {
-      replacements.emplace_back(key.as_or_throw<ffi::String>(), 
std::move(result).ValueUnchecked());
-    }
-  }
-  if (!replacements.empty()) {
-    if (config_mode == InplaceMode::kAllow) {
-      for (auto& [key, value] : replacements)
-        const_cast<ffi::MapObj*>(static_cast<const 
ffi::MapObj*>(op->config.get()))->at(key) =
-            std::move(value);
-    } else {
-      ffi::Map<ffi::String, ffi::Any> replacement = op->config;
-      for (auto& [key, value] : replacements) replacement.Set(key, 
std::move(value));
-      config = std::move(replacement);
-    }
-  }
+  auto args = Mutate(op->args, 
inplace_mode).as_or_throw<UnchangedOr<ffi::Array<Expr>>>();
+  auto config =
+      Mutate(op->config, 
inplace_mode).as_or_throw<UnchangedOr<ffi::Map<ffi::String, Expr>>>();
   if (args.UnchangedOrSameAs(op->args) && 
config.UnchangedOrSameAs(op->config)) {
     return ffi::Unchanged();
   }
diff --git a/src/tirx/ir/tir_visitor_with_path.cc 
b/src/tirx/ir/tir_visitor_with_path.cc
index 54cd11dbe8..6f7c072f11 100644
--- a/src/tirx/ir/tir_visitor_with_path.cc
+++ b/src/tirx/ir/tir_visitor_with_path.cc
@@ -251,21 +251,22 @@ void TIRVisitorWithPath::Dispatch_(const EvaluateNode* 
op, AccessPath path) {
 }
 
 void TIRVisitorWithPath::Dispatch_(const tirx::TilePrimitiveCallNode* op, 
AccessPath path) {
-  for (size_t i = 0; i < op->args.size(); i++) {
-    if (op->args[i] == nullptr) {
-      continue;
-    }
-    if (auto buf_region = op->args[i].as<TensorRegion>()) {
-      Visit(buf_region.value(), path->Attr("args")->ArrayItem(i));
-    } else if (auto lambda = op->args[i].as<LambdaExpr>()) {
-      Visit(lambda.value(), path->Attr("args")->ArrayItem(i));
-    } else if (auto expr = op->args[i].as<PrimExpr>()) {
-      Visit(expr.value(), path->Attr("args")->ArrayItem(i));
-    } else if (auto stmt = op->args[i].as<Stmt>()) {
-      Visit(stmt.value(), path->Attr("args")->ArrayItem(i));
-    } else if (auto buf = op->args[i].as<TensorVar>()) {
-      VisitBufferUse(buf.value(), path->Attr("args")->ArrayItem(i));
+  std::function<void(const Expr&, AccessPath)> visit = [&](const Expr& expr, 
AccessPath path) {
+    if (auto buffer = expr.as<TensorVar>()) {
+      VisitBufferUse(buffer.value(), path);
+    } else if (const auto* tuple = expr.as<TupleNode>()) {
+      for (size_t i = 0; i < tuple->fields.size(); ++i) {
+        visit(tuple->fields[i], path->Attr("fields")->ArrayItem(i));
+      }
+    } else if (!expr.as<OpNode>()) {
+      Visit(expr, path);
     }
+  };
+  for (size_t i = 0; i < op->args.size(); ++i) {
+    visit(op->args[i], path->Attr("args")->ArrayItem(i));
+  }
+  for (const auto& [key, value] : op->config) {
+    visit(value, path->Attr("config")->MapItem(key));
   }
 }
 
diff --git a/src/tirx/ir/tirx_stmt.cc b/src/tirx/ir/tirx_stmt.cc
index 09a7227bcb..39e081aefb 100644
--- a/src/tirx/ir/tirx_stmt.cc
+++ b/src/tirx/ir/tirx_stmt.cc
@@ -35,15 +35,26 @@ namespace tvm {
 namespace tirx {
 
 // TilePrimitiveCall
-TilePrimitiveCall::TilePrimitiveCall(tvm::Op op, ffi::Array<ffi::Any> args,
+TilePrimitiveCall::TilePrimitiveCall(tvm::Op op, ffi::Array<Expr> args,
                                      ffi::Map<ffi::String, TensorVar> 
workspace,
-                                     ffi::Map<ffi::String, ffi::Any> config,
+                                     ffi::Map<ffi::String, Expr> config,
                                      ffi::Optional<ffi::String> dispatch, 
ExecScope scope)
     : Stmt(ffi::UnsafeInit{}) {
   TVM_FFI_CHECK(op.defined(), ValueError) << "TilePrimitiveCall expects a 
defined operator";
   static const auto& category_map = 
Op::GetAttrMap<TIRxOpCategory>("TIRxOpCategory");
   TVM_FFI_ICHECK(category_map.get(op, ffi::String("")) == "tile_primitive")
       << "Only tile primitive ops can be used in tirx::TilePrimitiveCall";
+  TVM_FFI_CHECK_GE(args.size(), op->args_info.size(), ValueError)
+      << op->name << " requires " << op->args_info.size() << " operands";
+  if (!op->var_args_info.has_value()) {
+    TVM_FFI_CHECK_EQ(args.size(), op->args_info.size(), ValueError)
+        << op->name << " expects " << op->args_info.size() << " operands";
+  }
+  if (auto gather = config.Get("gather4")) {
+    auto tuple = gather.value().as<tvm::Tuple>();
+    TVM_FFI_CHECK(tuple.has_value() && tuple.value()->fields.size() == 4, 
ValueError)
+        << "gather4 must contain exactly four row coordinates";
+  }
   ffi::StructuralVisit(args,
                        [](const TensorRegionNode* region,
                           ffi::StructuralVisitorObj*) -> 
ffi::Optional<ffi::VisitInterrupt> {
@@ -78,11 +89,11 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
TilePrimitiveCallMutate
   const TilePrimitiveCallNode* self =
       ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TilePrimitiveCallNode>(
           value);
-  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<ffi::Any>>, 
mapped_args,
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Expr>>, 
mapped_args,
                                     mutator->MutateExpected(self->args));
 
   using WorkspaceMap = ffi::Map<ffi::String, TensorVar>;
-  using ConfigMap = ffi::Map<ffi::String, ffi::Any>;
+  using ConfigMap = ffi::Map<ffi::String, Expr>;
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<WorkspaceMap>, 
mapped_workspace,
                                     mutator->MutateExpected(self->workspace));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ConfigMap>, mapped_config,
@@ -106,11 +117,11 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
TilePrimitiveCallMaybeI
   TilePrimitiveCallNode* self = const_cast<TilePrimitiveCallNode*>(
       ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TilePrimitiveCallNode>(
           value));
-  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<ffi::Any>>, 
mapped_args,
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Expr>>, 
mapped_args,
                                     mutator->MutateExpected(self->args, 
ffi::InplaceMode::kAllow));
 
   using WorkspaceMap = ffi::Map<ffi::String, TensorVar>;
-  using ConfigMap = ffi::Map<ffi::String, ffi::Any>;
+  using ConfigMap = ffi::Map<ffi::String, Expr>;
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
       ffi::UnchangedOr<WorkspaceMap>, mapped_workspace,
       mutator->MutateExpected(self->workspace, ffi::InplaceMode::kAllow));
@@ -136,9 +147,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   TilePrimitiveCallNode::RegisterReflection();
   refl::GlobalDef().def(
       "tirx.TilePrimitiveCall",
-      [](tvm::Op op, ffi::Array<ffi::Any> args, ffi::Map<ffi::String, 
TensorVar> workspace,
-         ffi::Map<ffi::String, ffi::Any> config, ffi::Optional<ffi::String> 
dispatch,
-         ExecScope scope) {
+      [](tvm::Op op, ffi::Array<Expr> args, ffi::Map<ffi::String, TensorVar> 
workspace,
+         ffi::Map<ffi::String, Expr> config, ffi::Optional<ffi::String> 
dispatch, ExecScope scope) {
         return TilePrimitiveCall(op, args, workspace, config, dispatch, scope);
       });
   refl::TypeAttrDef<TilePrimitiveCallNode>()
diff --git a/src/tirx/op/tirx.cc b/src/tirx/op/tirx.cc
index 2eb26db1ca..1f07dfd3c7 100644
--- a/src/tirx/op/tirx.cc
+++ b/src/tirx/op/tirx.cc
@@ -138,9 +138,13 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
 TVM_DEFINE_CACHED_OP_GETTER(zero, "tirx.tile.zero")
 TVM_DEFINE_CACHED_OP_GETTER(sqrt, "tirx.tile.sqrt")
+TVM_DEFINE_CACHED_OP_GETTER(sqrt_with_scale_bias, 
"tirx.tile.sqrt_with_scale_bias")
 TVM_DEFINE_CACHED_OP_GETTER(exp, "tirx.tile.exp")
+TVM_DEFINE_CACHED_OP_GETTER(exp_with_scale_bias, 
"tirx.tile.exp_with_scale_bias")
 TVM_DEFINE_CACHED_OP_GETTER(exp2, "tirx.tile.exp2")
+TVM_DEFINE_CACHED_OP_GETTER(exp2_with_scale_bias, 
"tirx.tile.exp2_with_scale_bias")
 TVM_DEFINE_CACHED_OP_GETTER(log2, "tirx.tile.log2")
+TVM_DEFINE_CACHED_OP_GETTER(log2_with_scale_bias, 
"tirx.tile.log2_with_scale_bias")
 TVM_DEFINE_CACHED_OP_GETTER(add, "tirx.tile.add")
 TVM_DEFINE_CACHED_OP_GETTER(sub, "tirx.tile.sub")
 TVM_DEFINE_CACHED_OP_GETTER(mul, "tirx.tile.mul")
@@ -158,6 +162,7 @@ TVM_DEFINE_CACHED_OP_GETTER(memset, "tirx.tile.memset")
 TVM_DEFINE_CACHED_OP_GETTER(reduce_negate, "tirx.tile.reduce_negate")
 TVM_DEFINE_CACHED_OP_GETTER(binary_reduce, "tirx.tile.binary_reduce")
 TVM_DEFINE_CACHED_OP_GETTER(unary_reduce, "tirx.tile.unary_reduce")
+TVM_DEFINE_CACHED_OP_GETTER(unary_reduce_with_scale_bias, 
"tirx.tile.unary_reduce_with_scale_bias")
 TVM_DEFINE_CACHED_OP_GETTER(binary_chain, "tirx.tile.binary_chain")
 TVM_DEFINE_CACHED_OP_GETTER(select, "tirx.tile.select")
 TVM_DEFINE_CACHED_OP_GETTER(cast, "tirx.tile.cast")
@@ -176,29 +181,53 @@ TVM_FFI_STATIC_INIT_BLOCK() {
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
   OpDef("tirx.tile.sqrt")
-      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."),
-                 sig::var_args("args"))
+      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."))
       .set_attr<TScriptPrinterName>("TScriptPrinterName", 
ffi::String("tirx.tile.sqrt"))
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
-  OpDef("tirx.tile.exp")
+  OpDef("tirx.tile.sqrt_with_scale_bias")
       .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."),
-                 sig::var_args("args"))
+                 sig::arg("scale", "The scale factor."), sig::arg("bias", "The 
bias."))
+      .set_attr<TScriptPrinterName>("TScriptPrinterName",
+                                    
ffi::String("tirx.tile.sqrt_with_scale_bias"))
+      .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
+
+  OpDef("tirx.tile.exp")
+      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."))
       .set_attr<TScriptPrinterName>("TScriptPrinterName", 
ffi::String("tirx.tile.exp"))
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
-  OpDef("tirx.tile.exp2")
+  OpDef("tirx.tile.exp_with_scale_bias")
       .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."),
-                 sig::var_args("args"))
+                 sig::arg("scale", "The scale factor."), sig::arg("bias", "The 
bias."))
+      .set_attr<TScriptPrinterName>("TScriptPrinterName",
+                                    
ffi::String("tirx.tile.exp_with_scale_bias"))
+      .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
+
+  OpDef("tirx.tile.exp2")
+      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."))
       .set_attr<TScriptPrinterName>("TScriptPrinterName", 
ffi::String("tirx.tile.exp2"))
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
-  OpDef("tirx.tile.log2")
+  OpDef("tirx.tile.exp2_with_scale_bias")
       .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."),
-                 sig::var_args("args"))
+                 sig::arg("scale", "The scale factor."), sig::arg("bias", "The 
bias."))
+      .set_attr<TScriptPrinterName>("TScriptPrinterName",
+                                    
ffi::String("tirx.tile.exp2_with_scale_bias"))
+      .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
+
+  OpDef("tirx.tile.log2")
+      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."))
       .set_attr<TScriptPrinterName>("TScriptPrinterName", 
ffi::String("tirx.tile.log2"))
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
+  OpDef("tirx.tile.log2_with_scale_bias")
+      .signature(sig::arg("dst", "The destination."), sig::arg("src", "The 
source."),
+                 sig::arg("scale", "The scale factor."), sig::arg("bias", "The 
bias."))
+      .set_attr<TScriptPrinterName>("TScriptPrinterName",
+                                    
ffi::String("tirx.tile.log2_with_scale_bias"))
+      .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
+
   OpDef("tirx.tile.add")
       .signature(sig::arg("dst", "The destination."), sig::arg("src1", "The 
first source."),
                  sig::arg("src2", "The second source."))
@@ -306,12 +335,23 @@ TVM_FFI_STATIC_INIT_BLOCK() {
                  sig::arg("reduce_output", "The reduction output."),
                  sig::arg("unary_input", "The unary operation input."),
                  sig::arg("unary_op", "The unary operation."),
-                 sig::arg("reduce_op", "The reduction operation."), 
sig::arg("bias", "The bias."),
-                 sig::arg("scale", "The scale factor."),
+                 sig::arg("reduce_op", "The reduction operation."),
                  sig::arg("reduce_axes", "The reduction axes."))
       .set_attr<TScriptPrinterName>("TScriptPrinterName", 
ffi::String("tirx.tile.unary_reduce"))
       .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
 
+  OpDef("tirx.tile.unary_reduce_with_scale_bias")
+      .signature(sig::arg("unary_output", "The unary operation output."),
+                 sig::arg("reduce_output", "The reduction output."),
+                 sig::arg("unary_input", "The unary operation input."),
+                 sig::arg("unary_op", "The unary operation."),
+                 sig::arg("reduce_op", "The reduction operation."),
+                 sig::arg("scale", "The scale factor."), sig::arg("bias", "The 
bias."),
+                 sig::arg("reduce_axes", "The reduction axes."))
+      .set_attr<TScriptPrinterName>("TScriptPrinterName",
+                                    
ffi::String("tirx.tile.unary_reduce_with_scale_bias"))
+      .set_attr<TIRxOpCategory>("TIRxOpCategory", 
ffi::String("tile_primitive"));
+
   OpDef("tirx.tile.binary_chain")
       .signature(sig::arg("output", "The output."), sig::arg("data", "The 
input data."),
                  sig::arg("operand0", "The first operand."),
diff --git a/src/tirx/script/printer/stmt.cc b/src/tirx/script/printer/stmt.cc
index 352fc1d69e..08695fb90a 100644
--- a/src/tirx/script/printer/stmt.cc
+++ b/src/tirx/script/printer/stmt.cc
@@ -25,6 +25,7 @@
 #include <tvm/tirx/tile_primitive.h>
 
 #include <algorithm>
+#include <cmath>
 #include <optional>
 #include <string>
 #include <utility>
@@ -78,7 +79,6 @@ ffi::Optional<ExprDoc> 
TilePrimitiveCallDocTranslate(DocTranslatorObj* d, ffi::A
   }
   ffi::Array<Doc> args;
   size_t n = stmt->args.size();
-  while (n && stmt->args[n - 1].type_index() == ffi::TypeIndex::kTVMFFINone) 
--n;
   if (n == 2 &&
       (stmt->op->name == "tirx.tile.exp2" || stmt->op->name == 
"tirx.tile.sqrt" ||
        stmt->op->name == "tirx.tile.reciprocal") &&
@@ -115,7 +115,7 @@ ffi::Optional<ExprDoc> 
TilePrimitiveCallDocTranslate(DocTranslatorObj* d, ffi::A
       args.push_back(AnyValue(d, stmt->args[i]));
     }
   }
-  auto dict = [&](const auto& source) -> ffi::Optional<DictDoc> {
+  auto dict = [&](const auto& source, bool config = false) -> 
ffi::Optional<DictDoc> {
     if (source.empty()) return std::nullopt;
     std::vector<std::pair<ffi::String, ffi::Any>> sorted;
     for (const auto& [key, value] : source) sorted.emplace_back(key, value);
@@ -125,6 +125,31 @@ ffi::Optional<ExprDoc> 
TilePrimitiveCallDocTranslate(DocTranslatorObj* d, ffi::A
     ffi::Array<ExprDoc> values;
     for (const auto& [key, value] : sorted) {
       keys.push_back(LiteralDoc::Str(key, std::nullopt));
+      if (config) {
+        if (const auto* string = value.template as<StringImmNode>()) {
+          values.push_back(LiteralDoc::Str(string->value, std::nullopt));
+          continue;
+        }
+        if (const auto* integer = value.template as<IntImmNode>()) {
+          PrimType type = integer->ty.template as_or_throw<PrimType>();
+          if (type == PrimType::Bool()) {
+            values.push_back(LiteralDoc::Boolean(integer->value != 0, 
std::nullopt));
+            continue;
+          }
+          int bits = integer->value >= INT32_MIN && integer->value <= 
INT32_MAX ? 32 : 64;
+          if (type == PrimType::Int(bits)) {
+            values.push_back(LiteralDoc::Int(ffi::GetRef<IntImm>(integer), 
std::nullopt));
+            continue;
+          }
+        }
+        if (const auto* floating = value.template as<FloatImmNode>()) {
+          if (floating->ty.template as_or_throw<PrimType>() == 
PrimType::Float(32) &&
+              std::isfinite(floating->value)) {
+            values.push_back(LiteralDoc::Float(floating->value, std::nullopt));
+            continue;
+          }
+        }
+      }
       values.push_back(AnyValue(d, value));
     }
     return DictDoc(keys, values);
@@ -133,8 +158,21 @@ ffi::Optional<ExprDoc> 
TilePrimitiveCallDocTranslate(DocTranslatorObj* d, ffi::A
   if (stmt->dispatch.has_value()) {
     dispatch = LiteralDoc::Str(stmt->dispatch.value(), std::nullopt);
   }
+  auto keywords = dict(stmt->config, true);
+  if (name == "sqrt_with_scale_bias" || name == "exp_with_scale_bias" ||
+      name == "exp2_with_scale_bias" || name == "log2_with_scale_bias") {
+    ffi::Array<ExprDoc> keys{LiteralDoc::Str("scale", std::nullopt),
+                             LiteralDoc::Str("bias", std::nullopt)};
+    ffi::Array<ExprDoc> values{args[2].as_or_throw<ExprDoc>(), 
args[3].as_or_throw<ExprDoc>()};
+    if (keywords.has_value()) {
+      for (const auto& key : keywords.value()->keys) keys.push_back(key);
+      for (const auto& value : keywords.value()->values) 
values.push_back(value);
+    }
+    keywords = DictDoc(keys, values);
+    args = {args[0], args[1]};
+  }
   d->Emit(OpCallDoc(NamespaceDoc("tirx")->Attr(scope)->Attr(name), args, 
dict(stmt->workspace),
-                    dict(stmt->config), dispatch),
+                    keywords, dispatch),
           ffi::GetRef<ffi::ObjectRef>(stmt));
   return std::nullopt;
 }
diff --git a/src/tirx/transform/lower_tirx_cleanup.cc 
b/src/tirx/transform/lower_tirx_cleanup.cc
index 36dfdbeb85..308d7cdd9c 100644
--- a/src/tirx/transform/lower_tirx_cleanup.cc
+++ b/src/tirx/transform/lower_tirx_cleanup.cc
@@ -276,8 +276,8 @@ class LayoutApplier : public IRMutatorWithAnalyzer {
   }
 
   UnchangedOr<Stmt> Mutate_(const tirx::TilePrimitiveCallNode* op, InplaceMode 
inplace_mode) final {
-    ffi::Array<ffi::Any> args = op->args;
-    args.MutateByApply([this](ffi::Any arg) -> ffi::Any { return 
VisitAny(arg); });
+    ffi::Array<Expr> args = op->args;
+    args.MutateByApply([this](const Expr& arg) { return 
VisitAny(arg).as_or_throw<Expr>(); });
     if (args.same_as(op->args)) {
       return ffi::Unchanged();
     } else {
diff --git 
a/tests/python/tirx/operator/tile_primitive/cuda/elementwise/test_unary.py 
b/tests/python/tirx/operator/tile_primitive/cuda/elementwise/test_unary.py
index 0671c8a76f..3cf2546e70 100644
--- a/tests/python/tirx/operator/tile_primitive/cuda/elementwise/test_unary.py
+++ b/tests/python/tirx/operator/tile_primitive/cuda/elementwise/test_unary.py
@@ -263,33 +263,33 @@ def test_unary_op_shared_with_bias_scale(input, op_type, 
bias_type, src_dtype, d
             T.cuda.cta_sync()
             if bias_type == "const":
                 if op_type == "sqrt":
-                    Tx.cta.sqrt(
+                    Tx.cta.sqrt_with_scale_bias(
                         A_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        const_bias,
-                        scale,
+                        scale=scale,
+                        bias=const_bias,
                     )
                 elif op_type == "exp":
-                    Tx.cta.exp(
+                    Tx.cta.exp_with_scale_bias(
                         A_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        const_bias,
-                        scale,
+                        scale=scale,
+                        bias=const_bias,
                     )
             elif bias_type == "region":
                 if op_type == "sqrt":
-                    Tx.cta.sqrt(
+                    Tx.cta.sqrt_with_scale_bias(
                         A_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        bias_smem[tuple(map_slice_a)],
-                        scale,
+                        scale=scale,
+                        bias=bias_smem[tuple(map_slice_a)],
                     )
                 elif op_type == "exp":
-                    Tx.cta.exp(
+                    Tx.cta.exp_with_scale_bias(
                         A_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        bias_smem[tuple(map_slice_a)],
-                        scale,
+                        scale=scale,
+                        bias=bias_smem[tuple(map_slice_a)],
                     )
             T.cuda.cta_sync()
             Tx.cta.copy(A[tuple(copy_slice)], A_smem[tuple(copy_slice)])
@@ -312,33 +312,33 @@ def test_unary_op_shared_with_bias_scale(input, op_type, 
bias_type, src_dtype, d
             T.cuda.cta_sync()
             if bias_type == "const":
                 if op_type == "sqrt":
-                    Tx.cta.sqrt(
+                    Tx.cta.sqrt_with_scale_bias(
                         B_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        const_bias,
-                        scale,
+                        scale=scale,
+                        bias=const_bias,
                     )
                 elif op_type == "exp":
-                    Tx.cta.exp(
+                    Tx.cta.exp_with_scale_bias(
                         B_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        const_bias,
-                        scale,
+                        scale=scale,
+                        bias=const_bias,
                     )
             elif bias_type == "region":
                 if op_type == "sqrt":
-                    Tx.cta.sqrt(
+                    Tx.cta.sqrt_with_scale_bias(
                         B_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        bias_smem[tuple(map_slice_a)],
-                        scale,
+                        scale=scale,
+                        bias=bias_smem[tuple(map_slice_a)],
                     )
                 elif op_type == "exp":
-                    Tx.cta.exp(
+                    Tx.cta.exp_with_scale_bias(
                         B_smem[tuple(map_slice_res)],
                         A_smem[tuple(map_slice_a)],
-                        bias_smem[tuple(map_slice_a)],
-                        scale,
+                        scale=scale,
+                        bias=bias_smem[tuple(map_slice_a)],
                     )
             T.cuda.cta_sync()
             Tx.cta.copy(B[tuple(map_slice_res)], B_smem[tuple(map_slice_res)])
@@ -657,14 +657,14 @@ def test_unary_op_local_with_bias_scale(input, op_type, 
bias_type, src_dtype, ds
         bias_view = bias_local.view(*bias_shape, layout=acc_layout)
         if bias_type == "const":
             if op_type == "sqrt":
-                Tx.warp.sqrt(res_view, acc_view, const_bias, scale)
+                Tx.warp.sqrt_with_scale_bias(res_view, acc_view, scale=scale, 
bias=const_bias)
             elif op_type == "exp":
-                Tx.warp.exp(res_view, acc_view, const_bias, scale)
+                Tx.warp.exp_with_scale_bias(res_view, acc_view, scale=scale, 
bias=const_bias)
         elif bias_type == "region":
             if op_type == "sqrt":
-                Tx.warp.sqrt(res_view, acc_view, bias_view, scale)
+                Tx.warp.sqrt_with_scale_bias(res_view, acc_view, scale=scale, 
bias=bias_view)
             elif op_type == "exp":
-                Tx.warp.exp(res_view, acc_view, bias_view, scale)
+                Tx.warp.exp_with_scale_bias(res_view, acc_view, scale=scale, 
bias=bias_view)
 
             # write res into B
         for i in T.serial(NUM_COL // 8):
diff --git 
a/tests/python/tirx/operator/tile_primitive/trn/test_compose_op_trn.py 
b/tests/python/tirx/operator/tile_primitive/trn/test_compose_op_trn.py
index c292f06169..5aa3ffd39f 100644
--- a/tests/python/tirx/operator/tile_primitive/trn/test_compose_op_trn.py
+++ b/tests/python/tirx/operator/tile_primitive/trn/test_compose_op_trn.py
@@ -241,7 +241,7 @@ def test_activation_reduce_with_bias_scale():
         C = T.alloc_tensor(C_shape, dtype="float32", scope="trn.sbuf", 
layout=C_layout)
         bias = T.alloc_tensor(bias_shape, dtype="float32", scope="trn.sbuf", 
layout=bias_layout)
         for i in range(2):
-            Tx.unary_reduce(B, C, A[i*16:i*16+16], "sqrt", "sum", 
reduce_axes=1, bias=bias, scale=2.0)  # noqa: E501
+            Tx.unary_reduce_with_scale_bias(B, C, A[i*16:i*16+16], "sqrt", 
"sum", reduce_axes=1, bias=bias, scale=2.0)  # noqa: E501
 
     @T.prim_func
     def expected():
@@ -746,7 +746,7 @@ def test_unary_reduce_complex():
         qk = T.alloc_tensor((2, 128, 8192), scope="trn.sbuf", layout="FPF")
         running_max = T.alloc_tensor((16384, 1), dtype="float32", 
scope="trn.sbuf", layout="PF")
         for i in range(4):
-            Tx.unary_reduce(p[0:128, 0:8192], rowsum_p[i % 2, 0:128, 0], qk[i 
% 2, 0:128, 0:8192], "exp", "sum", bias=running_max[i * 128:i * 128 + 128, 0])  
# noqa: E501
+            Tx.unary_reduce_with_scale_bias(p[0:128, 0:8192], rowsum_p[i % 2, 
0:128, 0], qk[i % 2, 0:128, 0:8192], "exp", "sum", bias=running_max[i * 128:i * 
128 + 128, 0], scale=1.0)  # noqa: E501
 
     @T.prim_func
     def expected():
diff --git 
a/tests/python/tirx/operator/tile_primitive/trn/test_private_alloc_trn.py 
b/tests/python/tirx/operator/tile_primitive/trn/test_private_alloc_trn.py
index 21082e0a6e..f73a2cf73c 100644
--- a/tests/python/tirx/operator/tile_primitive/trn/test_private_alloc_trn.py
+++ b/tests/python/tirx/operator/tile_primitive/trn/test_private_alloc_trn.py
@@ -97,7 +97,7 @@ def test_unary_with_bias_scale():
         T.device_entry()
         A_sbuf = T.alloc_tensor(src_shape, "float32", scope="trn.sbuf", 
layout=src_layout)
         C_sbuf = T.alloc_tensor(dst_shape, "float32", scope="trn.sbuf", 
layout=dst_layout)
-        Tx.exp(C_sbuf, A_sbuf, bias=bias, scale=scale)
+        Tx.exp_with_scale_bias(C_sbuf, A_sbuf, bias=bias, scale=scale)
 
     @T.prim_func
     def expected():
@@ -112,7 +112,7 @@ def test_unary_with_bias_scale():
                                 layout=T.TileLayout(T.S[(128, 4096) : (1@P, 
1@F)]))
         C_sbuf = T.alloc_tensor((512, 1024), scope="trn.sbuf",
                                 layout=T.TileLayout(T.S[(128, 4096) : (1@P, 
1@F)]))
-        Tx.exp(C_sbuf[0:512, 0:1024], A_sbuf[0:512, 0:1024], T.float32(1.0), 
T.float32(2.0), workspace={"const_bias": const_bias})  # noqa: E501
+        Tx.exp_with_scale_bias(C_sbuf[0:512, 0:1024], A_sbuf[0:512, 0:1024], 
scale=T.float32(2.0), bias=T.float32(1.0), workspace={"const_bias": 
const_bias})  # noqa: E501
         # fmt: on
     with target:
         mod = tvm.IRModule({"main": unary})
@@ -264,7 +264,7 @@ def test_activation_reduce_two_stage():
         C = T.alloc_tensor((1, 128), scope="trn.sbuf",
                            layout=T.TileLayout(T.S[(1, 128) : (1@F, 1@P)]))
         for i in range(2):
-            Tx.unary_reduce(B[0:16, 0:512, 0:128], C[0, 0:128], A[i * 16:i * 
16 + 16, 0:512, 0:128], "sqrt", "sum", None, None, [0, 1], 
workspace={"const_bias": const_bias, "partial_reduce": partial_reduce})  # 
noqa: E501
+            Tx.unary_reduce(B[0:16, 0:512, 0:128], C[0, 0:128], A[i * 16:i * 
16 + 16, 0:512, 0:128], "sqrt", "sum", [0, 1], workspace={"const_bias": 
const_bias, "partial_reduce": partial_reduce})  # noqa: E501
         # fmt: on
     with target:
         mod = tvm.IRModule({"main": activation_reduce})
@@ -308,7 +308,7 @@ def test_partial_workspace_specify():
         C = T.alloc_tensor((1, 128), scope="trn.sbuf",
                            layout=T.TileLayout(T.S[(1, 128) : (1@F, 1@P)]))
         for i in range(2):
-            Tx.unary_reduce(B[0:16, 0:512, 0:128], C[0, 0:128], A[i * 16:i * 
16 + 16, 0:512, 0:128], "sqrt", "sum", None, None, [0, 1], 
workspace={"const_bias": const_bias, "partial_reduce": partial_reduce})  # 
noqa: E501
+            Tx.unary_reduce(B[0:16, 0:512, 0:128], C[0, 0:128], A[i * 16:i * 
16 + 16, 0:512, 0:128], "sqrt", "sum", [0, 1], workspace={"const_bias": 
const_bias, "partial_reduce": partial_reduce})  # noqa: E501
         # fmt: on
     with target:
         mod = tvm.IRModule({"main": activation_reduce})
@@ -329,7 +329,7 @@ def test_workspace_reuse():
         T.device_entry()
         A_sbuf = T.alloc_tensor(src_shape, "float32", scope="trn.sbuf", 
layout=src_layout)
         C_sbuf = T.alloc_tensor(dst_shape, "float32", scope="trn.sbuf", 
layout=dst_layout)
-        Tx.exp(C_sbuf, A_sbuf, bias=0.0, scale=scale, max_inst_size=1024)
+        Tx.exp_with_scale_bias(C_sbuf, A_sbuf, bias=0.0, scale=scale, 
max_inst_size=1024)
         Tx.exp(C_sbuf, C_sbuf)
 
     @T.prim_func
@@ -345,8 +345,8 @@ def test_workspace_reuse():
                                 layout=T.TileLayout(T.S[(128, 4096) : (1 @ P, 
1 @ F)]))
         C_sbuf = T.alloc_tensor((512, 1024), scope="trn.sbuf",
                                 layout=T.TileLayout(T.S[(128, 4096) : (1 @ P, 
1 @ F)]))
-        Tx.exp(C_sbuf[0:512, 0:1024], A_sbuf[0:512, 0:1024], T.float32(0.0), 
T.float32(2.0), workspace={"const_bias": const_bias}, max_inst_size=1024)  # 
noqa: E501
-        Tx.exp(C_sbuf[0:512, 0:1024], C_sbuf[0:512, 0:1024], None, None, 
workspace={"const_bias": const_bias})  # noqa: E501
+        Tx.exp_with_scale_bias(C_sbuf[0:512, 0:1024], A_sbuf[0:512, 0:1024], 
scale=T.float32(2.0), bias=T.float32(0.0), workspace={"const_bias": 
const_bias}, max_inst_size=1024)  # noqa: E501
+        Tx.exp(C_sbuf[0:512, 0:1024], C_sbuf[0:512, 0:1024], 
workspace={"const_bias": const_bias})
 
         # fmt: on
 
diff --git a/tests/python/tirx/operator/tile_primitive/trn/test_unary_trn.py 
b/tests/python/tirx/operator/tile_primitive/trn/test_unary_trn.py
index e3287afd3d..491ef43197 100644
--- a/tests/python/tirx/operator/tile_primitive/trn/test_unary_trn.py
+++ b/tests/python/tirx/operator/tile_primitive/trn/test_unary_trn.py
@@ -173,7 +173,7 @@ def test_unary_with_bias_scale(op_type):
     bias_shape = [512, 1]
     bias_layout = TileLayout(S[(128, 4) : (1 @ P, 1 @ F)])
     scale = T.float32(2.0)
-    tx_func = Tx_func_map[op_type]
+    tx_func = getattr(Tx, op_type + "_with_scale_bias")
 
     # fmt: off
     @T.prim_func
@@ -210,7 +210,7 @@ def test_unary_with_bias_scale_2(op_type):
     dst_layout = src_layout
     bias = T.float32(1.0)
     scale = T.float32(2.0)
-    tx_func = Tx_func_map[op_type]
+    tx_func = getattr(Tx, op_type + "_with_scale_bias")
 
     # fmt: off
     @T.prim_func
@@ -261,7 +261,7 @@ def test_unary_with_guard():
         C_sbuf = T.alloc_tensor(dst_shape, "float32", scope="trn.sbuf", 
layout=dst_layout)
         for i in range(4):
             for j in range(4):
-                Tx.sqrt(C_sbuf[0: (i+1) * 128, 0: (j+1)*256], A_sbuf[0: (i+1) 
* 128, 0: (j+1)*256], bias=B_sbuf[0: (i+1) * 128, 0], scale=scale)  # noqa: E501
+                Tx.sqrt_with_scale_bias(C_sbuf[0: (i+1) * 128, 0: (j+1)*256], 
A_sbuf[0: (i+1) * 128, 0: (j+1)*256], bias=B_sbuf[0: (i+1) * 128, 0], 
scale=scale)  # noqa: E501
 
     @T.prim_func
     def expected():
diff --git a/tests/python/tirx/script/test_tirx_script_gpu.py 
b/tests/python/tirx/script/test_tirx_script_gpu.py
index 4a582aba15..c22872d301 100644
--- a/tests/python/tirx/script/test_tirx_script_gpu.py
+++ b/tests/python/tirx/script/test_tirx_script_gpu.py
@@ -315,7 +315,7 @@ def test_workspace_default_none():
     assert len(op_br.workspace) == 0
 
     op_ur = tirx_op.UnaryReduce(
-        B, C, A, tirx_op.get_tirx_op("sqrt"), tirx_op.get_tirx_op("sum"), 
None, None, (-1,)
+        B, C, A, tirx_op.get_tirx_op("sqrt"), tirx_op.get_tirx_op("sum"), (-1,)
     )
     assert len(op_ur.workspace) == 0
 
diff --git a/tests/python/tirx/test_hint.py b/tests/python/tirx/test_hint.py
index 46b4322e9c..5b5a352df6 100644
--- a/tests/python/tirx/test_hint.py
+++ b/tests/python/tirx/test_hint.py
@@ -43,7 +43,7 @@ def test_hint_keyword_arg_on_tx_op():
         config={"hint": "3-input ptx"},
     )
     assert "hint" in op_call.config
-    assert str(op_call.config["hint"]) == "3-input ptx"
+    assert op_call.config["hint"].value == "3-input ptx"
 
 
 def test_hint_keyword_arg_on_tx_op_roundtrip():

Reply via email to