Random Operators#
Captured random operators execute ahead of Python, so DALI must keep their RNGs aligned with eager execution.
The draw-pattern rule#
An RNG used by a captured random operator must make the same draws in the same order on every iteration. RNGs used only by eager operators are not tracked.
With a fixed draw pattern, capture-mode and eager loops produce the same random values and leave the RNG in the same state. The guarantee holds across epochs for default and explicit RNGs, regardless of the source driving the loop.
Allowed and rejected#
Captured and eager operators may share an RNG. An operator consuming an eager result also runs eagerly; unrelated operators remain captured.
Changing the draw pattern after tracing raises RuntimeError.
Allowed |
Raises |
|---|---|
Sharing one RNG between captured and uncaptured operators |
Adding a random call that was not there when the loop was traced |
A random operator whose arguments cannot be captured, such as a probability read from
|
Dropping a random call that was traced, since the schedule still expects its draws |
Bare |
Reseeding or restoring state while the loop is live, including assigning the state it already has, because that replaces the generator |
Calling the same random site more than once per iteration |
Touching the RNG between epochs of an |
Examples
Allowed patterns
Captured and eager calls can share an RNG. Bare draws count towards the fixed pattern. DALI warns when a call site repeats and captures only its first call:
rng = ndd.random.RNG(seed=42)
for _ in source.captured(batch_size=4):
rng() # bare draw
eager = ndd.random.coin_flip(batch_size=4, probability=self.prob, rng=rng)
captured = ndd.random.uniform(batch_size=4, shape=3, rng=rng)
for repeat in range(2):
repeated = ndd.random.uniform(batch_size=4, shape=3, rng=rng)
coin_flip runs eagerly because self.prob cannot be proven constant. The other random
operators stay captured, except for the second call at the repeated site.
Adding or dropping a call
Adding this call after tracing raises:
for step, _ in enumerate(source.captured(batch_size=4)):
ndd.random.uniform(batch_size=4, shape=3, rng=rng)
if step > 0:
ndd.random.coin_flip(batch_size=4, rng=rng)
Dropping this traced call also raises:
for step, _ in enumerate(source.captured(batch_size=4)):
ndd.random.uniform(batch_size=4, shape=3, rng=rng)
if step == 0:
ndd.random.coin_flip(batch_size=4, rng=rng)
Replacing the RNG state
Replacing the state while the loop is live raises, even if the assigned state is unchanged:
for step, _ in enumerate(source.captured(batch_size=4)):
if step > 0:
rng.state = new_state
ndd.random.uniform(batch_size=4, shape=3, rng=rng)
Touching the RNG between epochs
Changing the RNG between ExternalSource epochs raises:
source = ndd.ExternalSource(batches, cycle="raise")
def epoch():
for _ in source.captured(batch_size=4):
ndd.random.uniform(batch_size=4, shape=3, rng=rng)
epoch()
rng()
epoch() # raises RuntimeError
Warning
A random operator that produces a single sample instead of a batch is never captured. That
happens when it has neither a batch input nor a batch_size, as in
ndd.random.uniform(shape=3, rng=rng). The call runs eagerly on every step.
Repeated call sites#
A random call site may run more than once per iteration. DALI warns and uses the pipeline result for the first call. Later calls at that site run eagerly because the RNG has already advanced. Use separate call sites to capture each call.