SIGN IN SIGN UP

[inductor] Replace `Any` with precise types in symbolic-shape predicates (#194482)

Inductor threads a heterogeneous "maybe-symbolic scalar" through its shape
reasoning -- a dimension that may be a plain `int`, a `sympy.Expr`, a `SymInt`,
or an `fx.Node` carrying one of those in `meta["val"]`. The small predicate
helpers that consume it were all annotated `Any`, so none of their call sites
were checked at all.

Reading them, they are not all the same shape, and this PR gives each the type
that actually fits rather than applying one blanket replacement:

- Helpers whose body `isinstance`-branches over every case take `object`. That
  matches the four helpers of exactly this shape that already use `object`:
  `ir._is_static`, `codegen/cutlass/utils.is_static_int`,
  `codegen/triton.is_sympy_integer_like`, and
  `codegen/cutedsl/cutedsl_kernel._static_integer_expr`.
- Helpers whose body does arithmetic or ordering on the value take `_IntLike`
  (`int | sympy.Expr`). `object` would be wrong there: `%`, `*`, and `<=` are
  not defined on `object`, and reaching for a `cast` to keep the `object`
  annotation would have thrown away the checking the annotation is there to buy.
- Helpers whose body indexes and iterates a size take `Sequence[_IntLike]`, and
  the scaled-mm heuristic's scale arguments -- on which the body calls
  `get_size()` -- take `IRNode`. The one exception is
  `statically_known_shape_equal`, which is handed `FakeTensor.shape` and
  `.stride()`, so its elements can be `SymInt` rather than `sympy.Expr`; since
  it only forwards them to `statically_known_equal`, its element type is
  `object`.

Narrowing a predicate buys nothing if its own caller still launders the value
through `Any` first, so the callers are narrowed up to the frame where the value
enters: `is_flex_gemm_partial_reduction_shape` and
`flex_gemm_output_config_supported` in `kernel/flex_gemm/constraints.py`,
`validate_flex_gemm_aux_outputs` and `flex_gemm_config_keys` in
`kernel/flex_gemm/lowering.py`, and the `NodeInfo` fields that
`codegen/triton_combo_kernel._size_hint` reads (`codegen/simd.py`).

Doing that surfaced several annotations that were already wrong, not merely
loose:

- `statically_known_multiple`'s `divisor` was `int`, but both worlds reach it:
  a plain `int` from `group` and `swap_ab_alignment`, and a `sympy.Expr` from
  `aux_m` / `aux_n`, which originate in `ir.convert_shape_to_inductor`
  (`-> list[sympy.Expr]`). `_IntLike` is the only type covering all four call
  sites.
- `flex_gemm_config_keys` declared `m: int, n: int`. Its production caller
  passes `TensorBox.get_size()` elements, and `test_flex_gemm.py:1169` already
  passes a `sympy.Symbol` for `n`. The function's own `has_free_symbols((n,))`
  guard and the `guard_or_false(sympy.Eq(n % swap_ab_alignment, 0))` branch
  below it would both be dead code if `n` really were an `int`.

`kernel/flex_gemm/constraints.py` needs two unions rather than one, neither of
which is `_IntLike` alone:

- Its local-reduce validators
  (`validate_local_reduce_selected_dim_divisible`,
  `local_reduce_compressed_shape`) are reached only with `torch.Size`-derived
  tuples -- from real tensors at `runtime.py:306, 363, 520`, and from
  `tuple(FakeTensor.shape)` at `fx_cutedsl_codegen.py:271` ->
  `quack_reductions.py:246`. So they take `Sequence[IntLikeType]`
  (`int | SymInt`). Keeping `sympy.Expr` out of them is not cosmetic: sympy's
  `==`/`!=` are structural, so `Mod(s0, 8) != 0` is Python `True`, and a
  merely-unknown dimension would raise `LOCAL_REDUCE_DIVISIBLE_SHAPE_ERROR`
  rather than fall through the way `constraints.py:239-242` intends.
- `statically_known_multiple` sits below both those validators and the
  inductor-lowering callers, so it takes `_IntLike | IntLikeType`. `object` is
  not available for either, since the bodies need `%` and `//`.

`NodeInfo.numel` / `rnumel` become `_IntLike`, but both tiling dicts in the same
NamedTuple are deliberately left as they were, for two different reasons.

For `tiling_scores` the accurate value type is `int`
(`_SubSplit.split_scores: list[int]`, forced through `int(...)` at
`simd.py:5128`, zipped in by `create_tiling` without sympifying). `dict` is
invariant, so correcting it without also moving `SIMDKernel.__init__`,
`SIMDKernelFeatures`, `CandidateTiling`, `create_tiling`,
`ComboKernel.create_triton_kernel`, `FusedUserDefinedTritonKernel`, and
`BaseSchedulerNode.get_tiling` would leave that chain's declarations
contradicting each other -- a coherent follow-up, not this PR.

`tiling` is a tighter bind rather than the same one. Its producer and consumer
both already declare `dict[str, sympy.Expr]`, so matching them would cost
nothing and would make the third `_size_hint` call site checked -- but it would
not be true, since `create_tiling` also receives `tiling_factor` and the
`prod = 1` initializer, both plain `int`. The accurate `dict[str, _IntLike]` is
the one that collides with the chain. Since this PR's whole argument is that
these annotations should mean something, asserting an element type its own
evidence contradicts is the wrong trade, so `tiling` stays bare too.

Flagging the consequence rather than leaving it silent: `_size_hint`'s
narrowing is load-bearing as documentation at two of its three call sites, not
three.

Suggested reading order: `kernel/gemm_epilogue_utils.py` first, since it holds
the base predicates the rest call; then its callers in
`kernel/gemm_epilogue_analysis.py`, `kernel/flex_gemm/constraints.py`, and
`kernel/flex_gemm/lowering.py`; then the independent files (`kernel/mm.py`,
`heuristics/template/triton.py`, `optimize_indexing.py`, and
`codegen/triton_combo_kernel.py` together with the `codegen/simd.py` field
types it reads).

Three changes are not purely annotations.

`statically_known` used to fall through to
`symbolic_shapes.statically_known_true`, which accepts `bool | SymBool` and
raises `AssertionError` on anything else. With the parameter narrowed to
`object` that call no longer type checks. Rather than a `cast`, the fallthrough
now checks `isinstance(expr, torch.SymBool)` and raises `AssertionError` itself
-- the same outcome the callee already produced for the same inputs, but it
states the helper's real contract (`bool | sympy.Basic | SymBool`) in the code.
In practice the new branch is unreachable: every caller passes a comparison
result, which is a `bool` for concrete operands and for `sympy.Expr` `==`
(`Expr.__eq__` is structural), a `sympy` relational for `sympy.Expr` `<`/`<=`
and friends, and a `SymBool` for `SymInt` operands.

`grouped_tensor_layout` called `normalize_shape(shape[0])` inside a branch that
had already established `shape[0]` is a `list | tuple | torch.Size`, so
`normalize_shape` could only ever return `tuple(shape[0])` there. Calling
`tuple` directly keeps `shape` narrowed to a tuple for the rest of the function,
which is what the functions it is then handed to
(`_guard_grouped_reshape_group`, `_syntactic_grouped_tensor_layout`) already
declare they want.

`is_desired_scaling` and `get_scaling_options` in `kernel/mm.py` declared
`scale_size: torch.Tensor`, which was simply wrong -- every caller passes a size
(`scale_a_real.shape`, `scale_a.get_size()`) and the body indexes it and takes
its `len`. Those are now `Sequence[_IntLike]`.

One alternative considered and rejected: a single repo-wide alias for the
"maybe-symbolic scalar" union. These helpers do not agree on the union -- the
`isinstance`-branching ones genuinely accept `fx.Node`, the flex_gemm
local-reduce validators see only `int | SymInt`, and the rest see
`int | sympy.Expr` -- so one alias would have had to be the widest of the three
and would have re-hidden the distinction that motivated the change.

No `cast` and no new type-checker suppression were needed anywhere.

## Test Plan

No behavior change is intended, so the primary check is that the type checker
sees no new errors anywhere in the repo -- not just in the touched files, since
these are signature changes that propagate to callers:

```
pyrefly check   # with the patch, and again with the nine files reverted
```

Both runs report `65 errors (10,965 suppressed)`, and the sorted diagnostic
lists diff clean: zero net-new. Because the trailing count line is part of the
comparison, a newly *suppressed* error would also have shown up. Coverage of the
touched files was confirmed rather than assumed, by appending a deliberately
ill-typed function to `kernel/gemm_epilogue_utils.py` and checking the
whole-repo run reports it.

One caveat on how much that proves, since it cuts against this PR's own
motivation and is worth stating plainly. `pyrefly.toml:134` sets
`replace-imports-with-any = ["sympy.*", ...]`, so `sympy.Expr` resolves to `Any`
and `_IntLike` is effectively `int | Any`. I probed it directly -- a throwaway
`def f(x: _IntLike)` called with a `str` -- and it produces no diagnostic, while
the same probe against `IntLikeType` does. So the `_IntLike` annotations here
are accurate documentation that will begin checking if sympy stubs are ever
enabled, not checking today. The `object`, `IRNode`, `Sequence[object]`, and
`Sequence[IntLikeType]` narrowings are enforced now. `statically_known_multiple`'s
`_IntLike | IntLikeType` is the one exception on that side: the `_IntLike`
member absorbs everything, so the union documents rather than checks.

Lint, clean on all nine touched files:

```
lintrunner --take PYFMT,RUFF,PYREFLY,CODESPELL,FLAKE8 -a \
  torch/_inductor/codegen/simd.py \
  torch/_inductor/codegen/triton_combo_kernel.py \
  torch/_inductor/heuristics/template/triton.py \
  torch/_inductor/kernel/flex_gemm/constraints.py \
  torch/_inductor/kernel/flex_gemm/lowering.py \
  torch/_inductor/kernel/gemm_epilogue_analysis.py \
  torch/_inductor/kernel/gemm_epilogue_utils.py \
  torch/_inductor/kernel/mm.py \
  torch/_inductor/optimize_indexing.py
```

Tests, on a local build of this branch:

```
python test/inductor/test_optimize_indexing.py
python test/inductor/test_indexing.py
python test/inductor/test_flex_gemm.py
python test/inductor/test_torchinductor.py -k cpu
```

`test_optimize_indexing.py` is `OK` (15 run), `test_indexing.py` is `OK` (93
run, 22 skipped), and `test_flex_gemm.py` is `OK` (437 run, 369 GPU-skipped) --
the last includes `test_swap_ab_alignment_filters_tuned_and_explicit_configs`,
which is the test that pushes a `sympy.Symbol` through the corrected
`flex_gemm_config_keys` signature. The CPU inductor suite runs 1493 with 4
errors, all four `TritonMissing` from this box having no Triton install rather
than anything in the diff. The GPU-only suites that cover the rest of the touched code
(`test_flex_gemm.py`, `test_combo_kernels.py`, and the scaled-mm paths) are left
to CI.

This PR was authored with the assistance of an AI coding assistant.

Pull Request resolved: https://github.com/pytorch/pytorch/pull/194482
Approved by: https://github.com/jansel
B
Bob Ren committed
c37ebe43d6f55f1cbc0f0e375d7cecb63c0af2c1
Parent: 029e923
Committed by PyTorch MergeBot <pytorchmergebot@users.noreply.github.com> on 8/23/2026, 7:48:35 PM