-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PERF: Optimize cudf-polars stable hashing #23582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Base class for IR nodes, and utilities.""" | ||
|
|
@@ -21,13 +21,26 @@ | |
| T = TypeVar("T", bound="Node[Any]") | ||
|
|
||
|
|
||
| def _expand_hashable(obj: Any) -> Any: | ||
| """Expand nested Node instances to their hashable form.""" | ||
| def _update_stable_hasher(hasher: Any, obj: Hashable) -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps a nicer way of doing this is something like the following. The pre-order I think?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohh I like the idea of using its position in the traversal. Let me see how that works.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't immediately able to get this to work :/ |
||
| """ | ||
| Feed ``hasher`` a bottom-up structural digest of ``obj``. | ||
|
|
||
| Nested :class:`Node` instances contribute their full cached digest | ||
| rather than a truncated id or a full subtree expansion, so hashing an | ||
| entire DAG is linear in total local payload size while distinct child | ||
| subtrees remain distinguishable when composing parent digests. | ||
| """ | ||
| if isinstance(obj, Node): | ||
| return _expand_hashable(obj.get_hashable()) | ||
| hasher.update(b"N") | ||
| hasher.update(obj._get_stable_digest()) | ||
| elif isinstance(obj, tuple): | ||
| return tuple(_expand_hashable(x) for x in obj) | ||
| return obj | ||
| hasher.update(b"T") | ||
| hasher.update(len(obj).to_bytes(4, "big")) | ||
| for x in obj: | ||
| _update_stable_hasher(hasher, x) | ||
| else: | ||
| hasher.update(b"L") | ||
| hasher.update(repr(obj).encode("utf-8")) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class Node(Generic[T]): | ||
|
|
@@ -49,11 +62,13 @@ class Node(Generic[T]): | |
| __slots__ = ( | ||
| "_hash_value", | ||
| "_repr_value", | ||
| "_stable_digest", | ||
| "_stable_hash_value", | ||
| "_stable_plan_id", | ||
| "children", | ||
| ) | ||
| _hash_value: int | ||
| _stable_digest: bytes | ||
| _stable_hash_value: int | ||
| _stable_plan_id: uuid.UUID | ||
| _repr_value: str | ||
|
|
@@ -102,17 +117,23 @@ def get_hashable(self) -> Hashable: | |
| """ | ||
| return (type(self), self._ctor_arguments(self.children)) | ||
|
|
||
| def _get_stable_digest(self) -> bytes: | ||
| """Return the full MD5 digest for this node, computing it if needed.""" | ||
| try: | ||
| return self._stable_digest | ||
| except AttributeError: | ||
| h = hashlib.md5(usedforsecurity=False) | ||
| _update_stable_hasher(h, self.get_hashable()) | ||
| self._stable_digest = h.digest() | ||
| return self._stable_digest | ||
|
|
||
| def get_stable_id(self) -> int: | ||
| """ | ||
| Compute a stable identifier for Node. | ||
|
|
||
| Uses MD5 hash of the node's hashable representation for determinism | ||
| across process boundaries (Python's hash() uses PYTHONHASHSEED). | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ir_node | ||
| The IR node. | ||
| Digests :meth:`get_hashable` bottom-up with MD5 so the result is | ||
| deterministic across process boundaries (Python's ``hash()`` uses | ||
| ``PYTHONHASHSEED``). | ||
|
|
||
| Returns | ||
| ------- | ||
|
|
@@ -122,8 +143,10 @@ def get_stable_id(self) -> int: | |
| try: | ||
| return self._stable_hash_value | ||
| except AttributeError: | ||
| content = repr(_expand_hashable(self)).encode("utf-8") | ||
| self._stable_hash_value = int(hashlib.md5(content).hexdigest()[:8], 16) | ||
| # First 4 digest bytes == int(hexdigest()[:8], 16). | ||
| self._stable_hash_value = int.from_bytes( | ||
| self._get_stable_digest()[:4], "big" | ||
| ) | ||
| return self._stable_hash_value | ||
|
|
||
| def get_stable_plan_id(self) -> uuid.UUID: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,6 +172,19 @@ def test_literal_hash(dtype, val): | |
| assert isinstance(hash(Literal(DataType(dtype), val)), int) | ||
|
|
||
|
|
||
| def test_literal_stable_id_is_content_based(): | ||
| a = Literal(DataType(pl.Int64()), 42) | ||
| b = Literal(DataType(pl.Int64()), 42) | ||
| c = Literal(DataType(pl.Int64()), 43) | ||
| assert a.get_stable_id() == b.get_stable_id() | ||
| assert a.get_stable_id() != c.get_stable_id() | ||
| # Distinct Python types with the same dtype stay distinct via value. | ||
| assert ( | ||
| Literal(DataType(pl.String()), "42").get_stable_id() | ||
| != Literal(DataType(pl.Int64()), 42).get_stable_id() | ||
| ) | ||
|
Comment on lines
+181
to
+185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those have different dtypes, so at the very least this comment is wrong. |
||
|
|
||
|
|
||
| def test_struct_literal_not_supported(engine: pl.GPUEngine): | ||
| df = pl.LazyFrame({"a": [1, 2, 3]}) | ||
| q = df.select(pl.lit({"x": 1, "y": "foo"})) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle heterogeneous dictionary keys without ordering them directly.
A dictionary can contain keys such as
1and"1".sortedthen compares these keys and raisesTypeError. This preventsLiteral.get_stable_id()from completing.Use a type-aware canonical key encoding before sorting, or reject unsupported dictionary key types during construction.
🤖 Prompt for AI Agents