← jax4jChapter 5 · Control flow and custom derivatives7 min read

Scan, while loops and structured state

Accumulate a sequence, carry a named running total and count, and explore the differentiation limits of a dynamic while loop.

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

A running total needs yesterday’s state

The increments are [1, 2, 3, 4]. Their running totals are [1, 3, 6, 10]. Each output depends on the previous carry, so independently mapping a function across the increments would miss the dependency. A scan expresses the sequence of state updates and collects an output from every step.

A while loop answers another iteration question: double a value until it reaches or exceeds 10. Starting at 2 gives 2 → 4 → 8 → 16. Its number of steps depends on runtime state. Both APIs keep the carry's shape fixed; the choice is about how iteration is determined, not permission to grow a tensor.

A scan starts with carry zero and processes increments 1,2,3,4. Each step adds its increment and emits the new carry, producing outputs 1,3,6,10 and final carry 10.

Choose Run all, then change the sequence or initial state. This notebook uses jax4j 0.2.1 on the emulated CPU. Structured regions are interpreted by that backend; these are shape and derivative examples rather than device performance measurements. Reloading discards declarations, edits and Java state.

1. Describe one scan step and one while loop

A scan step returns both its next carry and its per-step output. Those can be different tensors. Here they are the same expression: the updated total. The scan API returns separate final-carry and stacked-output tensors afterward.

The while predicate is positive when its scalar state is below 10. Taking sign().maximum(0) turns that condition into the zero/nonzero convention from the conditional lesson. The body doubles the state without changing its shape.

In [ ]:

1. Define the iteration rules

These typed declarations persist for the following cells. The record will organize a second scan's state. A Java int counter in that record would not be a runtime tensor leaf; we use a one-element count tensor so the update belongs to the structured calculation.

The callbacks build supported tensor operations. Keep host reads and mutable Java effects outside them. Their internal temporaries are managed through the region capture or enclosing transformation context; ordinary inputs and returned outputs still need explicit resource owners.

2. Scan over a leading sequence axis

The input has shape [4, 1]: four steps, each receiving a one-element sample. The carry has shape [1] throughout. The per-step output also has shape [1], so collecting all four outputs produces shape [4, 1].

In [ ]:

2. Collect the running totals

Expect final carry 10 and outputs [1, 3, 6, 10] with shape [4, 1]. Change the initial carry to 10. The outputs become [11, 13, 16, 20] and the final carry becomes 20. The sequence is unchanged; every step inherits the different starting state.

Try returning next.mul(next) as the step output while keeping next as the carry. With the original zero start, the collected outputs become [1, 9, 36, 100], while the final carry remains 10. This separates state needed by the next step from observations you want to retain.

Scan captures its body once and represents the fixed sequence length as a region loop. You do not write a Java loop that expands one copy of the body per step. This release can expand static scans for higher-order derivative construction, so that representation statement should not be generalized to every nested derivative at every order.

3. Carry a named total and count

A running mean needs two pieces of state: total and count. scanTree lets us keep those in a record while emitting the mean after each increment. The carry's tree structure and leaf shapes remain fixed across steps.

In [ ]:

3. Scan a structured running mean

Expect total 10, count 4 and means [1, 1.5, 2, 2.5]. Change the second increment from 2 to 6: the new means are [1, 3.5, 10 / 3, 3.5]. Predict the total and count before running it. The count advances through tensor arithmetic, not a Java side effect.

The result record contains a new carry tree plus collected outputs. It does not close its tensor leaves automatically. Whole-tree cleanup is appropriate here because these returned leaves are newly computed; the input tensors remain owned by the outer resource list. The same ownership distinction from Chapter 3 still applies when a different function returns borrowed inputs.

4. Push a tangent through a dynamic while loop

A while loop repeatedly evaluates its predicate on the current carry. Starting at 2, doubling takes three steps and finishes at 16. Forward differentiation can propagate a tangent through those executed updates: a tangent of 1 becomes 8 after three doublings.

In [ ]:

4. Differentiate the while loop in forward mode

Expect final value 16 and tangent 8. Try starting at 3: the loop runs twice, finishes at 12 and has tangent 4. These derivatives describe the path where the number of iterations is locally stable. At inputs where that count changes, the complete function can jump; a path derivative is not a derivative of a discrete iteration-count decision.

Starting from value 2 with tangent 1, three while-loop doublings produce value and tangent pairs 4,2 then 8,4 then 16,8. The loop stops when value is at least 10.

The carry must keep its shape. Use structured state when you need both a value and an explicit iteration counter, and design a terminating predicate. The public whileLoop call has no separate maximum-iteration argument. For a known number of sequence steps, scan makes that bound explicit in its input.

5. Reverse mode for dynamic while is rejected

jax4j 0.2.1 supports forward-mode differentiation of these dynamic while regions, but rejects reverse mode. The next cell intentionally asks grad for that unsupported transformation and catches the exception. A finite answer to the forward example does not imply that reverse mode is available.

In [ ]:

5. Catch the reverse-mode restriction

Do not replace a genuinely dynamic loop with an arbitrary fixed iteration count just to silence this error. If a fixed horizon is part of the model, express it as a scan and differentiate that computation. If you only need a few input directions through a dynamic process, a JVP may answer your actual question directly.

6. Reverse-differentiate a fixed scan

For the additive scan, the final total is the initial value plus every input increment. Its gradient with respect to the four increments is therefore [1, 1, 1, 1]. This fixed sequence supports the reverse-mode question that was unavailable for the dynamic while loop.

In [ ]:

6. Differentiate the scan with respect to its inputs

Expect shape [4, 1] and four ones. Try differentiating the sum of all collected running totals instead, using .outputs().sum() in the lambda. The answer becomes [4, 3, 2, 1]: the first increment influences four reported totals, while the last influences only one. The choice of output objective determines which paths contribute to the gradient.

Continue with custom JVPs, VJPs and checkpoints when an operation needs an explicit derivative contract or recomputation policy. Revisit structured parameters for the distinction between container structure and runtime tensor leaves.


Questions or corrections? open an issue