Skip to content

fix(core): validate Host(is_numa_current=...) as a bool - #2542

Open
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:core-host-is-numa-current-validation
Open

fix(core): validate Host(is_numa_current=...) as a bool#2542
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:core-host-is-numa-current-validation

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Host.__new__ validates numa_id carefully, and the existing test explains exactly why:

def test_numa_id_rejects_bool(self):
    # bool is an int subclass; reject explicitly so Host(True) doesn't
    # alias Host(1) (and vice versa) in the singleton cache.

is_numa_current gets no validation at all, even though it goes into the same process-wide singleton cache key and is stored verbatim as the is_numa_current property:

def __new__(cls, numa_id=None, *, is_numa_current=False):
    if is_numa_current and numa_id is not None: ...
    if numa_id is not None and (isinstance(numa_id, bool) or ...): ...   # numa_id IS validated
    return cls._get_or_create(numa_id, is_numa_current)                  # is_numa_current is NOT

@classmethod
def _get_or_create(cls, numa_id, is_numa_current):
    key = (numa_id, is_numa_current)
    ...
    inst._is_numa_current = is_numa_current     # stored as-is

The same aliasing hazard applies from the other side.

1. 1 poisons the numa_current singleton, process-wide. (None, 1) and (None, True) are the same dict key, so whichever call runs first wins for the rest of the process:

>>> Host(is_numa_current=1)          # int, not True
>>> h = Host.numa_current()
>>> h.is_numa_current
1
>>> h.is_numa_current is True
False

test_host.py::TestHost::test_numa_current already asserts h.is_numa_current is True, so that first call turns a shipped assertion into an order-dependent failure — the classic shape of a cross-test flake.

2. Any other truthy value mints a second singleton instead of poisoning the first:

>>> dup = Host(is_numa_current="yes")
>>> repr(dup)
'Host.numa_current()'
>>> dup is Host.numa_current(), dup == Host.numa_current()
(False, False)

Both coerce to the same _LocSpec(kind="host_numa_current") in _memory/_managed_location.py, so they name one physical location while breaking the class's documented contract ("constructor calls with the same arguments return the same instance") and the identity-based set arithmetic in ManagedBuffer.accessed_by.

3. Falsy non-bools hit the generic host. is_numa_current=0 / =None seed the Host() singleton with a non-bool is_numa_current, so Host().is_numa_current is False stops holding too.

4. Unhashable values escape as TypeError. Host(is_numa_current=[]) raises TypeError: unhashable type: 'list' from the cache lookup rather than a ValueError from the constructor.

Fix

Require a real bool, mirroring the numa_id check, with the same ValueError shape:

ValueError: is_numa_current must be a bool, got 1

Nothing else changes: the mutual-exclusion check still runs first (so Host(numa_id=0, is_numa_current=True) keeps its "mutually exclusive" message), and no in-tree caller passes a non-bool.

Tests

Added to cuda_core/tests/test_host.py, directly alongside the numa_id bool test that documents the same hazard:

  • test_is_numa_current_rejects_non_bool, parametrized over 1, 0, "yes", None, [].
  • test_numa_current_singleton_survives_a_rejected_construction, which pins the point of the fix: a rejected construction must not leave anything in the cache, so Host.numa_current().is_numa_current is True still holds afterwards.

Verification I could and could not do

  • _host.py imports only threading and typing, so I loaded the upstream/main and fixed versions side by side and ran both the new assertions and a replica of every existing test_host.py assertion against each.
    • On main: 1, 0, "yes", None raise nothing (the pytest.raises blocks fail) and [] raises TypeError, which pytest.raises(ValueError) does not catch — all five parametrizations fail.
    • With the fix: all five raise the expected ValueError.
    • Every existing behavior — default/numa_id/numa_current construction, singleton identity, numa_id rejection messages, mutual exclusion, __eq__/__hash__, repr, copy/pickle round-trip — is unchanged in both.
  • ruff check / ruff format --check clean on both changed files; python -m py_compile clean.
  • Not run: pytest cuda_core/tests/test_host.py itself. I have no environment where cuda.core is importable (no CUDA driver, no built extension modules), so the module-level from cuda.core import Host could not be exercised locally. Please treat CI as the first real run.

Not in this PR

Host._instances is a single ClassVar dict keyed without cls, so a subclass shares the base class's cache and can be handed a base-class instance (type(Sub()) is Host). __hash__ hardcodes the literal Host in its tuple, so the class looks intended to be final. Nothing in-tree subclasses Host, and fixing it is a separate decision (key on cls, per-subclass caches, or forbid subclassing) — happy to file it separately if you want it addressed.

`Host.__new__` validates `numa_id` carefully -- including an explicit bool
rejection whose comment spells out why:

    # bool is an int subclass; reject explicitly so Host(True) doesn't
    # alias Host(1) (and vice versa) in the singleton cache.

`is_numa_current` gets no validation at all, even though it lands in the
same process-wide singleton cache key and is stored verbatim as the
`is_numa_current` property. The same aliasing hazard applies from the other
side:

    >>> Host(is_numa_current=1)          # int, not True
    >>> h = Host.numa_current()
    >>> h.is_numa_current
    1
    >>> h.is_numa_current is True
    False

`(None, 1)` and `(None, True)` are the same dict key, so whichever call runs
first wins for the lifetime of the process -- and `test_host.py` already
asserts `Host.numa_current().is_numa_current is True`, which that first call
turns into an order-dependent failure.

Non-int truthy values are worse: they mint a second singleton instead of
poisoning the first.

    >>> dup = Host(is_numa_current="yes")
    >>> repr(dup)
    'Host.numa_current()'
    >>> dup is Host.numa_current(), dup == Host.numa_current()
    (False, False)

Both coerce to the same `_LocSpec(kind="host_numa_current")`, so they name
one physical location while breaking the documented "constructor calls with
the same arguments return the same instance" contract and the identity-based
set arithmetic in `ManagedBuffer.accessed_by`.

`is_numa_current=0` and `=None` are equally unhandled: they are falsy, so
they seed the *generic* `Host()` singleton with a non-bool
`is_numa_current`. And an unhashable value reaches the cache lookup and
raises `TypeError: unhashable type` out of the constructor.

Require a real bool, mirroring the numa_id check.
@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.core Everything related to the cuda.core module label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant