the xla path · 0/14
start the path

the xla path · chapter 11 of 14 · part ii, the runtime

The array moves up

PJRT hands a caller a pile of per-device buffers. IFRT hands back one array.

the goal Given a sharded array, describe it correctly at both the PJRT level and the IFRT level, and name what IFRT can do that PJRT cannot.

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

One object, not many buffers

PJRT's unit of data is the per-device buffer: a PjRtBuffer holds one shard's worth of memory on one device, and a single logical array spread across eight devices is eight separate PjRtBuffer objects, with the framework above keeping track of how they fit together. IFRT (xla/python/ifrt/) exists to erase that bookkeeping. ifrt::Array (xla/python/ifrt/array.h:65) is one object: it carries a dtype(), a shape(), and a sharding() of its own, and an eight-device array is one ifrt::Array, not eight of anything.

The array moved up the stack; the buffers stayed behind.

ifrt::Client is what builds these objects. MakeArrayFromHostBuffer turns a plain host buffer into one; AssembleArrayFromSingleDeviceArrays does the adjacent job in reverse, taking per-device shards a caller already has and gluing them into a single logical array. Where PJRT asks the framework to track eight buffers and a sharding by hand, IFRT asks for one array that already knows its own sharding.

where the array lives at each level: one object above the line, one buffer per device below it
jax.Array one object, global shape ifrt::Array dtype() · shape() · sharding()one object, still PJRT buffers one per device, N of them ifrt: pjrt in-processwraps PJRT objects ifrt: proxy client / serverthe runtime lives elsewhere implementations chapter 13 stands on the dashed box
§ 02

What the header actually holds

Four methods carry the whole idea: dtype(), shape(), sharding(), and layout(), each a pure virtual with no default implementation. An Array cannot be copied or moved either, on purpose: every reference to one is a tsl::RCReference<Array>, aliased as ArrayRef, a ref-counted handle rather than a value that could get duplicated and quietly drift out of sync with its own shards.

The rest of the interface is about taking that one object apart or putting it back together without touching shard data unnecessarily. DisassembleIntoSingleDeviceArrays is the exact inverse of AssembleArrayFromSingleDeviceArrays; FullyReplicatedShard is a shortcut for the common case where every shard is identical, one shard read instead of all of them. An ArrayCopySemantics enum, kAlwaysCopy, kReuseInput, kDonateInput, governs every one of these calls the same way donation governs a PJRT buffer: some operations promise a fresh copy, others let the caller trade ownership of the input away for speed.

verbatim, trimmed, from xla/python/ifrt/array.h
class Array : public llvm::RTTIExtends<Array, Value> {
 public:
  Array() = default;

  // Not copyable or movable.
  Array(const Array&) = delete;
  Array(Array&&) = delete;
  Array& operator=(const Array&) = delete;
  Array& operator=(Array&&) = delete;

  virtual DType dtype() const = 0;
  virtual const Shape& shape() const = 0;
  virtual const Sharding& sharding() const = 0;
§ 03

Two implementations, one interface

IFRT is an interface, not a runtime, and more than one thing implements it. The PJRT-backed implementation runs in-process: it wraps PjRtClient and PjRtBuffer objects directly, and it is what a single-process JAX program uses without ever knowing IFRT is there at all. The proxy implementation, under xla/python/ifrt_proxy/, splits client from server: the process holding the Python ifrt::Client talks over a wire to a server process that owns the actual runtime, wherever that runtime happens to live.

Chapter 13 depends entirely on that split existing. Swapping what sits behind the proxy's server, from an ordinary PJRT-backed runtime to something like Pathways, changes nothing about the JAX code running above the client. The abstraction is not decoration; it is the exact seam a single-controller runtime needs in order to be a drop-in replacement underneath code nobody had to touch.

§ 04

Why JAX needed this

A jax.Array sits on an ifrt::Array underneath, and that is the concrete reason a single jax.Array can span multiple host processes in JAX's multi-process world, the training run the JAX path meets from above (at /jax/training-run). The sharding lives on the array object itself, not stitched together by the framework from a pile of per-device buffers it has to track by hand.

Client exposes operations that only make sense once an array is a first-class object: RemapArrays shuffles shards between arrays, ReshardArrays changes an array's sharding, BitcastArrays reinterprets its bytes, all as metadata operations rather than data movement. None of this replaces PJRT. Every IFRT array still bottoms out in real per-device memory, and the PJRT-backed implementation is proof that IFRT is a layer built on top, not a rewrite underneath. What moved is where the bookkeeping lives: up, onto an object that already knows what it is, instead of a set of buffers a caller has to remember to keep in sync.

assigned

Readings