The GitHub Actions job "CI" on tvm-ffi.git/pyobject has failed.
Run started by GitHub user cyx-6 (triggered by cyx-6).

Head commit for run:
ab793e69f447c47cab7ba536362463f86e533796 / Yaxing Cai <[email protected]>
[FEAT][Python] Tie Python wrapper lifetime to underlying C++ FFI object

Make `a.x is a.x` and `id(a.x)` stable in Python by attaching every Python
wrapper to its underlying C++ object via a 16-byte `PyCustomAllocHeader`
prepended to every Object allocation. Reported by Junru Shao as a top
source of agent / OAI-monorepo migration failures. Implements the
state-(a)<->(b) portion of Tianqi Chen's "PyObjectTying" doc (2026-05-01).

Design:
  - Generic custom-allocator hook in core libtvm_ffi.so (no Python
    knowledge): `TVMFFICustomAllocHeader { delete_space }`,
    `TVMFFICustomAllocator { allocate }`, plus
    `TVMFFIGetCustomAllocator` / `TVMFFISetCustomAllocator` /
    `TVMFFISetDefaultCustomAllocator`. libtvm_ffi installs a builtin
    default at registry init, so every `make_object<T>` carries at least
    a 8-byte base header. The Python Cython module overrides the global
    default at module load with `TVMFFIPyAllocate`, which prepends the
    16-byte `PyCustomAllocHeader { py_object; base }`.
  - Single deleter per Handler: `Handler<T>::Deleter_` always invokes
    `GetCustomAllocHeader(tptr)->delete_space(tptr)`. No flag bit, no
    branching at deletion time. The deleter is uniform; dispatch is in
    the function pointer chosen at allocation time.
  - State-machine reduced to (a)<->(b): `py_object == NULL` (no
    canonical wrapper) <-> `py_object == wrapper` (canonical wrapper
    alive). `_install_chandle_binding` and `_detach_chandle_binding`
    flip a single field. `make_ret_object`'s cache-hit fast path
    type-checks the cached wrapper and Py_INCREFs it; stale entries
    (post-move chandle, type re-registration) clear the field and fall
    through to a fresh wrap.
  - Frontend-allocation detection by `delete_space` pointer comparison
    (`TVMFFIPyIsCanonical`): the Python frontend recognizes its own
    allocations by checking `base.delete_space == &TVMFFIPyDeleteSpace`,
    avoiding a flag bit on TVMFFIObject. Pre-Python-init chandles
    (statically-initialized global functions in libtvm_ffi.so) carry
    only the base header; the Python side detects this and skips the
    binding install.
  - State (c) (preserve wrapper memory across a Python finalize) is
    intentionally out of scope. The Cython side has no `tp_finalize`
    resurrection, no `cache_mem`, no cross-language `PyObject_GC_Del`.
    Wrapper memory is owned by Python's tp_free; the C++ block is owned
    by the chandle's deleter. `a.x is a.x` holds while the wrapper is
    held alive (the user-reported case); `id()` is not preserved across
    a `del + gc + re-fetch` cycle.

`PyClassDeleter` in extra/dataclass.cc and the `__ffi_new__` /
`__ffi_shallow_copy__` paths are routed through the same registry, so
Python-defined types share the layout and lifetime semantics.

`TVMFFIPyArgSetterObjectRValueRef_` clears the source's binding eagerly
before the C++ side nulls its `chandle`; otherwise a downstream cache
lookup would see a stale back-pointer to a still-alive wrapper.

Tests: full Python suite passes (2317 passed, 19 skipped, 2 xfailed).
New `tests/python/test_pyobject_tying.py` covers state-(b) identity,
last-ref clean-free, RValueRef move, pickle round-trip, type-mismatch
fallthrough, mutable-field replacement, and PyNativeObject exemption.

Report URL: https://github.com/apache/tvm-ffi/actions/runs/26214873682

With regards,
GitHub Actions via GitBox


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to