← jax4jChapter 4 · Batching and randomness7 min read

Explicit random keys and reproducible draws

Reproduce draws with immutable keys, allocate counter ranges deliberately and avoid accidentally sharing noise across a mapped batch.

Java notebookJava kernel · starts on first runWebGPU · not requested

Run cells from top to bottom. Typed declarations and imports carry into later cells. Shift + Enter runs a cell. Reloading or leaving this page discards edits and Java state.

Ready to start

The second call can return exactly the same numbers

Call key.uniform(4) twice using the same key. Both tensors contain the same four values. Nothing advanced behind your back. In jax4j, PrngKey is an immutable seed-and-counter pair; drawing from it does not replace or mutate it. Reproducibility is explicit enough to become a visible data dependency.

That behavior is useful when replaying a simulation or comparing two algorithms with matched noise. It also means you must deliberately choose new keys or counter ranges when you want fresh draws. Merely repeating a function call is not a stream-management strategy.

A key with seed 123 and counter zero generates uniform values at positions zero through three. Reusing it repeats those positions. Advancing by four selects positions four through seven without changing the original key.

Choose Run all, then change seeds, identifiers and shapes. This notebook uses the bundled jax4j 0.2.1 CPU backend. The examples print generated values and deterministic relationships; they do not claim measured statistical quality from these tiny samples. Reloading discards your Java state and edits.

1. Reusing a key repeats a request

A uniform draw returns F32 values in [0, 1). The request includes the key, distribution, shape and backend. Within this notebook's fixed implementation, repeating the same request reproduces the values exactly. A seed alone is not a complete record of an experiment if any of those other choices change.

In [ ]:

1. Repeat a uniform draw

Expect same values: true and root counter 0. Change the seed to 124 and rerun setup. The generated sequence changes, but its two repeated requests still agree. Return to 123 if you want the same printed values as an earlier run. Recording a seed makes a particular run repeatable; it does not eliminate sampling uncertainty in an estimator.

Try this: request shape [2, 2] instead of [4]. On this CPU uniform implementation, the same four counter positions fill the flattened output; the shape changes their organization. Print the shape alongside the values so reproducibility does not obscure an unintended layout change.

2. Reserve the next uniform counter range

For the CPU uniform generator in jax4j 0.2.1, a tensor with four entries uses four consecutive counter positions. advance(4) returns a new key beginning four positions later. The original key remains unchanged. We can compare this second block with the tail of a single eight-entry request.

In [ ]:

2. Continue a uniform sequence explicitly

Expect matching tail: true, then root=0, next=4. Advance by one instead and compare the two four-entry blocks: their counter ranges overlap in three positions. A different starting counter does not automatically imply a disjoint block of the size you plan to request.

Counter accounting depends on the distribution implementation. This version's CPU normal generator uses two counter positions per returned normal value. It applies a Box–Muller transformation to uniform values. A four-entry normal request therefore uses eight counter positions; advancing by four would not reserve a disjoint next block. Prefer derived keys when you do not want to couple stream management to such implementation details.

3. Derive named substreams by splitting

split(2) derives two child keys without consuming or mutating the parent. Repeating the same split produces the same child keys. Assigning a child to a specific purpose makes that purpose's randomness visible: for example, one for initialization and another for perturbing observations.

In [ ]:

3. Split the root into two child keys

Expect repeat split: true. The two child keys request different deterministic streams. Do not interpret four printed values as proof of statistical independence or distribution quality. The useful guarantee shown here is reproducible derivation: the same parent and derivation procedure recover the same children.

Splitting is not a hidden advancing iterator. Calling root.split(2) again for the next batch simply recreates those children. Keep a deliberate hierarchy of purposes, steps or identifiers instead of repeatedly asking the root for “new” children with the same arguments.

The root key deterministically splits into an initialization key and a perturbation key. Folding a stable observation identifier into the perturbation branch derives that observation's key regardless of processing order.

4. Tie randomness to a stable identifier

foldIn(id) deterministically mixes an identifier into a key. This lets an observation receive the same random request even if you process it in another order. The next cell visits identifiers 7, 2, then 7 again. The two lines for identifier seven should contain exactly the same values.

In [ ]:

4. Revisit an observation out of order

Try reordering the identifiers to [2, 7, 2]. The values associated with each identifier remain stable. Use a persistent observation identifier if you want that property; a position in a shuffled list names the position, not the observation. For step-specific randomness, derive a key from both step and observation, using a consistent order such as root.foldIn(step).foldIn(id).

The order of derivations matters. Do not assume folding in step then identifier is interchangeable with identifier then step. Give different uses their own branches so an observation's augmentation does not accidentally reuse the same request as its diagnostic noise. Keep the key-derivation convention with the rest of the experiment configuration.

5. A captured key stays fixed during replay

uniformLike generates in an existing tensor's backend, device and transformation context. The key itself remains static Java data. Closing over the same key in a compiled function therefore records the same random request for each replay; it does not produce a fresh seed per call.

In [ ]:

5. Replay a compiled random request

Expect same replay: true and one plan. Change the input values and the additive result changes, but the random component still comes from that fixed key and shape. When you need fresh noise for each application, generate it outside the compiled function using a deliberately derived key and pass the noise tensor as an explicit input leaf.

A PrngKey record is not automatically a dynamic compiled-tree argument. Its seed and counter are Java longs, while this version's compiled trees require tensor leaves. Changing a Java key in a closure also does not invalidate old plans. Use the capture rules from Chapter 3 when deciding where generation and key derivation belong.

6. Mapping does not split a fixed key

A fixed key used inside a mapped body requests one logical noise vector. That unmapped random vector is shared when it is added to each mapped row. To allocate different uniform counter positions to each row, generate a whole [2, 3] tensor outside the mapped function, then pass or use it explicitly.

In [ ]:

6. Compare shared noise with a full batch draw

The shared result repeats its first three values in the second row. The whole-batch draw uses counter positions zero through five: its first row matches that three-value request, and its second row uses the next three positions. Those statements describe counter allocation; they do not promise that every drawn value must differ from every other value.

Try shape [3, 3] in both inputs, updating the zero array to length nine. The shared request repeats across three rows, while the batch draw allocates nine positions. For stable per-observation draws across reordered batches, derive keys from identifiers as above rather than relying only on row position.

Return to mapped and shared arguments to feed a precomputed noise tensor alongside your data. Revisit capture rules when a replayed random computation is unexpectedly identical. The key, shape and mapping arrangement together explain exactly which request you made.


Questions or corrections? open an issue