Use Python properties for Cython descriptors#2056
Use Python properties for Cython descriptors#2056kkraus14 wants to merge 10 commits intoNVIDIA:mainfrom
Conversation
|
mdboom
left a comment
There was a problem hiding this comment.
One suggestion is just a micro-op for startup time that ultimately doesn't matter.
It would be nice to simplify the testing (if possible) though. My concern is corner case syntactic constructions that might not get caught.
| descriptor = inspect.getattr_static(cls, cython_property.name) | ||
|
|
||
| assert isinstance(descriptor, property) | ||
| assert not isinstance(descriptor, types.GetSetDescriptorType) |
There was a problem hiding this comment.
It seems kind of brittle to do a source-level analysis here.
Could we just iterate over all classes and assert that they don't have any GetSetDescriptorType's? It's possible I'm missing something (maybe there are implicit descriptors always that we can't remove), but that seems less complicated.
There was a problem hiding this comment.
Good call. I replaced the source-level parser with runtime introspection over the cuda-core modules/classes.
A literal “no GetSetDescriptorType anywhere” check still needs filtering because Cython also emits getset descriptors for non-property things like __dict__, __weakref__, and typed cdef fields such as option dataclass fields. The updated test now allows those known field descriptors, but fails on any unexpected getset descriptor exposed by cuda-core classes.
There was a problem hiding this comment.
Yeah, thanks for humoring me. The __dict__ and __weakref__ thing doesn't bother me (that's a pretty static set), but I forgot that cdef readonly/cdef public things would be indistinguishable from Cython properties from the outside. Probably best to revert to the first revision that parsed the code. /Maybe/ the agent could do better than regexes by using the Cython parser (I've had reasonable success with that elsewhere), but we could also cross that bridge if we find the regex approach breaks on some future code.
|
All Linux-64 CI segfaulted. (Ignored linux-arm64 CIs, there seems to be a widespread outage.) |
| _OPTIONAL_IMPORT_FAILURES = {"cuda.core._tensor_bridge"} | ||
|
|
||
| _GETSET_FIELD_ALLOWLIST = { | ||
| ("cuda.core._kernel_arg_handler", "ParamHolder", "ptr"), | ||
| ("cuda.core._layout", "_StridedLayout", "itemsize"), | ||
| ("cuda.core._layout", "_StridedLayout", "slice_offset"), | ||
| ("cuda.core._memoryview", "StridedMemoryView", "device_id"), | ||
| ("cuda.core._memoryview", "StridedMemoryView", "exporting_obj"), | ||
| ("cuda.core._memoryview", "StridedMemoryView", "is_device_accessible"), | ||
| ("cuda.core._memoryview", "StridedMemoryView", "ptr"), | ||
| ("cuda.core._memoryview", "StridedMemoryView", "readonly"), | ||
| ("cuda.core._memoryview", "_StridedMemoryViewProxy", "has_dlpack"), | ||
| ("cuda.core._memoryview", "_StridedMemoryViewProxy", "obj"), | ||
| } |
There was a problem hiding this comment.
This still feels very brittle where I'm not too happy about this and don't want to have to maintain a list like this.
| def _is_allowed_getset_descriptor(cls, name, descriptor) -> bool: | ||
| if name in {"__dict__", "__weakref__"}: | ||
| return True | ||
| # Typed cdef fields generate getset descriptors too, but their docs follow | ||
| # this compact "field: type" form rather than a property docstring. | ||
| doc = descriptor.__doc__ | ||
| if doc is not None and doc.startswith(f"{name}:"): | ||
| return True | ||
| return (cls.__module__, cls.__qualname__, name) in _GETSET_FIELD_ALLOWLIST |
There was a problem hiding this comment.
This also feels quite brittle
FYI, this should be restored now. |
| import cuda.core.graph | ||
| import cuda.core.system | ||
|
|
||
| pytestmark = pytest.mark.no_cuda |
leofang
left a comment
There was a problem hiding this comment.
The tensor bridge module is not designed to be importable casually, so it causes a test error.
There was a problem hiding this comment.
Review
Approach
python_property = property is elegant — zero runtime overhead, works with mypy/pyright (as confirmed in this thread), and makes Cython cdef class properties produce real property descriptors that rlcompleter's isinstance(..., property) guard recognizes. This is the right fix.
The _layout segfault (resolved)
The CI segfaults were caused by StridedMemoryView._layout: there's both a cdef _layout field and a @property _layout on the class. With Cython's native getset_descriptor, these coexist (the cdef field is C-only, the property is Python-only). But when @python_property is used, the Python property descriptor masks the cdef field even from Cython code, causing infinite recursion (_layout getter → get_layout() → self._layout → _layout getter → …). The rename to _get_layout() method is the correct fix.
Issues
-
CI failure:
_tensor_bridgeexclusion mismatch. The guardrail test fails in CI because the installed package uses a versioned subpackage (cuda.core.cu12._tensor_bridge), but the exclusion set only matched the unversioned name (cuda.core._tensor_bridge). I pushed a fix (6e182ef) that matches by suffix instead of exact name, socuda.core.cu12._tensor_bridge,cuda.core.cu13._tensor_bridge, etc. are all excluded. -
No functional
rlcompletertest. The guardrail test checks descriptor types structurally, which is good for preventing regressions. But a test that actually callsrlcompleter.Completer.complete()on affected objects and asserts no exception would be stronger end-to-end proof. For example:import rlcompleter completer = rlcompleter.Completer({"mr": mr}) assert completer.complete("mr.", 0) is not None # should not raise
-
Minor: setter pattern could use a comment. The pattern used for read-write properties:
def peer_accessible_by(self): ... def _set_peer_accessible_by(self, devices): ... peer_accessible_by = python_property(peer_accessible_by, _set_peer_accessible_by)
…works correctly, but it's non-obvious why the more Pythonic
@peer_accessible_by.setterpattern isn't used. A brief comment noting that Cythoncdef classbodies don't support.setteron apython_propertywould help future maintainers.
Not an issue
- Over-broad scope (converting all ~439 properties, not just the ~13 affected ones) — defensively correct, prevents future surprises. Worth the churn.
- Error message changes (
"attribute 'X' ... is not writable"→"can't set attribute 'X'") — correct, Pythonpropertyuses different wording thangetset_descriptor. graph/_graph_builder.pyxis not modified — this is fine.GraphBuilderis a regular Pythonclass(notcdef class), so its@propertydecorators already produce Pythonpropertydescriptors thatrlcompleterhandles correctly.
-- Leo's bot
|
(fixing it locally) |
In CI, _tensor_bridge lives under a versioned subpackage (cuda.core.cu12._tensor_bridge), but the exclusion set only matched the unversioned name. Switch to suffix matching so that all versioned variants are excluded.
In CI, _tensor_bridge lives under a versioned subpackage (cuda.core.cu12._tensor_bridge), but the exclusion set only matched the unversioned name. Switch to suffix matching so that all versioned variants are excluded.
|
/ok to test 6e182ef |
Python 3.10 uses "property '...' of '...' object has no setter" while 3.11+ uses "can't set attribute '...'". Relax the regex to accept both.
|
/ok to test 7c52713 |
Summary
python_propertydecorator that creates real Pythonpropertydescriptors inside Cythoncdef classbodies.@python_propertyinstead of generatedgetset_descriptors.propertyinstances and notgetset_descriptorobjects.Testing
cuda_core/pixi.tomlon this branch still has the knownpython_version < "3.11"package target parse error; the pixi fix is intentionally left to the separate PR)