C.W.K.
Stream
Lesson 03 of 05 · published

Scan Patterns and Structured Control Flow

~9 min · advanced, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

Inside JIT-compiled functions, Python's for, while, and if statements are evaluated at trace time, not runtime. This means they can't depend on runtime values (like array contents). JAX provides structured alternatives that compile into efficient XLA operations.

jax.lax.scan: Sequential Computation

import jax
import jax.numpy as jnp

# Scan replaces sequential loops with a compiled operation
# Pattern: carry, output = scan(fn, init_carry, inputs)

# Example: running sum (like cumsum but showing the pattern)
def running_sum(xs):
    def step(carry, x):
        new_carry = carry + x
        return new_carry, new_carry  # (new_carry, output)

    final_carry, outputs = jax.lax.scan(step, init=0.0, xs=xs)
    return outputs

xs = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(running_sum(xs))  # [1. 3. 6. 10. 15.]

# Example: apply N identical layers (weight sharing)
def apply_shared_layers(params, x, num_layers):
    def layer_fn(x, _):
        x = jax.nn.relu(x @ params['w'] + params['b'])
        return x, None  # carry = next input, no per-step output

    final_x, _ = jax.lax.scan(layer_fn, x, xs=None, length=num_layers)
    return final_x

jax.lax.while_loop: Dynamic Iteration

# while_loop for dynamic stopping conditions
def newton_sqrt(x, tol=1e-6):
    """Newton's method for square root — unknown number of iterations."""
    def cond_fn(state):
        guess, _ = state
        return jnp.abs(guess * guess - x) > tol

    def body_fn(state):
        guess, count = state
        guess = (guess + x / guess) / 2.0
        return guess, count + 1

    init_state = (x / 2.0, 0)
    final_guess, num_iters = jax.lax.while_loop(cond_fn, body_fn, init_state)
    return final_guess, num_iters

# Works under JIT!
sqrt_5, iters = jax.jit(newton_sqrt)(5.0)
print(f"sqrt(5) ≈ {sqrt_5:.6f} in {iters} iterations")

jax.lax.cond and jax.lax.switch: Branching

# Conditional execution under JIT
def safe_divide(x, y):
    return jax.lax.cond(
        y != 0,
        lambda: x / y,           # true branch
        lambda: jnp.float32(0),  # false branch
    )

# Multi-way switch (like a compiled if/elif/else)
def activation(x, choice):
    return jax.lax.switch(
        choice,
        [
            lambda x: jax.nn.relu(x),     # choice=0
            lambda x: jax.nn.gelu(x),     # choice=1
            lambda x: jnp.tanh(x),        # choice=2
            lambda x: jax.nn.swish(x),    # choice=3
        ],
        x,
    )

⚠️ Pure Function Check

Important limitation: Both branches of jax.lax.cond are traced and compiled. They must have the same output shape and dtype. You can't return a (3, 4) array from one branch and a (5,) array from the other. Similarly, while_loop carry must have fixed shape across iterations — no growing lists or dynamic shapes.

Code

import jax
import jax.numpy as jnp

# Scan replaces sequential loops with a compiled operation
# Pattern: carry, output = scan(fn, init_carry, inputs)

# Example: running sum (like cumsum but showing the pattern)
def running_sum(xs):
    def step(carry, x):
        new_carry = carry + x
        return new_carry, new_carry  # (new_carry, output)

    final_carry, outputs = jax.lax.scan(step, init=0.0, xs=xs)
    return outputs

xs = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(running_sum(xs))  # [1. 3. 6. 10. 15.]

# Example: apply N identical layers (weight sharing)
def apply_shared_layers(params, x, num_layers):
    def layer_fn(x, _):
        x = jax.nn.relu(x @ params['w'] + params['b'])
        return x, None  # carry = next input, no per-step output

    final_x, _ = jax.lax.scan(layer_fn, x, xs=None, length=num_layers)
    return final_x
# while_loop for dynamic stopping conditions
def newton_sqrt(x, tol=1e-6):
    """Newton's method for square root — unknown number of iterations."""
    def cond_fn(state):
        guess, _ = state
        return jnp.abs(guess * guess - x) > tol

    def body_fn(state):
        guess, count = state
        guess = (guess + x / guess) / 2.0
        return guess, count + 1

    init_state = (x / 2.0, 0)
    final_guess, num_iters = jax.lax.while_loop(cond_fn, body_fn, init_state)
    return final_guess, num_iters

# Works under JIT!
sqrt_5, iters = jax.jit(newton_sqrt)(5.0)
print(f"sqrt(5) ≈ {sqrt_5:.6f} in {iters} iterations")
# Conditional execution under JIT
def safe_divide(x, y):
    return jax.lax.cond(
        y != 0,
        lambda: x / y,           # true branch
        lambda: jnp.float32(0),  # false branch
    )

# Multi-way switch (like a compiled if/elif/else)
def activation(x, choice):
    return jax.lax.switch(
        choice,
        [
            lambda x: jax.nn.relu(x),     # choice=0
            lambda x: jax.nn.gelu(x),     # choice=1
            lambda x: jnp.tanh(x),        # choice=2
            lambda x: jax.nn.swish(x),    # choice=3
        ],
        x,
    )

External links

Exercise

Build an RNN forward pass three ways: (1) Python for-loop, (2) jax.lax.scan, (3) jax.lax.fori_loop. Time all three for sequence length 1024. Note when each is the right tool. The lesson is more pattern-recognition than performance.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.