<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marshud]]></title><description><![CDATA[Software Engineering, Python Programming, Machine Learning with JAX]]></description><link>https://marshud.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 06 Sep 2026 03:17:42 GMT</lastBuildDate><atom:link href="https://marshud.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[High Level Neural Network Modeling with Flax]]></title><description><![CDATA[In our previous article, we broke down why JAX requires explicit state handling. We saw that wrapping our parameters, optimizer momentum, and PRNG keys into a clean carry pattern lets us use jax.jit, ]]></description><link>https://marshud.dev/high-level-neural-network-modeling-with-flax</link><guid isPermaLink="true">https://marshud.dev/high-level-neural-network-modeling-with-flax</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:54:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/4476fc89-9873-45af-a9ed-59c1223286b5.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our <a href="https://marshud.dev/functional-state-management-and-low-level-control-flow-in-jax">previous article</a>, we broke down why JAX requires explicit state handling. We saw that wrapping our parameters, optimizer momentum, and PRNG keys into a clean carry pattern lets us use <code>jax.jit</code>, <code>jax.grad</code>, and <code>jax.lax.scan</code> without running into hidden side effects.</p>
<p>Manually writing parameter dictionaries (<code>{'w': ..., 'b': ...}</code>) and manually propagating shapes works fine for a two-layer linear regression. But as soon as you build multi-layer perceptrons, convolutional networks or architectures with batch normalization and dropout, handwriting nested dictionary updates becomes tedious and brittle and this is where Flax comes in.</p>
<p>Flax is a high-level neural network library built natively for JAX. It gives you the clean modular ergonomics of object-oriented layer definitions while staying 100% faithful to JAX's functional and stateless philosophy under the hood.</p>
<p>In this post, we will explore how Flax bridges the gap between ergonomic layer design and pure functional execution, how parameter initialization works, and how to use Flax's <code>TrainState</code> abstraction to run a complete training workflow.</p>
<h2>Modules as Blueprints, Not State Containers</h2>
<p>If you are coming from PyTorch's <code>nn.Module</code>, you are used to a class that holds both the computational architecture and the actual weight tensors inside <code>self</code> as expressed in the code example below</p>
<pre><code class="language-python"># In PyTorch Architecture and State are tightly coupled in the object
class PyTorchMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 128)  # Weights live inside this attribute
        
    def forward(self, x):
        return torch.relu(self.fc1(x))
</code></pre>
<p>In Flax, a <code>linen.Module</code> does <strong>not</strong> store your parameter tensors inside the instance. Instead, the module is purely a <strong>computational blueprint</strong>. It defines how inputs should be transformed when parameters are provided.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/95848d72-cde1-4ff6-8711-53811cc01f75.png" alt="" style="display:block;margin:0 auto" />

<p>A Flax module gives you two primary methods:</p>
<ul>
<li><p><code>module.init(rng_key, dummy_input)</code> which traces the flax module blueprint using a dummy input to allocate and return an immutable dictionary of initialized parameters.</p>
</li>
<li><p><code>module.apply(params, input)</code> which takes an explicit parameter dictionary and an input array, runs the forward pass, and returns the result.</p>
</li>
</ul>
<h2>Defining Neural Networks with <code>flax.linen</code></h2>
<p>Let's consider a Multi-Layer Perceptron (MLP) with dropout and batch normalization to see how Flax structures layer definitions</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp
from flax import linen as nn

class ClassifierMLP(nn.Module):
    hidden_dim: int
    num_classes: int
    dropout_rate: float = 0.1

    @nn.compact
    def __call__(self, x:jnp.ndarray, training: bool = False) -&gt; jnp.ndarray:
        """
        Forward pass blue print
        """
        # Layer 1
        # Dense -&gt; BatchNorm -&gt; Relu -&gt; Dropout
        x = nn.Dense(features=self.hidden_dim)(x)
        x = nn.BatchNorm(use_batch_average=not training)(x)
        x = nn.relu(x)
        x = nn.Dropout(rate=self.dropout_rate, deterministic=not training)(x)

        # Layer 2
        # Output Projection
        logits = nn.Dense(features=self.num_classes)(x)
        return logits
        
</code></pre>
<p>Before explaining what's going on in the code snippet above, I will draw your attention to the decorator <code>@nn.compact</code>. In standard Python classes, you define the child layers in the <code>__init__</code> and use them in <code>forward</code>. However, in Flax, the <code>@nn.compact</code> decorator lets you define child layers inside the <code>__call__</code> method right where they are used.</p>
<p>As for how the layers are working, the <code>nn.Dense(features=self.hidden_dim)(x)</code> linearly projects the input <code>x</code> to the hidden dimension. The <code>nn.BatchNorm(use_running_average=not training)(x)</code> normalizes activations accross the batch. During training when <code>training=True</code>, it calculates the batch statistics and updates running averages. During inference when <code>training=False</code>, it uses the stored running averages. The nnx.relu(x) function applies the standard ReLU activation, <code>max(0, x)</code>. The <code>nn.Dropout(rate=self.dropout_rate, deterministic=not training)(x)</code> randomly drops units to prevent overfitting and finally the nn.Dense(features=self.num_classes)(x) linearly maps hidden features to the final class logits.</p>
<h2>Initializing and Applying Modules</h2>
<p>Because the module is just a blue print, we instantiate the class without passing any tensor data, then initialize its parameters using a PRNG key and dummy input shape.</p>
<pre><code class="language-python"># Instantiate the blue print
model = ClassifierMLP(hidden_dim=64, num_classes=10, dropout_rate=0.2)

# Create PRNG keys for parameter init and dropout
main_key = jax.random.key(420)
main_key, init_key, dropout_key = jax.random.split(main_key, 3)

# Create dummy batch matching input shape, batch_size=4, input_features=32
dummy_x = jnp.ones((4, 32))

# Initialize parameters and collection variables
variables = model.init(
    {'params': init_key, 'dropout': dropout_key},
    dummy_x,
    training=False
)

# Inspect the returned pytrees
print("Initialized Variable Collections:", variables.keys() )
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Initialized Variable Collections: dict_keys(['params', 'batch_stats'])
</code></pre>
<p>Notice that the model.init returned a nested dictionary containing two collections i.e. <code>'params'</code>, the learnable weights and biases and <code>'batch_stats'</code>, the running mean and variance tracked by <code>BatchNorm</code>.</p>
<p>To run an inference pass, we pass those variables back to <code>model.apply</code>. Please note that this is a pure and stateless pass.</p>
<pre><code class="language-python"># Forward inference pass 
logits = model.apply(
    variables,
    dummy_x,
    training=False
)

print("Inference Output Shape: ", logits.shape)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Inference Output Shape:  (4, 10)
</code></pre>
<h2>Structuring the Training Loop using <code>TrainState</code></h2>
<p>In our previous article, we managed our parameter dictionary and momentum state manually inside a custom carry tuple. In real applications, tracking parameters, optimizer states, batch stats and update rules manually creates boilerplate.</p>
<p>Flax solves this with <code>flax.training.train_state.TrainState</code>.</p>
<p><code>TrainState</code> is a clean dataclass that bundles your model parameters, optimizer step and gradient update logic into a single immutable JAX PyTree.</p>
<p>Let's integrated Flax with Optax, a standard functional optimization library for JAX to construct a complete training step.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp
from flax import linen as nn
from flax.training import train_state
import optax 
from typing import Any

# Define custom TrainState to hold BatchNorm tracking statistics
class CustomTrainState(train_state.TrainState):
    batch_stats: Any

# Setup model, opitimizer and initial state
model = ClassifierMLP(hidden_dim=64, num_classes=10, dropout_rate=0.1)

init_rng = jax.random.key(42)
init_rng, init_param_key, init_drop_key = jax.random.split(init_rng, 3)

dummy_inputs = jnp.ones((8, 32))
init_variables = model.init(
    {'params': init_param_key, 'dropout': init_drop_key},
    dummy_inputs,
    training=False
)

# Setup Adam optimizer using Optax with clipping to avoid exploding gradients 
tx = optax.chain(
    optax.adam(learning_rate=1e-3),
    optax.clip_by_global_norm(1.0) 
)

# Bundle into TrainState
state = CustomTrainState.create(
    apply_fn=model.apply,
    params=init_variables['params'],
    batch_stats=init_variables['batch_stats'],
    tx=tx,
)
</code></pre>
<h3>The JIT-Compiled Training Step</h3>
<p>We now write a pure JIT-compiled <code>train_step</code>. It takes the current <code>state</code>, a data batch, and a PRNG key for dropout, performs the forward/backward pass, updates running statistics, and returns the updated <code>state</code>.</p>
<pre><code class="language-python">@jax.jit
def train_step(
    state: CustomTrainState,
    batch: tuple[jnp.ndarray, jnp.ndarray],
    dropout_key: jax.Array
) -&gt; tuple[CustomTrainState, jnp.ndarray]:
    """
    Executes a forward + backward + optimizer update step.
    """
    images, labels = batch

    def loss_fn(params):
        # run forward pass passing both params and batch_stats
        # mutable=['batch_stats'] tells Flax to record updated running stats
        logits, mutated_vars = state.apply_fn(
            {'params': params, 'batch_stats': state.batch_stats},
            images,
            training=True,
            rngs={'dropout': dropout_key},
            mutable=['batch_stats']            
        )

        # Cross-entropy loss
        one_hot_labels = jax.nn.one_hot(labels, num_classes=10)
        loss = optax.softmax_cross_entropy(logits=logits, labels=one_hot_labels).mean()

        return loss, (logits, mutated_vars['batch_stats'])

    # Compute loss and gradients with respect to params
    grad_fn = jax.value_and_grad(loss_fn, has_aux=True)
    (loss_val, (logits, new_batch_stats)), grads = grad_fn(state.params)

    # Optax optimizer step that updates params and opt_state inside TrainState
    new_state = state.apply_gradients(
        grads=grads,
        batch_stats=new_batch_stats
    )

    return new_state, loss_val
</code></pre>
<h3>Running the Training Loop</h3>
<p>With <code>train_step</code> compiled, our execution loop remains clean, readable and fast</p>
<pre><code class="language-python"># Generate a synthetic dataset for demo purposes
dataset_size = 64
batch_size = 8
features = 32

rng_data = jax.random.key(4747)
x_data = jax.random.normal(rng_data, (dataset_size, features))
y_data = jax.random.randint(rng_data, (dataset_size,), minval=0, maxval=10)

step_key = jax.random.key(7)

# Run mini-batch training updates
num_epochs=3
for epoch in range(num_epochs):
    epochs_loss = 0.0
    num_batches = dataset_size

    for i in range(num_batches):
        step_key, subkey = jax.random.split(step_key)
        batch = (
            x_data[i * batch_size : (i + 1) * batch_size],
            y_data[i * batch_size : (i + 1) * batch_size]
        )
        # State in -&gt; State out
        state, loss = train_step(state, batch, subkey)
        epochs_loss += loss
    print(f"Epoch {epoch + 1} | Average Loss: {epoch_loss / num_batches:.4f}")
</code></pre>
<p>Outputs</p>
<pre><code class="language-plaintext">Epoch 1 | Average Loss: 2.7051
Epoch 2 | Average Loss: 2.5244
Epoch 3 | Average Loss: 2.4118
</code></pre>
<p>Notice how Flax retains the raw performance and clarity of JAX like how <code>state</code> remains an explicit and immutable object. The <code>train_step</code> is placed and compiled via <code>@jax.jit</code> without hidden side effects. Auxiliary states like batch_stats are tracked and updated deterministically without mutating hidden class properties.</p>
<h3>All in all</h3>
<ul>
<li><p>Flax <code>linen.Module</code> instances describe computation rather than storing tensor state. Parameters live separately in dictionaries returned by <code>.init()</code>.</p>
</li>
<li><p>Use <code>@nn.compact</code> to define layers and operations directly in the execution path, allowing Flax to automatically manage shape inference.</p>
</li>
<li><p>Flax's <code>TrainState</code> brings together parameters, optimizer states, step counts, and custom statistics into a single immutable PyTree.</p>
</li>
<li><p>Flax structures its APIs to ensure your training steps integrate directly with <code>jax.jit</code>, <code>jax.grad</code>, and Optax optimization chains.</p>
</li>
</ul>
<p>Please note that the code above is better explored in a Jupyter Notebook.</p>
<p>See you guys in the next one.</p>
]]></content:encoded></item><item><title><![CDATA[Functional State Management and Low-Level Control Flow in JAX]]></title><description><![CDATA[Well be back guys, in our previous article on managing PRNG keys, we explored how JAX abandons the conventional hidden random state in favor of stateless random number generation. We learned that to g]]></description><link>https://marshud.dev/functional-state-management-and-low-level-control-flow-in-jax</link><guid isPermaLink="true">https://marshud.dev/functional-state-management-and-low-level-control-flow-in-jax</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Tue, 11 Aug 2026 16:46:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/53e7e232-ce9b-4da3-b68e-ff0f70b272e9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Well be back guys, in our previous article on managing PRNG keys, we explored how JAX abandons the conventional hidden random state in favor of stateless random number generation. We learned that to get deterministic sequences of random numbers, we must pass keys into functions and split them as state updates.</p>
<p>We now zoom out and tackle a broader design pattern that governs all execution in JAX as stated in topic.</p>
<p>For those familiar with Object-oriented ML frameworks like PyTorch or Tensorflow, the mental model of state likely revolves around object attributes that get modified in-place during computation. In JAX, functional purity means that hidden mutations are fundamentally incompatible with compilation with <code>jax.jit</code> and automatic differentiation with <code>jax.grad</code></p>
<p>In this article, we'll cover how state works in pure functional programming, how to express iterations and conditions using jax.lax primitives and hopefully build a training loop that handles parameters, optimizer state and PRNG keys in a unified state.</p>
<h3>Why state must be handled functionally</h3>
<p>In traditional code, functions often carry side effects. They may read or mutate global variables, alter internal class properties or perform inplace updates like <code>x+=1</code>. Consider the sample code below</p>
<pre><code class="language-python">class Counter: 
    def __init__(self):
        self.count=0
    
    def increment(self, amount):
        self.count += amount # This mutates the internal state
        return self.count
</code></pre>
<p>As expressed in earlier articles about how JAX transformations like <code>jax.jit</code> and <code>jax.grad</code> work using tracers, if a function relies on side effects or internal mutations, the trace will fail to capture the changes that happen outside the return values or hardcoded values during a single trace.</p>
<p>To avoid all that, JAX requires pure functions that if given the same input, they always produce the same output and above all have zero side effects on state or mutation.</p>
<h3>The Explicit State / Carry Pattern</h3>
<p>To update parameters, track stats or counters in JAX, we use the Carry Pattern. Instead of updating an object in-place, your function accepts the state as an argument(the carry), performs the required updates using immutable operations and returns the newly computed state alongside any results.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/724096c7-b87a-40b7-8ba9-1409a4c9409b.png" alt="" style="display:block;margin:0 auto" />

<p>Consider the Counter example code below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

# State management using a pure function
def increment(state: jnp.ndarray, amount: float) -&gt; tuple[jnp.ndarray, jnp.ndarray]
    """This pure function takes the current state, computes the next state and returns both the new state and output """
    new_state = state + amount
    return new_state, new_state

initial_state = jnp.array(0.0)

# Step 1
state, output1 = increment(initial_state, 1.0)
print(f"Output: {output1}, New State: {state}")

# Step 2
new_state, output2 = increment(state, 2.5)
print(f"Output: {output2}, New State: {new_state}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Output: 1.0, New State: 1.0
Output: 3.5, New State: 3.5
</code></pre>
<p>By making state flow through arguments and return values, JAX compilers can optimize operations, parallelize computation across hardware targets and safely differentiate across multi-step algorithms.</p>
<h2>Low-Level Control flow primitives with <code>jax.lax</code></h2>
<p>Writing Python conventional flow statements like <code>for</code> loops or <code>if/else</code> statements inside <code>jax.jit</code> compiled functions presents a unique set of challenges. Forexample, a standard <code>for</code> loop with 1000 iterations creates a Jaxpr computational graph with 1000 repeated blocks leading to high compilation overhead. While standard if statements evaluated on tracer values trigger a <code>ConcretizationTypeError</code> because JAX cannot determine truth values of abstract shapes during tracing. To write compiled loops and conditionals that scale efficiently, JAX provides primitives inside the low-level <code>jax.lax</code> module.</p>
<h3>Sequential Iteration with <code>jax.lax.scan</code></h3>
<p><code>jax.lax.scan</code> is JAX's workhorse primitive for sequential and recurrent processing. It executes a loop where an accumulated state, the carry, is passed from step to step while optionally consuming a sequence of inputs and outputting a sequence of results.</p>
<h3>Syntax</h3>
<pre><code class="language-python">final_carry, stacked_outputs = jax.lax.scan(
    f,
    init, 
    xs,
    Length=None
)
</code></pre>
<p>where</p>
<p><code>f</code> is the expected step function <code>f(carry, x) -&gt; (next_carry, y)</code> where the carry is the currently accumulated state and x is the single slice of the current iteration step <code>xs[i]</code> and returns a two-element tuple <code>next_carry</code> which is the updated step <code>i+1</code> and <code>y</code>, the perstep output you may save.</p>
<p><code>init</code> is the starting carry value before the first iteration begins. This can be a scalar, JAX Array or a PyTree.</p>
<p><code>xs</code> being the sequence of inputs to iterate through.</p>
<p>The return values are the <code>final_carry</code> which contains the updated state after processing the last element of <code>xs</code> with the same structure and shape as <code>init</code> and <code>stacked_outputs</code> containing all per-step outputs(y) concatenated together along axis 0 with a shape (<code>loop_length</code>, <code>*shape_of_y</code>)</p>
<h3>Consider running a cumulative sum</h3>
<p>Let's use <code>lax.scan</code> to compute a running sum across an array while tracking the current aggregate state.</p>
<pre><code class="language-python">import jax 
import jax.numpy as jnp

# step fn
def scan_running_sum(carry: jnp.ndarray, x: jnp.ndarray) -&gt; tuple[jnp.ndarray, jnp.ndarray]:
    """
    carry: running total so far
    x: individual element from input sequence
    """
    new_carry = carry + x
    output = new_carry
    return new_carry, output

xs = jnp.array([1.0, 2.0, 3.0, 4.0, 5.0])
init_state = jnp.array(0.0)

final_carry, running_totals = jax.lax.scan(scan_running_sum, init_state, xs)

print(f"Inputs: {xs}")
print(f"Final carry: {final_carry}") 
print(f"Running Totals: {running_totals}") 
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Inputs: [1. 2. 3. 4. 5.]
Final carry: 15.0
Running Totals: [ 1.  3.  6. 10. 15.]
</code></pre>
<h3>Consider a Recurrent Neural Network (RNN) Step</h3>
<p><code>lax.scan</code> is ideal for sequential models like Recurrent Neural Networks (RNNs) or Transformers processing temporal sequences because it compiles the iteration into a single optimized loop rather than thousands of unrolled nodes. Take an example of the codebase below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def rnn_cell(hidden_state: jnp.ndarray, x_t: jnp.ndarray) -&gt; tuple[jnp.ndarray, jnp.ndarray]:
  """
  step: h_t = tanh(W_h * h_{t-1} + W_x * x_t)
  fixed identity-like weights are used for demo purposes
  """
  W_h = jnp.array([[0.8, 0.1], [0.0, 0.8]])
  W_x = jnp.array([[1.0, 0.0], [0.0, 1.0]])

  next_hidden = jnp.tanh(jnp.dot(hidden_state, W_h) + jnp.dot(x_t, W_x))
  return next_hidden, next_hidden

sequence_inputs = jnp.array([
    [1.0, 0.5],
    [0.2, 0.8],
    [0.0, 1.0],
    [-0.5, 0.0]
])

init_hidden = jnp.zeros((2,))

# Trace and execute sequence via scan
final_hidden, hidden_states_history = jax.lax.scan(rnn_cell, init_hidden, sequence_inputs)

print(f"Sequence Length: {sequence_inputs.shape[0]}")
print(f"Hidden States Shape over time: {hidden_states_history.shape}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Sequence Length: 4
Hidden States Shape over time: (4, 2)
</code></pre>
<h2>Conditional Execution with <code>jax.lax.cond</code></h2>
<p>This is the JIT-compatible equivalent of an <code>if-else</code> statement. This is so because in a typical Python if-else, execution is dynamic based on values evaluated at runtime. Since JAX's <code>jax.jit</code> needs to build a static computational graph, jax.lad.cond allows JAX to trace both branches during compilation and execute only the appropriate branch based on a runtime boolean array.</p>
<h3>Syntax</h3>
<pre><code class="language-python">result = jax.lax.cond(
    pred,
    true_fn,
    false_fn,
    operand
)
</code></pre>
<p>where</p>
<p><code>pred</code> is a boolean scalar array like <code>jnp.array(True)</code> or a conditional statement like <code>x&gt;0</code>. It cannot be a multi-element array.</p>
<p><code>true_fn</code> and <code>false_fn</code> are the signature alignments that must accept a single argument passed via the <code>operand</code>. These are the independent paths based on the evaluation of the <code>pred</code>. If the functions must take multiple arguments, pass them as a tuple. These must also return outputs with identical shapes and dtypes as required by the static compilation graph to keep a fixed shape.</p>
<p><code>operand</code> is the data to be passed either to the <code>true_fn</code> or <code>false_fn</code></p>
<h3>Consider the gradient clipping example below</h3>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def threshold_activation(x: jnp.ndarray) -&gt; jnp.ndarray:
    """Applies custom piece scaling"""
    pred = jnp.mean(x) &gt; 0.0
    
    true_fn = lambda val: val * 2.0
    false_fn = lambda val: val * 0.1

    return jax.lax.cond(pred, true_fn, false_fn, x)

compiled_activation = jax.jit(threshold_activation)

pos_data = jnp.array([0.5, 1.5, 2.0])
neg_data = jnp.array([-1.0, -0.5, -0.2])

print("Positive batch result:", compiled_activation(pos_data))
print("Negative batch result:", compiled_activation(neg_data))
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Positive batch result: [1. 3. 4.]
Negative batch result: [-0.1  -0.05 -0.02]
</code></pre>
<h2>Dynamic Iterations with <code>jax.lax.while_loop</code></h2>
<p>During compilation, a standard <code>while</code> loop gets unrolled and requires a fixed compile-time iteration count, while a <code>jax.lax.while_loop</code> allows you to run dynamic condition-based loops without unrolling the computation graph.</p>
<h3>Syntax</h3>
<pre><code class="language-python">final_val = jax.lax.while_loop(
    cond_func,
    body_func,
    init_val
)
</code></pre>
<p>where</p>
<p><code>cond_func</code> takes the current loop state <code>value</code> starting with the <code>init_val</code> and returns a boolean scalar array i.e. <code>True</code> to keep looping and <code>False</code> to stop.</p>
<p><code>body_func</code> takes the current loop state <code>value</code> and returns an updated state of the exact same structure, shape and data type.</p>
<p><code>init_val</code> is the starting state object usually a PyTree passed into a loop.</p>
<h3>Consider the code example to find a convergence below</h3>
<pre><code class="language-python">import jax
import jax.numpy as jnp

@jax.jit
def find_next_power_of_two(target):
    # loop state carries 
    # (current_value, step_count)
    init_val = (1, 0)

    # checks if current_value &lt; target
    def cond_func(val):
        current_val, count = val
        return current_val &lt; target

    # doubles current_value and increments count 
    def body_func(val):
        current_val, count = val
        return (current_val * 2, count + 1)

    final_val, total_steps = jax.lax.while_loop(cond_func, body_func, init_val)
    return final_val, total_steps

print(find_next_power_of_two(50)) #(64, 6)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">(Array(64, dtype=int32, weak_type=True), Array(6, dtype=int32, weak_type=True))
</code></pre>
<p>Note: Because the loop count isn't fixed, reverse-mode automatic differentiation (<code>jax.grad</code>) cannot automatically differentiate through a while_loop without additional bounds.</p>
<h2>Combining functional state management with transformations</h2>
<p>The power of JAX reveals itself when combining these functional state principles with core transformations i.e <code>jax.jit</code> and <code>jax.grad</code>. Because state updates are represented explicitly as inputs and outputs, wrapping state-modifying functions with JAX transformations is seamless and free of hidden side effects.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp
from typing import Dict, Tuple, Any

def compute_loss(params: Dict[str, jnp.ndarray], x: jnp.ndarray, y: jnp.ndarray, key: jax.Array) -&gt; jnp.ndarray:
  """ Compute mean squared error with stochastic noise injection """
  # Inject stochasticity using PRNG key
  noise = jax.random.normal(key, shape=x.shape) * 0.01
  x_noisy = x + noise

  # model forward pass
  predictions = jnp.dot(x_noisy, params['w']) + params['b']
  loss = jnp.mean((predictions - y) ** 2)
  return loss

def train_step(
    state: Tuple[Dict[str, jnp.ndarray], Dict[str, jnp.ndarray], jax.Array],
    batch: Tuple[jnp.ndarray, jnp.ndarray]
    ) -&gt; Tuple[Tuple[Dict[str, jnp.ndarray], Dict[str, jnp.ndarray], jax.Array], jnp.ndarray]:
    """
    Performs a single SGD update with momentum and updates the key.
    
    Carry State: (params, opt_state, key)
    Batch: (x, y)
    """
    params, opt_state, key = state
    x, y = batch

    # Split the keys
    key, subkey = jax.random.split(key)

    # Compute loss and gradient with respect to params (arg 0)
    loss, grads = jax.value_and_grad(compute_loss, argnums=0)(params, x, y, subkey)

    # Momentum SGD Optimizer Logic (Pure functional updates)
    learning_rate = 0.01
    momentum = 0.9  

    new_opt_state = {}
    new_params = {}

    for param_name in params:
      # Update momentum state: v_t = mu * v_{t-1} + g_t
      velocity = momentum * opt_state[param_name] + grads[param_name]
      new_opt_state[param_name] = velocity

      # Update weights: w_t = w_{t-1} - lr * v_t
      new_params[param_name] = params[param_name] - learning_rate * velocity

    new_state = (new_params, new_opt_state, key)
    return new_state, loss


#Initialize inputs, state, and run demo updates
rng = jax.random.key(42)
rng, subkey = jax.random.split(rng)

# Model state initialization
params = {
    'w': jax.random.normal(subkey, (3, 1)),
    'b': jnp.zeros((1,))
}

# Optimizer state
opt_state = {
    'w': jnp.zeros_like(params['w']),
    'b': jnp.zeros_like(params['b'])
}

# Bundle state into carry tuple
state = (params, opt_state, rng)

# Mock Batch Data
x_batch = jnp.ones((8, 3))
y_batch = jnp.ones((8, 1)) * 2.0

# Perform 3 updates
for step in range(3):
  state, loss_val = train_step(state, (x_batch, y_batch))
  print(f"Step {step + 1} | Loss: {loss_val:.4f}")
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">Step 1 | Loss: 2.2618
Step 2 | Loss: 1.9221
Step 3 | Loss: 1.3474
</code></pre>
<p>If you look closely at the signature and return value of the train step, you'll notice how everything the step needs to know about the current world is packed into a single tuple.</p>
<p>Inside the step, you'll also notice a couple of functional operations happening simultaneously.</p>
<ul>
<li><p>We split the PRNG key so we can safely inject random noise into <code>compute_loss</code> using <code>subkey</code>, while saving <code>key</code> for the next iteration.</p>
</li>
<li><p><code>jax.value_and_grad</code> calculates both the scalar loss and the exact parameter gradients with respect to <code>params</code>.</p>
</li>
<li><p>Instead of modifying weights in-place like <code>param -= lr * grad</code>, we compute brand-new dictionaries for <code>new_opt_state</code> and <code>new_params</code>.</p>
</li>
</ul>
<p>Finally, we pack those updated values back into a <code>new_state</code> tuple and return it alongside the loss scalar.</p>
<p>When we run the outer for loop, we simply overwrite our local <code>state</code> variable with the updated tuple returned by <code>train_step</code>. Because <code>train_step</code> is decorated with <code>@jax.jit</code>, JAX compiles this entire pipeline into a single, highly optimized XLA executable that runs directly on your accelerator with zero side effects.</p>
<h3>All in all</h3>
<ol>
<li><p>In JAX, pure functions require state to be explicitly passed in as an argument and explicitly returned as a transformed output (the Carry Pattern).</p>
</li>
<li><p>Replace Python <code>for</code> loops in sequential or recurrent workflows with <code>lax.scan</code> to compile long sequences into optimized single-trace instructions.</p>
</li>
<li><p>Use JAX-native control flow primitives whenever branching logic or termination conditions depend on dynamic runtime arrays.</p>
</li>
<li><p>Combining explicit carry patterns with transformations like <code>jax.jit</code> and <code>jax.grad</code> allows you to write ultra-fast, auto-differentiated training logic without state leakage or compiler errors.</p>
</li>
</ol>
<p>See you guys in the next one.</p>
]]></content:encoded></item><item><title><![CDATA[Managing PRNG Keys in JAX]]></title><description><![CDATA[In the previous articles, we have explored the fundamental principles of JAX from functional purity to transformations to how it handles arrays to PyTrees. However, in real-world ML, when working with]]></description><link>https://marshud.dev/managing-prng-keys-in-jax</link><guid isPermaLink="true">https://marshud.dev/managing-prng-keys-in-jax</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Thu, 06 Aug 2026 10:00:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/b65b874a-27b5-4371-9447-58087a393aad.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous articles, we have explored the fundamental principles of JAX from functional purity to transformations to how it handles arrays to PyTrees. However, in real-world ML, when working with JAX especially from initializing model weights, applying dropout, shuffling datasets, among others, you'll notice a JAX unique design pattern using <strong>Pseudo-Random Number Generation (PRNG)</strong></p>
<p>Those with a background in PyTorch or NumPy, JAX approach to randomness will feel surprisingly strict as there's no global seed or random state. In this article, we shall explore JAX's PRNG system from why global random states break functional purity to understanding key splitting patterns in development. Then later, I'll try to illustrate how to manage them production pipelines and stateful models.</p>
<h3>Why JAX rejects global random states</h3>
<p>In standard numpy or PyTorch, generating random numbers relies on a global stateful random number generator.</p>
<p>Consider the NumPy code below</p>
<pre><code class="language-python">import numpy as np

np.random.seed(42)
print(np.random.normal()) # Generates a random number and mutates the global state
print(np.random.normal()) # Generates a different number because a different global state
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">0.4967141530112327
-0.13826430117118466
</code></pre>
<p>Every time you call <code>np.random.normal()</code>, it returns a pseudo-random value and rotates the internal global state counter so the next call produces a different figure. But as established in the first study JAM, JAX functions must remain pure i.e. given the same inputs, the same outputs are expected without any side effects on the output. A hidden mutating global state completely violates the principle of functional purity.</p>
<p>Furthermore, global state makes parallel execution nondeterministic. If two parallel threads request a random number from a single global state, the execution order determines which thread gets which number.</p>
<p>To guarantee reproducibility, parallel safety, and functional purity, JAX makes random states explicit using PRNG Keys.</p>
<h2>Abstractions with <code>jax.random.key</code> and <code>jax.random.split</code></h2>
<p>In JAX, randomness is functional. A random function takes a mathematical key array as an explicit argument, deterministically transforms that key, and returns the requested random numbers.</p>
<p>If you pass the exact same key to a random function twice, you will get the same output twice, as demonstrated in the example below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

#create an initial key
key = jax.random.key(42)

#generating randoms with the same key yields the same output
side_a = jax.random.normal(key, (3,))
side_b = jax.random.normal(key, (3,))

print("Draw 1:", side_a)
print("Draw 2:", side_b)
print("Are they identical?", jnp.array_equal(side_a, side_b))
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Draw 1: [-0.02830462  0.46713185  0.29570296]
Draw 2: [-0.02830462  0.46713185  0.29570296]
Are they identical? True
</code></pre>
<h2>Splitting Keys</h2>
<p>To generate new, independent random numbers, you must fold or split your existing key into new unique keys using <code>jax.random.split</code>.</p>
<pre><code class="language-python">import jax

#initialize the root key
root_key = jax.random.key(420)

# split into a usable subkey and hold out the root_key
root_key, subkey = jax.random.split(root_key)

#use the subkey for generating any randoms
new_random = jax.random.normal(subkey, (3,))
print(f"Draw with subkey: {new_random}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Draw with subkey: [-1.4182527   0.12200668  0.8509691 ]
</code></pre>
<p>As a rule of thumb</p>
<blockquote>
<p>Never reuse a key. Once a key generates random numbers or splits into subkeys, discard it and use a fresh subkey for subsequent operations.</p>
</blockquote>
<h2>Managing Keys in Development</h2>
<p>When prototyping algorithms or training scripts locally, managing key splitting manually is simple once you establish disciplined coding habits.</p>
<h3>Splitting and consuming keys in loops</h3>
<p>In sequential scripts or training loops, keep a single key variable in scope. At each step, split the current key into a fresh <code>step_key</code> to consume and update key with the remaining stream.</p>
<pre><code class="language-python">import jax

root_key = jax.random.key(420)

# training steps with random operations
for step in range(3):
    root_key, step_key = jax.random.split(root_key)

    # pass the key into your model or data transformation
    random_noise = jax.random.normal(step, (2,))
    print(f"Step {step + 1} noise sample: {random_noise}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Step 1 noise sample: [-1.4182527   0.12200668]
Step 2 noise sample: [-0.06860777  0.04123694]
Step 3 noise sample: [-1.8361075  1.5372876]
</code></pre>
<h3>Multi-Subkey Splitting</h3>
<p>If a single function or initialization step requires multiple random operations, you can split a key into ‭<em>N‬</em> subkeys simultaneously. Consider the example below</p>
<pre><code class="language-python">import jax

root_key = jax.random.key(256)

#Generate distinct subkeys at once
subkeys = jax.random.split(root_key, num=4)

print("Subkeys array shape:", subkeys.shape)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Subkeys array shape: (4,)
</code></pre>
<h2>Vectorizing and Compiling Randomness</h2>
<p>Because PRNG keys are standard <code>jax.Array</code> objects under the hood, they fit directly into JAX's core transformations i.e. <code>jit</code> and <code>vmap</code></p>
<h3>Vectorizing Random Sampling with <code>jax.vmap</code></h3>
<p>Suppose you want to generate independent random trajectories or samples across a batch. Instead of running a Python loop, you can split a key into a batch of keys and use <code>jax.vmap</code></p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

root_key = jax.random.key(254)

# Get 5 keys
batch_keys = jax.random.split(root_key, num=5)

# Vectorize jax.random.normal across axis 0 of batch_keys
vmapped_normal = jax.vmap(lambda k: jax.random.normal(k, shape=(3, )))

# Get 5 distinct samples of shape (3,) in parallel
batch_samples = vmapped_normal(batch_keys)
print("Batch random samples shape:", batch_samples.shape)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Batch random samples shape: (5, 3)
</code></pre>
<h3>Randomness inside <code>jax.jit</code></h3>
<p>Passing keys into jax.jit compiled functions works transparently. Since keys are pure arrays, compiling functions that accept keys introduces zero side effects.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

@jax.jit
def apply_dropout(x, key, dropout_rate=0.2):
    keep_prob = 1.0 - dropout_rate

    # Generate binary mask
    mask = jax.random.bernoulli(key, p=keep_prob, shape=x.shape)

    #Scale remaining values to preserve expected value
    return jnp.where(mask, x / keep_prob, 0)

key = jax.random.key(99)
data = jnp.ones((4, 4))

key, subkey = jax.random.split(key)
dropped_data = apply_dropout(data, subkey)
print("Dropped Data:\n", dropped_data)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Dropped Data:
 [[1.25 1.25 1.25 0.  ]
 [1.25 1.25 1.25 1.25]
 [1.25 1.25 1.25 0.  ]
 [1.25 0.   0.   1.25]]
</code></pre>
<h2>PRNG management when things get serious</h2>
<p>While manually splitting with <code>key, subkey = jax.random.split(key)</code> works well for simple scripts, doing this manually across involved neural network architectures becomes unwieldy and error-prone.</p>
<p>Consider the following scenarios from the codebases below</p>
<h3>Functional key passing</h3>
<p>In pure JAX systems, training steps accept a top-level key as an argument alongside model parameters and batch data. The step function handles the splitting internally and passes dedicated keys down to sub-routines as exhibited in the code sample below.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def model_forward(params, x, key, is_training=True):
    if not is_training:
        return jnp.dot(x, params['w'])

    w_key, dropout_key = jax.random.split(key)
    out = jnp.dot(x, params['w'])

    # Pass dropout_key to internal dropout logic
    mask = jax.random.bernoulli(dropout_key, p=0.8, shape=out.shape)
    
    return jnp.where(mask, out / 0.8, 0.0)

@jax.jit
def train_step(params, opt_state, x_batch, y_batch, key):
    # Split key for step operations
    step_key, dropout_key = jax.random.split(key)

    # Compute predictions using explicit key
    preds = model_forward(params, x_batch, dropout_key, is_training=True)
    loss = jnp.mean((preds - y_batch) ** 2)

    # ... Gradients and updates ...
    return params, opt_state, loss
 
</code></pre>
<h3>Stateful PRNG Generators in Class Abstractions</h3>
<p>Higher level libraries like Equinox or Flax encapsulate key managementwhen building layer stacks. For example Equinox provides a clean state management pattern but if you're feeling creative, a custom PRNG generator (as exhibited in the previous article) is also an option.</p>
<p>Consider the code sample below</p>
<pre><code class="language-python">import jax

class PRNGSequence: 
    """ for sequential key splitting """
    def __init__(self, seed_or_key):
        if isinstance(seed_or_key, int):
            self._key = jax.random.key(seed_or_key)
        else: 
            self._key = seed_or_key

    def __call__(self):
        """ returns a fresh subkey while maintaining the internal key state """
        self._key, subkey = jax.random.split(self._key)
        return subkey

# Driver usage
rng = PRNGSequence(seed=420)

# Each call returns a new key
key_0 = rng()
key_1 = rng()
key_2 = rng()

print(f"Key 0 {key_0} - Key 1 {key_1} - Key 2 {key_2}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Key 0 Array((), dtype=key&lt;fry&gt;) overlaying:
[1499546378 2761259651] - Key 1 Array((), dtype=key&lt;fry&gt;) overlaying:
[3856841681 3513101443] - Key 2 Array((), dtype=key&lt;fry&gt;) overlaying:
[2301777456 4258758523]
</code></pre>
<p>By decoupling the key splitting code from model forward passes, your architecture code remains legible while guaranteeing deterministic execution.</p>
<h2>All in all</h2>
<ol>
<li><p>JAX has no global seed. All random operations require an explicit PRNG Key array (<code>jax.Array</code>).</p>
</li>
<li><p>Calling a random sampler with the same key produces identical numbers. Always use <code>jax.random.split</code> to derive new subkeys.</p>
</li>
<li><p>PRNG keys are standard array payloads; they work seamlessly inside <code>jax.jit</code>, <code>jax.vmap</code>, and <code>jax.grad</code>.</p>
</li>
<li><p>Use the <code>key, subkey = jax.random.split(key)</code> pattern in loops to keep key streams organized.</p>
</li>
<li><p>Pass root keys explicitly into top-level <code>@jax.jit</code> step functions, or wrap key generation in a disciplined abstraction like a <code>PRNGSequence</code> driver or framework modules (Equinox/Flax).</p>
</li>
</ol>
<p>See you all in the next one.</p>
]]></content:encoded></item><item><title><![CDATA[Program Transformations and PyTrees in JAX]]></title><description><![CDATA[Welcome back everyone, in our previous article, we covered the foundational philosophy of JAX from functional purity to array immutability to basic transformations like jax.grad, jax.vmap and jax.jit.]]></description><link>https://marshud.dev/program-transformations-and-pytrees-in-jax</link><guid isPermaLink="true">https://marshud.dev/program-transformations-and-pytrees-in-jax</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Tue, 04 Aug 2026 18:23:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/757cbca7-54c2-4863-81b8-da075637a454.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome back everyone, in our previous article, we covered the foundational philosophy of JAX from functional purity to array immutability to basic transformations like <code>jax.grad</code>, <code>jax.vmap</code> and <code>jax.jit</code>. We firsthand saw how treating functions as pure mathematical transformations allows JAX to inspect, trace and optimize our code for hardware like TPUs and GPUs.</p>
<p>However, in the real world, ML architectures are rarely as simple as single arrays passed into scalar functions. Deep neural networks consist of nested layers, distinct weights and biases, optimizer states and hyperparameter dictionaries.</p>
<p>In this article, we shall move beyond flat arrays and explore how JAX uses Pytrees to represent nested data structures without abandoning functional purity or breaking functional rules. By the end of this article, we shall be able to perform functional parameter updates, stack transformations and leveraging object oriented abstractions.</p>
<h3>What are Pytrees and Why do they matter</h3>
<p>In JAX, a Pytree is a container-like structure built from standard Python collections that contains leaf elements at its tips</p>
<p>A PyTree typically consists of two main components i.e.</p>
<ol>
<li><p>Nodes(Branches): These are the structural containers e.g. dicts, lists, tuples, namedtuples or custom registered classes</p>
</li>
<li><p>Leaves: This is the actual data payload located at the innermost points of the tree structure. In JAX, these are typically <code>jax.Array</code> objects, NumPy arrays or scalar values.</p>
</li>
</ol>
<p>An example of a PyTree</p>
<pre><code class="language-python"># Params of a 2-layer network
params = {
    'layer_1': {
        'w': jnp.ones((4, 8)),
        'b': jnp.zeros((8,))
    },
    'layer_2': {
        'w': jnp.ones((8, 1)),
        'b': jnp.zeros((1,))
    },
}
</code></pre>
<h3>Why PyTrees Matter</h3>
<p>In object-oriented frameworks like Pytorch and Tensorflow, model parameters are stored inside stateful class attributes like <code>nn.Module</code> . Gradients are calculated by mutating these internal tensors in place using standard attribute lookup.</p>
<p>But because JAX functions must remain pure, parameters must be seperated from execution logic. PyTrees allow us to bundle arbitrarily complex model parameters into a single structured Python object that can be directly passed into transformed functions.</p>
<p>When you pass a PyTree into functions like jax.grad or jax.jit, JAX automatically traverses the tree structure, performs the required transformations on each individual leaf array and returns a new PyTree with the same container structure as the input.</p>
<h3>Basic PyTree Operations</h3>
<p>Working with nested structures using standard Python <code>for</code> loops can easily turn verbose and inefficient. JAX provides the <code>jax.tree</code> module and <code>jax.tree_util</code> to interact with PyTree functionality.</p>
<h3>Inspecting PyTree Structures and Leaves</h3>
<p>Before manipulating a PyTree, it's important to inspect its composition. This can be done by either extracting all leaves into a flat list using <code>jax.tree.leaves</code> or inspecting its container skeleton using <code>jax.tree.structure</code>.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

# A simple param PyTree 
params = {
    'w1': jnp.array([1.0, 2.0], [3.0, 4.0]),
    'b1': jnp.array([0.5, -0.5]),
    'extra': ('learning_rate', 0.01) #Any primitive types are valid tree elements
}

# Extracting Leaves
leaves = jax.tree.leaves(params)
print("Leaves in params: ")
for leaf in leaves:
    print("-", leaf)

# Inspecting the structure
structure = jax.tree.structure(params)
print("\nTree Structure: ")
print(structure)
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">Leaves in params: 
- [ 0.5 -0.5]
- learning_rate
- 0.01
- [[1. 2.]
 [3. 4.]]

Tree Structure: 
PyTreeDef({'b1': *, 'extra': (*, *), 'w1': *})
</code></pre>
<h3>Mapping functions across trees with <a href="http://jax.tree.map"><code>jax.tree.map</code></a></h3>
<p>The workhorse of PyTree manipulation is jax.tree.map. It applies a function to every leaf array inside one or more PyTrees, keeping the container architecture untouched.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

params = {
    'w': jnp.array([1.0, 2.0, 3.0]),
    'b': jnp.array([0.5])
}

# Scale all params by a constant factor
scaled_params = jax.tree.map(lambda x: x * 2.0, params)

print("Original params: ", params)
print("Scaled params: ", scaled_params)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Original params:  {'w': Array([1., 2., 3.], dtype=float32), 'b': Array([0.5], dtype=float32)}
Scaled params:  {'b': Array([1.], dtype=float32), 'w': Array([2., 4., 6.], dtype=float32)}
</code></pre>
<p><code>jax.tree.map</code> can also accept multiple PyTrees simultaneously, provided they share the same tree-structural definition. This allows for element-wise operations between matching leaves across distinct trees.</p>
<h2>Building a Multi-Layer Neural Network Parameter Tree</h2>
<p>Now, let's try to build a multi-layer neural network from scratch using pure functions and PyTrees to illustrate how JAX handles multi-layer architectures.</p>
<p>We shall define parameter initialization logic and forward pass for a Multi-Layer Perceptron(MLP)</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def init_mlp_params(layer_sizes, key):
    params = []
    keys = jax.random.split(key, len(layer_sizes) - 1)

    for in_dim, out_dim, k in zip(layer_sizes[:-1], layer_sizes[1:], keys):
        w_key, b_key = jax.random.split(k)
        scale = jnp.sqrt(2.0 / (in_dim + out_dim))

        weight = jax.random.normal(w_key, (in_dim, out_dim)) * scale
        bias = jnp.zeros((out_dim,))

        params.append({'weights': weight, 'biases': bias})
        
    return params

def mlp_predict(params, x):
    activations = x
    num_layers = len(params)

    for i, layer in enumerate(params):
        outputs = jnp.dot(activations, layer['weights']) + layer['biases']

    # Apply ReLU for hidden layers and linear output for final layer
        activations = jax.nn.relu(outputs) if i &lt; num_layers - 1 else outputs

    return activations

root_key = jax.random.key(42)
# network params:
# 3 input features -&gt; 8 hidden units -&gt; 1 output unit
mlp_params = init_mlp_params([3, 8, 1], root_key)

# Test forward pass with dummy input
dummy_x = jnp.array([1.0, -2.0, 0.5])
prediction = mlp_predict(mlp_params, dummy_x)

print("Initialized network param PyTree")
print(f"Number of layers: {len(mlp_params)}")
print(f"Layer 1 weight shape: {mlp_params[0]['weights'].shape}")
print(f"Prediction for single sample: {prediction}")
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">Initialized network param PyTree
Number of layers: 2
Layer 1 weight shape: (3, 8)
Prediction for single sample: [0.4792502]
</code></pre>
<h3>Combining Transformations on PyTrees</h3>
<p>One of JAX's strengths is functional composition, where transformations can be wrapped around each other seamlessly. Suppose we want to compute per gradients accross a batch of data, evaluate loss, and execute the entire operation with compiled XLA performance. We can chain <code>jax.vmap</code>, <code>jax.grad</code> and <code>jax.jit</code> together on functions operating directly on PyTree parameters.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def compute_loss_single(params, x, y):
    """ MSE for a single sample """
    prediction = mlp_predict(params, x)
    return jnp.squeeze((prediction - y) ** 2)

grad_single_fn = jax.grad(compute_loss_single, argnums=0)

# Vectorize single-sample gradient across batch dimension
per_sample_grads_fn = jax.vmap(grad_single_fn, in_axes=(None, 0, 0))

compiled_per_sample_grads = jax.jit(per_sample_grads_fn)

# Generate batched data
root_key = jax.random.key(101)
x_batch = jax.random.normal(root_key, (5,3))
y_batch = jnp.array([1.0, 0.0, 1.0, 0.5, -1.0])

per_sample_grads = compiled_per_sample_grads(mlp_params, x_batch, y_batch)

print("Per-sample weights gradient shape for Layer 1:", per_sample_grads[0]['weights'].shape)
print("Per-sample biases gradient shape for Layer 1: ", per_sample_grads[0]['biases'].shape)
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Per-sample weights gradient shape for Layer 1: (5, 3, 8)
Per-sample biases gradient shape for Layer 1:  (5, 8)
</code></pre>
<p>By nesting jax.vmap around jax.grad and compiling the pipeline with jax.jit, JAX avoids the standard Python overhead and converts the entire execution into a single GPU/TPU accelerated compute kernel.</p>
<h3>Functional Parameter Updates with <code>jax.tree.map</code></h3>
<p>Since JAX arrays are strictly immutable, we cannot perform inplace updates like <code>params -= learning_rate * grad</code>. Instead, model optimization is expressed as a pure function that inputs current parameters and compute gradients returning an updated parameter PyTree.</p>
<p>We achieved this cleanly across arbitrarily nested parameter structures using <code>jax.tree.map</code></p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def compute_batch_loss(params, x_batch, y_batch):
    """MSE averaged over a batch """
    predictions = jax.vmap(mlp_predict, in_axes=(None, 0))(params, x_batch)
    return jnp.mean((jnp.squeeze(predictions) - y_batch) ** 2)

@jax.jit
def train_step(params, x_batch, y_batch, learning_rate=0.05):
    """Compute batch loss, calculates the batch gradients and applies SGD updates"""
    loss, grads = jax.value_and_grad(compute_batch_loss)(params, x_batch, y_batch)

    # Perform functional gradient descent update across all PyTree leaves
    updated_params = jax.tree.map(
        lambda p, g: p - learning_rate * g,
        params,
        grads
    )

    return updated_params, loss


# Our training week
root_key = jax.random.key(420)
x_train = jax.random.normal(root_key, (100, 3))
y_train = jnp.sin(x_train[:, 0]) + x_train[:, 1]

current_params = init_mlp_params([3, 16, 1], root_key)
print("Training MLP Model using Functional PyTree Updates...")
for step in range(5)
    current_params, loss_val = train_step(current_params, x_train, y_train)
    print(f"Step {step + 1} | Batch Loss: {loss_val:.4f}")
</code></pre>
<p>Output:</p>
<pre><code class="language-plaintext">Training MLP Model using Functional PyTree Updates...
Step 1 | Batch Loss: 1.0306
Step 2 | Batch Loss: 0.7979
Step 3 | Batch Loss: 0.6517
Step 4 | Batch Loss: 0.5486
Step 5 | Batch Loss: 0.4708
</code></pre>
<p>Notice how <a href="http://jax.tree.map"><code>jax.tree.map</code></a> seamlessly traverses all weight and bias arrays across layers in a single line, subtracting the scaled gradient without requiring explicit manual key lookups or conditional checks.</p>
<h2>Introduction to Equinox</h2>
<p>While managing parameters manually via Python dictionaries and lists is educational, large production neural networks with complex layers can become tedious to organize using raw nested dictionaries.</p>
<p>To handle this, the JAX library offers libraries like Equinox. Equinox introduces class-based module definitions without breaking the purity rules.</p>
<p>In Equinox, model modules are Python <code>dataclasses</code> registered as PyTrees under the hood. Array attributes are treated as PyTree leaves, while non-array attributes like metadata or configuration integers are treated as static structure nodes.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp
import equinox as eqx

# Class based module
class EquinoxMLP(eqx.Module):
    layers: list
    
    def __init__(self, key):
        keys = jax.random.split(key, 2)

        self.layers = [
            eqx.nn.Linear(in_features=3, out_features=16, key=keys[0]),
            eqx.nn.Linear(in_features=16, out_features=1, key=keys[1])
        ]

    
    def __call__(self, x):
        x = jax.nn.relu(self.layers[0](x))
        return self.layers[1](x)

# Instantiate Equinox model module
key = jax.random.key(7)
model = EquinoxMLP(key)

# The model instance is a valid PyTree
leaves = jax.tree.leaves(model)
print(f"Total parameter leaf arrays in model: {len(leaves)}")

# Test forward evaluation
sample_input = jnp.array([0.5, 1.2, -0.8])
output = model(sample_input)
print(f"Model output: {output}")
</code></pre>
<p>Output</p>
<pre><code class="language-plaintext">Total parameter leaf arrays in model: 4
Model output: [-0.37182418]
</code></pre>
<p>Because an <code>eqx.Module</code> instance is a PyTree, you can pass the model object directly as the parameter argument into <code>jax.grad</code>, <code>jax.jit</code>, and <code>jax.vmap</code> without extra wrapper dictionaries or boilerplate logic.</p>
<h3>In Summary</h3>
<p>1. A PyTree is any nested structure (dicts, lists, tuples) whose leaves are array data. They allow JAX to process complex parameters in pure functional transformations.</p>
<p>2. You can apply function operations to every leaf in a nested parameter architecture simultaneously while maintaining tree definitions.</p>
<p>3.Advanced combinations like vmap(grad(fn)) work seamlessly on PyTree structures, allowing per-sample gradient computation and fast parallel processing.</p>
<p>4. Model optimization is performed by passing current parameter trees and gradient trees into <code>jax.tree.map</code>, producing updated parameter trees without in-place mutation.</p>
<p>5. Higher-level frameworks like Equinox rely on PyTrees under the hood, enabling clean, class-based neural network code that integrates directly with native JAX transformations.</p>
<p>See you guys in the next one.</p>
]]></content:encoded></item><item><title><![CDATA[JAX Core Fundamentals: Pure Functional Arrays]]></title><description><![CDATA[Welcome everyone, in this article, I will explore the core philosophy of JAX, it's array system and the critical concept of functional purity.
By the end of this article, you will understand why JAX w]]></description><link>https://marshud.dev/jax-core-fundamentals-pure-functional-arrays</link><guid isPermaLink="true">https://marshud.dev/jax-core-fundamentals-pure-functional-arrays</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Tue, 07 Jul 2026 13:38:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/7210ae8b-3792-4255-abb1-6c152e0a5e84.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome everyone, in this article, I will explore the core philosophy of JAX, it's array system and the critical concept of functional purity.</p>
<p>By the end of this article, you will understand why JAX works the way it does and how to write code that fully leverages its powerful transformations like automatic differentiation, vectorization and compilation.</p>
<h2>Step 0: Setting Up the JAX AI stack</h2>
<p>Before writing any code, we need to install the modern JAX Stack.</p>
<pre><code class="language-shell"># Install the JAX AI stack (includes JAX, Flax, Optax, and friends)
!pip install -q jax-ai-stack
</code></pre>
<p>And in order to verify our installation, we can run the code below</p>
<pre><code class="language-python">import jax
import flax
import optax
import orbax.checkpoint as ocp

print(f"JAX version: {jax.__version__}")
print(f"Flax version: {flax.__version__}")
print(f"Optax version: {optax.__version__}")
print(f"Orbax version: {ocp.__version__}")
print(f"Available devices: {jax.devices()}")
</code></pre>
<p>In the code above, we import and check the different parts of the JAX AI Stack, and the last line of code is to determine what kind of devices we are running on.</p>
<p>Results:</p>
<pre><code class="language-bash">JAX version: 0.8.0
Flax version: 0.12.0
Optax version: 0.2.6
Orbax version: 0.11.26
Available devices: [CpuDevice(id=0)]
</code></pre>
<h2>The JAX Philosophy: Why purity matters</h2>
<p>JAX is designed as a function transformation library. Its transformations like <code>jit</code>, <code>grad</code>, <code>vmap</code>, etc analyze your code like a compiler. For this analysis to hold reliably, your functions must be pure. In the world of programming, these have been known for characteristics such as:-</p>
<ul>
<li><p>Pure functions are deterministic i.e. for the same input, the same output will always be gotten</p>
</li>
<li><p>Pure functions are known to have no side effects. i.e. they don't modify state whether local or external scope.</p>
</li>
<li><p>And just like any function, pure funcs rely exclusively on Explicit arguments.</p>
</li>
<li><p>Finally, You should be able to replace a pure function call directly with it's evaluated return value anywhere in the code without changing the program's behavior.</p>
</li>
</ul>
<p>This purity enables aggresive optimizations including XLA compilation for GPUs/TPUs and automatic parallelization.</p>
<h3>Writing Pure Functions</h3>
<p>In the section below, I'll explore the difference with code blocks</p>
<pre><code class="language-python"># Impure version
counter = 0

def impure_double(x):
    global counter
    counter += 1 # Modifies state
    return 2 * x
</code></pre>
<p>The above codeblock modifies the counter which wasn't passed as a parameter.</p>
<pre><code class="language-python"># Pure version
def pure_double(x, counter):
    new_counter = counter + 1
    return 2 * x, new_counter # return everything needed
</code></pre>
<p>So the basis being that any data to be modified needs to be passed along. Careful while using condiotnal statements inside pure functions as they should follow the above rules.</p>
<p>Armed with that knowledge base, we can build a reusable linear layer as a pure function, and this is the function for almost all JAX models</p>
<pre><code class="language-python">def linear_layer(params, x):
    """
    params being a PyTree containing weights and bias.
    """
    w = params['w'] # shape: (in_features, out_features)
    b = params['b'] # shape: (out_features, )

    return jnp.dot(x, w) + b
</code></pre>
<p>Because this function is pure, we can safely apply transformations to it.</p>
<h2>Core JAX Transformations</h2>
<h3>Automatic Differentiation with <code>jax.grad</code></h3>
<p>Ideally, most of us think that differentiation can be achieved by one of two ways:-</p>
<ol>
<li><p><strong>Numerical Differentiation</strong> where we approximate gradients using fintie differences(using a mathematical formula), it's easy but prone to floating-point round off errors.</p>
</li>
<li><p><strong>Symbolic Differentiation</strong> where we use algebraic manipulation like how we did in high school with characters. It yields the exact formulas but suffers from expression swell leading to complex loops and control flows computationally significant.</p>
</li>
</ol>
<p>Automatic Differentiation (Autodiff) is the middle ground. It breaks down your function into an internal sequence of primitive operations. As it's executed, JAX applies the calculus chain rule to the exact numerical values of these primitives. It computes exact derivatives down to machine precision without generating massive symbolic equations.</p>
<p>In PyTorch or Tensorflow, you compute a forward pass, create a compute a computational graph attached to the data tensors, and call <code>.backward()</code> to mutate a .grad attribute on the variables. JAX rejects this stateful approach. In JAX, differentiation is a pure mathematical function transformation. If you have a function <code>f(x)</code>, passing it to <code>jax.grad(f)</code> gives you <code>f'(x)</code> that computes the derivative.</p>
<p>Consider a scalar function below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

def scalar_fn(x):
    return jnp.sin(x) ** 2 + 3 * x

# whereas jax.grad is a function transformation, it's a high-order function
grad_fn = jax.grad(scalar_fn)

x_value = 2.0
print(f"Value: {scalar_fn(x_value)}") # Get f(x)
print(f"Gradient: {grad_fn(x_value)}") # Get f'(x)
</code></pre>
<p>Code output</p>
<pre><code class="language-bash">Value: 6.826821804046631
Gradient: 2.243197441101074
</code></pre>
<p>Consider a more useful use case where we use <code>jax.grad</code> to compute the gradient function.</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

# Our main key for randoms
main_key = jax.random.key(0)

def linear_layer(params, x):
    return jnp.dot(x, params['w']) + params['b']

def loss_fn(params, x, y_true):
    """Mean square error loss"""
    y_pred = linear_layer(params, x)
    return jnp.mean((jnp.squeeze(y_pred) - y_true) ** 2) # squeeze to remove singletons

# Create a gradient function wrt the 0th argument (params)
grad_fn = jax.grad(loss_fn, argnums=0)


# Split the main key and keep a copy
w_key, x_key, main_key = jax.random.split(main_key, 3)

# Some data
x = jax.random.normal(x_key, (5, 3)) # Batch 5, 3 features each
y_true = jnp.ones(5)

params = {
    'w': jax.random.normal(w_key, (3, 1)),
    'b': jnp.zeros(1)
}

# Get the gradient tranformations
gradients = grad_fn(params, x, y_true)

print("Gradient shape for weights: ", gradients['w'].shape)
print("Gradient shape for biases: ", gradients['b'].shape)
</code></pre>
<p>Code output:</p>
<pre><code class="language-plaintext">Gradient shape for weights:  (3, 1)
Gradient shape for biases:  (1,)
</code></pre>
<p>In the above example, I exihibit how JAX seperates model parameters from execution logic. Unlike traditional frameworks, the linear layer maintains no internal state and instead the weights and biases are passed as a nested dictionary which JAX treats as a PyTree. By applying <code>jax.grad</code> with the 0th arg, JAX isolates this parameter tree and traces the execution path to return a grad with a similar structure as the input dict. Finally, <code>jnp.squeeze</code> is to be taken seriously as it prevents mismatched dimensions.</p>
<h3>Vectorization with <code>jax.vmap</code></h3>
<p>In standard frameworks, when you compute gradients, the framework automatically sums or averages the gradients across the entire batch to update the model weights. But some ML techniques require the precise gradient for each individual sample in the batch before they are averaged. Pytorch requires workarounds like backward hooks while standard Python for loop forces sequential execution.</p>
<p>Because JAX views differentiation(<code>grad</code>) and vectorization(<code>vmap</code>) as pure transformations, you can nest them inside each other. As opposed to writing your loss function to handle batches, it now calculates the error for a single sample. Then transform it with jax.grad to get a single sample grad function, and finally wrap that function in a jax.vmap to instantly vectorize accross the batch.</p>
<p>Consider the sample Loss function below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp

# Obtain our main key for generating randoms
main_key = jax.random.key(0)

def loss_single(params, x, y):
    """Computes MSE loss for a single data point"""
    pred = jnp.dot(x, params['w']) + params['b']
    return jnp.squeeze((pred - y) ** 2) # Squeeze to ensure it's always scalar

# Transform the gradient function for a single sample
grad_single = jax.grad(loss_single, argnums=0)

# Vectorise the grad_single func over a batch of data
# The in_axes parameters mean:-
# Don't batch the params, but batch x and y along the zero axis
per_sample_grads_fn = jax.vmap(grad_single, in_axes=(None, 0, 0))

# Split the main key and obtain the key for generating the x_batch of sample data
x_batch_key, main_key = jax.random.split(main_key, 2)

params = {
    'w': jnp.array([1.5, -2.0, 0.5]),
    'b': jnp.array([0.0])
}
x_batch = jax.random.normal(x_batch_key, (4, 3))
y_batch = jnp.array([1.0, -1.0, 0.5, 2.0])

# Now you can calculate gradients for every single sample simultaneously
per_sample_grads = per_sample_grads_fn(params, x_batch, y_batch)


print("Weight gradients shape:", per_sample_grads['w'].shape)
</code></pre>
<p>Code output:</p>
<pre><code class="language-plaintext">Weight gradients shape: (4, 3)
</code></pre>
<p>So what's happening with the above code is that with <code>jax.vmap(jax.grad(loss_single))</code> , JAX executes a two-step transformation before it ever touches the GPU or TPU.</p>
<p><code>jax.grad</code> traces the single sample function and generates the exact reverse mode auto diff graph required to compute the derivative of one vector.</p>
<p><code>jax.vmap</code> then intercepts that single sample differentiation graph, then looks at the low-level primitive operations and upgrades them to batch-aware XLA matrix operations.</p>
<p>Instead of running a loop of grads, XLA compiles the entire batch of backward passes into a single parallelized execution block.</p>
<h3>Just-In-Time Compilation with <code>jax.jit</code></h3>
<p>In a standard Python script or NumPy code, the Python interpreter executes your script line by line. For an expression like <code>y = jnp.sin(x) ** 2 + 3 * x</code>, Python has to allocate intermediate memory to hold the results per priliminary operation and the round trips are costly.</p>
<p>However, jax.jit intercepts your Python function and passes it to the XLA compiler which looks at the entire function globally and performs operation fusion. This means all operations are registered by the hardware once.</p>
<p>Consider the Loss function below</p>
<pre><code class="language-python">import jax
import jax.numpy as jnp
import time

# Obtain our main key for generating randoms
main_key = jax.random.key(0)

def loss_fn(x):
    """With our math func from earlier"""
    return jnp.sin(x) + jnp.cos(x) ** 2 + 3 * x

# jit transformation from normal Python func
compiled_loss_fn = jax.jit(loss_fn)

# Split key and get data
x_data_key, main_key = jax.random.split(main_key, 2)
x_data = jax.random.normal(x_data_key, (5000, 5000))

# The first call for tracing and compiling
# So first-time exections may be slower
t0 = time.time()
_ = compiled_loss_fn(x_data)
print(f"First call: {(time.time() - t0)*1000:.2f} ms")

# Second call will be machine code
t1 = time.time()
res = compiled_loss_fn(x_data)
jax.block_until_ready(res) # hold exection until the fn execution is done
print(f"Second call: {(time.time() - t1)*1000:.2f} ms")
</code></pre>
<p>So <code>jax.jit</code> relies entirely on <strong>Tracing</strong>. When you invoke a function the first time, JAX swaps out your actual data with an empty placeholder called a <strong>Tracer</strong>. This knows not your data but rather the datatype and shape dimensions.</p>
<p>Code Results:</p>
<pre><code class="language-plaintext">First call: 96.79 ms
Second call: 1481.97 ms
</code></pre>
<p>Before you step on the table, let me explain. The first timing shows the time taken to compile the function while the second timing shows the time taken to execute the function with our actual data but given the size of our matrix, those are the results, could have been worse.</p>
<h2>JAX and Arrays</h2>
<p>Now that you're well acquainted with how JAX works, we can easily tackle how it handles arrays.</p>
<h3>Immutability</h3>
<p>In standard Python and NumPy, arrays are mutable and can be overridden in place. However, in JAX, arrays are absolutely immutable. Once an array is created, it can not be altered and trying will lead to an error and this is because of the JAX design as earlier on explained.</p>
<p>The first solution JAX introduced the .at property helper. Instead of changing the original array, it returns a brand new array with updated values leaving the original array intact.</p>
<pre><code class="language-python">import jax.numpy as jnp

x = jnp.array([1.0, 2.0, 3.0])

# x[0] = 7.0 -- will fail with a TypeError

# The JAX way 
y = x.at[0].set(5.0)

print(f"Original x: {x}")
print(f"New y: {y}")
</code></pre>
<p>While creating new arrays may look inefficient, the XLA compiler keeps track of them and if it finds original arrays that were never used again, it optimizes the final machine code with in-place memory mutation.</p>
<h3>Duck Typing with NumPy</h3>
<p>It should also be noted that JAX implements standard dark typing. This means that jax.numpy mirrors the official numpy API almost perfectly. This also means that a JAX array can be passed directly into many functions exepcting a standard NumPy array. It also exposes the same properties like <code>.shape</code>, <code>.dtype</code> and <code>.ndim</code></p>
<pre><code class="language-python"># Creating a NumPy like array 
x = jnp.linspace(0, 10, 5)
print(x.shape)
print(x.dytpe)
</code></pre>
<h3>The Shift</h3>
<p>The biggest shift happens in how JAX handles arrays when you wrap your code in a transformation. Remember how JIT replaces actual values with Tracers, out of JIT, a JAX array holds actual values on your GPU/TPU while inside JIT, it's stripped of all that and is merely an abstract tracer. This is also why you can't use raw Python to inspect the elements of a JIT-Compuiled function.</p>
<p>And that's mostly about it for this article.</p>
<p>Notebooks will be available in future articles but I believe basics must be typed out by hand for the fundamental concepts to stick.</p>
<p>Additionally, Wes Kambale (GDE, AI) wrote a beautiful aritlcle about JAX being the NumPy you know but faster. Take a look at his article <a href="https://kambale.dev/why-jax-the-numpy-you-know-but-faster">here</a> now that you know how everyting comes about and experience the power first hand.</p>
<p>See you in the next one</p>
]]></content:encoded></item><item><title><![CDATA[Why the JAX AI Stack is built for the Future of Foundation Models]]></title><description><![CDATA[Foundation models like large-scale transformers, multimodal systems, MoEs, etc are pushing the currently known boundaries of AI. Training and serving them demands extreme scale, hardware efficiency, r]]></description><link>https://marshud.dev/why-the-jax-ai-stack-is-built-for-the-future-of-foundation-models</link><guid isPermaLink="true">https://marshud.dev/why-the-jax-ai-stack-is-built-for-the-future-of-foundation-models</guid><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Fri, 03 Jul 2026 16:45:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/7e029674-95b0-4aa7-b63d-a52b800e0835.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Foundation models like large-scale transformers, multimodal systems, MoEs, etc are pushing the currently known boundaries of AI. Training and serving them demands extreme scale, hardware efficiency, research agility and a smooth path to production. Traditional monolithic frameworks often struggle to balance these demands as they grow and hardware architectures evolve. Google’s JAX AI Stack offers a compelling alternative: a modular, composable, compiler-first ecosystem purpose-built for these challenges, particularly when paired with Cloud TPUs.</p>
<p>At it’s core, the JAX AI Stack extends JAX, the numerical computing library with a suite of loosely coupled Google-backed libraries. Rather than a single monolithic framework, it provides best-in-class tools for each stage of the ML Lifecycle. This design has profound advantages such as:-</p>
<ul>
<li><p><strong>Iterative evolution without breakage:-</strong> Data pipelines, checkpointing, and optimization can be updated independently without destabilizing the core numerics engine.</p>
</li>
<li><p><strong>Composability:-</strong> Developers mix and match components to fit specific needs, from rapid experimentation to hero-scale training.</p>
</li>
<li><p><strong>Durability:-</strong> The core JAX library remains focused and adaptable for future hardware and alogorithmic shifts.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/394755a5-6a3a-486f-bc7e-ee4d9d1433b6.svg" alt="" style="display:block;margin:0 auto" />

<p>This philososphy makes the stack resilient in a field where architectures converge but optmimzation techniques continue to advace quite fast.</p>
<h3>Core Compents</h3>
<p>The JAX AI Stack covers tge full production pipeline with battle-tested libraries:-</p>
<ol>
<li><p><a href="https://docs.jax.dev/en/latest/">JAX</a>:- This is the foundation. It offers a NumPy-like API with powerdul transformations like jit for compilation, grad for differentiation, vmap for vectorization and shard_map for parallelism. Its compiler-first design leverages XLA for aggressive whole-prorgam optimizations and seamless caling accross TPU pods</p>
</li>
<li><p><a href="https://flax.readthedocs.io/en/latest/">Flax</a>:- This is a flexible neural network authoring. It provides an intuitive, object-oriented(NNX) API on top of JAX’s functional core, making it easy to build, debuf, modify and combine models which is crucial for techniques like LoRA and quantization.</p>
</li>
<li><p><a href="https://orbax.readthedocs.io/en/latest/">Optax</a>:- This is used for composable optimization. It delivers modular gradient transformations, losses and optimzers that chain together declaratively. This enables complex strategies with minimal code while containing scalability correctness.</p>
</li>
<li><p><a href="https://google-grain.readthedocs.io/en/latest/">Grain</a>:- This is for deterministic, scalabale data pipelines that are checkpointable and reproducible which is essential for experiments at scale.</p>
</li>
</ol>
<p>Additional infrastracture like the <a href="https://openxla.org/xla">XLA</a> compiler and <a href="https://docs.cloud.google.com/ai-hypercomputer/docs/workloads/pathways-on-cloud/pathways-intro">Pathways</a> for orchestrating computation accross alot of multiple chips underpins extreme scale.</p>
<h3>Performance at the Edge</h3>
<p>As models mature, peak performance increasingly relies on megakernels which are highly optimized low-level implementations that maximize hardware utilization through overlapping compute, memory and communication. The JAX ecosystem addresses this head-on with a continuum of abstraction levels:-</p>
<ul>
<li><p>High-level XLA optimizations for automated gains.</p>
</li>
<li><p>Pallas for writing custom kernels directly in Python.</p>
</li>
<li><p>Tokamax for state-of-the-art kernels</p>
</li>
<li><p>Qwix for non-intrusive quantization to boost speed and efficiency</p>
</li>
<li><p>XProf for deep, hardware-aware profiling</p>
</li>
</ul>
<p>This range lets teams with high productivity drill down to expert-level control without leaving the ecosystem, as foundation models demand every last ounce of efficiency on accelerations like TPUs</p>
<h3>From Research to Production</h3>
<p>The stack isn’t just for training; it supports the entire journey:-</p>
<ul>
<li><p><a href="https://maxtext.readthedocs.io/en/latest/">MaxText / MaxDiffusion</a>:- Scalable reference implementations for LLMs and diffusion models, demostrating production-grade patterns.</p>
</li>
<li><p><a href="https://tunix.readthedocs.io/en/latest/index.html">Tunix</a>:- Advanced post-training and alignement</p>
</li>
<li><p><strong>Inference</strong>:- Tight integration with cLLM on TPUs and a dedicated JAX serving runtime for high-throughput, low-latency deployment</p>
</li>
</ul>
<p>Real-world scale is already proven, with massive distributed training runs on tens of thousands of TPUs powered by this stack.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68874bca8a40b70ffef830eb/e9f34b13-2d1b-41dd-b039-3b94726649dc.svg" alt="" style="display:block;margin:0 auto" />

<h3>Why It's Built for the Future</h3>
<p>The JAX AI stack excels for foundation models because it aligns with their core requirements.</p>
<ol>
<li><p><strong>Scale works</strong>:- It has native support for massive parallelism and orchestration across multiple TPU clusters.</p>
</li>
<li><p><strong>Designed for modern hardware</strong>:- it has a deep integration with TPUs via XLA and Pathways, delivering specialized performance while remaining open-source and portable.</p>
</li>
<li><p><strong>Accelerates research and is reliable at scale</strong>:- It's designed based on functional programming, composability, and modular libraries that accelerate iteration while providing tested, scalable primitives.</p>
</li>
<li><p><strong>It's here to stay</strong>:- it's absraction continuum and loose coupling prepare it for megakernel trends, new model architectures and evolving hardware without having to write the entire codebase</p>
</li>
<li><p><strong>Built in the Open</strong>:- Everything is Open-source, encouranging contributions and transparency.</p>
</li>
</ol>
<p>In the current era where foundation models drive breakthroughs but also demand unprecedented resources, the JAX AI stack strikes an elegant balance between high and low-level power. Whether you're training the next Gemini-scale model or deloying inference, it provides a robust, forward-looking platform on Google's Cloud TPUs.</p>
<p>For developers and organizations ready to tackle the next wave of AI, exploring the <a href="https://jaxstack.ai/">JAX AI Stack</a> is a startegic move towards building systems that are not only powerful today but adoptable for whatever comes next.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying on Fridays?]]></title><description><![CDATA[If you’re reading this on a Friday afternoon and you’re staring at a PR titled “Just a quick fix”, close the tab, open your terminal, and type:
git reset --hard origin/master
rm -rf node_modules # The JS guys need this for good luck

Then go touch gr...]]></description><link>https://marshud.dev/deploying-on-fridays</link><guid isPermaLink="true">https://marshud.dev/deploying-on-fridays</guid><category><![CDATA[Programming humor]]></category><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Fri, 26 Dec 2025 10:33:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Fa9b57hffnM/upload/a624eb05ca1e14b5f018a6fe2a5acd47.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you’re reading this on a Friday afternoon and you’re staring at a PR titled “Just a quick fix”, close the tab, open your terminal, and type:</p>
<pre><code class="lang-bash">git reset --hard origin/master
rm -rf node_modules <span class="hljs-comment"># The JS guys need this for good luck</span>
</code></pre>
<p>Then go touch grass. Because what you’re about to do is the software industry’s version of yelling “Hold my beer” right before natural selection happens.</p>
<h3 id="heading-the-sacred-law">The Sacred Law</h3>
<p>There’s an unwritten law in software engineering</p>
<blockquote>
<p>Thou shall not deploy on Friday after 2p.m unless thou hatest the entire team on-call</p>
</blockquote>
<p>This law is older than Docker, older than microservers, even older than companies that have their root branch as “main”. It was written in stone right after “Do not rewrite the entire backend in Rust the week before Black Friday.”</p>
<h3 id="heading-the-five-stages-of-a-friday-deployment">The Five Stages of a Friday Deployment</h3>
<ol>
<li><p><strong>Optimism (2:13p.m)</strong></p>
<p> “It’s just a one-line change. Literally one line. The tests passed. I can even merge this in my sleep.”</p>
</li>
<li><p><strong>Hubris (2:27 pm)</strong></p>
<p> “This is small, let me make the change ASAP. The evening meeting will swing in my favour. My portrait will hang in the office kitchen, right next to the water dispenser.” or so you tell yourself while patting yourself on the back.</p>
</li>
<li><p><strong>The Incident (3:02 p.m)</strong></p>
<p> Production is now returning 502s. The database is speaking in tongues. The monitoring dashboard looks like a Christmas tree that swallowed a rave. Slack is on fire. The engineering manager is typing “what did you do” with increasing aggression.</p>
</li>
<li><p><strong>Bargaining(4.45 p.m)</strong></p>
<p> You’re about to start the usual “But it’s working fine on my locallhost”. You’re promising the world you’ll now read the “Terms and Conditions” before clicking ‘Agree’ if the logs would give you a clue. You consider sacrificing an intern to the DevOps gods.</p>
</li>
<li><p><strong>Weekend through the roof (6:00 p.m - Sunday 11:59 pm)</strong></p>
<p> You are now legally married to PagerDuty. Your significant other has left you for someone who has weekends. Your cat misses you. Your plants are dead. You haven’t seen sunlight since the release of that first AI-generated video of Will Smith eating spaghetti.</p>
</li>
</ol>
<h3 id="heading-quotes-from-developers-who-deployed-on-friday">Quotes from Developers who deployed on Friday</h3>
<ul>
<li><p>“It was literally a typo in the config” - Famous last words, June 2022</p>
</li>
<li><p>“The CI passed, why wouldn’t production be fine?” - Man who now sleeps in office, since mid 2025</p>
</li>
</ul>
<h3 id="heading-rational-excuses-to-avoid-friday-deploys">Rational excuses to avoid Friday deploys</h3>
<ul>
<li><p>Sudden religious conversion (you now observe “No-Deploy Jumu’ah”)</p>
</li>
<li><p>Your cat is having an existential crisis and needs emotional support</p>
</li>
<li><p>Mercury is in retrograde and Jenkins is moody lately</p>
</li>
</ul>
<h3 id="heading-my-thoughts">My thoughts</h3>
<ol>
<li><p>If it can wait until Monday, it must wait until Monday.</p>
</li>
<li><p>“it’s just a one-liner” is how every war crime in software history started</p>
</li>
<li><p>Schedule the deploy for Monday 9:05a.m. Then immediately book a “dentist appointment” for 9:00-11:00a.m. You’re welcome.</p>
</li>
</ol>
<p>Deploying on Friday isn’t brave. It isn’t dedication to the craft. It’s just performance art titled “How to convert weekend plans into PTSD”. And if you absolutely must deploy on Friday, atleast update your LinkedIn to “Open to Work” first.</p>
<p>Thank you very much for your attention to this matter. Now go forth and sin no more…</p>
]]></content:encoded></item><item><title><![CDATA[Welcome. Let’s all settle in, shall we?]]></title><description><![CDATA[Hey, Marshud here👋🏼.
I have been a backend guy for some time now, dealing with APIs, queues, databases, and the usual grind that keeps systems running. Now I’m on a deliberate mission to level up my entire stack starting with foundation technologie...]]></description><link>https://marshud.dev/welcome-lets-all-settle-in-shall-we</link><guid isPermaLink="true">https://marshud.dev/welcome-lets-all-settle-in-shall-we</guid><category><![CDATA[introduction]]></category><dc:creator><![CDATA[Marshud]]></dc:creator><pubDate>Sun, 21 Dec 2025 09:34:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/233aa947f0fc2b26b5090fcc7114c249.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey, Marshud here👋🏼.</p>
<p>I have been a backend guy for some time now, dealing with APIs, queues, databases, and the usual grind that keeps systems running. Now I’m on a deliberate mission to level up my entire stack starting with foundation technologies that many people skip, then build all the way up to production-grade machine learning.</p>
<p>This blog is my live journal of that journey.</p>
<p>No polished courses, no fake expert vibes, just me figuring it out step by step, sharing some notes, dumb mistakes, and the “Hmm, that actually worked” moments when they happen.</p>
<h2 id="heading-what-youll-see-here">What You’ll See Here</h2>
<p>I am tackling this in a very intentional order:</p>
<ol>
<li><h3 id="heading-cloud-networking">Cloud Networking:</h3>
</li>
</ol>
<p>I realised most cloud problems like latency, costs, outages, security etc, trace back to networking. So I’m going hard on VPCs, peering, load balancers, firewalls, private service access, hybrid connectivity, and all the invisible plumbing that makes or breaks production cloud environments. Expect lots of “why is this taking so long?” Posts, terraform configs that finally fixed my egress bills, and stories from when I accidentally open-sourced my entire subnet (no, it won’t happen😂).</p>
<ol start="2">
<li><h3 id="heading-cloud-computing">Cloud Computing:</h3>
</li>
</ol>
<p>Once networking feels rock-solid, I’ll move into the broader cloud picture. I’ll take on the real horror stories of IAM, observability, cost optimisation, serverless vs GKE vs VMs, disaster recovery, and how to actually design systems that don’t explode at 3 a.m. This is where I’ll start connecting the dots between backend habits and cloud-native thinking.</p>
<ol start="3">
<li><h3 id="heading-aiml-and-mlops">AI/ML and MLOps:</h3>
</li>
</ol>
<p>After I’m confident in the infrastructure, I will go all-in on machine learning and MLOps. I want to build production-ready pipelines on Vertex AI, Kubeflow, feature stores, model monitoring, and everything that makes AI actually shippable and not just Jupyter notebooks. By the time I get there, I’ll have the networking and cloud foundation to understand why things break in production, not just how to train a model.</p>
<h2 id="heading-what-to-expect-along-the-way">What to expect along the way</h2>
<ul>
<li><p><strong>Spontaneous updates:</strong> I’ll post when I learn something, not when the calendar says so.</p>
</li>
<li><p><strong>First-person battle stories:</strong> If I run into something and I find it interesting after solving it, I am definitely sharing that one.</p>
</li>
<li><p><strong>Code, configs, and commands:</strong> Copy-paste ready snippets with explanations of what I tried and failed before.</p>
</li>
<li><p><strong>Zero fluff:</strong> I have no intention of selling courses, and I’m not pretending to have all the answers. I am just documenting my past so you can skip the same pain.</p>
</li>
</ul>
<h2 id="heading-why-im-sharing-this-publicly">Why I’m sharing this publicly</h2>
<ul>
<li><p>Writing forces me to understand the material.</p>
</li>
<li><p>If my mistakes and breakthroughs can save even one person the same late-night Google rabbit holes, it’s worth it</p>
</li>
<li><p>I want to look back in a year and see how far I’ve come</p>
</li>
</ul>
<h2 id="heading-how-to-follow-the-journey">How to follow the Journey</h2>
<ul>
<li><p>New posts will drop whenever something worth sharing happens (no fixed schedule).</p>
</li>
<li><p>I’ll cross-post or share summaries on LinkedIn, X, <a target="_blank" href="http://dev.to">dev.to</a>, or Reddit</p>
</li>
<li><p>If you want to ride along, subscribe here, on X or LinkedIn so you can know as soon as I drop something</p>
</li>
</ul>
<p>Now that this is known, let me get cooking. See you in the next one</p>
]]></content:encoded></item></channel></rss>