← jax4jChapter 0 · Getting started8 min read

Shapes, dtypes and tensor lifetimes

Arrange six values, reduce along an axis, explore a floating-point surprise and make tensor ownership explicit.

Java notebookJava kernel · starts on first runWebGPU · not requested

Run cells from top to bottom. Typed declarations and imports carry into later cells. Shift + Enter runs a cell. Reloading or leaving this page discards edits and Java state.

Ready to start

Six numbers can tell different stories

Take [1, 2, 3, 4, 5, 6]. Are these six consecutive measurements? Two readings from each of three sensors? Three readings from each of two sensors? The values alone cannot tell you. A shape assigns those values an arrangement, and your application assigns meaning to its axes.

The same six values arranged as two rows of three and three rows of two, alongside the different ordering produced by transpose.

This notebook keeps the numbers small so every rearrangement is visible. We will also meet a surprisingly large number to expose FP32 rounding, then return to small tensors to examine ownership. If you have just completed your first tensor, the operations will feel familiar. This lesson starts its own session and imports: you do not need to keep that earlier page open.

Choose Run all for the complete worked example. Afterwards, change the cells and explore. The intentional shape failures are caught and printed, so they do not stop the notebook. When you make an uncaught error of your own, fix the cell and run it again. Restart clears Java state; reloading also discards edited code.

1. Shape, rank and size

The rank is the number of axes. The size is the total element count, obtained by multiplying the dimensions. Shape [2, 3] therefore has rank two and size six. Axes are numbered from zero: axis zero has length two, and axis one has length three.

In [ ]:

1. Describe a matrix

Expect shape [2, 3], rank 2, element count 6 and dtype F32. The flat input fills rows in order: first [1, 2, 3], then [4, 5, 6]. A shape says nothing about “sensors” or “time” by itself. Write down which axis means what before a larger model starts passing tensors around.

Try this: change the factory dimensions to 1, 2, 3. The rank becomes three while the size stays six. Now try just 6: rank one, still six elements. Return to 2, 3 for the next diagram. Here is an especially useful distinction: shape [1] is a one-element vector; an empty shape [] is a rank-zero scalar. Both contain one value. Rank is not another name for element count.

2. Reshape preserves the sequence

reshape changes dimensions while keeping the flattened element order. One requested dimension may be -1; jax4j infers it from the total size. Six values reshaped to (-1, 2) become three rows of two. Nothing has been sorted or transposed.

In [ ]:

2. Reshape or transpose?

Both results have shape [3, 2], but their values differ. Reshape retains [1, 2, 3, 4, 5, 6]; transpose gives [1, 4, 2, 5, 3, 6]. A transpose swaps rows and columns of this rank-two tensor. The diagram shows why those two operations answer different questions even when their output shapes agree.

Try reshape(1, -1) and predict both dimensions. Then try reshape(3, 2) and compare it with the inferred version. A reshape produces a distinct tensor handle. Whether a backend shares storage or copies it is an implementation detail: the CPU backend can share this immutable storage. Close the handles you create without assuming that a reshape necessarily copied every byte.

Pause before editing: would reshape(4, 2) preserve the element count? Six values cannot fill eight positions. We can make that failure part of the experiment without interrupting later cells.

In [ ]:

3. Catch a shape mismatch

Change the requested dimensions to 2, -1 and the error disappears. Change them to -1, -1 and the request is ambiguous: there may only be one inferred dimension. These are valuable errors. Shape checks locate a disagreement near the operation that introduced it, before downstream numbers become mysterious.

3. Choose the axis you want to remove

A reduction along an axis combines values while preserving the remaining axes. For our two-by-three matrix, summing along axis zero combines the two rows, leaving three column totals. Summing along axis one combines each row's three entries, leaving two row totals.

In [ ]:

4. Reduce along an axis

Column totals are [5, 7, 9]; row totals are [6, 15]. Passing true keeps the reduced dimension with length one, giving shape [2, 1]. The values remain the row totals. Keeping dimensions is useful when later operations need the axis positions to line up, especially in broadcasting.

Change the input to [10, 20, 30, 40, 50, 60] and rerun the first cell before this one. Every total should grow by ten. Next, switch the factory dimensions in this cell to 3, 2. The same flat sequence now has different neighbors, so axis-one sums become [30, 70, 110]. You have changed the question by changing the shape. Restore the original values when you want the reference outputs again.

4. F32 has a precision budget

At 16,777,216, FP32 numbers are two apart: adding one rounds back, while adding two reaches 16,777,218.

A dtype specifies an element's numerical representation. jax4j's enum names several dtypes, including F32, F64, integer types and BOOL. That list is not a promise that every factory and backend executes every type. The float[] factory used here constructs F32 tensors; the current CPU upload path and notebook GPU bridge support FP32 execution.

FP32 cannot represent every integer above 2²⁴, or 16,777,216. Near that value, representable numbers are two apart. Adding one can therefore round right back to the number you started with. Let's make that visible.

In [ ]:

5. When adding one changes nothing

The first result is still 16,777,216; the second is 16,777,218. Java may print these in scientific notation. Nothing is wrong with the addition kernel: the result must fit the chosen representation. A Java method accepting a double argument does not automatically promote an F32 tensor to F64.

Try starting at 1_000_000f, where adding one is representable. Then return to 16,777,216 and try adding three. Which representable neighbor wins? For values exactly halfway between neighbors, FP32's usual nearest-even rounding chooses the one with an even final significand bit. This is why checking exact equality after a long floating-point calculation often answers the wrong question; choose an error tolerance that suits your computation.

5. A handle also has a lifetime

A tensor is created, used to make a result, read back and closed; the input remains alive until after its result has been used.

Each tensor operation returns a handle to result storage. A Java reference to that handle and the storage it owns have related, but different, lifetimes. GPU memory can outlive the line that created a tensor if you leave cleanup to garbage collection. A try resource declaration gives you a clear release point, including when the computation throws an exception.

The resource declarations below close in reverse order: first total, then doubled, then a. Nothing needed by a later operation is closed early.

In [ ]:

6. Own each intermediate

With the original six values, expect 42.0. Add a temporary tensor that shifts doubled by one before summing. Include it in the resource list, and predict the new total: six extra ones make 48.0. This is a small exercise in tracing both the numerical dependencies and the objects you own.

On this CPU backend, release is currently a no-op and arrays are reclaimed by Java's garbage collector. Consequently, using a CPU tensor after close() is not a reliable way to demonstrate an error. Do not build portable code around that behavior: a GPU backend releases real device buffers. The closing pattern expresses your ownership regardless of where the calculation runs.

Long fluent chains create temporary tensors too. The browser's GPU Monte Carlo example uses the notebook-specific BrowserWebGpuBackend.scope() to reclaim anonymous intermediates. That helper belongs to the browser adapter, not to the general jax4j core API. Here, short named resources keep the same issue visible without requiring GPU support.

Put the three questions together

Before running a new tensor calculation, ask: how are the values arranged, how are they represented, and who owns each result? Shape errors, rounding surprises and forgotten buffers become much easier to reason about when those questions have separate answers.

For a final experiment, return to the six original values, reshape them to [3, 2], sum along axis zero, and divide by three. The two column means should be [3, 4]. Use short resource declarations for the reshaped tensor, sum and mean. Then change the input and predict the answer again. You now have enough pieces to turn these little matrices into experiments of your own.

Continue with elementwise operations and broadcasting for more experiments, or revisit your first tensor and give its calibration example a two-dimensional shape.


Questions or corrections? open an issue