<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.chuyishang.com/feed.xml" rel="self" type="application/atom+xml"/><link href="https://www.chuyishang.com/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-06-05T14:22:42+00:00</updated><id>https://www.chuyishang.com/feed.xml</id><title type="html">Chuyi Shang</title><subtitle>A personal website for Chuyi Shang. </subtitle><entry><title type="html">JAX-LM Assignment</title><link href="https://www.chuyishang.com/blog/2026/assignment/" rel="alternate" type="text/html" title="JAX-LM Assignment"/><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://www.chuyishang.com/blog/2026/assignment</id><content type="html" xml:base="https://www.chuyishang.com/blog/2026/assignment/"><![CDATA[<figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/jax-lm_assignment-480.webp 480w,/assets/img/jax-lm/jax-lm_assignment-800.webp 800w,/assets/img/jax-lm/jax-lm_assignment-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/jax-lm_assignment.jpg" class="img-fluid rounded z-depth-1" width="416" height="auto" alt="Pasted image 20260318102536.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>This assignment serves as a companion for the <a href="https://www.chuyishang.com/blog/2026/jax-lm/">JAX-LM</a> blog. For the ambitious readers who want to take a stab at implementing our language model in JAX end-to-end, this assignment provides step-by-step instructions and a test suite to guide your implementation.</p> <p>This assignment is built on top of <a href="https://github.com/stanford-cs336/assignment1-basics/tree/main">Assignment 1 (basics): Building a Transformer LM</a> of <a href="https://cs336.stanford.edu/">Stanford’s CS336: Language Modeling From Scratch</a> course.</p> <p>The main differences are:</p> <ol> <li>We convert the test cases from PyTorch to JAX</li> <li>We add tests and instructions for distributed training in JAX</li> <li>Some sections are organized a little differently from the original assignment</li> </ol> <p>However, the modules can mostly be completed in the same order.</p> <details><summary>Device Requirements</summary> <p>It is strongly recommended to complete this assignment on a system with accelerators (GPU/TPU) to fully observe the speedups from JAX at scale.</p> <p>That being said, this assignment is compatible with Linux, Windows, and MacOS systems, for sections up to and including <strong>Section 3: Training Loop</strong> can be completed and run locally on a standard laptop. This will still be helpful for just getting comfortable coding in JAX. However, the distributed implementations assume access to GPUs/TPUs.</p> <p>If you don’t have acess to accelerators, you can simulate a device mesh using CPUs instead by setting <code class="language-plaintext highlighter-rouge">os.environ["XLA_FLAGS"] = "--xla_force_host_platform_device_count=8"</code>, where you can replace <code class="language-plaintext highlighter-rouge">8</code> with your number of desired devices. This must be done before JAX is imported. However, we have not fully tested this configuration.</p> </details> <h2 id="getting-started">Getting Started</h2> <p>The blank starter code is provided on the <code class="language-plaintext highlighter-rouge">assignment</code> branch. To clone only the starter code:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--single-branch</span> <span class="nt">--branch</span> assignment https://github.com/chuyishang/jax-lm.git
</code></pre></div></div> <p><strong>Repository Structure</strong> The repository is a fork of <code class="language-plaintext highlighter-rouge">stanford-cs336/assignment1-basics</code>, and contains:</p> <ul> <li><code class="language-plaintext highlighter-rouge">jax_tests/</code>: the core test suite for JAX-based implementations. <ul> <li>Suggested workflow: go through the sections below, passing all non-distributed tests (everything other than <code class="language-plaintext highlighter-rouge">test_sharding</code>). Then go back through to implement sharding.</li> <li><code class="language-plaintext highlighter-rouge">adapters.py</code>: This file starts as function stubs. You will edit it as you implement more components to get the relevant tests to run.</li> </ul> </li> <li><code class="language-plaintext highlighter-rouge">jax_impl/</code>: the directory where your code will live. Feel free to create files and organize your code as you see fit.</li> <li><code class="language-plaintext highlighter-rouge">data/tokenizer.py</code> and <code class="language-plaintext highlighter-rouge">data/train_bpe.py</code>: BPE implementation and training script are provided by default because that code does not differ from the original PyTorch assignment.</li> <li><code class="language-plaintext highlighter-rouge">uv.lock</code> and <code class="language-plaintext highlighter-rouge">pyproject.toml</code> which provide dependencies including JAX</li> </ul> <p>By default, <code class="language-plaintext highlighter-rouge">uv run</code> and <code class="language-plaintext highlighter-rouge">uv sync</code> will assume a Linux + CUDA setup and be able to run out of the box. If you are using a CPU-only setup, or on a macOS device, you should first run:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv <span class="nb">sync</span> <span class="nt">--no-group</span> cuda
</code></pre></div></div> <p><strong>Running Tests</strong> Tests can be run using <code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_file_name.py</code> or <code class="language-plaintext highlighter-rouge">uv run pytest -k test_file_name</code>. As a base, you should be able to run</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run pytest jax_tests/test_train_bpe.py jax_tests/test_tokenizer.py
</code></pre></div></div> <p>and pass these tests right away since we’ve provided byte-pair tokenization implementations in <code class="language-plaintext highlighter-rouge">data/train_bpe.py</code> and <code class="language-plaintext highlighter-rouge">data/tokenizer.py</code>.</p> <hr/> <h2 id="1-basic-building-blocks-nnx-modules">1. Basic Building Blocks: NNX Modules</h2> <blockquote class="block-tip"> <p>If you’re following along on the CS336 spec, this section is the JAX-equivalent to <strong>Section 3: Transformer Language Model Architecture</strong>.</p> </blockquote> <p>In this section, we will build our first <code class="language-plaintext highlighter-rouge">nnx.Module</code> classes that will become the building blocks for our Transformer LM. Specifically, we will implement</p> <ul> <li><code class="language-plaintext highlighter-rouge">Linear</code></li> <li><code class="language-plaintext highlighter-rouge">Embedding</code></li> <li><code class="language-plaintext highlighter-rouge">RMSNorm</code></li> <li><code class="language-plaintext highlighter-rouge">SwiGLU</code></li> <li><code class="language-plaintext highlighter-rouge">RoPE</code>: RoPE does not use random weight initialization</li> </ul> <p>In your implementation, these modules can be defined anywhere under <code class="language-plaintext highlighter-rouge">jax_impl/</code> as long as <code class="language-plaintext highlighter-rouge">jax_tests/adapters.py</code> is updated to call them.</p> <h3 id="random-number-generation">Random Number Generation</h3> <p>NNX modules that require random weight initialization should be initialized with an additional <code class="language-plaintext highlighter-rouge">rngs</code> parameter. This is because in NNX, random number generation uses explicit states passed in as <code class="language-plaintext highlighter-rouge">nnx.Rngs</code> objects.</p> <p>Thus, your class signature should look something like:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">flax</span> <span class="kn">import</span> <span class="n">nnx</span>
<span class="kn">import</span> <span class="n">jax.numpy</span> <span class="k">as</span> <span class="n">jnp</span>

<span class="k">class</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">out_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span><span class="o">=</span><span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">):</span>
		<span class="k">pass</span>
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
		<span class="k">pass</span>
</code></pre></div></div> <p>Another note is that the forward pass of an <code class="language-plaintext highlighter-rouge">nnx.Module</code> is implemented directly within the <code class="language-plaintext highlighter-rouge">__call__</code> method, instead of a separate <code class="language-plaintext highlighter-rouge">forward()</code> method like in PyTorch.</p> <h3 id="device-management">Device Management</h3> <p>Recall that we also don’t need to pass in devices explicitly. Later on, we will modify this section to support sharding annotations, but we don’t need to worry about that now.</p> <h3 id="11-linear">1.1 Linear</h3> <p>Let’s get started with the first module: a linear layer. Again, the class signature for your module should look something like:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">out_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span><span class="o">=</span><span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">):</span>
		<span class="k">pass</span>
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
		<span class="k">pass</span>
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">Linear</code> module performs the following transformation: \(y = xW\) We first need need to initialize our weight matrix $W \in \mathbb R^{d_{\text{in}} \times d_{\text{out}}}$. Following the original assignment, we will initialize our weights to a truncated normal distribution $W \sim \mathcal N(\mu=0, \sigma^2 = \frac{2}{d_\text{in} + d_\text{out}})$ truncated at $[-3\sigma, 3\sigma]$. This is made easy thanks to <a href="https://docs.jax.dev/en/latest/_autosummary/jax.nn.initializers.truncated_normal.html"><code class="language-plaintext highlighter-rouge">nnx.initializers.truncated_normal</code></a>. You can also play around with more sophisticated <a href="https://docs.jax.dev/en/latest/jax.nn.initializers.html">initializations</a> offered by the library, although it may not pass the autograder tests.</p> <p>Once the weight data is initialized, it should be wrapped in an <code class="language-plaintext highlighter-rouge">nnx.Param</code> object and stored as a class attribute.</p> <p>When the function is called, we just apply the linear transformation.</p> <blockquote class="block-tip"> <h5 id="todos">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">Linear</code> module using only JAX arrays and methods. <ul> <li>Note: don’t forget to call the superclass constructor.</li> </ul> </li> <li>Update <code class="language-plaintext highlighter-rouge">run_linear</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method. <ul> <li>Note: to pass the weights to your linear layer, call <a href="https://flax.readthedocs.io/en/latest/api_reference/flax.nnx/graph.html#flax.nnx.update"><code class="language-plaintext highlighter-rouge">nnx.update</code></a> with the parameters formatted in a <a href="https://flax.readthedocs.io/en/stable/api_reference/flax.nnx/state.html#flax.nnx.State"><code class="language-plaintext highlighter-rouge">State</code></a> dictionary. We do not include a bias term in the weights, and your implementation shouldn’t either to work with the tests and adapter as is.</li> </ul> </li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_linear</code> to check against the test case.</li> </ol> </blockquote> <blockquote class="block-tip"> <p>If you’re stuck, we provide an in-depth walkthrough for implementing a Linear layer in <a href="/blog/2026/jax-lm/#implementing-our-model">Implementing Our Model</a>. We only do this for select sections. To just see a completed implementation of all sections, refer to the reference implementation!</p> </blockquote> <h3 id="12-embedding">1.2 Embedding</h3> <p>Next up is the <code class="language-plaintext highlighter-rouge">Embedding</code> module, which takes as input a vector of integer token IDs and converts each of them to their vector representations. Its interface is very similar to <code class="language-plaintext highlighter-rouge">Linear</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Embedding</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">n_embeddings</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">embedding_dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span><span class="o">=</span><span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">):</span>
		<span class="k">pass</span>
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">token_ids</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
		<span class="k">pass</span>
</code></pre></div></div> <p>To make things a bit clearer:</p> <ul> <li><code class="language-plaintext highlighter-rouge">n_embeddings</code> = vocabulary size</li> <li>We maintain a weight matrix $W \in \mathbb R^{\text{n_embeddings} \times \text{embedding_dim}}$ where the $i$-th row gives the embedding for token ID $i$. <ul> <li>This can be initialized to $W \sim \mathcal N(0, 1)$ truncated at $[-3, 3]$</li> </ul> </li> <li>The output will itself be a matrix of size $(\text{token_ids.size}, \text{embedding_dim})$</li> </ul> <blockquote class="block-tip"> <h5 id="todos-1">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">Embedding</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_embedding</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_embedding</code> to check against the test case.</li> </ol> </blockquote> <h3 id="13-rmsnorm">1.3 RMSNorm</h3> <p><code class="language-plaintext highlighter-rouge">RMSNorm</code> is a basic layer normalization module. For convenience, the formula given a $d$-dimensional vector of activations as input:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RMSNorm</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">eps</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e-5</span><span class="p">,</span> <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span><span class="o">=</span><span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">):</span>
		<span class="k">pass</span>
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
		<span class="k">pass</span>
</code></pre></div></div> <p>and the formula</p> \[\text{RMSNorm}(x_{i}) = \frac{x_{i}}{\text{RMS}(x)} g_{i}\] <p>where</p> \[\text{RMS}(x) = \sqrt{\frac{1}{\text{d\_model}} \sum_{i=1}^{\text{d\_model}} a_{i}^2 + \epsilon}\] <p>Here, our <em>gain</em> vector $g \in \mathbb R^{1 \times \text{d_model}}$ consists of our learnable parameters and $\epsilon$ is a hyperparameter.</p> <blockquote class="block-tip"> <h5 id="todos-2">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">RMSNorm</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_rmsnorm</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_rmsnorm</code> to check against the test case.</li> </ol> </blockquote> <h3 id="14-feed-forward-network">1.4 Feed-Forward Network</h3> <p>Now we get into implementing the actual feed-forward network that will be used as part our Transformer LM. The assignment calls for a <a href="https://arxiv.org/abs/2002.05202">SwiGLU</a> (Swish-Gated Linear Unit) architecture, which consists of a Gated Linear Unit with a Swish (or SiLU) activation. Concretely:</p> \[\text{FFN}(x) = \text{SwiGLU}(x, W_1, W_2, W_3) = W_2(\text{SiLU}(W_1 x) \odot W_3 x)\] <p>The dimensions here are:</p> <ul> <li>$x \in \mathbb R^{d_\text{model}}$</li> <li>$W_1, W_3 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}$</li> <li>$W_2 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}}$ $d_{\text{ff}}$ is the hidden size of the feed-forward block, while $d_\text{model}$ is the hidden size of the token representation. $d_{\text{ff}}$ should be set to approximately $\frac{8}{3} \cdot d_{\text{model}}$.</li> </ul> <p>The SiLU activation function is \(\text{SiLU}(x) = x \cdot \sigma(x) = \frac{x}{1 + e^{-x}}\)</p> <blockquote class="block-tip"> <h5 id="todos-3">TODOs</h5> <ol> <li>Implement the SiLU activation function. <ul> <li>Update <code class="language-plaintext highlighter-rouge">adapters.run_silu</code> and call <code class="language-plaintext highlighter-rouge">uv run pytest -k test_silu</code> to test your implementation.</li> </ul> </li> <li>Implement the <code class="language-plaintext highlighter-rouge">SwiGLU</code> module using only JAX arrays and methods. <ul> <li>Note: use your own <code class="language-plaintext highlighter-rouge">Linear</code> class!</li> </ul> </li> <li>Update <code class="language-plaintext highlighter-rouge">run_swiglu</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_swiglu</code> to check against the test case.</li> </ol> </blockquote> <h3 id="15-relative-positional-embeddings">1.5 Relative Positional Embeddings</h3> <p>The last module for this section will implement <a href="https://arxiv.org/abs/2104.09864">Rotary Positional Embeddings</a> (RoPE) to inject information about each token’s relative position in the sequence. Here is the class interface:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RotaryPositionalEmbedding</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">d_k</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">theta</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
		<span class="k">pass</span>
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span> <span class="n">token_positions</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
		<span class="k">pass</span>
</code></pre></div></div> <p>Notably, this is the first module we’ve implemented that doesn’t include <code class="language-plaintext highlighter-rouge">rngs</code> in the class signature. This is because this layer has no learnable parameters and thus does not need any randomized initialization.</p> <p>Because of this, it’s also possible to optimize RoPE by caching the sine and cosine values using <a href="https://flax.readthedocs.io/en/latest/api_reference/flax.nnx/variables.html#flax.nnx.Cache"><code class="language-plaintext highlighter-rouge">nnx.Cache</code></a> at initialization.</p> <p><strong>TODO: put the definition of RoPE</strong></p> <blockquote class="block-tip"> <h5 id="todos-4">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">RotaryPositionalEmbedding</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_rope</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_rope</code> to check against the test case.</li> </ol> </blockquote> <hr/> <h2 id="2-attention--full-transformer-block">2. Attention &amp; Full Transformer Block</h2> <p>The modules we have so far together form almost the entire Transformer, with the exception of the actual self-attention part. That’s what we’ll implement now.</p> <p>In this section, we will implement</p> <ul> <li>Scaled Dot-Product Attention</li> <li>Causal Multi-Head Self-Attention</li> <li>Transformer Block</li> <li>Transformer Language Model <h3 id="21-scaled-dot-product-attention">2.1 Scaled Dot-Product Attention</h3> <p>The SDPA operation takes as input the query, key, and value matrices to perform the attention operation, defined as}”” \(\text{Attention}(Q, K, V) = \text{softmax}\bigg(\frac{Q K^\top}{\sqrt{ d_{k} }} \bigg) \cdot V\) where $Q \in \mathbb R ^{n \times d_{k}}$, $K \in \mathbb R^{m \times d_{k}}$, and $V \in \mathbb R^{m \times d_{v}}$. Here, $m$ is the sequence length and $n$ is the batch size.</p> </li> </ul> <p>For convenience, the numerically-stable softmax activation function is: \(\text{softmax}(x_{i}) = \frac{e^{x_{i} - \max(x)}}{\sum_{j} e^{x_{j} - \max(x)}}\)</p> <blockquote class="block-tip"> <h5 id="todos-5">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">softmax</code> activation function. <ul> <li>Update <code class="language-plaintext highlighter-rouge">adapters.run_softmax</code> and call <code class="language-plaintext highlighter-rouge">uv run pytest -k test_softmax_matches_pytorch</code> to test your implementation.</li> </ul> </li> <li>Implement the <code class="language-plaintext highlighter-rouge">scaled_dot_product_attention</code> using only JAX arrays and methods. <ul> <li>Note: include an optional <code class="language-plaintext highlighter-rouge">mask</code> parameter which accepts an array of shape <code class="language-plaintext highlighter-rouge">Float[Array, " ... queries keys"]</code></li> </ul> </li> <li>Update <code class="language-plaintext highlighter-rouge">run_scaled_dot_product_attention</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_4d_scaled_dot_product_attention</code> to check against the test case.</li> </ol> <p><em>Hint: if you’ve done the original PyTorch assignment, these methods will look nearly identical.</em></p> </blockquote> <h3 id="22-causal-multi-head-self-attention">2.2 Causal Multi-Head Self-Attention</h3> <p>We provide an in-depth walkthrough for implementing Causal Multi-Head Self-Attention in <a href="/blog/2026/jax-lm/#implementing-our-model">Implementing Our Model</a>.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MultiHeadSelfAttention</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
		<span class="n">self</span><span class="p">,</span>
		<span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
		<span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">rope_theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e4</span><span class="p">,</span>
		<span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">,</span>
	<span class="p">):</span>
		<span class="k">pass</span>
	
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">Array</span><span class="p">,</span> <span class="n">use_rope</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">):</span>
		<span class="k">pass</span>
</code></pre></div></div> <blockquote class="block-tip"> <h5 id="todos-6">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">MultiHeadSelfAttention</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_multihead_self_attention</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_multihead_self_attention</code> to check against the test cases.</li> </ol> </blockquote> <h3 id="23-transformer-block">2.3 Transformer Block</h3> <p>The last piece of the puzzle is the Transformer block. Conveniently, we’ve already implemented all the submodules we’ll need. Refer to Figure 2 of the below image for the architecture, taken from the original assignment:</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/transformer-480.webp 480w,/assets/img/jax-lm/transformer-800.webp 800w,/assets/img/jax-lm/transformer-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/transformer.jpg" class="img-fluid rounded z-depth-1" width="100%" height="auto" alt="transformer.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>Here’s the <code class="language-plaintext highlighter-rouge">TransformerBlock</code> interface:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TransformerBlock</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
		<span class="n">self</span><span class="p">,</span>
		<span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
		<span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">d_ff</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">theta</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
	<span class="p">):</span>
		<span class="k">pass</span>
		
	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">):</span>
		<span class="k">pass</span>
</code></pre></div></div> <blockquote class="block-tip"> <h5 id="todos-7">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">TransformerBlock</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_transformer_block</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_transformer_block</code> to check against the test case.</li> </ol> </blockquote> <h3 id="24-transformer-lm">2.4 Transformer LM</h3> <p>Now its finally time to put the pieces together to create the final Transformer language model. The structure is pasted in Figure 1 in the previous subsection. Similar to the Transformer block, we’ve already implemented all the layers we’ll be needing.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TransformerLM</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
		<span class="n">self</span><span class="p">,</span>
		<span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
		<span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">d_ff</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">theta</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
		<span class="n">vocab_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">context_length</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
		<span class="n">num_layers</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
	<span class="p">):</span>
		<span class="k">pass</span>

	<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">):</span>
		<span class="k">pass</span>
</code></pre></div></div> <blockquote class="block-tip"> <h5 id="todos-8">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">TransformerLM</code> module using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_transformer_lm</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_transformer_lm</code> to check against the test cases.</li> </ol> </blockquote> <p>And there you have it: your own Transformer language model from scratch, completely in JAX. We’ll upgrade this code in Section 4 to add sharding functionality, but this should’ve gotten you comfortable coding in JAX and NNX.</p> <hr/> <h2 id="3-training-loop">3. Training Loop</h2> <p>Now that we have all the pieces of the model, it’s time to build out the training infrastructure. This will include:</p> <ul> <li>Cross entropy loss function</li> <li>Optax gradient transformations: <ul> <li>AdamW</li> <li>Gradient clipping</li> <li>LR schedule</li> </ul> </li> <li>Train loop with checkpointing</li> </ul> <h3 id="31-cross-entropy-loss">3.1 Cross Entropy Loss</h3> <p>Cross entropy loss is defined as \(\ell_{i}(o, x) = -\log \text{softmax}(o_{i})_{x_{i+1}}\) where the Transformer outputs logits $o \in \mathbb R^{m \times \text{vocab size}}$ for each sequence $x$ of sequence length $m$. Then, the total cross entropy loss for a batch of size $B$ would be \(\mathcal{L}(o, x) = -\frac{1}{B}\sum_{i=1}^{B} \log \big(\mathrm{softmax}(o_i)\big)_{x_i}\) For numerical stability, you should implement the <strong>log-sum-exp trick</strong> (a helpful blog post about it <a href="https://gregorygundersen.com/blog/2020/02/09/log-sum-exp/">here</a>).</p> <p><em>Note: the test cases can be used as a helpful reference for understanding the expected dimensions of the inputs.</em></p> <blockquote class="block-tip"> <h5 id="todos-9">TODOs</h5> <ol> <li>Implement the cross entropy loss function.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_cross_entropy</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_cross_entropy</code> to check against the test case.</li> </ol> <p><em>Hint: if you’ve done the original PyTorch assignment, these methods will look nearly identical.</em></p> </blockquote> <h3 id="32-optimizer">3.2 Optimizer</h3> <p>In JAX, the optimizer is treated as a series of Optax transformations. We can first construct our pipeline by adding gradient clipping followed by AdamW. Note that we pass in our learning rate schedule <code class="language-plaintext highlighter-rouge">lr_schedule</code> as an argument to AdamW during initialization. Then, we can create an <code class="language-plaintext highlighter-rouge">nnx.Optimizer</code> using our <code class="language-plaintext highlighter-rouge">model</code>, the pipeline we just created, and the <code class="language-plaintext highlighter-rouge">wrt</code> argument that specifies what to optimize.</p> <p>This pipeline could look something like</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_optimizer_transform</span><span class="p">(</span>
    <span class="n">optimizer_config</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span>
    <span class="n">gradient_clip</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
    <span class="n">lr_schedule</span><span class="p">:</span> <span class="n">optax</span><span class="p">.</span><span class="n">Schedule</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">optax</span><span class="p">.</span><span class="n">GradientTransformation</span><span class="p">:</span>
    <span class="bp">...</span>
</code></pre></div></div> <blockquote class="block-tip"> <h5 id="todos-10">TODOs</h5> <ol> <li>Implement the <code class="language-plaintext highlighter-rouge">get_lr_cosine_schedule</code> method for cosine annealing LR scheduling using only JAX arrays and methods.</li> <li>Update <code class="language-plaintext highlighter-rouge">run_get_lr_cosine_schedule</code> in <code class="language-plaintext highlighter-rouge">adapters.py</code> to call your method.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest -k test_get_lr_cosine_schedule</code> to check against the test case.</li> </ol> </blockquote> <h3 id="34-training-loop">3.4 Training Loop</h3> <blockquote class="block-tip"> <p>An in-depth walkthrough for is provided in <a href="/blog/2026/jax-lm/#implementing-the-training-loop">Implementing the Training Loop</a>.</p> </blockquote> <hr/> <h2 id="4-sharding">4. Sharding</h2> <p>Now we’ll extend the model and data pipeline to support distributed execution with JAX sharding. The test suite here checks <strong>physical sharding</strong> (not just logical intent) of:</p> <ul> <li>input batches,</li> <li>model parameters, and</li> <li>optimizer states.</li> </ul> <p>We recommend implementing this section after Sections 1-3 are passing.</p> <p>For the specific sharding specs we want to do for each mode, you can refer to the <a href="https://www.chuyishang.com/blog/2026/jax-lm/#implementing-distributed-training">Implementing Sharding</a> section of the original blog post.</p> <h3 id="41-running-sharding-tests">4.1 Running Sharding Tests</h3> <p>The sharding suite is in <code class="language-plaintext highlighter-rouge">jax_tests/test_sharding.py</code>. It assumes we have at least 8 devices. If we don’t have that, it simulates 8 devices using CPU with <code class="language-plaintext highlighter-rouge">--xla_force_host_platform_device_count=8</code>.</p> <h3 id="42-mesh--mode-validation">4.2 Mesh + Mode Validation</h3> <p>Sharding is organized around a 2D mesh with axis names <code class="language-plaintext highlighter-rouge">data</code> and <code class="language-plaintext highlighter-rouge">tensor</code>.</p> <p>The tests cover four sharding modes: <code class="language-plaintext highlighter-rouge">dp</code>, <code class="language-plaintext highlighter-rouge">fsdp</code>, <code class="language-plaintext highlighter-rouge">tp</code>, and <code class="language-plaintext highlighter-rouge">fsdp_tp</code></p> <p>The tests also check for valid device meshes for each mode. Invalid mode/mesh combinations should raise <code class="language-plaintext highlighter-rouge">ValueError</code>, and unknown mode names should also raise <code class="language-plaintext highlighter-rouge">ValueError</code>.</p> <blockquote class="block-tip"> <h5 id="todos-11">TODOs</h5> <ol> <li>Implement mesh construction (for example via <code class="language-plaintext highlighter-rouge">create_mesh(mesh_shape, mesh_axis_names)</code>), which is called by <code class="language-plaintext highlighter-rouge">adapters.get_mesh</code>.</li> <li>Implement <code class="language-plaintext highlighter-rouge">get_sharding_config_for_mode(mode)</code> and make it raise <code class="language-plaintext highlighter-rouge">ValueError</code> for unsupported modes.</li> <li>Implement <code class="language-plaintext highlighter-rouge">validate_mesh_for_mode(mesh, mode)</code> and enforce valid mode/mesh pairings.</li> <li>Run: <ul> <li><code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_invalid_mode_name_raises -v</code></li> <li><code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_invalid_mode_mesh_combinations_raise -v</code></li> </ul> </li> </ol> </blockquote> <h3 id="43-sharded-batch-pipeline">4.3 Sharded Batch Pipeline</h3> <p>The sharding tests construct a tiny dataset wrapper with a <code class="language-plaintext highlighter-rouge">.data</code> field and call your sharded batch path through adapters. Your implementation should return <code class="language-plaintext highlighter-rouge">(inputs, targets)</code> with shape <code class="language-plaintext highlighter-rouge">(batch_size, context_length)</code>, and both arrays should be sharded according to the mode-specific batch <code class="language-plaintext highlighter-rouge">PartitionSpec</code>.</p> <p>In particular, the tests call:</p> <ul> <li><code class="language-plaintext highlighter-rouge">adapters.get_expected_batch_sharding_spec(mode)</code></li> <li><code class="language-plaintext highlighter-rouge">adapters.get_sharded_batch(...)</code></li> </ul> <p><code class="language-plaintext highlighter-rouge">adapters.get_expected_batch_sharding_spec(mode)</code> already forwards to your mode config via:</p> <ul> <li><code class="language-plaintext highlighter-rouge">model.get_batch_sharding_for_mode(mode)</code></li> </ul> <p><code class="language-plaintext highlighter-rouge">adapters.get_sharded_batch(...)</code> is still a TODO adapter path and should route to your sharded training/data batch utility.</p> <blockquote class="block-tip"> <h5 id="todos-12">TODOs</h5> <ol> <li>Implement the batch sharding policy for each mode (e.g. via <code class="language-plaintext highlighter-rouge">get_batch_sharding_for_mode(mode)</code>).</li> <li>Implement sharded batch loading/sampling and ensure returned arrays are placed with the requested <code class="language-plaintext highlighter-rouge">PartitionSpec</code>.</li> <li>Implement <code class="language-plaintext highlighter-rouge">adapters.get_sharded_batch</code>.</li> <li>Run <code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_batch_sharding -v</code>.</li> </ol> </blockquote> <h3 id="44-sharded-model--optimizer-initialization">4.4 Sharded Model + Optimizer Initialization</h3> <p>Next, shard model parameters and optimizer state. The tests call your model/optimizer initialization path in both:</p> <ul> <li>sharded mode (<code class="language-plaintext highlighter-rouge">mesh</code> + sharding config), and</li> <li>no-sharding mode (<code class="language-plaintext highlighter-rouge">mesh=None</code>, <code class="language-plaintext highlighter-rouge">sharding_config=None</code>).</li> </ul> <p>No-sharding mode should still initialize correctly and keep arrays local (single addressable shard per array). The sharding tests also check that the optimizer state arrays are physically sharded.</p> <blockquote class="block-tip"> <h5 id="todos-13">TODOs</h5> <ol> <li>Implement <code class="language-plaintext highlighter-rouge">create_model_and_optimizer(rngs, model_config, optimizer_config, sharding_config, mesh)</code> so it supports both sharded and unsharded paths.</li> <li>Make sure optimizer states are initialized with sharding consistent with their corresponding parameters.</li> <li>Implement <code class="language-plaintext highlighter-rouge">adapters.get_sharded_model_and_optimizer</code>.</li> <li>Run: <ul> <li><code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_model_sharding -v</code></li> <li><code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_optimizer_sharding -v</code></li> <li><code class="language-plaintext highlighter-rouge">uv run pytest jax_tests/test_sharding.py -k test_no_sharding_still_supported -v</code></li> </ul> </li> </ol> </blockquote> <p>Once this section passes, your implementation supports both local and distributed initialization paths and validates that data/model/optimizer arrays are actually distributed across devices.</p> <hr/> <p>Congratulations on finishing this assignment!</p>]]></content><author><name>Chuyi Shang</name></author><category term="distill"/><category term="formatting"/><summary type="html"><![CDATA[The assignment companion for JAX-LM]]></summary></entry><entry><title type="html">JAX-LM: Language Modelling and Distributed Training in JAX</title><link href="https://www.chuyishang.com/blog/2026/jax-lm/" rel="alternate" type="text/html" title="JAX-LM: Language Modelling and Distributed Training in JAX"/><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://www.chuyishang.com/blog/2026/jax-lm</id><content type="html" xml:base="https://www.chuyishang.com/blog/2026/jax-lm/"><![CDATA[<figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/jaxlm_cover-480.webp 480w,/assets/img/jax-lm/jaxlm_cover-800.webp 800w,/assets/img/jax-lm/jaxlm_cover-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/jaxlm_cover.png" class="img-fluid rounded z-depth-1" width="416" height="auto" alt="Pasted image 20260318102536.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <h2 id="introduction">Introduction</h2> <p>In this blog, we’ll implement a language model from scratch in JAX, then scale it up with distributed training across multiple GPUs/TPUs.</p> <p>We draw heavily upon 2 great resources. The first is Assignment 1 from Stanford’s <a href="https://cs336.stanford.edu/">CS336 course</a>, which goes over how to implement language models from scratch in PyTorch. The second resource is the <a href="https://jax-ml.github.io/scaling-book/">How to Scale Your Model</a> textbook, which explains the theory behind parallelism methods and distributed training.</p> <p>The goal of this blog is to connect these two perspectives by:</p> <ol> <li><strong>Implementing a language model from scratch in JAX</strong> instead of PyTorch</li> <li>Extending this implementation with <strong>distributed training in both JAX and PyTorch</strong></li> <li>Providing a simple, customizable codebase for <strong>empirically validating theoretical compute rooflines</strong>.</li> </ol> <p>We also <strong>provide an assignment with test cases</strong> so you can implement all of this in JAX yourself!</p> <blockquote class="block-tip"> <p>The associated code can be found at <a href="https://github.com/chuyishang/jax-lm/">https://github.com/chuyishang/jax-lm/</a>. If you want to try coding it yourself, you can use the <a href="https://github.com/chuyishang/jax-lm/tree/assignment"><code class="language-plaintext highlighter-rouge">assignment</code></a> branch of the repo as a starter. The code follows the same flow as CS336 Assignment 1, but with custom test cases for JAX and distributed training.</p> </blockquote> <p>This blog is split into 4 parts. First, we will provide a brief introduction to JAX and NNX. Next, we will implement the basic components of a language model in JAX, while providing comparisons to PyTorch. Then, we will introduce various distributed training methods (DP, TP, FSDP, FSDP+TP) and show how to implement them in JAX. Finally, we will use our code to empirically validate theoretical compute rooflines (coming soon!).</p> <p>With that being said, let’s dive in!</p> <h2 id="preliminaries">Preliminaries</h2> <h3 id="how-to-use-this-resource"><strong>How to Use this Resource</strong></h3> <p>This post is designed to be read front-to-back, but feel free to jump to whatever is most relevant to you. If you’re already comfortable with JAX and NNX, you can go straight to <a href="https://www.chuyishang.com/blog/2026/jax-lm/#implementing-our-model">Implementing Our Model</a>. If you’re familiar with distributed training concepts (DP, FSDP, TP) and just want to see the JAX implementation, skip ahead to <a href="https://www.chuyishang.com/blog/2026/jax-lm/#implementing-distributed-training">Implementing Distributed Training</a> and the <a href="https://github.com/chuyishang/jax-lm/tree/main/jax_impl/distributed">code implementation</a>.</p> <p>In addition to this blog post, we also release:</p> <ol> <li>An assignment for you to implement a JAX-based language model from scratch. This is based on Assignment 1 of Stanford’s CS336 course, but we add tests in JAX and tests for distributed training (DP, FSDP, TP, FSDP+TP). The assignment can be found <a href="https://www.chuyishang.com/blog/2026/assignment/">here</a>, and the starter code can be found <a href="https://github.com/chuyishang/jax-lm/tree/assignment">here</a>.</li> <li>We provide code implementations of a simple, non-distributed language model in both JAX and PyTorch. You can compare these two implementations side-by-side to get a feel of the differences. These can be found here: <ul> <li><a href="https://github.com/chuyishang/jax-lm/tree/main/jax_impl">JAX Implementation</a></li> <li><a href="https://github.com/chuyishang/jax-lm/tree/main/pytorch_impl">PyTorch Implementation</a></li> </ul> </li> <li>We extend both the JAX and PyTorch implementations to support distributed training with DP, FSDP, TP, and combined FSDP+TP. These can be found here: <ul> <li><a href="https://github.com/chuyishang/jax-lm/tree/main/jax_impl/distributed">JAX Distributed Implementation</a></li> <li><a href="https://github.com/chuyishang/jax-lm/tree/main/pytorch_impl/distributed">PyTorch Distributed Implementation</a></li> </ul> </li> </ol> <blockquote class="block-warning"> <p>This blog post is a living document and may contain errors. If you spot anything, please leave a comment below!</p> </blockquote> <h3 id="recommended-background"><strong>Recommended Background</strong></h3> <p>As mentioned, this blog post borrows a lot of code from the <a href="https://github.com/stanford-cs336/assignment1-basics">CS336 Assignment 1</a>, an excellent resource for learning how to build a language model from scratch with PyTorch. If you feel shaky on your PyTorch or Language Modeling fundamentals, we recommend you start there.</p> <p>This blog post also references the <a href="https://jax-ml.github.io/scaling-book/">How to Scale Your Model</a> book, which describes distributed training and sharding with JAX in much more detail. We recommend reading that book in parallel with this blog post to gain a deeper understanding of distributed training.</p> <p>With that being said, let’s dive in!</p> <h2 id="intro-to-jax-and-nnx">Intro to JAX and NNX</h2> <p>So what is JAX, and why would we want to use it at all? In this section, we’ll try to answer these questions by providing a brief overview of JAX and its neural network API, NNX. We will also touch upon some key concepts we’ll need for implementing our language model from scratch.</p> <details><summary>Note on Documentation</summary> <p>This section is not meant to be a comprehensive guide or a replacement for the docs. We recommend referring to the <a href="https://docs.jax.dev/en/latest/index.html">JAX</a> and <a href="https://flax.readthedocs.io/en/latest/index.html">Flax</a> docs for a full reference.</p> </details> <h3 id="intro-to-jax">Intro to JAX</h3> <p>JAX is an open-source Python library for high-performance numerical computing developed by Google. It combines a NumPy-like syntax with the ability to run on accelerators (GPUs/TPUs), and also provides an automatic differentiation system for efficiently computing gradients. While JAX was initially developed to be compatible with Google’s Tensor Processing Units (TPUs), it is also compatible with GPUs and CPUs and can provide significant speedups on these devices.</p> <p>So what makes JAX unique?</p> <ol> <li> <p>Firstly, JAX uses a <strong>functional programming</strong> model. Unlike PyTorch’s stateful, object-oriented approach, JAX adopts pure functions and immutable arrays. As an example, there is no model object that holds parameters. Instead, in core JAX, the model is represented as a nested collection of arrays (a PyTree) and passed explicitly into functions. This makes the execution model more transparent and composable.</p> </li> <li>Secondly, JAX provides a set of very powerful <strong>composable function transformations</strong>. The core idea is that we can write a plain Python or NumPy function and transform it into a more powerful function. Some common transforms are as follows: <ol> <li><a href="#jax-grad"><code class="language-plaintext highlighter-rouge">jax.grad</code></a>, which differentiates a function</li> <li><a href="#jax-jit"><code class="language-plaintext highlighter-rouge">jax.jit</code></a>, which compiles a function via XLA for fast execution</li> <li><a href="#jax-vmap"><code class="language-plaintext highlighter-rouge">jax.vmap</code></a>, which vectorizes a function over some batch dimension automatically</li> <li><a href="#jax-pmap"><code class="language-plaintext highlighter-rouge">jax.pmap</code></a>, which is similar to <code class="language-plaintext highlighter-rouge">jax.vmap</code> but parallelizes a function across devices These functions can be composed arbitrarily (such as <code class="language-plaintext highlighter-rouge">jit(vmap(grad(f)))</code>) and can often provide huge speedups in practice.</li> </ol> </li> <li>Lastly, JAX uses the <a href="https://openxla.org/xla"><strong>XLA Compiler</strong></a> as its backend, which optimizes across the entire computation graph instead of operator by operator as PyTorch does. This allows XLA to perform aggressive fusion and optimizations that span multiple operations and even devices, which can unlock large performance gains.</li> </ol> <p>Putting this all together, the use of pure, composable functions allows the JAX compiler to trace entire functional units into an intermediate representation, called the <a href="https://docs.jax.dev/en/latest/jaxpr.html">jaxpr</a>, instead of executing each line in the Python interpreter step-by-step. That’s where the speedup comes from.</p> <p>Let’s now take a look at some of the code behind these function transformations!</p> <h3 id="jax-grad"><code class="language-plaintext highlighter-rouge">jax.grad</code></h3> <p>At the heart of any deep learning library is automatic differentiation (autodiff), and JAX’s approach is built around <code class="language-plaintext highlighter-rouge">jax.grad</code>. <code class="language-plaintext highlighter-rouge">jax.grad</code> is a transform that takes in a function <code class="language-plaintext highlighter-rouge">f</code> and returns a new function that computes its gradient.</p> <p>This is a slightly different mental model from PyTorch. In PyTorch, the computation graph is built incrementally as we perform each operation in the forward pass. To get the backwards pass computation, PyTorch walks through the graph in reverse.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260318102536-480.webp 480w,/assets/img/jax-lm/pasted-image-20260318102536-800.webp 800w,/assets/img/jax-lm/pasted-image-20260318102536-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260318102536.png" class="img-fluid rounded z-depth-1" width="416" height="auto" alt="Pasted image 20260318102536.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <aside><p><a href="https://pytorch.org/blog/overview-of-pytorch-autograd-engine/">Image Source: PyTorch's Overview of the Autograd Engine</a></p></aside> <p>On the other hand, differentiation in JAX is expressed directly as a function transformation. If <code class="language-plaintext highlighter-rouge">f</code> computes a scalar output, then <code class="language-plaintext highlighter-rouge">jax.grad(f)</code> is a new function with the same inputs that returns the gradient of <code class="language-plaintext highlighter-rouge">f</code> with respect to whichever arguments we specify.</p> <p><strong>Code</strong></p> <p>To see this in code form, let’s look at the following example of a simple loss function:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">jax</span>
<span class="kn">import</span> <span class="n">jax.numpy</span> <span class="k">as</span> <span class="n">jnp</span>

<span class="k">def</span> <span class="nf">loss</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
    <span class="n">pred</span> <span class="o">=</span> <span class="n">params</span> <span class="o">@</span> <span class="n">x</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">mean</span><span class="p">((</span><span class="n">pred</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span>

<span class="n">grad_fn</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">grad</span><span class="p">(</span><span class="n">loss</span><span class="p">,</span> <span class="n">argnums</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">))</span>
<span class="n">grads</span> <span class="o">=</span> <span class="nf">grad_fn</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
<span class="n">dparams</span><span class="p">,</span> <span class="n">dx</span> <span class="o">=</span> <span class="n">grads</span>  <span class="c1"># for clarity
</span></code></pre></div></div> <p>We can define our <code class="language-plaintext highlighter-rouge">loss</code> function normally, using the NumPy-like operation <code class="language-plaintext highlighter-rouge">jnp.mean</code> to take the mean of our vector. Then, we can call <code class="language-plaintext highlighter-rouge">jax.grad</code> on our <code class="language-plaintext highlighter-rouge">loss</code> function to <em>transform</em> it into a new function <code class="language-plaintext highlighter-rouge">grad_fn</code> that computes the gradients of its inputs.</p> <p>One thing to note is that the new function <code class="language-plaintext highlighter-rouge">grad_fn</code> will be a wrapper around the original <code class="language-plaintext highlighter-rouge">loss</code> function. When we call <code class="language-plaintext highlighter-rouge">grad_fn</code> , JAX will run the forward pass (<code class="language-plaintext highlighter-rouge">loss</code>) and backwards pass (getting the gradients) in the same call. As a result, <code class="language-plaintext highlighter-rouge">grad_fn</code> must have the same function signature as the original <code class="language-plaintext highlighter-rouge">loss</code> function.</p> <p>To specify <em>which</em> arguments to differentiate w.r.t., we can pass in <code class="language-plaintext highlighter-rouge">argnums</code>. Here, <code class="language-plaintext highlighter-rouge">argnums=(0,1)</code> corresponds to <code class="language-plaintext highlighter-rouge">params</code> and <code class="language-plaintext highlighter-rouge">x</code>, so <code class="language-plaintext highlighter-rouge">grad_fn</code> returns a tuple of 2 gradients, <code class="language-plaintext highlighter-rouge">dparams</code> and <code class="language-plaintext highlighter-rouge">dx</code>.</p> <p>If we look carefully, the last two lines can actually be combined into the following form:<d-footnote>We drop the `argnums` here for simplicity; it defaults to 0 which corresponds to `params`.</d-footnote></p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">grads</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">grad</span><span class="p">(</span><span class="n">loss</span><span class="p">)(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span></code></pre></figure> <p>where <code class="language-plaintext highlighter-rouge">jax.grad</code> acts as a higher order function. If you are familiar with Python decorators, we see that this is actually the exact pattern of a decorator! So an equivalent way of writing the entire code would just be:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">jax</span>
<span class="kn">import</span> <span class="n">jax.numpy</span> <span class="k">as</span> <span class="n">jnp</span>

<span class="nd">@jax.grad</span>
<span class="k">def</span> <span class="nf">loss</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
    <span class="n">pred</span> <span class="o">=</span> <span class="n">params</span> <span class="o">@</span> <span class="n">x</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">mean</span><span class="p">((</span><span class="n">pred</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span>
    
<span class="n">grads</span> <span class="o">=</span> <span class="nf">loss</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span></code></pre></figure> <p>Since all JAX transforms are higher-order functions of the same form, they can all be used as decorators in this way.</p> <p>But while this is a nice illustration of the JAX programming model, for gradients specifically this is usually not the most convenient form. This is because after decoration with <code class="language-plaintext highlighter-rouge">@jax.grad</code>, our loss function would only return the gradients instead of the actual loss!</p> <p>Since we need both the loss in addition to the gradients during training, we typically use the <code class="language-plaintext highlighter-rouge">jax.value_and_grad</code> transform instead. The usage is identical, except the returned function now returns a <code class="language-plaintext highlighter-rouge">(loss, grads)</code> tuple instead of just <code class="language-plaintext highlighter-rouge">grads</code>.</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">loss_and_grad_fn</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">value_and_grad</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
<span class="n">loss_value</span><span class="p">,</span> <span class="n">grads</span> <span class="o">=</span> <span class="nf">loss_and_grad_fn</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span></code></pre></figure> <h3 id="jax-jit"><code class="language-plaintext highlighter-rouge">jax.jit</code></h3> <p><code class="language-plaintext highlighter-rouge">jax.jit</code> is a transform that <em>compiles</em> a function using the XLA compiler.<d-footnote>The JIT in `jax.jit` stands for Just-In-Time compilation, which refers to the fact that functions are compiled at runtime the first time they are called. This is in contrast to Ahead-Of-Time compilation, where everything is compiled before the program runs.</d-footnote> JAX compiles the function the first time it is called and reuses the compiled version for future calls.</p> <p>JIT compilation is powerful because XLA sees the whole function and optimizes it its entirety. This allows it to fuse operations, optimize memory layout, and parallelize in ways that line-by-line execution cannot.</p> <p>So what actually happens when we compile a function? On the first call, JAX <em>traces</em> the function by running it with abstract placeholders. You can think of these as something similar to types: instead of passing in <code class="language-plaintext highlighter-rouge">x = jnp.array([1.0, 2.0, 3.0])</code>, JAX traces with something like <code class="language-plaintext highlighter-rouge">x = float32[3]</code>. The idea is that XLA doesn’t need to know exact values of arrays to build the computation graph for optimization, it only needs to know the shapes and types of data.</p> <p>After tracing the function to construct a computation graph, JAX compiles it to optimized XLA bytecode. On subsequent calls, we can just run the compiled binaries instead of the Python code.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260307144539-480.webp 480w,/assets/img/jax-lm/pasted-image-20260307144539-800.webp 800w,/assets/img/jax-lm/pasted-image-20260307144539-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260307144539.png" class="img-fluid rounded z-depth-1" width="748" height="auto" alt="Pasted image 20260307144539.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>In practice, this can speed up code by a lot. The speedup usually gets larger the more operations we fuse in one <code class="language-plaintext highlighter-rouge">jax.jit</code> call, which is why many implementations wrap the entire training step (forward + backward + optimizer update) into a single <code class="language-plaintext highlighter-rouge">jax.jit</code> block, which we show below.</p> <details><summary>Additional Notes on Compilation</summary> <p>Why do we want to compile? Python can be slow for numerical computation since each operation needs to go through the Python interpreter, which has significant overhead. For example, performing <code class="language-plaintext highlighter-rouge">jnp.sum(x ** 2)</code> actually requires 2 operations (<code class="language-plaintext highlighter-rouge">**</code> and <code class="language-plaintext highlighter-rouge">sum</code>), which are dispatched separately to the GPU/TPU.</p> <p>Thus, each operation launches its own kernel and must read/write intermediate results from memory, which are both quite expensive. Compilation solves this by seeing the entire computation graph at once, allowing us to merge multiple operations into a single GPU kernel, reorder operations to maximize cache usage, and precompute anything that doesn’t depend on inputs (and store it for future calls).</p> </details> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260318113944-480.webp 480w,/assets/img/jax-lm/pasted-image-20260318113944-800.webp 800w,/assets/img/jax-lm/pasted-image-20260318113944-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260318113944.png" class="img-fluid rounded z-depth-1" width="900" height="auto" alt="Pasted image 20260318113944.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p><strong>Code</strong></p> <p>Here is what <code class="language-plaintext highlighter-rouge">jax.jit</code> might look like in code form:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">x</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span>

<span class="n">x</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">3</span><span class="p">,))</span>
<span class="n">output</span> <span class="o">=</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">)</span></code></pre></figure> <p>A more realistic training step might look something like this:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">train_step</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
    <span class="n">loss</span><span class="p">,</span> <span class="n">grads</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">value_and_grad</span><span class="p">(</span><span class="n">loss_fn</span><span class="p">)(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="n">params</span> <span class="o">=</span> <span class="n">params</span> <span class="o">-</span> <span class="n">lr</span> <span class="o">*</span> <span class="n">grads</span>
    <span class="k">return</span> <span class="n">params</span><span class="p">,</span> <span class="n">loss</span>

<span class="k">for</span> <span class="n">batch_x</span><span class="p">,</span> <span class="n">batch_y</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
    <span class="n">params</span><span class="p">,</span> <span class="n">loss</span> <span class="o">=</span> <span class="nf">train_step</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">batch_x</span><span class="p">,</span> <span class="n">batch_y</span><span class="p">)</span></code></pre></figure> <p>Note that we do the entire forwards, backwards, and parameter updates in a single function compiled by <code class="language-plaintext highlighter-rouge">jax.jit</code>!</p> <p><strong>Common Pitfalls</strong></p> <p>Note that JAX recompiles functions if the shapes of the input change. Take a look at the following example:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">x</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span>

<span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">3</span><span class="p">,)))</span>  <span class="c1"># compiles for float32[3]
</span><span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="mi">3</span><span class="p">,)))</span>  <span class="c1"># uses cached compiled function since same shape
</span><span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">5</span><span class="p">,)))</span>  <span class="c1"># !! recompiles for float32[5] !!
</span><span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">7</span><span class="p">,)))</span>  <span class="c1"># !! recompiles for float32[7] !!
</span><span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="mi">3</span><span class="p">,)))</span>  <span class="c1"># uses cached compiled function from the first call</span></code></pre></figure> <p>JAX compilation takes into account the input shapes to best optimize the program, so it saves a separate binary per shape. In our example, JAX compiles <code class="language-plaintext highlighter-rouge">f</code> for the first time with a <code class="language-plaintext highlighter-rouge">float32[3]</code> input and can reuse this for the second call. However, we trigger a costly recompilation each time our input shape is different!</p> <p>Secondly, since JAX traces Python control flow at compile time, ordinary <code class="language-plaintext highlighter-rouge">if</code> statements and loops can only depend on values known at trace time. Static loops are fine, but if a branch depends on a runtime value, a Python <code class="language-plaintext highlighter-rouge">if</code> inside <code class="language-plaintext highlighter-rouge">jax.jit</code> will fail. In those cases, we should use JAX control-flow primitives such as <code class="language-plaintext highlighter-rouge">lax.cond</code>, <code class="language-plaintext highlighter-rouge">lax.scan</code>, <code class="language-plaintext highlighter-rouge">lax.fori_loop</code>, or <code class="language-plaintext highlighter-rouge">lax.while_loop</code>.</p> <p>For example, the following looks natural in Python, but fails under <code class="language-plaintext highlighter-rouge">jax.jit</code> because the branch depends on the runtime value of <code class="language-plaintext highlighter-rouge">x</code>:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">jax</span>

<span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">bad_f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">3</span><span class="p">):</span>
        <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>  <span class="c1"># static loop: OK
</span>
    <span class="k">if</span> <span class="n">x</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>      <span class="c1"># not OK: value-dependent Python branch under jit
</span>        <span class="k">return</span> <span class="n">x</span> <span class="o">+</span> <span class="n">count</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">return</span> <span class="o">-</span><span class="n">x</span> <span class="o">+</span> <span class="n">count</span>

<span class="c1"># bad_f(1.0)  # raises TracerBoolConversionError</span></code></pre></figure> <p>To express the same logic in a JIT-compatible way, we can replace the Python <code class="language-plaintext highlighter-rouge">if</code> with <code class="language-plaintext highlighter-rouge">lax.cond</code>, which takes a scalar predicate and two branch functions:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">jax</span>
<span class="kn">from</span> <span class="n">jax</span> <span class="kn">import</span> <span class="n">lax</span>

<span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">3</span><span class="p">):</span>
        <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>  <span class="c1"># static loop: OK
</span>
    <span class="k">return</span> <span class="n">lax</span><span class="p">.</span><span class="nf">cond</span><span class="p">(</span>
        <span class="n">x</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span>
        <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span> <span class="o">+</span> <span class="n">count</span><span class="p">,</span>
        <span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="o">-</span><span class="n">x</span> <span class="o">+</span> <span class="n">count</span><span class="p">,</span>
        <span class="n">x</span><span class="p">,</span>
    <span class="p">)</span></code></pre></figure> <p>Finally, any Python-specific side effects (such as <code class="language-plaintext highlighter-rouge">print</code> statements) only happen at trace time and not during executing the compiled program. As a result, a <code class="language-plaintext highlighter-rouge">print</code> statement inside a JIT-compiled function may appear only when JAX first traces the function and on retraces. Here’s what this might look like:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="o">&gt;&gt;&gt;</span> <span class="nf">f</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">JitTracer</span><span class="o">&lt;~</span><span class="n">int32</span><span class="p">[]</span><span class="o">&gt;</span>
<span class="o">&gt;&gt;&gt;</span> <span class="nf">f</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># same shape, no print since we use cached compiled function
</span>
<span class="o">&gt;&gt;&gt;</span> <span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">array</span><span class="p">([</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]))</span> <span class="c1"># different shape, retrace
</span><span class="n">JitTracer</span><span class="o">&lt;</span><span class="n">int32</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="o">&gt;</span>
</code></pre></div></div> <p>Note that these prints happen only at trace time, so JAX does not yet know the concrete value, only the abstract tracer type!</p> <details><summary>Tip for Print Debugging</summary> <p>To print in a JIT-compiled function, we can use <code class="language-plaintext highlighter-rouge">jax.debug.print</code> instead of the default <code class="language-plaintext highlighter-rouge">print</code></p> </details> <h3 id="jax-vmap"><code class="language-plaintext highlighter-rouge">jax.vmap</code></h3> <p>The final core JAX transforms are for vectorization and parallelization. <code class="language-plaintext highlighter-rouge">jax.vmap</code> stands for “vectorized map” and transforms a function written for a single example into one that operates over a batch.</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">jax</span>
<span class="kn">import</span> <span class="n">jax.numpy</span> <span class="k">as</span> <span class="n">jnp</span>

<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">dot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>

<span class="n">batched_f</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">vmap</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
<span class="nf">batched_f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">)))</span></code></pre></figure> <p>Equivalently, we can also have this in decorator form:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@jax.vmap</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">dot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>

<span class="nf">f</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">)))</span></code></pre></figure> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260318125311-480.webp 480w,/assets/img/jax-lm/pasted-image-20260318125311-800.webp 800w,/assets/img/jax-lm/pasted-image-20260318125311-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260318125311.png" class="img-fluid rounded z-depth-1" width="774" height="auto" alt="Pasted image 20260318125311.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>An illustration of the above <code class="language-plaintext highlighter-rouge">jax.vmap</code>code.</p> <p>The idea here is that we can define a dot product function <code class="language-plaintext highlighter-rouge">f</code> that operates on a single vector, but then use <code class="language-plaintext highlighter-rouge">jax.vmap</code> to make it batched. By default, <code class="language-plaintext highlighter-rouge">jax.vmap</code> maps over the first dimension, but we can also specify which dimensions we want to batch over by passing in the <code class="language-plaintext highlighter-rouge">in_axes</code> argument.</p> <h3 id="jax-pmap"><code class="language-plaintext highlighter-rouge">jax.pmap</code></h3> <p><code class="language-plaintext highlighter-rouge">jax.pmap</code> stands for parallel map. It’s similar to <code class="language-plaintext highlighter-rouge">vmap</code> but operates over devices (different GPUs/TPUs) by running each slice of the batch on a separate accelerator. We describe <code class="language-plaintext highlighter-rouge">pmap</code> for background and historical completeness, but it’s worth noting that newer approaches often prefer mesh-based sharding instead, which we’ll discuss later in the parallelism section.</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">n_devices</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">device_count</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">loss_fn</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">params</span> <span class="o">*</span> <span class="n">x</span><span class="p">)</span>

<span class="n">parallel_loss</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">pmap</span><span class="p">(</span><span class="n">loss_fn</span><span class="p">,</span> <span class="n">in_axes</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>

<span class="n">xs</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="n">n_devices</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>  <span class="c1"># one slice per device. note that the batch dimension must equal `n_devices`
</span><span class="nf">parallel_loss</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">xs</span><span class="p">)</span>       <span class="c1"># runs shard i on device i</span></code></pre></figure> <p>In this example, we pass in <code class="language-plaintext highlighter-rouge">in_axes=(None, 0)</code>. This means that we want the first argument <code class="language-plaintext highlighter-rouge">params</code> to replicated across all devices (<code class="language-plaintext highlighter-rouge">None</code>), and the second argument to be split across the device axis <code class="language-plaintext highlighter-rouge">0</code>. Since we don’t specify a device mesh, our devices form a single <code class="language-plaintext highlighter-rouge">(n_devices,)</code> mesh, so this effectively sends row <code class="language-plaintext highlighter-rouge">i</code> to device <code class="language-plaintext highlighter-rouge">i</code>.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260318131840-480.webp 480w,/assets/img/jax-lm/pasted-image-20260318131840-800.webp 800w,/assets/img/jax-lm/pasted-image-20260318131840-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260318131840.png" class="img-fluid rounded z-depth-1" width="1020" height="auto" alt="Pasted image 20260318131840.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <details><summary>PyTorch comparison for <code class="language-plaintext highlighter-rouge">jax.pmap</code></summary> <p>The equivalent code in PyTorch would look something like the following:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.distributed</span> <span class="k">as</span> <span class="n">dist</span>

<span class="k">def</span> <span class="nf">loss_fn</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">torch</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="n">params</span> <span class="o">*</span> <span class="n">x</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">dist</span><span class="p">.</span><span class="nf">init_process_group</span><span class="p">(</span><span class="n">backend</span><span class="o">=</span><span class="sh">'</span><span class="s">nccl</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">rank</span> <span class="o">=</span> <span class="n">dist</span><span class="p">.</span><span class="nf">get_rank</span><span class="p">()</span>
    <span class="n">world_size</span> <span class="o">=</span> <span class="n">dist</span><span class="p">.</span><span class="nf">get_world_size</span><span class="p">()</span>

    <span class="n">params</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">ones</span><span class="p">(</span><span class="mi">4</span><span class="p">).</span><span class="nf">cuda</span><span class="p">(</span><span class="n">rank</span><span class="p">)</span>
    <span class="n">xs</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="n">world_size</span><span class="p">,</span> <span class="mi">4</span><span class="p">)).</span><span class="nf">cuda</span><span class="p">(</span><span class="n">rank</span><span class="p">)</span>  <span class="c1"># full batch, same shape as JAX
</span>
    <span class="n">x_local</span> <span class="o">=</span> <span class="n">xs</span><span class="p">[</span><span class="n">rank</span><span class="p">]</span>          <span class="c1"># manually shard: each process takes its slice
</span>    <span class="n">loss</span> <span class="o">=</span> <span class="nf">loss_fn</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">x_local</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">rank </span><span class="si">{</span><span class="n">rank</span><span class="si">}</span><span class="s">, loss: </span><span class="si">{</span><span class="n">loss</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span></code></pre></figure> <p>and we would need to launch it with <code class="language-plaintext highlighter-rouge">torchrun --nproc_per_node=4 script.py</code>. As you can see, we would need to manually send shards to their assigned devices and each process would also run its own Python interpreter. Meanwhile, JAX allows us to run all of this in a single process with <code class="language-plaintext highlighter-rouge">pmap</code> and provides cleaner scalability for more complex distributed methods.</p> </details> <h2 id="nnx">NNX</h2> <p>Now that we’ve talked about some of the core concepts behind JAX, let’s talk a bit about NNX, which is what we will be using for the rest of this blog.</p> <p>NNX is the neural network API for JAX. If you are familiar with PyTorch, you can think of NNX as playing a role similar to <code class="language-plaintext highlighter-rouge">torch.nn</code>. NNX was introduced in 2024 as an evolution of Flax Linen, with the goal of keeping the parts of Linen that worked well while making the programming model more Pythonic and easier to debug.</p> <p>In particular, NNX relaxes the purely functional approach of core JAX by introducing a module-based, object-oriented design similar to PyTorch. Instead of passing model states explicitly in and out of functions, NNX now allows us to define stateful module objects. NNX then provides object-aware versions of common JAX transforms so we can still leverage JAX’s power within this more familiar programming model. For example, <code class="language-plaintext highlighter-rouge">@jax.jit</code> becomes <code class="language-plaintext highlighter-rouge">@nnx.jit</code>, and <code class="language-plaintext highlighter-rouge">jax.value_and_grad</code> becomes <code class="language-plaintext highlighter-rouge">nnx.value_and_grad</code>.</p> <p>To learn a bit more about why you should use NNX, you can refer to the official explanation <a href="https://flax.readthedocs.io/en/stable/why.html">here</a>.</p> <h2 id="implementing-our-model">Implementing Our Model</h2> <p>In this section, we will implement a basic version of a Transformer language model in JAX and provide a version of the same code written in PyTorch for comparison. We will begin with a simple version without worrying too much about sharding and distributed training, then layer in that functionality afterwards.</p> <p>Let’s start by implementing the simplest component of any modern day neural network: the Linear layer.</p> <h3 id="the-linear-layer">The Linear Layer</h3> <p>The following is how you might implement a linear layer (without biases) in PyTorch:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span>
        <span class="n">in_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">out_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">device</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">device</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">dtype</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">dtype</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">weights</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">empty</span><span class="p">(</span><span class="n">out_features</span><span class="p">,</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">std</span> <span class="o">=</span> <span class="p">(</span><span class="mi">2</span> <span class="o">/</span> <span class="p">(</span><span class="n">in_features</span> <span class="o">+</span> <span class="n">out_features</span><span class="p">))</span> <span class="o">**</span> <span class="mf">0.5</span>
        <span class="n">nn</span><span class="p">.</span><span class="n">init</span><span class="p">.</span><span class="nf">trunc_normal_</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">mean</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">std</span><span class="o">=</span><span class="n">std</span><span class="p">,</span> <span class="n">a</span><span class="o">=-</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">,</span> <span class="n">b</span><span class="o">=</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">weights</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Parameter</span><span class="p">(</span><span class="n">weights</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">einsum</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">weights</span><span class="p">,</span> <span class="sh">"</span><span class="s">... d_in, d_out d_in -&gt; ... d_out</span><span class="sh">"</span><span class="p">)</span></code></pre></figure> <p>And here is how you would do it in NNX:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span> <span class="n">self</span><span class="p">,</span>
        <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
        <span class="n">in_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">out_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">std</span> <span class="o">=</span> <span class="p">(</span><span class="mi">2</span> <span class="o">/</span> <span class="p">(</span><span class="n">in_features</span> <span class="o">+</span> <span class="n">out_features</span><span class="p">))</span> <span class="o">**</span> <span class="mf">0.5</span>
        <span class="n">init_fn</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="n">initializers</span><span class="p">.</span><span class="nf">truncated_normal</span><span class="p">(</span><span class="n">stddev</span><span class="o">=</span><span class="n">std</span><span class="p">,</span> <span class="n">lower</span><span class="o">=-</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">,</span> <span class="n">upper</span><span class="o">=</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">)</span>
        <span class="n">weights_data</span> <span class="o">=</span> <span class="nf">init_fn</span><span class="p">(</span><span class="n">rngs</span><span class="p">.</span><span class="nf">params</span><span class="p">(),</span> <span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span> <span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">weights</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nc">Param</span><span class="p">(</span><span class="n">weights_data</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">einsum</span><span class="p">(</span><span class="sh">"</span><span class="s">...i,io-&gt;...o</span><span class="sh">"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">weights</span><span class="p">.</span><span class="nf">get_value</span><span class="p">())</span></code></pre></figure> <details><summary>Weight Storage Convention</summary> <p>In PyTorch, the weights for a Linear layer are typically stored with shape <code class="language-plaintext highlighter-rouge">[Out, In]</code>, whereas in JAX, the weights are stored as <code class="language-plaintext highlighter-rouge">[In, Out]</code>.</p> </details> <p>As you can see, the code looks very similar. Aside from minor differences in syntax and storage convention, there are a few key changes that illustrate some of the philosophical differences between JAX and PyTorch.</p> <ol> <li> <p><strong>Initialization</strong>: In PyTorch, initialization functions usually operate in-place on an already-allocated tensor. This is why we create an empty weights tensor in the desired shape and on the desired device before initializing it. On the other hand, initialization functions in JAX typically return a tensor (<code class="language-plaintext highlighter-rouge">weights_data</code> in this case), which we then wrap with <code class="language-plaintext highlighter-rouge">nnx.Param()</code> to make it a parameter object so that JAX knows how to use it for transforms and state management. The actual weight tensor of an <code class="language-plaintext highlighter-rouge">nnx.Param</code> actually lives in its <code class="language-plaintext highlighter-rouge">.value</code> attribute.</p> <p>This example highlights a core philosophical difference between PyTorch and JAX. PyTorch is designed in an object-oriented style centered around stateful modules, where parameters live inside modules and workflows mutate tensors in place. JAX treats parameters as explicit data and often represents common workflows as functions that produce new values.</p> </li> <li> <p><strong>Device Placement</strong>: In PyTorch, device placement is explicit. Typically, we create parameters directly on a target device with <code class="language-plaintext highlighter-rouge">device=...</code> or move them afterwards with <code class="language-plaintext highlighter-rouge">.to(device)</code>. Meanwhile, in JAX, device placement is implicit and determined automatically by the compiler and any sharding configs we specify.</p> </li> <li> <p><strong>RNG</strong>: In JAX, RNGs need to be passed in explicitly, and are split/consumed deterministically. On the other hand, RNG is largely global and ambient in PyTorch (i.e. we can set a global RNG seed) and we don’t need to pass it explicitly to each function.</p> <p>In the example above, we use <code class="language-plaintext highlighter-rouge">rngs.params()</code> in the <code class="language-plaintext highlighter-rouge">init_fn</code>, which generates an random number from the <code class="language-plaintext highlighter-rouge">params</code> stream. There are 2 main PRNG key stream names used by Flax NNX’s built-in layers, which are <code class="language-plaintext highlighter-rouge">params</code> and <code class="language-plaintext highlighter-rouge">dropout</code>.</p> </li> </ol> <p><strong>Some other minor differences</strong>:</p> <ul> <li>PyTorch uses the <code class="language-plaintext highlighter-rouge">forward</code> convention, which is wrapped by the <code class="language-plaintext highlighter-rouge">__call__</code> method implemented by the parent class <code class="language-plaintext highlighter-rouge">nn.Module</code>. JAX typically uses <code class="language-plaintext highlighter-rouge">__call__</code> directly.</li> </ul> <h3 id="multi-head-self-attention">Multi-head Self Attention</h3> <p>Let’s take a look at another example before moving on. For those following along on the assignment, note that the other building blocks of our model have minor syntax differences compared to PyTorch, but follow the same ideas discussed here.</p> <p>This is what code for multi-head self attention (MHA) in PyTorch would look like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">MultiHeadSelfAttention</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span>
        <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">rope_theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e4</span><span class="p">,</span>
        <span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">,</span>
        <span class="n">device</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">device</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">dtype</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">dtype</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="k">assert</span> <span class="n">d_model</span> <span class="o">%</span> <span class="n">num_heads</span> <span class="o">==</span> <span class="mi">0</span><span class="p">,</span> <span class="sh">"</span><span class="s">d_model must be divisible by num_heads</span><span class="sh">"</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">Q_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">K_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">V_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">O_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">register_buffer</span><span class="p">(</span>
            <span class="sh">"</span><span class="s">causal_mask</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">torch</span><span class="p">.</span><span class="nf">tril</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">ones</span><span class="p">(</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)),</span>
            <span class="n">persistent</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">rope</span> <span class="o">=</span> <span class="nc">RotaryPositionalEmbedding</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">theta</span><span class="o">=</span><span class="n">rope_theta</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="o">=</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">use_rope</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
        <span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>
        <span class="n">q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">Q_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">k</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">K_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">V_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">use_rope</span><span class="p">:</span>
            <span class="n">positions</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">seq_len</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">x</span><span class="p">.</span><span class="n">device</span><span class="p">)</span>
            <span class="n">q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">rope</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">positions</span><span class="p">)</span>
            <span class="n">k</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">rope</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">positions</span><span class="p">)</span>

        <span class="n">attn_out</span> <span class="o">=</span> <span class="nf">sdpa</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">causal_mask</span><span class="p">[:</span><span class="n">seq_len</span><span class="p">,</span> <span class="p">:</span><span class="n">seq_len</span><span class="p">])</span>
        <span class="n">attn_out</span> <span class="o">=</span> <span class="n">attn_out</span><span class="p">.</span><span class="nf">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">contiguous</span><span class="p">().</span><span class="nf">reshape_as</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nc">O_proj</span><span class="p">(</span><span class="n">attn_out</span><span class="p">)</span></code></pre></figure> <p>And here is what it looks like in JAX:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">MultiHeadSelfAttention</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span>
        <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
        <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">rope_theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e4</span><span class="p">,</span>
        <span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">,</span>
        <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="k">assert</span> <span class="n">d_model</span> <span class="o">%</span> <span class="n">num_heads</span> <span class="o">==</span> <span class="mi">0</span><span class="p">,</span> <span class="sh">"</span><span class="s">d_model must be divisible by num_heads</span><span class="sh">"</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">d_k</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>
        <span class="n">self</span><span class="p">.</span><span class="n">Q_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">rngs</span><span class="o">=</span><span class="n">rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">K_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">rngs</span><span class="o">=</span><span class="n">rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">V_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">rngs</span><span class="o">=</span><span class="n">rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">O_proj</span> <span class="o">=</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">rngs</span><span class="o">=</span><span class="n">rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="n">d_model</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">dtype</span><span class="p">)</span>

        <span class="n">self</span><span class="p">.</span><span class="n">causal_mask</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">tril</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">)))</span>
        <span class="n">self</span><span class="p">.</span><span class="n">rope</span> <span class="o">=</span> <span class="nc">RotaryPositionalEmbedding</span><span class="p">(</span><span class="n">d_k</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">,</span> <span class="n">theta</span><span class="o">=</span><span class="n">rope_theta</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="o">=</span><span class="n">max_seq_len</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">Array</span><span class="p">,</span> <span class="n">use_rope</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">):</span>
        <span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span>
        <span class="n">q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">Q_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">swapaxes</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">k</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">K_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">swapaxes</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nc">V_proj</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">d_k</span><span class="p">).</span><span class="nf">swapaxes</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">use_rope</span><span class="p">:</span>
            <span class="n">positions</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">arange</span><span class="p">(</span><span class="n">seq_len</span><span class="p">)</span>
            <span class="n">q</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">rope</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">positions</span><span class="p">)</span>
            <span class="n">k</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">rope</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">positions</span><span class="p">)</span>

        <span class="n">attn_out</span> <span class="o">=</span> <span class="nf">sdpa</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">causal_mask</span><span class="p">[:</span><span class="n">seq_len</span><span class="p">,</span> <span class="p">:</span><span class="n">seq_len</span><span class="p">])</span>
        <span class="n">attn_out</span> <span class="o">=</span> <span class="n">attn_out</span><span class="p">.</span><span class="nf">swapaxes</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">reshape</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nc">O_proj</span><span class="p">(</span><span class="n">attn_out</span><span class="p">)</span></code></pre></figure> <p>Aside from the syntax and RNG differences described earlier, the two implementations look basically the same!</p> <p>However, one interesting difference to note here is how causal masks are handled. In PyTorch, it’s common to register the mask as a buffer at initialization:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">self</span><span class="p">.</span><span class="nf">register_buffer</span><span class="p">(</span>
    <span class="sh">"</span><span class="s">causal_mask</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">torch</span><span class="p">.</span><span class="nf">tril</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">ones</span><span class="p">(</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)),</span>
    <span class="n">persistent</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="p">)</span></code></pre></figure> <p>Unlike parameters, buffers are not learnable and not tracked by the optimizer. However, they are automatically moved with the model during device placement.</p> <p>In JAX, we can just assign it directly:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">self</span><span class="p">.</span><span class="n">causal_mask</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">tril</span><span class="p">(</span><span class="n">jnp</span><span class="p">.</span><span class="nf">ones</span><span class="p">((</span><span class="n">max_seq_len</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">)))</span></code></pre></figure> <p>This is because device placement is automatic in JAX, so there’s no need for a special registration mechanism to ensure the mask follows the model to the correct device. As a bonus, since <code class="language-plaintext highlighter-rouge">jax.jit</code> treats any closed-over array as a constant in the compiled graph, the mask is never recomputed across calls.</p> <p>The other components of our language model (SwiGLU, RMSNorm, Embedding, LM Head) follow much of the same design. We recommend taking a look at the full model implementation in JAX, which you can find <a href="https://github.com/chuyishang/jax-lm/blob/main/jax_impl/basic/model.py">here</a>.</p> <h2 id="implementing-the-training-loop">Implementing the Training Loop</h2> <p>Now that we have our model implemented in JAX, let’s take a look at how we can train it!</p> <p>There are a few major differences in the JAX training loop compared to PyTorch. We’ll discuss each one and what it might reveal about the JAX paradigm.</p> <h3 id="the-training-step">The Training Step</h3> <p>In PyTorch, our train step looks something like this:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">inputs</span><span class="p">)</span>
<span class="n">B</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span> <span class="o">=</span> <span class="n">logits</span><span class="p">.</span><span class="n">shape</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">model_module</span><span class="p">.</span><span class="nf">cross_entropy_loss</span><span class="p">(</span><span class="n">logits</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span><span class="p">),</span> <span class="n">targets</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">))</span>
<span class="n">optimizer</span><span class="p">.</span><span class="nf">zero_grad</span><span class="p">()</span>
<span class="n">loss</span><span class="p">.</span><span class="nf">backward</span><span class="p">()</span>
<span class="n">optimizer</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span></code></pre></figure> <p>Meanwhile, in JAX, it looks something like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">loss</span><span class="p">,</span> <span class="n">grad_state</span> <span class="o">=</span> <span class="nf">train_step</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">optimizer</span><span class="p">,</span> <span class="n">inputs</span><span class="p">,</span> <span class="n">targets</span><span class="p">)</span></code></pre></figure> <p>Where the <code class="language-plaintext highlighter-rouge">train_step</code> function looks something like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@nnx.jit</span>
<span class="k">def</span> <span class="nf">train_step</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">,</span> <span class="n">optimizer</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Optimizer</span><span class="p">,</span> <span class="n">inputs</span><span class="p">:</span> <span class="n">Array</span><span class="p">,</span> <span class="n">targets</span><span class="p">:</span> <span class="n">Array</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">loss_fn</span><span class="p">(</span><span class="n">model</span><span class="p">):</span>
        <span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">inputs</span><span class="p">)</span>
        <span class="n">B</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span> <span class="o">=</span> <span class="n">logits</span><span class="p">.</span><span class="n">shape</span>
        <span class="k">return</span> <span class="nf">cross_entropy_loss</span><span class="p">(</span><span class="n">logits</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span><span class="p">),</span> <span class="n">targets</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">))</span>

    <span class="n">loss</span><span class="p">,</span> <span class="n">grads</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nf">value_and_grad</span><span class="p">(</span><span class="n">loss_fn</span><span class="p">)(</span><span class="n">model</span><span class="p">)</span>

    <span class="n">optimizer</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">grads</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">loss</span><span class="p">,</span> <span class="n">grads</span></code></pre></figure> <p><strong>Train Step Function</strong>: In PyTorch, we don’t need to wrap our training step into a function. Most of our operations operate in place. However, we need to create a <code class="language-plaintext highlighter-rouge">train_step</code> function in JAX so that we can wrap it with <code class="language-plaintext highlighter-rouge">nnx.jit</code>.</p> <p><strong>Gradient Handling</strong>: In PyTorch, gradients are implicit, since <code class="language-plaintext highlighter-rouge">loss.backward()</code> accumulates gradients into each parameter’s <code class="language-plaintext highlighter-rouge">.grad</code> field, and <code class="language-plaintext highlighter-rouge">optimizer.step()</code> reads those <code class="language-plaintext highlighter-rouge">.grad</code> buffers and updates parameters in place. As a result, our gradient states are effectively distributed across <code class="language-plaintext highlighter-rouge">{p.grad for p in model.parameters()}</code>, instead of being unified in a single gradient object <code class="language-plaintext highlighter-rouge">grad</code> like in JAX. Gradient clipping in PyTorch follows the same philosophy by scaling the <code class="language-plaintext highlighter-rouge">.grad</code> buffers in place.</p> <p>On the other hand, gradients are treated as explicit values in JAX. To get our <code class="language-plaintext highlighter-rouge">grad</code> object, we use <code class="language-plaintext highlighter-rouge">nnx.value_and_grad</code>, which is a <a href="#jax-grad">transformation</a> that turns the <code class="language-plaintext highlighter-rouge">loss_fn</code> into a function that also returns gradients. Since JAX transforms act as higher order functions, we need to define a loss function and pass it in as input to <code class="language-plaintext highlighter-rouge">nnx.value_and_grad</code> instead of just calling <code class="language-plaintext highlighter-rouge">loss.backward()</code> like we do in PyTorch.</p> <p>Now that we have the basic training step, let’s add gradient clipping and a learning rate schedule.</p> <h3 id="gradient-clipping-and-lr-schedule">Gradient Clipping and LR Schedule</h3> <p>If we wanted to add gradient clipping and a learning rate schedule in PyTorch, they would be called separately from our optimizer. In JAX, they would be processed by the optimizer internally.</p> <p><strong>PyTorch</strong> Here’s what it would look like in PyTorch:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">build_lr_scheduler</span><span class="p">(</span>
    <span class="n">optimizer</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">Optimizer</span><span class="p">,</span>
    <span class="n">min_learning_rate</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
    <span class="n">warmup_iters</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
    <span class="n">cosine_annealing_iters</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
    <span class="n">warmup_start_factor</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.1</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="n">LRScheduler</span><span class="p">:</span>
    <span class="n">cosine_decay_iters</span> <span class="o">=</span> <span class="n">cosine_annealing_iters</span> <span class="o">-</span> <span class="n">warmup_iters</span>
    <span class="n">warmup_scheduler</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="nc">LinearLR</span><span class="p">(</span>
        <span class="n">optimizer</span><span class="p">,</span>
        <span class="n">start_factor</span><span class="o">=</span><span class="n">warmup_start_factor</span><span class="p">,</span>
        <span class="n">end_factor</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span>
        <span class="n">total_iters</span><span class="o">=</span><span class="n">warmup_iters</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">cosine_scheduler</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="nc">CosineAnnealingLR</span><span class="p">(</span>
        <span class="n">optimizer</span><span class="p">,</span>
        <span class="n">T_max</span><span class="o">=</span><span class="n">cosine_decay_iters</span><span class="p">,</span>
        <span class="n">eta_min</span><span class="o">=</span><span class="n">min_learning_rate</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">scheduler</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="n">LRScheduler</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">lr_scheduler</span><span class="p">.</span><span class="nc">SequentialLR</span><span class="p">(</span>
        <span class="n">optimizer</span><span class="p">,</span>
        <span class="n">schedulers</span><span class="o">=</span><span class="p">[</span><span class="n">warmup_scheduler</span><span class="p">,</span> <span class="n">cosine_scheduler</span><span class="p">],</span>
        <span class="n">milestones</span><span class="o">=</span><span class="p">[</span><span class="n">warmup_iters</span><span class="p">],</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">scheduler</span></code></pre></figure> <p>and the new <code class="language-plaintext highlighter-rouge">train_step</code> would look something like this:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">logits</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">inputs</span><span class="p">)</span>
<span class="n">B</span><span class="p">,</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span> <span class="o">=</span> <span class="n">logits</span><span class="p">.</span><span class="n">shape</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">model_module</span><span class="p">.</span><span class="nf">cross_entropy_loss</span><span class="p">(</span><span class="n">logits</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">,</span> <span class="n">V</span><span class="p">),</span> <span class="n">targets</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="n">B</span> <span class="o">*</span> <span class="n">S</span><span class="p">))</span>
<span class="n">optimizer</span><span class="p">.</span><span class="nf">zero_grad</span><span class="p">()</span>
<span class="n">loss</span><span class="p">.</span><span class="nf">backward</span><span class="p">()</span>
<span class="k">if</span> <span class="n">gradient_clip</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>  <span class="c1"># gradient clipping
</span>    <span class="n">torch</span><span class="p">.</span><span class="n">nn</span><span class="p">.</span><span class="n">utils</span><span class="p">.</span><span class="nf">clip_grad_norm_</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">(),</span> <span class="nf">float</span><span class="p">(</span><span class="n">gradient_clip</span><span class="p">))</span>
<span class="n">optimizer</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span>
<span class="k">if</span> <span class="n">scheduler</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>  <span class="c1"># scheduler handling
</span>    <span class="n">scheduler</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span></code></pre></figure> <p>In the function <code class="language-plaintext highlighter-rouge">build_lr_scheduler</code>, we create a scheduler object that we chains together a linear warmup schedule with a cosine annealing schedule. To use this, we call <code class="language-plaintext highlighter-rouge">scheduler.step()</code>, which computes new LR values and mutates the optimizer’s <code class="language-plaintext highlighter-rouge">param_groups</code>’ <code class="language-plaintext highlighter-rouge">lr</code> attributes in place.</p> <p><strong>JAX</strong> In contrast, gradient clipping and the learning rate scheduler are used as inputs during optimizer construction in JAX. Our optimizer construction would look something like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">lr_schedule</span> <span class="o">=</span> <span class="n">optax</span><span class="p">.</span><span class="nf">warmup_cosine_decay_schedule</span><span class="p">(</span>
    <span class="n">init_value</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
    <span class="n">peak_value</span><span class="o">=</span><span class="n">a_max</span><span class="p">,</span>
    <span class="n">warmup_steps</span><span class="o">=</span><span class="n">warmup_iters</span><span class="p">,</span>
    <span class="n">decay_steps</span><span class="o">=</span><span class="n">decay_iters</span><span class="p">,</span>
    <span class="n">end_value</span><span class="o">=</span><span class="n">a_min</span>
<span class="p">)</span>

<span class="k">def</span> <span class="nf">build_optimizer_transform</span><span class="p">(</span>
    <span class="n">optimizer_config</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span>
    <span class="n">gradient_clip</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
    <span class="n">lr_schedule</span><span class="p">:</span> <span class="n">optax</span><span class="p">.</span><span class="n">Schedule</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">optax</span><span class="p">.</span><span class="n">GradientTransformation</span><span class="p">:</span>
    <span class="n">transforms</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">if</span> <span class="n">gradient_clip</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">transforms</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">optax</span><span class="p">.</span><span class="nf">clip_by_global_norm</span><span class="p">(</span><span class="nf">float</span><span class="p">(</span><span class="n">gradient_clip</span><span class="p">)))</span>
    <span class="n">transforms</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span>
        <span class="nf">adamw</span><span class="p">(</span>
            <span class="n">lr</span><span class="o">=</span><span class="n">lr_schedule</span><span class="p">,</span>
            <span class="n">betas</span><span class="o">=</span><span class="nf">tuple</span><span class="p">(</span><span class="n">optimizer_config</span><span class="p">[</span><span class="sh">"</span><span class="s">betas</span><span class="sh">"</span><span class="p">]),</span>
            <span class="n">weight_decay</span><span class="o">=</span><span class="nf">float</span><span class="p">(</span><span class="n">optimizer_config</span><span class="p">[</span><span class="sh">"</span><span class="s">weight_decay</span><span class="sh">"</span><span class="p">]),</span>
            <span class="n">eps</span><span class="o">=</span><span class="nf">float</span><span class="p">(</span><span class="n">optimizer_config</span><span class="p">[</span><span class="sh">"</span><span class="s">eps</span><span class="sh">"</span><span class="p">]),</span>
        <span class="p">)</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">optax</span><span class="p">.</span><span class="nf">chain</span><span class="p">(</span><span class="o">*</span><span class="n">transforms</span><span class="p">)</span>

<span class="n">optimizer</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nc">Optimizer</span><span class="p">(</span>
    <span class="n">model</span><span class="p">,</span>
    <span class="nf">build_optimizer_transform</span><span class="p">(</span>
        <span class="n">optimizer_config</span><span class="p">,</span>
        <span class="n">gradient_clip</span><span class="o">=</span><span class="n">gradient_clip</span><span class="p">,</span>
        <span class="n">lr_schedule</span><span class="o">=</span><span class="n">lr_schedule</span><span class="p">,</span>
    <span class="p">),</span>
    <span class="n">wrt</span><span class="o">=</span><span class="n">nnx</span><span class="p">.</span><span class="n">Param</span><span class="p">,</span>
<span class="p">)</span></code></pre></figure> <p>In JAX, the optimizer is treated as a series of Optax transformations. We can first construct our pipeline by adding gradient clipping followed by AdamW. Note that we pass in our learning rate schedule <code class="language-plaintext highlighter-rouge">lr_schedule</code> as an argument to AdamW during initialization. Then, we can create an <code class="language-plaintext highlighter-rouge">nnx.Optimizer</code> using our <code class="language-plaintext highlighter-rouge">model</code>, the pipeline we just created, and the <code class="language-plaintext highlighter-rouge">wrt</code> argument that specifies what to optimize.</p> <p>Finally, we can call this without changing our code by using <code class="language-plaintext highlighter-rouge">optimizer.update(model, grads)</code>, which will handle the gradient clipping and LR updates internally. Note that unlike PyTorch, we don’t need to call <code class="language-plaintext highlighter-rouge">scheduler.step()</code> or <code class="language-plaintext highlighter-rouge">clip_grad_norm_()</code> explicitly in each step.</p> <p>You can find the full training script <a href="https://github.com/chuyishang/jax-lm/blob/main/jax_impl/basic/train.py">here</a>.</p> <h2 id="intro-to-distributed-training">Intro to Distributed Training</h2> <p>So far, we have assumed that both the model and data batch fit on a single device. However, once the model state or activations become too large, or once we want more throughput than a single device can provide, we need to spread (a.k.a <strong>shard</strong>) the work across multiple GPUs/TPUs. Doing so can give us more total memory and compute, but it also introduces a new bottleneck: communication. As a result, a core problem in distributed training is deciding which tensors stay local to each device, which tensors are sharded, and which tensors should be communicated between devices.</p> <p>Different parallelism strategies answer this question in different ways. In this section, we will focus on 4 common strategies: Data Parallelism (DP), Fully Sharded Data Parallelism (FSDP), Tensor Parallelism (TP), and mixed FSDP+TP. A useful way to think about them is:</p> <ul> <li>DP splits the data batch</li> <li>FSDP splits the persistent model state</li> <li>TP splits the hidden-state computation inside a layer</li> <li>FSDP+TP does both at once</li> </ul> <p>Don’t worry if this doesn’t make much sense yet! The goal of this section is to build some intuition for these strategies, and in the next section we will show you how to implement them in JAX.</p> <details><summary>Note</summary> <p>This section is not meant as a complete guide to distributed training. We recommend starting with the <a href="https://jax-ml.github.io/scaling-book/">How to Scale Your Model</a> book for a more complete guide.</p> </details> <h3 id="notation">Notation</h3> <p>We will denote variables in the form $\text{VarName}[d_1, d_2, \ldots]$ giving its name and shape, respectively. To keep the notation simple, we will approximate a Transformer block as a pair of linear layers:</p> \[\text{In}[B, D] \cdot W_{1}[D, F] \cdot W_{2}[F, D] \to \text{Out}[B, D]\] <details><summary>Why can we use this approximation?</summary> <p>A Transformer block contains two main sub-blocks: multi-head attention (MHA) and the feed-forward network (FFN). Both include large projection matrices that map from the model dimension $D$ into some intermediate space and then back to $D$.</p> <p>For the FFN, this is exactly the $D \to F \to D$ pattern. For MHA, the input is first projected into Q/K/V representations, attention is applied, and the result is then projected back into $D$ by the output weights $W_O$.</p> <p>We simplify both sub-blocks to a pair of linear layers since we care mostly about how we shard the large projection matrices and the communication they induce. The operations in-between (activations, attention, layernorms) are typically performed locally on each device under the sharding strategies we discuss. As a result, they typically don’t introduce additional communication.</p> <p>In many common LLM settings, the FFN also accounts for a large fraction of the block FLOPs, which makes this simplification especially useful.</p> </details> <p>One important note here is that $\text{In}[B, D]$ represents the input activations, not necessarily of the raw input batch itself. In language modeling, we typically have a tokenized input batch of shape $[B, T]$ where $T$ is the number of tokens per sample. This input batch would then be fed through our embedding table to get our activations $[B, T, D]$, which is what we refer to here as $\text{In}$. Following common practice, we abuse our notation a bit to roll the $B$ and $T$ dimensions into one $B$ that represents the total number of tokens in a batch.</p> <p>For sharding, we will use subscripts like $B_X$ and $D_Y$ to denote sharding over device-mesh axes $X$ and $Y$, respectively. For example, $B_X$ means the batch dimension is split across the $X$-axis of the device mesh, while $D_Y$ means that the hidden dimension is split across the $Y$-axis. We’ll introduce the device mesh in the next section.</p> <p>In our illustrations, we will mostly show weight gradients and omit input gradients when they aren’t the main point of the conversation.</p> <p>Before we dive into parallelism schemes, let’s take a look at some important preliminaries: the device mesh, and communication collectives.</p> <h5 id="the-device-mesh">The Device Mesh</h5> <p>To talk about sharding, we need to have a way to refer to our devices. The standard abstraction is a <strong>device mesh</strong>, which you can imagine as arranging all of our devices into a matrix. For example, if we had 8 devices, we might arrange them as a $4 \times 2$ mesh, a $8 \times 1$ mesh, a $2 \times 4$ mesh, and so on. Each dimension of the mesh is called a <strong>device axis</strong>. In this section, we’ll refer to these axes abstractly as $X$ and $Y$. Later, in JAX, we will assign them names like <code class="language-plaintext highlighter-rouge">"data"</code> <code class="language-plaintext highlighter-rouge">"tensor"</code>.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260316200546-480.webp 480w,/assets/img/jax-lm/pasted-image-20260316200546-800.webp 800w,/assets/img/jax-lm/pasted-image-20260316200546-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260316200546.png" class="img-fluid rounded z-depth-1" width="569" height="auto" alt="Pasted image 20260316200546.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <h3 id="communication-collectives">Communication Collectives</h3> <p>When we talk about parallelism, there are also 3 communication operations that will pop up repeatedly. These are the All-Gather, Reduce-Scatter, and All-Reduce.</p> <h5 id="all-gather">All-Gather</h5> <p>During an all-gather operation, each node sends its shard of data to all other nodes, so each device can reconstruct the whole. When we have shards of a tensor on different devices, we can use an all-gather to build a copy of the full tensor on each device.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/all-gather-480.webp 480w,/assets/img/jax-lm/all-gather-800.webp 800w,/assets/img/jax-lm/all-gather-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/all-gather.gif" class="img-fluid rounded z-depth-1" width="613" height="auto" alt="all-gather.gif" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <aside><p><a href="https://jax-ml.github.io/scaling-book/sharding/">Image Source: How to Scale Your Model</a></p></aside> <h5 id="reduce-scatter">Reduce-Scatter</h5> <p>Each device computes the result on their own shard of data, but a “reduce” operation is needed to compute the overall result incorporating all shards. Then, the final result must be re-sharded and distributed back to the corresponding device—this is the “scatter” operation. Reduce-scatter combines these operations into a single step.</p> <p>It does this using a ring structure, illustrated below. Devices are organized in a loop and each device passes a segment to its neighbor and receives one from the other neighbor, repeating for $N-1$ steps. By the end, each device is holding their distinct, fully reduced segment of the data.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/reduce-scatter-480.webp 480w,/assets/img/jax-lm/reduce-scatter-800.webp 800w,/assets/img/jax-lm/reduce-scatter-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/reduce-scatter.gif" class="img-fluid rounded z-depth-1" width="623" height="auto" alt="reduce-scatter.gif" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <aside><p><a href="https://jax-ml.github.io/scaling-book/sharding/">Image Source: How to Scale Your Model</a></p></aside> <h5 id="all-reduce">All-Reduce</h5> <p>An all-reduce combines partial results across devices (often a sum) and leaves every device with a copy of the full result. Equivalently, you can think of it as a reduce-scatter (combine and distribute one shard per device) followed by an all-gather (collect all shards so every device has the complete result).</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260319025335-480.webp 480w,/assets/img/jax-lm/pasted-image-20260319025335-800.webp 800w,/assets/img/jax-lm/pasted-image-20260319025335-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260319025335.png" class="img-fluid rounded z-depth-1" width="609" height="auto" alt="Pasted image 20260319025335.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <aside><p><a href="https://cloud.google.com/blog/products/ai-machine-learning/faster-distributed-training-with-google-clouds-reduction-server">Image Source: Faster Distributed Training with Google Cloud's Reduction Server</a></p></aside> <p>The beauty of JAX is that we never need to implement or code these operations ourself. This is all handled implicitly once we define the device mesh. Now, with preliminaries out of the way, let’s talk about training!</p> <h3 id="standard-training">Standard Training</h3> <p>As a starting point, let’s take a look at the non-distributed case. We can represent this with the following:</p> \[\text{In}[B, D] \cdot_{D} W_{1}[D, F] \cdot_{F} W_{2}[F, D] \to \text{Out}[B, D]\] <p>On a single device, the full input, weights, activations, gradients, and optimizer state all live together. The forward pass computes $\text{Out}$, the backward pass computes gradients $\text{d}W_1$, $\text{d}W_2$, $\text{d}\text{In}$, and the optimizer uses the gradients to update the weights $W_1, W_2$. This is the simplest case, and it gives us a reference point for the distributed strategies below.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260320011439-480.webp 480w,/assets/img/jax-lm/pasted-image-20260320011439-800.webp 800w,/assets/img/jax-lm/pasted-image-20260320011439-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260320011439.png" class="img-fluid rounded z-depth-1" width="1063" height="auto" alt="Pasted image 20260320011439.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <h3 id="data-parallel-dp">Data Parallel (DP)</h3> <p>Data parallelism is the simplest distributed strategy. We shard the input across the batch dimension and keep a full copy of the weights and optimizer state on every device.</p> \[\text{In}[B_X, D] \cdot W_{1}[D, F] \cdot W_{2}[F,D] \rightarrow \text{Out}[B_X, D]\] <p>Each device therefore runs the same forward and backward computation, but on a different slice of the batch. Because every device has the full weights locally, the forward pass requires <em>no communication</em>.</p> <p>However, after the backward pass, each device only has a <em>local</em> contribution to the weight gradients from its own slice of the batch. These are displayed as <code class="language-plaintext highlighter-rouge">Grads1</code> and <code class="language-plaintext highlighter-rouge">Grads2</code> in the diagram below illustrating DP on a 2x1 mesh. To recover the gradient for the full batch, we need to all-reduce those gradients across devices. Each device then receives the same summed gradient <code class="language-plaintext highlighter-rouge">Grads</code> and applies the same update to the same weights, which ensures that our weights are always in sync across devices.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260320011922-480.webp 480w,/assets/img/jax-lm/pasted-image-20260320011922-800.webp 800w,/assets/img/jax-lm/pasted-image-20260320011922-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260320011922.png" class="img-fluid rounded z-depth-1" width="1064" height="auto" alt="Pasted image 20260320011922.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>To summarize, the main ideas of DP are:</p> <ol> <li>Cheap forward pass</li> <li>Replicated model state</li> <li>Synced gradients in the backward pass</li> </ol> <h3 id="fully-sharded-data-parallel-fsdp">Fully-Sharded Data Parallel (FSDP)</h3> <p>You might notice a disadvantage of DP is that each device must hold a complete copy of the weights, gradients, and optimizer state. FSDP tries to solve this by sharding the model state across devices as well:</p> \[\text{In}[B_X, D] \cdot W_{1}[D_{X}, F] \cdot W_{2}[F,D_{X}] \rightarrow \text{Out}[B_X, D]\] <p>Now, each device only permanently stores its own shard of the weights and optimizer state. The tradeoff here is that a device usually can’t perform the forward pass matmul using only its local weight shard, since our weights are sharded across the contracting dimension $D$. As a result, we need to all-gather the parameter shards so that each device can materialize the full weight matrix before the layer runs. After this forward pass computation, the temporary full copy can be discarded.</p> <p>In the backward pass, each device will only have the gradients of the local slice of the input, similar to DP. However, instead of all-reducing the full gradient like DP, we can reduce-scatter the gradient so that each device keeps only the gradient shard corresponding to the parameters it owns. The key idea is that the FSDP lowers the <em>persistent</em> per-device memory footprint, even though each device still temporarily materializes the full layer weights when the layer is executing.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260320012100-480.webp 480w,/assets/img/jax-lm/pasted-image-20260320012100-800.webp 800w,/assets/img/jax-lm/pasted-image-20260320012100-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260320012100.png" class="img-fluid rounded z-depth-1" width="1162" height="auto" alt="Pasted image 20260320012100.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>In this illustration, the crosshatched/dashed weight shards are received from the all-gather and discarded after use.</p> <h3 id="tensor-parallel-tp">Tensor Parallel (TP)</h3> <p>One important observation is that FSDP mostly reduces persistent parameter memory but not necessarily the peak per-device memory during the forward pass. As a result, if our model or sequence length is so large that our activations don’t fit in a single forward pass, FSDP won’t help us much. Tensor parallelism (TP) takes a different approach by <strong>sharding the computation itself</strong>, rather than only sharding stored parameters. Instead of moving weights across devices, we shard the weights and <strong>distribute the computation of a single layer across devices</strong>. Here is how we can represent TP:</p> \[\text{In}[B, D] \cdot_D W_{1}[D, F_{Y}] \cdot_{F} W_{2}[F_{Y},D] \rightarrow \text{Out}[B, D]\] <p>Here, we shard along the feature dimension (denoted by $Y$), so each device holds only a slice of the intermediate activations and weights. In this setup, each device computes only its local contribution to the output. Importantly, since each device can compute the gradient for its local weight shard directly, <strong>no communication is required for weight gradients</strong>.</p> <p>However, communication is still required elsewhere. After the forward pass, each device holds only a partial output from its local weight shards, so we must all-reduce our output across devices to obtain the full output needed for the loss. In the backward pass, the same issue appears for the input gradients: each device computes only a partial contribution to $\text{dIn}$, so we again need an all-reduce to combine these contributions (although input gradients are not shown in the diagram for simplicity).</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260320012754-480.webp 480w,/assets/img/jax-lm/pasted-image-20260320012754-800.webp 800w,/assets/img/jax-lm/pasted-image-20260320012754-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260320012754.png" class="img-fluid rounded z-depth-1" width="1146" height="auto" alt="Pasted image 20260320012754.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <h3 id="fsdp--tp">FSDP + TP</h3> <p>Can we take FSDP and TP a step further? Turns out we can actually combine them by assigning each to different device axes! Along the $X$ axis, we use FSDP to shard model state, while along the $Y$ axis, we use TP to shard the hidden-state computation:</p> \[\text{In}[B_{X}, D_{Y}] \cdot_D W_{1}[D_{X}, F_{Y}] \cdot_{F} W_{2}[F_{Y},D_{X}] \rightarrow \text{Out}[B_{X}, D_{Y}]\] <p>This combination is useful because the two strategies help each other. FSDP shards the batch across the $X$-axis, which reduces the amount of activation data that TP must move. TP shards the weights across $Y$-axis, which reduces the amount of parameter data that FSDP must move. The tradeoff is that we now pay both activation communication and weight communication, so the implementation is more complex and communication costs are higher as well. For a discussion on how to balance FSDP and TP, and when to use it, we recommend reading the related section in the <a href="https://jax-ml.github.io/scaling-book/training/#combining-fsdp-and-tensor-parallelism">How to Scale Your Model</a> book.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/fsdp+tp-480.webp 480w,/assets/img/jax-lm/fsdp+tp-800.webp 800w,/assets/img/jax-lm/fsdp+tp-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/fsdp+tp.jpg" width="100%" height="auto" alt="Pasted image 20260320015521.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>This image might be a bit hard to read due to the size, but you can find the full-size image <a href="https://chuyishang.com/assets/img/jax-lm/fsdp+tp.jpg">here</a>.</p> <p>In this example, we expand the number of devices we use to 4, since we need to use at least 2 devices to perform FSDP and another 2 to perform TP. We also omit the arrows showing the initial splitting of weights, but each device will hold a shard of both the input and the weights, corresponding to its position in the mesh.</p> <p>Before the matmul, TP all-gathers the inputs across $Y$ to get the full data dimension $D$. Then, we can use FSDP to gather our weight matrices across the $X$ axis. Then, we perform a TP-like All-Reduce for our output shards across the $Y$ axis, then finally reduce scatter our gradients across $X$ in an FSDP-like way. One thing to note here is that since we perform an all-reduce of the partial $\text{Out}[B_X, D]$ tensors across the $Y$-axis, each row will actually have the same loss.</p> <h2 id="implementing-distributed-training">Implementing Distributed Training</h2> <p>Now that we have a conceptual picture of distributed training, implementing it in JAX is mostly about expressing the same layouts explicitly. We define a device mesh, specify which tensor dimensions are sharded over which mesh axes, and let JAX/XLA insert the necessary collective communications.</p> <details><summary>How does this compare to PyTorch?</summary> <p>In PyTorch’s distributed training stack, the programming model is similar (we create a device mesh and specify how tensors are sharded) but the underlying execution is different, as PyTorch relies on explicit collective wrappers. For example, FSDP registers pre-forward/post-forward hooks that call <code class="language-plaintext highlighter-rouge">all-gather</code> and <code class="language-plaintext highlighter-rouge">reduce-scatter</code> at precise points, and Tensor Parallel inserts <code class="language-plaintext highlighter-rouge">all-reduce</code> or <code class="language-plaintext highlighter-rouge">reduce-scatter</code> calls around colwise/rowwise parallel layers. In JAX, we don’t write these collectives ourselves since XLA takes care of this automatically. This means that switching from something like DP to FSDP+TP in JAX just requires us to change our partition specs and mesh shape, whereas in PyTorch it would often require swapping wrapper classes and restructuring the training loop.</p> </details> <p>In this section, we will go over:</p> <ol> <li>Initializing our device mesh</li> <li>Sharding annotations</li> <li>How to use sharding annotations to initialize the model</li> <li>How to define our sharding annotations for each parallelism mode</li> </ol> <p>Let’s begin!</p> <h5 id="initializing-our-device-mesh">Initializing our Device Mesh</h5> <p>In the previous section, we referred to our mesh axes as $X$ and $Y$. We can now give these axes names, which are conventionally named <code class="language-plaintext highlighter-rouge">"data"</code> and <code class="language-plaintext highlighter-rouge">"tensor"</code>. The <code class="language-plaintext highlighter-rouge">"data"</code> axis is used for DP/FSDP-style sharding, while the <code class="language-plaintext highlighter-rouge">"tensor"</code> axis is used for TP-style sharding.</p> <p>We can define our device mesh as following:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="n">jax.sharding</span> <span class="kn">import</span> <span class="n">Mesh</span>

<span class="k">def</span> <span class="nf">create_mesh</span><span class="p">(</span><span class="n">mesh_shape</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">mesh_axis_names</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Mesh</span><span class="p">:</span>
    <span class="n">auto_mesh</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">make_mesh</span><span class="p">(</span><span class="nf">tuple</span><span class="p">(</span><span class="n">mesh_shape</span><span class="p">),</span> <span class="nf">tuple</span><span class="p">(</span><span class="n">mesh_axis_names</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">auto_mesh</span>

<span class="n">mesh</span> <span class="o">=</span> <span class="nf">create_mesh</span><span class="p">((</span><span class="mi">4</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">))</span></code></pre></figure> <p>Here, we use the <code class="language-plaintext highlighter-rouge">jax.make_mesh()</code> function to create our <code class="language-plaintext highlighter-rouge">Mesh</code> object. We pass in <code class="language-plaintext highlighter-rouge">(4, 2)</code> as our mesh shape to create a 4x2 device mesh, with the <code class="language-plaintext highlighter-rouge">"data"</code> being the X device axis and <code class="language-plaintext highlighter-rouge">"tensor"</code> being the Y device axis.</p> <h5 id="attaching-sharding-annotations-to-our-model">Attaching Sharding Annotations to our Model</h5> <p>Now that we have our device mesh, our next step is to specify how we want our data and model to be sharded on this mesh. We can use the <code class="language-plaintext highlighter-rouge">PartitionSpec</code> class to do this, which is a tuple abstraction that defines our sharding annotations.</p> <p>For example, after importing <code class="language-plaintext highlighter-rouge">PartitionSpec</code> using <code class="language-plaintext highlighter-rouge">from jax.sharding import PartitionSpec as P</code>, as is customary, we can define:</p> <ul> <li><code class="language-plaintext highlighter-rouge">P("data", None)</code> , which means that we shard the 1st array dimension across the “data” axis and leave the second dimension unsharded</li> <li><code class="language-plaintext highlighter-rouge">P(None, "tensor")</code> means that we shard the 2nd array dimension across the “tensor” axis and leave the first dimension unsharded</li> <li><code class="language-plaintext highlighter-rouge">P(None, None)</code> means that we do not shard either array dimension. Essentially, this means that the tensor is replicated across devices. To make this concrete, if I had a device mesh of shape $4 \times 2$ and a tensor $X[32, 8]$, then <code class="language-plaintext highlighter-rouge">P("data", None)</code> will result in a $X[32/4, 8] = X[8, 8]$ tensor that is replicated across the $Y$ axis. If I use <code class="language-plaintext highlighter-rouge">P("data", "tensor")</code> instead, each device would have a $X[32/4, 8/2] = X[8, 4]$ slice of the original tensor.</li> </ul> <p>The diagram below illustrates several sharding configurations for our 4×2 mesh. Note that the notation <code class="language-plaintext highlighter-rouge">Device A,B,C</code> indicates that slice of the matrix is replicated across devices $A$, $B$, and $C$.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/jax-lm/pasted-image-20260316203617-480.webp 480w,/assets/img/jax-lm/pasted-image-20260316203617-800.webp 800w,/assets/img/jax-lm/pasted-image-20260316203617-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/jax-lm/pasted-image-20260316203617.png" class="img-fluid rounded z-depth-1" width="717" height="auto" alt="Pasted image 20260316203617.png" data-zoomable="" loading="eager" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> <p>For now, you don’t need to worry about what specific <code class="language-plaintext highlighter-rouge">PartitionSpec</code> to use, as it depends on what parallelism strategy we choose.</p> <h5 id="initializing-our-model-with-sharding">Initializing our Model with Sharding</h5> <p>The next step is to attach these sharding annotations to the tensors that we want to shard. In NNX, the easiest way to do this is to wrap the initializer with <code class="language-plaintext highlighter-rouge">nnx.with_partitioning(init_fn, sharding)</code>, which will attach <code class="language-plaintext highlighter-rouge">sharding</code> annotations to the variable created with <code class="language-plaintext highlighter-rouge">init_fn</code>. Typically, these sharding annotations should be passed in at model initialization so that JAX knows how to distribute tensors across devices.</p> <p>Here’s what it might look like in code:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">class</span> <span class="nc">Linear</span><span class="p">(</span><span class="n">nnx</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">in_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">out_features</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
    <span class="n">sharding</span><span class="p">:</span> <span class="n">Sharding</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span> <span class="n">dtype</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">dtype</span> <span class="o">=</span> <span class="n">jnp</span><span class="p">.</span><span class="n">float32</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">std</span> <span class="o">=</span> <span class="p">(</span><span class="mi">2</span> <span class="o">/</span> <span class="p">(</span><span class="n">in_features</span> <span class="o">+</span> <span class="n">out_features</span><span class="p">))</span> <span class="o">**</span> <span class="mf">0.5</span>
        <span class="c1"># We use `nnx.with_partitioning` here!
</span>        <span class="n">init_fn</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nf">with_partitioning</span><span class="p">(</span>
            <span class="n">nnx</span><span class="p">.</span><span class="n">initializers</span><span class="p">.</span><span class="nf">truncated_normal</span><span class="p">(</span><span class="n">stddev</span><span class="o">=</span><span class="n">std</span><span class="p">,</span> <span class="n">lower</span><span class="o">=-</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">,</span> <span class="n">upper</span><span class="o">=</span><span class="mf">3.0</span> <span class="o">*</span> <span class="n">std</span><span class="p">),</span> <span class="n">sharding</span>
        <span class="p">)</span>
        <span class="n">weights_data</span> <span class="o">=</span> <span class="nf">init_fn</span><span class="p">(</span><span class="n">rngs</span><span class="p">.</span><span class="nf">params</span><span class="p">(),</span> <span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">),</span> <span class="n">dtype</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">weights</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nc">Param</span><span class="p">(</span><span class="n">weights_data</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">jnp</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">jnp</span><span class="p">.</span><span class="nf">einsum</span><span class="p">(</span><span class="sh">"</span><span class="s">...i,io-&gt;...o</span><span class="sh">"</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">weights</span><span class="p">.</span><span class="nf">get_value</span><span class="p">())</span></code></pre></figure> <p>The only differences from a standard Linear layer are that:</p> <ol> <li>We wrap the original <code class="language-plaintext highlighter-rouge">nnx.initializers.truncated_normal()</code> call with the <code class="language-plaintext highlighter-rouge">nnx.with_partitioning()</code> function</li> <li>We pass in the <code class="language-plaintext highlighter-rouge">sharding</code> as input The process is the same for other components of the model, all we need to do is just wrap the initializer with <code class="language-plaintext highlighter-rouge">nnx.with_partitioning</code>.</li> </ol> <p>Now that we’ve annotated each component of our model with <code class="language-plaintext highlighter-rouge">nnx.with_partitioning</code>, we need to actually initialize our model so that our tensors are created on the right devices. The cleanest initialization pattern is to create the model inside a mesh context manager and JIT-compile the initialization for performance.</p> <p>Here’s what it might look like in code:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">optax</span>

<span class="nd">@jax.jit</span>
<span class="k">def</span> <span class="nf">init_model_and_optimizer</span><span class="p">(</span><span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span> <span class="n">model_config</span><span class="p">,</span> <span class="n">optimizer_config</span><span class="p">,</span> <span class="n">sharding_config</span><span class="p">):</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nc">TransformerLM</span><span class="p">(</span>
        <span class="n">rngs</span><span class="o">=</span><span class="n">rngs</span><span class="p">,</span>
        <span class="n">model_config</span><span class="o">=</span><span class="n">model_config</span><span class="p">,</span>
        <span class="n">sharding_config</span><span class="o">=</span><span class="n">sharding_config</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">optimizer</span> <span class="o">=</span> <span class="n">nnx</span><span class="p">.</span><span class="nc">Optimizer</span><span class="p">(</span>
        <span class="n">model</span><span class="p">,</span>
        <span class="nf">build_optimizer_transform</span><span class="p">(</span>
            <span class="n">optimizer_config</span><span class="p">,</span>
            <span class="n">gradient_clip</span><span class="o">=</span><span class="n">optimizer_config</span><span class="p">[</span><span class="sh">"</span><span class="s">gradient_clip</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">lr_schedule</span><span class="o">=</span><span class="n">optimizer_config</span><span class="p">[</span><span class="sh">"</span><span class="s">lr_schedule</span><span class="sh">"</span><span class="p">],</span>
        <span class="p">),</span>
        <span class="n">wrt</span><span class="o">=</span><span class="n">nnx</span><span class="p">.</span><span class="n">Param</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">model</span><span class="p">,</span> <span class="n">optimizer</span>

<span class="k">with</span> <span class="n">jax</span><span class="p">.</span><span class="nf">set_mesh</span><span class="p">(</span><span class="n">mesh</span><span class="p">):</span>
    <span class="n">model</span><span class="p">,</span> <span class="n">optimizer</span> <span class="o">=</span> <span class="nf">init_model_and_optimizer</span><span class="p">(</span>
        <span class="n">nnx</span><span class="p">.</span><span class="nc">Rngs</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
        <span class="n">model_config</span><span class="p">,</span>
        <span class="n">optimizer_config</span><span class="p">,</span>
        <span class="n">sharding_config</span><span class="p">,</span>
    <span class="p">)</span></code></pre></figure> <h5 id="sharding-our-data">Sharding our Data</h5> <p>Beyond the model itself, we also need to shard the input data. The approach depends on the parallelism strategy: in TP, each device needs the full batch, while in DP, FSDP, and combined FSDP+TP we split the batch across the data axis so each device processes a different slice.</p> <p>We can capture this logic with a simple helper:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_batch_sharding_for_mode</span><span class="p">(</span><span class="n">mode</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Sharding</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">tp</span><span class="sh">"</span><span class="p">:</span>
        <span class="nf">return </span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>          <span class="c1"># replicate across all devices
</span>    <span class="k">if</span> <span class="n">mode</span> <span class="ow">in</span> <span class="p">(</span><span class="sh">"</span><span class="s">dp</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">fsdp</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">fsdp_tp</span><span class="sh">"</span><span class="p">):</span>
        <span class="nf">return </span><span class="p">(</span><span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>        <span class="c1"># shard batch over data axis, replicate over tensor axis
</span>    <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">"</span><span class="s">invalid mode!</span><span class="sh">"</span><span class="p">)</span>

<span class="n">batch_partition_spec</span> <span class="o">=</span> <span class="nc">P</span><span class="p">(</span><span class="o">*</span><span class="n">model_module</span><span class="p">.</span><span class="nf">get_batch_sharding_for_mode</span><span class="p">(</span><span class="n">sharding_mode</span><span class="p">))</span></code></pre></figure> <details><summary>Why is FSDP+TP not (“data”, “tensor”)?</summary> <p>If you are paying attention, you might notice that in the FSDP+TP formula, $\text{In}$ is sharded as $\text{In}[B_X, D_Y]$. This would equate to <code class="language-plaintext highlighter-rouge">("data", "tensor")</code>, but we instead have <code class="language-plaintext highlighter-rouge">("data", None)</code> here. Why?</p> <p>Recall that the raw batches we load from the dataset are <strong>not</strong> the same tensors as the hidden activations used in the simplified formulas above.</p> <ul> <li>The dataloader returns token IDs and targets, which usually have shape $[B, T]$.</li> <li>The notation $\text{In}[B_X, D_Y]$ in the previous section refers to the <strong>input activations</strong> after embedding into the model dimension $D$ to get $[B, T, D]$.</li> </ul> <p>Because raw token batches don’t have a hidden dimension $D$ yet, even in combined FSDP+TP we only shard them over the <code class="language-plaintext highlighter-rouge">"data"</code> axis and replicate them over the <code class="language-plaintext highlighter-rouge">"tensor"</code> axis. The tensor-axis sharding appears later, once the model has produced hidden states.</p> </details> <p>Then, at data loading time, we use <code class="language-plaintext highlighter-rouge">jax.device_put</code> to place the batch according to this spec:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_batch_from_memmap</span><span class="p">(</span>
    <span class="n">rngs</span><span class="p">:</span> <span class="n">nnx</span><span class="p">.</span><span class="n">Rngs</span><span class="p">,</span>
    <span class="n">dataset</span><span class="p">:</span> <span class="n">DatasetLike</span><span class="p">,</span>
    <span class="n">batch_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
    <span class="n">context_length</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
    <span class="n">mesh</span><span class="p">:</span> <span class="n">Mesh</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="n">batch_partition_spec</span><span class="p">:</span> <span class="n">P</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="n">Array</span><span class="p">,</span> <span class="n">Array</span><span class="p">]:</span>
    <span class="n">inputs</span><span class="p">,</span> <span class="n">targets</span> <span class="o">=</span> <span class="n">model_module</span><span class="p">.</span><span class="nf">get_batch</span><span class="p">(</span>
        <span class="n">rngs</span><span class="p">,</span>
        <span class="n">dataset</span><span class="p">.</span><span class="n">data</span><span class="p">,</span>
        <span class="n">batch_size</span><span class="p">,</span>
        <span class="n">context_length</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Place batch on devices according to the partition spec
</span>    <span class="k">if</span> <span class="n">mesh</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">with</span> <span class="n">jax</span><span class="p">.</span><span class="nf">set_mesh</span><span class="p">(</span><span class="n">mesh</span><span class="p">):</span>
            <span class="n">inputs</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">device_put</span><span class="p">(</span><span class="n">inputs</span><span class="p">,</span> <span class="n">batch_partition_spec</span><span class="p">)</span>
            <span class="n">targets</span> <span class="o">=</span> <span class="n">jax</span><span class="p">.</span><span class="nf">device_put</span><span class="p">(</span><span class="n">targets</span><span class="p">,</span> <span class="n">batch_partition_spec</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">inputs</span><span class="p">,</span> <span class="n">targets</span></code></pre></figure> <h5 id="defining-our-sharding">Defining Our Sharding</h5> <p>Before discussing each parallelism strategy separately, it’d be helpful to first identify the major tensors in the model and the sharding pattern each one will use. Our model contains the following components:</p> <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> </tr> <tr> <td>$\text{LM Head}$</td> <td>$[D, V]$</td> </tr> </tbody> </table> </div> <p>Rather than assigning every tensor a completely separate sharding rule, we can group tensors by the role they play in the computation.</p> <p>In particular, many weight matrices fall into one of two common patterns:</p> <ol> <li>Projection out of the model dimension $D$ into some larger or different dimension. This includes $W_{Q}, W_{K}, W_{V}$ and $W_{1}, W_{3}$, which have shape $[D, *]$. We can refer to this pattern as <code class="language-plaintext highlighter-rouge">dense_in</code></li> <li>Projection back into the model dimension $D$ from some intermediate dimension. This includes $W_{O}$ and $W_{2}$. We will refer to this pattern as <code class="language-plaintext highlighter-rouge">dense_out</code>. This naming is useful because these matrices often share the same partitioning logic under a given parallelism strategy, even if their exact shapes differ.</li> </ol> <details><summary>What about Embedding and LM Head?</summary> <p>You might notice that the Embedding and LM Head (the unembedding matrix) are not grouped into <code class="language-plaintext highlighter-rouge">dense_in</code> and <code class="language-plaintext highlighter-rouge">dense_out</code>. This is because they often play a special role because they involve the vocabulary dimension $V$ instead of hidden dimensions like $D$, $F$, or $N*H$. As a result, they often need different sharding rules since the embedding performs lookup over the vocabulary rows, while the LM head produces vocab logits, which may be sharded across vocab and may require special handling for softmax and cross-entropy. Finally, these 2 layers are often also weight-tied in many language models, which adds an extra layout constraint. For these reasons, it’s cleaner to keep them as separate sharding categories.</p> </details> <p>With this grouping, we can assign each component a <strong>sharding label</strong>:</p> <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> <th>Sharding Name</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> <td><code>batch</code></td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> <td><code>dense_in</code></td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> <td><code>dense_out</code></td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> <td><code>dense_in</code></td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> <td><code>dense_out</code></td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> <td><code>norm</code></td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> <td><code>embedding</code></td> </tr> <tr> <td>$\text{LM Head}$<br/></td> <td>$[D, V]$</td> <td><code>lm_head</code></td> </tr> </tbody> </table> </div> <p>These names only serve as categories for now. In the next step, for each parallelism strategy, we will map each one to an actual <code class="language-plaintext highlighter-rouge">sharding</code> specification: the tuple of strings describing the 2D partition over the tensor’s axes. For 1D tensors such as RMSNorm, the corresponding specification will be 1D rather than 2D.</p> <p>Now, let’s take a look at how we can define what each of these sharding names should be under data parallelism, FSDP, tensor parallelism, and FSDP+TP!</p> <h3 id="shardings-for-each-strategy">Shardings for Each Strategy</h3> <h5 id="dp">DP</h5> <p>In Data Parallel, our basic formula looks something like the following:</p> \[\text{In}[B_X, D] \cdot_D W_{1}[D, F] \cdot_F W_{2}[F, D] \rightarrow \text{Out}[B_X, D]\] <p>We want to split our batches across the $X$, or “data” axis, and replicate our weights otherwise.</p> <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> <th>Sharding Name</th> <th>DP</th> <th>Resulting Shape (per device)</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> <td><code>batch</code></td> <td><code>("data", None)</code></td> <td>$\left[ \frac{B}{X}, D \right]$</td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> <td><code>dense_in</code></td> <td><code>(None, None)</code></td> <td>$[D, N*H]$</td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> <td><code>dense_out</code></td> <td><code>(None, None)</code></td> <td>$[N*H, D]$</td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> <td><code>dense_in</code></td> <td><code>(None, None)</code></td> <td>$[D, F]$</td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> <td><code>dense_out</code></td> <td><code>(None, None)</code></td> <td>$[F, D]$</td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> <td><code>norm</code></td> <td><code>(None)</code></td> <td>$[D]$</td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> <td><code>embedding</code></td> <td><code>(None, None)</code></td> <td>$[V, D]$</td> </tr> <tr> <td>$\text{LM Head}$<br/></td> <td>$[D, V]$</td> <td><code>lm_head</code></td> <td><code>(None, None)</code></td> <td>$[D, V]$</td> </tr> </tbody> </table> </div> <p>Remember that <code class="language-plaintext highlighter-rouge">None</code> replicates the dimension of the tensor across that axis!</p> <p>Here’s what it might look like in code:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_sharding_config_for_mode</span><span class="p">(</span><span class="n">mode</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ShardingConfig</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">dp</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">ShardingConfig</span><span class="p">(</span>
            <span class="n">dense_in</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">dense_out</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">embedding</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">lm_head</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">norm</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,),</span>
        <span class="p">)</span>
    <span class="bp">...</span></code></pre></figure> <p>And that’s all we need to do! Simple, right?</p> <h5 id="fsdp">FSDP</h5> <p>In FSDP, we have the following:</p> \[\text{In}[B_X, D] \cdot_D W_{1}[D_X, F] \cdot_F W_{2}[F, D_X] \rightarrow \text{Out}[B_X, D]\] <p>We want to shard both our input activations and weights along the data axis.</p> <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> <th>Sharding Name</th> <th>FSDP</th> <th>Resulting Shape (per device)</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> <td><code>batch</code></td> <td><code>("data", None)</code></td> <td>$\left[ \frac{B}{X}, D \right]$</td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> <td><code>dense_in</code></td> <td><code>("data", None)</code></td> <td>$\left[ \frac{D}{X}, N*H \right]$</td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> <td><code>dense_out</code></td> <td><code>(None, "data")</code></td> <td>$\left[ N*H, \frac{D}{X} \right]$</td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> <td><code>dense_in</code></td> <td><code>("data", None)</code></td> <td>$\left[ \frac{D}{X}, F \right]$</td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> <td><code>dense_out</code></td> <td><code>(None, "data")</code></td> <td>$\left[ F, \frac{D}{X} \right]$</td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> <td><code>norm</code></td> <td><code>(None)</code></td> <td>$[D]$</td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> <td><code>embedding</code></td> <td><code>(None, None)</code></td> <td>$[V, D]$</td> </tr> <tr> <td>$\text{LM Head}$<br/></td> <td>$[D, V]$</td> <td><code>lm_head</code></td> <td><code>(None, None)</code></td> <td>$[D, V]$</td> </tr> </tbody> </table> </div> <p>And here’s what it would look like in code:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_sharding_config_for_mode</span><span class="p">(</span><span class="n">mode</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ShardingConfig</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">fsdp</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">ShardingConfig</span><span class="p">(</span>
            <span class="n">dense_in</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">dense_out</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">embedding</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">lm_head</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">norm</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,),</span>
        <span class="p">)</span>
    <span class="bp">...</span></code></pre></figure> <h4 id="tp">TP</h4> <p>In TP, we have the following:</p> \[\text{In}[B, D] \cdot_D W_{1}[D, F_Y] \cdot_F W_{2}[F_Y, D] \rightarrow \text{Out}[B, D]\] <p>where we want to shard our weights along the “tensor” axis.</p> <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> <th>Sharding Name</th> <th>TP</th> <th>Resulting Shape (per device)</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> <td><code>batch</code></td> <td><code>(None, None)</code></td> <td>$[B, D]$</td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> <td><code>dense_in</code></td> <td><code>(None, "tensor")</code></td> <td>$\left[ D, \frac{N}{Y}*H \right]$</td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> <td><code>dense_out</code></td> <td><code>("tensor", None)</code></td> <td>$\left[ \frac{N}{Y}*H, D \right]$</td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> <td><code>dense_in</code></td> <td><code>(None, "tensor")</code></td> <td>$\left[ D, \frac{F}{Y} \right]$</td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> <td><code>dense_out</code></td> <td><code>("tensor", None)</code></td> <td>$\left[ \frac{F}{Y}, D \right]$</td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> <td><code>norm</code></td> <td><code>(None)</code></td> <td>$[D]$</td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> <td><code>embedding</code></td> <td><code>(None, "tensor")</code></td> <td>$\left[ V, \frac{D}{Y} \right]$</td> </tr> <tr> <td>$\text{LM Head}$<br/></td> <td>$[D, V]$</td> <td><code>lm_head</code></td> <td><code>("tensor", None)</code></td> <td>$\left[ \frac{D}{Y}, V \right]$</td> </tr> </tbody> </table> </div> <p>And here’s what the code would look like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_sharding_config_for_mode</span><span class="p">(</span><span class="n">mode</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ShardingConfig</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">tp</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">ShardingConfig</span><span class="p">(</span>
            <span class="n">dense_in</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">dense_out</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">embedding</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">lm_head</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">norm</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,),</span>
        <span class="p">)</span>
    <span class="bp">...</span></code></pre></figure> <h5 id="fsdptp">FSDP+TP</h5> <p>Finally, in FSDP+TP, we have the following:</p> \[\text{In}[B_X, D_Y] \cdot_D W_{1}[D_X, F_Y] \cdot_F W_{2}[F_Y, D_X] \rightarrow \text{Out}[B_X, D_Y]\] <div class="table-responsive"> <table class="table table-sm table-bordered"> <thead> <tr> <th>Component</th> <th>Shape</th> <th>Sharding Name</th> <th>FSDP+TP</th> <th>Resulting Shape (per device)</th> </tr> </thead> <tbody> <tr> <td>$\text{Input}$</td> <td>$[B, D]$</td> <td><code>batch</code></td> <td><code>("data", None)</code></td> <td>$\left[ \frac{B}{X}, D \right]$</td> </tr> <tr> <td>$W_{Q}, W_{K}, W_{V}$</td> <td>$[D, N*H]$</td> <td><code>dense_in</code></td> <td><code>("data", "tensor")</code></td> <td>$\left[ \frac{D}{X}, \frac{N}{Y}*H \right]$</td> </tr> <tr> <td>$W_{O}$</td> <td>$[N*H, D]$</td> <td><code>dense_out</code></td> <td><code>("tensor", "data")</code></td> <td>$\left[ \frac{N}{Y}*H, \frac{D}{X} \right]$</td> </tr> <tr> <td>$W_{1}, W_{3}$</td> <td>$[D, F]$</td> <td><code>dense_in</code></td> <td><code>("data", "tensor")</code></td> <td>$\left[ \frac{D}{X}, \frac{F}{Y} \right]$</td> </tr> <tr> <td>$W_{2}$</td> <td>$[F, D]$</td> <td><code>dense_out</code></td> <td><code>("tensor", "data")</code></td> <td>$\left[ \frac{F}{Y}, \frac{D}{X} \right]$</td> </tr> <tr> <td>$\text{RMSNorm}$</td> <td>$[D]$</td> <td><code>norm</code></td> <td><code>(None)</code></td> <td>$[D]$</td> </tr> <tr> <td>$\text{Embedding}$</td> <td>$[V, D]$</td> <td><code>embedding</code></td> <td><code>(None, "tensor")</code></td> <td>$\left[ V, \frac{D}{Y} \right]$</td> </tr> <tr> <td>$\text{LM Head}$<br/></td> <td>$[D, V]$</td> <td><code>lm_head</code></td> <td><code>("tensor", None)</code></td> <td>$\left[ \frac{D}{Y}, V \right]$</td> </tr> </tbody> </table> </div> <p>And here’s what the code looks like:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">get_sharding_config_for_mode</span><span class="p">(</span><span class="n">mode</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ShardingConfig</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">fsdp_tp</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="nc">ShardingConfig</span><span class="p">(</span>
            <span class="n">dense_in</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">dense_out</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">data</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">embedding</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">),</span>
            <span class="n">lm_head</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">tensor</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="n">norm</span><span class="o">=</span><span class="p">(</span><span class="bp">None</span><span class="p">,),</span>
        <span class="p">)</span>
    <span class="bp">...</span></code></pre></figure> <p>As you can see, implementing distributed training at this point is mostly plumbing: 1) initialize the model inside a mesh scope, 2) place the external token batch with the appropriate batch sharding, and 3) let JAX/XLA infer the collective communication implied by those layouts. That’s the main appeal of JAX’s sharding model—rather than manually writing all-gathers and reduce-scatters at the Python level, we can simply describe the layout we want and let the compiler handle these communications implicitly.</p> <p>We also provide a PyTorch implementation of these distributed training methods (DP, TP, FSDP, FSDP+TP) for comparison. You can find it <a href="https://github.com/chuyishang/jax-lm/tree/main/pytorch_impl/distributed">here</a>. Note how much more complex it is!</p> <h2 id="conclusion">Conclusion</h2> <p>Hopefully this was helpful! In this blog post, we went over some of the basic JAX/NNX concepts, demonstrated how we can build a language model from scratch using NNX, and discussed how we can implement distributed training.</p> <p>Now, you can try applying your knowledge with our <a href="https://github.com/chuyishang/jax-lm/tree/assignment">assignment</a> or refer to our <a href="https://github.com/chuyishang/jax-lm/">reference implementations</a>.</p> <p>This blog is still a work in progress! Some of the things that we plan to release soon:</p> <ul> <li>Introduction to Roofline Analysis</li> <li>Empirical Results and Comparison</li> <li>Mixture of Experts (MoE) implementation</li> <li>Additional LLM Tricks</li> </ul> <p>We aim for this to be an easily customizable and useful resource for tinkering around in JAX.</p> <hr/> <h3 id="acknowledgements">Acknowledgements</h3> <p>Thank you to Henry Ko for providing helpful feedback on this blog post! We would also like to thank Machine Learning @ Berkeley for providing compute resources and feedback on an early version.</p> <h3 id="references">References</h3> <p>Austin et al., “How to Scale Your Model”, Google DeepMind, online, 2025.</p> <p>Hashimoto et al., “CS336: Language Modeling from Scratch”, Stanford NLP, online, 2026.</p> <p>https://pytorch.org/blog/overview-of-pytorch-autograd-engine/</p> <p>https://docs.jax.dev/en/latest/</p> <p>https://flax.readthedocs.io/en/stable/</p>]]></content><author><name>Chuyi Shang</name></author><category term="distill"/><category term="formatting"/><summary type="html"><![CDATA[A guide to language modelling and distributed training from scratch in JAX]]></summary></entry></feed>