Skip to content

[FEAT] Add structural map, maybe_inplace_mutate, and var_remap APIs - #649

Open
Kathryn-cat wants to merge 1 commit into
apache:mainfrom
Kathryn-cat:kathrync/map
Open

[FEAT] Add structural map, maybe_inplace_mutate, and var_remap APIs#649
Kathryn-cat wants to merge 1 commit into
apache:mainfrom
Kathryn-cat:kathrync/map

Conversation

@Kathryn-cat

@Kathryn-cat Kathryn-cat commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds structural transformation support to TVM FFI, complementing the existing structural equality, hashing, and walking APIs.

The new StructuralMutator recursively transforms reflected object graphs, while structural_map provides a convenient callback-based interface for compiler passes.

Structural mapping

structural_map follows the same typed callback model as structural_walk. Callbacks are matched by runtime type and return either the unchanged value or a replacement.

Mapping defaults to post-order, so callbacks observe values whose children have already been transformed. This makes bottom-up rewrites such as constant folding straightforward:

  def fold_add(expr):
      if isinstance(expr.lhs, IntImm) and isinstance(expr.rhs, IntImm):
          return IntImm(expr.lhs.value + expr.rhs.value)
      return expr

  optimized = tvm_ffi.structural_map(function, (Add, fold_add))

The PR exposes:

  • structural_map in Python.
  • StructuralMap and StructuralMapExpected in C++.
  • Ordered typed callbacks and grouped callback types.
  • Pre-order and post-order transformation.
  • Callbacks receiving either value or (value, def_region_kind).
  • Structural error context when transformation fails.

StructuralMutator

StructuralMutator provides the low-level transformation engine through two operations:

  • Mutate transforms a value without intentionally modifying the input.
  • MaybeInplaceMutate permits implementations to reuse an object when doing so is safe.

Custom mutation behavior

Object types can customize mutation using:

  • __s_mutate__ for canonical non-in-place transformation.
  • __s_maybe_inplace_mutate__ for an optional type-specific in-place optimization.

The maybe-in-place hook owns its safety policy and may reuse the input, delegate to normal mutation, or return another value. A type providing it must also provide __s_mutate__.

Built-in hooks are registered for Array, List, Map, and Dict.

Variable identity remapping

The mutator maintains an identity-substitution environment for FreeVar objects. Once an identity is mapped, later occurrences reuse the same result.

For example:

  def specialize_shape_var(var):
      if var.name == "n":
          return IntImm(10)
      return var

  specialized = tvm_ffi.structural_map(
      function,
      (Var, specialize_shape_var),
  )

If the same Var occurs in a function parameter and its body, both occurrences resolve to the same mapped result.

The mutator also exposes get_var_remap and set_var_remap, allowing custom DAG-style variable wrappers to use their underlying identity object as the remapping key.

@Kathryn-cat
Kathryn-cat marked this pull request as ready for review June 29, 2026 05:58

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the StructuralMapper and StructuralMapperObj APIs in include/tvm/ffi/extra/structural_map.h to support structural mapping and in-place mutation of object-backed values. It also adds corresponding custom hooks (kStructuralMap and kStructuralInplaceMutate), registers test leaf objects, and includes comprehensive unit tests. The review feedback suggests two minor optimizations in structural_map.h: avoiding a const_cast when obtaining the field address, and reusing the calculated field address instead of recalculating it for the field setter.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated
Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 2 times, most recently from 2d1fe02 to 94ef772 Compare June 29, 2026 06:37
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 4 times, most recently from d407312 to a72586a Compare July 16, 2026 15:39
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 2 times, most recently from 1fa228c to 994c4f7 Compare July 16, 2026 23:24
Comment thread include/tvm/ffi/extra/structural_map.h Outdated
@Kathryn-cat Kathryn-cat changed the title wip: structural map [FEAT] Add structural map and inplace mutate APIs Jul 17, 2026
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 3 times, most recently from 9858fd5 to a86c5cf Compare July 20, 2026 18:39
@Kathryn-cat Kathryn-cat changed the title [FEAT] Add structural map and inplace mutate APIs [FEAT] Add structural map, maybe_inplace_mutate, and var_remap APIs Jul 20, 2026
Comment thread include/tvm/ffi/extra/structural_mutator.h Outdated
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 12 times, most recently from ee20d47 to 9952e82 Compare July 21, 2026 14:25
Comment thread include/tvm/ffi/extra/structural_visit.h Outdated
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 9 times, most recently from 0c891b6 to 43b5ba1 Compare July 22, 2026 20:36
@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 5 times, most recently from 10276ce to 6b570b3 Compare August 1, 2026 06:36
@Kathryn-cat
Kathryn-cat requested a review from tqchen August 1, 2026 06:40
Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated

@tqchen tqchen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main issue to to clarify the var handling in callback and testcase simplification and coverage

Comment thread include/tvm/ffi/extra/structural_mutate.h
Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated
Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated
Comment thread include/tvm/ffi/extra/structural_mutate.h
Comment thread include/tvm/ffi/extra/structural_mutate.h Outdated
TEST(StructuralMutator, HandlesPODAndPerInstanceVariableRemapAPIs) {
TestStructuralMutator mutator;

EXPECT_EQ(mutator->Mutate(int64_t{42}).cast<int64_t>(), 42);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are too basic test, we just need to integrate test StructuralMap with the new instances

}

// The first changed value lazily creates a copy when the source map is shared.
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need test case coverage of inplace mutate case

* \param value The borrowed value to transform.
* \return The transformed value or an Error.
*/
Expected<Any> MaybeInplaceMutateImpl(AnyView value) noexcept {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One case we need to fix is when value is a freevar, we need a special path for it. Since callback may again update the Var mapped value, in which case we need to override by setting free var map

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, just tested and now preorder and postorder would both fail when the callback directly replaces the same freevar used multiple times. I think we need to have tests to cover them

* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.

@tqchen tqchen Aug 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us have a few compact cases covering high-level usage of StructurualMap, need to be able to test

  • inplace happens correctly for Map/Array when certain values are unique, while none implace happens when certain values are shared copies in map
  • the hook works e2e as part of StructurualMap, no need to have shallow UT

Comment thread docs/concepts/structural_eq_hash.rst
@tqchen

tqchen commented Aug 2, 2026

Copy link
Copy Markdown
Member

One thing that worth double click on is var remapping handling when callback also presence. In an ideal case, the logic should be like follows:

  • In MutateExpectedImpl and MaybeInplaceMutateExpectedImpl
  • First run a quick check if value is free_var (through TypeInfo and var , two path:
    • not a free var: apply the original old logic
    • value is a free var, look up var remap
      • if exist, return original value(not invoking callback)
      • if missing, run callback + default, or default + callback, in both case, after execution finish, need to override the final value to var remap(since callback may further change the var remapped value)
      • future invocations will hit the var remap and not recurse into callback.

To keep things consistent with structural hash, we can turn on the remapping for both

structural_eq_hash_kind == kTVMFFISEqHashKindDAGNode ||
structural_eq_hash_kind == kTVMFFISEqHashKindFreeVar

This allows node being declared as DAG node also being mapped once on first occurance.

Comment thread include/tvm/ffi/c_api.h
Comment on lines +409 to +414
static reflection::TypeAttrColumn column(reflection::type_attr::kShallowCopy);
AnyView attr = column[type_index];
if (TVM_FFI_PREDICT_FALSE(attr.type_index() != TypeIndex::kTVMFFIFunction)) {
return Unexpected(
Error("TypeError",
std::string(reflection::type_attr::kShallowCopy) + " must be an ffi.Function", ""));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that it does not support classes like ffi::string.

import tvm_ffi

identity = (int, lambda v: v)

tvm_ffi.structural_walk(tvm_ffi.Array([123, "12345678"]), (int, lambda v: None))
tvm_ffi.structural_map(tvm_ffi.Array([123, "12345678"]), (int, lambda v: v))

it cannot work, not sure if expected

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems something to address, might post the full error message?

@Kathryn-cat
Kathryn-cat force-pushed the kathrync/map branch 6 times, most recently from 51744da to 7c1ab6c Compare August 7, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants