the path · 0/15
start the path

the path · chapter 02 of 15 · part i, the descent

jaxpr

The traced program: every primitive op written down, every hidden decision made visible.

mastery work · this chapter0/9
  1. go →auto
  2. go →auto
  3. go →auto
  4. go →auto
  5. go →auto
  6. go →auto
  7. go →auto
  8. go →auto
manual items are your word; auto items complete from your streaks, labs, and can-you ticks · stored in your browser only
the specimen · at this floor L1 · the jaxpr

Tracing turns that block into this, with one call to jax.nn.softmax already spread across several primitives.

    j:f32[] = sqrt 32.0
    k:f32[16,16] = div i j
    l:f32[16] = reduce_max[axes=(1,)] k
    m:f32[16] = max -inf l
    n:f32[16,1] = broadcast_in_dim[
      broadcast_dimensions=(0,)
      shape=(16, 1)
      sharding=None
    ] m
    o:f32[16,1] = stop_gradient n
read the whole artifact, and the levels above and below it →
bench/specimen/artifacts/jaxpr.txt · lines 19 to 28 of 42 · python 3.12.0, jax 0.4.38, jaxlib 0.4.38
the layer

Reading a jaxpr

A jaxpr is the recording tracing produced: a flat list of equations in single-assignment form. Each line binds one fresh variable to one primitive applied to earlier variables, with every shape and dtype stated. There is no control flow left, no module structure, no Python. It reads like assembly for tensor math, and it is the most honest description of your program you can get without leaving Python: print it with jax.make_jaxpr(fn)(*args).

Here is our attention function as a real jaxpr. Things Python never showed you are now explicit: k.T became a transpose equation, the matmuls became dot_general with their contraction dimensions spelled out, and the broadcast that aligned the row max for subtraction became a broadcast_in_dim with stated shapes.

the real jaxpr of naive attention (generated, not retyped): first half
{ lambda ; a:bf16[128,64] b:bf16[128,64] c:bf16[128,64]. let
    d:bf16[64,128] = transpose[permutation=(1, 0)] b
    e:bf16[128,128] = dot_general[
      dimension_numbers=(([1], [0]), ([], []))
      preferred_element_type=bfloat16
    ] a d
    f:bf16[128] = reduce_max[axes=(1,)] e
    g:bf16[128,1] = broadcast_in_dim[
      broadcast_dimensions=(0,)
      shape=(128, 1)
      sharding=None
    ] f
    h:bf16[128,128] = sub e g
    i:bf16[128,128] = exp h
the layer

The detail worth staring at

Look at the convert_element_type pair around the reduce_sum in the second half: JAX upcasts the bf16 summation to f32 and converts back. Nobody wrote that. It is a numerics decision the framework made for you, and the jaxpr is the first place it becomes visible. Learning to read IRs is learning to notice decisions like this one, because one layer further down, you will be the one making them.

second half: the f32 upcast around the sum, then the normalize and output matmul
    j:f32[128,128] = convert_element_type[new_dtype=float32 weak_type=False] i
    k:f32[128] = reduce_sum[axes=(1,)] j
    l:f32[128,1] = broadcast_in_dim[
      broadcast_dimensions=(0,)
      shape=(128, 1)
      sharding=None
    ] k
    m:bf16[128,1] = convert_element_type[new_dtype=bfloat16 weak_type=False] l
    n:bf16[128,128] = div i m
    o:bf16[128,64] = dot_general[
      dimension_numbers=(([1], [0]), ([], []))
      preferred_element_type=bfloat16
    ] n c
  in (o,) }
go deeper, in order

Lessons

  1. 01The anatomy of a jaxprA ClosedJaxpr is a jaxpr plus the values it closed over, and the grammar underneath is small enough to hold whole. ·
  2. 02Transforms are jaxpr rewritesgrad, vmap, scan, remat, shard_map: one small dense function, traced once, then rewritten five ways in front of you. ·
  3. 03Reading nested jaxprsSome equations carry whole programs in their params. The indented block underneath is a jaxpr like any other, and kernels nest exactly this way. ·
  4. 04Params, and the drills that fix themParams are the configuration a primitive needs before it can run. Fluency is repetition, and the gym exists for exactly that. ·
  5. 05One name, one definitionPython let you assign to x three times. The jaxpr that came back has four names, and the rule behind that is the one property every IR below this one also keeps. ·
later on the path Stage 2 drills jaxpr reading until it is boring (chapter 12). Keep descending; the path arrives there in order.