Skip to content

Add ndd.compile.invariant#6429

Open
rostan-t wants to merge 4 commits into
NVIDIA:mainfrom
rostan-t:ndd-compile-invariant
Open

Add ndd.compile.invariant#6429
rostan-t wants to merge 4 commits into
NVIDIA:mainfrom
rostan-t:ndd-compile-invariant

Conversation

@rostan-t

Copy link
Copy Markdown
Collaborator

Category:

New feature (non-breaking change which adds functionality)

Description:

For an operator to be captured in transparent pipelining, we require all of its arguments to be either outputs of another captured operator or constants. We statically detect invariant function parameters, local variables and closure cells. However, it is not tractable to extend this static analysis to e.g. module globals or attributes.

This PR introduces ndd.compile.invariant, which allows marking any variable as invariant in order for it to be blindly trusted during capture in transparent pipelining. This function returns a proxy object that tries to be as close as possible to the original object and is undistinguishable from it in most cases:

>>> x = [1, 2, 3]
>>> y = ndd.compile.invariant(x); print(y)
[1, 2, 3]
>>> y.append(4)
>>> print(x)
[1, 2, 3, 4]
>>> y + [5]
[1, 2, 3, 4, 5]
>>> isinstance(y, list)
True
>>> type(y)
<class 'nvidia.dali.experimental.dynamic.compile._invariant._Invariant_list'>

Objects marked with ndd.compile.invariant also propagate their invariant property to their attributes, allowing for such cases:

args = parser.parse()
args = ndd.compile.invariant(args)

...

for jpegs, labels in reader.next_epoch(batch_size=32, compile=True):
    images = ndd.decoders.image(
        jpegs,
        device=args.device,
        output_type=types.RGB,
        hw_decoder_load=args.hw_load,
        preallocate_width_hint=args.width_hint,
        preallocate_height_hint=args.height_hint,
    )

Additional information:

Affected modules and functionalities:

Dynamic mode, transparent pipelining

Key points relevant for the review:

Two things to review:

  • ndd.compile.invariant itself
  • Its integration in ndd and transparent pipelining

Tests:

  • Existing tests apply
  • New tests added
    • Python tests
    • GTests
    • Benchmark
    • Other
  • N/A

Checklist

Documentation

  • Existing documentation applies
  • Documentation updated
    • Docstring
    • Doxygen
    • RST
    • Jupyter
    • Other
  • N/A

DALI team only

Requirements

  • Implements new requirements
  • Affects existing requirements
  • N/A

REQ IDs: N/A

JIRA TASK: DALI-4818

rostan-t added 4 commits July 22, 2026 20:21
Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
Comment thread dali/test/python/experimental_mode/test_invariant.py Dismissed
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces ndd.compile.invariant, a proxy wrapper that lets users tag values as stable across compiled iterations — covering module globals, config attributes, and other variables that static source analysis cannot prove constant. The integration threads unwrap_invariant/unwrap_invariants calls through every DALI dynamic-mode entry point (Batch, Tensor, ExternalSource, Reader, _op_builder, _compile_intercept), extends the transparent-pipelining classifier to capture invariant-marked arguments even when source code is unavailable, and adds _value_matches to enforce the invariant contract at replay time.

  • Core proxy (_invariant.py): per-type proxy generated lazily via _make_proxy_type; dunders forwarded through _DunderForwarder; isinstance works transparently via __class__ delegation; attribute access propagates the marker automatically.
  • Classification change (_source_analysis.py): classify() no longer short-circuits when module_info is None, allowing invariant-marked args to be captured from exec()'d or C-extension frames where no source AST exists.
  • Compile-graph match (_compile.py): the old direct == comparison in record() is replaced with per-element _value_matches, which trusts invariant pairs without comparing values and raises RuntimeError if the marker is subsequently removed.

Confidence Score: 5/5

Safe to merge; all changed paths are well-covered by new pixel-level tests and a RuntimeError removal test.

The invariant proxy, its integration across all DALI dynamic-mode entry points, and the classifier extension are all correct. The _value_matches helper correctly enforces the invariant contract. Tests validate both the proxy mechanics and end-to-end compile correctness via assert_compiled_matches_eager. The only finding is a one-word docstring inaccuracy.

No files require special attention.

Important Files Changed

Filename Overview
dali/python/nvidia/dali/experimental/dynamic/compile/_invariant.py New core invariant proxy type: lazy per-type proxy factory, correct dunder forwarding, thread-safe lazy cache, and recursive container unwrapping. Well-structured and carefully handles edge cases.
dali/python/nvidia/dali/experimental/dynamic/compile/init.py New package init that re-exports invariant as the public API surface for ndd.compile.invariant.
dali/python/nvidia/dali/experimental/dynamic/_compile.py Refactored record() matching to use per-value _value_matches helper; _matches now unwraps invariant before CompileRef checks; _wire_compile_graph unwraps invariants when building the DALI pipeline. Logic is correct.
dali/python/nvidia/dali/experimental/dynamic/_source_analysis.py Key change: classify() no longer short-circuits on missing module_info, allowing invariant-tagged args to be captured even from exec()'d code; _is_explicit_invariant_expr added to walk CST for explicit invariant expressions; _is_name_invariant restructured to check runtime invariant marker first. Minor docstring inaccuracy.
dali/python/nvidia/dali/experimental/dynamic/_op_builder.py _scalar_decay now starts with unwrap_invariants; reader init args, batch_size, rng, and convert_args loops all gain unwrap_invariant calls; the init_args/call_args split loop was refactored to thread the unwrapping correctly.
dali/test/python/experimental_mode/test_invariant.py New unit tests for the invariant proxy: proxy behaviour, magic methods, recursive unwrapping, and API-level smoke tests. Tests validate correctness with assert_array_equal comparisons.
dali/test/python/experimental_mode/test_compile_invariants.py New compile-path tests for invariant: module-level markers, parameter passing, list args, source-unavailable paths, and the removal-of-marker error. assert_compiled_matches_eager verifies pixel-level correctness.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    User["User code\n(angle = ndd.compile.invariant(x))"] --> Wrapper["_compile_intercept wrapper\nunwrap_invariant(batch_size)"]
    Wrapper --> Classify["classify(frame, inputs, raw_kwargs)\n_Classifier(module_info, frame)"]
    Classify --> CaptureArg["_capture_arg(node, value)"]
    CaptureArg -->|"isinstance(value, CompiledBatch)"| CompileRef["CompileRef stored"]
    CaptureArg -->|"is_invariant(value)"| StoreInvariant["Invariant proxy stored as-is"]
    CaptureArg -->|"is_invariant(node) [static]"| StoreValue["Plain value stored"]
    CaptureArg -->|"else"| Unresolved["_Unresolved → eager execution"]
    StoreInvariant --> Record["record() in CompileContext\n_value_matches(actual, expected)"]
    StoreValue --> Record
    CompileRef --> Record
    Record -->|"is_invariant(expected) & is_invariant(actual)"| TrustInvariant["Return True (unchecked promise)"]
    Record -->|"is_invariant(expected) & not is_invariant(actual)"| RaiseError["RuntimeError: must remain marked"]
    Record -->|"plain comparison"| EqualityCheck["actual == expected"]
    TrustInvariant --> BuildPipeline["_wire_compile_graph\nunwrap_invariants on all inputs/kwargs"]
    EqualityCheck --> BuildPipeline
    BuildPipeline --> DALIPipe["DALI Pipeline\n(compiled)"]
Loading

Reviews (2): Last reviewed commit: "Test ndd.compile.invariant in transparen..." | Re-trigger Greptile

Comment thread dali/python/nvidia/dali/experimental/dynamic/compile/_invariant.py
Comment thread dali/python/nvidia/dali/experimental/dynamic/_compile.py
Comment thread dali/python/nvidia/dali/experimental/dynamic/_batch.py
Comment thread .gitignore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants