fix: include class-based UDF instance state in hash - #1925
Conversation
Deploying datachain with
|
| Latest commit: |
615bc8a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ce310d55.datachain-2g6.pages.dev |
| Branch Preview URL: | https://ilongin-1903-class-udf-hash.datachain-2g6.pages.dev |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…om:datachain-ai/datachain into ilongin/1903-class-udf-hash-instance-state
There was a problem hiding this comment.
Pull request overview
Updates UDF cache identities to distinguish class instances with different constructor arguments, preventing stale cached results.
Changes:
- Adds normalized value hashing for class-based UDF constructor arguments.
- Preserves deterministic Arrow/Hugging Face and LLM identities.
- Adds unit and functional regression coverage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/datachain/hash_utils.py |
Adds normalized value hashing utilities. |
src/datachain/lib/udf.py |
Incorporates constructor arguments into UDF hashes. |
src/datachain/lib/arrow.py |
Excludes dynamic output schemas from constructor hashes. |
src/datachain/lib/hf.py |
Excludes dynamic output schemas from constructor hashes. |
src/datachain/llm/spec.py |
Reuses shared hash normalization. |
tests/func/test_udf.py |
Tests distinct aggregate results by constructor state. |
tests/unit/lib/test_arrow.py |
Tests Arrow constructor hashing. |
tests/unit/lib/test_hf.py |
Tests Hugging Face constructor hashing. |
tests/unit/lib/test_llm.py |
Removes superseded canonicalization coverage. |
tests/unit/lib/test_udf.py |
Tests UDF hash variation and determinism. |
tests/unit/test_hash_utils.py |
Tests hash-value normalization. |
tests/unit/test_query_steps_hash.py |
Updates expected query hashes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| @classmethod | ||
| def _constructor_hash_args(cls, arguments): | ||
| # output_schema is a dynamically-created pydantic class with a random |
There was a problem hiding this comment.
do we need long docstring even for internal methods?
|
@ilongin check please this AI feedback: [P1] Dict normalization can still cause stale-cache collisions — hash_utils.py:27 [P2] Frozen dataclass UDFs can no longer be instantiated — udf.py:310 [P2] Cyclic constructor containers crash instead of disabling reuse — hash_utils.py:27 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
src/datachain/hash_utils.py:26
- Floats are still hashed through
repr(normalized), which collapses distinct NaN bit patterns (including positive and negative NaN) to the same text. A UDF can observe that difference, for example withmath.copysignorstruct.pack, so different constructor arguments can still share a cache key and return stale rows. Normalize floats to their IEEE-754 bytes before hashing rather than retaining the float object.
if value_type in (bool, int, float, str, bytes):
return (value_type.__name__, value)
|
| self.tokenizer_id = tokenizer_id | ||
| self.max_length = max_length | ||
|
|
||
| def state_hash(self) -> str: |
There was a problem hiding this comment.
Q: is it really a "state" hash? does it change during the operation?
| del self.model | ||
| ``` | ||
|
|
||
| ### Caching class-based operations |
There was a problem hiding this comment.
Q: do we document class based approach at all? where?
There was a problem hiding this comment.
I wonder what terminology we use there ...
There was a problem hiding this comment.
"operations" or something else
| the stored result instead of re-calling the model; the cache invalidates when any | ||
| output-affecting input changes (model, prompt, schema, the input column, `type`, | ||
| params, ...). | ||
| params, ...). Custom objects in parameters receive a per-operation identity, safely |
There was a problem hiding this comment.
this not clear tbh ... in the user facing docs ... why is that a problem, why we do this, etc
| def _constructor_hash_args(cls, arguments): | ||
| arguments = arguments.copy() | ||
| generated_output_schema = arguments.pop("_generated_output_schema") | ||
| if generated_output_schema: |
There was a problem hiding this comment.
is it really a problem to hash it again?
|
Check this feedback also: |
Fixes #1903.
Class-based UDFs (
Mapper/Generator/Aggregatorsubclasses) hashed to the same value regardless of constructor args, so the second.agg(CountAbove(3), ...)call was served the first.agg(CountAbove(0), ...)'s cached rows. No error, no version bump, wrong numbers.Changes:
UDFBase.hash()now mixes in_hash_state()bytes when the UDF is class-based (self._func is None). Default_hash_state()returnsfiltered_cloudpickle_dumps(self), so instance attributes likeself.limitend up in the hash.ArrowGeneratorandHFGeneratoroverride_hash_state()to skipself.output_schema, which is a dynamically-created pydantic class with a random name suffix. The schema's stable field shape is already inself.output.hash(), so no signal is lost anddc.read_csv/dc.read_parquethashes stay deterministic across calls.Function-based UDFs (plain lambdas and
deffunctions passed to.map()) are untouched - the pickle branch only runs for class-based UDFs.