← All chapters

08 · Content revision 5

From tensor coordinates to one flat buffer

Map language-model matrices and attention tensors onto one flat Rust vector with checked row-major strides and deterministic offsets.

Predict one address before flattening the picture

A tensor gives an nn-dimensional meaning to a collection of scalar values. Start with this shape:

[2, 2, 3]

It has three axes, so its rank is 3. The extents say that axis 0 has two slices, each slice has two rows along axis 1, and each row has three positions along axis 2. Here are the logical values:

slice 0: [[10, 11, 12], [20, 21, 22]]
slice 1: [[30, 31, 32], [40, 41, 42]]

This tensor implementation does not store nested row objects. It owns these values in one flat Vec<f64>:

[10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42]

Before reading further, make two predictions:

  1. At which zero-based flat position does coordinate [1,0,2] land, and which value is stored there?
  2. Can coordinate [1,2,0] select a value? If not, which axis fails first?

In row-major order, the last coordinate changes fastest. Moving one step on axis 2 advances by one flat element. A complete row contains three values, so moving one step on axis 1 advances by three. A complete slice contains two rows of three, so moving one step on axis 0 advances by six. Those distances are the element strides:

[6, 3, 1]

Now expand the first prediction:

i0s0=16=6,i1s1=03=0,i2s2=21=2,offset=6+0+2=8,data[8]=32.\begin{aligned} i_0s_0 &= 1\cdot 6 = 6, \\ i_1s_1 &= 0\cdot 3 = 0, \\ i_2s_2 &= 2\cdot 1 = 2, \\ \operatorname{offset} &= 6+0+2 = 8, \\ \operatorname{data}[8] &= 32. \end{aligned}

The answer contains two different results. Offset 8 is an integer position in the flat buffer. Value 32 is the f64 stored at that position. Calling both of them “the index” hides the operation that connects coordinates to storage.

The second prediction fails before any buffer access. Axis 1 has extent 2, so its only valid indices are 0 and 1. Coordinate [1,2,0] supplies index 2 at that axis. The error reports axis 1, index 2, and size 2 rather than allowing the arithmetic to wander into another logical row.

Turn each axis movement into one stride term

For a rank-dd tensor, the coordinate-to-offset rule is:

offset(i0,,id1)=k=0d1iksk\operatorname{offset}(i_0,\ldots,i_{d-1})=\sum_{k=0}^{d-1} i_k s_k

The coordinate contains one zero-based index iki_k for every axis kk. Its element stride sks_k tells how many positions the flat offset moves when that index increases by one and all other indices stay fixed. Row-major strides are derived from right to left. The last stride is one; each earlier stride is the checked product of the extents to its right.

For shape [2,2,3], the suffix to the right of axis 00 has 23=62\cdot3=6 elements, the suffix to the right of axis 11 has 33, and nothing follows axis 22, giving [6,3,1]. The stride unit is f64 elements, not bytes.

The formula assumes a valid coordinate. The implementation first requires exactly dd indices, then visits axes from first to last, checking 0ik<shape[k]0 \le i_k < \operatorname{shape}[k] before adding that axis’s term. For [1,0,2], the terms are 6, 0, and 2, so the offset is 8.

Two edge shapes make the product convention precise. Shape [] has rank zero and an empty stride list, but it represents one scalar value: the empty coordinate maps to offset 0. Shape [2,0,3] has strides [0,3,1] and zero values. The zero extent makes the tensor empty, yet the constructor still derives and checks the complete suffix-stride chain.

Locate every symbol in the tensor

SymbolOperational meaning
offset\operatorname{offset}The zero-based position in the flat Vec<f64> selected by one valid coordinate; it is not the stored f64 value.
ddThe tensor rank. It equals shape.len() and the required coordinate length.
iki_kThe coordinate index on axis kk. It must be smaller than shape[k].
kkThe zero-based axis number, from 00 through d1d-1. Bounds errors report this number.
sks_kThe row-major element stride on axis kk: the checked product of all shape extents to its right.

Shape entries and strides answer different questions. shape[k] is how many positions exist on axis kk; sks_k is how far one step on that axis moves through the flat buffer. In the fixture, axis 0 has extent 2 but stride 6, and axis 1 has extent 2 but stride 3.

The formula has no terms when d=0d=0, so its sum is zero. That gives the scalar’s empty coordinate offset 0; the constructor’s separate shape invariant gives the scalar one stored value. For an axis with extent zero, there is no valid iki_k, so an empty tensor has no valid full coordinate even though its strides are well defined.

From bigram counts to learned matrices and attention tensors

The Chapter 6 bigram gives each current-token and next-token pair its own count and uses only one token of context, so it cannot share evidence through learned word similarity.

The two primary checkpoints are Bengio et al., A Neural Probabilistic Language Model and Vaswani et al., Attention Is All You Need.

Bengio et al. describe n-gram models as short-context conditional-probability tables that do not use word similarity, then define a neural language model with a vocabulary-size-by-feature-width matrix CC of learned word features and neural parameter matrices for next-word prediction. Vaswani et al. later pack simultaneous queries, keys, and values into matrices QQ, KK, and VV and use learned projections to run multiple attention heads in parallel before concatenating their outputs.

Bengio’s model still predicts from a fixed window of preceding words. For the toy dimensions in this chapter, CC has shape [V,m]=[5,3][|V|,m]=[5,3] and stores three learned features for each of five vocabulary items. A two-word context gives the hidden mapping HH shape [h,2m]=[4,6][h,2m]=[4,6], and UU shape [V,h]=[5,4][|V|,h]=[5,4] maps four hidden activations toward five next-word scores. These are small shape stand-ins, not the paper’s experiment sizes or a complete list of its parameters.

For the Transformer checkpoint, each one-head QQ, KK, and VV stand-in has shape [tokens,head width]=[2,3][\text{tokens},\text{head width}]=[2,3]. The local QQ stack has shape [heads,tokens,head width]=[2,2,3][\text{heads},\text{tokens},\text{head width}]=[2,2,3], making a separate head axis visible. The paper supports the matrix computation and parallel-head claims; it does not require that particular axis order.

Explicit tensor shapes let this course represent embeddings, learned weights, activations, and attention intermediates in the cumulative decoder; the single contiguous row-major buffer is a local implementation policy, not a requirement of either paper.

The runnable Rust contrast uses the same Tensor abstraction for all seven model-shaped stand-ins and prints their shapes, strides, and element counts. The zero values are inert placeholders: this chapter exposes a storage consequence of the model mathematics without implementing either architecture’s operations.

Make shape validity and indexing one checked responsibility

Tensor::from_vec accepts an owned Vec<usize> shape and an owned Vec<f64> data buffer. It derives row-major strides from right to left with checked multiplication, then requires the data length to equal the checked element count. The empty shape starts from the multiplicative identity, so it needs exactly one value. Any zero extent produces a zero element count.

The implementation does not inspect, clamp, or normalize data values. A tensor may store finite numbers, either infinity, NaNs, signed zero, or any other f64 bit pattern. The invariant concerns storage length and coordinate meaning, not the numerical interpretation that later operations will give the values.

Derive checked row-major strides and require an exact flat data length rust/crates/llm-from-scratch/src/tensor/storage.rs#tensor-storage-invariants
pub(crate) fn checked_row_major_layout(
    shape: &[usize],
) -> Result<(Vec<usize>, usize), TensorError> {
    if shape.is_empty() {
        return Ok((Vec::new(), 1));
    }

    let mut strides = vec![1; shape.len()];
    for axis in (0..shape.len() - 1).rev() {
        strides[axis] = shape[axis + 1]
            .checked_mul(strides[axis + 1])
            .ok_or(TensorError::ShapeOverflow)?;
    }

    let element_count = shape[0]
        .checked_mul(strides[0])
        .ok_or(TensorError::ShapeOverflow)?;
    Ok((strides, element_count))
}

pub(crate) fn checked_offset(
    shape: &[usize],
    strides: &[usize],
    base_offset: usize,
    coordinate: &[usize],
) -> Result<usize, TensorError> {
    debug_assert_eq!(shape.len(), strides.len());

    if coordinate.len() != shape.len() {
        return Err(TensorError::RankMismatch {
            expected: shape.len(),
            actual: coordinate.len(),
        });
    }

    let mut offset = base_offset;
    for (axis, ((&index, &dimension), &stride)) in
        coordinate.iter().zip(shape).zip(strides).enumerate()
    {
        if index >= dimension {
            return Err(TensorError::IndexOutOfBounds {
                axis,
                index,
                dimension,
            });
        }
        let contribution = index
            .checked_mul(stride)
            .ok_or(TensorError::ShapeOverflow)?;
        offset = offset
            .checked_add(contribution)
            .ok_or(TensorError::ShapeOverflow)?;
    }
    Ok(offset)
}

impl Tensor {
    /// Builds a tensor after checking its row-major layout and buffer length.
    pub fn from_vec(shape: Vec<usize>, data: Vec<f64>) -> Result<Self, TensorError> {
        let (strides, expected) = checked_row_major_layout(&shape)?;
        let actual = data.len();
        if actual != expected {
            return Err(TensorError::DataLengthMismatch { expected, actual });
        }

        Ok(Self {
            data,
            shape,
            strides,
        })
    }

    /// Returns the number of logical axes.
    pub fn rank(&self) -> usize {
        self.shape.len()
    }

    /// Returns the extent of every axis.
    pub fn shape(&self) -> &[usize] {
        &self.shape
    }

    /// Returns the row-major suffix-product stride for every axis.
    pub fn strides(&self) -> &[usize] {
        &self.strides
    }

    /// Returns the number of stored values.
    pub fn len(&self) -> usize {
        self.data.len()
    }

    /// Reports whether the flat buffer stores no values.
    pub fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    /// Borrows the contiguous value buffer.
    pub fn as_slice(&self) -> &[f64] {
        &self.data
    }

    /// Mutably borrows the contiguous value buffer without changing its length.
    pub fn as_mut_slice(&mut self) -> &mut [f64] {
        &mut self.data
    }

    /// Consumes the tensor and returns its contiguous value buffer.
    pub fn into_vec(self) -> Vec<f64> {
        self.data
    }

The second region owns coordinate validation. offset first compares coordinate length with rank. It then visits axes from left to right and reports the first index that is not smaller than its extent. get and get_mut both call this same operation, so reading and mutation cannot drift into separate indexing rules.

Check rank and bounds before reusing one coordinate-to-offset rule rust/crates/llm-from-scratch/src/tensor/storage.rs#row-major-indexing
    /// Maps one in-bounds coordinate to its row-major flat-buffer offset.
    pub fn offset(&self, coordinate: &[usize]) -> Result<usize, TensorError> {
        checked_offset(&self.shape, &self.strides, 0, coordinate)
    }

    /// Borrows the value at one checked coordinate.
    pub fn get(&self, coordinate: &[usize]) -> Result<&f64, TensorError> {
        let offset = self.offset(coordinate)?;
        Ok(&self.data[offset])
    }

    /// Mutably borrows the value at one checked coordinate.
    pub fn get_mut(&mut self, coordinate: &[usize]) -> Result<&mut f64, TensorError> {
        let offset = self.offset(coordinate)?;
        Ok(&mut self.data[offset])
    }

The public accessors expose the invariants without permitting shape and stride metadata to diverge. rank, shape, strides, len, is_empty, and as_slice inspect the tensor. as_mut_slice can change values but cannot resize the buffer; into_vec consumes the tensor and returns its owned values.

TensorError separates four failures. ShapeOverflow means a required usize product could not be represented. DataLengthMismatch { expected, actual } means the product was valid but the supplied buffer had the wrong length. RankMismatch { expected, actual } is checked before axes, and IndexOutOfBounds { axis, index, dimension } names the first invalid axis. The methods return these errors through Result; ordinary invalid input does not panic.

The demo’s first library region carries the model-history progression through one concrete type. It constructs tiny CC, HH, and UU parameter stand-ins, one-head QQ, KK, and VV activation stand-ins, and a local stacked-head QQ tensor. Every value is zero because only shape, strides, and element count matter here; later chapters own the actual model operations.

Represent Bengio parameter matrices and Transformer attention shapes with one Tensor type rust/demos/ch08-tensor-storage/src/lib.rs#llm-shape-history
/// Builds tiny shape-only stand-ins for parameters and activations from the LLM history.
pub fn llm_shape_history_fixture() -> Result<Vec<(&'static str, Tensor)>, TensorError> {
    Ok(vec![
        ("toy Bengio C", Tensor::from_vec(vec![5, 3], vec![0.0; 15])?),
        ("toy Bengio H", Tensor::from_vec(vec![4, 6], vec![0.0; 24])?),
        ("toy Bengio U", Tensor::from_vec(vec![5, 4], vec![0.0; 20])?),
        (
            "toy Transformer Q (one head)",
            Tensor::from_vec(vec![2, 3], vec![0.0; 6])?,
        ),
        (
            "toy Transformer K (one head)",
            Tensor::from_vec(vec![2, 3], vec![0.0; 6])?,
        ),
        (
            "toy Transformer V (one head)",
            Tensor::from_vec(vec![2, 3], vec![0.0; 6])?,
        ),
        (
            "toy Transformer Q head stack",
            Tensor::from_vec(vec![2, 2, 3], vec![0.0; 12])?,
        ),
    ])
}

The second library region owns the chapter’s shared fixture. The learner program and diagram trace both construct the same shape and value sequence through this function, so the code and visual explanation use one tensor definition.

Construct the one [2,2,3] tensor used throughout Chapter 8 rust/demos/ch08-tensor-storage/src/lib.rs#frozen-tensor-fixture
/// Reconstructs the immutable, contiguous tensor used throughout the chapter.
pub fn frozen_tensor_fixture() -> Result<Tensor, TensorError> {
    Tensor::from_vec(FROZEN_SHAPE.to_vec(), FROZEN_VALUES.to_vec())
}

The following main.rs excerpt first constructs the model-history shape fixture. It then computes the checked lookup [1,0,2], mutates [0,1,1] to 99, constructs a scalar and an empty tensor, and captures rank, bounds, and overflow errors. The code immediately after the displayed region prints those values. Its final output line makes the next boundary explicit: Chapter 9 will add a new shape, stride, and base-offset interpretation while retaining the same storage.

Build the model-history shapes, then compute valid access, edge shapes, and deterministic errors rust/demos/ch08-tensor-storage/src/main.rs#learner-output
    let llm_shapes = llm_shape_history_fixture()?;

    let mut tensor = frozen_tensor_fixture()?;
    let selected_offset = tensor.offset(&SELECTED_COORDINATE)?;
    let selected_value = *tensor.get(&SELECTED_COORDINATE)?;
    *tensor.get_mut(&[0, 1, 1])? = 99.0;

    let scalar = Tensor::from_vec(vec![], vec![7.0])?;
    let scalar_offset = scalar.offset(&[])?;
    let scalar_value = scalar.get(&[])?;
    let empty = Tensor::from_vec(vec![2, 0, 3], vec![])?;

    let rank_error = tensor.offset(&[1, 0]).unwrap_err();
    let bounds_error = tensor.offset(&INVALID_COORDINATE).unwrap_err();
    let overflow_error = Tensor::from_vec(vec![usize::MAX, 2], vec![]).unwrap_err();

The separate diagram producer asks the frozen Tensor for its shape, strides, length, checked offset, value, and out-of-bounds error. Its serialization code then records the two logical slices and each axis contribution. Together, those records expose one checked coordinate, its stride terms and selected value, and one coordinate rejected before access.

Record the frozen tensor, three stride terms, checked lookup, and rejected coordinate rust/demos/ch08-tensor-storage/src/diagram_trace.rs#tensor-storage-trace
    let tensor = frozen_tensor_fixture()?;
    let offset = tensor.offset(&SELECTED_COORDINATE)?;
    let value = tensor.get(&SELECTED_COORDINATE)?;
    let bounds_error = match tensor.offset(&INVALID_COORDINATE) {
        Err(error) => error,
        Ok(_) => {
            return Err(TensorError::IndexOutOfBounds {
                axis: 1,
                index: 2,
                dimension: 2,
            });
        }
    };
    let shape = usize_csv(tensor.shape());
    let strides = usize_csv(tensor.strides());
    let buffer = value_csv(tensor.as_slice());
    let coordinate = usize_csv(&SELECTED_COORDINATE);
    let slice0 = format!(
        "SLICE axis0=0 row0={} row1={}",
        row_csv(&tensor, 0, 0)?,
        row_csv(&tensor, 0, 1)?
    );
    let slice1 = format!(
        "SLICE axis0=1 row0={} row1={}",
        row_csv(&tensor, 1, 0)?,
        row_csv(&tensor, 1, 1)?
    );
    let terms = SELECTED_COORDINATE
        .iter()
        .zip(tensor.strides())
        .enumerate()
        .map(|(axis, (&index, &stride))| {
            format!(
                "TERM axis={axis} index={index} stride={stride} contribution={}",
                index * stride
            )
        })
        .collect::<Vec<_>>();

The recorded trace preserves the exact decimal spelling and ordering emitted by the Rust example. Read alongside the Tensor output, it shows that the slices, offset, selected value, and bounds error all describe the same stored values.

Follow the coordinate through slices, arithmetic, and storage

Read the figure in four passes:

  1. Locate slice 1, row 0, and the third position in that row.
  2. Match coordinate [1,0,2] with strides [6,3,1] and inspect all three recorded contributions.
  3. Find offset 8 in the flat buffer, then distinguish its position from value 32.0.
  4. Compare valid axis-1 index 0 with rejected index 2 in coordinate [1,2,0].

One coordinate, one row-major offset

Two slices with two rows and three columns, plus one flat buffer, come from the same Rust fixture. Follow [1, 0, 2] through its three stride contributions, then compare the checked out-of-bounds coordinate.

Shape
[2, 2, 3]
Row-major strides
[6, 3, 1]
Stored values
12

Two slices from one rank-3 tensor

The first coordinate chooses a slice; the next two choose a row and column inside it.

  1. Slice i0=0i_0=0
    Row i2=0i_2=0i2=1i_2=1i2=2i_2=2
    i1=0i_1=0 10.0 11.0 12.0
    i1=1i_1=1 20.0 21.0 22.0
  2. Slice i0=1i_0=1
    Row i2=0i_2=0i2=1i_2=1i2=2i_2=2
    i1=0i_1=0 30.0 31.0 Selected coordinate and buffer element: 32.0
    i1=1i_1=1 40.0 41.0 42.0

Turn [1, 0, 2] into offset 8

Each term is printed by the Rust trace; their sum names a position in the flat buffer, not the stored value.

Coordinate: [1, 0, 2]

  1. Contribution: i0s0=16=6i_{0}s_{0}=1\cdot6=6
  2. Contribution: i1s1=03=0i_{1}s_{1}=0\cdot3=0
  3. Contribution: i2s2=21=2i_{2}s_{2}=2\cdot1=2
Offset
8
Value
32.0

Find offset 8 in the flat buffer

The double border and diamond mark offset 8 without relying on color.

  1. Offset 0 10.0
  2. Offset 1 11.0
  3. Offset 2 12.0
  4. Offset 3 20.0
  5. Offset 4 21.0
  6. Offset 5 22.0
  7. Offset 6 30.0
  8. Offset 7 31.0
  9. Offset 8 Selected coordinate and buffer element: 32.0
  10. Offset 9 40.0
  11. Offset 10 41.0
  12. Offset 11 42.0

Reject an invalid coordinate before access

Axis 1 has size 2, so index 2 is rejected before any buffer access.

Coordinate
[1, 2, 0]
Axis
1
Index
2
Axis size
2

The two slice tables and flat buffer contain the same twelve values from the Rust trace. They are different views in the explanatory sense only: Chapter 8 has not implemented a TensorView API. The stride calculation shows why advancing the outermost axis skips six elements, while advancing the last axis skips one.

The diamond and double border mark offset 8 without relying on color. Match that marker first to coordinate [1,0,2], then to value 32.0 in the flat buffer.

The bounds panel is part of the same lesson, not an afterthought. It shows axis 1, index 2, and size 2; the tensor rejects that coordinate before producing an offset or reading a value. A plausible arithmetic result for invalid indices would not make the access valid.

Predict each layout result before checking it

Work from right to left when deriving strides, but check coordinate rank and axes from left to right when validating access.

  1. Derive the row-major element strides for shape [3,4,5].
  2. For shape [2,2,3], strides [6,3,1], and the frozen buffer, compute every contribution for coordinate [1,0,2]. State its offset and value separately.
  3. Predict the error for coordinate [1,2,0]. Which axis, index, and size must the error report?
  4. For scalar shape [], state rank, strides, element count, and the only valid coordinate and offset.
  5. For shape [2,0,3], derive the strides and element count. Why can no full coordinate be valid?
  6. Compare two failed constructors: shape [2,2] with three data values, and shape [usize::MAX,2] with an empty data vector. Which error belongs to each?
  7. Predict which flat offset changes when get_mut(&[0, 1, 1]) assigns 99, and write the resulting flat buffer.
Check the seven stride, access, and invariant results
  1. Start with stride 11 on the last axis. The axis-11 stride is 51=55\cdot1=5; the axis-00 stride is 45=204\cdot5=20. The row-major strides are therefore [20,5,1].
  2. The contributions are 16=61\cdot6=6, 03=00\cdot3=0, and 21=22\cdot1=2. Their sum is flat offset 8. The value at data[8] is 32.0. Offset 8 is a usize position; value 32.0 is the stored f64.
  3. Rank is correct, so validation visits axes. Axis 0 accepts index 1. Axis 1 has size 2 and rejects index 2, yielding IndexOutOfBounds { axis: 1, index: 2, dimension: 2 }. Axis 2 and the buffer are never consulted.
  4. Shape [] has rank 0, strides [], and one element. Coordinate [] has the exact required rank and its empty sum maps to offset 0. Any nonempty coordinate produces a rank mismatch.
  5. Right-to-left derivation gives [0,3,1]: the last stride is 1, the next is 3, and the zero extent makes the earlier stride and total count 0. No full coordinate is valid because axis 1 has no index smaller than extent 0.
  6. Shape [2,2] has a representable element count of 4, so three values produce DataLengthMismatch { expected: 4, actual: 3 }. For [usize::MAX,2], a required checked product overflows first, so the result is ShapeOverflow, not a data-length mismatch and not an allocation attempt.
  7. Coordinate [0,1,1] contributes 06+13+11=40\cdot6 + 1\cdot3 + 1\cdot1 = 4. Only data[4] changes. The buffer becomes [10.0, 11.0, 12.0, 20.0, 99.0, 22.0, 30.0, 31.0, 32.0, 40.0, 41.0, 42.0]; shape and strides remain [2,2,3] and [6,3,1].

Misconception check: strides are not shape extents. An extent counts valid positions on an axis; a stride measures the flat-buffer movement caused by one step on that axis. Row-major is also a convention, not an intrinsic law of tensors. A column-major tensor or an indirect ragged structure can represent multidimensional data with a different coordinate-to-storage rule.

Reuse one checked numeric container throughout the model

The course now has a Tensor that owns one contiguous Vec<f64>. Shape gives its nn-dimensional interpretation, row-major strides map valid coordinates to deterministic offsets, and checked access prevents rank or bounds mistakes from becoming unrelated buffer reads. Later chapters can build embeddings, activations, weights, attention intermediates, logits, losses, and gradients on this same storage invariant.

Chapter 9 will reinterpret the same storage through views and axis transforms. That work may introduce a base offset and non-default stride interpretations, but it must not silently copy the buffer or change its values. Arithmetic, broadcasting, matrix multiplication, and differentiation remain separate later chapters; Chapter 8 contributes only the stable storage and checked indexing they will rely on.