the jax path · 0/12
start the path

the jax path · Control flow · lesson 01 of 3

Both branches, one index

Two branch functions go into a cond and both of them run while you trace. The predicate stops being a boolean before the primitive sees it, and what finally picks a branch is an integer.

the goal Predict what a cond or a switch puts into a jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr →, name everything the branches have to agree about, and say what batching changes about the cost of the branch nobody took and about the gradient through it.

mastery work · this chapter0/3
  1. go →
  2. go →
manual items are your word; auto items complete from your streaks, labs, and can-you ticks · stored in your browser only
§ 01

Both branch functions run while you trace

Put a print inside each branch function and trace the function once. Both prints fire. A jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → for each branch has to exist before the primitive can be built, so your Python runs once per branch, at trace time, whatever the predicate later turns out to be.

The jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → then shows what became of the predicate. gt produced a bool[], and the very next equation converts it to i32[]. cond never receives a boolean at all; it receives an index and a tuple of branches.

Read the branch order twice, because it is the reverse of how the call is written. div sits in the first slot and mul in the second, and div was the false branch. One line in the source says why: cond_p.bind(index, *consts, *ops, branches=(false_jaxpr, true_jaxpr)). Slot 0 is false, slot 1 is true, so a two-way cond is a two-entry switch that was handed a converted boolean.

run it (verified, jax 0.4.38 CPU): two prints at trace time, and the index the predicate turned into
import jax
import jax.numpy as jnp

def flip(x):
    def hot(v):
        print("tracing hot")
        return v * 2.0
    def cold(v):
        print("tracing cold")
        return v / 2.0
    return jax.lax.cond(x.sum() > 0, hot, cold, x)

print(jax.make_jaxpr(flip)(jnp.ones(3)))

# tracing hot
# tracing cold
# { lambda ; a:f32[3]. let
#     b:f32[] = reduce_sum[axes=(0,)] a
#     c:bool[] = gt b 0.0
#     d:i32[] = convert_element_type[new_dtype=int32 weak_type=False] c
#     e:f32[3] = cond[
#       branches=(
#         { lambda ; f:f32[3]. let g:f32[3] = div f 2.0 in (g,) }
#         { lambda ; h:f32[3]. let i:f32[3] = mul h 2.0 in (i,) }
#       )
#     ] d a
#   in (e,) }
§ 02

A literal predicate does not delete a branch

Hand cond a Python True and nothing collapses. Both branches are still in the jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → and the index is the literal 1. The primitive was built the same way it always is; only the value feeding it is known early.

That is worth holding against the chapter above, where the branching that really does resolve during tracing is a Python if on a static value. The two cases look similar in the source and land in different places, and the jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → is where you tell them apart: an if leaves no cond equation behind at all, and a cond leaves one even when its predicate is a constant.

run it (verified, jax 0.4.38 CPU): a constant predicate, and still two branches
import jax
import jax.numpy as jnp

def always(x):
    return jax.lax.cond(True, lambda v: v * 2.0, lambda v: v / 2.0, x)

print(jax.make_jaxpr(always)(jnp.ones(3)))

# { lambda ; a:f32[3]. let
#     b:f32[3] = cond[
#       branches=(
#         { lambda ; c:f32[3]. let d:f32[3] = div c 2.0 in (d,) }
#         { lambda ; e:f32[3]. let f:f32[3] = mul e 2.0 in (f,) }
#       )
#     ] 1 a
#   in (b,) }
§ 03

Switch clamps the index and says nothing

jax.lax.switch is the n-way form, and its docstring states the semantics as three lines of Python you can hold in your head. Clamp the index into range, then apply that branch. The clamp is not a footnote: it is an equation in the jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr →, sitting right before the cond.

So an index of 7 against three branches runs branch 2, and an index of -5 runs branch 0. No error, no warning, and a result that looks like a legitimate answer. If your index is computed from data, the failure mode for an off-by-one is a plausible number rather than a raise.

The rows below came from one jitted function called five times. Only the index changed between calls, and the same executable served all five, because the index is a traced value like any other.

verbatim, jax/_src/lax/control_flow/conditionals.py:71-79 at jax-v0.4.38 (the switch docstring), then a run on this machine (jax 0.4.38 CPU)
  """Apply exactly one of the ``branches`` given by ``index``.

  If ``index`` is out of bounds, it is clamped to within bounds.

  Has the semantics of the following Python::

    def switch(index, branches, *operands):
      index = clamp(0, index, len(branches) - 1)
      return branches[index](*operands)

# >>> three = [lambda v: v + 1.0, lambda v: v + 2.0, lambda v: v + 3.0]
# >>> def pick(i, x): return jax.lax.switch(i, three, x)
# >>> print(jax.make_jaxpr(pick)(jnp.int32(0), jnp.ones(2)))
# { lambda ; a:i32[] b:f32[2]. let
#     c:i32[] = clamp 0 a 2
#     d:f32[2] = cond[
#       branches=(
#         { lambda ; e:f32[2]. let f:f32[2] = add e 1.0 in (f,) }
#         { lambda ; g:f32[2]. let h:f32[2] = add g 2.0 in (h,) }
#         { lambda ; i:f32[2]. let j:f32[2] = add i 3.0 in (j,) }
#       )
#     ] c b
#   in (d,) }
index passedbranch that ranresult
-50[2. 2.]
00[2. 2.]
11[3. 3.]
22[4. 4.]
72[4. 4.]
the same jitted switch called five times with three branches, measured on jax 0.4.38 CPU
§ 04

What the two branches have to agree about

The branches meet at one output type, and the check is exact rather than sympathetic. Different shapes raise. Different dtypes raise. Different pytree structures raise, and that one is reported separately, naming both treedefs instead of comparing avals.

One disagreement is allowed through, and it is the one people expect to break. A branch returning the Python float 1.0 against a branch returning an f32[] array is fine, because the weak scalar promotes to meet the array. Weakness is a property of the type here, not a special case in cond.

The habit worth building is to read the two avals in the message before rereading your code. DIFFERENT ShapedArray(float32[3]) vs. ShapedArray(float32[2]) tells you which axis moved, and it is usually a slice or a reduction inside one branch that you did not mirror in the other.

run it (verified, jax 0.4.38 CPU): three refusals and one acceptance; current jax has reworded the structure message to report differences per output path
import jax
import jax.numpy as jnp

x = jnp.ones(3)
pred = x.sum() > 0

def show(true_fn, false_fn):
    try:
        jax.lax.cond(pred, true_fn, false_fn, x)
        print("accepted")
    except TypeError as err:
        print(err)

show(lambda v: v, lambda v: v[:2])                    # shapes disagree
show(lambda v: v, lambda v: v.astype(jnp.float16))    # dtypes disagree
show(lambda v: (v, v), lambda v: v)                   # structures disagree
show(lambda v: v.sum(), lambda v: 1.0)                # a weak python float

# true_fun and false_fun output must have identical types, got
# DIFFERENT ShapedArray(float32[3]) vs. ShapedArray(float32[2]).
# true_fun and false_fun output must have identical types, got
# DIFFERENT ShapedArray(float32[3]) vs. ShapedArray(float16[3]).
# true_fun and false_fun output must have same type structure, got PyTreeDef((*, *)) and PyTreeDef(*).
# accepted
§ 05

A value one branch uses enters both

Close over a value in one branch only and watch what the jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → does with it. Both branch signatures grow, both get the value, and the one that has no use for it marks its binder with a trailing underscore. The operand list carries it twice, once for each branch.

The comment above the function that does this states the reason in three sentences: the staged jaxprsThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → are the branches of one primitive, so their input signatures have to match, and the fix is to make each accept all the constants and drop the ones it does not need.

This is where a closure over something large stops being free. Two branches closing over two different big arrays produce one primitive whose operand list holds both, whichever branch runs. Passing the value as an operand instead of closing over it changes nothing about that, but it does make the cost visible at the call site.

verbatim, jax/_src/lax/control_flow/common.py:78-83 at jax-v0.4.38, then a run on this machine (jax 0.4.38 CPU) where only one branch uses w
  # When staging the branches of a conditional into jaxprs, constants are
  # extracted from each branch and converted to jaxpr arguments. To use the
  # staged jaxprs as the branches to a conditional *primitive*, we need for
  # their (input) signatures to match. This function "joins" the staged jaxprs:
  # for each one, it makes another that accepts *all* constants, but only uses
  # those that it needs (dropping the rest).

# >>> def f(x, w):
# ...     return jax.lax.cond(x.sum() > 0, lambda v: v * w, lambda v: v - w, x)
# >>> print(jax.make_jaxpr(f)(jnp.ones(3), jnp.full((3,), 2.0)))
# { lambda ; a:f32[3] b:f32[3]. let
#     c:f32[] = reduce_sum[axes=(0,)] a
#     d:bool[] = gt c 0.0
#     e:i32[] = convert_element_type[new_dtype=int32 weak_type=False] d
#     f:f32[3] = cond[
#       branches=(
#         { lambda ; g_:f32[3] h:f32[3] i:f32[3]. let
#             j:f32[3] = convert_element_type[new_dtype=float32 weak_type=False] h
#             k:f32[3] = sub i j
#           in (k,) }
#         { lambda ; l:f32[3] m_:f32[3] n:f32[3]. let
#             o:f32[3] = convert_element_type[new_dtype=float32 weak_type=False] l
#             p:f32[3] = mul n o
#           in (p,) }
#       )
#     ] e b b a
#   in (f,) }
§ 06

The branch nobody took is free until you batch it

Give one branch five chained 512 by 512 matmuls and the other an add, then time the same compiled function under both predicates. The heavy predicate costs milliseconds and the light one costs almost nothing, so on this backend the executable really is skipping the branch it did not select.

Batch that same function and the timing stops depending on the predicate. Two lanes with both predicates false still pay for the matmuls, because the batched form has to produce both results before it can select between them per lane. Chapter 6 states that cost; the stopwatch is here so you can see how large it gets when one branch is expensive.

Which gives a rule for where to put a cond in a batched program. A branch that guards a cheap correction costs little either way. A branch that guards the expensive half of your model is worth hoisting out of the vmap, so the predicate applies to the whole batch instead of per element.

run it (verified, jax 0.4.38 CPU): best of 50 calls each, on the timings in the table below
import time
import jax
import jax.numpy as jnp

A = jnp.ones((512, 512))
heavy = lambda v: (A @ A @ A @ A @ A).sum() + v
light = lambda v: v + 1.0
one = jax.jit(lambda p, x: jax.lax.cond(p, heavy, light, x))
many = jax.jit(jax.vmap(lambda p, x: jax.lax.cond(p, heavy, light, x)))

def best(fn, *a):
    fn(*a).block_until_ready()
    ts = []
    for _ in range(50):
        t0 = time.perf_counter()
        fn(*a).block_until_ready()
        ts.append(time.perf_counter() - t0)
    return 1e3 * min(ts)

x, xs = jnp.float32(1.0), jnp.ones(2)
print(f"cond, predicate True   {best(one, jnp.bool_(True), x):6.2f} ms")
print(f"cond, predicate False  {best(one, jnp.bool_(False), x):6.2f} ms")
print(f"vmapped, both False    {best(many, jnp.array([False, False]), xs):6.2f} ms")
print(f"vmapped, both True     {best(many, jnp.array([True, True]), xs):6.2f} ms")
what ranbest of 50 calls
cond, predicate True3.42 ms
cond, predicate False0.03 ms
vmap of the same cond, two lanes, both predicates False5.26 ms
vmap of the same cond, two lanes, both predicates True8.48 ms
measured on this machine, jax 0.4.38 CPU, best of 50 calls each; across three runs of the same script the first row stayed near 3.5 ms, the second under 0.1 ms, and the two batched rows moved between 5.3 and 8.5 ms
§ 07

The batched branch still blocks the gradient

Guard a 1 / x with jnp.where and differentiate it at zero, and back comes nan. The masked-out value was still computed, its derivative was still taken, and multiplying an infinite derivative by a zero cotangent produces the indeterminate form. This is the classic reason people distrust masking.

Write the same guard as a cond, batch it, differentiate it, and the answer is 0. That is not because the batched form skipped anything. It computed both branches, the same way the timings above showed.

The jaxprThe traced program: one equation per primitive in single-assignment form, every shape and dtype stated.taught in /l/jaxpr → says where the protection comes from. Before each branch runs, its operand goes through a select_n against stop_gradient of itself: the lanes that branch owns receive the real value, and every other lane receives a gradient-blocked copy. So the 1 / x still evaluates at zero, and no cotangent from it can reach the input.

Batching a cond costs you both branches. It does not cost you the gradient.
run it (verified, jax 0.4.38 CPU): the same guard two ways, then the batched jaxpr that explains the difference
import jax
import jax.numpy as jnp

masked = lambda x: jnp.where(x > 0, 1.0 / x, 0.0)
branched = lambda x: jax.lax.cond(x > 0, lambda v: 1.0 / v, lambda v: 0.0 * v, x)

xs = jnp.array([0.0, 2.0])
print(jax.vmap(masked)(xs), jax.vmap(branched)(xs))
print(jax.vmap(jax.grad(masked))(xs))
print(jax.vmap(jax.grad(branched))(xs))
print(jax.make_jaxpr(jax.vmap(branched))(xs))

# [0.  0.5] [0.  0.5]
# [  nan -0.25]
# [ 0.   -0.25]
# { lambda ; a:f32[2]. let
#     b:bool[2] = gt a 0.0
#     c:i32[2] = convert_element_type[new_dtype=int32 weak_type=False] b
#     d:bool[2] = eq c 0
#     e:f32[2] = stop_gradient a
#     f:f32[2] = select_n d e a
#     g:f32[2] = mul 0.0 f
#     h:bool[2] = eq c 1
#     i:f32[2] = stop_gradient a
#     j:f32[2] = select_n h i a
#     k:f32[2] = div 1.0 j
#     l:f32[2] = select_n c g k
#   in (l,) }
before you move on

Check yourself

01 A cond jaxpr shows div in the first branch slot and mul in the second, but you wrote mul as the true branch. What happened?

cond binds branches as (false_jaxpr, true_jaxpr) and passes an int32 index, so slot 0 is the false branch and slot 1 the true one. Your bool predicate was converted to i32 by a convert_element_type equation before the primitive saw it.

02 switch got an index of 7 against three branches. What runs, and what tells you?

Branch 2 runs, and nothing tells you. switch emits a clamp equation ahead of the cond, so out-of-range indices are pinned to the ends silently: -5 runs branch 0 and 7 runs branch 2, both returning a plausible-looking answer.

03 grad through a jnp.where guard returned nan at zero where the same guard written as lax.cond returned 0, even under vmap. What in the batched jaxpr accounts for that?

Each branch gets its operand through a select_n against stop_gradient of that operand: the lanes the branch owns see the real value, the others see a gradient-blocked copy. Both branches still compute, but no cotangent flows out of the branch a lane did not select.

assigned

Readings