Skip to content

← Flax NNX step 85 of 100

Hard Primitives

NNX Tensor-Parallel Linear

Jump to the task

Why this matters

A linear layer computes x @ kernel + bias. Each output column depends on the input and one column of the kernel, independently of the other output columns. Column-parallel computation uses this fact to divide a layer's output features into groups.

This exercise simulates those groups with a loop over array slices. It creates the full kernel and returns the full output. It does not allocate a device mesh, communicate between devices, or demonstrate reduced memory use or faster execution.

Split output columns

For input x of shape (N, in_dim), an NNX Linear kernel has shape (in_dim, features) and its bias has shape (features,). Let chunk = features // tp_size, assuming exact division.

Group i uses:

  • kernel[:, i * chunk : (i + 1) * chunk], shape (in_dim, chunk).
  • bias[i * chunk : (i + 1) * chunk], shape (chunk,).

Apply the full x to each group: x @ kernel_slice + bias_slice. Each result has shape (N, chunk). Concatenate results in group order along the last axis to recover (N, features).

The sum over input features still occurs inside each matrix multiplication. Do not sum the groups' outputs: they represent different output columns, not partial sums of the same columns.

A complete example

The following two groups reproduce one affine layer, including a nonzero bias:

import jax.numpy as jnp
import numpy as np

x = jnp.array([[1., 2.]])
kernel = jnp.array([[1., 0., 2., -1.],
                    [0., 1., 1.,  3.]])
bias = jnp.array([0.5, -0.5, 1., 2.])
pieces = [x @ kernel[:, start:start + 2] + bias[start:start + 2]
          for start in (0, 2)]
output = jnp.concatenate(pieces, axis=-1)
np.testing.assert_allclose(output, [[1.5, 1.5, 5., 7.]])
np.testing.assert_allclose(output, x @ kernel + bias)

The equality follows from matrix multiplication over separate output columns. In floating-point implementations, different compiled kernels may give small rounding differences, so compare with a suitable tolerance rather than requiring bitwise identity.

How this relates to real tensor parallelism

On a model-parallel mesh, column layouts are commonly expressed as P(None, "model") for the kernel and P("model") for the bias. In the simple scheme shown here, each model-axis device needs the full input-feature vector and computes different output columns. Inputs can still be split along the batch axis on a separate mesh axis.

The global output can remain sharded. Materializing every output column on every device would require communication such as an all-gather; a distributed array's global shape does not imply each device holds that entire array. This exercise's local concatenation does not simulate that communication.

Row-parallel computation instead splits the kernel's input rows and the matching input features. Those products contribute partial sums to the same outputs, so they must be combined by addition. A replicated full output can use an all-reduce; other output layouts can use other collective patterns. Add the bias once to the combined result, rather than once per partial sum.

Read the initialized NNX parameters

Construct one nnx.Linear(in_dim, features, rngs=nnx.Rngs(seed)) with its default kernel initialization and enabled zero-initialized bias. Read its arrays with model.kernel[...] and model.bias[...]. These parameter wrappers let NNX track model state; the array slices can be passed directly to JAX operations.

Do not create a separately initialized Linear for each group. That would generally draw different random weights from the slices of the one full layer required here. There is no training step.

Problem

Implement tensor_parallel_linear(seed, x, features, tp_size):

  1. Cast seed, features and tp_size to Python integers. Assume positive sizes, x shaped (N, in_dim), and features divisible by tp_size.
  2. Build the full Linear with input width x.shape[-1] and the requested output width. Read its kernel and bias arrays.
  3. Loop over tp_size contiguous output-column groups, calculating x @ kernel_slice + bias_slice for each one.
  4. Concatenate the results along axis=-1 in the original column order, then return output.reshape(-1).

You may use normal array slicing or jax.lax.dynamic_slice_in_dim. Slice the kernel on axis 1 and the bias on axis 0. Each output group retains every batch row; concatenating along the batch axis is wrong. The returned array has length N * features, in row-major order.

With tp_size=1, there is one group containing the whole kernel. With tp_size=features, each group has one output column. Both should agree numerically with applying the same full Linear. The group count need not match the number of devices available to run this simulation.

The diagram illustrates column sharding on a separate two-axis mesh. Its unused data axis replicates each column shard. It describes a possible layout, not the placement of arrays created by this function.

Loading visualization…